diff --git a/apps/web/src/api/ai/prompts.ts b/apps/web/src/api/ai/prompts.ts index c591dab..a8fcc24 100644 --- a/apps/web/src/api/ai/prompts.ts +++ b/apps/web/src/api/ai/prompts.ts @@ -11,6 +11,7 @@ export interface PromptItem { version: number; is_active: boolean; category: string; + analysis_type: string; tags: Record | null; created_at: string; updated_at: string; @@ -23,10 +24,11 @@ export interface CreatePromptReq { user_prompt_template: string; model_config: Record; category: string; + analysis_type: string; } export const promptApi = { - list: async (params?: { category?: string; page?: number; page_size?: number }) => { + list: async (params?: { category?: string; analysis_type?: string; page?: number; page_size?: number }) => { const resp = await client.get('/ai/prompts', { params }); return resp.data.data as PaginatedResponse; }, @@ -38,8 +40,15 @@ export const promptApi = { const resp = await client.post(`/ai/prompts/${id}/activate`); return resp.data.data as PromptItem; }, + deactivate: async (id: string) => { + const resp = await client.post(`/ai/prompts/${id}/deactivate`); + return resp.data.data as PromptItem; + }, rollback: async (id: string) => { const resp = await client.post(`/ai/prompts/${id}/rollback`); return resp.data.data as PromptItem; }, + delete: async (id: string) => { + await client.delete(`/ai/prompts/${id}`); + }, }; diff --git a/apps/web/src/components/DrawerForm.tsx b/apps/web/src/components/DrawerForm.tsx index 9ca1635..dbb65cd 100644 --- a/apps/web/src/components/DrawerForm.tsx +++ b/apps/web/src/components/DrawerForm.tsx @@ -20,6 +20,7 @@ interface DrawerFormProps { children?: React.ReactNode; columns?: 1 | 2; form?: ReturnType[0]; + onValuesChange?: (changedValues: Record, allValues: Record) => void; } export function DrawerForm({ @@ -34,6 +35,7 @@ export function DrawerForm({ children, columns = 2, form: externalForm, + onValuesChange, }: DrawerFormProps) { const [internalForm] = Form.useForm(); const form = externalForm ?? internalForm; @@ -84,7 +86,7 @@ export function DrawerForm({ } > -
+ {sections ? sections.map((s, i) => (
diff --git a/apps/web/src/pages/health/AiPromptList.tsx b/apps/web/src/pages/health/AiPromptList.tsx index fd292a7..01bc44d 100644 --- a/apps/web/src/pages/health/AiPromptList.tsx +++ b/apps/web/src/pages/health/AiPromptList.tsx @@ -3,36 +3,68 @@ import { Table, Button, Space, - Modal, Form, Input, Select, Tag, + Badge, message, + Drawer, + Descriptions, + Typography, + Slider, + InputNumber, + Modal, } from 'antd'; -import { PlusOutlined, UndoOutlined, CheckOutlined } from '@ant-design/icons'; -import { promptApi, type PromptItem, type CreatePromptReq } from '../../api/ai/prompts'; +import { + PlusOutlined, + UndoOutlined, + CheckOutlined, + EyeOutlined, + StopOutlined, + DeleteOutlined, +} from '@ant-design/icons'; +import { promptApi, type PromptItem } from '../../api/ai/prompts'; import { AuthButton } from '../../components/AuthButton'; +import { DrawerForm } from '../../components/DrawerForm'; +import type { FormSection } from '../../components/DrawerForm'; +import { PageContainer } from '../../components/PageContainer'; import { useThemeMode } from '../../hooks/useThemeMode'; +import { formatDateTime } from '../../utils/format'; -const CATEGORIES = [ - { value: 'lab_report_interpretation', label: '化验单解读' }, - { value: 'health_trend_analysis', label: '趋势分析' }, - { value: 'personalized_checkup_plan', label: '体检方案' }, - { value: 'report_summary_generation', label: '报告摘要' }, +// --- 分析类型定义(与后端 AnalysisType::prompt_name() 一一对应) --- + +const ANALYSIS_TYPES = [ + { value: 'lab_report_interpretation', label: '化验单解读', api: '化验单解读 API' }, + { value: 'health_trend_analysis', label: '趋势分析', api: '趋势分析 API' }, + { value: 'personalized_checkup_plan', label: '体检方案', api: '体检方案 API' }, + { value: 'report_summary_generation', label: '报告摘要', api: '报告摘要 API' }, + { value: 'follow_up_summary_generation', label: '随访摘要', api: '随访摘要 API' }, +] as const; + +const ANALYSIS_TYPE_MAP = Object.fromEntries( + ANALYSIS_TYPES.map((t) => [t.value, t]), +); + +const MODEL_OPTIONS = [ + { value: 'deepseek-chat', label: 'DeepSeek Chat' }, + { value: 'deepseek-reasoner', label: 'DeepSeek Reasoner' }, + { value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' }, + { value: 'gpt-4o', label: 'GPT-4o' }, + { value: 'qwen-plus', label: 'Qwen Plus' }, ]; -const CATEGORY_MAP: Record = Object.fromEntries( - CATEGORIES.map((c) => [c.value, c.label]), -); +const DEFAULT_MODEL_CONFIG = { model: 'deepseek-chat', temperature: 0.7, max_tokens: 4096 }; export default function AiPromptList() { const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); - const [categoryFilter, setCategoryFilter] = useState(); - const [modalOpen, setModalOpen] = useState(false); + const [analysisTypeFilter, setAnalysisTypeFilter] = useState(); + const [drawerOpen, setDrawerOpen] = useState(false); + const [detailOpen, setDetailOpen] = useState(false); + const [viewing, setViewing] = useState(null); const [form] = Form.useForm(); const isDark = useThemeMode(); @@ -43,7 +75,7 @@ export default function AiPromptList() { const result = await promptApi.list({ page: p, page_size: 20, - category: categoryFilter, + analysis_type: analysisTypeFilter, }); setData(result.data); setTotal(result.total); @@ -53,18 +85,29 @@ export default function AiPromptList() { setLoading(false); } }, - [page, categoryFilter], + [page, analysisTypeFilter], ); useEffect(() => { fetchData(); }, [fetchData]); - const handleCreate = async (values: CreatePromptReq) => { + const handleCreate = async (values: Record) => { + const model = String(values.model ?? 'deepseek-chat'); + const temperature = Number(values.temperature ?? 0.7); + const max_tokens = Number(values.max_tokens ?? 4096); try { - await promptApi.create(values); + await promptApi.create({ + name: String(values.name ?? ''), + analysis_type: String(values.analysis_type ?? ''), + category: String(values.analysis_type ?? ''), + description: values.description ? String(values.description) : undefined, + system_prompt: String(values.system_prompt ?? ''), + user_prompt_template: String(values.user_prompt_template ?? ''), + model_config: { model, temperature, max_tokens }, + }); message.success('Prompt 创建成功'); - setModalOpen(false); + setDrawerOpen(false); form.resetFields(); fetchData(); } catch { @@ -72,7 +115,7 @@ export default function AiPromptList() { } }; - const handleActivate = async (id: string) => { + const handleActivate = useCallback(async (id: string) => { try { await promptApi.activate(id); message.success('已激活'); @@ -80,9 +123,19 @@ export default function AiPromptList() { } catch { message.error('激活失败'); } - }; + }, [fetchData]); - const handleRollback = async (id: string) => { + const handleDeactivate = useCallback(async (id: string) => { + try { + await promptApi.deactivate(id); + message.success('已停用'); + fetchData(); + } catch { + message.error('停用失败'); + } + }, [fetchData]); + + const handleRollback = useCallback(async (id: string) => { try { await promptApi.rollback(id); message.success('已回滚'); @@ -90,31 +143,78 @@ export default function AiPromptList() { } catch { message.error('回滚失败'); } + }, [fetchData]); + + const handleDelete = useCallback((record: PromptItem) => { + Modal.confirm({ + title: '确认删除', + content: `确定要删除 Prompt「${record.name}」(v${record.version}) 吗?`, + okType: 'danger', + onOk: async () => { + try { + await promptApi.delete(record.id); + message.success('删除成功'); + fetchData(); + } catch { + message.error('删除失败'); + } + }, + }); + }, [fetchData]); + + const openDetail = (record: PromptItem) => { + setViewing(record); + setDetailOpen(true); }; + // 按 analysis_type 汇总当前激活版本 + const activeVersionMap = useMemo(() => { + const map = new Map(); + for (const item of data) { + if (item.is_active) { + map.set(item.analysis_type, item.version); + } + } + return map; + }, [data]); + const columns = useMemo(() => [ { title: '名称', dataIndex: 'name', key: 'name', - width: 180, + width: 160, render: (name: string) => {name}, }, { - title: '类别', - dataIndex: 'category', - key: 'category', - width: 120, - render: (v: string) => ( - {CATEGORY_MAP[v] || v} - ), + title: '分析类型', + dataIndex: 'analysis_type', + key: 'analysis_type', + width: 130, + render: (v: string) => { + const cfg = ANALYSIS_TYPE_MAP[v]; + return cfg ? {cfg.label} : {v}; + }, + }, + { + title: '调用链路', + dataIndex: 'analysis_type', + key: 'api_route', + width: 160, + render: (v: string) => { + const cfg = ANALYSIS_TYPE_MAP[v]; + return {cfg?.api ?? v}; + }, }, { title: '版本', dataIndex: 'version', key: 'version', width: 70, - render: (v: number) => `v${v}`, + render: (v: number, record: PromptItem) => { + const isActive = activeVersionMap.get(record.analysis_type) === v && record.is_active; + return isActive ? v{v} : v{v}; + }, }, { title: '状态', @@ -122,7 +222,7 @@ export default function AiPromptList() { key: 'is_active', width: 80, render: (v: boolean) => ( - {v ? '启用' : '停用'} + ), }, { @@ -130,145 +230,247 @@ export default function AiPromptList() { dataIndex: 'updated_at', key: 'updated_at', width: 170, - render: (v: string) => (v ? new Date(v).toLocaleString('zh-CN') : '-'), + render: (v: string) => formatDateTime(v), }, { title: '操作', key: 'actions', - width: 160, + width: 220, render: (_: unknown, record: PromptItem) => ( + {!record.is_active && ( - )} - + )} + + ), }, - ], [handleActivate, handleRollback]); + ], [handleActivate, handleDeactivate, handleRollback, handleDelete, activeVersionMap]); - return ( -
-
-
-

AI Prompt 管理

-
管理 AI 分析提示词模板和版本
-
- - ({ value: t.value, label: t.label }))} + placeholder="选择分析类型" + /> + - - - - - + + + ), + }, + { + title: '模型配置', + fields: ( + <> + +