diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index cbaf68c..5593241 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -46,6 +46,7 @@ const AiPromptList = lazy(() => import('./pages/health/AiPromptList')); const AiAnalysisList = lazy(() => import('./pages/health/AiAnalysisList')); const AiUsageDashboard = lazy(() => import('./pages/health/AiUsageDashboard')); const AiConfigPage = lazy(() => import('./pages/health/AiConfigPage')); +const AiKnowledgePage = lazy(() => import('./pages/health/AiKnowledgePage')); const AlertList = lazy(() => import('./pages/health/AlertList')); const AlertDashboard = lazy(() => import('./pages/health/AlertDashboard')); const AlertRuleList = lazy(() => import('./pages/health/AlertRuleList')); @@ -255,7 +256,7 @@ export default function App() { "/health/follow-up-records", "/health/consultations", "/health/points-rules", "/health/points-products", "/health/points-orders", "/health/offline-events", "/health/ai-prompts", "/health/ai-analysis", - "/health/ai-usage", "/health/ai-config", "/health/alerts", "/health/alert-dashboard", + "/health/ai-usage", "/health/ai-config", "/health/ai-knowledge", "/health/alerts", "/health/alert-dashboard", "/health/alert-rules", "/health/devices", "/health/realtime-monitor", "/health/oauth-clients", "/health/dialysis", "/health/action-inbox", "/health/follow-up-templates", "/health/care-plans", "/health/shifts", @@ -327,6 +328,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/web/src/api/ai/knowledge.ts b/apps/web/src/api/ai/knowledge.ts new file mode 100644 index 0000000..40bf9c0 --- /dev/null +++ b/apps/web/src/api/ai/knowledge.ts @@ -0,0 +1,110 @@ +import client from '../client'; + +// === Types === + +export interface KnowledgeReference { + id: string; + tenant_id: string; + title: string; + analysis_type: string; + source_name: string; + content_summary: string; + tags: Record | null; + is_enabled: boolean; + created_at: string; + updated_at: string; +} + +export interface KnowledgeGuide { + id: string; + tenant_id: string; + title: string; + analysis_type: string; + content: string; + category: string | null; + is_enabled: boolean; + created_at: string; + updated_at: string; +} + +export interface CreateReferenceReq { + title: string; + analysis_type: string; + source_name: string; + content_summary: string; + tags?: Record; + is_enabled?: boolean; +} + +export interface UpdateReferenceReq { + title?: string; + analysis_type?: string; + source_name?: string; + content_summary?: string; + tags?: Record; + is_enabled?: boolean; +} + +export interface CreateGuideReq { + title: string; + analysis_type: string; + content: string; + category?: string; + is_enabled?: boolean; +} + +export interface UpdateGuideReq { + title?: string; + analysis_type?: string; + content?: string; + category?: string; + is_enabled?: boolean; +} + +// === API === + +export const knowledgeApi = { + // References + listReferences: async (params?: { analysis_type?: string }) => { + const resp = await client.get('/ai/knowledge/references', { params }); + return resp.data.data as { data: KnowledgeReference[]; total: number }; + }, + createReference: async (data: CreateReferenceReq) => { + const resp = await client.post('/ai/knowledge/references', data); + return resp.data.data as { id: string }; + }, + updateReference: async (id: string, data: UpdateReferenceReq) => { + const resp = await client.put(`/ai/knowledge/references/${id}`, data); + return resp.data.data as { id: string }; + }, + deleteReference: async (id: string) => { + const resp = await client.delete(`/ai/knowledge/references/${id}`); + return resp.data.data as { id: string }; + }, + reEmbedReference: async (id: string) => { + const resp = await client.post(`/ai/knowledge/references/${id}/re-embed`); + return resp.data.data as { id: string }; + }, + + // Guides + listGuides: async (params?: { analysis_type?: string }) => { + const resp = await client.get('/ai/knowledge/guides', { params }); + return resp.data.data as { data: KnowledgeGuide[]; total: number }; + }, + createGuide: async (data: CreateGuideReq) => { + const resp = await client.post('/ai/knowledge/guides', data); + return resp.data.data as { id: string }; + }, + updateGuide: async (id: string, data: UpdateGuideReq) => { + const resp = await client.put(`/ai/knowledge/guides/${id}`, data); + return resp.data.data as { id: string }; + }, + deleteGuide: async (id: string) => { + const resp = await client.delete(`/ai/knowledge/guides/${id}`); + return resp.data.data as { id: string }; + }, + reEmbedGuide: async (id: string) => { + const resp = await client.post(`/ai/knowledge/guides/${id}/re-embed`); + return resp.data.data as { id: string }; + }, +}; diff --git a/apps/web/src/pages/health/AiKnowledgePage.tsx b/apps/web/src/pages/health/AiKnowledgePage.tsx new file mode 100644 index 0000000..418ecb7 --- /dev/null +++ b/apps/web/src/pages/health/AiKnowledgePage.tsx @@ -0,0 +1,508 @@ +import { useCallback, useEffect, useState } from 'react'; +import { + Card, + Table, + Button, + Space, + Modal, + Form, + Input, + Select, + Switch, + message, + Popconfirm, + Tabs, + Tag, + Tooltip, +} from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + ReloadOutlined, + ThunderboltOutlined, +} from '@ant-design/icons'; +import { + knowledgeApi, + type KnowledgeReference, + type KnowledgeGuide, + type CreateReferenceReq, + type UpdateReferenceReq, + type CreateGuideReq, + type UpdateGuideReq, +} from '../../api/ai/knowledge'; +import { AuthButton } from '../../components/AuthButton'; + +const ANALYSIS_TYPES = [ + { value: 'lab_report', label: '化验报告' }, + { value: 'trend', label: '趋势分析' }, + { value: 'report_summary', label: '报告摘要' }, + { value: 'dialysis_risk', label: '透析风险' }, + { value: 'checkup_plan', label: '体检计划' }, + { value: 'follow_up', label: '随访总结' }, +]; + +export default function AiKnowledgePage() { + return ( + + }, + { key: 'guides', label: '临床指南', children: }, + ]} + /> + + ); +} + +// === References Tab === + +function ReferencesTab() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [filterType, setFilterType] = useState(); + const [form] = Form.useForm(); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const result = await knowledgeApi.listReferences( + filterType ? { analysis_type: filterType } : undefined, + ); + setData(result.data); + } catch { + message.error('加载参考资料失败'); + } finally { + setLoading(false); + } + }, [filterType]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const openCreate = () => { + setEditing(null); + form.resetFields(); + form.setFieldsValue({ is_enabled: true }); + setModalOpen(true); + }; + + const openEdit = (record: KnowledgeReference) => { + setEditing(record); + form.setFieldsValue({ + title: record.title, + analysis_type: record.analysis_type, + source_name: record.source_name, + content_summary: record.content_summary, + is_enabled: record.is_enabled, + }); + setModalOpen(true); + }; + + const handleSubmit = async () => { + const values = await form.validateFields(); + try { + if (editing) { + const req: UpdateReferenceReq = { + title: values.title, + analysis_type: values.analysis_type, + source_name: values.source_name, + content_summary: values.content_summary, + is_enabled: values.is_enabled, + }; + await knowledgeApi.updateReference(editing.id, req); + message.success('更新成功'); + } else { + const req: CreateReferenceReq = { + title: values.title, + analysis_type: values.analysis_type, + source_name: values.source_name, + content_summary: values.content_summary, + is_enabled: values.is_enabled, + }; + await knowledgeApi.createReference(req); + message.success('创建成功'); + } + setModalOpen(false); + fetchData(); + } catch { + message.error(editing ? '更新失败' : '创建失败'); + } + }; + + const handleDelete = async (id: string) => { + try { + await knowledgeApi.deleteReference(id); + message.success('删除成功'); + fetchData(); + } catch { + message.error('删除失败'); + } + }; + + const handleReEmbed = async (id: string) => { + try { + await knowledgeApi.reEmbedReference(id); + message.success('向量重新生成已触发'); + } catch { + message.error('向量重新生成失败'); + } + }; + + const columns = [ + { title: '标题', dataIndex: 'title', key: 'title', ellipsis: true }, + { + title: '分析类型', + dataIndex: 'analysis_type', + key: 'analysis_type', + width: 120, + render: (v: string) => { + const found = ANALYSIS_TYPES.find((t) => t.value === v); + return {found?.label ?? v}; + }, + }, + { title: '来源', dataIndex: 'source_name', key: 'source_name', width: 150, ellipsis: true }, + { + title: '状态', + dataIndex: 'is_enabled', + key: 'is_enabled', + width: 80, + render: (v: boolean) => ( + {v ? '启用' : '禁用'} + ), + }, + { + title: '更新时间', + dataIndex: 'updated_at', + key: 'updated_at', + width: 170, + render: (v: string) => (v ? new Date(v).toLocaleString() : '-'), + }, + { + title: '操作', + key: 'actions', + width: 200, + render: (_: unknown, record: KnowledgeReference) => ( + + +