feat(i18n): add internationalization support across multiple components

This commit is contained in:
2025-10-27 14:41:58 +08:00
parent 49742f6219
commit 46cc8a254a
23 changed files with 777 additions and 2623 deletions

View File

@@ -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>
);
}

View File

@@ -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 settingMenuItems: SettingMenuItem[] = [
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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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') },
},
},
]

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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}

View File

@@ -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 }}>
JPGPNG 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);