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 { BaseBreadcrumbs, type BreadcrumbItem } from '@/components/Breadcrumbs';
import SettingSidebar from './SettingSidebar'; import SettingSidebar from './SettingSidebar';
import { Home as HomeIcon } from '@mui/icons-material'; import { Home as HomeIcon } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import LanguageSwitcher from '../LanguageSwitcher';
const LayoutContainer = styled(Box)({ const LayoutContainer = styled(Box)({
display: 'flex', display: 'flex',
@@ -21,6 +23,9 @@ const HeaderContainer = styled(Box)(({ theme }) => ({
padding: theme.spacing(2, 3), padding: theme.spacing(2, 3),
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`, borderBottom: `1px solid ${theme.palette.divider}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
})); }));
const MainContent = styled(Box)({ const MainContent = styled(Box)({
@@ -29,23 +34,28 @@ const MainContent = styled(Box)({
padding: '24px', padding: '24px',
}); });
// 设置页面路径映射
const settingPathMap: Record<string, string> = {
'/setting/profile': '个人资料',
'/setting/models': '模型配置',
'/setting/system': '系统设置',
'/setting/teams': '团队管理',
'/setting/mcp': 'MCP配置',
};
function SettingHeader() { function SettingHeader() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); 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[] = [ const breadcrumbItems: BreadcrumbItem[] = [
{ {
label: '首页', label: t('header.home'),
path: '/', path: '/',
onClick: () => navigate('/'), onClick: () => navigate('/'),
}, },
@@ -65,6 +75,7 @@ function SettingHeader() {
items={breadcrumbItems} items={breadcrumbItems}
sx={{ mb: 0 }} sx={{ mb: 0 }}
/> />
<LanguageSwitcher textColor='text.primary' />
</HeaderContainer> </HeaderContainer>
); );
} }

View File

@@ -8,6 +8,8 @@ import {
SmartToy as SmartToyIcon, SmartToy as SmartToyIcon,
Extension as ExtensionIcon, Extension as ExtensionIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
interface SettingMenuItem { interface SettingMenuItem {
key: string; key: string;
@@ -16,40 +18,43 @@ interface SettingMenuItem {
path: string; path: string;
} }
const SettingSidebar: React.FC = () => {
const { t } = useTranslation();
const settingMenuItems: SettingMenuItem[] = [ const settingMenuItems: SettingMenuItem[] = [
{ {
key: 'profile', key: 'profile',
label: '个人资料', label: t('setting.profile'),
icon: PersonIcon, icon: PersonIcon,
path: '/setting/profile', path: '/setting/profile',
}, },
{ {
key: 'models', key: 'models',
label: '模型配置', label: t('setting.model'),
icon: SmartToyIcon, icon: SmartToyIcon,
path: '/setting/models', path: '/setting/models',
}, },
{ {
key: 'system', key: 'system',
label: '系统设置', label: t('setting.system'),
icon: ComputerIcon, icon: ComputerIcon,
path: '/setting/system', path: '/setting/system',
}, },
{ {
key: 'teams', key: 'teams',
label: '团队管理', label: t('setting.team'),
icon: GroupIcon, icon: GroupIcon,
path: '/setting/teams', path: '/setting/teams',
}, },
{ {
key: 'mcp', key: 'mcp',
label: 'MCP配置', label: t('setting.mcp'),
icon: ExtensionIcon, icon: ExtensionIcon,
path: '/setting/mcp', path: '/setting/mcp',
}, },
]; ];
const SettingSidebar: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@@ -79,7 +84,7 @@ const SettingSidebar: React.FC = () => {
letterSpacing: '0.5px', letterSpacing: '0.5px',
}} }}
> >
{t('header.setting')}
</Typography> </Typography>
<List> <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 Alert
} from '@mui/material'; } from '@mui/material';
import { Visibility, VisibilityOff, Close } from '@mui/icons-material'; import { Visibility, VisibilityOff, Close } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import { useSnackbar } from '@/hooks/useSnackbar'; import { useSnackbar } from '@/hooks/useSnackbar';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
@@ -32,6 +33,7 @@ interface PasswordFormData {
* 修改密码对话框 * 修改密码对话框
*/ */
function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePasswordDialogProps) { function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePasswordDialogProps) {
const { t } = useTranslation();
const { showMessage } = useSnackbar(); const { showMessage } = useSnackbar();
const [formData, setFormData] = useState<PasswordFormData>({ const [formData, setFormData] = useState<PasswordFormData>({
@@ -100,23 +102,23 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
const newErrors: Partial<PasswordFormData> = {}; const newErrors: Partial<PasswordFormData> = {};
if (!formData.currentPassword.trim()) { if (!formData.currentPassword.trim()) {
newErrors.currentPassword = '请输入当前密码'; newErrors.currentPassword = t('setting.currentPasswordRequired');
} }
if (!formData.newPassword.trim()) { if (!formData.newPassword.trim()) {
newErrors.newPassword = '请输入新密码'; newErrors.newPassword = t('setting.newPasswordRequired');
} else if (formData.newPassword.length < 6) { } else if (formData.newPassword.length < 6) {
newErrors.newPassword = '新密码长度至少6位'; newErrors.newPassword = t('setting.passwordMinLength');
} }
if (!formData.confirmPassword.trim()) { if (!formData.confirmPassword.trim()) {
newErrors.confirmPassword = '请确认新密码'; newErrors.confirmPassword = t('setting.confirmPasswordRequired');
} else if (formData.newPassword !== formData.confirmPassword) { } else if (formData.newPassword !== formData.confirmPassword) {
newErrors.confirmPassword = '两次输入的密码不一致'; newErrors.confirmPassword = t('setting.passwordMismatch');
} }
if (formData.currentPassword === formData.newPassword) { if (formData.currentPassword === formData.newPassword) {
newErrors.newPassword = '新密码不能与当前密码相同'; newErrors.newPassword = t('setting.newPasswordSameAsCurrent');
} }
setErrors(newErrors); setErrors(newErrors);
@@ -136,10 +138,15 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
new_password: formData.newPassword new_password: formData.newPassword
}); });
showMessage.success('密码修改成功'); showMessage.success(t('setting.passwordChangeSuccess'));
handleClose(); handleClose();
} catch (error: any) { } catch (error: any) {
logger.error('修改密码失败:', error); logger.error('修改密码失败:', error);
if (error.response?.status === 400) {
showMessage.error(t('setting.currentPasswordIncorrect'));
} else {
showMessage.error(t('setting.passwordChangeError'));
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -157,7 +164,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
> >
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6" component="div"> <Typography variant="h6" component="div">
{t('setting.changePassword')}
</Typography> </Typography>
<IconButton <IconButton
onClick={handleClose} onClick={handleClose}
@@ -171,13 +178,17 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
<DialogContent dividers> <DialogContent dividers>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, pt: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, pt: 1 }}>
<Alert severity="info" sx={{ mb: 1 }}> <Alert severity="info" sx={{ mb: 1 }}>
6 {t('setting.passwordSecurityTip')}
</Alert> </Alert>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('setting.passwordUpdateTip')}
</Typography>
{/* 当前密码 */} {/* 当前密码 */}
<TextField <TextField
fullWidth fullWidth
label="当前密码" label={t('setting.currentPassword')}
type={showPasswords.current ? 'text' : 'password'} type={showPasswords.current ? 'text' : 'password'}
value={formData.currentPassword} value={formData.currentPassword}
onChange={handleInputChange('currentPassword')} onChange={handleInputChange('currentPassword')}
@@ -201,7 +212,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
{/* 新密码 */} {/* 新密码 */}
<TextField <TextField
fullWidth fullWidth
label="新密码" label={t('setting.newPassword')}
type={showPasswords.new ? 'text' : 'password'} type={showPasswords.new ? 'text' : 'password'}
value={formData.newPassword} value={formData.newPassword}
onChange={handleInputChange('newPassword')} onChange={handleInputChange('newPassword')}
@@ -225,7 +236,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
{/* 确认新密码 */} {/* 确认新密码 */}
<TextField <TextField
fullWidth fullWidth
label="确认新密码" label={t('setting.confirmNewPassword')}
type={showPasswords.confirm ? 'text' : 'password'} type={showPasswords.confirm ? 'text' : 'password'}
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={handleInputChange('confirmPassword')} onChange={handleInputChange('confirmPassword')}
@@ -254,7 +265,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
variant="outlined" variant="outlined"
disabled={loading} disabled={loading}
> >
{t('setting.cancel')}
</Button> </Button>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
@@ -262,7 +273,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
disabled={loading} disabled={loading}
sx={{ minWidth: 100 }} sx={{ minWidth: 100 }}
> >
{loading ? '修改中...' : '确认修改'} {loading ? t('setting.changing') : t('setting.confirmChange')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@@ -14,6 +14,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material'; import { Visibility, VisibilityOff } from '@mui/icons-material';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
// 表单数据接口 // 表单数据接口
export interface ApiKeyFormData { export interface ApiKeyFormData {
@@ -45,6 +46,7 @@ function ApiKeyDialog({
initialData, initialData,
editMode = false, editMode = false,
}: ApiKeyDialogProps) { }: ApiKeyDialogProps) {
const { t } = useTranslation();
const [showApiKey, setShowApiKey] = React.useState(false); const [showApiKey, setShowApiKey] = React.useState(false);
const { const {
@@ -81,14 +83,14 @@ function ApiKeyDialog({
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle> <DialogTitle>
{editMode ? '编辑' : '配置'} {factoryName} API Key {editMode ? t('common.edit') : t('common.configure')} {factoryName} API Key
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<Box component="form" sx={{ mt: 2 }}> <Box component="form" sx={{ mt: 2 }}>
<Controller <Controller
name="api_key" name="api_key"
control={control} control={control}
rules={{ required: 'API Key 是必填项' }} rules={{ required: t('setting.apiKeyRequired') }}
render={({ field }) => ( render={({ field }) => (
<TextField <TextField
{...field} {...field}
@@ -126,7 +128,7 @@ function ApiKeyDialog({
label="Base URL" label="Base URL"
placeholder="https://api.openai.com/v1" placeholder="https://api.openai.com/v1"
margin="normal" margin="normal"
helperText="可选,自定义 API 端点" helperText={t('setting.baseUrlOptional')}
/> />
)} )}
/> />
@@ -142,7 +144,7 @@ function ApiKeyDialog({
fullWidth fullWidth
label="Group ID" label="Group ID"
margin="normal" margin="normal"
helperText="Minimax 专用的 Group ID" helperText={t('setting.minimaxGroupId')}
/> />
)} )}
/> />
@@ -151,7 +153,7 @@ function ApiKeyDialog({
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose} disabled={loading}> <Button onClick={onClose} disabled={loading}>
{t('common.cancel')}
</Button> </Button>
<Button <Button
onClick={handleSubmit(handleFormSubmit)} onClick={handleSubmit(handleFormSubmit)}
@@ -159,7 +161,7 @@ function ApiKeyDialog({
disabled={loading} disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null} startIcon={loading ? <CircularProgress size={20} /> : null}
> >
{editMode ? '更新' : '保存'} {editMode ? t('common.update') : t('common.save')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </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'; } from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material'; import { Visibility, VisibilityOff } from '@mui/icons-material';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import type { IAddLlmRequestBody } from '@/interfaces/request/llm'; import type { IAddLlmRequestBody } from '@/interfaces/request/llm';
// 表单项配置接口 // 表单项配置接口
@@ -79,6 +80,7 @@ function ConfigurationDialog({
docLink, docLink,
editMode = false, editMode = false,
}: ConfigurationDialogProps) { }: ConfigurationDialogProps) {
const { t } = useTranslation();
const [passwordVisibility, setPasswordVisibility] = React.useState<Record<string, boolean>>({}); const [passwordVisibility, setPasswordVisibility] = React.useState<Record<string, boolean>>({});
// 构建默认值 // 构建默认值
@@ -138,7 +140,7 @@ function ConfigurationDialog({
// 构建验证规则 // 构建验证规则
const rules: any = {}; const rules: any = {};
if (item.required) { if (item.required) {
rules.required = `${item.label}是必填项`; rules.required = t('setting.fieldRequired', { field: item.label });
} }
if (item.validation) { if (item.validation) {
Object.assign(rules, item.validation); Object.assign(rules, item.validation);
@@ -266,7 +268,7 @@ function ConfigurationDialog({
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle> <DialogTitle>
{editMode ? '编辑' : ''} {title} {editMode ? t('setting.edit') : ''} {title}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<Box component="form" sx={{ mt: 2 }}> <Box component="form" sx={{ mt: 2 }}>
@@ -290,7 +292,7 @@ function ConfigurationDialog({
{/* 右侧按钮组 */} {/* 右侧按钮组 */}
<Box sx={{ ml: 'auto' }}> <Box sx={{ ml: 'auto' }}>
<Button onClick={onClose} disabled={loading} sx={{ mr: 1 }}> <Button onClick={onClose} disabled={loading} sx={{ mr: 1 }}>
{t('setting.cancel')}
</Button> </Button>
<Button <Button
onClick={handleSubmit(handleFormSubmit)} onClick={handleSubmit(handleFormSubmit)}
@@ -298,7 +300,7 @@ function ConfigurationDialog({
disabled={loading} disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null} startIcon={loading ? <CircularProgress size={20} /> : null}
> >
{t('setting.confirm')}
</Button> </Button>
</Box> </Box>
</Box> </Box>

View File

@@ -17,8 +17,10 @@ import {
Link, Link,
} from '@mui/material'; } from '@mui/material';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { LLM_FACTORY_LIST, type LLMFactory } from '@/constants/llm'; import { LLM_FACTORY_LIST, type LLMFactory } from '@/constants/llm';
import i18n from '@/locales';
// 表单数据接口 // 表单数据接口
export interface OllamaFormData { export interface OllamaFormData {
@@ -64,7 +66,7 @@ const llmFactoryToUrlMap: { [x: string]: string } = {
function getURLByFactory(factory: LLMFactory) { function getURLByFactory(factory: LLMFactory) {
const url = llmFactoryToUrlMap[factory]; const url = llmFactoryToUrlMap[factory];
return { return {
textTip: `如何集成 ${factory}`, textTip: `${i18n.t('setting.howToIntegrate')} ${factory}`,
url, url,
} }
} }
@@ -91,6 +93,7 @@ function OllamaDialog({
initialData, initialData,
editMode = false, editMode = false,
}: OllamaDialogProps) { }: OllamaDialogProps) {
const { t } = useTranslation();
const { const {
control, control,
@@ -163,7 +166,7 @@ function OllamaDialog({
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle> <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> </DialogTitle>
<DialogContent> <DialogContent>
<Box component="form" sx={{ mt: 2 }}> <Box component="form" sx={{ mt: 2 }}>
@@ -171,13 +174,13 @@ function OllamaDialog({
<Controller <Controller
name="model_type" name="model_type"
control={control} control={control}
rules={{ required: '模型类型是必填项' }} rules={{ required: t('setting.modelTypeRequired') }}
render={({ field }) => ( render={({ field }) => (
<FormControl fullWidth margin="normal" error={!!errors.model_type}> <FormControl fullWidth margin="normal" error={!!errors.model_type}>
<InputLabel> *</InputLabel> <InputLabel>{t('setting.modelType')} *</InputLabel>
<Select <Select
{...field} {...field}
label="模型类型 *" label={`${t('setting.modelType')} *`}
> >
{modelTypeOptions.map((option) => ( {modelTypeOptions.map((option) => (
<MenuItem key={option.value} value={option.value}> <MenuItem key={option.value} value={option.value}>
@@ -196,17 +199,17 @@ function OllamaDialog({
<Controller <Controller
name="llm_name" name="llm_name"
control={control} control={control}
rules={{ required: '模型名称是必填项' }} rules={{ required: t('setting.modelNameRequired') }}
render={({ field }) => ( render={({ field }) => (
<TextField <TextField
{...field} {...field}
fullWidth fullWidth
label="模型名称" label={t('setting.modelName')}
margin="normal" margin="normal"
required required
error={!!errors.llm_name} error={!!errors.llm_name}
helperText={errors.llm_name?.message || '请输入模型名称'} helperText={errors.llm_name?.message || t('setting.modelNamePlaceholder')}
placeholder="例如: llama2, mistral" placeholder={t('setting.modelNameExample')}
/> />
)} )}
/> />
@@ -216,21 +219,21 @@ function OllamaDialog({
name="api_base" name="api_base"
control={control} control={control}
rules={{ rules={{
required: '基础 URL 是必填项', required: t('setting.baseUrlRequired'),
pattern: { pattern: {
value: /^https?:\/\/.+/, value: /^https?:\/\/.+/,
message: '基础 URL 必须是有效的 URL' message: t('setting.baseUrlInvalid')
} }
}} }}
render={({ field }) => ( render={({ field }) => (
<TextField <TextField
{...field} {...field}
fullWidth fullWidth
label="基础 URL" label={t('setting.baseUrl')}
margin="normal" margin="normal"
required required
error={!!errors.api_base} error={!!errors.api_base}
helperText={errors.api_base?.message || '基础 URL'} helperText={errors.api_base?.message || t('setting.baseUrl')}
placeholder="http://localhost:8888" placeholder="http://localhost:8888"
/> />
)} )}
@@ -247,8 +250,8 @@ function OllamaDialog({
label="API Key" label="API Key"
margin="normal" margin="normal"
error={!!errors.api_key} error={!!errors.api_key}
helperText={errors.api_key?.message || 'API Key (可选)'} helperText={errors.api_key?.message || t('setting.apiKeyTip')}
placeholder="如果需要认证,请输入 API Key" placeholder={t('setting.apiKeyPlaceholder')}
/> />
)} )}
/> />
@@ -258,26 +261,26 @@ function OllamaDialog({
name="max_tokens" name="max_tokens"
control={control} control={control}
rules={{ rules={{
required: '最大 Token 数是必填项', required: t('setting.maxTokensRequired'),
min: { min: {
value: 1, value: 1,
message: '最大 Token 数必须大于 0' message: t('setting.maxTokensMin')
}, },
max: { max: {
value: 100000, value: 100000,
message: '最大 Token 数不能超过 100000' message: t('setting.maxTokensMax')
} }
}} }}
render={({ field }) => ( render={({ field }) => (
<TextField <TextField
{...field} {...field}
fullWidth fullWidth
label="最大 Token 数" label={t('setting.maxTokens')}
margin="normal" margin="normal"
type="number" type="number"
required required
error={!!errors.max_tokens} error={!!errors.max_tokens}
helperText={errors.max_tokens?.message || '模型支持的最大 Token 数'} helperText={errors.max_tokens?.message || t('setting.maxTokensValidation')}
placeholder="4096" placeholder="4096"
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)} onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
/> />
@@ -307,7 +310,7 @@ function OllamaDialog({
{/* 右侧按钮组 */} {/* 右侧按钮组 */}
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={onClose} disabled={loading}> <Button onClick={onClose} disabled={loading}>
{t('setting.cancel')}
</Button> </Button>
<Button <Button
onClick={handleSubmit(handleFormSubmit)} onClick={handleSubmit(handleFormSubmit)}
@@ -315,7 +318,7 @@ function OllamaDialog({
disabled={loading} disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null} startIcon={loading ? <CircularProgress size={20} /> : null}
> >
{t('setting.confirm')}
</Button> </Button>
</Box> </Box>
</Box> </Box>

View File

@@ -15,6 +15,7 @@ import {
ListSubheader, ListSubheader,
} from '@mui/material'; } from '@mui/material';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { LlmSvgIcon } from '@/components/AppSvgIcon'; import { LlmSvgIcon } from '@/components/AppSvgIcon';
import { IconMap, type LLMFactory } from '@/constants/llm'; import { IconMap, type LLMFactory } from '@/constants/llm';
import type { ITenantInfo } from '@/interfaces/database/knowledge'; import type { ITenantInfo } from '@/interfaces/database/knowledge';
@@ -69,6 +70,7 @@ function SystemModelDialog({
editMode = false, editMode = false,
allModelOptions allModelOptions
}: SystemModelDialogProps) { }: SystemModelDialogProps) {
const { t } = useTranslation();
const { control, handleSubmit, reset, formState: { errors } } = useForm<ITenantInfo>({ const { control, handleSubmit, reset, formState: { errors } } = useForm<ITenantInfo>({
defaultValues: {} defaultValues: {}
}); });
@@ -106,25 +108,25 @@ function SystemModelDialog({
await onSubmit(data); await onSubmit(data);
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('提交失败:', error); console.error(t('setting.submitFailed'), error);
} }
}; };
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle> <DialogTitle>
{t('setting.setDefaultModel')}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<Box component="form" sx={{ mt: 2 }}> <Box component="form" sx={{ mt: 2 }}>
<Controller <Controller
name="llm_id" name="llm_id"
control={control} control={control}
rules={{ required: '聊天模型是必填项' }} rules={{ required: t('setting.chatModelRequired') }}
render={({ field }) => ( render={({ field }) => (
<FormControl fullWidth margin="normal" error={!!errors.llm_id}> <FormControl fullWidth margin="normal" error={!!errors.llm_id}>
<InputLabel></InputLabel> <InputLabel>{t('setting.chatModel')}</InputLabel>
<Select {...field} label="聊天模型"> <Select {...field} label={t('setting.chatModel')}>
{llmOptions.map((group) => [ {llmOptions.map((group) => [
<ListSubheader key={group.label}>{group.label}</ListSubheader>, <ListSubheader key={group.label}>{group.label}</ListSubheader>,
...group.options.map((option) => ( ...group.options.map((option) => (
@@ -152,11 +154,11 @@ function SystemModelDialog({
<Controller <Controller
name="embd_id" name="embd_id"
control={control} control={control}
rules={{ required: '嵌入模型是必填项' }} rules={{ required: t('setting.embeddingModelRequired') }}
render={({ field }) => ( render={({ field }) => (
<FormControl fullWidth margin="normal" error={!!errors.embd_id}> <FormControl fullWidth margin="normal" error={!!errors.embd_id}>
<InputLabel></InputLabel> <InputLabel>{t('setting.embeddingModel')}</InputLabel>
<Select {...field} label="嵌入模型"> <Select {...field} label={t('setting.embeddingModel')}>
{embdOptions.map((group) => [ {embdOptions.map((group) => [
<ListSubheader key={group.label}>{group.label}</ListSubheader>, <ListSubheader key={group.label}>{group.label}</ListSubheader>,
...group.options.map((option) => ( ...group.options.map((option) => (
@@ -186,8 +188,8 @@ function SystemModelDialog({
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel>Img2txt模型</InputLabel> <InputLabel>{t('setting.img2txtModel')}</InputLabel>
<Select {...field} label="Img2txt模型"> <Select {...field} label={t('setting.img2txtModel')}>
{img2txtOptions.map((group) => [ {img2txtOptions.map((group) => [
<ListSubheader key={group.label}>{group.label}</ListSubheader>, <ListSubheader key={group.label}>{group.label}</ListSubheader>,
...group.options.map((option) => ( ...group.options.map((option) => (
@@ -212,8 +214,8 @@ function SystemModelDialog({
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel>Speech2txt模型</InputLabel> <InputLabel>{t('setting.speech2txtModel')}</InputLabel>
<Select {...field} label="Speech2txt模型"> <Select {...field} label={t('setting.speech2txtModel')}>
{asrOptions.map((group) => [ {asrOptions.map((group) => [
<ListSubheader key={group.label}>{group.label}</ListSubheader>, <ListSubheader key={group.label}>{group.label}</ListSubheader>,
...group.options.map((option) => ( ...group.options.map((option) => (
@@ -238,8 +240,8 @@ function SystemModelDialog({
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel>Rerank模型</InputLabel> <InputLabel>{t('setting.rerankModel')}</InputLabel>
<Select {...field} label="Rerank模型"> <Select {...field} label={t('setting.rerankModel')}>
{rerankOptions.map((group) => [ {rerankOptions.map((group) => [
<ListSubheader key={group.label}>{group.label}</ListSubheader>, <ListSubheader key={group.label}>{group.label}</ListSubheader>,
...group.options.map((option) => ( ...group.options.map((option) => (
@@ -264,8 +266,8 @@ function SystemModelDialog({
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel>TTS模型</InputLabel> <InputLabel>{t('setting.ttsModel')}</InputLabel>
<Select {...field} label="TTS模型"> <Select {...field} label={t('setting.ttsModel')}>
{ttsOptions.map((group) => [ {ttsOptions.map((group) => [
<ListSubheader key={group.label}>{group.label}</ListSubheader>, <ListSubheader key={group.label}>{group.label}</ListSubheader>,
...group.options.map((option) => ( ...group.options.map((option) => (
@@ -288,7 +290,7 @@ function SystemModelDialog({
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose} disabled={loading}> <Button onClick={onClose} disabled={loading}>
{t('setting.cancel')}
</Button> </Button>
<Button <Button
onClick={handleSubmit(handleFormSubmit)} onClick={handleSubmit(handleFormSubmit)}
@@ -296,7 +298,7 @@ function SystemModelDialog({
disabled={loading} disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null} startIcon={loading ? <CircularProgress size={20} /> : null}
> >
{t('setting.confirm')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@@ -1,5 +1,6 @@
import type { ConfigFormItem, DocLinkConfig } from './ConfigurationDialog'; import type { ConfigFormItem, DocLinkConfig } from './ConfigurationDialog';
import { LLM_FACTORY_LIST } from '@/constants/llm'; import { LLM_FACTORY_LIST } from '@/constants/llm';
import i18n from '@/locales';
// AWS Bedrock 支持的区域列表 // AWS Bedrock 支持的区域列表
export const BEDROCK_REGIONS = [ export const BEDROCK_REGIONS = [
@@ -54,71 +55,71 @@ export const MODEL_TYPE_OPTIONS = [
export const DOC_LINKS: Record<string, DocLinkConfig> = { export const DOC_LINKS: Record<string, DocLinkConfig> = {
[LLM_FACTORY_LIST.AzureOpenAI]: { [LLM_FACTORY_LIST.AzureOpenAI]: {
url: 'https://azure.microsoft.com/en-us/products/ai-services/openai-service', 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]: { [LLM_FACTORY_LIST.Bedrock]: {
url: 'https://console.aws.amazon.com/', url: 'https://console.aws.amazon.com/',
text: '如何集成 Bedrock', text: `${i18n.t('setting.howToIntegrate')} Bedrock`,
}, },
[LLM_FACTORY_LIST.Ollama]: { [LLM_FACTORY_LIST.Ollama]: {
url: 'https://github.com/infiniflow/ragflow/blob/main/docs/guides/models/deploy_local_llm.mdx', 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]: { [LLM_FACTORY_LIST.Xinference]: {
url: 'https://inference.readthedocs.io/en/latest/user_guide', url: 'https://inference.readthedocs.io/en/latest/user_guide',
text: '如何集成 Xinference', text: `${i18n.t('setting.howToIntegrate')} Xinference`,
}, },
[LLM_FACTORY_LIST.ModelScope]: { [LLM_FACTORY_LIST.ModelScope]: {
url: 'https://www.modelscope.cn/docs/model-service/API-Inference/intro', url: 'https://www.modelscope.cn/docs/model-service/API-Inference/intro',
text: '如何集成 ModelScope', text: `${i18n.t('setting.howToIntegrate')} ModelScope`,
}, },
[LLM_FACTORY_LIST.LocalAI]: { [LLM_FACTORY_LIST.LocalAI]: {
url: 'https://localai.io/docs/getting-started/models/', url: 'https://localai.io/docs/getting-started/models/',
text: '如何集成 LocalAI', text: `${i18n.t('setting.howToIntegrate')} LocalAI`,
}, },
[LLM_FACTORY_LIST.LMStudio]: { [LLM_FACTORY_LIST.LMStudio]: {
url: 'https://lmstudio.ai/docs/basics', url: 'https://lmstudio.ai/docs/basics',
text: '如何集成 LMStudio', text: `${i18n.t('setting.howToIntegrate')} LMStudio`,
}, },
[LLM_FACTORY_LIST.OpenAiAPICompatible]: { [LLM_FACTORY_LIST.OpenAiAPICompatible]: {
url: 'https://platform.openai.com/docs/models/gpt-4', 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]: { [LLM_FACTORY_LIST.TogetherAI]: {
url: 'https://docs.together.ai/docs/deployment-options', url: 'https://docs.together.ai/docs/deployment-options',
text: '如何集成 TogetherAI', text: `${i18n.t('setting.howToIntegrate')} TogetherAI`,
}, },
[LLM_FACTORY_LIST.Replicate]: { [LLM_FACTORY_LIST.Replicate]: {
url: 'https://replicate.com/docs/topics/deployments', url: 'https://replicate.com/docs/topics/deployments',
text: '如何集成 Replicate', text: `${i18n.t('setting.howToIntegrate')} Replicate`,
}, },
[LLM_FACTORY_LIST.OpenRouter]: { [LLM_FACTORY_LIST.OpenRouter]: {
url: 'https://openrouter.ai/docs', url: 'https://openrouter.ai/docs',
text: '如何集成 OpenRouter', text: `${i18n.t('setting.howToIntegrate')} OpenRouter`,
}, },
[LLM_FACTORY_LIST.HuggingFace]: { [LLM_FACTORY_LIST.HuggingFace]: {
url: 'https://huggingface.co/docs/text-embeddings-inference/quick_tour', url: 'https://huggingface.co/docs/text-embeddings-inference/quick_tour',
text: '如何集成 HuggingFace', text: `${i18n.t('setting.howToIntegrate')} HuggingFace`,
}, },
[LLM_FACTORY_LIST.GPUStack]: { [LLM_FACTORY_LIST.GPUStack]: {
url: 'https://docs.gpustack.ai/latest/quickstart', url: 'https://docs.gpustack.ai/latest/quickstart',
text: '如何集成 GPUStack', text: `${i18n.t('setting.howToIntegrate')} GPUStack`,
}, },
[LLM_FACTORY_LIST.VLLM]: { [LLM_FACTORY_LIST.VLLM]: {
url: 'https://docs.vllm.ai/en/latest/', url: 'https://docs.vllm.ai/en/latest/',
text: '如何集成 VLLM', text: `${i18n.t('setting.howToIntegrate')} VLLM`,
}, },
[LLM_FACTORY_LIST.FishAudio]: { [LLM_FACTORY_LIST.FishAudio]: {
url: 'https://www.fish.audio/', url: 'https://www.fish.audio/',
text: '如何集成 Fish Audio', text: `${i18n.t('setting.howToIntegrate')} Fish Audio`,
}, },
[LLM_FACTORY_LIST.TencentCloud]: { [LLM_FACTORY_LIST.TencentCloud]: {
url: 'https://cloud.tencent.com/document/api/1093/37823', url: 'https://cloud.tencent.com/document/api/1093/37823',
text: '如何集成 腾讯云语音识别', text: `${i18n.t('setting.howToIntegrate')} 腾讯云语音识别`,
}, },
[LLM_FACTORY_LIST.VolcEngine]: { [LLM_FACTORY_LIST.VolcEngine]: {
url: 'https://www.volcengine.com/docs/82379/1302008', 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[] = [ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
{ {
name: 'model_type', name: 'model_type',
label: '模型类型', label: i18n.t('setting.modelType'),
type: 'select', type: 'select',
required: true, required: true,
options: [ options: [
@@ -138,24 +139,24 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'llm_name', name: 'llm_name',
label: '模型名称', label: i18n.t('setting.modelName'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: 'gpt-3.5-turbo', placeholder: 'gpt-3.5-turbo',
helperText: '请输入模型名称', helperText: i18n.t('setting.modelNameHelperText'),
defaultValue: 'gpt-3.5-turbo', defaultValue: 'gpt-3.5-turbo',
}, },
{ {
name: 'api_base', name: 'api_base',
label: '基础 Url', label: i18n.t('setting.baseUrl'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: 'https://your-resource.openai.azure.com/', placeholder: 'https://your-resource.openai.azure.com/',
helperText: 'Azure OpenAI 服务的端点 URL', helperText: i18n.t('setting.azureOpenAIEndpointHelperText'),
validation: { validation: {
pattern: { pattern: {
value: /^https?:\/\/.+/, value: /^https?:\/\/.+/,
message: '基础 URL 必须是有效的 URL', message: i18n.t('setting.baseUrlValidationMessage'),
}, },
}, },
}, },
@@ -163,7 +164,7 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
name: 'api_key', name: 'api_key',
label: 'API-Key', label: 'API-Key',
type: 'password', type: 'password',
helperText: '输入api key如果是本地部署的模型请忽略', helperText: i18n.t('setting.apiKeyHelperText'),
}, },
{ {
name: 'api_version', name: 'api_version',
@@ -171,20 +172,20 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
type: 'text', type: 'text',
required: true, required: true,
placeholder: '2024-02-01', placeholder: '2024-02-01',
helperText: 'Azure OpenAI API 版本', helperText: i18n.t('setting.azureAPIVersionHelperText'),
defaultValue: '2024-02-01', defaultValue: '2024-02-01',
}, },
{ {
name: 'max_tokens', name: 'max_tokens',
label: '最大token数', label: i18n.t('setting.maxTokens'),
type: 'number', type: 'number',
required: true, required: true,
placeholder: '设置了模型输出的最大长度以token单词片段的数量表示', placeholder: i18n.t('setting.maxTokensPlaceholder'),
helperText: '设置了模型输出的最大长度以token单词片段的数量表示', helperText: i18n.t('setting.maxTokensHelperText'),
defaultValue: 4096, defaultValue: 4096,
validation: { validation: {
min: { value: 1, message: '最大token数必须大于0' }, min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
max: { value: 100000, message: '最大token数不能超过100000' }, max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
}, },
}, },
]; ];
@@ -193,7 +194,7 @@ export const AZURE_OPENAI_CONFIG: ConfigFormItem[] = [
export const BEDROCK_CONFIG: ConfigFormItem[] = [ export const BEDROCK_CONFIG: ConfigFormItem[] = [
{ {
name: 'model_type', name: 'model_type',
label: '模型类型', label: i18n.t('setting.modelType'),
type: 'select', type: 'select',
required: true, required: true,
options: MODEL_TYPE_OPTIONS.slice(0, 2), // 只支持 chat 和 embedding options: MODEL_TYPE_OPTIONS.slice(0, 2), // 只支持 chat 和 embedding
@@ -201,24 +202,24 @@ export const BEDROCK_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'llm_name', name: 'llm_name',
label: '模型名称', label: i18n.t('setting.modelName'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入模型名称', placeholder: i18n.t('setting.modelNamePlaceholder'),
}, },
{ {
name: 'bedrock_ak', name: 'bedrock_ak',
label: 'ACCESS KEY', label: 'ACCESS KEY',
type: 'password', type: 'password',
required: true, required: true,
placeholder: '请输入 ACCESS KEY', placeholder: i18n.t('setting.accessKeyPlaceholder'),
}, },
{ {
name: 'bedrock_sk', name: 'bedrock_sk',
label: 'SECRET KEY', label: 'SECRET KEY',
type: 'password', type: 'password',
required: true, required: true,
placeholder: '请输入 SECRET KEY', placeholder: i18n.t('setting.secretKeyPlaceholder'),
}, },
{ {
name: 'bedrock_region', name: 'bedrock_region',
@@ -230,14 +231,14 @@ export const BEDROCK_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'max_tokens', name: 'max_tokens',
label: '最大token数', label: i18n.t('setting.maxTokens'),
type: 'number', type: 'number',
required: true, required: true,
placeholder: '这设置了模型输出的最大长度以token单词或词片段的数量来衡量', placeholder: i18n.t('setting.maxTokensPlaceholder'),
helperText: '这设置了模型输出的最大长度以token单词或词片段的数量来衡量', helperText: i18n.t('setting.maxTokensHelperText'),
defaultValue: 4096, defaultValue: 4096,
validation: { 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[] = [ export const OLLAMA_CONFIG: ConfigFormItem[] = [
{ {
name: 'model_type', name: 'model_type',
label: '模型类型', label: i18n.t('setting.modelType'),
type: 'select', type: 'select',
required: true, required: true,
options: MODEL_TYPE_OPTIONS, options: MODEL_TYPE_OPTIONS,
@@ -254,24 +255,24 @@ export const OLLAMA_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'llm_name', name: 'llm_name',
label: '模型名称', label: i18n.t('setting.modelName'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '例如: llama2, mistral', placeholder: i18n.t('setting.ollamaModelNamePlaceholder'),
helperText: '请输入模型名称', helperText: i18n.t('setting.modelNameHelperText'),
}, },
{ {
name: 'api_base', name: 'api_base',
label: '基础 URL', label: i18n.t('setting.baseUrl'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: 'http://localhost:8888', placeholder: 'http://localhost:8888',
helperText: '基础 URL', helperText: i18n.t('setting.baseUrlHelperText'),
defaultValue: 'http://localhost:11434', defaultValue: 'http://localhost:11434',
validation: { validation: {
pattern: { pattern: {
value: /^https?:\/\/.+/, value: /^https?:\/\/.+/,
message: '基础 URL 必须是有效的 URL', message: i18n.t('setting.baseUrlValidationMessage'),
}, },
}, },
}, },
@@ -279,20 +280,20 @@ export const OLLAMA_CONFIG: ConfigFormItem[] = [
name: 'api_key', name: 'api_key',
label: 'API Key', label: 'API Key',
type: 'text', type: 'text',
placeholder: '如果需要认证,请输入 API Key', placeholder: i18n.t('setting.apiKeyOptionalPlaceholder'),
helperText: 'API Key (可选)', helperText: i18n.t('setting.apiKeyOptional'),
}, },
{ {
name: 'max_tokens', name: 'max_tokens',
label: '最大 Token 数', label: i18n.t('setting.maxTokens'),
type: 'number', type: 'number',
required: true, required: true,
placeholder: '4096', placeholder: '4096',
helperText: '模型支持的最大 Token 数', helperText: i18n.t('setting.maxTokensSupportedHelperText'),
defaultValue: 4096, defaultValue: 4096,
validation: { validation: {
min: { value: 1, message: '最大 Token 数必须大于 0' }, min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
max: { value: 100000, message: '最大 Token 数不能超过 100000' }, max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
}, },
}, },
]; ];
@@ -300,7 +301,7 @@ export const OLLAMA_CONFIG: ConfigFormItem[] = [
export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [ export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [
{ {
name: 'model_type', name: 'model_type',
label: '模型类型', label: i18n.t('setting.modelType'),
type: 'select', type: 'select',
required: true, required: true,
options: MODEL_TYPE_OPTIONS.slice(0, 3), options: MODEL_TYPE_OPTIONS.slice(0, 3),
@@ -308,38 +309,38 @@ export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'llm_name', name: 'llm_name',
label: '模型名称', label: i18n.t('setting.modelName'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入模型名称', placeholder: i18n.t('setting.modelNamePlaceholder'),
}, },
{ {
name: 'yiyan_ak', name: 'yiyan_ak',
label: '一言 API KEY', label: i18n.t('setting.baiduYiYanAPIKey'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 API KEY', placeholder: i18n.t('setting.apiKeyPlaceholder'),
helperText: 'Baidu YiYan API KEY', helperText: 'Baidu YiYan API KEY',
}, },
{ {
name: 'yiyan_sk', name: 'yiyan_sk',
label: '一言 Secret KEY', label: i18n.t('setting.baiduYiYanSecretKey'),
type: 'password', type: 'password',
required: true, required: true,
placeholder: '请输入 Secret KEY', placeholder: i18n.t('setting.secretKeyPlaceholder'),
helperText: 'Baidu YiYan Secret KEY', helperText: 'Baidu YiYan Secret KEY',
}, },
{ {
name: 'max_tokens', name: 'max_tokens',
label: '最大token数', label: i18n.t('setting.maxTokens'),
type: 'number', type: 'number',
required: true, required: true,
placeholder: '设置了模型输出的最大长度以token单词片段的数量表示', placeholder: i18n.t('setting.maxTokensPlaceholder'),
helperText: '设置了模型输出的最大长度以token单词片段的数量表示', helperText: i18n.t('setting.maxTokensHelperText'),
defaultValue: 4096, defaultValue: 4096,
validation: { validation: {
min: { value: 1, message: '最大token数必须大于0' }, min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
max: { value: 100000, message: '最大token数不能超过100000' }, max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
}, },
}, },
]; ];
@@ -347,7 +348,7 @@ export const BAIDU_YIYAN_CONFIG: ConfigFormItem[] = [
export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [ export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
{ {
name: 'model_type', name: 'model_type',
label: '模型类型', label: i18n.t('setting.modelType'),
type: 'select', type: 'select',
required: true, required: true,
options: [{ value: 'tts', label: 'TTS' },], options: [{ value: 'tts', label: 'TTS' },],
@@ -355,17 +356,17 @@ export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'llm_name', name: 'llm_name',
label: '模型名称', label: i18n.t('setting.modelName'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入模型名称', placeholder: i18n.t('setting.modelNamePlaceholder'),
}, },
{ {
name: 'fish_audio_ak', name: 'fish_audio_ak',
label: 'Fish Audio API KEY', label: 'Fish Audio API KEY',
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 API KEY', placeholder: i18n.t('setting.apiKeyPlaceholder'),
helperText: 'Fish Audio API KEY', helperText: 'Fish Audio API KEY',
}, },
{ {
@@ -373,20 +374,20 @@ export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
label: 'FishAudio Refrence ID', label: 'FishAudio Refrence ID',
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 Refrence ID', placeholder: i18n.t('setting.fishAudioRefIdPlaceholder'),
helperText: 'Fish Audio Refrence ID', helperText: 'Fish Audio Refrence ID',
}, },
{ {
name: 'max_tokens', name: 'max_tokens',
label: '最大token数', label: i18n.t('setting.maxTokens'),
type: 'number', type: 'number',
required: true, required: true,
placeholder: '设置了模型输出的最大长度以token单词片段的数量表示', placeholder: i18n.t('setting.maxTokensPlaceholder'),
helperText: '设置了模型输出的最大长度以token单词片段的数量表示', helperText: i18n.t('setting.maxTokensHelperText'),
defaultValue: 4096, defaultValue: 4096,
validation: { validation: {
min: { value: 1, message: '最大token数必须大于0' }, min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
max: { value: 100000, message: '最大token数不能超过100000' }, max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
}, },
}, },
] ]
@@ -394,7 +395,7 @@ export const FISH_AUDIO_CONFIG: ConfigFormItem[] = [
export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [ export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [
{ {
name: 'model_type', name: 'model_type',
label: '模型类型', label: i18n.t('setting.modelType'),
type: 'select', type: 'select',
required: true, required: true,
options: [{ value: 'chat', label: 'Chat' }, { value: 'image2text', label: 'Image2Text' }], options: [{ value: 'chat', label: 'Chat' }, { value: 'image2text', label: 'Image2Text' }],
@@ -402,46 +403,46 @@ export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'llm_name', name: 'llm_name',
label: '模型名称', label: i18n.t('setting.modelName'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入模型名称', placeholder: i18n.t('setting.modelNamePlaceholder'),
}, },
{ {
name: 'google_project_id', name: 'google_project_id',
label: 'Project ID', label: 'Project ID',
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 Project ID', placeholder: i18n.t('setting.googleProjectIdPlaceholder'),
helperText: 'Google Cloud Project ID', helperText: 'Google Cloud Project ID',
}, },
{ {
name: 'google_region', name: 'google_region',
label: 'Google Cloud 区域', label: i18n.t('setting.googleCloudRegion'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 Google Cloud 区域', placeholder: i18n.t('setting.googleCloudRegionPlaceholder'),
helperText: 'Google Cloud 区域', helperText: i18n.t('setting.googleCloudRegionHelperText'),
}, },
{ {
name: 'google_service_account_key', name: 'google_service_account_key',
label: 'Google Cloud Service Account Key', label: 'Google Cloud Service Account Key',
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 Google Cloud Service Account Key', placeholder: i18n.t('setting.googleServiceAccountKeyPlaceholder'),
helperText: 'Google Cloud Service Account Key', helperText: 'Google Cloud Service Account Key',
}, },
{ {
name: 'max_tokens', name: 'max_tokens',
label: '最大token数', label: i18n.t('setting.maxTokens'),
type: 'number', type: 'number',
required: true, required: true,
placeholder: '设置了模型输出的最大长度以token单词片段的数量表示', placeholder: i18n.t('setting.maxTokensPlaceholder'),
helperText: '设置了模型输出的最大长度以token单词片段的数量表示', helperText: i18n.t('setting.maxTokensHelperText'),
defaultValue: 4096, defaultValue: 4096,
validation: { validation: {
min: { value: 1, message: '最大token数必须大于0' }, min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
max: { value: 100000, message: '最大token数不能超过100000' }, max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
}, },
}, },
] ]
@@ -449,7 +450,7 @@ export const GOOGLE_CLOUD_CONFIG: ConfigFormItem[] = [
export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [ export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [
{ {
name: 'model_type', name: 'model_type',
label: '模型类型', label: i18n.t('setting.modelType'),
type: 'select', type: 'select',
required: true, required: true,
options: [{ value: 'speech2text', label: 'Speech2Text' }], options: [{ value: 'speech2text', label: 'Speech2Text' }],
@@ -457,7 +458,7 @@ export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'llm_name', name: 'llm_name',
label: '模型名称', label: i18n.t('setting.modelName'),
type: 'select', type: 'select',
required: true, required: true,
options: [ options: [
@@ -469,38 +470,38 @@ export const TENCENT_CLOUD_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'tencent_ak', name: 'tencent_ak',
label: '腾讯云 Secret ID', label: i18n.t('setting.tencentSecretId'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 Secret ID', placeholder: i18n.t('setting.secretIdPlaceholder'),
helperText: '腾讯云 Secret ID', helperText: i18n.t('setting.tencentSecretIdHelperText'),
}, },
{ {
name: 'tencent_sk', name: 'tencent_sk',
label: '腾讯云 Secret KEY', label: i18n.t('setting.tencentSecretKey'),
type: 'password', type: 'password',
required: true, required: true,
placeholder: '请输入 Secret KEY', placeholder: i18n.t('setting.secretKeyPlaceholder'),
helperText: '腾讯云 Secret KEY', helperText: i18n.t('setting.tencentSecretKeyHelperText'),
}, },
] ]
export const TENCENT_HUNYUAN_CONFIG: ConfigFormItem[] = [ export const TENCENT_HUNYUAN_CONFIG: ConfigFormItem[] = [
{ {
name: 'hunyuan_sid', name: 'hunyuan_sid',
label: '混元 Secret ID', label: i18n.t('setting.hunyuanSecretId'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 Secret ID', placeholder: i18n.t('setting.secretIdPlaceholder'),
helperText: '混元 Secret ID', helperText: i18n.t('setting.hunyuanSecretIdHelperText'),
}, },
{ {
name: 'hunyuan_sk', name: 'hunyuan_sk',
label: '混元 Secret KEY', label: i18n.t('setting.hunyuanSecretKey'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 Secret KEY', placeholder: i18n.t('setting.secretKeyPlaceholder'),
helperText: '混元 Secret KEY', helperText: i18n.t('setting.hunyuanSecretKeyHelperText'),
}, },
] ]
@@ -508,7 +509,7 @@ export const TENCENT_HUNYUAN_CONFIG: ConfigFormItem[] = [
export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [ export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [
{ {
name: 'model_type', name: 'model_type',
label: '模型类型', label: i18n.t('setting.modelType'),
type: 'select', type: 'select',
required: true, required: true,
options: [{ value: 'chat', label: 'Chat' }, { value: 'tts', label: 'TTS' }], options: [{ value: 'chat', label: 'Chat' }, { value: 'tts', label: 'TTS' }],
@@ -516,30 +517,30 @@ export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'llm_name', name: 'llm_name',
label: '模型名称', label: i18n.t('setting.modelName'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入模型名称', placeholder: i18n.t('setting.modelNamePlaceholder'),
}, },
{ {
name: 'xunfei_spark_password', name: 'xunfei_spark_password',
label: '讯飞星火 API Password', label: i18n.t('setting.xunfeiSparkAPIPassword'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 API Password', placeholder: i18n.t('setting.apiPasswordPlaceholder'),
helperText: '讯飞星火 API Password', helperText: i18n.t('setting.xunfeiSparkAPIPasswordHelperText'),
}, },
{ {
name: 'max_tokens', name: 'max_tokens',
label: '最大token数', label: i18n.t('setting.maxTokens'),
type: 'number', type: 'number',
required: true, required: true,
placeholder: '设置了模型输出的最大长度以token单词片段的数量表示', placeholder: i18n.t('setting.maxTokensPlaceholder'),
helperText: '设置了模型输出的最大长度以token单词片段的数量表示', helperText: i18n.t('setting.maxTokensHelperText'),
defaultValue: 4096, defaultValue: 4096,
validation: { validation: {
min: { value: 1, message: '最大token数必须大于0' }, min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
max: { value: 100000, message: '最大token数不能超过100000' }, max: { value: 100000, message: i18n.t('setting.maxTokensMaxMessage') },
}, },
}, },
] ]
@@ -548,7 +549,7 @@ export const XUNFEI_SPARK_CONFIG: ConfigFormItem[] = [
export const VOLC_ENGINE_CONFIG: ConfigFormItem[] = [ export const VOLC_ENGINE_CONFIG: ConfigFormItem[] = [
{ {
name: 'model_type', name: 'model_type',
label: '模型类型', label: i18n.t('setting.modelType'),
type: 'select', type: 'select',
required: true, required: true,
options: [{ value: 'chat', label: 'Chat' }, { value: 'embedding', label: 'Embedding' }], options: [{ value: 'chat', label: 'Chat' }, { value: 'embedding', label: 'Embedding' }],
@@ -556,38 +557,38 @@ export const VOLC_ENGINE_CONFIG: ConfigFormItem[] = [
}, },
{ {
name: 'llm_name', name: 'llm_name',
label: '模型名称', label: i18n.t('setting.modelName'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入模型名称', placeholder: i18n.t('setting.modelNamePlaceholder'),
}, },
{ {
name: 'endpoint_id', name: 'endpoint_id',
label: '模型 EndpointID', label: i18n.t('setting.modelEndpointId'),
type: 'text', type: 'text',
required: true, required: true,
placeholder: '请输入 EndpointID', placeholder: i18n.t('setting.endpointIdPlaceholder'),
helperText: '模型 EndpointID', helperText: i18n.t('setting.modelEndpointIdHelperText'),
}, },
{ {
name: 'ark_api_key', name: 'ark_api_key',
label: '火山 ARK_API_KEY', label: i18n.t('setting.volcEngineARKAPIKey'),
type: 'password', type: 'password',
required: true, required: true,
placeholder: '请输入 ARK_API_KEY', placeholder: i18n.t('setting.arkApiKeyPlaceholder'),
helperText: '模型 ARK_API_KEY', helperText: i18n.t('setting.modelARKAPIKeyHelperText'),
}, },
{ {
name: 'max_tokens', name: 'max_tokens',
label: '最大token数', label: i18n.t('setting.maxTokens'),
type: 'number', type: 'number',
required: true, required: true,
placeholder: '设置了模型输出的最大长度以token单词片段的数量表示', placeholder: i18n.t('setting.maxTokensPlaceholder'),
helperText: '设置了模型输出的最大长度以token单词片段的数量表示', helperText: i18n.t('setting.maxTokensHelperText'),
defaultValue: 4096, defaultValue: 4096,
validation: { validation: {
min: { value: 1, message: '最大token数必须大于0' }, min: { value: 1, message: i18n.t('setting.maxTokensMinMessage') },
max: { value: 100000, message: '最大token数不能超过100000' }, 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 { IconMap, type LLMFactory } from "@/constants/llm";
import type { IFactory } from "@/interfaces/database/llm"; import type { IFactory } from "@/interfaces/database/llm";
import { Box, Button, Card, CardContent, Chip, Typography } from "@mui/material"; import { Box, Button, Card, CardContent, Chip, Typography } from "@mui/material";
import { useTranslation } from 'react-i18next';
// 模型类型标签颜色映射 // 模型类型标签颜色映射
export const MODEL_TYPE_COLORS: Record<string, string> = { export const MODEL_TYPE_COLORS: Record<string, string> = {
@@ -27,6 +28,7 @@ const LLMFactoryCard: React.FC<ModelFactoryCardProps> = ({
factory, factory,
onConfigure, onConfigure,
}) => { }) => {
const { t } = useTranslation();
// 获取工厂图标名称 // 获取工厂图标名称
const getFactoryIconName = (factoryName: LLMFactory) => { const getFactoryIconName = (factoryName: LLMFactory) => {
@@ -89,7 +91,7 @@ const LLMFactoryCard: React.FC<ModelFactoryCardProps> = ({
width: 'fit-content' width: 'fit-content'
}} }}
> >
{t('setting.addModel')}
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -55,8 +55,6 @@ const McpDialog: React.FC<McpDialogProps> = ({
const [submitError, setSubmitError] = useState<string | null>(null); const [submitError, setSubmitError] = useState<string | null>(null);
const [submitSuccess, setSubmitSuccess] = useState(false); const [submitSuccess, setSubmitSuccess] = useState(false);
logger.debug('McpDialog 组件加载', initialData);
const handleSubmit = useCallback(async (data: ICreateMcpServerRequestBody) => { const handleSubmit = useCallback(async (data: ICreateMcpServerRequestBody) => {
setSubmitLoading(true); setSubmitLoading(true);
setSubmitError(null); setSubmitError(null);

View File

@@ -22,6 +22,7 @@ import {
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import { useTranslation } from 'react-i18next';
import type { IMcpServer } from '@/interfaces/database/mcp'; import type { IMcpServer } from '@/interfaces/database/mcp';
import type { ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp'; import type { ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
import { Mode } from '@mui/icons-material'; import { Mode } from '@mui/icons-material';
@@ -74,6 +75,7 @@ const McpForm: React.FC<McpFormProps> = ({
loading = false, loading = false,
disabled = false, disabled = false,
}) => { }) => {
const { t } = useTranslation();
const [formData, setFormData] = useState<McpFormData>({ const [formData, setFormData] = useState<McpFormData>({
name: initialData?.name || '', name: initialData?.name || '',
url: initialData?.url || '', url: initialData?.url || '',
@@ -131,17 +133,17 @@ const McpForm: React.FC<McpFormProps> = ({
const errors: Partial<McpFormData> = {}; const errors: Partial<McpFormData> = {};
if (!formData.name?.trim()) { if (!formData.name?.trim()) {
errors.name = '名称不能为空'; errors.name = t('setting.nameRequired');
} }
if (!formData.url.trim()) { if (!formData.url.trim()) {
errors.url = 'URL不能为空'; errors.url = t('setting.urlRequired');
} else if (!/^https?:\/\/.+/.test(formData.url)) { } else if (!/^https?:\/\/.+/.test(formData.url)) {
errors.url = 'URL格式不正确'; errors.url = t('setting.urlFormatInvalid');
} }
if (!formData.server_type) { if (!formData.server_type) {
errors.server_type = '请选择服务器类型'; errors.server_type = t('setting.serverTypeRequired');
} }
setFormErrors(errors); setFormErrors(errors);
@@ -166,7 +168,7 @@ const McpForm: React.FC<McpFormProps> = ({
if (!formData.url.trim()) { if (!formData.url.trim()) {
setTestResult({ setTestResult({
success: false, success: false,
error: '请先填写 URL', error: t('setting.fillUrlFirst'),
}); });
return; return;
} }
@@ -205,13 +207,13 @@ const McpForm: React.FC<McpFormProps> = ({
} else { } else {
setTestResult({ setTestResult({
success: false, success: false,
error: result.error || '测试失败', error: result.error || t('setting.testFailed'),
}); });
} }
} catch (error: any) { } catch (error: any) {
setTestResult({ setTestResult({
success: false, success: false,
error: error.message || '测试连接时发生错误', error: error.message || t('setting.testConnectionError'),
}); });
} finally { } finally {
setTestLoading(false); setTestLoading(false);
@@ -234,7 +236,7 @@ const McpForm: React.FC<McpFormProps> = ({
return ( return (
<FormContainer> <FormContainer>
<TextField <TextField
label="名称" label={t('common.name')}
value={formData.name || ''} value={formData.name || ''}
onChange={handleInputChange('name')} onChange={handleInputChange('name')}
error={!!formErrors.name} error={!!formErrors.name}
@@ -257,12 +259,12 @@ const McpForm: React.FC<McpFormProps> = ({
/> />
<FormControl fullWidth required error={!!formErrors.server_type}> <FormControl fullWidth required error={!!formErrors.server_type}>
<InputLabel>Server Type</InputLabel> <InputLabel>{t('mcp.serverType')}</InputLabel>
<Select <Select
value={formData.server_type} value={formData.server_type}
onChange={handleInputChange('server_type')} onChange={handleInputChange('server_type')}
disabled={disabled} disabled={disabled}
label="Server Type" label={t('mcp.serverType')}
> >
<MenuItem value="sse">SSE</MenuItem> <MenuItem value="sse">SSE</MenuItem>
<MenuItem value="streamable-http">Streamable HTTP</MenuItem> <MenuItem value="streamable-http">Streamable HTTP</MenuItem>
@@ -276,21 +278,21 @@ const McpForm: React.FC<McpFormProps> = ({
disabled={disabled} disabled={disabled}
fullWidth fullWidth
placeholder="e.g. eyJhbGciOiJIUzI1Ni..." placeholder="e.g. eyJhbGciOiJIUzI1Ni..."
helperText="可选:用于身份验证的令牌" helperText={t('setting.authTokenOptional')}
/> />
{/* 测试连接部分 */} {/* 测试连接部分 */}
{onTest && ( {onTest && (
<TestSection> <TestSection>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}> <Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
<Typography variant="h6"></Typography> <Typography variant="h6">{t('setting.testConnection')}</Typography>
<Button <Button
variant="outlined" variant="outlined"
onClick={handleTest} onClick={handleTest}
disabled={testLoading || disabled} disabled={testLoading || disabled}
startIcon={testLoading ? <CircularProgress size={16} /> : <RefreshIcon />} startIcon={testLoading ? <CircularProgress size={16} /> : <RefreshIcon />}
> >
{testLoading ? '测试中...' : '测试连接'} {testLoading ? t('setting.testing') : t('setting.testConnection')}
</Button> </Button>
</Box> </Box>
@@ -299,16 +301,16 @@ const McpForm: React.FC<McpFormProps> = ({
{testResult.success ? ( {testResult.success ? (
<Box> <Box>
<Alert severity="success" sx={{ mb: 2 }}> <Alert severity="success" sx={{ mb: 2 }}>
{testResult.tools?.length || 0} {t('setting.connectionSuccess', { count: testResult.tools?.length || 0 })}
</Alert> </Alert>
{testResult.tools && testResult.tools.length > 0 && ( {testResult.tools && testResult.tools.length > 0 && (
<Accordion defaultExpanded> <Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<Typography variant="subtitle1"></Typography> <Typography variant="subtitle1">{t('setting.availableTools')}</Typography>
<Chip <Chip
label={`${testResult.tools.length} tools available`} label={`${testResult.tools.length} ${t('mcp.toolsAvailable')}`}
size="small" size="small"
color="primary" color="primary"
/> />
@@ -344,7 +346,7 @@ const McpForm: React.FC<McpFormProps> = ({
</Box> </Box>
) : ( ) : (
<Alert severity="error"> <Alert severity="error">
{testResult.error || '连接失败'} {testResult.error || t('setting.connectionFailed')}
</Alert> </Alert>
)} )}
</Box> </Box>
@@ -360,7 +362,7 @@ const McpForm: React.FC<McpFormProps> = ({
disabled={loading || disabled || !testResult?.success} disabled={loading || disabled || !testResult?.success}
startIcon={loading ? <CircularProgress size={16} /> : undefined} startIcon={loading ? <CircularProgress size={16} /> : undefined}
> >
{loading ? '保存中...' : '保存'} {loading ? t('setting.saving') : t('common.save')}
</Button> </Button>
</Box> </Box>
</FormContainer> </FormContainer>

View File

@@ -2,8 +2,6 @@ import React from 'react';
// 导入独立的对话框组件 // 导入独立的对话框组件
import ApiKeyDialog, { type ApiKeyFormData, type ApiKeyDialogProps } from './Dialog/ApiKeyDialog'; 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 OllamaDialog, { type OllamaFormData, type OllamaDialogProps } from './Dialog/OllamaDialog';
import SystemModelDialog, { type SystemModelFormData, type SystemModelDialogProps, type ModelOption, type ModelGroup } from './Dialog/SystemModelDialog'; 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'; 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 接口 // 导出所有对话框 Props 接口
export type { ApiKeyDialogProps, AzureOpenAIDialogProps, BedrockDialogProps, OllamaDialogProps, SystemModelDialogProps, ConfigurationDialogProps }; export type { ApiKeyDialogProps, OllamaDialogProps, SystemModelDialogProps, ConfigurationDialogProps };
// 导出其他相关接口和常量 // 导出其他相关接口和常量
export type { ModelOption, ModelGroup }; export type { ModelOption, ModelGroup };
export { BEDROCK_REGIONS };
// 模型对话框整合组件的 Props 接口 // 模型对话框整合组件的 Props 接口
export interface ModelDialogsProps { export interface ModelDialogsProps {
@@ -39,22 +36,6 @@ export interface ModelDialogsProps {
initialData?: ApiKeyFormData; initialData?: ApiKeyFormData;
editMode?: boolean; 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: { ollamaDialog: {
open: boolean; open: boolean;
closeDialog: () => void; closeDialog: () => void;
@@ -93,8 +74,6 @@ export interface ModelDialogsProps {
*/ */
export const ModelDialogs: React.FC<ModelDialogsProps> = ({ export const ModelDialogs: React.FC<ModelDialogsProps> = ({
apiKeyDialog, apiKeyDialog,
azureDialog,
bedrockDialog,
ollamaDialog, ollamaDialog,
configurationDialog, configurationDialog,
systemDialog, systemDialog,
@@ -112,26 +91,6 @@ export const ModelDialogs: React.FC<ModelDialogsProps> = ({
editMode={apiKeyDialog!.editMode} 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 对话框 */} {/* Ollama 对话框 */}
<OllamaDialog <OllamaDialog
open={ollamaDialog.open} open={ollamaDialog.open}

View File

@@ -15,6 +15,7 @@ import {
Tooltip Tooltip
} from '@mui/material'; } from '@mui/material';
import { PhotoCamera, Edit } from '@mui/icons-material'; import { PhotoCamera, Edit } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import { useProfileSetting } from '@/hooks/setting-hooks'; import { useProfileSetting } from '@/hooks/setting-hooks';
import { useMessage } from '@/hooks/useSnackbar'; import { useMessage } from '@/hooks/useSnackbar';
import type { IUserInfo } from '@/interfaces/database/user-setting'; import type { IUserInfo } from '@/interfaces/database/user-setting';
@@ -45,6 +46,7 @@ interface ProfileFormProps {
* 个人信息表单 * 个人信息表单
*/ */
function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) { function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
const { t } = useTranslation();
const showMessage = useMessage(); const showMessage = useMessage();
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@@ -91,13 +93,13 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
if (file) { if (file) {
// 检查文件类型 // 检查文件类型
if (!file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
showMessage.error('请选择图片文件'); showMessage.error(t('setting.pleaseSelectImageFile'));
return; return;
} }
// 检查文件大小 (限制为2MB) // 检查文件大小 (限制为2MB)
if (file.size > 2 * 1024 * 1024) { if (file.size > 2 * 1024 * 1024) {
showMessage.error('图片大小不能超过2MB'); showMessage.error(t('setting.imageSizeLimit'));
return; return;
} }
@@ -123,7 +125,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
const handleSave = async () => { const handleSave = async () => {
try { try {
if (!formData.nickname?.trim()) { if (!formData.nickname?.trim()) {
showMessage.error('用户名不能为空'); showMessage.error(t('setting.usernameRequired'));
return; return;
} }
@@ -136,17 +138,17 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
}; };
await onSubmit(updateData); await onSubmit(updateData);
showMessage.success('个人信息更新成功'); showMessage.success(t('setting.profileUpdateSuccess'));
} catch (error) { } catch (error) {
console.error('更新用户信息失败:', error); console.error('更新用户信息失败:', error);
showMessage.error('更新失败,请重试'); showMessage.error(t('setting.updateFailed'));
} }
}; };
return ( return (
<Paper elevation={0} sx={{ p: 3, backgroundColor: 'transparent' }}> <Paper elevation={0} sx={{ p: 3, backgroundColor: 'transparent' }}>
<Typography variant="h6" gutterBottom sx={{ mb: 3, fontWeight: 600 }}> <Typography variant="h6" gutterBottom sx={{ mb: 3, fontWeight: 600 }}>
{t('setting.personalProfile')}
</Typography> </Typography>
<Grid container spacing={3}> <Grid container spacing={3}>
@@ -166,9 +168,9 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
</Avatar> </Avatar>
<Box> <Box>
<Typography variant="body2" color="text.secondary" gutterBottom> <Typography variant="body2" color="text.secondary" gutterBottom>
{t('setting.avatar')}
</Typography> </Typography>
<Tooltip title="上传头像"> <Tooltip title={t('setting.uploadAvatar')}>
<IconButton <IconButton
color="primary" color="primary"
onClick={triggerFileSelect} onClick={triggerFileSelect}
@@ -192,7 +194,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
style={{ display: 'none' }} style={{ display: 'none' }}
/> />
<Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 1 }}> <Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 1 }}>
JPGPNG 2MB {t('setting.avatarFormatTip')}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -202,7 +204,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
<Grid size={{ xs: 12, sm: 6 }}> <Grid size={{ xs: 12, sm: 6 }}>
<TextField <TextField
fullWidth fullWidth
label="用户名" label={t('setting.username')}
value={formData.nickname} value={formData.nickname}
onChange={handleInputChange('nickname')} onChange={handleInputChange('nickname')}
variant="outlined" variant="outlined"
@@ -214,21 +216,21 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
<Grid size={{ xs: 12, sm: 6 }}> <Grid size={{ xs: 12, sm: 6 }}>
<TextField <TextField
fullWidth fullWidth
label="邮箱" label={t('setting.email')}
value={formData.email} value={formData.email}
variant="outlined" variant="outlined"
disabled disabled
helperText="邮箱地址不可修改" helperText={t('setting.emailNotEditable')}
/> />
</Grid> </Grid>
{/* 语言 */} {/* 语言 */}
<Grid size={{ xs: 12, sm: 6 }}> <Grid size={{ xs: 12, sm: 6 }}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel></InputLabel> <InputLabel>{t('setting.language')}</InputLabel>
<Select <Select
value={formData.language} value={formData.language}
label="语言" label={t('setting.language')}
onChange={handleSelectChange('language')} onChange={handleSelectChange('language')}
> >
{languageOptions.map((option) => ( {languageOptions.map((option) => (
@@ -243,10 +245,10 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
{/* 时区 */} {/* 时区 */}
<Grid size={{ xs: 12, sm: 6 }}> <Grid size={{ xs: 12, sm: 6 }}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel></InputLabel> <InputLabel>{t('setting.timezone')}</InputLabel>
<Select <Select
value={formData.timezone} value={formData.timezone}
label="时区" label={t('setting.timezone')}
onChange={handleSelectChange('timezone')} onChange={handleSelectChange('timezone')}
> >
{timezoneOptions.map((option) => ( {timezoneOptions.map((option) => (
@@ -266,7 +268,7 @@ function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
onClick={handleSave} onClick={handleSave}
sx={{ minWidth: 120 }} sx={{ minWidth: 120 }}
> >
{t('setting.save')}
</Button> </Button>
</Box> </Box>
</Grid> </Grid>

View File

@@ -35,6 +35,7 @@ import McpDialog from '@/pages/setting/components/McpDialog';
import type { IMcpServer } from '@/interfaces/database/mcp'; import type { IMcpServer } from '@/interfaces/database/mcp';
import type { IImportMcpServersRequestBody, ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp'; import type { IImportMcpServersRequestBody, ICreateMcpServerRequestBody, ITestMcpRequestBody } from '@/interfaces/request/mcp';
import { useMessage } from '@/hooks/useSnackbar'; import { useMessage } from '@/hooks/useSnackbar';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const PageContainer = styled(Box)(({ theme }) => ({ const PageContainer = styled(Box)(({ theme }) => ({
@@ -69,6 +70,7 @@ const SearchContainer = styled(Box)(({ theme }) => ({
export default function McpSettingPage() { export default function McpSettingPage() {
const { t } = useTranslation();
const handleRefreshServer = async (initial?: boolean) => { const handleRefreshServer = async (initial?: boolean) => {
if (initial) { if (initial) {
@@ -187,9 +189,9 @@ export default function McpSettingPage() {
if (selectedServerId) { if (selectedServerId) {
const result = await deleteMcpServer(selectedServerId); const result = await deleteMcpServer(selectedServerId);
if (result.success) { if (result.success) {
showMessage.success('删除成功'); showMessage.success(t('mcp.deleteSuccess'));
} else { } else {
showMessage.error(result.error || '删除失败'); showMessage.error(t('mcp.deleteFailed'));
} }
} }
handleMenuClose(); handleMenuClose();
@@ -198,19 +200,19 @@ export default function McpSettingPage() {
const handleBulkDelete = async () => { const handleBulkDelete = async () => {
const result = await batchDeleteMcpServers(selectedServers); const result = await batchDeleteMcpServers(selectedServers);
if (result.success) { if (result.success) {
showMessage.success('批量删除成功'); showMessage.success(t('mcp.batchDeleteSuccess'));
setSelectedServers([]); setSelectedServers([]);
} else { } else {
showMessage.error(result.error || '批量删除失败'); showMessage.error(t('mcp.batchDeleteFailed'));
} }
}; };
const handleExport = async () => { const handleExport = async () => {
const result = await exportMcpServers(selectedServers); const result = await exportMcpServers(selectedServers);
if (result.success) { if (result.success) {
showMessage.success('导出成功'); showMessage.success(t('mcp.exportSuccess'));
} else { } else {
showMessage.error(result.error || '导出失败'); showMessage.error(t('mcp.exportFailed'));
} }
}; };
@@ -218,12 +220,12 @@ export default function McpSettingPage() {
try { try {
if (editingServer) { if (editingServer) {
if (!editingServer.id) { if (!editingServer.id) {
showMessage.error('server id 不能为空'); showMessage.error(t('mcp.serverIdRequired'));
return { success: false, error: 'server id 不能为空' }; return { success: false, error: 'server id 不能为空' };
} }
const result = await updateMcpServer({ ...data, mcp_id: editingServer.id ?? '' }); const result = await updateMcpServer({ ...data, mcp_id: editingServer.id ?? '' });
if (result.success) { if (result.success) {
showMessage.success('MCP 服务器更新成功'); showMessage.success(t('mcp.mcpServerUpdateSuccess'));
setMcpDialogOpen(false); setMcpDialogOpen(false);
setEditingServer(null); setEditingServer(null);
return result; return result;
@@ -233,7 +235,7 @@ export default function McpSettingPage() {
} else { } else {
const result = await createMcpServer(data); const result = await createMcpServer(data);
if (result.success) { if (result.success) {
showMessage.success('MCP 服务器创建成功'); showMessage.success(t('mcp.mcpServerCreateSuccess'));
setMcpDialogOpen(false); setMcpDialogOpen(false);
setEditingServer(null); setEditingServer(null);
return result; return result;
@@ -251,7 +253,7 @@ export default function McpSettingPage() {
try { try {
const result = await testMcpServer(data); const result = await testMcpServer(data);
if (result.success) { if (result.success) {
showMessage.success('测试成功'); showMessage.success(t('mcp.testSuccess'));
return result; return result;
} else { } else {
return result; return result;
@@ -283,7 +285,7 @@ export default function McpSettingPage() {
setImportJson(''); setImportJson('');
} }
} else { } else {
showMessage.error('JSON 格式错误'); showMessage.error(t('mcp.jsonFormatError'));
} }
} catch (error) { } catch (error) {
showMessage.error('JSON 格式错误'); showMessage.error('JSON 格式错误');
@@ -294,11 +296,11 @@ export default function McpSettingPage() {
<PageContainer> <PageContainer>
<PageHeader> <PageHeader>
<Box> <Box>
<Typography variant="h4" fontWeight={600} color="#333"> <Typography variant="h5" component="h1" fontWeight="bold">
MCP Servers {t('mcp.mcpServers')}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}> <Typography variant="body2" color="text.secondary">
Customize the list of MCP servers {t('mcp.customizeTheListOfMcpServers')}
</Typography> </Typography>
</Box> </Box>
<Box display="flex" gap={2}> <Box display="flex" gap={2}>
@@ -307,14 +309,14 @@ export default function McpSettingPage() {
startIcon={<ImportIcon />} startIcon={<ImportIcon />}
onClick={() => setImportDialogOpen(true)} onClick={() => setImportDialogOpen(true)}
> >
Import {t('mcp.import')}
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
startIcon={<AddIcon />} startIcon={<AddIcon />}
onClick={handleAdd} onClick={handleAdd}
> >
Add MCP {t('mcp.addMCP')}
</Button> </Button>
</Box> </Box>
</PageHeader> </PageHeader>
@@ -323,7 +325,7 @@ export default function McpSettingPage() {
<Box> <Box>
<TextField <TextField
size="small" size="small"
placeholder="Search MCP servers..." placeholder={t('mcp.searchPlaceholder')}
value={searchString} value={searchString}
onChange={handleSearchChange} onChange={handleSearchChange}
InputProps={{ InputProps={{
@@ -338,16 +340,17 @@ export default function McpSettingPage() {
size="small" size="small"
startIcon={<DeleteIcon />} startIcon={<DeleteIcon />}
onClick={handleBulkDelete} onClick={handleBulkDelete}
color="error" disabled={selectedServers.length === 0}
> >
Delete ({selectedServers.length}) {t('mcp.deleteSelected')}
</Button> </Button>
<Button <Button
size="small" size="small"
startIcon={<ExportIcon />} startIcon={<ExportIcon />}
onClick={handleExport} onClick={handleExport}
disabled={selectedServers.length === 0}
> >
Export ({selectedServers.length}) {t('mcp.exportSelected')}
</Button> </Button>
</Box> </Box>
)} )}
@@ -389,10 +392,10 @@ export default function McpSettingPage() {
{server.name} {server.name}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" gutterBottom> <Typography variant="body2" color="text.secondary" gutterBottom>
: {server.server_type} {t('mcp.type')}: {server.server_type}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <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> </Typography>
</CardContent> </CardContent>
</Card> </Card>
@@ -409,7 +412,7 @@ export default function McpSettingPage() {
color="primary" color="primary"
/> />
<Typography variant="body2" sx={{ ml: 2 }}> <Typography variant="body2" sx={{ ml: 2 }}>
{total} {t('mcp.totalItems', { count: total })}
</Typography> </Typography>
</Box> </Box>
</> </>
@@ -423,11 +426,11 @@ export default function McpSettingPage() {
> >
<MenuItem onClick={handleEdit}> <MenuItem onClick={handleEdit}>
<EditIcon sx={{ mr: 1 }} fontSize="small" /> <EditIcon sx={{ mr: 1 }} fontSize="small" />
Edit {t('common.edit')}
</MenuItem> </MenuItem>
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}> <MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
<DeleteIcon sx={{ mr: 1 }} fontSize="small" /> <DeleteIcon sx={{ mr: 1 }} fontSize="small" />
Delete {t('common.delete')}
</MenuItem> </MenuItem>
</Menu> </Menu>
@@ -446,16 +449,16 @@ export default function McpSettingPage() {
{/* 导入对话框 */} {/* 导入对话框 */}
<Dialog open={importDialogOpen} onClose={() => setImportDialogOpen(false)} maxWidth="md" fullWidth> <Dialog open={importDialogOpen} onClose={() => setImportDialogOpen(false)} maxWidth="md" fullWidth>
<DialogTitle>Import MCP Servers</DialogTitle> <DialogTitle>{t('mcp.importTitle')}</DialogTitle>
<DialogContent> <DialogContent>
<Alert severity="info" sx={{ mb: 2 }}> <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> </Alert>
<TextField <TextField
fullWidth fullWidth
multiline multiline
rows={10} rows={10}
label="JSON Configuration" label={t('mcp.jsonConfiguration')}
value={importJson} value={importJson}
onChange={(e) => setImportJson(e.target.value)} onChange={(e) => setImportJson(e.target.value)}
placeholder={`{ placeholder={`{
@@ -472,9 +475,9 @@ export default function McpSettingPage() {
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setImportDialogOpen(false)}>Cancel</Button> <Button onClick={() => setImportDialogOpen(false)}>{t('common.cancel')}</Button>
<Button onClick={handleImport} variant="contained"> <Button onClick={handleImport} variant="contained">
Import {t('mcp.import')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@@ -23,6 +23,7 @@ import {
Edit as EditIcon, Edit as EditIcon,
Delete as DeleteIcon, Delete as DeleteIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import { useLlmModelSetting } from '@/hooks/setting-hooks'; import { useLlmModelSetting } from '@/hooks/setting-hooks';
import { useModelDialogs } from './hooks/useModelDialogs'; import { useModelDialogs } from './hooks/useModelDialogs';
import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm'; import type { IFactory, IMyLlmModel, ILlmItem } from '@/interfaces/database/llm';
@@ -66,6 +67,7 @@ function MyLlmGridItem({ model, onDelete }: { model: ILlmItem, onDelete: (model:
// 主页面组件 // 主页面组件
function ModelsPage() { function ModelsPage() {
const { t } = useTranslation();
const { llmFactory, myLlm, refreshLlmModel } = useLlmModelSetting(); const { llmFactory, myLlm, refreshLlmModel } = useLlmModelSetting();
const modelDialogs = useModelDialogs(refreshLlmModel); const modelDialogs = useModelDialogs(refreshLlmModel);
@@ -106,12 +108,15 @@ function ModelsPage() {
LLM_FACTORY_LIST.VolcEngine, LLM_FACTORY_LIST.VolcEngine,
] ]
if (LocalLlmFactories.includes(factoryName)) { if (LocalLlmFactories.includes(factoryName)) {
// local llm
modelDialogs.ollamaDialog.openDialog({ modelDialogs.ollamaDialog.openDialog({
llm_factory: factory.name, llm_factory: factory.name,
}); });
} else if (configurationFactories.includes(factoryName)) { } else if (configurationFactories.includes(factoryName)) {
// custom configuration llm
modelDialogs.configurationDialog.openConfigurationDialog(factory.name); modelDialogs.configurationDialog.openConfigurationDialog(factory.name);
} else { } else {
// llm set api
modelDialogs.apiKeyDialog.openApiKeyDialog(factoryName); modelDialogs.apiKeyDialog.openApiKeyDialog(factoryName);
} }
logger.debug('handleConfigureFactory', factory); logger.debug('handleConfigureFactory', factory);
@@ -129,26 +134,26 @@ function ModelsPage() {
// 处理删除单个模型 // 处理删除单个模型
const handleDeleteModel = useCallback(async (factoryName: string, modelName: string) => { const handleDeleteModel = useCallback(async (factoryName: string, modelName: string) => {
dialog.confirm({ dialog.confirm({
title: '确认删除', title: t('setting.confirmDelete'),
content: `是否确认删除模型 ${modelName}`, content: t('setting.confirmDeleteModel', { modelName }),
showCancel: true, showCancel: true,
onConfirm: async () => { onConfirm: async () => {
await modelDialogs.deleteOps.deleteLlm(factoryName, modelName); await modelDialogs.deleteOps.deleteLlm(factoryName, modelName);
}, },
}); });
}, [dialog, modelDialogs.deleteOps]); }, [dialog, modelDialogs.deleteOps, t]);
// 处理删除模型工厂 // 处理删除模型工厂
const handleDeleteFactory = useCallback(async (factoryName: string) => { const handleDeleteFactory = useCallback(async (factoryName: string) => {
dialog.confirm({ dialog.confirm({
title: '确认删除', title: t('setting.confirmDelete'),
content: `是否确认删除模型工厂 ${factoryName}`, content: t('setting.confirmDeleteFactory', { factoryName }),
showCancel: true, showCancel: true,
onConfirm: async () => { onConfirm: async () => {
await modelDialogs.deleteOps.deleteFactory(factoryName); await modelDialogs.deleteOps.deleteFactory(factoryName);
}, },
}); });
}, [dialog, modelDialogs.deleteOps]); }, [dialog, modelDialogs.deleteOps, t]);
if (!llmFactory || !myLlm) { if (!llmFactory || !myLlm) {
return ( return (
@@ -168,28 +173,28 @@ function ModelsPage() {
}}> }}>
<Box> <Box>
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>
{t('setting.modelSettings')}
</Typography> </Typography>
<Typography variant="body1" color="text.secondary" gutterBottom> <Typography variant="body1" color="text.secondary" gutterBottom>
LLM {t('setting.modelSettingsDescription')}
</Typography> </Typography>
</Box> </Box>
{/* 设置默认模型 */} {/* 设置默认模型 */}
<Button variant="contained" color="primary" onClick={() => modelDialogs.systemDialog.openDialog()}> <Button variant="contained" color="primary" onClick={() => modelDialogs.systemDialog.openDialog()}>
{t('setting.setDefaultModel')}
</Button> </Button>
</Box> </Box>
{/* My LLM 部分 */} {/* My LLM 部分 */}
<Box mb={4} mt={2}> <Box mb={4} mt={2}>
{!myLlm || Object.keys(myLlm).length === 0 ? ( {!myLlm || Object.keys(myLlm).length === 0 ? (
<Alert severity="info"> <Alert severity="info">
LLM {t('setting.noModelsConfigured')}
</Alert> </Alert>
) : ( ) : (
<Accordion defaultExpanded> <Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
LLM {t('setting.myLlmModels')}
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
@@ -242,13 +247,13 @@ function ModelsPage() {
variant='contained' color='primary' startIcon={<EditIcon />} variant='contained' color='primary' startIcon={<EditIcon />}
onClick={() => handleEditLlmFactory(factoryName)} onClick={() => handleEditLlmFactory(factoryName)}
> >
{t('setting.edit')}
</Button> </Button>
<Button <Button
variant='outlined' color='primary' startIcon={<DeleteIcon />} variant='outlined' color='primary' startIcon={<DeleteIcon />}
onClick={() => handleDeleteFactory(factoryName)} onClick={() => handleDeleteFactory(factoryName)}
> >
{t('setting.delete')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -281,7 +286,7 @@ function ModelsPage() {
<Accordion defaultExpanded> <Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
LLM {t('setting.llmModelFactories')}
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>

View File

@@ -1,12 +1,14 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Box, Button, Divider, Typography } from "@mui/material"; import { Box, Button, Divider, Typography } from "@mui/material";
import { Lock } from "@mui/icons-material"; import { Lock } from "@mui/icons-material";
import { useTranslation } from 'react-i18next';
import ProfileForm from "./components/ProfileForm"; import ProfileForm from "./components/ProfileForm";
import ChangePasswordDialog from "./components/ChangePasswordDialog"; import ChangePasswordDialog from "./components/ChangePasswordDialog";
import { useProfileSetting } from "@/hooks/setting-hooks"; import { useProfileSetting } from "@/hooks/setting-hooks";
import logger from "@/utils/logger"; import logger from "@/utils/logger";
function ProfileSetting() { function ProfileSetting() {
const { t } = useTranslation();
const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting(); const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting();
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
@@ -31,10 +33,10 @@ function ProfileSetting() {
{/* 密码修改部分 */} {/* 密码修改部分 */}
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom sx={{ mb: 2, fontWeight: 600 }}> <Typography variant="h6" gutterBottom sx={{ mb: 2, fontWeight: 600 }}>
{t('setting.accountSecurity')}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('setting.passwordUpdateTip')}
</Typography> </Typography>
<Button <Button
@@ -51,7 +53,7 @@ function ProfileSetting() {
} }
}} }}
> >
{t('setting.changePassword')}
</Button> </Button>
</Box> </Box>

View File

@@ -11,6 +11,7 @@ import {
Divider, Divider,
Paper, Paper,
} from '@mui/material'; } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { import {
Storage as StorageIcon, Storage as StorageIcon,
Memory as RedisIcon, Memory as RedisIcon,
@@ -39,6 +40,7 @@ const TITLE_MAP = {
task_executor_heartbeat: 'Task Executor', task_executor_heartbeat: 'Task Executor',
}; };
// 图标映射 // 图标映射
const ICON_MAP = { const ICON_MAP = {
es: SearchIcon, es: SearchIcon,
@@ -49,7 +51,16 @@ const ICON_MAP = {
}; };
function SystemSetting() { function SystemSetting() {
const { t } = useTranslation();
const { systemStatus, loading, error, fetchSystemStatus } = useSystemStatus(); 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(() => { useEffect(() => {
fetchSystemStatus(); fetchSystemStatus();
@@ -170,11 +181,11 @@ function SystemSetting() {
<Box p={3}> <Box p={3}>
<Box mb={4}> <Box mb={4}>
<Typography variant="h4" component="h1" gutterBottom sx={{ fontWeight: 'bold' }}> <Typography variant="h4" component="h1" gutterBottom sx={{ fontWeight: 'bold' }}>
{t('setting.systemStatus')}
</Typography> </Typography>
<Typography variant="body1" color="text.secondary" paragraph> <Typography variant="body1" color="text.secondary" paragraph>
{t('setting.systemStatusDescription')}
</Typography> </Typography>
</Box> </Box>
@@ -186,7 +197,7 @@ function SystemSetting() {
</Grid> </Grid>
) : ( ) : (
<Alert severity="info" sx={{ mt: 2 }}> <Alert severity="info" sx={{ mt: 2 }}>
{t('setting.noSystemStatusData')}
</Alert> </Alert>
)} )}
</Box> </Box>

View File

@@ -270,7 +270,7 @@ function TeamsSetting() {
variant="outlined" variant="outlined"
value={inviteEmail} value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)} onChange={(e) => setInviteEmail(e.target.value)}
placeholder={t('setting.emailPlaceholder') || '请输入邀请用户的邮箱地址'} placeholder={t('setting.emailPlaceholder')}
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
@@ -282,7 +282,7 @@ function TeamsSetting() {
variant="contained" variant="contained"
disabled={loading || !inviteEmail.trim()} disabled={loading || !inviteEmail.trim()}
> >
{loading ? t('setting.inviting', 'inviting') : t('setting.invite')} {loading ? t('setting.inviting') : t('setting.invite')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@@ -139,8 +139,7 @@ request.interceptors.response.use(
redirectToLogin(); redirectToLogin();
} else if (data?.code !== 0) { } else if (data?.code !== 0) {
// 处理其他业务错误 // 处理其他业务错误
logger.info('请求出现错误:', data?.message); const error = new CustomError(data?.message || i18n.t('message.requestError'));
const error = new CustomError(data?.message || '请求出现错误');
error.code = data?.code || -1; error.code = data?.code || -1;
error.response = data; error.response = data;
snackbar.warning(error.message); snackbar.warning(error.message);