TuringToken/web/src/components/settings/BenchmarkDataManager.jsx

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>
);
}