244 lines
8.5 KiB
TypeScript
244 lines
8.5 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
|
|
} from 'antd';
|
|
import {
|
|
RobotOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
|
|
} from '@ant-design/icons';
|
|
import { getModelConfigs, saveModelConfig, deleteModelConfig } from '../api';
|
|
import type { ModelConfig } from '../types';
|
|
|
|
const AdminModels: React.FC = () => {
|
|
const [models, setModels] = useState<ModelConfig[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [modal, setModal] = useState<{ open: boolean; model: ModelConfig | null }>({ open: false, model: null });
|
|
const [form] = Form.useForm();
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await getModelConfigs();
|
|
setModels(data);
|
|
} catch { /* auth error handled by client */ }
|
|
setLoading(false);
|
|
};
|
|
|
|
useEffect(() => { load(); }, []);
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
const values = await form.validateFields();
|
|
const payload = {
|
|
name: values.name,
|
|
provider: values.provider,
|
|
model_name: values.modelName,
|
|
api_base: values.apiBase,
|
|
api_key: values.apiKey,
|
|
weight: values.weight,
|
|
max_tokens: values.maxTokens,
|
|
temperature: values.temperature,
|
|
is_active: values.isActive ?? true,
|
|
priority: values.priority ?? 0,
|
|
};
|
|
if (modal.model?.id) {
|
|
await saveModelConfig({ id: modal.model.id, ...payload });
|
|
} else {
|
|
await saveModelConfig(payload);
|
|
}
|
|
message.success(modal.model?.id ? '模型配置已更新' : '模型配置已添加');
|
|
setModal({ open: false, model: null });
|
|
form.resetFields();
|
|
load();
|
|
} catch (e: any) {
|
|
if (e?.errorFields) return;
|
|
message.error(e?.message || '保存失败');
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
try {
|
|
await deleteModelConfig(id);
|
|
message.success('模型配置已删除');
|
|
load();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '删除失败');
|
|
}
|
|
};
|
|
|
|
const openEdit = (model?: ModelConfig) => {
|
|
setModal({ open: true, model: model || null });
|
|
if (model) {
|
|
form.setFieldsValue(model);
|
|
} else {
|
|
form.resetFields();
|
|
form.setFieldsValue({
|
|
provider: 'sdk',
|
|
weight: 1,
|
|
maxTokens: 4096,
|
|
temperature: 0.7,
|
|
isActive: true,
|
|
priority: 0,
|
|
});
|
|
}
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: '模型名称', dataIndex: 'name', width: 150,
|
|
render: (v: string, r: ModelConfig) => (
|
|
<Space>
|
|
<div style={{
|
|
width: 32, height: 32, borderRadius: 8,
|
|
background: r.isActive
|
|
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
|
: 'linear-gradient(135deg, #94a3b8, #cbd5e1)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
color: '#fff', fontSize: 14,
|
|
}}>
|
|
<RobotOutlined />
|
|
</div>
|
|
<div>
|
|
<div style={{ fontWeight: 600 }}>{v}</div>
|
|
<div style={{ color: '#94a3b8', fontSize: 12 }}>{r.modelName}</div>
|
|
</div>
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: '提供商', dataIndex: 'provider', width: 140,
|
|
render: (v: string) => {
|
|
const labelMap: Record<string, string> = {
|
|
sdk: 'SDK模式',
|
|
openai_compatible: 'OpenAI兼容',
|
|
mock: 'Mock模式',
|
|
};
|
|
return <Tag color={v === 'mock' ? 'default' : 'blue'}>{labelMap[v] || v}</Tag>;
|
|
},
|
|
},
|
|
{
|
|
title: 'API地址', dataIndex: 'apiBase', width: 200,
|
|
render: (v: string) => (
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }} ellipsis>
|
|
{v || '-'}
|
|
</Typography.Text>
|
|
),
|
|
},
|
|
{
|
|
title: '权重', dataIndex: 'weight', width: 80, sorter: (a: ModelConfig, b: ModelConfig) => a.weight - b.weight,
|
|
},
|
|
{
|
|
title: 'Max Tokens', dataIndex: 'maxTokens', width: 100,
|
|
},
|
|
{
|
|
title: 'Temperature', dataIndex: 'temperature', width: 100,
|
|
render: (v: number) => v.toFixed(1),
|
|
},
|
|
{
|
|
title: '状态', dataIndex: 'isActive', width: 80,
|
|
render: (v: boolean) => (
|
|
<Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '操作', key: 'action', width: 150, fixed: 'right' as const,
|
|
render: (_: any, r: ModelConfig) => (
|
|
<Space size={4}>
|
|
<Button type="link" size="small" icon={<EditOutlined />}
|
|
onClick={() => openEdit(r)}>
|
|
编辑
|
|
</Button>
|
|
<Popconfirm title="确定删除该模型配置?" onConfirm={() => handleDelete(r.id)}>
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
|
<Typography.Text type="secondary">
|
|
共 {models.length} 个模型配置,按权重进行加权随机调度
|
|
</Typography.Text>
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => openEdit()}
|
|
style={{ borderRadius: 8 }}>
|
|
添加模型
|
|
</Button>
|
|
</div>
|
|
|
|
<Table
|
|
columns={columns}
|
|
dataSource={models}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={false}
|
|
scroll={{ x: 900 }}
|
|
/>
|
|
</Card>
|
|
|
|
{/* Edit Modal */}
|
|
<Modal
|
|
title={<Space><RobotOutlined />{modal.model?.id ? '编辑模型' : '添加模型'}</Space>}
|
|
open={modal.open}
|
|
onOk={handleSave}
|
|
onCancel={() => { setModal({ open: false, model: null }); form.resetFields(); }}
|
|
okText="确认" cancelText="取消" width={560}
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="name" label="显示名称"
|
|
rules={[{ required: true, message: '请输入模型名称' }]}>
|
|
<Input placeholder="例如:GPT-4o" size="large" />
|
|
</Form.Item>
|
|
<div style={{ display: 'flex', gap: 16 }}>
|
|
<Form.Item name="provider" label="提供商" style={{ flex: 1 }}
|
|
rules={[{ required: true }]}>
|
|
<Select size="large" options={[
|
|
{ value: 'sdk', label: 'SDK模式' },
|
|
{ value: 'openai_compatible', label: 'OpenAI兼容' },
|
|
{ value: 'mock', label: 'Mock模式' },
|
|
]} />
|
|
</Form.Item>
|
|
<Form.Item name="modelName" label="模型标识" style={{ flex: 1 }}
|
|
rules={[{ required: true, message: '请输入模型标识' }]}>
|
|
<Input placeholder="例如:gpt-4o" size="large" />
|
|
</Form.Item>
|
|
</div>
|
|
<Form.Item name="apiBase" label="API地址">
|
|
<Input placeholder="https://api.openai.com/v1" size="large" />
|
|
</Form.Item>
|
|
<Form.Item name="apiKey" label="API Key">
|
|
<Input.Password placeholder="sk-****" size="large" />
|
|
</Form.Item>
|
|
<div style={{ display: 'flex', gap: 16 }}>
|
|
<Form.Item name="weight" label="权重" style={{ flex: 1 }}
|
|
rules={[{ required: true }]}>
|
|
<InputNumber min={0} max={10} style={{ width: '100%' }} size="large" />
|
|
</Form.Item>
|
|
<Form.Item name="maxTokens" label="Max Tokens" style={{ flex: 1 }}
|
|
rules={[{ required: true }]}>
|
|
<InputNumber min={256} max={128000} style={{ width: '100%' }} size="large" />
|
|
</Form.Item>
|
|
<Form.Item name="temperature" label="Temperature" style={{ flex: 1 }}
|
|
rules={[{ required: true }]}>
|
|
<InputNumber min={0} max={2} step={0.1} style={{ width: '100%' }} size="large" />
|
|
</Form.Item>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 16 }}>
|
|
<Form.Item name="priority" label="优先级" style={{ flex: 1 }}
|
|
rules={[{ required: true }]}>
|
|
<InputNumber min={0} max={10} style={{ width: '100%' }} size="large" />
|
|
</Form.Item>
|
|
<Form.Item name="isActive" label="启用" valuePropName="checked" style={{ flex: 1, paddingTop: 30 }}>
|
|
<Switch />
|
|
</Form.Item>
|
|
</div>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminModels;
|