"use client"; import type { ChatStatus } from "ai"; import { ArrowUp, Mic, Plus, Square, UserPlus, X } from "lucide-react"; import { type ChangeEvent, type KeyboardEvent, useCallback, useRef, useState, } from "react"; import { useTranslation } from "react-i18next"; import { ModelPicker } from "@/components/chat/model-picker"; import { PatientFormDialog } from "@/components/chat/patient-form-dialog"; import type { Effort } from "@/lib/ai-models"; import { cn } from "@/lib/utils"; type ChatInputProps = { onSubmit: (text: string) => void; status: ChatStatus; onStop?: () => void; model: string; effort: Effort; onModelChange: (model: string) => void; onEffortChange: (effort: Effort) => void; }; const iconButton = "flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"; const pillButton = "flex h-8 items-center gap-1.5 rounded-lg px-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"; const contextPill = "flex h-7 items-center gap-1.5 rounded-md px-2 text-[13px] text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"; export function ChatInput({ onSubmit, status, onStop, model, effort, onModelChange, onEffortChange, }: ChatInputProps) { const { t } = useTranslation(); const [value, setValue] = useState(""); const [files, setFiles] = useState([]); const [addOpen, setAddOpen] = useState(false); // Bumped on each open so the dialog remounts with a fresh file number + form. const [addKey, setAddKey] = useState(0); const fileInputRef = useRef(null); const isGenerating = status === "submitted" || status === "streaming"; const canSend = (value.trim().length > 0 || files.length > 0) && !isGenerating; const submit = useCallback(async () => { const trimmed = value.trim(); if ((!trimmed && files.length === 0) || isGenerating) { return; } const parts: string[] = []; if (trimmed) { parts.push(trimmed); } // Include the text of attached files so the agent can parse them (e.g. a // database export to import). Read text-like files; cap each so a huge file // can't blow the context. Binary files are referenced by name only. for (const file of files) { const textLike = /\.(csv|tsv|json|txt|md|xml|ndjson|tab)$/i.test(file.name) || file.type.startsWith("text/") || file.type === "application/json"; if (textLike) { try { const content = (await file.text()).slice(0, 200_000); parts.push(`--- File: ${file.name} ---\n${content}`); } catch { parts.push(`[Attached: ${file.name}]`); } } else { parts.push(`[Attached: ${file.name}]`); } } onSubmit(parts.join("\n\n")); setValue(""); setFiles([]); }, [value, files, isGenerating, onSubmit]); const handleKeyDown = useCallback( (event: KeyboardEvent) => { if ( event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing ) { event.preventDefault(); submit(); } }, [submit] ); const handleFilesSelected = useCallback( (event: ChangeEvent) => { const selected = event.target.files; if (selected && selected.length > 0) { setFiles((prev) => [...prev, ...Array.from(selected)]); } // Reset so picking the same file again still fires onChange. event.target.value = ""; }, [] ); const removeFile = useCallback((index: number) => { setFiles((prev) => prev.filter((_, i) => i !== index)); }, []); return ( <>
{ event.preventDefault(); submit(); }} className="w-full overflow-hidden rounded-[28px] border border-border bg-input shadow-sm" > {/* Textarea + toolbar, filling the rounded card. */}