"use client"; import { useState } from "react"; import { Pill, Send } from "lucide-react"; import type { ChatMessage, Citation } from "@duoc-thu/shared-types"; import { ChatBubble, CitationCard, Card, Input, Button, cn } from "@duoc-thu/ui"; import { sendChatMessage } from "@duoc-thu/api-client"; function TypingIndicator() { return (
); } export interface ChatPanelProps { onCitationClick?: (citation: Citation) => void; className?: string; } const ERROR_MESSAGE = "Hệ thống tạm thời không phản hồi. Vui lòng thử lại — nếu vẫn lỗi, có thể dịch vụ tra cứu đang tạm ngưng."; export function ChatPanel({ onCitationClick, className }: ChatPanelProps) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isSending, setIsSending] = useState(false); // One id per chat session, so follow-ups ("còn trẻ em thì sao?") resolve // against the same conversation on the backend. const [conversationId] = useState(() => typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `conv-${Date.now()}` ); async function handleSubmit(event: React.FormEvent) { event.preventDefault(); const content = input.trim(); if (!content || isSending) return; const userMessage: ChatMessage = { id: `local-${messages.length}`, role: "user", content, createdAt: new Date().toISOString(), }; setMessages((prev) => [...prev, userMessage]); setInput(""); setIsSending(true); try { const response = await sendChatMessage(content, conversationId); setMessages((prev) => [...prev, response.message]); } catch { // Never leave the user staring at their own message with no reply: an // error is surfaced as a labelled bubble, not swallowed silently. setMessages((prev) => [ ...prev, { id: `error-${messages.length}`, role: "assistant", content: ERROR_MESSAGE, createdAt: new Date().toISOString(), }, ]); } finally { setIsSending(false); } } return (
{messages.length === 0 && (
)} {messages.map((message) => (
{message.citations && message.citations.length > 0 && (
{message.citations.map((citation) => ( onCitationClick(citation) : undefined} /> ))}
)}
))} {isSending && }
setInput(event.target.value)} placeholder="Hỏi về một loại thuốc..." disabled={isSending} aria-label="Nhập câu hỏi" />
); }