AgentEvalTool/frontend/web/src/pages/ModelConfigs.tsx

258 lines
9.6 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 { useEffect, useState } from 'react'
import {
Button, Drawer, Form, Input, message, Popconfirm, Select, Space, Switch, Table, Tag, Tooltip,
} from 'antd'
import {
ApiOutlined, CheckCircleOutlined, DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined,
} from '@ant-design/icons'
import {
modelConfigsApi, type ModelCapability, type ModelConfig, type ModelConfigPayload,
} from '../api'
import PageWrapper from '../components/PageWrapper'
import { formatDateTime } from '../utils/date'
import { notifyModelConfigsChanged } from '../utils/modelConfigEvents'
const capabilityLabels: Record<ModelCapability, string> = {
chat: '对话',
embedding: '向量',
moderation: '内容审核',
}
const capabilityColors: Record<ModelCapability, string> = {
chat: 'blue',
embedding: 'green',
moderation: 'orange',
}
export default function ModelConfigsPage() {
const [configs, setConfigs] = useState<ModelConfig[]>([])
const [loading, setLoading] = useState(false)
const [drawerOpen, setDrawerOpen] = useState(false)
const [editing, setEditing] = useState<ModelConfig | null>(null)
const [testingId, setTestingId] = useState<string | null>(null)
const [capability, setCapability] = useState<ModelCapability | undefined>()
const [enabled, setEnabled] = useState<boolean | undefined>()
const [form] = Form.useForm<ModelConfigPayload>()
const load = async () => {
setLoading(true)
try {
const response = await modelConfigsApi.list({ capability, enabled })
setConfigs(response.data)
} finally {
setLoading(false)
}
}
useEffect(() => { load() }, [capability, enabled])
const openCreate = () => {
setEditing(null)
form.resetFields()
form.setFieldsValue({
provider: 'openai_compatible', capability: 'chat', enabled: true, is_default: false, description: '',
})
setDrawerOpen(true)
}
const openEdit = (config: ModelConfig) => {
setEditing(config)
form.setFieldsValue({
name: config.name,
provider: config.provider,
capability: config.capability,
endpoint_url: config.endpoint_url,
model_name: config.model_name,
api_key: null,
clear_api_key: false,
enabled: config.enabled,
is_default: config.is_default,
description: config.description,
})
setDrawerOpen(true)
}
const submit = async () => {
const values = await form.validateFields()
const payload: ModelConfigPayload = {
...values,
api_key: values.api_key || null,
model_name: values.model_name || null,
description: values.description || '',
}
if (editing) {
await modelConfigsApi.update(editing.id, payload)
message.success('模型配置已更新')
} else {
await modelConfigsApi.create(payload)
message.success('模型配置已创建')
}
notifyModelConfigsChanged()
setDrawerOpen(false)
load()
}
const testConnection = async (config: ModelConfig) => {
setTestingId(config.id)
try {
const response = await modelConfigsApi.test(config.id)
if (response.data.ok) message.success(`${config.name} 连接成功`)
else message.warning(response.data.message)
} finally {
setTestingId(null)
}
}
const remove = async (config: ModelConfig) => {
await modelConfigsApi.delete(config.id)
message.success('模型配置已删除')
notifyModelConfigsChanged()
load()
}
const columns = [
{
title: '配置名称', dataIndex: 'name', key: 'name', width: 190,
render: (value: string, record: ModelConfig) => (
<Space size={6}>
<span style={{ fontWeight: 500 }}>{value}</span>
{record.is_default && <Tag color="gold" style={{ margin: 0 }}></Tag>}
</Space>
),
},
{
title: '能力', dataIndex: 'capability', key: 'capability', width: 110,
render: (value: ModelCapability) => <Tag color={capabilityColors[value]}>{capabilityLabels[value]}</Tag>,
},
{ title: '模型', dataIndex: 'model_name', key: 'model_name', width: 180, render: (value: string | null) => value || '-' },
{ title: 'Endpoint', dataIndex: 'endpoint_url', key: 'endpoint_url', ellipsis: true },
{
title: '凭据', dataIndex: 'has_api_key', key: 'has_api_key', width: 90,
render: (value: boolean) => value ? <Tag color="success"></Tag> : <Tag></Tag>,
},
{
title: '状态', dataIndex: 'enabled', key: 'enabled', width: 90,
render: (value: boolean) => value ? <Tag color="success"></Tag> : <Tag></Tag>,
},
{
title: '更新时间', dataIndex: 'updated_at', key: 'updated_at', width: 170,
render: (value: string | null) => formatDateTime(value),
},
{
title: '操作', key: 'actions', width: 145, fixed: 'right' as const,
render: (_: unknown, record: ModelConfig) => (
<Space size={2}>
<Tooltip title="连接测试">
<Button
type="text"
size="small"
icon={<ApiOutlined />}
loading={testingId === record.id}
disabled={!record.enabled}
onClick={() => testConnection(record)}
/>
</Tooltip>
<Tooltip title="编辑">
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => openEdit(record)} />
</Tooltip>
<Popconfirm title="确认删除该模型配置?" onConfirm={() => remove(record)}>
<Tooltip title="删除">
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
</Tooltip>
</Popconfirm>
</Space>
),
},
]
return (
<PageWrapper
title="模型配置"
description="统一管理评测工程使用的外部模型连接"
inline
fullHeight
extra={
<Space>
<Select
allowClear
placeholder="全部能力"
style={{ width: 120 }}
value={capability}
onChange={setCapability}
options={Object.entries(capabilityLabels).map(([value, label]) => ({ value, label }))}
/>
<Select
allowClear
placeholder="全部状态"
style={{ width: 110 }}
value={enabled}
onChange={setEnabled}
options={[{ value: true, label: '启用' }, { value: false, label: '停用' }]}
/>
<Tooltip title="刷新"><Button icon={<ReloadOutlined />} onClick={load} /></Tooltip>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}></Button>
</Space>
}
>
<div style={{ height: '100%', minHeight: 0, overflow: 'hidden' }}>
<Table
dataSource={configs}
columns={columns}
rowKey="id"
loading={loading}
pagination={{ pageSize: 15, showSizeChanger: true, showTotal: (total) => `${total}` }}
scroll={{ y: 'calc(100vh - 200px)' }}
style={{ height: '100%' }}
/>
</div>
<Drawer
title={editing ? '编辑模型配置' : '新增模型配置'}
width={520}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
extra={<Button type="primary" icon={<CheckCircleOutlined />} onClick={submit}></Button>}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="配置名称" rules={[{ required: true, message: '请输入配置名称' }]}>
<Input placeholder="例如:评估对话模型" />
</Form.Item>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<Form.Item name="provider" label="协议" rules={[{ required: true }]}>
<Select options={[{ value: 'openai_compatible', label: 'OpenAI Compatible' }]} />
</Form.Item>
<Form.Item name="capability" label="能力" rules={[{ required: true }]}>
<Select options={Object.entries(capabilityLabels).map(([value, label]) => ({ value, label }))} />
</Form.Item>
</div>
<Form.Item name="endpoint_url" label="完整 Endpoint URL" rules={[{ required: true, message: '请输入 Endpoint URL' }, { type: 'url', message: '请输入有效 URL' }]}>
<Input placeholder="https://api.example.com/v1/chat/completions" />
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => form.getFieldValue('capability') !== 'moderation' && (
<Form.Item name="model_name" label="模型名称" rules={[{ required: true, message: '请输入模型名称' }]}>
<Input placeholder="例如gpt-4o-mini" />
</Form.Item>
)}
</Form.Item>
<Form.Item name="api_key" label="API Key" extra={editing?.has_api_key ? '已配置;留空将保留现有凭据' : undefined}>
<Input.Password autoComplete="new-password" placeholder={editing?.has_api_key ? '留空保留现有凭据' : '可选'} />
</Form.Item>
{editing?.has_api_key && (
<Form.Item name="clear_api_key" label="清除现有凭据" valuePropName="checked">
<Switch />
</Form.Item>
)}
<Form.Item name="description" label="说明">
<Input.TextArea rows={3} placeholder="该配置的使用范围或注意事项" />
</Form.Item>
<Space size={24}>
<Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item>
<Form.Item name="is_default" label="设为该能力默认配置" valuePropName="checked"><Switch /></Form.Item>
</Space>
</Form>
</Drawer>
</PageWrapper>
)
}