Initial commit: HealthCarePregnant project documentation and platform

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
selfrelease
2026-06-18 09:48:05 +08:00
commit eab91174db
301 changed files with 42491 additions and 0 deletions
@@ -0,0 +1,145 @@
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>
);
}