Files
video-gen/video-gen-admin/src/pages/AdminModels.tsx
T
2026-06-25 17:07:32 +08:00

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;