Remove corpus counts from chat chrome

This commit is contained in:
2026-08-10 17:26:58 +07:00
parent 46469468bb
commit 97cb6d16f4
31 changed files with 2192 additions and 424 deletions
+45 -16
View File
@@ -19,7 +19,10 @@ import { cn } from "@duoc-thu/ui";
interface ChatPanelProps {
sessionId: string;
messages: ChatMessage[];
onMessagesChange: React.Dispatch<React.SetStateAction<ChatMessage[]>>;
initialQuery?: string;
initialQueryToken?: number;
onCitationClick?: (citation: Citation, index: number, allCitations: Citation[]) => void;
onCitationsLoaded?: (citations: Citation[]) => void;
activeCitationIndex?: number | null;
@@ -28,42 +31,45 @@ interface ChatPanelProps {
const STARTER_QUESTIONS = [
{
category: "Liều Dùng Lâm Sàng",
query: "Liều dùng Paracetamol người lớn và trẻ em theo cân nặng là bao nhiêu?",
category: "Chỉ Định",
query: "Levetiracetam được chỉ định trong những trường hợp nào?",
icon: Pill,
},
{
category: "Chống Chỉ Định",
query: "Chống chỉ định tuyệt đối và tương đối của Amoxicillin là gì?",
query: "Chống chỉ định của Metformin là gì?",
icon: Stethoscope,
},
{
category: "Tương Tác Thuốc",
query: "Tương tác giữa Metformin và thuốc cản quang chứa iốt xử trí thế nào?",
category: "ADR Theo Tần Suất",
query: "Tác dụng không mong muốn của Zolpidem là gì?",
icon: Activity,
},
{
category: "Thận Trọng & ADR",
query: "Thận trọng khi dùng Aspirin cho bệnh nhân có tiền sử loét dạ dày?",
category: "Thời Kỳ Mang Thai",
query: "Có thể dùng Fluoxetin trong thời kỳ mang thai không?",
icon: Zap,
},
];
export function ChatPanel({
sessionId,
messages,
onMessagesChange: setMessages,
initialQuery,
initialQueryToken,
onCitationClick,
onCitationsLoaded,
activeCitationIndex = null,
className,
}: ChatPanelProps) {
const { resolvedTheme } = useTheme();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const initialQuerySentRef = useRef<string | undefined>(undefined);
const initialQuerySentRef = useRef<number | undefined>(undefined);
const stopRequestedRef = useRef(false);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
@@ -87,8 +93,12 @@ export function ChatPanel({
setMessages((prev) => [...prev, userMsg]);
setIsLoading(true);
stopRequestedRef.current = false;
abortControllerRef.current = new AbortController();
const timeoutId = window.setTimeout(() => {
abortControllerRef.current?.abort();
}, 25_000);
try {
const res = await fetch("/api/chat", {
@@ -115,10 +125,16 @@ export function ChatPanel({
}
} catch (err: any) {
if (err.name === "AbortError") {
setError(
stopRequestedRef.current
? "Đã dừng chờ trên giao diện. Tác vụ đang chạy có thể cần vài giây để kết thúc an toàn."
: "Yêu cầu vượt quá 25 giây và đã được dừng. Vui lòng thử lại với câu hỏi cụ thể hơn."
);
return;
}
setError("Không thể kết nối đến máy chủ AI Service. Vui lòng kiểm tra lại dịch vụ backend.");
} finally {
window.clearTimeout(timeoutId);
setIsLoading(false);
abortControllerRef.current = null;
}
@@ -126,12 +142,15 @@ export function ChatPanel({
const handleStop = () => {
if (abortControllerRef.current) {
stopRequestedRef.current = true;
abortControllerRef.current.abort();
setIsLoading(false);
abortControllerRef.current = null;
}
};
useEffect(() => {
return () => abortControllerRef.current?.abort();
}, []);
useEffect(() => {
// Guard against firing twice for the same query: React 18 Strict Mode
// (dev only) runs this effect setup twice on mount, and with no guard
@@ -140,12 +159,16 @@ export function ChatPanel({
// muốn của Aspirin" turns in the trace). The ref persists across the
// Strict Mode replay, so the second invocation for the same
// `initialQuery` is a no-op; a genuinely new query still sends once.
if (initialQuery && initialQuerySentRef.current !== initialQuery) {
initialQuerySentRef.current = initialQuery;
if (
initialQuery &&
initialQueryToken !== undefined &&
initialQuerySentRef.current !== initialQueryToken
) {
initialQuerySentRef.current = initialQueryToken;
handleSendMessage(initialQuery);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialQuery]);
}, [initialQuery, initialQueryToken]);
// Empty state renderer per theme
const renderEmptyState = () => {
@@ -165,7 +188,7 @@ export function ChatPanel({
Tra Cứu Dược Thư Quốc Gia Việt Nam
</h2>
<p className="text-xs sm:text-sm text-txt-secondary mt-1.5 leading-relaxed max-w-xl mx-auto">
Hệ thống AI y tế tra cứu chính xác theo 684 chuyên luận chính thức. Mọi thông tin đu đưc xác thực suy luận (Entailment Verification) kèm trích dẫn trang in PDF.
Tra cứu 684 chuyên luận Dược thư Quốc gia Việt Nam 2018 với căn cứ theo trang in. Khi cần, bác thể tiếp tục trao đi đ làm dữ kiện đi chiếu với bối cảnh lâm sàng.
</p>
</div>
@@ -321,7 +344,13 @@ export function ChatPanel({
}
activeCitationIndex={activeCitationIndex}
onRetry={retryQuery ? () => handleSendMessage(retryQuery) : undefined}
onQuickReply={(text) => handleSendMessage(text)}
onQuickReply={
msgIdx === messages.length - 1 &&
msg.decision === "clarify" &&
!isLoading
? (text) => handleSendMessage(text)
: undefined
}
/>
);
})
+13 -19
View File
@@ -12,14 +12,6 @@ interface ComposerProps {
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,
@@ -76,13 +68,9 @@ export function Composer({
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);
setSuggestions([]);
setShowSuggestions(false);
} catch {
if (!cancelled) {
setSuggestions([]);
@@ -122,6 +110,15 @@ export function Composer({
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<HTMLTextAreaElement>) => {
if (showSuggestions && suggestions.length > 0) {
if (e.key === "ArrowDown") {
@@ -136,9 +133,7 @@ export function Composer({
}
if (e.key === "Enter" && selectedIndex >= 0) {
e.preventDefault();
setValue(suggestions[selectedIndex]);
setShowSuggestions(false);
setSelectedIndex(-1);
applySuggestion(suggestions[selectedIndex]);
return;
}
}
@@ -166,8 +161,7 @@ export function Composer({
<button
key={idx}
onClick={() => {
setValue(item);
setShowSuggestions(false);
applySuggestion(item);
inputRef.current?.focus();
}}
className={cn(
+27 -8
View File
@@ -32,11 +32,31 @@ interface SidebarProps {
}
const QUICK_PROMPTS = [
{ drug: "Paracetamol", label: "Liều dùng Paracetamol người lớn & trẻ em" },
{ drug: "Amoxicillin", label: "Chống chỉ định & Thận trọng khi dùng Amoxicillin" },
{ drug: "Metformin", label: "Liều lượng & Tương tác thuốc Metformin" },
{ drug: "Aspirin", label: "Chỉ định & Tác dụng không mong muốn của Aspirin" },
{ drug: "Ibuprofen", label: "Liều dùng Ibuprofen theo trọng lượng cơ thể" },
{
drug: "Levetiracetam",
label: "Chỉ định của Levetiracetam",
query: "Levetiracetam được chỉ định trong những trường hợp nào?",
},
{
drug: "Metformin",
label: "Chống chỉ định của Metformin",
query: "Chống chỉ định của Metformin là gì?",
},
{
drug: "Zolpidem",
label: "ADR Zolpidem theo tần suất",
query: "Tác dụng không mong muốn của Zolpidem là gì?",
},
{
drug: "Fluoxetin",
label: "Fluoxetin trong thời kỳ mang thai",
query: "Có thể dùng Fluoxetin trong thời kỳ mang thai không?",
},
{
drug: "Danazol",
label: "Tương tác thuốc của Danazol",
query: "Danazol có những tương tác thuốc nào?",
},
];
export function Sidebar({
@@ -194,7 +214,7 @@ export function Sidebar({
{QUICK_PROMPTS.map((prompt, idx) => (
<button
key={idx}
onClick={() => onQuickQuery(prompt.label)}
onClick={() => onQuickQuery(prompt.query)}
className="w-full text-left p-2 rounded-xl bg-surface-elevated hover:bg-surface-hover border border-border-subtle text-txt-secondary hover:text-txt-primary text-[0.72rem] leading-snug transition-all flex items-center justify-between group"
>
<span className="truncate pr-1">{prompt.label}</span>
@@ -206,12 +226,11 @@ export function Sidebar({
</div>
{/* System Stats Footer */}
<div className="p-3 border-t border-border-subtle bg-surface-elevated/40 text-[0.68rem] text-txt-muted flex items-center justify-between">
<div className="p-3 border-t border-border-subtle bg-surface-elevated/40 text-[0.68rem] text-txt-muted flex items-center">
<div className="flex items-center gap-1.5">
<BookOpen className="w-3.5 h-3.5 text-accent-primary" />
<span>Dược thư QGVN 2018</span>
</div>
<span className="font-semibold text-accent-primary">684 Chuyên luận</span>
</div>
</aside>
);