fix:初始化
This commit is contained in:
468
src/pages/ModelSettings.tsx
Normal file
468
src/pages/ModelSettings.tsx
Normal file
@@ -0,0 +1,468 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Form, Input, InputNumber, Button, Space, Divider, Select, Spin, Tag, App } from 'antd';
|
||||
import { ApiOutlined, SaveOutlined, SyncOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { OllamaService } from '../utils/ollama';
|
||||
import { useOllama } from '../contexts/OllamaContext';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface ModelInfo {
|
||||
name: string;
|
||||
size?: number;
|
||||
modified?: string;
|
||||
}
|
||||
|
||||
const ModelSettings: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [detectingModels, setDetectingModels] = useState(false);
|
||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||
const [connectionStatus, setConnectionStatus] = useState<'unknown' | 'success' | 'error'>('unknown');
|
||||
const [currentModel, setCurrentModel] = useState<string>('');
|
||||
const { status, refreshModels } = useOllama();
|
||||
const { message: messageApi } = App.useApp();
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
// 从 localStorage 加载配置
|
||||
const configData = localStorage.getItem('ai_system_config');
|
||||
const config = configData ? JSON.parse(configData) : {
|
||||
ollamaUrl: 'http://localhost:11434',
|
||||
model: '',
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
maxTokens: 2000
|
||||
};
|
||||
|
||||
form.setFieldsValue(config);
|
||||
setCurrentModel(config.model || '');
|
||||
|
||||
// 如果已连接且有模型,自动显示当前状态
|
||||
if (status.isConnected && status.availableModels.length > 0) {
|
||||
setAvailableModels(status.availableModels);
|
||||
setConnectionStatus('success');
|
||||
}
|
||||
};
|
||||
loadConfig();
|
||||
}, [form, status]);
|
||||
|
||||
const handleDetectModels = async () => {
|
||||
setDetectingModels(true);
|
||||
setConnectionStatus('unknown');
|
||||
try {
|
||||
const values = await form.validateFields(['ollamaUrl']);
|
||||
const ollamaService = new OllamaService(values);
|
||||
|
||||
// 测试连接
|
||||
const isConnected = await ollamaService.testConnection();
|
||||
if (!isConnected) {
|
||||
setConnectionStatus('error');
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: '无法连接到 Ollama 服务,请检查服务地址和状态',
|
||||
});
|
||||
setAvailableModels([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取模型列表
|
||||
const models = await ollamaService.getAvailableModelsWithInfo();
|
||||
setAvailableModels(models);
|
||||
setConnectionStatus('success');
|
||||
|
||||
if (models.length === 0) {
|
||||
messageApi.open({
|
||||
type: 'warning',
|
||||
content: '未检测到已安装的模型,请先使用 ollama pull 命令安装模型',
|
||||
});
|
||||
} else {
|
||||
messageApi.open({
|
||||
type: 'success',
|
||||
content: `成功检测到 ${models.length} 个已安装模型`,
|
||||
});
|
||||
|
||||
// 如果当前模型不在列表中,清空选择
|
||||
if (currentModel && !models.find(m => m.name === currentModel)) {
|
||||
form.setFieldValue('model', undefined);
|
||||
setCurrentModel('');
|
||||
messageApi.open({
|
||||
type: 'warning',
|
||||
content: '当前选择的模型未在本地安装,请重新选择',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新全局状态
|
||||
await refreshModels();
|
||||
} catch (error) {
|
||||
setConnectionStatus('error');
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: '模型检测失败,请检查 Ollama 服务状态',
|
||||
});
|
||||
setAvailableModels([]);
|
||||
} finally {
|
||||
setDetectingModels(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setTesting(true);
|
||||
setConnectionStatus('unknown');
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const ollamaService = new OllamaService(values);
|
||||
const isConnected = await ollamaService.testConnection();
|
||||
|
||||
if (isConnected) {
|
||||
setConnectionStatus('success');
|
||||
messageApi.open({
|
||||
type: 'success',
|
||||
content: 'Ollama 服务连接成功!',
|
||||
});
|
||||
await refreshModels();
|
||||
} else {
|
||||
setConnectionStatus('error');
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: 'Ollama 服务连接失败,请检查服务地址',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setConnectionStatus('error');
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: '连接测试失败,请检查配置',
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelChange = (value: string) => {
|
||||
setCurrentModel(value);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 验证选择的模型是否可用
|
||||
if (currentModel && availableModels.length > 0) {
|
||||
const modelExists = availableModels.find(m => m.name === currentModel);
|
||||
if (!modelExists) {
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: '请选择本地已安装的模型,或点击"检测模型"刷新列表',
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保包含当前选择的模型
|
||||
const configToSave = {
|
||||
...values,
|
||||
model: currentModel || values.model
|
||||
};
|
||||
|
||||
// 保存到 localStorage
|
||||
localStorage.setItem('ai_system_config', JSON.stringify(configToSave));
|
||||
|
||||
// 显示保存成功提示
|
||||
messageApi.open({
|
||||
type: 'success',
|
||||
content: '配置保存成功!',
|
||||
});
|
||||
|
||||
// 更新当前模型显示
|
||||
setCurrentModel(configToSave.model);
|
||||
|
||||
// 刷新全局状态
|
||||
await refreshModels();
|
||||
} catch (error) {
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: '配置保存失败,请检查输入',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatModelSize = (bytes?: number) => {
|
||||
if (!bytes) return '未知';
|
||||
const gb = bytes / (1024 * 1024 * 1024);
|
||||
return `${gb.toFixed(2)} GB`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 600, color: '#262626' }}>模型设置</h2>
|
||||
<p style={{ margin: '8px 0 0 0', color: '#8c8c8c', fontSize: '14px' }}>配置本地 Ollama 服务和 AI 模型参数</p>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'white',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #f0f0f0',
|
||||
marginBottom: '16px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '16px 24px',
|
||||
background: '#fafafa',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: connectionStatus === 'success' ? '#52c41a' : connectionStatus === 'error' ? '#ff4d4f' : '#faad14'
|
||||
}} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 500, color: '#262626' }}>
|
||||
{connectionStatus === 'success' ? '服务正常' : connectionStatus === 'error' ? '服务异常' : '未检测'}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>
|
||||
{availableModels.length > 0
|
||||
? `已安装 ${availableModels.length} 个模型`
|
||||
: connectionStatus === 'success'
|
||||
? '未安装模型'
|
||||
: '请先连接服务'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card bordered={false} style={{ boxShadow: 'none' }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
ollamaUrl: 'http://localhost:11434',
|
||||
model: '',
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
maxTokens: 2000
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
服务配置
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>Ollama 服务地址</span>}
|
||||
name="ollamaUrl"
|
||||
rules={[{ required: true, message: '请输入Ollama服务地址' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="http://localhost:11434"
|
||||
style={{ borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ marginBottom: '16px' }}>
|
||||
<Button
|
||||
icon={<ApiOutlined />}
|
||||
onClick={handleTestConnection}
|
||||
loading={testing}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
测试连接
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={handleDetectModels}
|
||||
loading={detectingModels}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
检测模型
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
模型选择
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<span style={{ color: '#595959' }}>AI 模型</span>
|
||||
{availableModels.length > 0 && (
|
||||
<Tag color="success">{availableModels.length} 个可用</Tag>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
name="model"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: '请选择AI模型'
|
||||
}]}
|
||||
tooltip="只能选择本地已安装的模型"
|
||||
>
|
||||
<Select
|
||||
placeholder={availableModels.length === 0 ? "请先点击检测模型" : "选择AI模型"}
|
||||
showSearch
|
||||
allowClear
|
||||
onChange={handleModelChange}
|
||||
notFoundContent={detectingModels ? <Spin size="small" /> : "未检测到可用模型"}
|
||||
disabled={availableModels.length === 0}
|
||||
style={{ borderRadius: '6px' }}
|
||||
>
|
||||
{availableModels.map(model => (
|
||||
<Option key={model.name} value={model.name}>
|
||||
<Space>
|
||||
<span>{model.name}</span>
|
||||
{model.size && (
|
||||
<span style={{ color: '#8c8c8c', fontSize: '12px' }}>
|
||||
({formatModelSize(model.size)})
|
||||
</span>
|
||||
)}
|
||||
</Space>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{currentModel && (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<Space>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
<span style={{ color: '#52c41a', fontSize: '14px' }}>
|
||||
当前使用模型: {currentModel}
|
||||
</span>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
生成参数
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>温度</span>}
|
||||
name="temperature"
|
||||
rules={[{ required: true, message: '请输入温度值' }]}
|
||||
tooltip="控制生成文本的随机性"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
precision={1}
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>Top P</span>}
|
||||
name="topP"
|
||||
rules={[{ required: true, message: '请输入Top P值' }]}
|
||||
tooltip="控制生成文本的多样性"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
precision={1}
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span style={{ color: '#595959' }}>最大 Tokens</span>}
|
||||
name="maxTokens"
|
||||
rules={[{ required: true, message: '请输入最大生成长度' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={100}
|
||||
max={8000}
|
||||
step={100}
|
||||
style={{ width: '100%', borderRadius: '6px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
loading={loading}
|
||||
size="large"
|
||||
style={{ borderRadius: '6px', minWidth: '120px' }}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Divider style={{ margin: '32px 0' }} />
|
||||
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: '#262626',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
使用说明
|
||||
</div>
|
||||
<ul style={{
|
||||
margin: 0,
|
||||
paddingLeft: '20px',
|
||||
color: '#595959',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.8'
|
||||
}}>
|
||||
<li>确保 Ollama 服务正在运行(默认端口 11434)</li>
|
||||
<li>点击"测试连接"验证 Ollama 服务是否正常</li>
|
||||
<li>点击"检测模型"获取本地已安装的模型列表</li>
|
||||
<li>模型选择限制为只能使用本地已安装的模型</li>
|
||||
<li>如需新模型,请使用命令:ollama pull 模型名</li>
|
||||
<li>推荐:qwen3:8b起步</li>
|
||||
<li>温度参数控制生成文本的随机性,建议值为 0.7</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelSettings;
|
||||
Reference in New Issue
Block a user