feat(i18n): add internationalization support across multiple components
This commit is contained in:
@@ -4,6 +4,8 @@ import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { BaseBreadcrumbs, type BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||
import SettingSidebar from './SettingSidebar';
|
||||
import { Home as HomeIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LanguageSwitcher from '../LanguageSwitcher';
|
||||
|
||||
const LayoutContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
@@ -21,6 +23,9 @@ const HeaderContainer = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(2, 3),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const MainContent = styled(Box)({
|
||||
@@ -29,23 +34,28 @@ const MainContent = styled(Box)({
|
||||
padding: '24px',
|
||||
});
|
||||
|
||||
// 设置页面路径映射
|
||||
const settingPathMap: Record<string, string> = {
|
||||
'/setting/profile': '个人资料',
|
||||
'/setting/models': '模型配置',
|
||||
'/setting/system': '系统设置',
|
||||
'/setting/teams': '团队管理',
|
||||
'/setting/mcp': 'MCP配置',
|
||||
};
|
||||
|
||||
function SettingHeader() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
// 设置页面路径映射
|
||||
const settingPathMap: Record<string, string> = {
|
||||
'/setting/profile': t('setting.profile'),
|
||||
'/setting/models': t('setting.model'),
|
||||
'/setting/system': t('setting.system'),
|
||||
'/setting/teams': t('setting.team'),
|
||||
'/setting/mcp': t('setting.mcp'),
|
||||
};
|
||||
|
||||
|
||||
// 生成面包屑导航
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{
|
||||
label: '首页',
|
||||
label: t('header.home'),
|
||||
path: '/',
|
||||
onClick: () => navigate('/'),
|
||||
},
|
||||
@@ -65,6 +75,7 @@ function SettingHeader() {
|
||||
items={breadcrumbItems}
|
||||
sx={{ mb: 0 }}
|
||||
/>
|
||||
<LanguageSwitcher textColor='text.primary' />
|
||||
</HeaderContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
SmartToy as SmartToyIcon,
|
||||
Extension as ExtensionIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
interface SettingMenuItem {
|
||||
key: string;
|
||||
@@ -16,40 +18,43 @@ interface SettingMenuItem {
|
||||
path: string;
|
||||
}
|
||||
|
||||
const SettingSidebar: React.FC = () => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const settingMenuItems: SettingMenuItem[] = [
|
||||
{
|
||||
key: 'profile',
|
||||
label: '个人资料',
|
||||
label: t('setting.profile'),
|
||||
icon: PersonIcon,
|
||||
path: '/setting/profile',
|
||||
},
|
||||
{
|
||||
key: 'models',
|
||||
label: '模型配置',
|
||||
label: t('setting.model'),
|
||||
icon: SmartToyIcon,
|
||||
path: '/setting/models',
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
label: '系统设置',
|
||||
label: t('setting.system'),
|
||||
icon: ComputerIcon,
|
||||
path: '/setting/system',
|
||||
},
|
||||
{
|
||||
key: 'teams',
|
||||
label: '团队管理',
|
||||
label: t('setting.team'),
|
||||
icon: GroupIcon,
|
||||
path: '/setting/teams',
|
||||
},
|
||||
{
|
||||
key: 'mcp',
|
||||
label: 'MCP配置',
|
||||
label: t('setting.mcp'),
|
||||
icon: ExtensionIcon,
|
||||
path: '/setting/mcp',
|
||||
},
|
||||
];
|
||||
|
||||
const SettingSidebar: React.FC = () => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -79,7 +84,7 @@ const SettingSidebar: React.FC = () => {
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
设置
|
||||
{t('header.setting')}
|
||||
</Typography>
|
||||
|
||||
<List>
|
||||
|
||||
1001
src/locales/en.ts
1001
src/locales/en.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import {
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import { Visibility, VisibilityOff, Close } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSnackbar } from '@/hooks/useSnackbar';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
@@ -32,6 +33,7 @@ interface PasswordFormData {
|
||||
* 修改密码对话框
|
||||
*/
|
||||
function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePasswordDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const { showMessage } = useSnackbar();
|
||||
|
||||
const [formData, setFormData] = useState<PasswordFormData>({
|
||||
@@ -100,23 +102,23 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
||||
const newErrors: Partial<PasswordFormData> = {};
|
||||
|
||||
if (!formData.currentPassword.trim()) {
|
||||
newErrors.currentPassword = '请输入当前密码';
|
||||
newErrors.currentPassword = t('setting.currentPasswordRequired');
|
||||
}
|
||||
|
||||
if (!formData.newPassword.trim()) {
|
||||
newErrors.newPassword = '请输入新密码';
|
||||
newErrors.newPassword = t('setting.newPasswordRequired');
|
||||
} else if (formData.newPassword.length < 6) {
|
||||
newErrors.newPassword = '新密码长度至少6位';
|
||||
newErrors.newPassword = t('setting.passwordMinLength');
|
||||
}
|
||||
|
||||
if (!formData.confirmPassword.trim()) {
|
||||
newErrors.confirmPassword = '请确认新密码';
|
||||
newErrors.confirmPassword = t('setting.confirmPasswordRequired');
|
||||
} else if (formData.newPassword !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = '两次输入的密码不一致';
|
||||
newErrors.confirmPassword = t('setting.passwordMismatch');
|
||||
}
|
||||
|
||||
if (formData.currentPassword === formData.newPassword) {
|
||||
newErrors.newPassword = '新密码不能与当前密码相同';
|
||||
newErrors.newPassword = t('setting.newPasswordSameAsCurrent');
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
@@ -136,10 +138,15 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
||||
new_password: formData.newPassword
|
||||
});
|
||||
|
||||
showMessage.success('密码修改成功');
|
||||
showMessage.success(t('setting.passwordChangeSuccess'));
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
logger.error('修改密码失败:', error);
|
||||
if (error.response?.status === 400) {
|
||||
showMessage.error(t('setting.currentPasswordIncorrect'));
|
||||
} else {
|
||||
showMessage.error(t('setting.passwordChangeError'));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -157,7 +164,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
||||
>
|
||||
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6" component="div">
|
||||
修改密码
|
||||
{t('setting.changePassword')}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
@@ -171,13 +178,17 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
||||
<DialogContent dividers>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, pt: 1 }}>
|
||||
<Alert severity="info" sx={{ mb: 1 }}>
|
||||
为了您的账户安全,请设置一个强密码。密码长度至少6位,建议包含字母、数字和特殊字符。
|
||||
{t('setting.passwordSecurityTip')}
|
||||
</Alert>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{t('setting.passwordUpdateTip')}
|
||||
</Typography>
|
||||
|
||||
{/* 当前密码 */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label="当前密码"
|
||||
label={t('setting.currentPassword')}
|
||||
type={showPasswords.current ? 'text' : 'password'}
|
||||
value={formData.currentPassword}
|
||||
onChange={handleInputChange('currentPassword')}
|
||||
@@ -201,7 +212,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
||||
{/* 新密码 */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label="新密码"
|
||||
label={t('setting.newPassword')}
|
||||
type={showPasswords.new ? 'text' : 'password'}
|
||||
value={formData.newPassword}
|
||||
onChange={handleInputChange('newPassword')}
|
||||
@@ -225,7 +236,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
||||
{/* 确认新密码 */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label="确认新密码"
|
||||
label={t('setting.confirmNewPassword')}
|
||||
type={showPasswords.confirm ? 'text' : 'password'}
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange('confirmPassword')}
|
||||
@@ -254,7 +265,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
||||
variant="outlined"
|
||||
disabled={loading}
|
||||
>
|
||||
取消
|
||||
{t('setting.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
@@ -262,7 +273,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
|
||||
disabled={loading}
|
||||
sx={{ minWidth: 100 }}
|
||||
>
|
||||
{loading ? '修改中...' : '确认修改'}
|
||||
{loading ? t('setting.changing') : t('setting.confirmChange')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// 表单数据接口
|
||||
export interface ApiKeyFormData {
|
||||
@@ -45,6 +46,7 @@ function ApiKeyDialog({
|
||||
initialData,
|
||||
editMode = false,
|
||||
}: ApiKeyDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [showApiKey, setShowApiKey] = React.useState(false);
|
||||
|
||||
const {
|
||||
@@ -81,14 +83,14 @@ function ApiKeyDialog({
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{editMode ? '编辑' : '配置'} {factoryName} API Key
|
||||
{editMode ? t('common.edit') : t('common.configure')} {factoryName} API Key
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box component="form" sx={{ mt: 2 }}>
|
||||
<Controller
|
||||
name="api_key"
|
||||
control={control}
|
||||
rules={{ required: 'API Key 是必填项' }}
|
||||
rules={{ required: t('setting.apiKeyRequired') }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
@@ -126,7 +128,7 @@ function ApiKeyDialog({
|
||||
label="Base URL"
|
||||
placeholder="https://api.openai.com/v1"
|
||||
margin="normal"
|
||||
helperText="可选,自定义 API 端点"
|
||||
helperText={t('setting.baseUrlOptional')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -142,7 +144,7 @@ function ApiKeyDialog({
|
||||
fullWidth
|
||||
label="Group ID"
|
||||
margin="normal"
|
||||
helperText="Minimax 专用的 Group ID"
|
||||
helperText={t('setting.minimaxGroupId')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -151,7 +153,7 @@ function ApiKeyDialog({
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} disabled={loading}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
@@ -159,7 +161,7 @@ function ApiKeyDialog({
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||
>
|
||||
{editMode ? '更新' : '保存'}
|
||||
{editMode ? t('common.update') : t('common.save')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
CircularProgress,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
FormHelperText,
|
||||
Link,
|
||||
} from '@mui/material';
|
||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import type { IAddLlmRequestBody } from '@/interfaces/request/llm';
|
||||
|
||||
// 模型类型选项
|
||||
const MODEL_TYPE_OPTIONS = [
|
||||
{ value: 'chat', label: 'Chat' },
|
||||
{ value: 'embedding', label: 'Embedding' },
|
||||
{ value: 'image2text', label: 'Image2Text' },
|
||||
];
|
||||
|
||||
// 表单数据接口
|
||||
export interface AzureOpenAIFormData extends IAddLlmRequestBody {
|
||||
api_version: string;
|
||||
}
|
||||
|
||||
// 对话框 Props 接口
|
||||
export interface AzureOpenAIDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: AzureOpenAIFormData) => void;
|
||||
loading: boolean;
|
||||
initialData?: AzureOpenAIFormData;
|
||||
editMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Azure OpenAI 配置对话框
|
||||
*/
|
||||
function AzureOpenAIDialog({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
loading,
|
||||
initialData,
|
||||
editMode = false,
|
||||
}: AzureOpenAIDialogProps) {
|
||||
const [showApiKey, setShowApiKey] = React.useState(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<AzureOpenAIFormData>({
|
||||
defaultValues: {
|
||||
model_type: 'embedding',
|
||||
llm_name: 'gpt-3.5-turbo',
|
||||
api_base: '',
|
||||
api_key: '',
|
||||
api_version: '2024-02-01',
|
||||
max_tokens: 4096,
|
||||
llm_factory: 'Azure-OpenAI',
|
||||
},
|
||||
});
|
||||
|
||||
// 当对话框打开或初始数据变化时重置表单
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
reset({
|
||||
model_type: 'embedding',
|
||||
llm_name: 'gpt-3.5-turbo',
|
||||
api_base: '',
|
||||
api_key: '',
|
||||
api_version: '2024-02-01',
|
||||
max_tokens: 4096,
|
||||
llm_factory: initialData?.llm_factory || 'Azure-OpenAI',
|
||||
});
|
||||
}
|
||||
}, [open, initialData, reset]);
|
||||
|
||||
const handleFormSubmit = (data: AzureOpenAIFormData) => {
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
const toggleShowApiKey = () => {
|
||||
setShowApiKey(!showApiKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{editMode ? '编辑' : '配置'} Azure OpenAI
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box component="form" sx={{ mt: 2 }}>
|
||||
{/* 模型类型选择 */}
|
||||
<Controller
|
||||
name="model_type"
|
||||
control={control}
|
||||
rules={{ required: '模型类型是必填项' }}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth margin="normal" error={!!errors.model_type}>
|
||||
<InputLabel>模型类型</InputLabel>
|
||||
<Select
|
||||
{...field}
|
||||
label="模型类型"
|
||||
>
|
||||
{MODEL_TYPE_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.model_type && (
|
||||
<FormHelperText>{errors.model_type.message}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 模型名称 */}
|
||||
<Controller
|
||||
name="llm_name"
|
||||
control={control}
|
||||
rules={{ required: '模型名称是必填项' }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="模型名称"
|
||||
margin="normal"
|
||||
error={!!errors.llm_name}
|
||||
helperText={errors.llm_name?.message || '请输入模型名称'}
|
||||
placeholder="gpt-3.5-turbo"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 基础 URL */}
|
||||
<Controller
|
||||
name="api_base"
|
||||
control={control}
|
||||
rules={{
|
||||
required: '基础 URL 是必填项',
|
||||
pattern: {
|
||||
value: /^https?:\/\/.+/,
|
||||
message: '基础 URL 必须是有效的 URL'
|
||||
}
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="基础 Url"
|
||||
margin="normal"
|
||||
error={!!errors.api_base}
|
||||
helperText={errors.api_base?.message || 'Azure OpenAI 服务的端点 URL'}
|
||||
placeholder="https://your-resource.openai.azure.com/"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* API Key */}
|
||||
<Controller
|
||||
name="api_key"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="API-Key"
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
margin="normal"
|
||||
error={!!errors.api_key}
|
||||
helperText={errors.api_key?.message || '输入api key(如果是本地部署的模型,请忽略)'}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle api key visibility"
|
||||
onClick={toggleShowApiKey}
|
||||
edge="end"
|
||||
>
|
||||
{showApiKey ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* API Version */}
|
||||
<Controller
|
||||
name="api_version"
|
||||
control={control}
|
||||
rules={{ required: 'API Version 是必填项' }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="API Version"
|
||||
margin="normal"
|
||||
error={!!errors.api_version}
|
||||
helperText={errors.api_version?.message || 'Azure OpenAI API 版本'}
|
||||
placeholder="2024-02-01"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 最大token数 */}
|
||||
<Controller
|
||||
name="max_tokens"
|
||||
control={control}
|
||||
rules={{
|
||||
required: '最大token数是必填项',
|
||||
min: { value: 1, message: '最大token数必须大于0' },
|
||||
max: { value: 100000, message: '最大token数不能超过100000' }
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="最大token数"
|
||||
type="number"
|
||||
margin="normal"
|
||||
error={!!errors.max_tokens}
|
||||
helperText={errors.max_tokens?.message || '设置了模型输出的最大长度,以token(单词片段)的数量表示'}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{/* 右侧按钮组 */}
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button onClick={onClose} disabled={loading}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AzureOpenAIDialog;
|
||||
@@ -1,336 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
CircularProgress,
|
||||
FormHelperText,
|
||||
Link,
|
||||
} from '@mui/material';
|
||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import type { IAddLlmRequestBody } from '@/interfaces/request/llm';
|
||||
|
||||
// AWS Bedrock 支持的区域列表
|
||||
export const BEDROCK_REGIONS = [
|
||||
'us-east-2',
|
||||
'us-east-1',
|
||||
'us-west-1',
|
||||
'us-west-2',
|
||||
'af-south-1',
|
||||
'ap-east-1',
|
||||
'ap-south-2',
|
||||
'ap-southeast-3',
|
||||
'ap-southeast-5',
|
||||
'ap-southeast-4',
|
||||
'ap-south-1',
|
||||
'ap-northeast-3',
|
||||
'ap-northeast-2',
|
||||
'ap-southeast-1',
|
||||
'ap-southeast-2',
|
||||
'ap-east-2',
|
||||
'ap-southeast-7',
|
||||
'ap-northeast-1',
|
||||
'ca-central-1',
|
||||
'ca-west-1',
|
||||
'eu-central-1',
|
||||
'eu-west-1',
|
||||
'eu-west-2',
|
||||
'eu-south-1',
|
||||
'eu-west-3',
|
||||
'eu-south-2',
|
||||
'eu-north-1',
|
||||
'eu-central-2',
|
||||
'il-central-1',
|
||||
'mx-central-1',
|
||||
'me-south-1',
|
||||
'me-central-1',
|
||||
'sa-east-1',
|
||||
'us-gov-east-1',
|
||||
'us-gov-west-1',
|
||||
];
|
||||
|
||||
// 模型类型选项
|
||||
const MODEL_TYPE_OPTIONS = [
|
||||
{ value: 'chat', label: 'Chat' },
|
||||
{ value: 'embedding', label: 'Embedding' },
|
||||
];
|
||||
|
||||
// 表单数据接口
|
||||
export interface BedrockFormData extends IAddLlmRequestBody {
|
||||
bedrock_ak: string;
|
||||
bedrock_sk: string;
|
||||
bedrock_region: string;
|
||||
}
|
||||
|
||||
// 对话框 Props 接口
|
||||
export interface BedrockDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: BedrockFormData) => void;
|
||||
loading: boolean;
|
||||
initialData?: BedrockFormData;
|
||||
editMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* AWS Bedrock 配置对话框
|
||||
*/
|
||||
function BedrockDialog ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
loading,
|
||||
initialData,
|
||||
editMode = false,
|
||||
}: BedrockDialogProps) {
|
||||
const [showAccessKey, setShowAccessKey] = React.useState(false);
|
||||
const [showSecretKey, setShowSecretKey] = React.useState(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<BedrockFormData>({
|
||||
defaultValues: {
|
||||
model_type: 'chat',
|
||||
llm_name: '',
|
||||
bedrock_ak: '',
|
||||
bedrock_sk: '',
|
||||
bedrock_region: 'us-east-1',
|
||||
max_tokens: 4096,
|
||||
llm_factory: 'Bedrock',
|
||||
},
|
||||
});
|
||||
|
||||
// 当对话框打开或初始数据变化时重置表单
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
reset({
|
||||
model_type: 'chat',
|
||||
llm_name: '',
|
||||
bedrock_ak: '',
|
||||
bedrock_sk: '',
|
||||
bedrock_region: 'us-east-1',
|
||||
max_tokens: 4096,
|
||||
llm_factory: initialData?.llm_factory || 'Bedrock',
|
||||
});
|
||||
}
|
||||
}, [open, initialData, reset]);
|
||||
|
||||
const handleFormSubmit = (data: BedrockFormData) => {
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
const toggleShowAccessKey = () => {
|
||||
setShowAccessKey(!showAccessKey);
|
||||
};
|
||||
|
||||
const toggleShowSecretKey = () => {
|
||||
setShowSecretKey(!showSecretKey);
|
||||
};
|
||||
|
||||
const docInfo = {
|
||||
url: 'https://console.aws.amazon.com/',
|
||||
text: '如何集成 Bedrock',
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{editMode ? '编辑' : '添加'} LLM
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box component="form" sx={{ mt: 2 }}>
|
||||
{/* 模型类型 */}
|
||||
<Controller
|
||||
name="model_type"
|
||||
control={control}
|
||||
rules={{ required: '模型类型是必填项' }}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth margin="normal" error={!!errors.model_type}>
|
||||
<InputLabel>* 模型类型</InputLabel>
|
||||
<Select {...field} label="* 模型类型">
|
||||
{MODEL_TYPE_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.model_type && (
|
||||
<FormHelperText>{errors.model_type.message}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 模型名称 */}
|
||||
<Controller
|
||||
name="llm_name"
|
||||
control={control}
|
||||
rules={{ required: '模型名称是必填项' }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="* 模型名称"
|
||||
margin="normal"
|
||||
placeholder="请输入模型名称"
|
||||
error={!!errors.llm_name}
|
||||
helperText={errors.llm_name?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* ACCESS KEY */}
|
||||
<Controller
|
||||
name="bedrock_ak"
|
||||
control={control}
|
||||
rules={{ required: 'ACCESS KEY 是必填项' }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="* ACCESS KEY"
|
||||
type={showAccessKey ? 'text' : 'password'}
|
||||
margin="normal"
|
||||
placeholder="请输入 ACCESS KEY"
|
||||
error={!!errors.bedrock_ak}
|
||||
helperText={errors.bedrock_ak?.message}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle access key visibility"
|
||||
onClick={toggleShowAccessKey}
|
||||
edge="end"
|
||||
>
|
||||
{showAccessKey ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* SECRET KEY */}
|
||||
<Controller
|
||||
name="bedrock_sk"
|
||||
control={control}
|
||||
rules={{ required: 'SECRET KEY 是必填项' }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="* SECRET KEY"
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
margin="normal"
|
||||
placeholder="请输入 SECRET KEY"
|
||||
error={!!errors.bedrock_sk}
|
||||
helperText={errors.bedrock_sk?.message}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle secret key visibility"
|
||||
onClick={toggleShowSecretKey}
|
||||
edge="end"
|
||||
>
|
||||
{showSecretKey ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* AWS Region */}
|
||||
<Controller
|
||||
name="bedrock_region"
|
||||
control={control}
|
||||
rules={{ required: 'AWS Region 是必填项' }}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth margin="normal" error={!!errors.bedrock_region}>
|
||||
<InputLabel>* AWS Region</InputLabel>
|
||||
<Select {...field} label="* AWS Region">
|
||||
{BEDROCK_REGIONS.map((region) => (
|
||||
<MenuItem key={region} value={region}>
|
||||
{region}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.bedrock_region && (
|
||||
<FormHelperText>{errors.bedrock_region.message}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 最大token数 */}
|
||||
<Controller
|
||||
name="max_tokens"
|
||||
control={control}
|
||||
rules={{
|
||||
required: '最大token数是必填项',
|
||||
min: { value: 1, message: '最大token数必须大于0' },
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="* 最大token数"
|
||||
type="number"
|
||||
margin="normal"
|
||||
placeholder="这设置了模型输出的最大长度,以token(单词或词片段)的数量来衡量"
|
||||
error={!!errors.max_tokens}
|
||||
helperText={errors.max_tokens?.message}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
||||
<Link
|
||||
href={docInfo.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{ alignSelf: 'center', textDecoration: 'none', ml:2 }}
|
||||
>
|
||||
{docInfo.text}
|
||||
</Link>
|
||||
<Box>
|
||||
<Button onClick={onClose} disabled={loading} sx={{ mr: 1 }}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default BedrockDialog;
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { IAddLlmRequestBody } from '@/interfaces/request/llm';
|
||||
|
||||
// 表单项配置接口
|
||||
@@ -79,6 +80,7 @@ function ConfigurationDialog({
|
||||
docLink,
|
||||
editMode = false,
|
||||
}: ConfigurationDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [passwordVisibility, setPasswordVisibility] = React.useState<Record<string, boolean>>({});
|
||||
|
||||
// 构建默认值
|
||||
@@ -138,7 +140,7 @@ function ConfigurationDialog({
|
||||
// 构建验证规则
|
||||
const rules: any = {};
|
||||
if (item.required) {
|
||||
rules.required = `${item.label}是必填项`;
|
||||
rules.required = t('setting.fieldRequired', { field: item.label });
|
||||
}
|
||||
if (item.validation) {
|
||||
Object.assign(rules, item.validation);
|
||||
@@ -266,7 +268,7 @@ function ConfigurationDialog({
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{editMode ? '编辑' : ''} {title}
|
||||
{editMode ? t('setting.edit') : ''} {title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box component="form" sx={{ mt: 2 }}>
|
||||
@@ -290,7 +292,7 @@ function ConfigurationDialog({
|
||||
{/* 右侧按钮组 */}
|
||||
<Box sx={{ ml: 'auto' }}>
|
||||
<Button onClick={onClose} disabled={loading} sx={{ mr: 1 }}>
|
||||
取消
|
||||
{t('setting.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
@@ -298,7 +300,7 @@ function ConfigurationDialog({
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||
>
|
||||
确定
|
||||
{t('setting.confirm')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
Link,
|
||||
} from '@mui/material';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import logger from '@/utils/logger';
|
||||
import { LLM_FACTORY_LIST, type LLMFactory } from '@/constants/llm';
|
||||
import i18n from '@/locales';
|
||||
|
||||
// 表单数据接口
|
||||
export interface OllamaFormData {
|
||||
@@ -64,7 +66,7 @@ const llmFactoryToUrlMap: { [x: string]: string } = {
|
||||
function getURLByFactory(factory: LLMFactory) {
|
||||
const url = llmFactoryToUrlMap[factory];
|
||||
return {
|
||||
textTip: `如何集成 ${factory}`,
|
||||
textTip: `${i18n.t('setting.howToIntegrate')} ${factory}`,
|
||||
url,
|
||||
}
|
||||
}
|
||||
@@ -91,6 +93,7 @@ function OllamaDialog({
|
||||
initialData,
|
||||
editMode = false,
|
||||
}: OllamaDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -163,7 +166,7 @@ function OllamaDialog({
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{editMode ? `编辑 ${initialData?.llm_factory || LLM_FACTORY_LIST.Ollama}` : `配置 ${initialData?.llm_factory || LLM_FACTORY_LIST.Ollama}`}
|
||||
{editMode ? t('setting.edit') : t('setting.configure')} {initialData?.llm_factory || LLM_FACTORY_LIST.Ollama}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box component="form" sx={{ mt: 2 }}>
|
||||
@@ -171,13 +174,13 @@ function OllamaDialog({
|
||||
<Controller
|
||||
name="model_type"
|
||||
control={control}
|
||||
rules={{ required: '模型类型是必填项' }}
|
||||
rules={{ required: t('setting.modelTypeRequired') }}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth margin="normal" error={!!errors.model_type}>
|
||||
<InputLabel>模型类型 *</InputLabel>
|
||||
<InputLabel>{t('setting.modelType')} *</InputLabel>
|
||||
<Select
|
||||
{...field}
|
||||
label="模型类型 *"
|
||||
label={`${t('setting.modelType')} *`}
|
||||
>
|
||||
{modelTypeOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
@@ -196,17 +199,17 @@ function OllamaDialog({
|
||||
<Controller
|
||||
name="llm_name"
|
||||
control={control}
|
||||
rules={{ required: '模型名称是必填项' }}
|
||||
rules={{ required: t('setting.modelNameRequired') }}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="模型名称"
|
||||
label={t('setting.modelName')}
|
||||
margin="normal"
|
||||
required
|
||||
error={!!errors.llm_name}
|
||||
helperText={errors.llm_name?.message || '请输入模型名称'}
|
||||
placeholder="例如: llama2, mistral"
|
||||
helperText={errors.llm_name?.message || t('setting.modelNamePlaceholder')}
|
||||
placeholder={t('setting.modelNameExample')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -216,21 +219,21 @@ function OllamaDialog({
|
||||
name="api_base"
|
||||
control={control}
|
||||
rules={{
|
||||
required: '基础 URL 是必填项',
|
||||
required: t('setting.baseUrlRequired'),
|
||||
pattern: {
|
||||
value: /^https?:\/\/.+/,
|
||||
message: '基础 URL 必须是有效的 URL'
|
||||
message: t('setting.baseUrlInvalid')
|
||||
}
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="基础 URL"
|
||||
label={t('setting.baseUrl')}
|
||||
margin="normal"
|
||||
required
|
||||
error={!!errors.api_base}
|
||||
helperText={errors.api_base?.message || '基础 URL'}
|
||||
helperText={errors.api_base?.message || t('setting.baseUrl')}
|
||||
placeholder="http://localhost:8888"
|
||||
/>
|
||||
)}
|
||||
@@ -247,8 +250,8 @@ function OllamaDialog({
|
||||
label="API Key"
|
||||
margin="normal"
|
||||
error={!!errors.api_key}
|
||||
helperText={errors.api_key?.message || 'API Key (可选)'}
|
||||
placeholder="如果需要认证,请输入 API Key"
|
||||
helperText={errors.api_key?.message || t('setting.apiKeyTip')}
|
||||
placeholder={t('setting.apiKeyPlaceholder')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -258,26 +261,26 @@ function OllamaDialog({
|
||||
name="max_tokens"
|
||||
control={control}
|
||||
rules={{
|
||||
required: '最大 Token 数是必填项',
|
||||
required: t('setting.maxTokensRequired'),
|
||||
min: {
|
||||
value: 1,
|
||||
message: '最大 Token 数必须大于 0'
|
||||
message: t('setting.maxTokensMin')
|
||||
},
|
||||
max: {
|
||||
value: 100000,
|
||||
message: '最大 Token 数不能超过 100000'
|
||||
message: t('setting.maxTokensMax')
|
||||
}
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
label="最大 Token 数"
|
||||
label={t('setting.maxTokens')}
|
||||
margin="normal"
|
||||
type="number"
|
||||
required
|
||||
error={!!errors.max_tokens}
|
||||
helperText={errors.max_tokens?.message || '模型支持的最大 Token 数'}
|
||||
helperText={errors.max_tokens?.message || t('setting.maxTokensValidation')}
|
||||
placeholder="4096"
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
@@ -307,7 +310,7 @@ function OllamaDialog({
|
||||
{/* 右侧按钮组 */}
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button onClick={onClose} disabled={loading}>
|
||||
取消
|
||||
{t('setting.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
@@ -315,7 +318,7 @@ function OllamaDialog({
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||
>
|
||||
确定
|
||||
{t('setting.confirm')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ListSubheader,
|
||||
} from '@mui/material';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LlmSvgIcon } from '@/components/AppSvgIcon';
|
||||
import { IconMap, type LLMFactory } from '@/constants/llm';
|
||||
import type { ITenantInfo } from '@/interfaces/database/knowledge';
|
||||
@@ -69,6 +70,7 @@ function SystemModelDialog({
|
||||
editMode = false,
|
||||
allModelOptions
|
||||
}: SystemModelDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const { control, handleSubmit, reset, formState: { errors } } = useForm<ITenantInfo>({
|
||||
defaultValues: {}
|
||||
});
|
||||
@@ -106,25 +108,25 @@ function SystemModelDialog({
|
||||
await onSubmit(data);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
console.error(t('setting.submitFailed'), error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
设置默认模型
|
||||
{t('setting.setDefaultModel')}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box component="form" sx={{ mt: 2 }}>
|
||||
<Controller
|
||||
name="llm_id"
|
||||
control={control}
|
||||
rules={{ required: '聊天模型是必填项' }}
|
||||
rules={{ required: t('setting.chatModelRequired') }}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth margin="normal" error={!!errors.llm_id}>
|
||||
<InputLabel>聊天模型</InputLabel>
|
||||
<Select {...field} label="聊天模型">
|
||||
<InputLabel>{t('setting.chatModel')}</InputLabel>
|
||||
<Select {...field} label={t('setting.chatModel')}>
|
||||
{llmOptions.map((group) => [
|
||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||
...group.options.map((option) => (
|
||||
@@ -152,11 +154,11 @@ function SystemModelDialog({
|
||||
<Controller
|
||||
name="embd_id"
|
||||
control={control}
|
||||
rules={{ required: '嵌入模型是必填项' }}
|
||||
rules={{ required: t('setting.embeddingModelRequired') }}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth margin="normal" error={!!errors.embd_id}>
|
||||
<InputLabel>嵌入模型</InputLabel>
|
||||
<Select {...field} label="嵌入模型">
|
||||
<InputLabel>{t('setting.embeddingModel')}</InputLabel>
|
||||
<Select {...field} label={t('setting.embeddingModel')}>
|
||||
{embdOptions.map((group) => [
|
||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||
...group.options.map((option) => (
|
||||
@@ -186,8 +188,8 @@ function SystemModelDialog({
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>Img2txt模型</InputLabel>
|
||||
<Select {...field} label="Img2txt模型">
|
||||
<InputLabel>{t('setting.img2txtModel')}</InputLabel>
|
||||
<Select {...field} label={t('setting.img2txtModel')}>
|
||||
{img2txtOptions.map((group) => [
|
||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||
...group.options.map((option) => (
|
||||
@@ -212,8 +214,8 @@ function SystemModelDialog({
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>Speech2txt模型</InputLabel>
|
||||
<Select {...field} label="Speech2txt模型">
|
||||
<InputLabel>{t('setting.speech2txtModel')}</InputLabel>
|
||||
<Select {...field} label={t('setting.speech2txtModel')}>
|
||||
{asrOptions.map((group) => [
|
||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||
...group.options.map((option) => (
|
||||
@@ -238,8 +240,8 @@ function SystemModelDialog({
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>Rerank模型</InputLabel>
|
||||
<Select {...field} label="Rerank模型">
|
||||
<InputLabel>{t('setting.rerankModel')}</InputLabel>
|
||||
<Select {...field} label={t('setting.rerankModel')}>
|
||||
{rerankOptions.map((group) => [
|
||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||
...group.options.map((option) => (
|
||||
@@ -264,8 +266,8 @@ function SystemModelDialog({
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>TTS模型</InputLabel>
|
||||
<Select {...field} label="TTS模型">
|
||||
<InputLabel>{t('setting.ttsModel')}</InputLabel>
|
||||
<Select {...field} label={t('setting.ttsModel')}>
|
||||
{ttsOptions.map((group) => [
|
||||
<ListSubheader key={group.label}>{group.label}</ListSubheader>,
|
||||
...group.options.map((option) => (
|
||||
@@ -288,7 +290,7 @@ function SystemModelDialog({
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} disabled={loading}>
|
||||
取消
|
||||
{t('setting.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit(handleFormSubmit)}
|
||||
@@ -296,7 +298,7 @@ function SystemModelDialog({
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||
>
|
||||
确定
|
||||
{t('setting.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ConfigFormItem, DocLinkConfig } from './ConfigurationDialog';
|
||||
import { LLM_FACTORY_LIST } from '@/constants/llm';
|
||||
import i18n from '@/locales';
|
||||
|
||||
// AWS Bedrock 支持的区域列表
|
||||
export const BEDROCK_REGIONS = [
|
||||
@@ -54,71 +55,71 @@ export const MODEL_TYPE_OPTIONS = [
|
||||
export const DOC_LINKS: Record<string, DocLinkConfig> = {
|
||||
[LLM_FACTORY_LIST.AzureOpenAI]: {
|
||||
url: 'https://azure.microsoft.com/en-us/products/ai-services/openai-service',
|
||||
text: '如何集成 Azure OpenAI',
|
||||
text: `${i18n.t('setting.howToIntegrate')} Azure OpenAI`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.Bedrock]: {
|
||||
url: 'https://console.aws.amazon.com/',
|
||||
text: '如何集成 Bedrock',
|
||||
text: `${i18n.t('setting.howToIntegrate')} Bedrock`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.Ollama]: {
|
||||
url: 'https://github.com/infiniflow/ragflow/blob/main/docs/guides/models/deploy_local_llm.mdx',
|
||||
text: '如何集成 Ollama',
|
||||
text: `${i18n.t('setting.howToIntegrate')} Ollama`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.Xinference]: {
|
||||
url: 'https://inference.readthedocs.io/en/latest/user_guide',
|
||||
text: '如何集成 Xinference',
|
||||
text: `${i18n.t('setting.howToIntegrate')} Xinference`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.ModelScope]: {
|
||||
url: 'https://www.modelscope.cn/docs/model-service/API-Inference/intro',
|
||||
text: '如何集成 ModelScope',
|
||||
text: `${i18n.t('setting.howToIntegrate')} ModelScope`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.LocalAI]: {
|
||||
url: 'https://localai.io/docs/getting-started/models/',
|
||||
text: '如何集成 LocalAI',
|
||||
text: `${i18n.t('setting.howToIntegrate')} LocalAI`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.LMStudio]: {
|
||||
url: 'https://lmstudio.ai/docs/basics',
|
||||
text: '如何集成 LMStudio',
|
||||
text: `${i18n.t('setting.howToIntegrate')} LMStudio`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.OpenAiAPICompatible]: {
|
||||
url: 'https://platform.openai.com/docs/models/gpt-4',
|
||||
text: '如何集成 OpenAI API Compatible',
|
||||
text: `${i18n.t('setting.howToIntegrate')} OpenAI API Compatible`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.TogetherAI]: {
|
||||
url: 'https://docs.together.ai/docs/deployment-options',
|
||||
text: '如何集成 TogetherAI',
|
||||
text: `${i18n.t('setting.howToIntegrate')} TogetherAI`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.Replicate]: {
|
||||
url: 'https://replicate.com/docs/topics/deployments',
|
||||
text: '如何集成 Replicate',
|
||||
text: `${i18n.t('setting.howToIntegrate')} Replicate`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.OpenRouter]: {
|
||||
url: 'https://openrouter.ai/docs',
|
||||
text: '如何集成 OpenRouter',
|
||||
text: `${i18n.t('setting.howToIntegrate')} OpenRouter`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.HuggingFace]: {
|
||||
url: 'https://huggingface.co/docs/text-embeddings-inference/quick_tour',
|
||||
text: '如何集成 HuggingFace',
|
||||
text: `${i18n.t('setting.howToIntegrate')} HuggingFace`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.GPUStack]: {
|
||||
url: 'https://docs.gpustack.ai/latest/quickstart',
|
||||
text: '如何集成 GPUStack',
|
||||
text: `${i18n.t('setting.howToIntegrate')} GPUStack`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.VLLM]: {
|
||||
url: 'https://docs.vllm.ai/en/latest/',
|
||||
text: '如何集成 VLLM',
|
||||
text: `${i18n.t('setting.howToIntegrate')} VLLM`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.FishAudio]: {
|
||||
url: 'https://www.fish.audio/',
|
||||
text: '如何集成 Fish Audio',
|
||||
text: `${i18n.t('setting.howToIntegrate')} Fish Audio`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.TencentCloud]: {
|
||||
url: 'https://cloud.tencent.com/document/api/1093/37823',
|
||||
text: '如何集成 腾讯云语音识别',
|
||||
text: `${i18n.t('setting.howToIntegrate')} 腾讯云语音识别`,
|
||||
},
|
||||
[LLM_FACTORY_LIST.VolcEngine]: {
|
||||
url: 'https://www.volcengine.com/docs/82379/1302008',
|
||||
text: '如何集成 VolcEngine',
|
||||
text: `${i18n.t('setting.howToIntegrate')} VolcEngine`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -126,7 +127,7 @@ export const DOC_LINKS: Record<string, DocLinkConfig> = {
|
||||
export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
||||
{
|
||||
name: 'model_type',
|
||||
label: '模型类型',
|
||||
label: i18n.t('setting.modelType'),
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
@@ -138,24 +139,24 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: '模型名称',
|
||||
label: i18n.t('setting.modelName'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'gpt-3.5-turbo',
|
||||
helperText: '请输入模型名称',
|
||||
helperText: i18n.t('setting.modelNameHelperText'),
|
||||
defaultValue: 'gpt-3.5-turbo',
|
||||
},
|
||||
{
|
||||
name: 'api_base',
|
||||
label: '基础 Url',
|
||||
label: i18n.t('setting.baseUrl'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'https://your-resource.openai.azure.com/',
|
||||
helperText: 'Azure OpenAI 服务的端点 URL',
|
||||
helperText: i18n.t('setting.azureOpenAIEndpointHelperText'),
|
||||
validation: {
|
||||
pattern: {
|
||||
value: /^https?:\/\/.+/,
|
||||
message: '基础 URL 必须是有效的 URL',
|
||||
message: i18n.t('setting.baseUrlValidationMessage'),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -163,7 +164,7 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
||||
name: 'api_key',
|
||||
label: 'API-Key',
|
||||
type: 'password',
|
||||
helperText: '输入api key(如果是本地部署的模型,请忽略)',
|
||||
helperText: i18n.t('setting.apiKeyHelperText'),
|
||||
},
|
||||
{
|
||||
name: 'api_version',
|
||||
@@ -171,20 +172,20 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '2024-02-01',
|
||||
helperText: 'Azure OpenAI API 版本',
|
||||
helperText: i18n.t('setting.azureAPIVersionHelperText'),
|
||||
defaultValue: '2024-02-01',
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: '最大token数',
|
||||
label: i18n.t('setting.maxTokens'),
|
||||
type: 'number',
|
||||
required: true,
|
||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||
defaultValue: 4096,
|
||||
validation: {
|
||||
min: { value: 1, message: '最大token数必须大于0' },
|
||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
||||
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -193,7 +194,7 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
|
||||
export const BEDROCK_CONFIG: ConfigFormItem[] = [
|
||||
{
|
||||
name: 'model_type',
|
||||
label: '模型类型',
|
||||
label: i18n.t('setting.modelType'),
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: MODEL_TYPE_OPTIONS.slice(0, 2), // 只支持 chat 和 embedding
|
||||
@@ -201,24 +202,24 @@ export const BEDROCK_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: '模型名称',
|
||||
label: i18n.t('setting.modelName'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入模型名称',
|
||||
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||
},
|
||||
{
|
||||
name: 'bedrock_ak',
|
||||
label: 'ACCESS KEY',
|
||||
type: 'password',
|
||||
required: true,
|
||||
placeholder: '请输入 ACCESS KEY',
|
||||
placeholder: i18n.t('setting.accessKeyPlaceholder'),
|
||||
},
|
||||
{
|
||||
name: 'bedrock_sk',
|
||||
label: 'SECRET KEY',
|
||||
type: 'password',
|
||||
required: true,
|
||||
placeholder: '请输入 SECRET KEY',
|
||||
placeholder: i18n.t('setting.secretKeyPlaceholder'),
|
||||
},
|
||||
{
|
||||
name: 'bedrock_region',
|
||||
@@ -230,14 +231,14 @@ export const BEDROCK_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: '最大token数',
|
||||
label: i18n.t('setting.maxTokens'),
|
||||
type: 'number',
|
||||
required: true,
|
||||
placeholder: '这设置了模型输出的最大长度,以token(单词或词片段)的数量来衡量',
|
||||
helperText: '这设置了模型输出的最大长度,以token(单词或词片段)的数量来衡量',
|
||||
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||
defaultValue: 4096,
|
||||
validation: {
|
||||
min: { value: 1, message: '最大token数必须大于0' },
|
||||
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -246,7 +247,7 @@ export const BEDROCK_CONFIG: ConfigFormItem[] = [
|
||||
export const OLLAMA_CONFIG: ConfigFormItem[] = [
|
||||
{
|
||||
name: 'model_type',
|
||||
label: '模型类型',
|
||||
label: i18n.t('setting.modelType'),
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: MODEL_TYPE_OPTIONS,
|
||||
@@ -254,24 +255,24 @@ export const OLLAMA_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: '模型名称',
|
||||
label: i18n.t('setting.modelName'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '例如: llama2, mistral',
|
||||
helperText: '请输入模型名称',
|
||||
placeholder: i18n.t('setting.ollamaModelNamePlaceholder'),
|
||||
helperText: i18n.t('setting.modelNameHelperText'),
|
||||
},
|
||||
{
|
||||
name: 'api_base',
|
||||
label: '基础 URL',
|
||||
label: i18n.t('setting.baseUrl'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'http://localhost:8888',
|
||||
helperText: '基础 URL',
|
||||
helperText: i18n.t('setting.baseUrlHelperText'),
|
||||
defaultValue: 'http://localhost:11434',
|
||||
validation: {
|
||||
pattern: {
|
||||
value: /^https?:\/\/.+/,
|
||||
message: '基础 URL 必须是有效的 URL',
|
||||
message: i18n.t('setting.baseUrlValidationMessage'),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -279,20 +280,20 @@ export const OLLAMA_CONFIG: ConfigFormItem[] = [
|
||||
name: 'api_key',
|
||||
label: 'API Key',
|
||||
type: 'text',
|
||||
placeholder: '如果需要认证,请输入 API Key',
|
||||
helperText: 'API Key (可选)',
|
||||
placeholder: i18n.t('setting.apiKeyOptionalPlaceholder'),
|
||||
helperText: i18n.t('setting.apiKeyOptional'),
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: '最大 Token 数',
|
||||
label: i18n.t('setting.maxTokens'),
|
||||
type: 'number',
|
||||
required: true,
|
||||
placeholder: '4096',
|
||||
helperText: '模型支持的最大 Token 数',
|
||||
helperText: i18n.t('setting.maxTokensSupportedHelperText'),
|
||||
defaultValue: 4096,
|
||||
validation: {
|
||||
min: { value: 1, message: '最大 Token 数必须大于 0' },
|
||||
max: { value: 100000, message: '最大 Token 数不能超过 100000' },
|
||||
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -300,7 +301,7 @@ export const OLLAMA_CONFIG: ConfigFormItem[] = [
|
||||
export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [
|
||||
{
|
||||
name: 'model_type',
|
||||
label: '模型类型',
|
||||
label: i18n.t('setting.modelType'),
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: MODEL_TYPE_OPTIONS.slice(0, 3),
|
||||
@@ -308,38 +309,38 @@ export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: '模型名称',
|
||||
label: i18n.t('setting.modelName'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入模型名称',
|
||||
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||
},
|
||||
{
|
||||
name: 'yiyan_ak',
|
||||
label: '一言 API KEY',
|
||||
label: i18n.t('setting.baiduYiYanAPIKey'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 API KEY',
|
||||
placeholder: i18n.t('setting.apiKeyPlaceholder'),
|
||||
helperText: 'Baidu YiYan API KEY',
|
||||
},
|
||||
{
|
||||
name: 'yiyan_sk',
|
||||
label: '一言 Secret KEY',
|
||||
label: i18n.t('setting.baiduYiYanSecretKey'),
|
||||
type: 'password',
|
||||
required: true,
|
||||
placeholder: '请输入 Secret KEY',
|
||||
placeholder: i18n.t('setting.secretKeyPlaceholder'),
|
||||
helperText: 'Baidu YiYan Secret KEY',
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: '最大token数',
|
||||
label: i18n.t('setting.maxTokens'),
|
||||
type: 'number',
|
||||
required: true,
|
||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||
defaultValue: 4096,
|
||||
validation: {
|
||||
min: { value: 1, message: '最大token数必须大于0' },
|
||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
||||
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -347,7 +348,7 @@ export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [
|
||||
export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
|
||||
{
|
||||
name: 'model_type',
|
||||
label: '模型类型',
|
||||
label: i18n.t('setting.modelType'),
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [{ value: 'tts', label: 'TTS' },],
|
||||
@@ -355,17 +356,17 @@ export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: '模型名称',
|
||||
label: i18n.t('setting.modelName'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入模型名称',
|
||||
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||
},
|
||||
{
|
||||
name: 'fish_audio_ak',
|
||||
label: 'Fish Audio API KEY',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 API KEY',
|
||||
placeholder: i18n.t('setting.apiKeyPlaceholder'),
|
||||
helperText: 'Fish Audio API KEY',
|
||||
},
|
||||
{
|
||||
@@ -373,20 +374,20 @@ export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
|
||||
label: 'FishAudio Refrence ID',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 Refrence ID',
|
||||
placeholder: i18n.t('setting.fishAudioRefIdPlaceholder'),
|
||||
helperText: 'Fish Audio Refrence ID',
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: '最大token数',
|
||||
label: i18n.t('setting.maxTokens'),
|
||||
type: 'number',
|
||||
required: true,
|
||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||
defaultValue: 4096,
|
||||
validation: {
|
||||
min: { value: 1, message: '最大token数必须大于0' },
|
||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
||||
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -394,7 +395,7 @@ export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
|
||||
export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [
|
||||
{
|
||||
name: 'model_type',
|
||||
label: '模型类型',
|
||||
label: i18n.t('setting.modelType'),
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [{ value: 'chat', label: 'Chat' }, { value: 'image2text', label: 'Image2Text' }],
|
||||
@@ -402,46 +403,46 @@ export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: '模型名称',
|
||||
label: i18n.t('setting.modelName'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入模型名称',
|
||||
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||
},
|
||||
{
|
||||
name: 'google_project_id',
|
||||
label: 'Project ID',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 Project ID',
|
||||
placeholder: i18n.t('setting.googleProjectIdPlaceholder'),
|
||||
helperText: 'Google Cloud Project ID',
|
||||
},
|
||||
{
|
||||
name: 'google_region',
|
||||
label: 'Google Cloud 区域',
|
||||
label: i18n.t('setting.googleCloudRegion'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 Google Cloud 区域',
|
||||
helperText: 'Google Cloud 区域',
|
||||
placeholder: i18n.t('setting.googleCloudRegionPlaceholder'),
|
||||
helperText: i18n.t('setting.googleCloudRegionHelperText'),
|
||||
},
|
||||
{
|
||||
name: 'google_service_account_key',
|
||||
label: 'Google Cloud Service Account Key',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 Google Cloud Service Account Key',
|
||||
placeholder: i18n.t('setting.googleServiceAccountKeyPlaceholder'),
|
||||
helperText: 'Google Cloud Service Account Key',
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: '最大token数',
|
||||
label: i18n.t('setting.maxTokens'),
|
||||
type: 'number',
|
||||
required: true,
|
||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||
defaultValue: 4096,
|
||||
validation: {
|
||||
min: { value: 1, message: '最大token数必须大于0' },
|
||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
||||
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -449,7 +450,7 @@ export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [
|
||||
export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [
|
||||
{
|
||||
name: 'model_type',
|
||||
label: '模型类型',
|
||||
label: i18n.t('setting.modelType'),
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [{ value: 'speech2text', label: 'Speech2Text' }],
|
||||
@@ -457,7 +458,7 @@ export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: '模型名称',
|
||||
label: i18n.t('setting.modelName'),
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
@@ -469,38 +470,38 @@ export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'tencent_ak',
|
||||
label: '腾讯云 Secret ID',
|
||||
label: i18n.t('setting.tencentSecretId'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 Secret ID',
|
||||
helperText: '腾讯云 Secret ID',
|
||||
placeholder: i18n.t('setting.secretIdPlaceholder'),
|
||||
helperText: i18n.t('setting.tencentSecretIdHelperText'),
|
||||
},
|
||||
{
|
||||
name: 'tencent_sk',
|
||||
label: '腾讯云 Secret KEY',
|
||||
label: i18n.t('setting.tencentSecretKey'),
|
||||
type: 'password',
|
||||
required: true,
|
||||
placeholder: '请输入 Secret KEY',
|
||||
helperText: '腾讯云 Secret KEY',
|
||||
placeholder: i18n.t('setting.secretKeyPlaceholder'),
|
||||
helperText: i18n.t('setting.tencentSecretKeyHelperText'),
|
||||
},
|
||||
]
|
||||
|
||||
export const TENCENT_HUNYUAN_CONFIG: ConfigFormItem[] = [
|
||||
{
|
||||
name: 'hunyuan_sid',
|
||||
label: '混元 Secret ID',
|
||||
label: i18n.t('setting.hunyuanSecretId'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 Secret ID',
|
||||
helperText: '混元 Secret ID',
|
||||
placeholder: i18n.t('setting.secretIdPlaceholder'),
|
||||
helperText: i18n.t('setting.hunyuanSecretIdHelperText'),
|
||||
},
|
||||
{
|
||||
name: 'hunyuan_sk',
|
||||
label: '混元 Secret KEY',
|
||||
label: i18n.t('setting.hunyuanSecretKey'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 Secret KEY',
|
||||
helperText: '混元 Secret KEY',
|
||||
placeholder: i18n.t('setting.secretKeyPlaceholder'),
|
||||
helperText: i18n.t('setting.hunyuanSecretKeyHelperText'),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -508,7 +509,7 @@ export const TENCENT_HUNYUAN_CONFIG: ConfigFormItem[] = [
|
||||
export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [
|
||||
{
|
||||
name: 'model_type',
|
||||
label: '模型类型',
|
||||
label: i18n.t('setting.modelType'),
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [{ value: 'chat', label: 'Chat' }, { value: 'tts', label: 'TTS' }],
|
||||
@@ -516,30 +517,30 @@ export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: '模型名称',
|
||||
label: i18n.t('setting.modelName'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入模型名称',
|
||||
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||
},
|
||||
{
|
||||
name: 'xunfei_spark_password',
|
||||
label: '讯飞星火 API Password',
|
||||
label: i18n.t('setting.xunfeiSparkAPIPassword'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 API Password',
|
||||
helperText: '讯飞星火 API Password',
|
||||
placeholder: i18n.t('setting.apiPasswordPlaceholder'),
|
||||
helperText: i18n.t('setting.xunfeiSparkAPIPasswordHelperText'),
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: '最大token数',
|
||||
label: i18n.t('setting.maxTokens'),
|
||||
type: 'number',
|
||||
required: true,
|
||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||
defaultValue: 4096,
|
||||
validation: {
|
||||
min: { value: 1, message: '最大token数必须大于0' },
|
||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
||||
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -548,7 +549,7 @@ export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [
|
||||
export const VOLC_ENGINE_CONFIG: ConfigFormItem[] = [
|
||||
{
|
||||
name: 'model_type',
|
||||
label: '模型类型',
|
||||
label: i18n.t('setting.modelType'),
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [{ value: 'chat', label: 'Chat' }, { value: 'embedding', label: 'Embedding' }],
|
||||
@@ -556,38 +557,38 @@ export const VOLC_ENGINE_CONFIG: ConfigFormItem[] = [
|
||||
},
|
||||
{
|
||||
name: 'llm_name',
|
||||
label: '模型名称',
|
||||
label: i18n.t('setting.modelName'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入模型名称',
|
||||
placeholder: i18n.t('setting.modelNamePlaceholder'),
|
||||
},
|
||||
{
|
||||
name: 'endpoint_id',
|
||||
label: '模型 EndpointID',
|
||||
label: i18n.t('setting.modelEndpointId'),
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: '请输入 EndpointID',
|
||||
helperText: '模型 EndpointID',
|
||||
placeholder: i18n.t('setting.endpointIdPlaceholder'),
|
||||
helperText: i18n.t('setting.modelEndpointIdHelperText'),
|
||||
},
|
||||
{
|
||||
name: 'ark_api_key',
|
||||
label: '火山 ARK_API_KEY',
|
||||
label: i18n.t('setting.volcEngineARKAPIKey'),
|
||||
type: 'password',
|
||||
required: true,
|
||||
placeholder: '请输入 ARK_API_KEY',
|
||||
helperText: '模型 ARK_API_KEY',
|
||||
placeholder: i18n.t('setting.arkApiKeyPlaceholder'),
|
||||
helperText: i18n.t('setting.modelARKAPIKeyHelperText'),
|
||||
},
|
||||
{
|
||||
name: 'max_tokens',
|
||||
label: '最大token数',
|
||||
label: i18n.t('setting.maxTokens'),
|
||||
type: 'number',
|
||||
required: true,
|
||||
placeholder: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
helperText: '设置了模型输出的最大长度,以token(单词片段)的数量表示',
|
||||
placeholder: i18n.t('setting.maxTokensPlaceholder'),
|
||||
helperText: i18n.t('setting.maxTokensHelperText'),
|
||||
defaultValue: 4096,
|
||||
validation: {
|
||||
min: { value: 1, message: '最大token数必须大于0' },
|
||||
max: { value: 100000, message: '最大token数不能超过100000' },
|
||||
min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
|
||||
max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LlmSvgIcon } from "@/components/AppSvgIcon";
|
||||
import { IconMap, type LLMFactory } from "@/constants/llm";
|
||||
import type { IFactory } from "@/interfaces/database/llm";
|
||||
import { Box, Button, Card, CardContent, Chip, Typography } from "@mui/material";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// 模型类型标签颜色映射
|
||||
export const MODEL_TYPE_COLORS: Record<string, string> = {
|
||||
@@ -27,6 +28,7 @@ const LLMFactoryCard: React.FC<ModelFactoryCardProps> = ({
|
||||
factory,
|
||||
onConfigure,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 获取工厂图标名称
|
||||
const getFactoryIconName = (factoryName: LLMFactory) => {
|
||||
@@ -89,7 +91,7 @@ const LLMFactoryCard: React.FC<ModelFactoryCardProps> = ({
|
||||
width: 'fit-content'
|
||||
}}
|
||||
>
|
||||
添加模型
|
||||
{t('setting.addModel')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -55,8 +55,6 @@ const McpDialog: React.FC<McpDialogProps> = ({
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
|
||||
logger.debug('McpDialog 组件加载', initialData);
|
||||
|
||||
const handleSubmit = useCallback(async (data: ICreateMcpServerRequestBody) => {
|
||||
setSubmitLoading(true);
|
||||
setSubmitError(null);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { styled } from '@mui/material/styles';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { IMcpServer } from '@/interfaces/database/mcp';
|
||||
import type { ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
||||
import { Mode } from '@mui/icons-material';
|
||||
@@ -74,6 +75,7 @@ const McpForm: React.FC<McpFormProps> = ({
|
||||
loading = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [formData, setFormData] = useState<McpFormData>({
|
||||
name: initialData?.name || '',
|
||||
url: initialData?.url || '',
|
||||
@@ -131,17 +133,17 @@ const McpForm: React.FC<McpFormProps> = ({
|
||||
const errors: Partial<McpFormData> = {};
|
||||
|
||||
if (!formData.name?.trim()) {
|
||||
errors.name = '名称不能为空';
|
||||
errors.name = t('setting.nameRequired');
|
||||
}
|
||||
|
||||
if (!formData.url.trim()) {
|
||||
errors.url = 'URL不能为空';
|
||||
errors.url = t('setting.urlRequired');
|
||||
} else if (!/^https?:\/\/.+/.test(formData.url)) {
|
||||
errors.url = 'URL格式不正确';
|
||||
errors.url = t('setting.urlFormatInvalid');
|
||||
}
|
||||
|
||||
if (!formData.server_type) {
|
||||
errors.server_type = '请选择服务器类型';
|
||||
errors.server_type = t('setting.serverTypeRequired');
|
||||
}
|
||||
|
||||
setFormErrors(errors);
|
||||
@@ -166,7 +168,7 @@ const McpForm: React.FC<McpFormProps> = ({
|
||||
if (!formData.url.trim()) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
error: '请先填写 URL',
|
||||
error: t('setting.fillUrlFirst'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -205,13 +207,13 @@ const McpForm: React.FC<McpFormProps> = ({
|
||||
} else {
|
||||
setTestResult({
|
||||
success: false,
|
||||
error: result.error || '测试失败',
|
||||
error: result.error || t('setting.testFailed'),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
error: error.message || '测试连接时发生错误',
|
||||
error: error.message || t('setting.testConnectionError'),
|
||||
});
|
||||
} finally {
|
||||
setTestLoading(false);
|
||||
@@ -234,7 +236,7 @@ const McpForm: React.FC<McpFormProps> = ({
|
||||
return (
|
||||
<FormContainer>
|
||||
<TextField
|
||||
label="名称"
|
||||
label={t('common.name')}
|
||||
value={formData.name || ''}
|
||||
onChange={handleInputChange('name')}
|
||||
error={!!formErrors.name}
|
||||
@@ -257,12 +259,12 @@ const McpForm: React.FC<McpFormProps> = ({
|
||||
/>
|
||||
|
||||
<FormControl fullWidth required error={!!formErrors.server_type}>
|
||||
<InputLabel>Server Type</InputLabel>
|
||||
<InputLabel>{t('mcp.serverType')}</InputLabel>
|
||||
<Select
|
||||
value={formData.server_type}
|
||||
onChange={handleInputChange('server_type')}
|
||||
disabled={disabled}
|
||||
label="Server Type"
|
||||
label={t('mcp.serverType')}
|
||||
>
|
||||
<MenuItem value="sse">SSE</MenuItem>
|
||||
<MenuItem value="streamable-http">Streamable HTTP</MenuItem>
|
||||
@@ -276,21 +278,21 @@ const McpForm: React.FC<McpFormProps> = ({
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
placeholder="e.g. eyJhbGciOiJIUzI1Ni..."
|
||||
helperText="可选:用于身份验证的令牌"
|
||||
helperText={t('setting.authTokenOptional')}
|
||||
/>
|
||||
|
||||
{/* 测试连接部分 */}
|
||||
{onTest && (
|
||||
<TestSection>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Typography variant="h6">测试连接</Typography>
|
||||
<Typography variant="h6">{t('setting.testConnection')}</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleTest}
|
||||
disabled={testLoading || disabled}
|
||||
startIcon={testLoading ? <CircularProgress size={16} /> : <RefreshIcon />}
|
||||
>
|
||||
{testLoading ? '测试中...' : '测试连接'}
|
||||
{testLoading ? t('setting.testing') : t('setting.testConnection')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -299,16 +301,16 @@ const McpForm: React.FC<McpFormProps> = ({
|
||||
{testResult.success ? (
|
||||
<Box>
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
连接成功!发现 {testResult.tools?.length || 0} 个工具
|
||||
{t('setting.connectionSuccess', { count: testResult.tools?.length || 0 })}
|
||||
</Alert>
|
||||
|
||||
{testResult.tools && testResult.tools.length > 0 && (
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography variant="subtitle1">可用工具</Typography>
|
||||
<Typography variant="subtitle1">{t('setting.availableTools')}</Typography>
|
||||
<Chip
|
||||
label={`${testResult.tools.length} tools available`}
|
||||
label={`${testResult.tools.length} ${t('mcp.toolsAvailable')}`}
|
||||
size="small"
|
||||
color="primary"
|
||||
/>
|
||||
@@ -344,7 +346,7 @@ const McpForm: React.FC<McpFormProps> = ({
|
||||
</Box>
|
||||
) : (
|
||||
<Alert severity="error">
|
||||
{testResult.error || '连接失败'}
|
||||
{testResult.error || t('setting.connectionFailed')}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
@@ -360,7 +362,7 @@ const McpForm: React.FC<McpFormProps> = ({
|
||||
disabled={loading || disabled || !testResult?.success}
|
||||
startIcon={loading ? <CircularProgress size={16} /> : undefined}
|
||||
>
|
||||
{loading ? '保存中...' : '保存'}
|
||||
{loading ? t('setting.saving') : t('common.save')}
|
||||
</Button>
|
||||
</Box>
|
||||
</FormContainer>
|
||||
|
||||
@@ -2,8 +2,6 @@ import React from 'react';
|
||||
|
||||
// 导入独立的对话框组件
|
||||
import ApiKeyDialog, { type ApiKeyFormData, type ApiKeyDialogProps } from './Dialog/ApiKeyDialog';
|
||||
import AzureOpenAIDialog, { type AzureOpenAIFormData, type AzureOpenAIDialogProps } from './Dialog/AzureOpenAIDialog';
|
||||
import BedrockDialog, { type BedrockFormData, type BedrockDialogProps, BEDROCK_REGIONS } from './Dialog/BedrockDialog';
|
||||
import OllamaDialog, { type OllamaFormData, type OllamaDialogProps } from './Dialog/OllamaDialog';
|
||||
import SystemModelDialog, { type SystemModelFormData, type SystemModelDialogProps, type ModelOption, type ModelGroup } from './Dialog/SystemModelDialog';
|
||||
import ConfigurationDialog, { type ConfigurationFormData, type ConfigurationDialogProps, type ConfigFormItem, type DocLinkConfig } from './Dialog/ConfigurationDialog';
|
||||
@@ -19,14 +17,13 @@ export interface BaseDialogProps {
|
||||
}
|
||||
|
||||
// 导出所有表单数据接口
|
||||
export type { ApiKeyFormData, AzureOpenAIFormData, BedrockFormData, OllamaFormData, SystemModelFormData, ConfigurationFormData };
|
||||
export type { ApiKeyFormData, OllamaFormData, SystemModelFormData, ConfigurationFormData };
|
||||
|
||||
// 导出所有对话框 Props 接口
|
||||
export type { ApiKeyDialogProps, AzureOpenAIDialogProps, BedrockDialogProps, OllamaDialogProps, SystemModelDialogProps, ConfigurationDialogProps };
|
||||
export type { ApiKeyDialogProps, OllamaDialogProps, SystemModelDialogProps, ConfigurationDialogProps };
|
||||
|
||||
// 导出其他相关接口和常量
|
||||
export type { ModelOption, ModelGroup };
|
||||
export { BEDROCK_REGIONS };
|
||||
|
||||
// 模型对话框整合组件的 Props 接口
|
||||
export interface ModelDialogsProps {
|
||||
@@ -39,22 +36,6 @@ export interface ModelDialogsProps {
|
||||
initialData?: ApiKeyFormData;
|
||||
editMode?: boolean;
|
||||
};
|
||||
azureDialog: {
|
||||
open: boolean;
|
||||
closeDialog: () => void;
|
||||
submitAzureOpenAI: (data: AzureOpenAIFormData) => void;
|
||||
loading: boolean;
|
||||
initialData?: AzureOpenAIFormData;
|
||||
editMode?: boolean;
|
||||
};
|
||||
bedrockDialog: {
|
||||
open: boolean;
|
||||
closeDialog: () => void;
|
||||
submitBedrock: (data: BedrockFormData) => void;
|
||||
loading: boolean;
|
||||
initialData?: BedrockFormData;
|
||||
editMode?: boolean;
|
||||
};
|
||||
ollamaDialog: {
|
||||
open: boolean;
|
||||
closeDialog: () => void;
|
||||
@@ -93,8 +74,6 @@ export interface ModelDialogsProps {
|
||||
*/
|
||||
export const ModelDialogs: React.FC<ModelDialogsProps> = ({
|
||||
apiKeyDialog,
|
||||
azureDialog,
|
||||
bedrockDialog,
|
||||
ollamaDialog,
|
||||
configurationDialog,
|
||||
systemDialog,
|
||||
@@ -112,26 +91,6 @@ export const ModelDialogs: React.FC<ModelDialogsProps> = ({
|
||||
editMode={apiKeyDialog!.editMode}
|
||||
/>
|
||||
|
||||
{/* Azure OpenAI 对话框 */}
|
||||
<AzureOpenAIDialog
|
||||
open={azureDialog.open}
|
||||
onClose={azureDialog.closeDialog}
|
||||
onSubmit={azureDialog.submitAzureOpenAI}
|
||||
loading={azureDialog.loading}
|
||||
initialData={azureDialog.initialData}
|
||||
editMode={azureDialog.editMode}
|
||||
/>
|
||||
|
||||
{/* AWS Bedrock 对话框 */}
|
||||
<BedrockDialog
|
||||
open={bedrockDialog.open}
|
||||
onClose={bedrockDialog.closeDialog}
|
||||
onSubmit={bedrockDialog.submitBedrock}
|
||||
loading={bedrockDialog.loading}
|
||||
initialData={bedrockDialog.initialData}
|
||||
editMode={bedrockDialog.editMode}
|
||||
/>
|
||||
|
||||
{/* Ollama 对话框 */}
|
||||
<OllamaDialog
|
||||
open={ollamaDialog.open}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Tooltip
|
||||
} from '@mui/material';
|
||||
import { PhotoCamera, Edit } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useProfileSetting } from '@/hooks/setting-hooks';
|
||||
import { useMessage } from '@/hooks/useSnackbar';
|
||||
import type { IUserInfo } from '@/interfaces/database/user-setting';
|
||||
@@ -45,6 +46,7 @@ interface ProfileFormProps {
|
||||
* 个人信息表单
|
||||
*/
|
||||
function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const showMessage = useMessage();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -91,13 +93,13 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||
if (file) {
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showMessage.error('请选择图片文件');
|
||||
showMessage.error(t('setting.pleaseSelectImageFile'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件大小 (限制为2MB)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
showMessage.error('图片大小不能超过2MB');
|
||||
showMessage.error(t('setting.imageSizeLimit'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,7 +125,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (!formData.nickname?.trim()) {
|
||||
showMessage.error('用户名不能为空');
|
||||
showMessage.error(t('setting.usernameRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -136,17 +138,17 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||
};
|
||||
|
||||
await onSubmit(updateData);
|
||||
showMessage.success('个人信息更新成功');
|
||||
showMessage.success(t('setting.profileUpdateSuccess'));
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error);
|
||||
showMessage.error('更新失败,请重试');
|
||||
showMessage.error(t('setting.updateFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper elevation={0} sx={{ p: 3, backgroundColor: 'transparent' }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ mb: 3, fontWeight: 600 }}>
|
||||
个人资料
|
||||
{t('setting.personalProfile')}
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
@@ -166,9 +168,9 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
头像
|
||||
{t('setting.avatar')}
|
||||
</Typography>
|
||||
<Tooltip title="上传头像">
|
||||
<Tooltip title={t('setting.uploadAvatar')}>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={triggerFileSelect}
|
||||
@@ -192,7 +194,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 1 }}>
|
||||
支持 JPG、PNG 格式,文件大小不超过 2MB
|
||||
{t('setting.avatarFormatTip')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -202,7 +204,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="用户名"
|
||||
label={t('setting.username')}
|
||||
value={formData.nickname}
|
||||
onChange={handleInputChange('nickname')}
|
||||
variant="outlined"
|
||||
@@ -214,21 +216,21 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="邮箱"
|
||||
label={t('setting.email')}
|
||||
value={formData.email}
|
||||
variant="outlined"
|
||||
disabled
|
||||
helperText="邮箱地址不可修改"
|
||||
helperText={t('setting.emailNotEditable')}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 语言 */}
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>语言</InputLabel>
|
||||
<InputLabel>{t('setting.language')}</InputLabel>
|
||||
<Select
|
||||
value={formData.language}
|
||||
label="语言"
|
||||
label={t('setting.language')}
|
||||
onChange={handleSelectChange('language')}
|
||||
>
|
||||
{languageOptions.map((option) => (
|
||||
@@ -243,10 +245,10 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||
{/* 时区 */}
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>时区</InputLabel>
|
||||
<InputLabel>{t('setting.timezone')}</InputLabel>
|
||||
<Select
|
||||
value={formData.timezone}
|
||||
label="时区"
|
||||
label={t('setting.timezone')}
|
||||
onChange={handleSelectChange('timezone')}
|
||||
>
|
||||
{timezoneOptions.map((option) => (
|
||||
@@ -266,7 +268,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||
onClick={handleSave}
|
||||
sx={{ minWidth: 120 }}
|
||||
>
|
||||
保存
|
||||
{t('setting.save')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
@@ -35,6 +35,7 @@ import McpDialog from '@/pages/setting/components/McpDialog';
|
||||
import type { IMcpServer } from '@/interfaces/database/mcp';
|
||||
import type { IImportMcpServersRequestBody, ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
||||
import { useMessage } from '@/hooks/useSnackbar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const PageContainer = styled(Box)(({ theme }) => ({
|
||||
@@ -69,6 +70,7 @@ const SearchContainer = styled(Box)(({ theme }) => ({
|
||||
|
||||
|
||||
export default function McpSettingPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleRefreshServer = async (initial?: boolean) => {
|
||||
if (initial) {
|
||||
@@ -187,9 +189,9 @@ export default function McpSettingPage() {
|
||||
if (selectedServerId) {
|
||||
const result = await deleteMcpServer(selectedServerId);
|
||||
if (result.success) {
|
||||
showMessage.success('删除成功');
|
||||
showMessage.success(t('mcp.deleteSuccess'));
|
||||
} else {
|
||||
showMessage.error(result.error || '删除失败');
|
||||
showMessage.error(t('mcp.deleteFailed'));
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
@@ -198,19 +200,19 @@ export default function McpSettingPage() {
|
||||
const handleBulkDelete = async () => {
|
||||
const result = await batchDeleteMcpServers(selectedServers);
|
||||
if (result.success) {
|
||||
showMessage.success('批量删除成功');
|
||||
showMessage.success(t('mcp.batchDeleteSuccess'));
|
||||
setSelectedServers([]);
|
||||
} else {
|
||||
showMessage.error(result.error || '批量删除失败');
|
||||
showMessage.error(t('mcp.batchDeleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
const result = await exportMcpServers(selectedServers);
|
||||
if (result.success) {
|
||||
showMessage.success('导出成功');
|
||||
showMessage.success(t('mcp.exportSuccess'));
|
||||
} else {
|
||||
showMessage.error(result.error || '导出失败');
|
||||
showMessage.error(t('mcp.exportFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -218,12 +220,12 @@ export default function McpSettingPage() {
|
||||
try {
|
||||
if (editingServer) {
|
||||
if (!editingServer.id) {
|
||||
showMessage.error('server id 不能为空');
|
||||
showMessage.error(t('mcp.serverIdRequired'));
|
||||
return { success: false, error: 'server id 不能为空' };
|
||||
}
|
||||
const result = await updateMcpServer({ ...data, mcp_id: editingServer.id ?? '' });
|
||||
if (result.success) {
|
||||
showMessage.success('MCP 服务器更新成功');
|
||||
showMessage.success(t('mcp.mcpServerUpdateSuccess'));
|
||||
setMcpDialogOpen(false);
|
||||
setEditingServer(null);
|
||||
return result;
|
||||
@@ -233,7 +235,7 @@ export default function McpSettingPage() {
|
||||
} else {
|
||||
const result = await createMcpServer(data);
|
||||
if (result.success) {
|
||||
showMessage.success('MCP 服务器创建成功');
|
||||
showMessage.success(t('mcp.mcpServerCreateSuccess'));
|
||||
setMcpDialogOpen(false);
|
||||
setEditingServer(null);
|
||||
return result;
|
||||
@@ -251,7 +253,7 @@ export default function McpSettingPage() {
|
||||
try {
|
||||
const result = await testMcpServer(data);
|
||||
if (result.success) {
|
||||
showMessage.success('测试成功');
|
||||
showMessage.success(t('mcp.testSuccess'));
|
||||
return result;
|
||||
} else {
|
||||
return result;
|
||||
@@ -283,7 +285,7 @@ export default function McpSettingPage() {
|
||||
setImportJson('');
|
||||
}
|
||||
} else {
|
||||
showMessage.error('JSON 格式错误');
|
||||
showMessage.error(t('mcp.jsonFormatError'));
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage.error('JSON 格式错误');
|
||||
@@ -294,11 +296,11 @@ export default function McpSettingPage() {
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<Box>
|
||||
<Typography variant="h4" fontWeight={600} color="#333">
|
||||
MCP Servers
|
||||
<Typography variant="h5" component="h1" fontWeight="bold">
|
||||
{t('mcp.mcpServers')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||
Customize the list of MCP servers
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('mcp.customizeTheListOfMcpServers')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" gap={2}>
|
||||
@@ -307,14 +309,14 @@ export default function McpSettingPage() {
|
||||
startIcon={<ImportIcon />}
|
||||
onClick={() => setImportDialogOpen(true)}
|
||||
>
|
||||
Import
|
||||
{t('mcp.import')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
Add MCP
|
||||
{t('mcp.addMCP')}
|
||||
</Button>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
@@ -323,7 +325,7 @@ export default function McpSettingPage() {
|
||||
<Box>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Search MCP servers..."
|
||||
placeholder={t('mcp.searchPlaceholder')}
|
||||
value={searchString}
|
||||
onChange={handleSearchChange}
|
||||
InputProps={{
|
||||
@@ -338,16 +340,17 @@ export default function McpSettingPage() {
|
||||
size="small"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={handleBulkDelete}
|
||||
color="error"
|
||||
disabled={selectedServers.length === 0}
|
||||
>
|
||||
Delete ({selectedServers.length})
|
||||
{t('mcp.deleteSelected')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<ExportIcon />}
|
||||
onClick={handleExport}
|
||||
disabled={selectedServers.length === 0}
|
||||
>
|
||||
Export ({selectedServers.length})
|
||||
{t('mcp.exportSelected')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
@@ -389,10 +392,10 @@ export default function McpSettingPage() {
|
||||
{server.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
类型: {server.server_type}
|
||||
{t('mcp.type')}: {server.server_type}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
更新时间: {dayjs(server.update_date).format('YYYY-MM-DD HH:mm:ss')}
|
||||
{t('mcp.updateTime')}: {dayjs(server.update_date).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -409,7 +412,7 @@ export default function McpSettingPage() {
|
||||
color="primary"
|
||||
/>
|
||||
<Typography variant="body2" sx={{ ml: 2 }}>
|
||||
共 {total} 条
|
||||
{t('mcp.totalItems', { count: total })}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
@@ -423,11 +426,11 @@ export default function McpSettingPage() {
|
||||
>
|
||||
<MenuItem onClick={handleEdit}>
|
||||
<EditIcon sx={{ mr: 1 }} fontSize="small" />
|
||||
Edit
|
||||
{t('common.edit')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
|
||||
<DeleteIcon sx={{ mr: 1 }} fontSize="small" />
|
||||
Delete
|
||||
{t('common.delete')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -446,16 +449,16 @@ export default function McpSettingPage() {
|
||||
|
||||
{/* 导入对话框 */}
|
||||
<Dialog open={importDialogOpen} onClose={() => setImportDialogOpen(false)} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Import MCP Servers</DialogTitle>
|
||||
<DialogTitle>{t('mcp.importTitle')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
Paste your MCP servers JSON configuration below. The format should match the Mock.json structure.
|
||||
{t('mcp.importDescription')}
|
||||
</Alert>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={10}
|
||||
label="JSON Configuration"
|
||||
label={t('mcp.jsonConfiguration')}
|
||||
value={importJson}
|
||||
onChange={(e) => setImportJson(e.target.value)}
|
||||
placeholder={`{
|
||||
@@ -472,9 +475,9 @@ export default function McpSettingPage() {
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setImportDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={() => setImportDialogOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleImport} variant="contained">
|
||||
Import
|
||||
{t('mcp.import')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLlmModelSetting } from '@/hooks/setting-hooks';
|
||||
import { useModelDialogs } from './hooks/useModelDialogs';
|
||||
import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm';
|
||||
@@ -66,6 +67,7 @@ function MyLlmGridItem({ model, onDelete }: { model: ILlmItem, onDelete: (model:
|
||||
|
||||
// 主页面组件
|
||||
function ModelsPage() {
|
||||
const { t } = useTranslation();
|
||||
const { llmFactory, myLlm, refreshLlmModel } = useLlmModelSetting();
|
||||
const modelDialogs = useModelDialogs(refreshLlmModel);
|
||||
|
||||
@@ -106,12 +108,15 @@ function ModelsPage() {
|
||||
LLM_FACTORY_LIST.VolcEngine,
|
||||
]
|
||||
if (LocalLlmFactories.includes(factoryName)) {
|
||||
// local llm
|
||||
modelDialogs.ollamaDialog.openDialog({
|
||||
llm_factory: factory.name,
|
||||
});
|
||||
} else if (configurationFactories.includes(factoryName)) {
|
||||
// custom configuration llm
|
||||
modelDialogs.configurationDialog.openConfigurationDialog(factory.name);
|
||||
} else {
|
||||
// llm set api
|
||||
modelDialogs.apiKeyDialog.openApiKeyDialog(factoryName);
|
||||
}
|
||||
logger.debug('handleConfigureFactory', factory);
|
||||
@@ -129,26 +134,26 @@ function ModelsPage() {
|
||||
// 处理删除单个模型
|
||||
const handleDeleteModel = useCallback(async (factoryName: string, modelName: string) => {
|
||||
dialog.confirm({
|
||||
title: '确认删除',
|
||||
content: `是否确认删除模型 ${modelName}?`,
|
||||
title: t('setting.confirmDelete'),
|
||||
content: t('setting.confirmDeleteModel', { modelName }),
|
||||
showCancel: true,
|
||||
onConfirm: async () => {
|
||||
await modelDialogs.deleteOps.deleteLlm(factoryName, modelName);
|
||||
},
|
||||
});
|
||||
}, [dialog, modelDialogs.deleteOps]);
|
||||
}, [dialog, modelDialogs.deleteOps, t]);
|
||||
|
||||
// 处理删除模型工厂
|
||||
const handleDeleteFactory = useCallback(async (factoryName: string) => {
|
||||
dialog.confirm({
|
||||
title: '确认删除',
|
||||
content: `是否确认删除模型工厂 ${factoryName}?`,
|
||||
title: t('setting.confirmDelete'),
|
||||
content: t('setting.confirmDeleteFactory', { factoryName }),
|
||||
showCancel: true,
|
||||
onConfirm: async () => {
|
||||
await modelDialogs.deleteOps.deleteFactory(factoryName);
|
||||
},
|
||||
});
|
||||
}, [dialog, modelDialogs.deleteOps]);
|
||||
}, [dialog, modelDialogs.deleteOps, t]);
|
||||
|
||||
if (!llmFactory || !myLlm) {
|
||||
return (
|
||||
@@ -168,28 +173,28 @@ function ModelsPage() {
|
||||
}}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
模型设置
|
||||
{t('setting.modelSettings')}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" gutterBottom>
|
||||
管理您的 LLM 模型工厂和个人模型配置
|
||||
{t('setting.modelSettingsDescription')}
|
||||
</Typography>
|
||||
</Box>
|
||||
{/* 设置默认模型 */}
|
||||
<Button variant="contained" color="primary" onClick={() => modelDialogs.systemDialog.openDialog()}>
|
||||
设置默认模型
|
||||
{t('setting.setDefaultModel')}
|
||||
</Button>
|
||||
</Box>
|
||||
{/* My LLM 部分 */}
|
||||
<Box mb={4} mt={2}>
|
||||
{!myLlm || Object.keys(myLlm).length === 0 ? (
|
||||
<Alert severity="info">
|
||||
您还没有配置任何 LLM 模型。请在下方的模型工厂中进行配置。
|
||||
{t('setting.noModelsConfigured')}
|
||||
</Alert>
|
||||
) : (
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
我的 LLM 模型
|
||||
{t('setting.myLlmModels')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
@@ -242,13 +247,13 @@ function ModelsPage() {
|
||||
variant='contained' color='primary' startIcon={<EditIcon />}
|
||||
onClick={() => handleEditLlmFactory(factoryName)}
|
||||
>
|
||||
编辑
|
||||
{t('setting.edit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined' color='primary' startIcon={<DeleteIcon />}
|
||||
onClick={() => handleDeleteFactory(factoryName)}
|
||||
>
|
||||
删除
|
||||
{t('setting.delete')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -281,7 +286,7 @@ function ModelsPage() {
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
LLM 模型工厂
|
||||
{t('setting.llmModelFactories')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Button, Divider, Typography } from "@mui/material";
|
||||
import { Lock } from "@mui/icons-material";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ProfileForm from "./components/ProfileForm";
|
||||
import ChangePasswordDialog from "./components/ChangePasswordDialog";
|
||||
import { useProfileSetting } from "@/hooks/setting-hooks";
|
||||
import logger from "@/utils/logger";
|
||||
|
||||
function ProfileSetting() {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting();
|
||||
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||
|
||||
@@ -31,10 +33,10 @@ function ProfileSetting() {
|
||||
{/* 密码修改部分 */}
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ mb: 2, fontWeight: 600 }}>
|
||||
账户安全
|
||||
{t('setting.accountSecurity')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
定期更新密码有助于保护您的账户安全
|
||||
{t('setting.passwordUpdateTip')}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
@@ -51,7 +53,7 @@ function ProfileSetting() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
修改密码
|
||||
{t('setting.changePassword')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Divider,
|
||||
Paper,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Storage as StorageIcon,
|
||||
Memory as RedisIcon,
|
||||
@@ -39,6 +40,7 @@ const TITLE_MAP = {
|
||||
task_executor_heartbeat: 'Task Executor',
|
||||
};
|
||||
|
||||
|
||||
// 图标映射
|
||||
const ICON_MAP = {
|
||||
es: SearchIcon,
|
||||
@@ -49,7 +51,16 @@ const ICON_MAP = {
|
||||
};
|
||||
|
||||
function SystemSetting() {
|
||||
const { t } = useTranslation();
|
||||
const { systemStatus, loading, error, fetchSystemStatus } = useSystemStatus();
|
||||
const componentTitleMap: Record<string, string> = {
|
||||
'doc_engine': t('setting.docEngine'),
|
||||
'object_storage': t('setting.objectStorage'),
|
||||
'redis': t('setting.redis'),
|
||||
'database': t('setting.database'),
|
||||
'elasticsearch': t('setting.elasticsearch'),
|
||||
'task_executor': t('setting.taskExecutor'),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSystemStatus();
|
||||
@@ -170,11 +181,11 @@ function SystemSetting() {
|
||||
<Box p={3}>
|
||||
<Box mb={4}>
|
||||
<Typography variant="h4" component="h1" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||
系统状态
|
||||
{t('setting.systemStatus')}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" paragraph>
|
||||
查看系统各个组件的运行状态和性能指标
|
||||
{t('setting.systemStatusDescription')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -186,7 +197,7 @@ function SystemSetting() {
|
||||
</Grid>
|
||||
) : (
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
暂无系统状态数据
|
||||
{t('setting.noSystemStatusData')}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -270,7 +270,7 @@ function TeamsSetting() {
|
||||
variant="outlined"
|
||||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
placeholder={t('setting.emailPlaceholder') || '请输入邀请用户的邮箱地址'}
|
||||
placeholder={t('setting.emailPlaceholder')}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -282,7 +282,7 @@ function TeamsSetting() {
|
||||
variant="contained"
|
||||
disabled={loading || !inviteEmail.trim()}
|
||||
>
|
||||
{loading ? t('setting.inviting', 'inviting') : t('setting.invite')}
|
||||
{loading ? t('setting.inviting') : t('setting.invite')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -139,8 +139,7 @@ request.interceptors.response.use(
|
||||
redirectToLogin();
|
||||
} else if (data?.code !== 0) {
|
||||
// 处理其他业务错误
|
||||
logger.info('请求出现错误:', data?.message);
|
||||
const error = new CustomError(data?.message || '请求出现错误');
|
||||
const error = new CustomError(data?.message || i18n.t('message.requestError'));
|
||||
error.code = data?.code || -1;
|
||||
error.response = data;
|
||||
snackbar.warning(error.message);
|
||||
|
||||
Reference in New Issue
Block a user