import { useLayoutEffect, useRef, useState } from "preact/hooks"; import { Paperclip, Search, SendHorizontal } from "lucide-preact"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; type MutableValueRef = { current: string; }; type Props = { draftRef: MutableValueRef; draftRevision: number; error: string | null; isSearchMode: boolean; isSending: boolean; pendingAttachmentCount: number; attachmentButtonDisabled: boolean; onOpenAttachmentPicker: () => void; onPaste: (event: ClipboardEvent) => void; onSend: (draft: string) => void | Promise; }; const MIRROR_SENTINEL = "\u200b"; const HAS_NON_WHITESPACE = /\S/; export function ChatComposer({ draftRef, draftRevision, error, isSearchMode, isSending, pendingAttachmentCount, attachmentButtonDisabled, onOpenAttachmentPicker, onPaste, onSend, }: Props) { // The draft stays in the DOM/ref so typing never schedules a render of the workspace transcript. const textareaContainerRef = useRef(null); const mirrorRef = useRef(null); const [hasDraft, setHasDraft] = useState(() => HAS_NON_WHITESPACE.test(draftRef.current)); const hasDraftRef = useRef(hasDraft); const getTextarea = () => textareaContainerRef.current?.querySelector("textarea") ?? null; const updateMirror = (value: string) => { // The overlapping mirror lets normal layout size the textarea without synchronous scrollHeight reads. if (mirrorRef.current) mirrorRef.current.textContent = `${value}${MIRROR_SENTINEL}`; }; const updateHasDraft = (value: string) => { const nextHasDraft = HAS_NON_WHITESPACE.test(value); if (nextHasDraft !== hasDraftRef.current) { hasDraftRef.current = nextHasDraft; setHasDraft(nextHasDraft); } }; useLayoutEffect(() => { const value = draftRef.current; const textarea = getTextarea(); if (textarea && textarea.value !== value) { textarea.value = value; } updateMirror(value); updateHasDraft(value); }, [draftRevision]); const submit = () => { const textarea = getTextarea(); const draft = textarea?.value ?? draftRef.current; const canSend = HAS_NON_WHITESPACE.test(draft) || (!isSearchMode && pendingAttachmentCount > 0); if (isSending || !canSend) return; draftRef.current = ""; if (textarea) textarea.value = ""; updateMirror(""); updateHasDraft(""); void onSend(draft); }; return ( <>