"use client"; import React, { useState, useEffect, useRef } from "react"; import { Send, Square, Sparkles, Pill, Search, Command } from "lucide-react"; import { cn } from "@duoc-thu/ui"; interface ComposerProps { onSubmit: (query: string) => void; isLoading?: boolean; onStop?: () => void; initialValue?: string; className?: string; } export function Composer({ onSubmit, isLoading = false, onStop, initialValue = "", className, }: ComposerProps) { const [value, setValue] = useState(initialValue); const [suggestions, setSuggestions] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); const inputRef = useRef(null); const dropdownRef = useRef(null); useEffect(() => { if (initialValue) { setValue(initialValue); } }, [initialValue]); // Fetch suggestions from API route when query length > 1. // // `term` is the LAST WORD being typed, not the whole textarea — a natural // sentence like "cho tôi thuốc parace" has no drug alias containing that // full phrase, so matching against it always returns empty (found live // 2026-08-10: this silently killed every suggestion once a user typed a // sentence instead of a bare drug name, which is the more common case). useEffect(() => { const words = value.trim().split(/\s+/); const term = words[words.length - 1] ?? ""; if (term.length < 2) { setSuggestions([]); setShowSuggestions(false); return; } // Race guard: a slower request for an earlier keystroke must not // overwrite a faster one for a later keystroke — found live 2026-08-10 // typing fast could show a stale, unrelated suggestion list because the // fetch callback set state unconditionally regardless of whether `term` // (and therefore `value`) had already changed by the time it resolved. let cancelled = false; const timer = setTimeout(async () => { try { const res = await fetch(`/api/suggest?q=${encodeURIComponent(term)}`); if (cancelled) return; if (res.ok) { const data = await res.json(); if (cancelled) return; if (data?.suggestions && Array.isArray(data.suggestions) && data.suggestions.length > 0) { setSuggestions(data.suggestions); setShowSuggestions(true); return; } } if (cancelled) return; setSuggestions([]); setShowSuggestions(false); } catch { if (!cancelled) { setSuggestions([]); setShowSuggestions(false); } } }, 200); return () => { cancelled = true; clearTimeout(timer); }; }, [value]); // Close suggestions on outside click useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && inputRef.current && !inputRef.current.contains(event.target as Node) ) { setShowSuggestions(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const handleSubmit = () => { const trimmed = value.trim(); if (!trimmed || isLoading) return; onSubmit(trimmed); setValue(""); setSuggestions([]); setShowSuggestions(false); }; const applySuggestion = (suggestion: string) => { // Keep the clinical intent already typed and replace only the unfinished // final token: "liều para" -> "liều Paracetamol", not "Paracetamol". const prefix = value.match(/^([\s\S]*\s)[^\s]*$/)?.[1] ?? ""; setValue(`${prefix}${suggestion}`); setShowSuggestions(false); setSelectedIndex(-1); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (showSuggestions && suggestions.length > 0) { if (e.key === "ArrowDown") { e.preventDefault(); setSelectedIndex((prev) => (prev < suggestions.length - 1 ? prev + 1 : 0)); return; } if (e.key === "ArrowUp") { e.preventDefault(); setSelectedIndex((prev) => (prev > 0 ? prev - 1 : suggestions.length - 1)); return; } if (e.key === "Enter" && selectedIndex >= 0) { e.preventDefault(); applySuggestion(suggestions[selectedIndex]); return; } } if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(); } }; return (
{/* Autocomplete Suggestions Dropdown */} {showSuggestions && suggestions.length > 0 && (
Gợi ý tra cứu Dược thư
{suggestions.map((item, idx) => ( ))}
)} {/* Main Composer Box */}