mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-12 08:46:02 +00:00
Add i18n support with language selector on login page (#1410)
* feat: add i18n support with language selector on login page Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * feat: complete i18n implementation for all webui components Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com> * feat: complete all hardcoded text * feat: dynamic label i18n * fix: lint errors * fix: lint errors * delete sh fils * fix: edit model dialog title --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
This commit is contained in:
committed by
GitHub
parent
91cd8cf380
commit
2bf94539bd
@@ -1,7 +1,9 @@
|
||||
import styles from './LLMCard.module.css';
|
||||
import { LLMCardVO } from '@/app/home/models/component/llm-card/LLMCardVO';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function checkAbilityBadges(abilities: string[]) {
|
||||
function AbilityBadges(abilities: string[]) {
|
||||
const { t } = useTranslation();
|
||||
const abilityBadges = {
|
||||
vision: (
|
||||
<div key="vision" className={`${styles.abilityBadge}`}>
|
||||
@@ -13,7 +15,9 @@ function checkAbilityBadges(abilities: string[]) {
|
||||
>
|
||||
<path d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM12 7C14.7614 7 17 9.23858 17 12C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12C7 11.4872 7.07719 10.9925 7.22057 10.5268C7.61175 11.3954 8.48527 12 9.5 12C10.8807 12 12 10.8807 12 9.5C12 8.48527 11.3954 7.61175 10.5269 7.21995C10.9925 7.07719 11.4872 7 12 7Z"></path>
|
||||
</svg>
|
||||
<span className={`${styles.abilityLabel}`}>视觉能力</span>
|
||||
<span className={`${styles.abilityLabel}`}>
|
||||
{t('models.visionAbility')}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
func_call: (
|
||||
@@ -26,7 +30,9 @@ function checkAbilityBadges(abilities: string[]) {
|
||||
>
|
||||
<path d="M5.32943 3.27158C6.56252 2.8332 7.9923 3.10749 8.97927 4.09446C10.1002 5.21537 10.3019 6.90741 9.5843 8.23385L20.293 18.9437L18.8788 20.3579L8.16982 9.64875C6.84325 10.3669 5.15069 10.1654 4.02952 9.04421C3.04227 8.05696 2.7681 6.62665 3.20701 5.39332L5.44373 7.63C6.02952 8.21578 6.97927 8.21578 7.56505 7.63C8.15084 7.04421 8.15084 6.09446 7.56505 5.50868L5.32943 3.27158ZM15.6968 5.15512L18.8788 3.38736L20.293 4.80157L18.5252 7.98355L16.7574 8.3371L14.6361 10.4584L13.2219 9.04421L15.3432 6.92289L15.6968 5.15512ZM8.97927 13.2868L10.3935 14.7011L5.09018 20.0044C4.69966 20.3949 4.06649 20.3949 3.67597 20.0044C3.31334 19.6417 3.28744 19.0699 3.59826 18.6774L3.67597 18.5902L8.97927 13.2868Z"></path>
|
||||
</svg>
|
||||
<span className={`${styles.abilityLabel}`}>函数调用</span>
|
||||
<span className={`${styles.abilityLabel}`}>
|
||||
{t('models.functionCallAbility')}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -83,7 +89,7 @@ export default function LLMCard({ cardVO }: { cardVO: LLMCardVO }) {
|
||||
</div>
|
||||
{/* 能力 */}
|
||||
<div className={`${styles.abilitiesContainer}`}>
|
||||
{checkAbilityBadges(cardVO.abilities)}
|
||||
{AbilityBadges(cardVO.abilities)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { UUID } from 'uuidjs';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@@ -38,41 +39,47 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { toast } from 'sonner';
|
||||
const extraArgSchema = z
|
||||
.object({
|
||||
key: z.string().min(1, { message: '键名不能为空' }),
|
||||
type: z.enum(['string', 'number', 'boolean']),
|
||||
value: z.string(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.type === 'number' && isNaN(Number(data.value))) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: '必须是有效的数字',
|
||||
path: ['value'],
|
||||
});
|
||||
}
|
||||
if (
|
||||
data.type === 'boolean' &&
|
||||
data.value !== 'true' &&
|
||||
data.value !== 'false'
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: '必须是 true 或 false',
|
||||
path: ['value'],
|
||||
});
|
||||
}
|
||||
});
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, { message: '模型名称不能为空' }),
|
||||
model_provider: z.string().min(1, { message: '模型供应商不能为空' }),
|
||||
url: z.string().min(1, { message: '请求URL不能为空' }),
|
||||
api_key: z.string().min(1, { message: 'API Key不能为空' }),
|
||||
abilities: z.array(z.string()),
|
||||
extra_args: z.array(extraArgSchema).optional(),
|
||||
});
|
||||
const getExtraArgSchema = (t: (key: string) => string) =>
|
||||
z
|
||||
.object({
|
||||
key: z.string().min(1, { message: t('models.keyNameRequired') }),
|
||||
type: z.enum(['string', 'number', 'boolean']),
|
||||
value: z.string(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.type === 'number' && isNaN(Number(data.value))) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t('models.mustBeValidNumber'),
|
||||
path: ['value'],
|
||||
});
|
||||
}
|
||||
if (
|
||||
data.type === 'boolean' &&
|
||||
data.value !== 'true' &&
|
||||
data.value !== 'false'
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t('models.mustBeTrueOrFalse'),
|
||||
path: ['value'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const getFormSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
name: z.string().min(1, { message: t('models.modelNameRequired') }),
|
||||
model_provider: z
|
||||
.string()
|
||||
.min(1, { message: t('models.modelProviderRequired') }),
|
||||
url: z.string().min(1, { message: t('models.requestURLRequired') }),
|
||||
api_key: z.string().min(1, { message: t('models.apiKeyRequired') }),
|
||||
abilities: z.array(z.string()),
|
||||
extra_args: z.array(getExtraArgSchema(t)).optional(),
|
||||
});
|
||||
|
||||
export default function LLMForm({
|
||||
editMode,
|
||||
@@ -87,6 +94,9 @@ export default function LLMForm({
|
||||
onFormCancel: () => void;
|
||||
onLLMDeleted: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const formSchema = getFormSchema(t);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
@@ -106,11 +116,11 @@ export default function LLMForm({
|
||||
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
|
||||
const abilityOptions: { label: string; value: string }[] = [
|
||||
{
|
||||
label: '视觉能力',
|
||||
label: t('models.visionAbility'),
|
||||
value: 'vision',
|
||||
},
|
||||
{
|
||||
label: '函数调用',
|
||||
label: t('models.functionCallAbility'),
|
||||
value: 'func_call',
|
||||
},
|
||||
];
|
||||
@@ -185,7 +195,7 @@ export default function LLMForm({
|
||||
setRequesterNameList(
|
||||
requesterNameList.requesters.map((item) => {
|
||||
return {
|
||||
label: item.label.zh_CN,
|
||||
label: i18nObj(item.label),
|
||||
value: item.name,
|
||||
};
|
||||
}),
|
||||
@@ -223,15 +233,17 @@ export default function LLMForm({
|
||||
|
||||
function handleFormSubmit(value: z.infer<typeof formSchema>) {
|
||||
const extraArgsObj: Record<string, string | number | boolean> = {};
|
||||
value.extra_args?.forEach((arg) => {
|
||||
if (arg.type === 'number') {
|
||||
extraArgsObj[arg.key] = Number(arg.value);
|
||||
} else if (arg.type === 'boolean') {
|
||||
extraArgsObj[arg.key] = arg.value === 'true';
|
||||
} else {
|
||||
extraArgsObj[arg.key] = arg.value;
|
||||
}
|
||||
});
|
||||
value.extra_args?.forEach(
|
||||
(arg: { key: string; type: string; value: string }) => {
|
||||
if (arg.type === 'number') {
|
||||
extraArgsObj[arg.key] = Number(arg.value);
|
||||
} else if (arg.type === 'boolean') {
|
||||
extraArgsObj[arg.key] = arg.value === 'true';
|
||||
} else {
|
||||
extraArgsObj[arg.key] = arg.value;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const llmModel: LLMModel = {
|
||||
uuid: editMode ? initLLMId || '' : UUID.generate(),
|
||||
@@ -262,9 +274,9 @@ export default function LLMForm({
|
||||
try {
|
||||
await httpClient.createProviderLLMModel(llmModel);
|
||||
onFormSubmit();
|
||||
toast.success('创建成功');
|
||||
toast.success(t('models.createSuccess'));
|
||||
} catch (err) {
|
||||
toast.error('创建失败:' + (err as Error).message);
|
||||
toast.error(t('models.createError') + (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,9 +284,9 @@ export default function LLMForm({
|
||||
try {
|
||||
await httpClient.updateProviderLLMModel(initLLMId || '', llmModel);
|
||||
onFormSubmit();
|
||||
toast.success('保存成功');
|
||||
toast.success(t('models.saveSuccess'));
|
||||
} catch (err) {
|
||||
toast.error('保存失败:' + (err as Error).message);
|
||||
toast.error(t('models.saveError') + (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,10 +296,10 @@ export default function LLMForm({
|
||||
.deleteProviderLLMModel(initLLMId)
|
||||
.then(() => {
|
||||
onLLMDeleted();
|
||||
toast.success('删除成功');
|
||||
toast.success(t('models.deleteSuccess'));
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('删除失败:' + err.message);
|
||||
toast.error(t('models.deleteError') + err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -300,15 +312,17 @@ export default function LLMForm({
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>删除确认</DialogTitle>
|
||||
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>你确定要删除这个模型吗?</DialogDescription>
|
||||
<DialogDescription>
|
||||
{t('models.deleteConfirmation')}
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDeleteConfirmModal(false)}
|
||||
>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -317,7 +331,7 @@ export default function LLMForm({
|
||||
setShowDeleteConfirmModal(false);
|
||||
}}
|
||||
>
|
||||
确认删除
|
||||
{t('common.confirmDelete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -335,14 +349,15 @@ export default function LLMForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
模型名称<span className="text-red-500">*</span>
|
||||
{t('models.modelName')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
请填写供应商向您提供的模型名称
|
||||
{t('models.modelProviderDescription')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -354,7 +369,8 @@ export default function LLMForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
模型供应商<span className="text-red-500">*</span>
|
||||
{t('models.modelProvider')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
@@ -370,7 +386,9 @@ export default function LLMForm({
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="选择模型供应商" />
|
||||
<SelectValue
|
||||
placeholder={t('models.selectModelProvider')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -394,7 +412,8 @@ export default function LLMForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
请求URL<span className="text-red-500">*</span>
|
||||
{t('models.requestURL')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
@@ -409,7 +428,8 @@ export default function LLMForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
API Key<span className="text-red-500">*</span>
|
||||
{t('models.apiKey')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
@@ -423,9 +443,11 @@ export default function LLMForm({
|
||||
name="abilities"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>能力</FormLabel>
|
||||
<FormLabel>{t('models.abilities')}</FormLabel>
|
||||
<div className="mb-0">
|
||||
<FormDescription>选择模型能力</FormDescription>
|
||||
<FormDescription>
|
||||
{t('models.selectModelAbilities')}
|
||||
</FormDescription>
|
||||
</div>
|
||||
{abilityOptions.map((item) => (
|
||||
<FormField
|
||||
@@ -452,7 +474,8 @@ export default function LLMForm({
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) => value !== item.value,
|
||||
(value: string) =>
|
||||
value !== item.value,
|
||||
),
|
||||
);
|
||||
}}
|
||||
@@ -472,12 +495,12 @@ export default function LLMForm({
|
||||
/>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel>额外参数</FormLabel>
|
||||
<FormLabel>{t('models.extraParameters')}</FormLabel>
|
||||
<div className="space-y-2">
|
||||
{extraArgs.map((arg, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder="键名"
|
||||
placeholder={t('models.keyName')}
|
||||
value={arg.key}
|
||||
onChange={(e) =>
|
||||
updateExtraArg(index, 'key', e.target.value)
|
||||
@@ -490,16 +513,22 @@ export default function LLMForm({
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="类型" />
|
||||
<SelectValue placeholder={t('models.type')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">字符串</SelectItem>
|
||||
<SelectItem value="number">数字</SelectItem>
|
||||
<SelectItem value="boolean">布尔值</SelectItem>
|
||||
<SelectItem value="string">
|
||||
{t('models.string')}
|
||||
</SelectItem>
|
||||
<SelectItem value="number">
|
||||
{t('models.number')}
|
||||
</SelectItem>
|
||||
<SelectItem value="boolean">
|
||||
{t('models.boolean')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
placeholder="值"
|
||||
placeholder={t('models.value')}
|
||||
value={arg.value}
|
||||
onChange={(e) =>
|
||||
updateExtraArg(index, 'value', e.target.value)
|
||||
@@ -522,11 +551,11 @@ export default function LLMForm({
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" variant="outline" onClick={addExtraArg}>
|
||||
添加参数
|
||||
{t('models.addParameter')}
|
||||
</Button>
|
||||
</div>
|
||||
<FormDescription>
|
||||
将在请求时附加到请求体中,如 max_tokens, temperature, top_p 等
|
||||
{t('models.extraParametersDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -538,18 +567,20 @@ export default function LLMForm({
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteConfirmModal(true)}
|
||||
>
|
||||
删除
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button type="submit">{editMode ? '保存' : '提交'}</Button>
|
||||
<Button type="submit">
|
||||
{editMode ? t('common.save') : t('common.submit')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onFormCancel()}
|
||||
>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user