"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; } const SAMPLE_SUGGESTIONS = [ "Liều dùng Paracetamol người lớn và trẻ em theo cân nặng", "Chống chỉ định và tác dụng không mong muốn của Amoxicillin", "Tương tác thuốc giữa Metformin và thuốc cản quang", "Thận trọng khi dùng Aspirin cho bệnh nhân loét dạ dày", "Hướng dẫn liều dùng Ibuprofen và giới hạn tối đa ngày", ]; 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; } } // Fallback filter local suggestions const filtered = SAMPLE_SUGGESTIONS.filter((s) => s.toLowerCase().includes(term.toLowerCase()) ); if (cancelled) return; setSuggestions(filtered); setShowSuggestions(filtered.length > 0); } 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 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(); setValue(suggestions[selectedIndex]); setShowSuggestions(false); setSelectedIndex(-1); 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 */}