AgentEvalTool/frontend/web/src/pages/Targets.tsx
sinohqb 050c674ee2
Some checks failed
CI / test (push) Failing after 1m10s
refactor(frontend): extract useResource/usePolling shared hooks
Seven pages repeated the same load-on-mount + loading + try/finally +
reload-button skeleton, each re-implementing tab-active refresh, silent
polling, and (in two places) a hand-rolled requestId race guard. Extract two
composable hooks: useResource(fetcher, {tabPath, deps}) owning data/loading/
reload with a built-in race guard and auto tab-active refresh, and
usePolling(fn, ms, enabled) replacing the hand-written setInterval effects.
Migrate all seven pages onto them; Targets/Scenarios/ModelConfigs also gain a
uniform tab-active refresh they previously lacked. Verified via tsc --noEmit
and npm run build (no frontend test runner exists).
2026-07-31 11:01:24 +08:00

225 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import {
Button, Drawer, Form, Input, message, Popconfirm,
Space, Table, Tag, Select, Tooltip,
} from 'antd'
import { PlusOutlined, ApiOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons'
import { targetsApi, type Target } from '../api'
import PageWrapper from '../components/PageWrapper'
import { useResource } from '../hooks/useResource'
import { colors } from '../tokens'
import { formatDateTime } from '../utils/date'
export default function TargetsPage() {
const { data: targets, loading, reload } = useResource(
() => targetsApi.list().then((r) => r.data),
{ tabPath: '/targets' },
)
const [drawerOpen, setDrawerOpen] = useState(false)
const [editingTarget, setEditingTarget] = useState<Target | null>(null)
const [form] = Form.useForm()
const openCreate = () => {
setEditingTarget(null)
form.resetFields()
form.setFieldsValue({
platform: 'ai_digital_employee',
channel_type: 'tutu-api',
})
setDrawerOpen(true)
}
const openEdit = (target: Target) => {
setEditingTarget(target)
form.setFieldsValue({
name: target.name,
description: target.description,
platform: target.platform,
channel_type: target.channel_type,
base_url: (target.channel_config as any)?.base_url || '',
token: (target.channel_config as any)?.token || '',
tenant: (target.channel_config as any)?.tenant || '',
chat_channel_id: (target.channel_config as any)?.chat_channel_id || '',
chat_contact_id: (target.channel_config as any)?.chat_contact_id || '',
})
setDrawerOpen(true)
}
const handleSubmit = async () => {
const values = await form.validateFields()
const channel_config = {
base_url: values.base_url,
token: values.token,
tenant: values.tenant,
chat_channel_id: values.chat_channel_id,
chat_contact_id: values.chat_contact_id,
}
const payload = {
name: values.name,
description: values.description || '',
platform: values.platform,
channel_type: values.channel_type,
channel_config,
status: 'active',
}
if (editingTarget) {
await targetsApi.update(editingTarget.id, payload)
message.success('更新成功')
} else {
await targetsApi.create(payload)
message.success('创建成功')
}
setDrawerOpen(false)
reload()
}
const handleTest = async (id: string) => {
try {
const res = await targetsApi.test(id)
if (res.data.ok) {
message.success('通道连通正常')
} else {
message.warning(`通道异常: ${res.data.message}`)
}
} catch {
message.error('连通性测试失败')
}
}
const handleDelete = async (id: string) => {
await targetsApi.delete(id)
message.success('已删除')
reload()
}
const statusColor: Record<string, string> = {
active: 'success',
inactive: 'default',
error: 'error',
}
const columns = [
{ title: '名称', dataIndex: 'name', key: 'name', width: 180,
render: (v: string) => <span style={{ fontWeight: 500 }}>{v}</span>,
},
{ title: '平台', dataIndex: 'platform', key: 'platform', width: 160,
render: (p: string) => p === 'ai_digital_employee' ? 'AI 数字员工' : 'AI 助手',
},
{ title: '通道类型', dataIndex: 'channel_type', key: 'channel_type', width: 120 },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100,
render: (s: string) => <Tag color={statusColor[s] || 'default'}>{s}</Tag>,
},
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180,
render: (t: string) => formatDateTime(t),
},
{ title: '操作', key: 'action', width: 160, fixed: 'right' as const,
render: (_: any, record: Target) => (
<Space size={4}>
<Tooltip title="连通测试">
<Button size="small" type="text" icon={<ApiOutlined />} onClick={() => handleTest(record.id)} />
</Tooltip>
<Tooltip title="编辑">
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} />
</Tooltip>
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Tooltip title="删除">
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
</Tooltip>
</Popconfirm>
</Space>
),
},
]
return (
<PageWrapper
title="评测对象"
description="管理被评测的 AI 智能体及其通信通道"
inline
fullHeight
extra={
<Space>
<Tooltip title="刷新">
<Button size="middle" icon={<ReloadOutlined />} onClick={() => reload()} />
</Tooltip>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</Space>
}
>
{/* 表格区 — 撑满剩余高度 */}
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
<Table
dataSource={targets ?? []}
columns={columns}
rowKey="id"
loading={loading}
pagination={{ pageSize: 15, showSizeChanger: true, showTotal: (t) => `${t}` }}
scroll={{ y: 'calc(100vh - 200px)' }}
style={{ height: '100%' }}
/>
</div>
<Drawer
title={editingTarget ? '编辑评测对象' : '新增评测对象'}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
width={480}
extra={
<Button type="primary" onClick={handleSubmit}>
{editingTarget ? '更新' : '创建'}
</Button>
}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="例如社区医院AI客服" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} placeholder="对象描述" />
</Form.Item>
<Form.Item name="platform" label="平台类型" rules={[{ required: true }]}>
<Select options={[
{ value: 'ai_digital_employee', label: 'AI 数字员工' },
{ value: 'ai_assistant', label: 'AI 助手' },
]} />
</Form.Item>
<Form.Item name="channel_type" label="通道类型" rules={[{ required: true }]}>
<Select options={[
{ value: 'tutu-api', label: 'Tutu API' },
{ value: 'http', label: 'HTTP 通用' },
{ value: 'openclaw', label: 'OpenClaw' },
]} />
</Form.Item>
<div style={{
border: `1px solid ${colors.border}`, borderRadius: 8,
padding: '12px 16px', marginBottom: 16,
}}>
<div style={{ fontWeight: 600, fontSize: 13, marginBottom: 12, color: colors.text }}>
</div>
<Form.Item name="base_url" label="Base URL" rules={[{ required: true, message: '请输入 API 地址' }]}>
<Input placeholder="https://api.example.com" />
</Form.Item>
<Form.Item name="token" label="Token" rules={[{ required: true, message: '请输入 Token' }]}>
<Input.Password placeholder="Bearer Token" />
</Form.Item>
<Form.Item name="tenant" label="Tenant">
<Input placeholder="租户 ID" />
</Form.Item>
<Form.Item name="chat_channel_id" label="Chat Channel ID" rules={[{ required: true }]}>
<Input placeholder="聊天频道 ID" />
</Form.Item>
<Form.Item name="chat_contact_id" label="Chat Contact ID">
<Input placeholder="联系人 ID" />
</Form.Item>
</div>
</Form>
</Drawer>
</PageWrapper>
)
}