"use client"; import { useSyncExternalStore } from "react"; import type { ChangeEvent, FormEvent, KeyboardEvent, ReactNode, RefObject } from "react"; import { ArrowUp, Square } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { CharacterRemaining } from "@/components/character-remaining"; import { characterRemainingVisible } from "@/lib/character-remaining"; import { composerDraftSnapshot, serverComposerDraftSnapshot, subscribeComposerDraft, } from "@/lib/composer-draft"; import { COMPOSER_QUEUE_LABEL, COMPOSER_QUEUE_RECALL_LABEL } from "@/lib/queued-draft"; export const composerRemainingId = "composer-character-remaining"; export function useComposerDraft(): string { return useSyncExternalStore( subscribeComposerDraft, composerDraftSnapshot, serverComposerDraftSnapshot, ); } type ChatComposerProps = { readonly inputRef: RefObject; /** * A surface that owns its own draft (the rectification session) passes it here; * the main chat leaves it undefined and the composer reads the draft store. */ readonly value?: string; readonly remainingId?: string; /** Extra description (a question stem shown above the field) merged with the count id. */ readonly describedBy?: string; readonly inputLabel: string; readonly placeholder: string; readonly maxLength: number; readonly inputDisabled: boolean; readonly submitLabel: string; readonly submitBlocked: boolean; readonly stopVisible: boolean; readonly stopLabel: string; readonly stopTitle: string; readonly queued?: { text: string; onRecall: () => void }; /** * Controls that belong to the composer itself (model picker, chart subject). * They render on the composer's own bottom row; there is no longer a strip * below the composer holding a permanent 44px of chrome. */ readonly toolbar?: ReactNode; readonly onSubmit: (event: FormEvent) => void; readonly onChange: (event: ChangeEvent) => void; readonly onKeyDown: (event: KeyboardEvent) => void; readonly onStop: () => void; }; export function ChatComposer({ inputRef, value, remainingId = composerRemainingId, describedBy, inputLabel, placeholder, maxLength, inputDisabled, submitLabel, submitBlocked, stopVisible, stopLabel, stopTitle, queued, toolbar, onSubmit, onChange, onKeyDown, onStop, }: ChatComposerProps) { const storeDraft = useComposerDraft(); const draft = value ?? storeDraft; const showRemaining = characterRemainingVisible(draft.length, maxLength); const describedByIds = [describedBy, showRemaining ? remainingId : undefined].filter(Boolean).join(" ") || undefined; return ( <> {queued ? (

{COMPOSER_QUEUE_LABEL}

{queued.text}

) : null}