345 lines
12 KiB
TypeScript
345 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Card, Button, Form, Input, Select, Space, message, Tabs, Slider, Typography, Divider, Row, Col, Tag, Modal, Tooltip, Switch } from 'antd';
|
|
import {
|
|
RobotOutlined,
|
|
AudioOutlined,
|
|
SettingOutlined,
|
|
SafetyCertificateOutlined,
|
|
InfoCircleOutlined,
|
|
CheckCircleOutlined,
|
|
ThunderboltOutlined,
|
|
PlusOutlined,
|
|
EditOutlined,
|
|
DeleteOutlined,
|
|
StarOutlined,
|
|
StarFilled,
|
|
SaveOutlined
|
|
} from '@ant-design/icons';
|
|
import { api } from '../api';
|
|
import ListTable from '../components/ListTable/ListTable';
|
|
import DetailDrawer from '../components/DetailDrawer/DetailDrawer';
|
|
|
|
const { Option } = Select;
|
|
const { Title, Text } = Typography;
|
|
|
|
const ModelManage: React.FC = () => {
|
|
const [loading, setLoading] = useState(false);
|
|
const [data, setData] = useState<any[]>([]);
|
|
const [vendors, setVendors] = useState<any[]>([]);
|
|
const [activeTab, setActiveTab] = useState('llm');
|
|
const [isDrawerVisible, setIsDrawerVisible] = useState(false);
|
|
const [editingItem, setEditingItem] = useState<any>(null);
|
|
const [testLoading, setTestLoading] = useState(false);
|
|
const [form] = Form.useForm();
|
|
|
|
const fetchModels = async (type: string) => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await api.listAIModels(type);
|
|
setData(res);
|
|
} catch (e) {
|
|
message.error('加载模型列表失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchVendors = async () => {
|
|
try {
|
|
const res = await api.listDictItems('MODEL_VENDOR');
|
|
setVendors(res);
|
|
} catch (e) {
|
|
console.error('获取供应商码表失败');
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchModels(activeTab);
|
|
}, [activeTab]);
|
|
|
|
useEffect(() => {
|
|
fetchVendors();
|
|
}, []);
|
|
|
|
const handleAdd = () => {
|
|
setEditingItem(null);
|
|
form.resetFields();
|
|
form.setFieldsValue({
|
|
model_type: activeTab,
|
|
status: 1,
|
|
is_default: data.length === 0,
|
|
api_path: activeTab === 'llm' ? '/chat/completions' : '',
|
|
temperature: 0.7,
|
|
top_p: 0.9
|
|
});
|
|
setIsDrawerVisible(true);
|
|
};
|
|
|
|
const handleEdit = (record: any) => {
|
|
setEditingItem(record);
|
|
const formData = { ...record };
|
|
if (record.config) {
|
|
formData.temperature = record.config.temperature;
|
|
formData.top_p = record.config.top_p;
|
|
}
|
|
form.setFieldsValue(formData);
|
|
setIsDrawerVisible(true);
|
|
};
|
|
|
|
const handleDelete = (id: number) => {
|
|
Modal.confirm({
|
|
title: '确认删除',
|
|
content: '删除后该模型配置将无法在业务中使用。',
|
|
onOk: async () => {
|
|
await api.deleteAIModel(id);
|
|
message.success('已删除');
|
|
fetchModels(activeTab);
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleSetDefault = async (record: any) => {
|
|
try {
|
|
await api.updateAIModel(record.model_id, { ...record, is_default: true });
|
|
message.success(`${record.model_name} 已设为默认`);
|
|
fetchModels(activeTab);
|
|
} catch (e) {
|
|
message.error('设置失败');
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
const values = await form.validateFields();
|
|
const payload = {
|
|
...values,
|
|
model_type: activeTab,
|
|
config: activeTab === 'llm' ? {
|
|
temperature: values.temperature,
|
|
top_p: values.top_p
|
|
} : {}
|
|
};
|
|
|
|
try {
|
|
if (editingItem) {
|
|
await api.updateAIModel(editingItem.model_id, payload);
|
|
} else {
|
|
await api.createAIModel(payload);
|
|
}
|
|
setIsDrawerVisible(false);
|
|
fetchModels(activeTab);
|
|
message.success('配置已保存');
|
|
} catch (e: any) {
|
|
message.error('保存失败: ' + e.message);
|
|
}
|
|
};
|
|
|
|
const handleTestConnection = () => {
|
|
setTestLoading(true);
|
|
setTimeout(() => {
|
|
message.success('连接测试成功!模型响应正常。');
|
|
setTestLoading(false);
|
|
}, 1500);
|
|
};
|
|
|
|
const getVendorLabel = (value: string) => {
|
|
return vendors.find(v => v.item_value === value)?.item_label || value;
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: '模型名称',
|
|
dataIndex: 'model_name',
|
|
key: 'model_name',
|
|
render: (t: string, r: any) => (
|
|
<Space>
|
|
<span style={{ fontWeight: 600 }}>{t}</span>
|
|
{r.is_default && <Tag color="blue">默认使用</Tag>}
|
|
{r.status === 0 && <Tag>已禁用</Tag>}
|
|
</Space>
|
|
)
|
|
},
|
|
{
|
|
title: '提供商',
|
|
dataIndex: 'provider',
|
|
key: 'provider',
|
|
render: (v: string) => <Tag color="default">{getVendorLabel(v)}</Tag>
|
|
},
|
|
{ title: 'Base URL', dataIndex: 'base_url', key: 'base_url', ellipsis: true },
|
|
{
|
|
title: '操作',
|
|
key: 'action',
|
|
width: 200,
|
|
render: (_: any, r: any) => (
|
|
<Space size="middle">
|
|
<Tooltip title={r.is_default ? "当前已是默认" : "设为默认"}>
|
|
<Button
|
|
type="text"
|
|
icon={r.is_default ? <StarFilled style={{color: '#faad14'}} /> : <StarOutlined />}
|
|
onClick={() => handleSetDefault(r)}
|
|
disabled={r.is_default || r.status === 0}
|
|
/>
|
|
</Tooltip>
|
|
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(r)}>编辑</Button>
|
|
{!r.is_default && (
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(r.model_id)}>删除</Button>
|
|
)}
|
|
</Space>
|
|
)
|
|
}
|
|
];
|
|
|
|
const renderFormContent = () => (
|
|
<Form form={form} layout="vertical" style={{ padding: '0 8px' }}>
|
|
<div style={{ marginBottom: 24 }}>
|
|
<Space size="middle">
|
|
<div style={{ background: activeTab === 'llm' ? '#e6f7ff' : '#fff7e6', padding: '12px', borderRadius: '8px' }}>
|
|
{activeTab === 'llm' ? <RobotOutlined style={{ fontSize: '24px', color: '#1890ff' }} /> : <AudioOutlined style={{ fontSize: '24px', color: '#fa8c16' }} />}
|
|
</div>
|
|
<div>
|
|
<Title level={4} style={{ margin: 0 }}>{activeTab === 'llm' ? 'AI 总结模型配置' : '语音识别 (ASR) 配置'}</Title>
|
|
<Text type="secondary">{activeTab === 'llm' ? '选择用于生成会议纪要的大语言模型' : '转录模型与参数配置'}</Text>
|
|
</div>
|
|
</Space>
|
|
</div>
|
|
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item name="provider" label="模型提供商" rules={[{ required: true, message: '请选择提供商' }]}>
|
|
<Select size="large" placeholder="请选择提供商">
|
|
{vendors.map(v => (
|
|
<Option key={v.dict_item_id} value={v.item_value}>{v.item_label}</Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item name="model_name" label="模型名称 (MODEL NAME)" rules={[{ required: true, message: '请输入模型标识' }]}>
|
|
<Input size="large" placeholder="例如: qwen-max" />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Form.Item name="api_key" label="API KEY">
|
|
<Input.Password placeholder="请输入 API Key" size="large" prefix={<ThunderboltOutlined style={{ color: '#bfbfbf' }} />} />
|
|
</Form.Item>
|
|
|
|
<Form.Item name="base_url" label="BASE URL (OPTIONAL)">
|
|
<Input placeholder="https://api.example.com/v1" size="large" />
|
|
</Form.Item>
|
|
|
|
{activeTab === 'llm' && (
|
|
<>
|
|
<Form.Item name="api_path" label="API PATH (OPTIONAL)">
|
|
<Input placeholder="/chat/completions" size="large" />
|
|
</Form.Item>
|
|
<Row gutter={32}>
|
|
<Col span={12}>
|
|
<Form.Item name="temperature" label="TEMPERATURE (0.7)">
|
|
<Slider min={0} max={2} step={0.1} marks={{0: '0', 1: '1', 2: '2'}} />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item name="top_p" label="TOP P (0.9)">
|
|
<Slider min={0} max={1} step={0.1} marks={{0: '0', 1: '1'}} />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
</>
|
|
)}
|
|
|
|
<Divider />
|
|
|
|
<Space size="large">
|
|
<Form.Item name="is_default" label="设为默认" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item name="status" label="状态" valuePropName="checked">
|
|
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
|
</Form.Item>
|
|
</Space>
|
|
|
|
{activeTab === 'llm' && (
|
|
<div style={{ marginTop: 16 }}>
|
|
<Button block size="large" icon={<CheckCircleOutlined />} onClick={handleTestConnection} loading={testLoading}>
|
|
测试连接
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</Form>
|
|
);
|
|
|
|
return (
|
|
<div className="settings-container" style={{ padding: '24px', background: '#f5f7f9', minHeight: '100vh' }}>
|
|
<Card variant="borderless" styles={{ body: { padding: 0 } }} style={{ borderRadius: '12px', overflow: 'hidden' }}>
|
|
<Tabs
|
|
activeKey={activeTab}
|
|
onChange={setActiveTab}
|
|
tabPosition="left"
|
|
style={{ minHeight: 'calc(100vh - 100px)' }}
|
|
className="system-settings-tabs"
|
|
items={[
|
|
{ key: 'general', label: <div style={{ padding: '8px 16px' }}><Space><SettingOutlined />通用设置</Space></div>, disabled: true },
|
|
{
|
|
key: 'llm',
|
|
label: <div style={{ padding: '8px 16px' }}><Space><RobotOutlined />AI 模型</Space></div>,
|
|
children: (
|
|
<div style={{ padding: '40px' }}>
|
|
<div style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<div>
|
|
<Title level={4} style={{margin: 0}}>大模型配置列表</Title>
|
|
<Text type="secondary">配置并管理用于会议总结的 LLM 服务</Text>
|
|
</div>
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增模型</Button>
|
|
</div>
|
|
<ListTable columns={columns} dataSource={data} rowKey="model_id" loading={loading} />
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
key: 'asr',
|
|
label: <div style={{ padding: '8px 16px' }}><Space><AudioOutlined />语音识别</Space></div>,
|
|
children: (
|
|
<div style={{ padding: '40px' }}>
|
|
<div style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<div>
|
|
<Title level={4} style={{margin: 0}}>语音转译模型列表</Title>
|
|
<Text type="secondary">管理用于音频转文字的 ASR 服务</Text>
|
|
</div>
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增配置</Button>
|
|
</div>
|
|
<ListTable columns={columns} dataSource={data} rowKey="model_id" loading={loading} />
|
|
</div>
|
|
)
|
|
},
|
|
{ key: 'voiceprint', label: <div style={{ padding: '8px 16px' }}><Space><ThunderboltOutlined />声纹识别</Space></div>, disabled: true },
|
|
{ key: 'security', label: <div style={{ padding: '8px 16px' }}><Space><SafetyCertificateOutlined />安全设置</Space></div>, disabled: true },
|
|
{ key: 'about', label: <div style={{ padding: '8px 16px' }}><Space><InfoCircleOutlined />关于系统</Space></div>, disabled: true },
|
|
]}
|
|
/>
|
|
</Card>
|
|
|
|
<DetailDrawer
|
|
visible={isDrawerVisible}
|
|
onClose={() => setIsDrawerVisible(false)}
|
|
title={{ text: editingItem ? '编辑配置' : '新增模型配置' }}
|
|
width={600}
|
|
headerActions={[
|
|
{
|
|
key: 'save',
|
|
label: activeTab === 'asr' ? '保存并重载 ASR' : '保存配置',
|
|
type: 'primary',
|
|
onClick: handleSave,
|
|
style: activeTab === 'asr' ? { background: '#fa541c', borderColor: '#fa541c' } : {}
|
|
} as any
|
|
]}
|
|
>
|
|
<div style={{ padding: '24px' }}>
|
|
{renderFormContent()}
|
|
</div>
|
|
</DetailDrawer>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ModelManage;
|