feat(ai): Prompt 管理 Phase 2 — analysis_type 后端选择键 + 筛选修复
- 新增 ai_prompt.analysis_type 列作为后端按链路选择 Prompt 的唯一键 - name 回归显示标识符用途,不再承担选择键角色 - 迁移 000164: 新增 analysis_type 列 + 从 name 回填 + 索引 - 迁移 000165: 修复旧数据从 category 错误回填的问题 - AiPromptList 页面重构: 分析类型/调用链路列、详情抽屉、新建表单 - DrawerForm 组件新增 onValuesChange 回调支持跨字段联动 - 新建表单选择分析类型后自动填充标识符 - 筛选过滤器改为按 analysis_type 而非 category 过滤(后端+前端同步) - 停用/激活/回滚/删除操作完整可用
This commit is contained in:
@@ -11,6 +11,7 @@ export interface PromptItem {
|
||||
version: number;
|
||||
is_active: boolean;
|
||||
category: string;
|
||||
analysis_type: string;
|
||||
tags: Record<string, unknown> | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -23,10 +24,11 @@ export interface CreatePromptReq {
|
||||
user_prompt_template: string;
|
||||
model_config: Record<string, unknown>;
|
||||
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<PromptItem>;
|
||||
},
|
||||
@@ -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}`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ interface DrawerFormProps {
|
||||
children?: React.ReactNode;
|
||||
columns?: 1 | 2;
|
||||
form?: ReturnType<typeof Form.useForm>[0];
|
||||
onValuesChange?: (changedValues: Record<string, unknown>, allValues: Record<string, unknown>) => 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({
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={initialValues}>
|
||||
<Form form={form} layout="vertical" initialValues={initialValues} onValuesChange={onValuesChange}>
|
||||
{sections
|
||||
? sections.map((s, i) => (
|
||||
<div key={i}>
|
||||
|
||||
@@ -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<string, string> = 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<PromptItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [categoryFilter, setCategoryFilter] = useState<string | undefined>();
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [analysisTypeFilter, setAnalysisTypeFilter] = useState<string | undefined>();
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [viewing, setViewing] = useState<PromptItem | null>(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<string, unknown>) => {
|
||||
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<string, number>();
|
||||
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) => <span style={{ fontWeight: 500 }}>{name}</span>,
|
||||
},
|
||||
{
|
||||
title: '类别',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
width: 120,
|
||||
render: (v: string) => (
|
||||
<Tag color="blue">{CATEGORY_MAP[v] || v}</Tag>
|
||||
),
|
||||
title: '分析类型',
|
||||
dataIndex: 'analysis_type',
|
||||
key: 'analysis_type',
|
||||
width: 130,
|
||||
render: (v: string) => {
|
||||
const cfg = ANALYSIS_TYPE_MAP[v];
|
||||
return cfg ? <Tag color="blue">{cfg.label}</Tag> : <Tag>{v}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '调用链路',
|
||||
dataIndex: 'analysis_type',
|
||||
key: 'api_route',
|
||||
width: 160,
|
||||
render: (v: string) => {
|
||||
const cfg = ANALYSIS_TYPE_MAP[v];
|
||||
return <Typography.Text type="secondary" style={{ fontSize: 12 }}>{cfg?.api ?? v}</Typography.Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
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 ? <Tag color="green">v{v}</Tag> : <span>v{v}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
@@ -122,7 +222,7 @@ export default function AiPromptList() {
|
||||
key: 'is_active',
|
||||
width: 80,
|
||||
render: (v: boolean) => (
|
||||
<Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag>
|
||||
<Badge status={v ? 'success' : 'default'} text={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) => (
|
||||
<AuthButton code="ai.prompt.manage">
|
||||
<Space size={4}>
|
||||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => openDetail(record)}>
|
||||
查看
|
||||
</Button>
|
||||
{!record.is_active && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<CheckOutlined />}
|
||||
onClick={() => handleActivate(record.id)}
|
||||
>
|
||||
<Button type="link" size="small" icon={<CheckOutlined />} onClick={() => handleActivate(record.id)}>
|
||||
激活
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<UndoOutlined />}
|
||||
onClick={() => handleRollback(record.id)}
|
||||
>
|
||||
{record.is_active && (
|
||||
<Button type="link" size="small" icon={<StopOutlined />} onClick={() => handleDeactivate(record.id)}>
|
||||
停用
|
||||
</Button>
|
||||
)}
|
||||
<Button type="link" size="small" icon={<UndoOutlined />} onClick={() => handleRollback(record.id)}>
|
||||
回滚
|
||||
</Button>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</AuthButton>
|
||||
),
|
||||
},
|
||||
], [handleActivate, handleRollback]);
|
||||
], [handleActivate, handleDeactivate, handleRollback, handleDelete, activeVersionMap]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="erp-page-header">
|
||||
<div>
|
||||
<h4>AI Prompt 管理</h4>
|
||||
<div className="erp-page-subtitle">管理 AI 分析提示词模板和版本</div>
|
||||
</div>
|
||||
<Space size={8}>
|
||||
<Select
|
||||
placeholder="筛选类别"
|
||||
value={categoryFilter}
|
||||
onChange={(v) => {
|
||||
setCategoryFilter(v);
|
||||
setPage(1);
|
||||
}}
|
||||
options={CATEGORIES}
|
||||
allowClear
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
<AuthButton code="ai.prompt.manage">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
新建 Prompt
|
||||
</Button>
|
||||
</AuthButton>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: isDark ? '#111827' : '#FFFFFF',
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
total,
|
||||
pageSize: 20,
|
||||
onChange: (p) => {
|
||||
setPage(p);
|
||||
fetchData(p);
|
||||
},
|
||||
showTotal: (t) => `共 ${t} 条记录`,
|
||||
style: { padding: '12px 16px', margin: 0 },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title="新建 Prompt"
|
||||
open={modalOpen}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
onOk={() => form.submit()}
|
||||
width={600}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={form} onFinish={handleCreate} layout="vertical" style={{ marginTop: 16 }}>
|
||||
const formSections: FormSection[] = [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item
|
||||
name="analysis_type"
|
||||
label="分析类型"
|
||||
rules={[{ required: true, message: '请选择分析类型' }]}
|
||||
extra="选择后自动填充名称,决定该 Prompt 被哪条分析链路调用"
|
||||
>
|
||||
<Select
|
||||
options={ANALYSIS_TYPES.map((t) => ({ value: t.value, label: t.label }))}
|
||||
placeholder="选择分析类型"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="名称"
|
||||
rules={[{ required: true, message: '请输入 Prompt 名称' }]}
|
||||
label="标识符"
|
||||
rules={[
|
||||
{ required: true, message: '请输入标识符' },
|
||||
{ pattern: /^[a-z0-9_]{3,64}$/, message: '仅允许小写字母、数字、下划线,3-64位' },
|
||||
]}
|
||||
extra="后端按此标识符查找 Prompt,通常与分析类型一致,非必要勿改"
|
||||
>
|
||||
<Input placeholder="如:化验单解读 V2" />
|
||||
</Form.Item>
|
||||
<Form.Item name="category" label="类别" rules={[{ required: true, message: '请选择类别' }]}>
|
||||
<Select options={CATEGORIES} placeholder="选择类别" />
|
||||
<Input placeholder="如 lab_report_interpretation" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={2} placeholder="Prompt 用途说明" />
|
||||
<Input.TextArea rows={2} placeholder="Prompt 用途说明(仅展示,不影响选择)" />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '模型配置',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item name="model" label="模型" rules={[{ required: true, message: '请选择模型' }]}>
|
||||
<Select options={MODEL_OPTIONS} placeholder="选择 AI 模型" />
|
||||
</Form.Item>
|
||||
<Form.Item name="temperature" label="Temperature" extra="越低越确定,越高越多样">
|
||||
<Slider min={0} max={2} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item name="max_tokens" label="Max Tokens">
|
||||
<InputNumber min={256} max={8192} step={256} style={{ width: '100%' }} placeholder="4096" />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '提示词模板',
|
||||
fields: (
|
||||
<>
|
||||
<Form.Item
|
||||
name="system_prompt"
|
||||
label="System Prompt"
|
||||
rules={[{ required: true, message: '请输入 System Prompt' }]}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="系统提示词" />
|
||||
<Input.TextArea rows={6} placeholder="系统提示词,定义 AI 的角色和行为规则" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="user_prompt_template"
|
||||
label="User Prompt 模板"
|
||||
rules={[{ required: true, message: '请输入 User Prompt 模板' }]}
|
||||
extra="支持 Handlebars {{变量}} 语法,如 {{patient_name}}、{{report_date}}"
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="用户提示词模板,可用 {{变量}} 占位" />
|
||||
<Input.TextArea rows={6} placeholder="用户提示词模板,可用 {{变量}} 占位" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="model_config"
|
||||
label="模型配置 (JSON)"
|
||||
initialValue={{ model: 'deepseek-chat', temperature: 0.7 }}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="AI Prompt 管理"
|
||||
subtitle="管理 AI 分析提示词模板和版本"
|
||||
filters={
|
||||
<Select
|
||||
placeholder="筛选分析类型"
|
||||
value={analysisTypeFilter}
|
||||
onChange={(v) => {
|
||||
setAnalysisTypeFilter(v);
|
||||
setPage(1);
|
||||
}}
|
||||
options={ANALYSIS_TYPES.map((t) => ({ value: t.value, label: t.label }))}
|
||||
allowClear
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<AuthButton code="ai.prompt.manage">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
form.setFieldsValue(DEFAULT_MODEL_CONFIG);
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
>
|
||||
<Input.TextArea rows={3} placeholder='{"model": "deepseek-chat", "temperature": 0.7}' />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
新建 Prompt
|
||||
</Button>
|
||||
</AuthButton>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
total,
|
||||
pageSize: 20,
|
||||
onChange: (p) => {
|
||||
setPage(p);
|
||||
fetchData(p);
|
||||
},
|
||||
showTotal: (t) => `共 ${t} 条记录`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 新建 Prompt Drawer */}
|
||||
<DrawerForm
|
||||
title="新建 Prompt"
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
onSubmit={handleCreate}
|
||||
form={form}
|
||||
onValuesChange={(changed) => {
|
||||
if ('analysis_type' in changed && changed.analysis_type) {
|
||||
form.setFieldValue('name', changed.analysis_type);
|
||||
}
|
||||
}}
|
||||
initialValues={{
|
||||
...DEFAULT_MODEL_CONFIG,
|
||||
category: '',
|
||||
analysis_type: undefined,
|
||||
name: '',
|
||||
description: '',
|
||||
}}
|
||||
width={720}
|
||||
columns={1}
|
||||
sections={formSections}
|
||||
/>
|
||||
|
||||
{/* 查看 Prompt 详情 */}
|
||||
<Drawer
|
||||
title={viewing ? `${viewing.name} (v${viewing.version})` : 'Prompt 详情'}
|
||||
open={detailOpen}
|
||||
onClose={() => { setDetailOpen(false); setViewing(null); }}
|
||||
width={640}
|
||||
styles={{ body: { background: isDark ? '#141414' : undefined } }}
|
||||
>
|
||||
{viewing && (
|
||||
<>
|
||||
<Descriptions column={2} size="small" style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="分析类型">
|
||||
<Tag color="blue">{ANALYSIS_TYPE_MAP[viewing.analysis_type]?.label ?? viewing.analysis_type}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="标识符">
|
||||
<Typography.Text code style={{ fontSize: 12 }}>{viewing.analysis_type}</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Badge status={viewing.is_active ? 'success' : 'default'} text={viewing.is_active ? '启用' : '停用'} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="版本">v{viewing.version}</Descriptions.Item>
|
||||
<Descriptions.Item label="调用链路" span={2}>
|
||||
<Typography.Text type="secondary">{ANALYSIS_TYPE_MAP[viewing.analysis_type]?.api ?? viewing.analysis_type}</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="更新时间">{formatDateTime(viewing.updated_at)}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{viewing.description && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>描述</Typography.Text>
|
||||
<div style={{ marginTop: 4 }}>{viewing.description}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{[
|
||||
{ label: 'System Prompt', content: viewing.system_prompt },
|
||||
{ label: 'User Prompt 模板', content: viewing.user_prompt_template },
|
||||
{ label: '模型配置', content: JSON.stringify(viewing.model_config, null, 2) },
|
||||
].map(({ label, content }) => (
|
||||
<div key={label} style={{ marginBottom: 16 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>{label}</Typography.Text>
|
||||
<pre style={{
|
||||
marginTop: 4,
|
||||
padding: 12,
|
||||
background: isDark ? '#1e293b' : '#f8fafc',
|
||||
borderRadius: 8,
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
maxHeight: 300,
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
{content}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user