import { useEffect, useState, useCallback, useMemo } from 'react'; import { useSearchParams, Link } from 'react-router-dom'; import { Table, Select, Tag, Space, Button, message, Result, Typography } from 'antd'; import { RobotOutlined, CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, WarningOutlined, } from '@ant-design/icons'; import { useThemeMode } from '../../hooks/useThemeMode'; import { usePermission } from '../../hooks/usePermission'; import { analysisApi, type AnalysisItem } from '../../api/ai/analysis'; import { suggestionApi, type SuggestionItem } from '../../api/ai/suggestions'; import { EntityName } from '../../components/EntityName'; const { Text } = Typography; const ANALYSIS_TYPE_MAP: Record = { lab_report_interpretation: '化验单解读', health_trend_analysis: '趋势分析', personalized_checkup_plan: '体检方案', report_summary_generation: '报告摘要', }; const STATUS_CONFIG: Record = { completed: { color: 'green', text: '已完成' }, failed: { color: 'red', text: '失败' }, streaming: { color: 'blue', text: '进行中' }, pending: { color: 'orange', text: '等待中' }, }; const TYPE_OPTIONS = Object.entries(ANALYSIS_TYPE_MAP).map(([value, label]) => ({ value, label, })); const RISK_CONFIG: Record = { low: { color: 'green', text: '低风险', icon: }, medium: { color: 'orange', text: '中风险', icon: }, high: { color: 'red', text: '高风险', icon: }, }; const SUGGESTION_TYPE_MAP: Record = { followup: '随访建议', appointment: '预约建议', alert: '预警通知', }; const SUGGESTION_STATUS_CONFIG: Record = { pending: { color: 'orange', text: '待审批' }, approved: { color: 'green', text: '已批准' }, rejected: { color: 'red', text: '已拒绝' }, executed: { color: 'blue', text: '已执行' }, expired: { color: 'default', text: '已过期' }, parse_failed: { color: 'red', text: '解析失败' }, }; // --------------------------------------------------------------------------- // 分析结果渲染(Markdown 风格) // --------------------------------------------------------------------------- /** 递归提取 JSON 嵌套中的实际文本内容 */ function extractPlainText(raw: string): string { try { const parsed = JSON.parse(raw); if (typeof parsed === 'object' && parsed !== null && typeof parsed.content === 'string') { return extractPlainText(parsed.content); } return raw; } catch { return raw; } } function AnalysisContent({ content, isDark }: { content: string; isDark: boolean }) { const text = extractPlainText(content); // 简单的 Markdown 风格渲染 const lines = text.split('\n'); const rendered = lines.map((line, i) => { // 标题行 if (line.startsWith('### ')) { return ( {line.slice(4)} ); } if (line.startsWith('## ')) { return ( {line.slice(3)} ); } // 列表项 if (line.startsWith('- ') || line.startsWith('* ')) { return (
{renderInlineStyles(line.slice(2))}
); } // 有序列表 const orderedMatch = line.match(/^(\d+)\.\s/); if (orderedMatch) { return (
{orderedMatch[1]}. {renderInlineStyles(line.slice(orderedMatch[0].length))}
); } // 空行 if (line.trim() === '') { return
; } // 普通段落 return
{renderInlineStyles(line)}
; }); return
{rendered}
; } /** 简单行内样式渲染(粗体、代码) */ function renderInlineStyles(text: string) { // 拆分 **bold** 和 `code` 模式 const parts = text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g); return parts.map((part, i) => { if (part.startsWith('**') && part.endsWith('**')) { return {part.slice(2, -2)}; } if (part.startsWith('`') && part.endsWith('`')) { return ( {part.slice(1, -1)} ); } return {part}; }); } // --------------------------------------------------------------------------- // AI 建议面板 // --------------------------------------------------------------------------- function SuggestionPanel({ analysisId, isDark }: { analysisId: string; isDark: boolean }) { const [suggestions, setSuggestions] = useState([]); const [loading, setLoading] = useState(false); const [actionLoading, setActionLoading] = useState(null); const fetchSuggestions = useCallback(async () => { setLoading(true); try { const result = await suggestionApi.list({ analysis_id: analysisId }); setSuggestions(result.data || []); } catch { // 静默处理 } finally { setLoading(false); } }, [analysisId]); useEffect(() => { if (analysisId) fetchSuggestions(); }, [analysisId, fetchSuggestions]); const handleAction = async (id: string, action: 'approve' | 'reject') => { setActionLoading(id); try { await suggestionApi.approve(id, action); message.success(action === 'approve' ? '已批准' : '已拒绝'); fetchSuggestions(); } catch { message.error('操作失败'); } finally { setActionLoading(null); } }; if (loading) return
加载建议中...
; if (suggestions.length === 0) return null; return (
AI 建议列表 ({suggestions.length}) {suggestions.map((s) => { const risk = RISK_CONFIG[s.risk_level] || { color: 'default', text: s.risk_level, icon: null }; const status = SUGGESTION_STATUS_CONFIG[s.status] || { color: 'default', text: s.status }; const typeLabel = SUGGESTION_TYPE_MAP[s.suggestion_type] || s.suggestion_type; const isPending = s.status === 'pending'; const params = s.params as Record | null; const reason = params?.reason as string || params?.message as string || ''; return (
{risk.icon} {risk.text} {typeLabel} {reason && {reason}} {status.text} {isPending && ( )}
); })}
); } // --------------------------------------------------------------------------- // 主组件 // --------------------------------------------------------------------------- export default function AiAnalysisList() { const { hasPermission } = usePermission('ai.analysis.list'); if (!hasPermission) return ; const [searchParams] = useSearchParams(); const urlPatientId = searchParams.get('patient_id'); const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [query, setQuery] = useState<{ page: number; page_size: number; analysis_type?: string; patient_id?: string }>({ page: 1, page_size: 20, patient_id: urlPatientId || undefined, }); const [expandedId, setExpandedId] = useState(null); const [detail, setDetail] = useState(null); const isDark = useThemeMode(); const fetchData = useCallback( async (params: { page: number; page_size: number; analysis_type?: string }) => { setLoading(true); try { const result = await analysisApi.list(params); setData(result.data); setTotal(result.total); } catch { message.error('加载分析历史失败'); } finally { setLoading(false); } }, [], ); useEffect(() => { fetchData(query); }, [query, fetchData]); const handleExpand = async (expanded: boolean, record: AnalysisItem) => { if (expanded && record.id !== expandedId) { try { const item = await analysisApi.get(record.id); setDetail(item); setExpandedId(record.id); } catch { // 展开失败不阻塞 } } else if (!expanded) { setExpandedId(null); setDetail(null); } }; // 解析 metadata 中的趋势统计信息 const trendMetrics = useMemo(() => { if (!detail?.result_metadata) return null; const meta = detail.result_metadata as Record; // 自动分析结果中可能包含 metrics 统计 // 也从 sanitized_input 中解析(如果有的话) return meta; }, [detail]); const columns = useMemo(() => [ { title: '分析类型', dataIndex: 'analysis_type', key: 'analysis_type', width: 130, render: (v: string) => ( {ANALYSIS_TYPE_MAP[v] || v} ), }, { title: '患者', dataIndex: 'patient_id', key: 'patient_id', width: 140, render: (_: unknown, record: AnalysisItem) => ( ), }, { title: '模型', dataIndex: 'model_used', key: 'model_used', width: 130, render: (v: string) => v || '-', }, { title: '状态', dataIndex: 'status', key: 'status', width: 90, render: (v: string) => { const cfg = STATUS_CONFIG[v] || { color: 'default', text: v }; return {cfg.text}; }, }, { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170, render: (v: string) => (v ? new Date(v).toLocaleString('zh-CN') : '-'), }, ], []); return (

AI 分析历史

查看所有 AI 智能分析记录和结果