"use client"; import { useEffect, useRef, useState } from "react"; import { createPendingChatPromotion } from "@/lib/navigation/chat_unlock_session"; import { useActiveCharacter, useActiveCharacterRoutes, } from "@/providers/character-provider"; import { useChatDispatch } from "@/stores/chat/chat-context"; import { Logger } from "@/utils/logger"; import { useChatKeyboardAvoidance } from "../hooks/use-chat-keyboard-avoidance"; import { useChatKeyboardDiagnostics } from "../hooks/use-chat-keyboard-diagnostics"; import { ChatComposerActionMenu } from "./chat-composer-action-menu"; import { ChatInputTextField } from "./chat-input-text-field"; import { ChatSendButton } from "./chat-send-button"; import styles from "./chat-input-bar.module.css"; const log = new Logger("AppChatComponentsChatInputBar"); const POINTER_SEND_CLICK_DEDUPE_MS = 500; const CHAT_ACTION_MENU_ID = "chat-composer-action-menu"; export interface ChatInputBarProps { disabled?: boolean; } export function ChatInputBar({ disabled = false }: ChatInputBarProps) { const dispatch = useChatDispatch(); const character = useActiveCharacter(); const characterRoutes = useActiveCharacterRoutes(); const [input, setInput] = useState(""); const [isFocused, setIsFocused] = useState(false); const [isActionMenuOpen, setIsActionMenuOpen] = useState(false); const [previousDisabled, setPreviousDisabled] = useState(disabled); const barRef = useRef(null); const textareaRef = useRef(null); const lastPointerSendAtRef = useRef(0); const hasContent = input.trim().length > 0; if (disabled !== previousDisabled) { setPreviousDisabled(disabled); if (disabled && isActionMenuOpen) setIsActionMenuOpen(false); } useChatKeyboardAvoidance({ active: isFocused, containerRef: barRef, }); useChatKeyboardDiagnostics({ containerRef: barRef }); useEffect(() => { if (!isActionMenuOpen) return; const handlePointerDown = (event: PointerEvent) => { const target = event.target; if (target instanceof Node && barRef.current?.contains(target)) return; setIsActionMenuOpen(false); }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") setIsActionMenuOpen(false); }; document.addEventListener("pointerdown", handlePointerDown); document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("pointerdown", handlePointerDown); document.removeEventListener("keydown", handleKeyDown); }; }, [isActionMenuOpen]); const handleInputChange = (value: string) => { setInput(value); if (value.trim().length > 0) setIsActionMenuOpen(false); }; const handleFocusChange = (focused: boolean) => { setIsFocused(focused); if (focused) setIsActionMenuOpen(false); }; const handleSend = (source: "click" | "keyboard" | "pointerdown") => { if (source === "click") { const elapsed = Date.now() - lastPointerSendAtRef.current; if (elapsed >= 0 && elapsed < POINTER_SEND_CLICK_DEDUPE_MS) { log.debug("[chat-input-bar] suppress duplicate click", { elapsed, contentLength: input.length, hasContent, disabled, isFocused, }); return; } } if (disabled || !hasContent) return; log.debug("[chat-input-bar] handleSend", { source, contentLength: input.length, contentPreview: input.slice(0, 50), hasContent, disabled, isFocused, }); dispatch({ type: "ChatSendMessage", content: input }); setInput(""); setIsActionMenuOpen(false); textareaRef.current?.focus(); }; const handlePointerDownSend = () => { lastPointerSendAtRef.current = Date.now(); handleSend("pointerdown"); }; const handleMenuToggle = () => { if (disabled || hasContent) return; if (!isActionMenuOpen) textareaRef.current?.blur(); setIsActionMenuOpen((open) => !open); }; const handlePromotion = (promotionType: "image" | "voice") => { if (disabled) return; dispatch({ type: "ChatPromotionInjected", promotion: createPendingChatPromotion( promotionType, character.id, ), }); setIsActionMenuOpen(false); }; return (
{isActionMenuOpen ? ( handlePromotion("image")} onVoice={() => handlePromotion("voice")} onNavigate={() => setIsActionMenuOpen(false)} /> ) : null}
handleSend("keyboard")} onFocusChange={handleFocusChange} disabled={disabled} /> handleSend("click")} onMenuToggle={handleMenuToggle} onPointerDownSend={handlePointerDownSend} />
); }