Files
HealthCarePregnant/pcm-platform/patient-app/src/pages/ChatPage.tsx
T
2026-06-18 09:48:05 +08:00

146 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { ArrowLeft, AlertTriangle } from 'lucide-react';
import { api, ApiError } from '../api/client';
import type { KnowledgeCitation } from '../api/types';
import './ChatPage.css';
interface Message {
id: string;
role: 'user' | 'assistant';
text: string;
grounded?: boolean;
citations?: KnowledgeCitation[];
}
const AUTHORITY_LABEL: Record<string, string> = {
authoritative: '权威',
reference: '参考',
self: '自建',
};
const WELCOME: Message = {
id: 'welcome',
role: 'assistant',
text: '你好呀~我是你的孕期助手。关于孕期饮食、监测、不适或注意事项,都可以问我。我会附上知识来源,遇到拿不准的会建议你及时就医。',
};
export function ChatPage(): JSX.Element {
const navigate = useNavigate();
const location = useLocation();
const initialQuestion = (location.state as { initialQuestion?: string } | null)?.initialQuestion;
const [messages, setMessages] = useState<Message[]>([WELCOME]);
const [input, setInput] = useState('');
const [sending, setSending] = useState(false);
const listRef = useRef<HTMLDivElement>(null);
const askedInitial = useRef(false);
async function send(question: string): Promise<void> {
const q = question.trim();
if (!q || sending) return;
const userMsg: Message = { id: `u-${Date.now()}`, role: 'user', text: q };
setMessages((prev) => [...prev, userMsg]);
setInput('');
setSending(true);
try {
const answer = await api.ask(q);
setMessages((prev) => [
...prev,
{
id: `a-${Date.now()}`,
role: 'assistant',
text: answer.answer,
grounded: answer.grounded,
citations: answer.citations,
},
]);
} catch (err) {
setMessages((prev) => [
...prev,
{
id: `e-${Date.now()}`,
role: 'assistant',
text: err instanceof ApiError ? err.message : '抱歉,我暂时没能回答,请稍后再试。',
},
]);
} finally {
setSending(false);
}
}
useEffect(() => {
if (initialQuestion && !askedInitial.current) {
askedInitial.current = true;
void send(initialQuestion);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialQuestion]);
useEffect(() => {
listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: 'smooth' });
}, [messages]);
return (
<div className="chat">
<header className="chat__header">
<button className="chat__back" onClick={() => navigate(-1)} type="button" aria-label="返回">
<ArrowLeft size={22} strokeWidth={1.75} />
</button>
<span></span>
</header>
<div className="chat__list" ref={listRef}>
{messages.map((m) => (
<div key={m.id} className={`chat__row chat__row--${m.role}`}>
<div className={`chat__bubble chat__bubble--${m.role}`}>
<p>{m.text}</p>
{m.role === 'assistant' && m.grounded === false && (
<p className="chat__nogrounded">
<AlertTriangle size={15} strokeWidth={1.9} />
</p>
)}
{m.citations && m.citations.length > 0 && (
<div className="chat__citations">
<p className="chat__citations-title"></p>
{m.citations.map((c) => (
<div key={c.id} className="chat__citation">
<span className="badge badge-ok">{AUTHORITY_LABEL[c.authority] ?? c.authority}</span>
<span className="chat__citation-text">
{c.title}
<span className="muted"> · {c.source}</span>
</span>
</div>
))}
</div>
)}
</div>
</div>
))}
{sending && (
<div className="chat__row chat__row--assistant">
<div className="chat__bubble chat__bubble--assistant chat__typing"></div>
</div>
)}
</div>
<form
className="chat__input"
onSubmit={(e) => {
e.preventDefault();
void send(input);
}}
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="问问孕期助手…"
/>
<button className="btn btn-primary" type="submit" disabled={sending || !input.trim()}>
</button>
</form>
</div>
);
}