258 lines
9.6 KiB
TypeScript
258 lines
9.6 KiB
TypeScript
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>
|
||
)
|
||
}
|