380 lines
11 KiB
JavaScript
380 lines
11 KiB
JavaScript
/*
|
|
Copyright (C) 2025 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
|
|
import React, { useEffect, useState, useCallback } from 'react';
|
|
import {
|
|
Button,
|
|
Card,
|
|
Form,
|
|
Table,
|
|
Modal,
|
|
Typography,
|
|
Toast,
|
|
} from '@douyinfe/semi-ui';
|
|
import { API, showError, showSuccess } from '../../helpers';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Plus, Pencil, Trash2, RefreshCw } from 'lucide-react';
|
|
|
|
const { Text } = Typography;
|
|
|
|
const EMPTY_MODEL = {
|
|
name: '',
|
|
provider: '',
|
|
intelligence: 0,
|
|
speed: 0,
|
|
price: 0,
|
|
latency: 0,
|
|
context_window: '',
|
|
type: 'non-reasoning',
|
|
openness: 'proprietary',
|
|
category: 'language',
|
|
};
|
|
|
|
export default function BenchmarkDataManager() {
|
|
const { t } = useTranslation();
|
|
const [models, setModels] = useState([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
|
const [editingIndex, setEditingIndex] = useState(null);
|
|
const [editForm, setEditForm] = useState({ ...EMPTY_MODEL });
|
|
|
|
const fetchData = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await API.get('/api/option/');
|
|
if (res.data.success && res.data.data) {
|
|
const option = res.data.data.find(
|
|
(o) => o.key === 'benchmark_data.models',
|
|
);
|
|
if (option && option.value) {
|
|
setModels(JSON.parse(option.value));
|
|
} else {
|
|
// try to fetch via the specific key
|
|
const fallbackRes = await API.get(
|
|
'/api/option/?key=benchmark_data.models',
|
|
);
|
|
if (fallbackRes.data?.data) {
|
|
setModels(JSON.parse(fallbackRes.data.data.value || '[]'));
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
showError(t('获取评测数据失败'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [t]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
const saveData = async (newModels) => {
|
|
setSaving(true);
|
|
try {
|
|
const res = await API.put('/api/option/', {
|
|
key: 'benchmark_data.models',
|
|
value: JSON.stringify(newModels),
|
|
});
|
|
if (res.data.success) {
|
|
showSuccess(t('保存成功'));
|
|
setModels(newModels);
|
|
} else {
|
|
showError(res.data.message || t('保存失败'));
|
|
}
|
|
} catch (error) {
|
|
showError(t('保存失败,请重试'));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const openAddModal = () => {
|
|
setEditingIndex(null);
|
|
setEditForm({ ...EMPTY_MODEL });
|
|
setEditModalVisible(true);
|
|
};
|
|
|
|
const openEditModal = (index) => {
|
|
setEditingIndex(index);
|
|
setEditForm({ ...models[index] });
|
|
setEditModalVisible(true);
|
|
};
|
|
|
|
const handleSaveModel = async () => {
|
|
if (!editForm.name || !editForm.provider) {
|
|
showError(t('模型名称和供应商不能为空'));
|
|
return;
|
|
}
|
|
const newModels = [...models];
|
|
if (editingIndex !== null) {
|
|
newModels[editingIndex] = { ...editForm };
|
|
} else {
|
|
newModels.push({ ...editForm });
|
|
}
|
|
await saveData(newModels);
|
|
setEditModalVisible(false);
|
|
};
|
|
|
|
const handleDeleteModel = async (index) => {
|
|
const newModels = models.filter((_, i) => i !== index);
|
|
await saveData(newModels);
|
|
};
|
|
|
|
const handleFieldChange = (field, value) => {
|
|
setEditForm((prev) => ({
|
|
...prev,
|
|
[field]:
|
|
['intelligence', 'speed', 'price', 'latency'].includes(field)
|
|
? parseFloat(value) || 0
|
|
: value,
|
|
}));
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: t('模型名称'),
|
|
dataIndex: 'name',
|
|
width: 180,
|
|
render: (text, record) => (
|
|
<div>
|
|
<div className='font-medium'>{text}</div>
|
|
<Text type='secondary' size='small'>
|
|
{record.provider}
|
|
</Text>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: t('智能指数'),
|
|
dataIndex: 'intelligence',
|
|
width: 100,
|
|
render: (val) => (
|
|
<span className='text-purple-400 font-mono'>{val}</span>
|
|
),
|
|
},
|
|
{
|
|
title: t('速度 (tok/s)'),
|
|
dataIndex: 'speed',
|
|
width: 120,
|
|
render: (val) => (
|
|
<span className='text-cyan-400 font-mono'>
|
|
{val?.toLocaleString()}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
title: t('价格 ($/M)'),
|
|
dataIndex: 'price',
|
|
width: 100,
|
|
render: (val) => (
|
|
<span className='text-emerald-400 font-mono'>${val?.toFixed(2)}</span>
|
|
),
|
|
},
|
|
{
|
|
title: t('延迟 (s)'),
|
|
dataIndex: 'latency',
|
|
width: 100,
|
|
render: (val) => (
|
|
<span className='text-amber-400 font-mono'>{val?.toFixed(2)}s</span>
|
|
),
|
|
},
|
|
{
|
|
title: t('类型'),
|
|
dataIndex: 'type',
|
|
width: 80,
|
|
render: (val) => (
|
|
<span
|
|
className={`px-2 py-0.5 rounded text-xs ${
|
|
val === 'reasoning'
|
|
? 'bg-purple-900/50 text-purple-300'
|
|
: 'bg-blue-900/50 text-blue-300'
|
|
}`}
|
|
>
|
|
{val === 'reasoning' ? t('推理') : t('通用')}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
title: t('操作'),
|
|
width: 120,
|
|
render: (_, __, index) => (
|
|
<div className='flex gap-2'>
|
|
<Button
|
|
size='small'
|
|
type='tertiary'
|
|
icon={<Pencil size={14} />}
|
|
onClick={() => openEditModal(index)}
|
|
/>
|
|
<Button
|
|
size='small'
|
|
type='danger'
|
|
icon={<Trash2 size={14} />}
|
|
onClick={() => handleDeleteModel(index)}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Card
|
|
title={t('AI 模型评测数据管理')}
|
|
style={{ marginBottom: 16 }}
|
|
headerExtraContent={
|
|
<div className='flex gap-2'>
|
|
<Button
|
|
size='small'
|
|
type='tertiary'
|
|
icon={<RefreshCw size={14} />}
|
|
onClick={fetchData}
|
|
loading={loading}
|
|
>
|
|
{t('刷新')}
|
|
</Button>
|
|
<Button
|
|
size='small'
|
|
type='primary'
|
|
icon={<Plus size={14} />}
|
|
onClick={openAddModal}
|
|
>
|
|
{t('添加模型')}
|
|
</Button>
|
|
</div>
|
|
}
|
|
>
|
|
<Text type='secondary' size='small' style={{ marginBottom: 16, display: 'block' }}>
|
|
{t('在此管理 AI 模型评测数据,添加、编辑或删除模型条目。保存后前端评测页面将自动更新。')}
|
|
</Text>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={models}
|
|
pagination={{ pageSize: 20 }}
|
|
loading={loading}
|
|
rowKey={(_, idx) => idx}
|
|
size='small'
|
|
bordered
|
|
/>
|
|
|
|
{/* Edit/Add Modal */}
|
|
<Modal
|
|
title={editingIndex !== null ? t('编辑模型') : t('添加模型')}
|
|
visible={editModalVisible}
|
|
onOk={handleSaveModel}
|
|
onCancel={() => setEditModalVisible(false)}
|
|
okText={t('保存')}
|
|
cancelText={t('取消')}
|
|
confirmLoading={saving}
|
|
style={{ width: 520 }}
|
|
>
|
|
<Form layout='vertical'>
|
|
<div className='grid grid-cols-2 gap-3'>
|
|
<Form.Input
|
|
field='name'
|
|
label={t('模型名称')}
|
|
value={editForm.name}
|
|
onChange={(v) => handleFieldChange('name', v)}
|
|
placeholder='e.g. Claude Opus 4.8'
|
|
/>
|
|
<Form.Input
|
|
field='provider'
|
|
label={t('供应商')}
|
|
value={editForm.provider}
|
|
onChange={(v) => handleFieldChange('provider', v)}
|
|
placeholder='e.g. Anthropic'
|
|
/>
|
|
<Form.InputNumber
|
|
field='intelligence'
|
|
label={t('智能指数')}
|
|
value={editForm.intelligence}
|
|
onChange={(v) => handleFieldChange('intelligence', v)}
|
|
min={0}
|
|
max={100}
|
|
/>
|
|
<Form.InputNumber
|
|
field='speed'
|
|
label={t('速度 (tokens/s)')}
|
|
value={editForm.speed}
|
|
onChange={(v) => handleFieldChange('speed', v)}
|
|
min={0}
|
|
/>
|
|
<Form.InputNumber
|
|
field='price'
|
|
label={t('价格 ($/M tokens)')}
|
|
value={editForm.price}
|
|
onChange={(v) => handleFieldChange('price', v)}
|
|
min={0}
|
|
step={0.01}
|
|
/>
|
|
<Form.InputNumber
|
|
field='latency'
|
|
label={t('延迟 (秒)')}
|
|
value={editForm.latency}
|
|
onChange={(v) => handleFieldChange('latency', v)}
|
|
min={0}
|
|
step={0.01}
|
|
/>
|
|
<Form.Input
|
|
field='context_window'
|
|
label={t('上下文窗口')}
|
|
value={editForm.context_window}
|
|
onChange={(v) => handleFieldChange('context_window', v)}
|
|
placeholder='e.g. 128K'
|
|
/>
|
|
<Form.Select
|
|
field='type'
|
|
label={t('模型类型')}
|
|
value={editForm.type}
|
|
onChange={(v) => handleFieldChange('type', v)}
|
|
>
|
|
<Form.Select.Option value='reasoning'>
|
|
{t('推理模型')}
|
|
</Form.Select.Option>
|
|
<Form.Select.Option value='non-reasoning'>
|
|
{t('通用模型')}
|
|
</Form.Select.Option>
|
|
</Form.Select>
|
|
<Form.Select
|
|
field='openness'
|
|
label={t('开放性')}
|
|
value={editForm.openness}
|
|
onChange={(v) => handleFieldChange('openness', v)}
|
|
>
|
|
<Form.Select.Option value='proprietary'>
|
|
{t('闭源')}
|
|
</Form.Select.Option>
|
|
<Form.Select.Option value='open-weights'>
|
|
{t('开放权重')}
|
|
</Form.Select.Option>
|
|
</Form.Select>
|
|
<Form.Select
|
|
field='category'
|
|
label={t('类别')}
|
|
value={editForm.category}
|
|
onChange={(v) => handleFieldChange('category', v)}
|
|
>
|
|
<Form.Select.Option value='language'>
|
|
{t('语言模型')}
|
|
</Form.Select.Option>
|
|
<Form.Select.Option value='image'>
|
|
{t('图像模型')}
|
|
</Form.Select.Option>
|
|
<Form.Select.Option value='video'>
|
|
{t('视频模型')}
|
|
</Form.Select.Option>
|
|
</Form.Select>
|
|
</div>
|
|
</Form>
|
|
</Modal>
|
|
</Card>
|
|
);
|
|
}
|