nex_basse/frontend/src/pages/ModelManage.tsx

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;