import { useEffect, useState } from 'react' import { Button, Card, Drawer, Form, Input, message, Modal, Popconfirm, Space, Table, Tag, Tooltip, } from 'antd' import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, ReloadOutlined, AppstoreAddOutlined, } from '@ant-design/icons' import Editor from '@monaco-editor/react' import { scenariosApi, type Scenario } from '../api' import PageWrapper from '../components/PageWrapper' import { colors } from '../tokens' import { formatDateTime } from '../utils/date' 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 [scenarios, setScenarios] = useState([]) const [loading, setLoading] = useState(false) 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 [llmConfig, setLlmConfig] = useState({ api_url: '', api_key: '', model: '' }) const [form] = Form.useForm() // Template picker const [tplModalOpen, setTplModalOpen] = useState(false) const [templates, setTemplates] = useState([]) const [tplLoading, setTplLoading] = useState(false) const load = async () => { setLoading(true) try { const res = await scenariosApi.list() setScenarios(res.data) } finally { setLoading(false) } } useEffect(() => { load() }, []) const openCreate = () => { setEditingScenario(null) form.resetFields() setCasesJson(YAML_TEMPLATE) setLlmConfig({ api_url: '', api_key: '', model: '' }) 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) => { 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)) const lc = tpl.llm_config || {} setLlmConfig({ api_url: lc.api_url || '', api_key: lc.api_key || '', model: lc.model || '' }) setDrawerOpen(true) } const openEdit = (scenario: Scenario) => { setEditingScenario(scenario) form.setFieldsValue({ name: scenario.name, description: scenario.description, tags: scenario.tags.join(', '), }) setCasesJson(JSON.stringify(scenario.cases, null, 2)) const lc = (scenario as any).llm_config || {} setLlmConfig({ api_url: lc.api_url || '', api_key: lc.api_key || '', model: lc.model || '', }) 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, llm_config: (llmConfig.api_url || llmConfig.api_key || llmConfig.model) ? { api_url: llmConfig.api_url, api_key: llmConfig.api_key, model: llmConfig.model } : null, } if (editingScenario) { await scenariosApi.update(editingScenario.id, payload) message.success('更新成功') } else { await scenariosApi.create(payload) message.success('创建成功') } setDrawerOpen(false) load() } const handleDelete = async (id: string) => { await scenariosApi.delete(id) message.success('已删除') load() } const columns = [ { title: '名称', dataIndex: 'name', key: 'name', width: 200, render: (v: string) => {v}, }, { 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={ } >
LLM 配置(动态用例使用)
setLlmConfig({ ...llmConfig, api_url: e.target.value })} />
setLlmConfig({ ...llmConfig, api_key: e.target.value })} />
setLlmConfig({ ...llmConfig, model: e.target.value })} />
用例定义(JSON)
setCasesJson(v || '')} options={{ minimap: { enabled: false }, fontSize: 13, lineNumbers: 'on', scrollBeyondLastLine: false, automaticLayout: true, }} />
setPreviewOpen(false)} footer={null} width={700} > {previewData && (

描述:{previewData.description || '无'}

标签:{previewData.tags?.join(', ') || '无'}

用例数:{previewData.cases?.length || 0}

                {JSON.stringify(previewData.cases, null, 2)}
              
)}
{/* Template picker */} setTplModalOpen(false)} footer={null} width={680} > {tplLoading ? (
加载模板…
) : (
{templates.map((tpl) => ( applyTemplate(tpl)} style={{ cursor: 'pointer', borderColor: colors.border }} >
{tpl.name}
{tpl.description}
{tpl.tags.map((t: string) => {t})}
{tpl.cases?.length ?? 0} 个用例
))}
)}
) }