"use client"; import type { ChatStatus } from "ai"; import { ArrowUp, Building2, CalendarRange, ChevronDown, Hand, Mic, Plus, Square, Stethoscope, UserPlus, X, } from "lucide-react"; import { type ChangeEvent, type KeyboardEvent, type ReactNode, useCallback, useMemo, useRef, useState, } from "react"; import { useTranslation } from "react-i18next"; import { PatientFormDialog } from "@/components/chat/patient-form-dialog"; import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger, } from "@/components/ui/menu"; import { cn } from "@/lib/utils"; type ChatInputProps = { onSubmit: (text: string) => void; status: ChatStatus; onStop?: () => void; }; type Option = { value: string; label: string }; type OptionKey = { value: string; labelKey: string }; const ACCESS_OPTIONS: OptionKey[] = [ { value: "standard", labelKey: "chat.input.access.standard" }, { value: "break-glass", labelKey: "chat.input.access.breakGlass" }, { value: "read-only", labelKey: "chat.input.access.readOnly" }, ]; const RESPONSE_OPTIONS: OptionKey[] = [ { value: "concise", labelKey: "chat.input.response.concise" }, { value: "detailed", labelKey: "chat.input.response.detailed" }, { value: "comprehensive", labelKey: "chat.input.response.comprehensive" }, ]; const SPECIALTY_OPTIONS: OptionKey[] = [ { value: "internal-medicine", labelKey: "chat.input.specialtyOptions.internalMedicine" }, { value: "cardiology", labelKey: "chat.input.specialtyOptions.cardiology" }, { value: "pediatrics", labelKey: "chat.input.specialtyOptions.pediatrics" }, { value: "emergency", labelKey: "chat.input.specialtyOptions.emergency" }, { value: "all", labelKey: "chat.input.specialtyOptions.all" }, ]; const FACILITY_OPTIONS: OptionKey[] = [ { value: "main-hospital", labelKey: "chat.input.facilityOptions.mainHospital" }, { value: "north-clinic", labelKey: "chat.input.facilityOptions.northClinic" }, { value: "telehealth", labelKey: "chat.input.facilityOptions.telehealth" }, ]; const TIME_OPTIONS: OptionKey[] = [ { value: "30d", labelKey: "chat.input.timeOptions.30d" }, { value: "12m", labelKey: "chat.input.timeOptions.12m" }, { value: "5y", labelKey: "chat.input.timeOptions.5y" }, { value: "all", labelKey: "chat.input.timeOptions.all" }, ]; 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-foreground/5 hover:text-foreground"; function SelectPill({ ariaLabel, triggerClassName, chevronClassName, icon, prefix, value, onValueChange, options, align = "start", }: { ariaLabel: string; triggerClassName: string; chevronClassName: string; icon: ReactNode; prefix?: string; value: string; onValueChange: (value: string) => void; options: Option[]; align?: "start" | "center" | "end"; }) { const selected = options.find((option) => option.value === value); return ( } > {icon} {prefix ? ( {prefix} ) : null} {selected?.label} {options.map((option) => ( {option.label} ))} ); } export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) { const { t } = useTranslation(); const toOptions = useCallback( (opts: OptionKey[]): Option[] => opts.map((o) => ({ value: o.value, label: t(o.labelKey) })), [t], ); const accessOptions = useMemo(() => toOptions(ACCESS_OPTIONS), [toOptions]); const responseOptions = useMemo(() => toOptions(RESPONSE_OPTIONS), [toOptions]); const specialtyOptions = useMemo( () => toOptions(SPECIALTY_OPTIONS), [toOptions], ); const facilityOptions = useMemo(() => toOptions(FACILITY_OPTIONS), [toOptions]); const timeOptions = useMemo(() => toOptions(TIME_OPTIONS), [toOptions]); const [value, setValue] = useState(""); const [files, setFiles] = useState([]); const [access, setAccess] = useState("standard"); const [responseMode, setResponseMode] = useState("detailed"); const [specialty, setSpecialty] = useState("internal-medicine"); const [facility, setFacility] = useState("main-hospital"); const [timeRange, setTimeRange] = useState("12m"); 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(() => { const trimmed = value.trim(); if ((!trimmed && files.length === 0) || isGenerating) { return; } const parts: string[] = []; if (trimmed) { parts.push(trimmed); } if (files.length > 0) { parts.push(`[Attached: ${files.map((file) => file.name).join(", ")}]`); } 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/60 bg-muted shadow-sm" > {/* Top (lighter) card: textarea + toolbar, with a slightly smaller bottom radius */}