diff --git a/apps/web/src/api/ai/analysis.ts b/apps/web/src/api/ai/analysis.ts index 5c7e238..cdfbff9 100644 --- a/apps/web/src/api/ai/analysis.ts +++ b/apps/web/src/api/ai/analysis.ts @@ -16,6 +16,21 @@ export interface AnalysisItem { updated_at: string; } +export interface HealthSummaryResponse { + patient_id: string; + risk_level: 'low' | 'medium' | 'high' | 'critical'; + active_insights_count: number; + recent_analyses_count: number; + latest_insight_title: string | null; + latest_analysis_type: string | null; + summary_items: Array<{ + category: string; + title: string; + severity: string | null; + created_at: string; + }>; +} + export const analysisApi = { list: async (params?: { patient_id?: string; analysis_type?: string; page?: number; page_size?: number }) => { const resp = await client.get('/ai/analysis/history', { params }); @@ -25,4 +40,8 @@ export const analysisApi = { const resp = await client.get(`/ai/analysis/${id}`); return resp.data.data as AnalysisItem; }, + getHealthSummary: async (patientId: string) => { + const resp = await client.get('/ai/health-summary', { params: { patient_id: patientId } }); + return resp.data.data as HealthSummaryResponse; + }, }; diff --git a/apps/web/src/components/ai/AiSidebar.tsx b/apps/web/src/components/ai/AiSidebar.tsx index 50b6989..6400f6e 100644 --- a/apps/web/src/components/ai/AiSidebar.tsx +++ b/apps/web/src/components/ai/AiSidebar.tsx @@ -1,8 +1,25 @@ import { useState, useRef, useEffect, useCallback } from 'react'; -import { Drawer, Input, Button, Space, Typography, Spin, Tag, theme } from 'antd'; -import { SendOutlined, RobotOutlined, DeleteOutlined } from '@ant-design/icons'; +import { + Drawer, + Input, + Button, + Space, + Typography, + Spin, + Tag, + Card, + theme, +} from 'antd'; +import { + SendOutlined, + RobotOutlined, + DeleteOutlined, + WarningOutlined, + SafetyCertificateOutlined, +} from '@ant-design/icons'; import { useLocation } from 'react-router-dom'; import { aiChatApi, type ChatHistoryItem } from '../../api/ai/chat'; +import { analysisApi, type HealthSummaryResponse } from '../../api/ai/analysis'; import { useAuthStore } from '../../stores/auth'; const { Text } = Typography; @@ -19,6 +36,13 @@ function extractPatientId(pathname: string): string | null { return match?.[1] ?? null; } +const RISK_CONFIG: Record = { + low: { color: 'green', label: '低风险' }, + medium: { color: 'orange', label: '中风险' }, + high: { color: 'red', label: '高风险' }, + critical: { color: '#cf1322', label: '严重' }, +}; + export default function AiSidebar({ open, onClose, @@ -29,6 +53,8 @@ export default function AiSidebar({ const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); + const [summary, setSummary] = useState(null); + const [summaryLoading, setSummaryLoading] = useState(false); const messagesEndRef = useRef(null); const location = useLocation(); const { token } = theme.useToken(); @@ -36,6 +62,7 @@ export default function AiSidebar({ const patientId = extractPatientId(location.pathname); const permissions = useAuthStore((s) => s.permissions); const canChat = permissions.includes('ai.chat.send'); + const canViewSummary = permissions.includes('ai.analysis.list'); // 欢迎消息 useEffect(() => { @@ -52,6 +79,30 @@ export default function AiSidebar({ } }, [open]); // eslint-disable-line react-hooks/exhaustive-deps + // 自动加载患者健康摘要 + useEffect(() => { + if (!open || !patientId || !canViewSummary) { + setSummary(null); + return; + } + let cancelled = false; + setSummaryLoading(true); + analysisApi + .getHealthSummary(patientId) + .then((data) => { + if (!cancelled) setSummary(data); + }) + .catch(() => { + if (!cancelled) setSummary(null); + }) + .finally(() => { + if (!cancelled) setSummaryLoading(false); + }); + return () => { + cancelled = true; + }; + }, [open, patientId, canViewSummary]); + const scrollToBottom = useCallback(() => { setTimeout(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); @@ -85,7 +136,11 @@ export default function AiSidebar({ content: m.content, })); - const resp = await aiChatApi.sendMessage(text, history, patientId ?? undefined); + const resp = await aiChatApi.sendMessage( + text, + history, + patientId ?? undefined + ); setMessages((prev) => [ ...prev, @@ -126,6 +181,8 @@ export default function AiSidebar({ ]); }; + const riskInfo = summary ? RISK_CONFIG[summary.risk_level] ?? RISK_CONFIG.low : null; + return ( } > + {/* 患者健康摘要卡片 */} + {patientId && canViewSummary && ( +
+ {summaryLoading ? ( + + 加载健康摘要... + + ) : summary ? ( + + + 健康摘要 + {riskInfo && ( + + {riskInfo.label} + + )} + + } + style={{ marginBottom: 4 }} + styles={{ body: { padding: '8px 12px' } }} + > +
+
+ 活跃洞察 {summary.active_insights_count} 项 + {summary.recent_analyses_count > 0 && + ` | 近期分析 ${summary.recent_analyses_count} 次`} +
+ {summary.summary_items.length > 0 && ( +
+ {summary.summary_items.slice(0, 3).map((item, i) => ( +
+ + {item.category} + + {item.title} +
+ ))} + {summary.summary_items.length > 3 && ( + + 还有 {summary.summary_items.length - 3} 项... + + )} +
+ )} + {summary.latest_insight_title && ( +
+ + 最新: {summary.latest_insight_title} +
+ )} +
+
+ ) : null} +
+ )} + {/* 消息列表 */}
@@ -199,7 +337,13 @@ export default function AiSidebar({
))} {loading && ( -
+
- 思考中... + {' '} + 思考中...
)} @@ -229,7 +374,9 @@ export default function AiSidebar({ onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder={ - canChat ? '输入消息... (Enter 发送, Shift+Enter 换行)' : '无 AI 聊天权限' + canChat + ? '输入消息... (Enter 发送, Shift+Enter 换行)' + : '无 AI 聊天权限' } disabled={loading || !canChat} autoSize={{ minRows: 1, maxRows: 4 }} @@ -241,11 +388,18 @@ export default function AiSidebar({ onClick={handleSend} loading={loading} disabled={!input.trim() || !canChat} - style={{ height: 'auto', borderRadius: '0 8px 8px 0', minHeight: 40 }} + style={{ + height: 'auto', + borderRadius: '0 8px 8px 0', + minHeight: 40, + }} /> {!canChat && ( - + 你没有 AI 聊天权限,请联系管理员开通。 )}