100 lines
3.6 KiB
TypeScript
100 lines
3.6 KiB
TypeScript
"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 (
|
|
<div className="inline-flex items-center gap-1 px-4 py-3" aria-label="Đang soạn câu trả lời">
|
|
<span className="h-1.5 w-1.5 animate-bounce-dot rounded-full bg-muted-foreground/60" />
|
|
<span className="h-1.5 w-1.5 animate-bounce-dot rounded-full bg-muted-foreground/60 [animation-delay:0.15s]" />
|
|
<span className="h-1.5 w-1.5 animate-bounce-dot rounded-full bg-muted-foreground/60 [animation-delay:0.3s]" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export interface ChatPanelProps {
|
|
onCitationClick?: (citation: Citation) => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function ChatPanel({ onCitationClick, className }: ChatPanelProps) {
|
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
const [input, setInput] = useState("");
|
|
const [isSending, setIsSending] = useState(false);
|
|
|
|
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);
|
|
setMessages((prev) => [...prev, response.message]);
|
|
} finally {
|
|
setIsSending(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card className={cn("flex w-full flex-col overflow-hidden", className)}>
|
|
<div className="flex min-h-[32rem] flex-1 flex-col gap-1 overflow-y-auto p-6">
|
|
{messages.length === 0 && (
|
|
<div className="m-auto max-w-sm text-center text-muted-foreground">
|
|
<Pill className="mx-auto mb-2 h-10 w-10 text-primary" aria-hidden="true" />
|
|
<p className="mb-1.5 text-lg font-semibold text-foreground">
|
|
Hỏi về bất kỳ loại thuốc nào
|
|
</p>
|
|
<p className="text-[0.95rem]">
|
|
Ví dụ: “Liều dùng paracetamol cho người lớn?” hoặc “Chống chỉ
|
|
định của amoxicillin là gì?”
|
|
</p>
|
|
</div>
|
|
)}
|
|
{messages.map((message) => (
|
|
<div key={message.id}>
|
|
<ChatBubble message={message} />
|
|
{message.citations && message.citations.length > 0 && (
|
|
<div className="mb-4 mt-1.5 flex flex-wrap">
|
|
{message.citations.map((citation) => (
|
|
<CitationCard
|
|
key={citation.drugName}
|
|
citation={citation}
|
|
onClick={onCitationClick ? () => onCitationClick(citation) : undefined}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
{isSending && <TypingIndicator />}
|
|
</div>
|
|
<form className="flex gap-2.5 border-t bg-muted/40 p-4" onSubmit={handleSubmit}>
|
|
<Input
|
|
value={input}
|
|
onChange={(event) => setInput(event.target.value)}
|
|
placeholder="Hỏi về một loại thuốc..."
|
|
disabled={isSending}
|
|
aria-label="Nhập câu hỏi"
|
|
/>
|
|
<Button type="submit" disabled={isSending}>
|
|
<Send className="h-4 w-4" aria-hidden="true" />
|
|
{isSending ? "Đang gửi" : "Gửi"}
|
|
</Button>
|
|
</form>
|
|
</Card>
|
|
);
|
|
}
|