diff --git a/apps/web/src/api/ai/suggestions.ts b/apps/web/src/api/ai/suggestions.ts new file mode 100644 index 0000000..bbfaacd --- /dev/null +++ b/apps/web/src/api/ai/suggestions.ts @@ -0,0 +1,34 @@ +import client from '../client'; + +export interface SuggestionItem { + id: string; + analysis_id: string; + suggestion_type: string; + risk_level: string; + params: Record | null; + status: string; + created_at: string; +} + +export interface ComparisonReport { + suggestion_id: string; + baseline: Record | null; + current: Record | null; + comparison_available: boolean; + message?: string; +} + +export const suggestionApi = { + list: async (params?: { analysis_id?: string; status?: string }) => { + const resp = await client.get('/ai/suggestions', { params }); + return resp.data.data as { data: SuggestionItem[]; total: number }; + }, + approve: async (id: string, action: 'approve' | 'reject') => { + const resp = await client.post(`/ai/suggestions/${id}/approve`, { action }); + return resp.data.data as { id: string; status: string }; + }, + getComparison: async (id: string) => { + const resp = await client.get(`/ai/suggestions/${id}/comparison`); + return resp.data.data as ComparisonReport; + }, +}; diff --git a/apps/web/src/pages/health/AiAnalysisList.tsx b/apps/web/src/pages/health/AiAnalysisList.tsx index f5ecc99..08879ea 100644 --- a/apps/web/src/pages/health/AiAnalysisList.tsx +++ b/apps/web/src/pages/health/AiAnalysisList.tsx @@ -1,10 +1,15 @@ import { useEffect, useState, useCallback, useMemo } from 'react'; -import { Table, Select, Tag, Space, message, Typography } from 'antd'; +import { Table, Select, Tag, Space, Button, message, Typography } from 'antd'; import { RobotOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + ExclamationCircleOutlined, + WarningOutlined, } from '@ant-design/icons'; import { useThemeMode } from '../../hooks/useThemeMode'; import { analysisApi, type AnalysisItem } from '../../api/ai/analysis'; +import { suggestionApi, type SuggestionItem } from '../../api/ai/suggestions'; const { Text } = Typography; @@ -27,6 +32,27 @@ const TYPE_OPTIONS = Object.entries(ANALYSIS_TYPE_MAP).map(([value, label]) => ( 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 风格) // --------------------------------------------------------------------------- @@ -105,6 +131,117 @@ function renderInlineStyles(text: string) { }); } +// --------------------------------------------------------------------------- +// 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 && ( + + + + + )} +
+ ); + })} +
+ ); +} + // --------------------------------------------------------------------------- // 主组件 // --------------------------------------------------------------------------- @@ -298,6 +435,12 @@ export default function AiAnalysisList() { 暂无结果内容 )} + + {/* AI 建议面板 */} + {detail.id && ( + + )} + ); }, }}