feat: add new SVG assets for file icons, LLM providers, and UI elements

This commit is contained in:
2025-10-21 16:41:22 +08:00
parent 6ca5e235b4
commit 504156fb95
220 changed files with 17943 additions and 23 deletions

View File

@@ -0,0 +1,12 @@
import { Box, Typography } from "@mui/material";
function LLMFactoryCard() {
return (
<Box>
<Typography variant="h6" gutterBottom>
</Typography>
</Box>
)
}
export default LLMFactoryCard;

View File

@@ -0,0 +1,601 @@
import React, { useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
Box,
Typography,
Alert,
CircularProgress,
IconButton,
InputAdornment,
} from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material';
import { useForm, Controller } from 'react-hook-form';
// 接口定义
export interface ApiKeyFormData {
api_key: string;
base_url?: string;
group_id?: string;
}
export interface AzureOpenAIFormData {
api_key: string;
azure_endpoint: string;
api_version: string;
deployment_name: string;
}
export interface BedrockFormData {
access_key_id: string;
secret_access_key: string;
region: string;
}
export interface OllamaFormData {
base_url: string;
model_name: string;
}
// Bedrock 区域列表
const BEDROCK_REGIONS = [
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
'eu-west-1', 'eu-west-2', 'eu-central-1', 'eu-north-1',
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2',
'ap-south-1', 'ca-central-1', 'sa-east-1'
];
// 通用 API Key 对话框
interface ApiKeyDialogProps {
open: boolean;
onClose: () => void;
onSubmit: (data: ApiKeyFormData) => Promise<void>;
loading: boolean;
factoryName: string;
initialData?: Partial<ApiKeyFormData>;
editMode?: boolean;
}
/**
* 通用 API Key 对话框
*/
export const ApiKeyDialog: React.FC<ApiKeyDialogProps> = ({
open,
onClose,
onSubmit,
loading,
factoryName,
initialData,
editMode = false
}) => {
const { control, handleSubmit, reset, formState: { errors } } = useForm<ApiKeyFormData>({
defaultValues: {
api_key: '',
base_url: '',
group_id: ''
}
});
const [showApiKey, setShowApiKey] = React.useState(false);
useEffect(() => {
if (open && initialData) {
reset(initialData);
} else if (open) {
reset({ api_key: '', base_url: '', group_id: '' });
}
}, [open, initialData, reset]);
const handleFormSubmit = async (data: ApiKeyFormData) => {
try {
await onSubmit(data);
onClose();
} catch (error) {
console.error('提交失败:', error);
}
};
const needsBaseUrl = ['OpenAI', 'AzureOpenAI'].includes(factoryName);
const needsGroupId = factoryName.toLowerCase() === 'minimax';
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>
{editMode ? `编辑 ${factoryName}` : `配置 ${factoryName}`}
</DialogTitle>
<DialogContent>
<Box component="form" sx={{ mt: 2 }}>
<Controller
name="api_key"
control={control}
rules={{ required: 'API Key 是必填项' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="API Key"
type={showApiKey ? 'text' : 'password'}
error={!!errors.api_key}
helperText={errors.api_key?.message}
margin="normal"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowApiKey(!showApiKey)}
edge="end"
>
{showApiKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
)}
/>
{needsBaseUrl && (
<Controller
name="base_url"
control={control}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="Base URL"
placeholder="https://api.openai.com/v1"
margin="normal"
helperText="可选,自定义 API 端点"
/>
)}
/>
)}
{needsGroupId && (
<Controller
name="group_id"
control={control}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="Group ID"
margin="normal"
helperText="Minimax 专用的 Group ID"
/>
)}
/>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={loading}>
</Button>
<Button
onClick={handleSubmit(handleFormSubmit)}
variant="contained"
disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null}
>
{editMode ? '更新' : '保存'}
</Button>
</DialogActions>
</Dialog>
);
};
// Azure OpenAI 对话框
interface AzureOpenAIDialogProps {
open: boolean;
onClose: () => void;
onSubmit: (data: AzureOpenAIFormData) => Promise<void>;
loading: boolean;
initialData?: Partial<AzureOpenAIFormData>;
editMode?: boolean;
}
/**
* Azure OpenAI 对话框
*/
export const AzureOpenAIDialog: React.FC<AzureOpenAIDialogProps> = ({
open,
onClose,
onSubmit,
loading,
initialData,
editMode = false
}) => {
const { control, handleSubmit, reset, formState: { errors } } = useForm<AzureOpenAIFormData>({
defaultValues: {
api_key: '',
azure_endpoint: '',
api_version: '2023-12-01-preview',
deployment_name: ''
}
});
const [showApiKey, setShowApiKey] = React.useState(false);
useEffect(() => {
if (open && initialData) {
reset(initialData);
} else if (open) {
reset({
api_key: '',
azure_endpoint: '',
api_version: '2023-12-01-preview',
deployment_name: ''
});
}
}, [open, initialData, reset]);
const handleFormSubmit = async (data: AzureOpenAIFormData) => {
try {
await onSubmit(data);
onClose();
} catch (error) {
console.error('提交失败:', error);
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>
{editMode ? '编辑 Azure OpenAI' : '配置 Azure OpenAI'}
</DialogTitle>
<DialogContent>
<Box component="form" sx={{ mt: 2 }}>
<Controller
name="api_key"
control={control}
rules={{ required: 'API Key 是必填项' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="API Key"
type={showApiKey ? 'text' : 'password'}
error={!!errors.api_key}
helperText={errors.api_key?.message}
margin="normal"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowApiKey(!showApiKey)}
edge="end"
>
{showApiKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
)}
/>
<Controller
name="azure_endpoint"
control={control}
rules={{ required: 'Azure Endpoint 是必填项' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="Azure Endpoint"
placeholder="https://your-resource.openai.azure.com"
error={!!errors.azure_endpoint}
helperText={errors.azure_endpoint?.message}
margin="normal"
/>
)}
/>
<Controller
name="api_version"
control={control}
rules={{ required: 'API Version 是必填项' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="API Version"
error={!!errors.api_version}
helperText={errors.api_version?.message}
margin="normal"
/>
)}
/>
<Controller
name="deployment_name"
control={control}
rules={{ required: 'Deployment Name 是必填项' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="Deployment Name"
error={!!errors.deployment_name}
helperText={errors.deployment_name?.message}
margin="normal"
/>
)}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={loading}>
</Button>
<Button
onClick={handleSubmit(handleFormSubmit)}
variant="contained"
disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null}
>
{editMode ? '更新' : '保存'}
</Button>
</DialogActions>
</Dialog>
);
};
// AWS Bedrock 对话框
interface BedrockDialogProps {
open: boolean;
onClose: () => void;
onSubmit: (data: BedrockFormData) => Promise<void>;
loading: boolean;
initialData?: Partial<BedrockFormData>;
editMode?: boolean;
}
/**
* AWS Bedrock 对话框
*/
export const BedrockDialog: React.FC<BedrockDialogProps> = ({
open,
onClose,
onSubmit,
loading,
initialData,
editMode = false
}) => {
const { control, handleSubmit, reset, formState: { errors } } = useForm<BedrockFormData>({
defaultValues: {
access_key_id: '',
secret_access_key: '',
region: 'us-east-1'
}
});
const [showSecretKey, setShowSecretKey] = React.useState(false);
useEffect(() => {
if (open && initialData) {
reset(initialData);
} else if (open) {
reset({
access_key_id: '',
secret_access_key: '',
region: 'us-east-1'
});
}
}, [open, initialData, reset]);
const handleFormSubmit = async (data: BedrockFormData) => {
try {
await onSubmit(data);
onClose();
} catch (error) {
console.error('提交失败:', error);
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>
{editMode ? '编辑 AWS Bedrock' : '配置 AWS Bedrock'}
</DialogTitle>
<DialogContent>
<Box component="form" sx={{ mt: 2 }}>
<Controller
name="access_key_id"
control={control}
rules={{ required: 'Access Key ID 是必填项' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="Access Key ID"
error={!!errors.access_key_id}
helperText={errors.access_key_id?.message}
margin="normal"
/>
)}
/>
<Controller
name="secret_access_key"
control={control}
rules={{ required: 'Secret Access Key 是必填项' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="Secret Access Key"
type={showSecretKey ? 'text' : 'password'}
error={!!errors.secret_access_key}
helperText={errors.secret_access_key?.message}
margin="normal"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowSecretKey(!showSecretKey)}
edge="end"
>
{showSecretKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
)}
/>
<Controller
name="region"
control={control}
rules={{ required: 'Region 是必填项' }}
render={({ field }) => (
<FormControl fullWidth margin="normal" error={!!errors.region}>
<InputLabel>Region</InputLabel>
<Select {...field} label="Region">
{BEDROCK_REGIONS.map((region) => (
<MenuItem key={region} value={region}>
{region}
</MenuItem>
))}
</Select>
{errors.region && (
<Typography variant="caption" color="error" sx={{ mt: 1, ml: 2 }}>
{errors.region.message}
</Typography>
)}
</FormControl>
)}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={loading}>
</Button>
<Button
onClick={handleSubmit(handleFormSubmit)}
variant="contained"
disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null}
>
{editMode ? '更新' : '保存'}
</Button>
</DialogActions>
</Dialog>
);
};
interface OllamaDialogProps {
open: boolean;
onClose: () => void;
onSubmit: (data: OllamaFormData) => Promise<void>;
loading: boolean;
initialData?: Partial<OllamaFormData>;
editMode?: boolean;
}
/**
* Ollama 对话框
*/
export const OllamaDialog: React.FC<OllamaDialogProps> = ({
open,
onClose,
onSubmit,
loading,
initialData,
editMode = false
}) => {
const { control, handleSubmit, reset, formState: { errors } } = useForm<OllamaFormData>({
defaultValues: {
base_url: 'http://localhost:11434',
model_name: ''
}
});
useEffect(() => {
if (open && initialData) {
reset(initialData);
} else if (open) {
reset({
base_url: 'http://localhost:11434',
model_name: ''
});
}
}, [open, initialData, reset]);
const handleFormSubmit = async (data: OllamaFormData) => {
try {
await onSubmit(data);
onClose();
} catch (error) {
console.error('提交失败:', error);
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>
{editMode ? '编辑 Ollama' : '配置 Ollama'}
</DialogTitle>
<DialogContent>
<Alert severity="info" sx={{ mb: 2 }}>
Ollama URL 访
</Alert>
<Box component="form" sx={{ mt: 2 }}>
<Controller
name="base_url"
control={control}
rules={{ required: 'Base URL 是必填项' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="Base URL"
error={!!errors.base_url}
helperText={errors.base_url?.message || 'Ollama 服务的访问地址'}
margin="normal"
/>
)}
/>
<Controller
name="model_name"
control={control}
rules={{ required: '模型名称是必填项' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
label="模型名称"
placeholder="llama2, codellama, mistral 等"
error={!!errors.model_name}
helperText={errors.model_name?.message || '要使用的 Ollama 模型名称'}
margin="normal"
/>
)}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={loading}>
</Button>
<Button
onClick={handleSubmit(handleFormSubmit)}
variant="contained"
disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null}
>
{editMode ? '更新' : '保存'}
</Button>
</DialogActions>
</Dialog>
);
};

View File

@@ -0,0 +1,288 @@
import { useState, useCallback } from 'react';
import { useMessage } from '@/hooks/useSnackbar';
import userService from '@/services/user_service';
import logger from '@/utils/logger';
import type {
ApiKeyFormData,
AzureOpenAIFormData,
BedrockFormData,
OllamaFormData,
} from '../components/ModelDialogs';
// 对话框状态管理 hook
export const useDialogState = () => {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [editMode, setEditMode] = useState(false);
const [initialData, setInitialData] = useState<any>(null);
const openDialog = useCallback((data?: any, isEdit = false) => {
setInitialData(data);
setEditMode(isEdit);
setOpen(true);
}, []);
const closeDialog = useCallback(() => {
setOpen(false);
setInitialData(null);
setEditMode(false);
}, []);
return {
open,
loading,
editMode,
initialData,
setLoading,
openDialog,
closeDialog,
};
};
// API Key 对话框管理
export const useApiKeyDialog = () => {
const dialogState = useDialogState();
const showMessage = useMessage();
const [factoryName, setFactoryName] = useState('');
const openApiKeyDialog = useCallback((factory: string, data?: Partial<ApiKeyFormData>, isEdit = false) => {
setFactoryName(factory);
dialogState.openDialog(data, isEdit);
}, [dialogState]);
const submitApiKey = useCallback(async (data: ApiKeyFormData) => {
dialogState.setLoading(true);
try {
await userService.set_api_key({
factory_name: factoryName,
model_name: '', // 根据实际需求调整
// api_key: data.api_key,
...data
});
showMessage.success('API Key 配置成功');
dialogState.closeDialog();
} catch (error) {
logger.error('API Key 配置失败:', error);
showMessage.error('API Key 配置失败');
throw error;
} finally {
dialogState.setLoading(false);
}
}, [factoryName, dialogState]);
return {
...dialogState,
factoryName,
openApiKeyDialog,
submitApiKey,
};
};
// Azure OpenAI 对话框管理
export const useAzureOpenAIDialog = () => {
const dialogState = useDialogState();
const showMessage = useMessage();
const submitAzureOpenAI = useCallback(async (data: AzureOpenAIFormData) => {
dialogState.setLoading(true);
try {
// 调用 Azure OpenAI 特定的 API
await userService.set_api_key({
factory_name: 'AzureOpenAI',
model_name: data.deployment_name,
api_key: data.api_key,
// azure_endpoint: data.azure_endpoint,
// api_version: data.api_version,
});
showMessage.success('Azure OpenAI 配置成功');
dialogState.closeDialog();
} catch (error) {
logger.error('Azure OpenAI 配置失败:', error);
showMessage.error('Azure OpenAI 配置失败');
throw error;
} finally {
dialogState.setLoading(false);
}
}, [dialogState]);
return {
...dialogState,
submitAzureOpenAI,
};
};
// AWS Bedrock 对话框管理
export const useBedrockDialog = () => {
const dialogState = useDialogState();
const showMessage = useMessage();
const submitBedrock = useCallback(async (data: BedrockFormData) => {
dialogState.setLoading(true);
try {
// 调用 Bedrock 特定的 API
await userService.set_api_key({
factory_name: 'Bedrock',
model_name: '',
api_key: '', // Bedrock 使用 access key
// access_key_id: data.access_key_id,
// secret_access_key: data.secret_access_key,
// region: data.region,
});
showMessage.success('AWS Bedrock 配置成功');
dialogState.closeDialog();
} catch (error) {
logger.error('AWS Bedrock 配置失败:', error);
showMessage.error('AWS Bedrock 配置失败');
throw error;
} finally {
dialogState.setLoading(false);
}
}, [dialogState]);
return {
...dialogState,
submitBedrock,
};
};
// Ollama 对话框管理
export const useOllamaDialog = () => {
const dialogState = useDialogState();
const showMessage = useMessage();
const submitOllama = useCallback(async (data: OllamaFormData) => {
dialogState.setLoading(true);
try {
// 调用添加 LLM 的 API
await userService.add_llm({
factory_name: 'Ollama',
model_name: data.model_name,
// base_url: data.base_url,
});
showMessage.success('Ollama 模型添加成功');
dialogState.closeDialog();
} catch (error) {
logger.error('Ollama 模型添加失败:', error);
showMessage.error('Ollama 模型添加失败');
throw error;
} finally {
dialogState.setLoading(false);
}
}, [dialogState]);
return {
...dialogState,
submitOllama,
};
};
// 删除操作管理
export const useDeleteOperations = () => {
const showMessage = useMessage();
const [loading, setLoading] = useState(false);
const deleteLlm = useCallback(async (factoryName: string, modelName: string) => {
setLoading(true);
try {
await userService.delete_llm({
factory_name: factoryName,
model_name: modelName,
});
showMessage.success('模型删除成功');
} catch (error) {
logger.error('模型删除失败:', error);
showMessage.error('模型删除失败');
throw error;
} finally {
setLoading(false);
}
}, []);
const deleteFactory = useCallback(async (factoryName: string) => {
setLoading(true);
try {
await userService.deleteFactory({
factory_name: factoryName,
});
showMessage.success('模型工厂删除成功');
} catch (error) {
logger.error('模型工厂删除失败:', error);
showMessage.error('模型工厂删除失败');
throw error;
} finally {
setLoading(false);
}
}, []);
return {
loading,
deleteLlm,
deleteFactory,
};
};
// 系统默认模型设置
export const useSystemModelSetting = () => {
const dialogState = useDialogState();
const showMessage = useMessage();
const submitSystemModelSetting = useCallback(async (data: { defaultModel: string }) => {
dialogState.setLoading(true);
try {
// 这里需要根据实际的 API 接口调整
// await userService.setSystemDefaultModel(data);
showMessage.success('系统默认模型设置成功');
dialogState.closeDialog();
} catch (error) {
logger.error('系统默认模型设置失败:', error);
showMessage.error('系统默认模型设置失败');
throw error;
} finally {
dialogState.setLoading(false);
}
}, [dialogState]);
return {
...dialogState,
submitSystemModelSetting,
};
};
// 统一的模型对话框管理器
export const useModelDialogs = () => {
const apiKeyDialog = useApiKeyDialog();
const azureDialog = useAzureOpenAIDialog();
const bedrockDialog = useBedrockDialog();
const ollamaDialog = useOllamaDialog();
const systemDialog = useSystemModelSetting();
const deleteOps = useDeleteOperations();
// 根据工厂类型打开对应的对话框
const openFactoryDialog = useCallback((factoryName: string, data?: any, isEdit = false) => {
switch (factoryName.toLowerCase()) {
case 'azureopenai':
azureDialog.openDialog(data, isEdit);
break;
case 'bedrock':
bedrockDialog.openDialog(data, isEdit);
break;
case 'ollama':
ollamaDialog.openDialog(data, isEdit);
break;
default:
// 默认使用 API Key 对话框
apiKeyDialog.openApiKeyDialog(factoryName, data, isEdit);
break;
}
}, [apiKeyDialog, azureDialog, bedrockDialog, ollamaDialog]);
return {
apiKeyDialog,
azureDialog,
bedrockDialog,
ollamaDialog,
systemDialog,
deleteOps,
openFactoryDialog,
};
};

View File

@@ -0,0 +1,119 @@
# Models Setting 页面功能分析
## 概述
Models Setting 页面是一个复杂的模型管理界面,主要用于管理 LLM 模型工厂和用户的个人模型配置。
## 主要功能模块
### 1. 核心数据结构
- **LLM Factory**: 模型工厂列表,包含各种 AI 服务提供商
- **My LLM**: 用户已配置的模型列表
- **Model Types**: 支持多种模型类型LLM、TEXT EMBEDDING、TEXT RE-RANK、TTS、SPEECH2TEXT、IMAGE2TEXT、MODERATION
### 2. 支持的模型工厂
根据参考代码,支持以下模型工厂:
- **OpenAI**: 通用 API Key 配置
- **Azure OpenAI**: 需要特殊配置的 Azure 服务
- **Google Cloud**: Google 云服务模型
- **AWS Bedrock**: Amazon 的 AI 服务
- **Ollama**: 本地部署模型
- **VolcEngine**: 火山引擎
- **Tencent HunYuan**: 腾讯混元
- **XunFei Spark**: 讯飞星火
- **Baidu YiYan**: 百度一言
- **Fish Audio**: 语音合成服务
- **Tencent Cloud**: 腾讯云
- **Langfuse**: 模型监控和分析
### 3. 主要组件结构
#### 3.1 主页面组件 (index.tsx)
- **ModelCard**: 模型工厂卡片组件
- 显示工厂信息、支持的模型类型标签
- 提供配置按钮API Key 或添加模型)
- 显示/隐藏模型列表功能
- 删除工厂功能
- **模型列表**: 展示每个工厂下的具体模型
- 模型名称和类型
- 编辑和删除操作
#### 3.2 配置模态框组件
每个模型工厂都有对应的配置模态框:
- **api-key-modal**: 通用 API Key 配置
- API Key 输入
- Base URL 配置OpenAI 等)
- Group ID 配置Minimax 等)
- **azure-openai-modal**: Azure OpenAI 特殊配置
- **google-modal**: Google Cloud 配置
- **bedrock-modal**: AWS Bedrock 配置
- **ollama-modal**: Ollama 本地模型配置
- **volcengine-modal**: 火山引擎配置
- **hunyuan-modal**: 腾讯混元配置
- **spark-modal**: 讯飞星火配置
- **yiyan-modal**: 百度一言配置
- **fish-audio-modal**: Fish Audio 配置
- **next-tencent-modal**: 腾讯云配置
#### 3.3 系统设置
- **system-model-setting-modal**: 系统默认模型设置
- **langfuse**: 模型监控和分析配置
### 4. 状态管理 (hooks.ts)
提供统一的状态管理 hooks
- **useSubmitApiKey**: API Key 配置状态管理
- **useSubmitAzure**: Azure 配置状态管理
- **useSubmitGoogle**: Google 配置状态管理
- **useSubmitBedrock**: Bedrock 配置状态管理
- **useSubmitOllama**: Ollama 配置状态管理
- **useSubmitSystemModelSetting**: 系统模型设置状态管理
- **useHandleDeleteLlm**: 删除模型操作
- **useHandleDeleteFactory**: 删除工厂操作
### 5. 常量配置 (constant.ts)
- **BedrockRegionList**: AWS Bedrock 支持的区域列表
## 用户交互流程
### 5.1 添加模型工厂
1. 用户点击"添加模型"或"API-Key"按钮
2. 根据工厂类型打开对应的配置模态框
3. 用户填写必要的配置信息API Key、Base URL 等)
4. 提交配置,系统验证并保存
### 5.2 管理已有模型
1. 查看模型工厂卡片,显示基本信息和标签
2. 点击"显示更多模型"查看具体模型列表
3. 对单个模型进行编辑或删除操作
4. 删除整个模型工厂
### 5.3 系统设置
1. 设置系统默认使用的 LLM 模型
2. 配置模型监控和分析工具
## 技术特点
### 6.1 模块化设计
- 每个模型工厂都有独立的配置组件
- 统一的状态管理和操作接口
- 可扩展的架构设计
### 6.2 用户体验
- 直观的卡片式布局
- 标签系统清晰显示模型能力
- 折叠/展开功能优化空间利用
- 确认对话框防止误操作
### 6.3 数据处理
- 支持多种配置参数
- 实时验证和错误处理
- 本地和云端模型统一管理
## 实现要点
1. **组件复用**: 通用的模态框组件可以复用基础结构
2. **状态管理**: 使用 hooks 统一管理各种模态框的显示状态
3. **类型安全**: 严格的 TypeScript 类型定义
4. **错误处理**: 完善的表单验证和错误提示
5. **国际化**: 支持多语言界面

View File

@@ -1,9 +1,457 @@
function ModelsSetting() {
return (
<div>
<h1>Model Setting</h1>
</div>
);
import React, { useState, useCallback } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Button,
Chip,
IconButton,
Collapse,
Grid,
Divider,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
FormControl,
InputLabel,
Select,
MenuItem,
Alert,
CircularProgress,
Tooltip,
} from '@mui/material';
import {
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Settings as SettingsIcon,
Delete as DeleteIcon,
Edit as EditIcon,
Add as AddIcon,
Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon,
} from '@mui/icons-material';
import { useLlmModelSetting } from '@/hooks/setting-hooks';
import { useModelDialogs } from './hooks/useModelDialogs';
import { LlmSvgIcon } from '@/components/AppSvgIcon';
import { LLM_FACTORY_LIST, IconMap, type LLMFactory } from '@/constants/llm';
import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm';
// 模型类型标签颜色映射
const MODEL_TYPE_COLORS: Record<string, string> = {
'LLM': '#1976d2',
'TEXT EMBEDDING': '#388e3c',
'TEXT RE-RANK': '#f57c00',
'TTS': '#7b1fa2',
'SPEECH2TEXT': '#c2185b',
'IMAGE2TEXT': '#5d4037',
'MODERATION': '#455a64',
};
// 模型工厂卡片组件
interface ModelFactoryCardProps {
factory: IFactory;
myModels: ILlmItem[];
onConfigure: (factory: IFactory) => void;
onDeleteFactory: (factoryName: string) => void;
onDeleteModel: (factoryName: string, modelName: string) => void;
onEditModel: (factory: IFactory, model: ILlmItem) => void;
}
export default ModelsSetting;
const ModelFactoryCard: React.FC<ModelFactoryCardProps> = ({
factory,
myModels,
onConfigure,
onDeleteFactory,
onDeleteModel,
onEditModel,
}) => {
const [expanded, setExpanded] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const handleExpandClick = () => {
setExpanded(!expanded);
};
const handleDeleteFactory = () => {
onDeleteFactory(factory.name);
setShowDeleteConfirm(false);
};
// 获取工厂图标名称
const getFactoryIconName = (factoryName: LLMFactory) => {
return IconMap[factoryName] || 'default';
};
return (
<>
<Card sx={{ mb: 2, border: '1px solid #e0e0e0' }}>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Box display="flex" alignItems="center" gap={2}>
<LlmSvgIcon
name={getFactoryIconName(factory.name as LLMFactory)}
sx={{ width: 40, height: 40, color: 'primary.main' }}/>
<Box>
<Typography variant="h6" component="div">
{factory.name}
</Typography>
<Box display="flex" gap={1} mt={1}>
{factory.model_types.map((type) => (
<Chip
key={type}
label={type.toUpperCase()}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[type.toUpperCase()] || '#757575',
color: 'white',
fontSize: '0.7rem',
}}
/>
))}
</Box>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Button
variant="contained"
size="small"
startIcon={<SettingsIcon />}
onClick={() => onConfigure(factory)}
>
</Button>
{myModels.length > 0 && (
<IconButton onClick={handleExpandClick}>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
)}
<IconButton
color="error"
onClick={() => setShowDeleteConfirm(true)}
>
<DeleteIcon />
</IconButton>
</Box>
</Box>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle2" gutterBottom>
({myModels.length})
</Typography>
<Grid container spacing={2}>
{myModels.map((model) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={model.name}>
<Card variant="outlined" sx={{ p: 2 }}>
<Box display="flex" justifyContent="space-between" alignItems="start">
<Box>
<Typography variant="body2" fontWeight="bold">
{model.name}
</Typography>
<Typography variant="caption" color="text.secondary">
{model.type}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Max Tokens: {model.max_tokens}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Used: {model.used_token}
</Typography>
</Box>
<Box>
<IconButton
size="small"
onClick={() => onEditModel(factory, model)}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="error"
onClick={() => onDeleteModel(factory.name, model.name)}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
</Box>
</Card>
</Grid>
))}
</Grid>
</Collapse>
</CardContent>
</Card>
{/* 删除确认对话框 */}
<Dialog open={showDeleteConfirm} onClose={() => setShowDeleteConfirm(false)}>
<DialogTitle></DialogTitle>
<DialogContent>
<Typography>
"{factory.name}"
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowDeleteConfirm(false)}></Button>
<Button onClick={handleDeleteFactory} color="error" variant="contained">
</Button>
</DialogActions>
</Dialog>
</>
);
};
// 系统默认模型设置组件
interface SystemModelSettingProps {
myLlm: Record<string, IMyLlmModel> | undefined;
}
const SystemModelSetting: React.FC<SystemModelSettingProps> = ({ myLlm }) => {
const [defaultModel, setDefaultModel] = useState('');
const [loading, setLoading] = useState(false);
// 获取所有可用的聊天模型
const chatModels = myLlm ? Object.values(myLlm).flatMap(group =>
group.llm.filter(model => model.type.toLowerCase().includes('chat') || model.type.toLowerCase().includes('llm'))
) : [];
const handleSaveDefaultModel = async () => {
if (!defaultModel) return;
setLoading(true);
try {
// TODO: 调用设置系统默认模型的 API
console.log('Setting default model:', defaultModel);
// await userService.setSystemDefaultModel({ model: defaultModel });
} catch (error) {
console.error('设置默认模型失败:', error);
} finally {
setLoading(false);
}
};
return (
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
LLM
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
使
</Typography>
<Box display="flex" gap={2} alignItems="center" mt={2}>
<FormControl sx={{ minWidth: 300 }}>
<InputLabel></InputLabel>
<Select
value={defaultModel}
label="选择默认模型"
onChange={(e) => setDefaultModel(e.target.value)}
>
{chatModels.map((model) => (
<MenuItem key={`${model.name}-${model.type}`} value={model.name}>
{model.name} ({model.type})
</MenuItem>
))}
</Select>
</FormControl>
<Button
variant="contained"
onClick={handleSaveDefaultModel}
disabled={!defaultModel || loading}
startIcon={loading ? <CircularProgress size={16} /> : undefined}
>
</Button>
</Box>
</CardContent>
</Card>
);
};
// 主页面组件
function ModelsPage() {
const { llmFactory, myLlm } = useLlmModelSetting();
const modelDialogs = useModelDialogs();
// 处理配置模型工厂
const handleConfigureFactory = useCallback((factory: IFactory) => {
// modelDialogs.openDialog(factory.name);
}, [modelDialogs]);
// 处理删除模型工厂
const handleDeleteFactory = useCallback(async (factoryName: string) => {
try {
// await modelDialogs.deleteOperations.deleteFactory(factoryName);
// 刷新数据
window.location.reload();
} catch (error) {
console.error('删除工厂失败:', error);
}
}, []);
// 处理删除单个模型
const handleDeleteModel = useCallback(async (factoryName: string, modelName: string) => {
try {
// await modelDialogs.deleteOperations.deleteLlm(factoryName, modelName);
// 刷新数据
window.location.reload();
} catch (error) {
console.error('删除模型失败:', error);
}
}, []);
// 处理编辑模型
const handleEditModel = useCallback((factory: IFactory, model: ILlmItem) => {
// 设置编辑模式并打开对话框
// modelDialogs.openDialog(factory.name, {
// model_name: model.name,
// api_base: model.api_base,
// max_tokens: model.max_tokens,
// });
}, [modelDialogs]);
// 根据工厂名称获取对应的模型列表
const getModelsForFactory = (factoryName: LLMFactory): ILlmItem[] => {
if (!myLlm) return [];
const factoryGroup = myLlm[factoryName];
return factoryGroup?.llm || [];
};
if (!llmFactory || !myLlm) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
<CircularProgress />
</Box>
);
}
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
</Typography>
<Typography variant="body1" color="text.secondary" gutterBottom>
LLM
</Typography>
{/* 系统默认模型设置 */}
<SystemModelSetting myLlm={myLlm} />
{/* My LLM 部分 */}
{/* <Box mb={4}>
<Typography variant="h5" gutterBottom>
我的 LLM 模型
</Typography>
{!myLlm || Object.keys(myLlm).length === 0 ? (
<Alert severity="info">
您还没有配置任何 LLM 模型。请在下方的模型工厂中进行配置。
</Alert>
) : (
<Grid container spacing={2}>
{Object.entries(myLlm).map(([factoryName, group]) => (
<Grid size={12} key={factoryName}>
<Card variant="outlined">
<CardContent>
<Typography variant="h6" gutterBottom>
{factoryName}
</Typography>
<Box display="flex" gap={1} mb={2}>
{group.tags.split(',').map((tag) => (
<Chip
key={tag}
label={tag.trim()}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[tag.trim()] || '#757575',
color: 'white',
}}
/>
))}
</Box>
<Grid container spacing={2}>
{group.llm.map((model) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={model.name}>
<Card variant="outlined" sx={{ p: 2 }}>
<Box display="flex" justifyContent="space-between" alignItems="flex-start" mb={1}>
<Typography variant="body2" fontWeight="bold">
{model.name}
</Typography>
<Box>
<IconButton
size="small"
onClick={() => handleEditModel({ name: factoryName } as IFactory, model)}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
color="error"
onClick={() => handleDeleteModel(factoryName, model.name)}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
</Box>
<Chip
label={model.type}
size="small"
sx={{
backgroundColor: MODEL_TYPE_COLORS[model.type.toUpperCase()] || '#757575',
color: 'white',
mb: 1,
}}
/>
<Typography variant="caption" display="block" color="text.secondary">
Max Tokens: {model.max_tokens}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Used: {model.used_token}
</Typography>
{model.api_base && (
<Typography variant="caption" display="block" color="text.secondary">
Base URL: {model.api_base}
</Typography>
)}
</Card>
</Grid>
))}
</Grid>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Box> */}
{/* LLM Factory 部分 */}
<Box>
<Typography variant="h5" gutterBottom>
LLM
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
AI
</Typography>
{llmFactory.map((factory) => (
<ModelFactoryCard
key={factory.name}
factory={factory}
myModels={getModelsForFactory(factory.name as LLMFactory)}
onConfigure={handleConfigureFactory}
onDeleteFactory={handleDeleteFactory}
onDeleteModel={handleDeleteModel}
onEditModel={handleEditModel}
/>
))}
</Box>
{/* 模型配置对话框 */}
{/* <ModelDialogs {...modelDialogs} /> */}
</Box>
);
};
export default ModelsPage;