import { useEffect, useState } from 'react' import { Button, Card, Drawer, Form, Input, message, Modal, Popconfirm, Select, Space, Table, Tag, Tooltip, } from 'antd' import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, ReloadOutlined, AppstoreAddOutlined, } from '@ant-design/icons' import Editor from '@monaco-editor/react' import { modelConfigsApi, scenariosApi, type Scenario } from '../api' import PageWrapper from '../components/PageWrapper' import { useResource } from '../hooks/useResource' import { colors } from '../tokens' import { formatDateTime } from '../utils/date' import { MODEL_CONFIGS_CHANGED_EVENT } from '../utils/modelConfigEvents' const YAML_TEMPLATE = `# 场景用例示例(JSON 格式) [ { "id": "case-1", "type": "single", "messages": ["你好,请问挂号怎么操作?"], "eval_rules": [ { "type": "response_time", "params": { "max_ms": 30000 } } ] }, { "id": "case-dynamic-1", "type": "dynamic", "prompt": "你是一个来社区医院咨询的患者,请围绕挂号流程提几个问题", "turns": 3, "eval_rules": [ { "type": "response_time", "params": { "max_ms": 30000 } } ] } ]` export default function ScenariosPage() { const { data: scenarios, loading, reload } = useResource( () => scenariosApi.list().then((r) => r.data), { tabPath: '/scenarios' }, ) const { data: modelConfigs, loading: modelConfigsLoading, reload: reloadModelConfigs, } = useResource(() => modelConfigsApi.list({ enabled: true }).then((r) => r.data)) const [drawerOpen, setDrawerOpen] = useState(false) const [previewOpen, setPreviewOpen] = useState(false) const [previewData, setPreviewData] = useState(null) const [editingScenario, setEditingScenario] = useState(null) const [casesJson, setCasesJson] = useState(YAML_TEMPLATE) const [modelBindings, setModelBindings] = useState>({}) const [form] = Form.useForm() // Template picker const [tplModalOpen, setTplModalOpen] = useState(false) const [templates, setTemplates] = useState([]) const [tplLoading, setTplLoading] = useState(false) useEffect(() => { const onChanged = () => void reloadModelConfigs() window.addEventListener(MODEL_CONFIGS_CHANGED_EVENT, onChanged) return () => window.removeEventListener(MODEL_CONFIGS_CHANGED_EVENT, onChanged) }, [reloadModelConfigs]) const openCreate = () => { reloadModelConfigs() setEditingScenario(null) form.resetFields() setCasesJson(YAML_TEMPLATE) setModelBindings({}) setDrawerOpen(true) } const openTemplateModal = async () => { setTplModalOpen(true) if (templates.length === 0) { setTplLoading(true) try { const res = await scenariosApi.listTemplates() setTemplates(res.data) } catch { /* handled by interceptor */ } finally { setTplLoading(false) } } } const applyTemplate = (tpl: any) => { reloadModelConfigs() setTplModalOpen(false) setEditingScenario(null) form.resetFields() form.setFieldsValue({ name: `${tpl.name}(副本)`, description: tpl.description, tags: tpl.tags.join(', ') }) setCasesJson(JSON.stringify(tpl.cases, null, 2)) setModelBindings(tpl.model_bindings || {}) setDrawerOpen(true) } const openEdit = (scenario: Scenario) => { reloadModelConfigs() setEditingScenario(scenario) form.setFieldsValue({ name: scenario.name, description: scenario.description, tags: scenario.tags.join(', '), }) setCasesJson(JSON.stringify(scenario.cases, null, 2)) setModelBindings(scenario.model_bindings || {}) setDrawerOpen(true) } const openPreview = (scenario: Scenario) => { setPreviewData(scenario) setPreviewOpen(true) } const handleSubmit = async () => { const values = await form.validateFields() let cases: any[] try { cases = JSON.parse(casesJson) } catch { message.error('用例 JSON 格式错误') return } const tags = values.tags ? values.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : [] const payload = { name: values.name, description: values.description || '', tags, cases, model_bindings: modelBindings, } if (editingScenario) { await scenariosApi.update(editingScenario.id, payload) message.success('更新成功') } else { await scenariosApi.create(payload) message.success('创建成功') } setDrawerOpen(false) reload() } const handleDelete = async (id: string) => { await scenariosApi.delete(id) message.success('已删除') reload() } const columns = [ { title: '名称', dataIndex: 'name', key: 'name', width: 200, render: (v: string) => {v}, }, { title: '版本', dataIndex: 'version', key: 'version', width: 70, render: (v: number) => v{v ?? 1}, }, { title: '描述', dataIndex: 'description', key: 'description', ellipsis: true }, { title: '标签', dataIndex: 'tags', key: 'tags', width: 200, render: (tags: string[]) => tags.map((t) => {t}), }, { title: '用例数', key: 'cases', width: 80, render: (_: any, r: Scenario) => r.cases.length, }, { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180, render: (t: string) => formatDateTime(t), }, { title: '操作', key: 'action', width: 140, fixed: 'right' as const, render: (_: any, record: Scenario) => ( } > {/* 表格区 — 撑满剩余高度 */}
`共 ${t} 个` }} scroll={{ y: 'calc(100vh - 200px)' }} style={{ height: '100%' }} /> setDrawerOpen(false)} width={720} extra={ } >
模型引用
{[ { purpose: 'generator', label: '动态用例生成', capability: 'chat' }, { purpose: 'judge', label: 'LLM 评分', capability: 'chat' }, { purpose: 'embedding', label: '语义相似度', capability: 'embedding' }, { purpose: 'moderation', label: '内容审核', capability: 'moderation' }, ].map((item) => (