mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 11:29:39 +08: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
98
web/package-lock.json
generated
98
web/package-lock.json
generated
@@ -25,6 +25,8 @@
|
||||
"axios": "^1.8.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^25.1.2",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.507.0",
|
||||
"next": "15.2.4",
|
||||
@@ -33,6 +35,7 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"react-i18next": "^15.5.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.5",
|
||||
@@ -68,6 +71,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
|
||||
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
@@ -4375,6 +4387,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz",
|
||||
@@ -4384,6 +4405,46 @@
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.1.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.1.2.tgz",
|
||||
"integrity": "sha512-SP63m8LzdjkrAjruH7SCI3ndPSgjt4/wX7ouUUOzCW/eY+HzlIo19IQSfYA9X3qRiRP1SYtaTsg/Oz/PGsfD8w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz",
|
||||
"integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -6033,6 +6094,32 @@
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz",
|
||||
"integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -7069,7 +7156,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7184,6 +7271,15 @@
|
||||
"uuidjs": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
"axios": "^1.8.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^25.1.2",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.507.0",
|
||||
"next": "15.2.4",
|
||||
@@ -41,6 +43,7 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"react-i18next": "^15.5.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.5",
|
||||
|
||||
@@ -18,6 +18,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@@ -46,15 +47,19 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, { message: '机器人名称不能为空' }),
|
||||
description: z.string().min(1, { message: '机器人描述不能为空' }),
|
||||
adapter: z.string().min(1, { message: '适配器不能为空' }),
|
||||
adapter_config: z.record(z.string(), z.any()),
|
||||
enable: z.boolean().optional(),
|
||||
use_pipeline_uuid: z.string().optional(),
|
||||
});
|
||||
const getFormSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
name: z.string().min(1, { message: t('bots.botNameRequired') }),
|
||||
description: z
|
||||
.string()
|
||||
.min(1, { message: t('bots.botDescriptionRequired') }),
|
||||
adapter: z.string().min(1, { message: t('bots.adapterRequired') }),
|
||||
adapter_config: z.record(z.string(), z.any()),
|
||||
enable: z.boolean().optional(),
|
||||
use_pipeline_uuid: z.string().optional(),
|
||||
});
|
||||
|
||||
export default function BotForm({
|
||||
initBotId,
|
||||
@@ -64,16 +69,19 @@ export default function BotForm({
|
||||
onNewBotCreated,
|
||||
}: {
|
||||
initBotId?: string;
|
||||
onFormSubmit: (value: z.infer<typeof formSchema>) => void;
|
||||
onFormSubmit: (value: z.infer<ReturnType<typeof getFormSchema>>) => void;
|
||||
onFormCancel: () => void;
|
||||
onBotDeleted: () => void;
|
||||
onNewBotCreated: (botId: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const formSchema = getFormSchema(t);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '一个机器人',
|
||||
description: t('bots.defaultDescription'),
|
||||
adapter: '',
|
||||
adapter_config: {},
|
||||
enable: true,
|
||||
@@ -129,7 +137,7 @@ export default function BotForm({
|
||||
// dynamicForm.setFieldsValue(val.adapter_config);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('获取机器人配置失败:' + err.message);
|
||||
toast.error(t('bots.getBotConfigError') + err.message);
|
||||
});
|
||||
} else {
|
||||
form.reset();
|
||||
@@ -156,7 +164,7 @@ export default function BotForm({
|
||||
setAdapterNameList(
|
||||
adaptersRes.adapters.map((item) => {
|
||||
return {
|
||||
label: item.label.zh_CN,
|
||||
label: i18nObj(item.label),
|
||||
value: item.name,
|
||||
};
|
||||
}),
|
||||
@@ -177,7 +185,7 @@ export default function BotForm({
|
||||
setAdapterDescriptionList(
|
||||
adaptersRes.adapters.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.name] = item.description.zh_CN;
|
||||
acc[item.name] = i18nObj(item.description);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
@@ -266,10 +274,10 @@ export default function BotForm({
|
||||
.then((res) => {
|
||||
console.log('update bot success', res);
|
||||
onFormSubmit(form.getValues());
|
||||
toast.success('保存成功');
|
||||
toast.success(t('bots.saveSuccess'));
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('保存失败:' + err.message);
|
||||
toast.error(t('bots.saveError') + err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
@@ -289,7 +297,7 @@ export default function BotForm({
|
||||
.createBot(newBot)
|
||||
.then((res) => {
|
||||
console.log('create bot success', res);
|
||||
toast.success('创建成功 请启用或修改绑定流水线');
|
||||
toast.success(t('bots.createSuccess'));
|
||||
initBotId = res.uuid;
|
||||
|
||||
setBotFormValues();
|
||||
@@ -297,7 +305,7 @@ export default function BotForm({
|
||||
onNewBotCreated(res.uuid);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('创建失败:' + err.message);
|
||||
toast.error(t('bots.createError') + err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
@@ -315,10 +323,10 @@ export default function BotForm({
|
||||
.deleteBot(initBotId)
|
||||
.then(() => {
|
||||
onBotDeleted();
|
||||
toast.success('删除成功');
|
||||
toast.success(t('bots.deleteSuccess'));
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('删除失败:' + err.message);
|
||||
toast.error(t('bots.deleteError') + err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -331,9 +339,9 @@ export default function BotForm({
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>删除确认</DialogTitle>
|
||||
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>你确定要删除这个机器人吗?</DialogDescription>
|
||||
<DialogDescription>{t('bots.deleteConfirmation')}</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -348,7 +356,7 @@ export default function BotForm({
|
||||
setShowDeleteConfirmModal(false);
|
||||
}}
|
||||
>
|
||||
确认删除
|
||||
{t('common.confirmDelete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -368,7 +376,7 @@ export default function BotForm({
|
||||
name="enable"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
||||
<FormLabel>是否启用</FormLabel>
|
||||
<FormLabel>{t('common.enable')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
@@ -384,11 +392,13 @@ export default function BotForm({
|
||||
name="use_pipeline_uuid"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col justify-start gap-[0.8rem] h-[3.8rem]">
|
||||
<FormLabel>绑定流水线</FormLabel>
|
||||
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} {...field}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择流水线" />
|
||||
<SelectValue
|
||||
placeholder={t('bots.selectPipeline')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="fixed z-[1000]">
|
||||
<SelectGroup>
|
||||
@@ -413,7 +423,8 @@ export default function BotForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
机器人名称<span className="text-red-500">*</span>
|
||||
{t('bots.botName')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
@@ -428,7 +439,8 @@ export default function BotForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
机器人描述<span className="text-red-500">*</span>
|
||||
{t('bots.botDescription')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
@@ -444,7 +456,8 @@ export default function BotForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
平台/适配器选择<span className="text-red-500">*</span>
|
||||
{t('bots.platformAdapter')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
@@ -456,7 +469,7 @@ export default function BotForm({
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="选择适配器" />
|
||||
<SelectValue placeholder={t('bots.selectAdapter')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="fixed z-[1000]">
|
||||
<SelectGroup>
|
||||
@@ -499,7 +512,9 @@ export default function BotForm({
|
||||
|
||||
{showDynamicForm && dynamicFormConfigList.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="text-lg font-medium">适配器配置</div>
|
||||
<div className="text-lg font-medium">
|
||||
{t('bots.adapterConfig')}
|
||||
</div>
|
||||
<DynamicFormComponent
|
||||
itemConfigList={dynamicFormConfigList}
|
||||
initialValues={form.watch('adapter_config')}
|
||||
@@ -518,7 +533,7 @@ export default function BotForm({
|
||||
type="submit"
|
||||
onClick={form.handleSubmit(onDynamicFormSubmit)}
|
||||
>
|
||||
提交
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
)}
|
||||
{initBotId && (
|
||||
@@ -528,13 +543,13 @@ export default function BotForm({
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteConfirmModal(true)}
|
||||
>
|
||||
删除
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={form.handleSubmit(onDynamicFormSubmit)}
|
||||
>
|
||||
保存
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -543,7 +558,7 @@ export default function BotForm({
|
||||
variant="outline"
|
||||
onClick={() => onFormCancel()}
|
||||
>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
export default function BotConfigPage() {
|
||||
const { t } = useTranslation();
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [botList, setBotList] = useState<BotCardVO[]>([]);
|
||||
const [isEditForm, setIsEditForm] = useState(false);
|
||||
@@ -29,7 +33,7 @@ export default function BotConfigPage() {
|
||||
const adapterListResp = await httpClient.getAdapters();
|
||||
const adapterList = adapterListResp.adapters.map((adapter: Adapter) => {
|
||||
return {
|
||||
label: adapter.label.zh_CN,
|
||||
label: i18nObj(adapter.label),
|
||||
value: adapter.name,
|
||||
};
|
||||
});
|
||||
@@ -53,7 +57,7 @@ export default function BotConfigPage() {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('get bot list error', err);
|
||||
toast.error('获取机器人列表失败:' + err.message);
|
||||
toast.error(t('bots.getBotListError') + err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
// setIsLoading(false);
|
||||
@@ -78,7 +82,7 @@ export default function BotConfigPage() {
|
||||
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 pb-4">
|
||||
<DialogTitle>
|
||||
{isEditForm ? '编辑机器人' : '创建机器人'}
|
||||
{isEditForm ? t('bots.editBot') : t('bots.createBot')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6">
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
|
||||
import { useEffect } from 'react';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
export default function DynamicFormComponent({
|
||||
itemConfigList,
|
||||
@@ -141,7 +142,7 @@ export default function DynamicFormComponent({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{config.label.zh_CN}{' '}
|
||||
{i18nObj(config.label)}{' '}
|
||||
{config.required && <span className="text-red-500">*</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
@@ -149,7 +150,7 @@ export default function DynamicFormComponent({
|
||||
</FormControl>
|
||||
{config.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{config.description.zh_CN}
|
||||
{i18nObj(config.description)}
|
||||
</p>
|
||||
)}
|
||||
<FormMessage />
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/hover-card';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
export default function DynamicFormItemComponent({
|
||||
config,
|
||||
@@ -33,6 +35,7 @@ export default function DynamicFormItemComponent({
|
||||
field: ControllerRenderProps<any, any>;
|
||||
}) {
|
||||
const [llmModels, setLlmModels] = useState<LLMModel[]>([]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (config.type === DynamicFormItemType.LLM_MODEL_SELECTOR) {
|
||||
@@ -106,7 +109,7 @@ export default function DynamicFormItemComponent({
|
||||
field.onChange([...field.value, '']);
|
||||
}}
|
||||
>
|
||||
添加
|
||||
{t('common.add')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -115,13 +118,13 @@ export default function DynamicFormItemComponent({
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择" />
|
||||
<SelectValue placeholder={t('common.select')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{config.options?.map((option) => (
|
||||
<SelectItem key={option.name} value={option.name}>
|
||||
{option.label.zh_CN}
|
||||
{i18nObj(option.label)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -133,7 +136,7 @@ export default function DynamicFormItemComponent({
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择模型" />
|
||||
<SelectValue placeholder={t('models.selectModel')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -205,9 +208,9 @@ export default function DynamicFormItemComponent({
|
||||
)}
|
||||
<span>
|
||||
{ability === 'vision'
|
||||
? '视觉能力'
|
||||
? t('models.visionAbility')
|
||||
: ability === 'func_call'
|
||||
? '函数调用'
|
||||
? t('models.functionCallAbility')
|
||||
: ability}
|
||||
</span>
|
||||
</div>
|
||||
@@ -217,7 +220,9 @@ export default function DynamicFormItemComponent({
|
||||
{model.extra_args &&
|
||||
Object.keys(model.extra_args).length > 0 && (
|
||||
<div className="text-xs">
|
||||
<div className="font-semibold mb-1">额外参数:</div>
|
||||
<div className="font-semibold mb-1">
|
||||
{t('models.extraParameters')}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{Object.entries(
|
||||
model.extra_args as Record<string, unknown>,
|
||||
@@ -321,7 +326,7 @@ export default function DynamicFormItemComponent({
|
||||
field.onChange([...field.value, { role: 'user', content: '' }]);
|
||||
}}
|
||||
>
|
||||
添加回合
|
||||
{t('common.addRound')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useRouter, usePathname } from 'next/navigation';
|
||||
import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList';
|
||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// TODO 侧边导航栏要加动画
|
||||
export default function HomeSidebar({
|
||||
@@ -27,14 +28,15 @@ export default function HomeSidebar({
|
||||
|
||||
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('HomeSidebar挂载完成');
|
||||
initSelect();
|
||||
if (!localStorage.getItem('token')) {
|
||||
localStorage.setItem('token', 'test-token');
|
||||
localStorage.setItem('userEmail', 'test@example.com');
|
||||
}
|
||||
return () => console.log('HomeSidebar卸载');
|
||||
return () => console.log('sidebar.unmounted');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -148,7 +150,7 @@ export default function HomeSidebar({
|
||||
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM13 13.3551V14H11V12.5C11 11.9477 11.4477 11.5 12 11.5C12.8284 11.5 13.5 10.8284 13.5 10C13.5 9.17157 12.8284 8.5 12 8.5C11.2723 8.5 10.6656 9.01823 10.5288 9.70577L8.56731 9.31346C8.88637 7.70919 10.302 6.5 12 6.5C13.933 6.5 15.5 8.067 15.5 10C15.5 11.5855 14.4457 12.9248 13 13.3551Z"></path>
|
||||
</svg>
|
||||
}
|
||||
name="帮助文档"
|
||||
name={t('common.helpDocs')}
|
||||
/>
|
||||
<SidebarChild
|
||||
onClick={() => {
|
||||
@@ -164,7 +166,7 @@ export default function HomeSidebar({
|
||||
<path d="M4 18H6V20H18V4H6V6H4V3C4 2.44772 4.44772 2 5 2H19C19.5523 2 20 2.44772 20 3V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V18ZM6 11H13V13H6V16L1 12L6 8V11Z"></path>
|
||||
</svg>
|
||||
}
|
||||
name="退出登录"
|
||||
name={t('common.logout')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild';
|
||||
import styles from './HomeSidebar.module.css';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
const t = (key: string) => {
|
||||
return i18n.t(key);
|
||||
};
|
||||
|
||||
export const sidebarConfigList = [
|
||||
new SidebarChildVO({
|
||||
id: 'bots',
|
||||
name: '机器人',
|
||||
name: t('bots.title'),
|
||||
icon: (
|
||||
<svg
|
||||
className={`${styles.sidebarChildIcon}`}
|
||||
@@ -16,12 +21,12 @@ export const sidebarConfigList = [
|
||||
</svg>
|
||||
),
|
||||
route: '/home/bots',
|
||||
description: '创建和管理机器人,这是 LangBot 与各个平台连接的入口',
|
||||
description: t('bots.description'),
|
||||
helpLink: 'https://docs.langbot.app/zh/deploy/platforms/readme.html',
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'models',
|
||||
name: '模型配置',
|
||||
name: t('models.title'),
|
||||
icon: (
|
||||
<svg
|
||||
className={`${styles.sidebarChildIcon}`}
|
||||
@@ -33,12 +38,12 @@ export const sidebarConfigList = [
|
||||
</svg>
|
||||
),
|
||||
route: '/home/models',
|
||||
description: '配置和管理可在流水线中使用的模型',
|
||||
description: t('models.description'),
|
||||
helpLink: 'https://docs.langbot.app/zh/deploy/models/readme.html',
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'pipelines',
|
||||
name: '流水线',
|
||||
name: t('pipelines.title'),
|
||||
icon: (
|
||||
<svg
|
||||
className={`${styles.sidebarChildIcon}`}
|
||||
@@ -50,12 +55,12 @@ export const sidebarConfigList = [
|
||||
</svg>
|
||||
),
|
||||
route: '/home/pipelines',
|
||||
description: '流水线定义了对消息事件的处理流程,用于绑定到机器人',
|
||||
description: t('pipelines.description'),
|
||||
helpLink: 'https://docs.langbot.app/zh/deploy/pipelines/readme.html',
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'plugins',
|
||||
name: '插件管理',
|
||||
name: t('plugins.title'),
|
||||
icon: (
|
||||
<svg
|
||||
className={`${styles.sidebarChildIcon}`}
|
||||
@@ -67,7 +72,7 @@ export const sidebarConfigList = [
|
||||
</svg>
|
||||
),
|
||||
route: '/home/plugins',
|
||||
description: '安装和配置用于扩展 LangBot 功能的插件',
|
||||
description: t('plugins.description'),
|
||||
helpLink: 'https://docs.langbot.app/zh/plugin/plugin-intro.html',
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -15,8 +15,11 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
export default function LLMConfigPage() {
|
||||
const { t } = useTranslation();
|
||||
const [cardList, setCardList] = useState<LLMCardVO[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [isEditForm, setIsEditForm] = useState(false);
|
||||
@@ -30,7 +33,7 @@ export default function LLMConfigPage() {
|
||||
const requesterNameListResp = await httpClient.getProviderRequesters();
|
||||
const requesterNameList = requesterNameListResp.requesters.map((item) => {
|
||||
return {
|
||||
label: item.label.zh_CN,
|
||||
label: i18nObj(item.label),
|
||||
value: item.name,
|
||||
};
|
||||
});
|
||||
@@ -56,7 +59,7 @@ export default function LLMConfigPage() {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('get LLM model list error', err);
|
||||
toast.error('获取模型列表失败:' + err.message);
|
||||
toast.error(t('models.getModelListError') + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,7 +80,9 @@ export default function LLMConfigPage() {
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="w-[700px] p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEditForm ? '预览模型' : '创建模型'}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{isEditForm ? t('models.editModel') : t('models.createModel')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<LLMForm
|
||||
editMode={isEditForm}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import styles from './pipelineCard.module.css';
|
||||
import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
<div className={`${styles.basicInfoContainer}`}>
|
||||
@@ -24,7 +26,8 @@ export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
|
||||
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"></path>
|
||||
</svg>
|
||||
<div className={`${styles.basicInfoUpdateTimeText}`}>
|
||||
更新于{cardVO.lastUpdatedTimeAgo}
|
||||
{t('pipelines.updateTime')}
|
||||
{cardVO.lastUpdatedTimeAgo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +43,9 @@ export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
|
||||
>
|
||||
<path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z"></path>
|
||||
</svg>
|
||||
<div className={styles.operationDefaultBadgeText}>默认</div>
|
||||
<div className={styles.operationDefaultBadgeText}>
|
||||
{t('pipelines.defaultBadge')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
export default function PipelineFormComponent({
|
||||
initValues,
|
||||
@@ -48,11 +50,14 @@ export default function PipelineFormComponent({
|
||||
onFinish: () => void;
|
||||
onNewPipelineCreated: (pipelineId: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const formSchema = isEditMode
|
||||
? z.object({
|
||||
basic: z.object({
|
||||
name: z.string().min(1, { message: '名称不能为空' }),
|
||||
description: z.string().min(1, { message: '描述不能为空' }),
|
||||
name: z.string().min(1, { message: t('pipelines.nameRequired') }),
|
||||
description: z
|
||||
.string()
|
||||
.min(1, { message: t('pipelines.descriptionRequired') }),
|
||||
}),
|
||||
ai: z.record(z.string(), z.any()),
|
||||
trigger: z.record(z.string(), z.any()),
|
||||
@@ -61,8 +66,10 @@ export default function PipelineFormComponent({
|
||||
})
|
||||
: z.object({
|
||||
basic: z.object({
|
||||
name: z.string().min(1, { message: '名称不能为空' }),
|
||||
description: z.string().min(1, { message: '描述不能为空' }),
|
||||
name: z.string().min(1, { message: t('pipelines.nameRequired') }),
|
||||
description: z
|
||||
.string()
|
||||
.min(1, { message: t('pipelines.descriptionRequired') }),
|
||||
}),
|
||||
ai: z.record(z.string(), z.any()).optional(),
|
||||
trigger: z.record(z.string(), z.any()).optional(),
|
||||
@@ -74,13 +81,13 @@ export default function PipelineFormComponent({
|
||||
// 这里不好,可以改成enum等
|
||||
const formLabelList: FormLabel[] = isEditMode
|
||||
? [
|
||||
{ label: '基础信息', name: 'basic' },
|
||||
{ label: 'AI 能力', name: 'ai' },
|
||||
{ label: '触发条件', name: 'trigger' },
|
||||
{ label: '安全控制', name: 'safety' },
|
||||
{ label: '输出处理', name: 'output' },
|
||||
{ label: t('pipelines.basicInfo'), name: 'basic' },
|
||||
{ label: t('pipelines.aiCapabilities'), name: 'ai' },
|
||||
{ label: t('pipelines.triggerConditions'), name: 'trigger' },
|
||||
{ label: t('pipelines.safetyControls'), name: 'safety' },
|
||||
{ label: t('pipelines.outputProcessing'), name: 'output' },
|
||||
]
|
||||
: [{ label: '基础信息', name: 'basic' }];
|
||||
: [{ label: t('pipelines.basicInfo'), name: 'basic' }];
|
||||
|
||||
const [aiConfigTabSchema, setAIConfigTabSchema] =
|
||||
useState<PipelineConfigTab>();
|
||||
@@ -156,10 +163,10 @@ export default function PipelineFormComponent({
|
||||
.then((resp) => {
|
||||
onFinish();
|
||||
onNewPipelineCreated(resp.uuid);
|
||||
toast.success('创建成功 请编辑流水线详细参数');
|
||||
toast.success(t('pipelines.createSuccess'));
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('创建失败:' + err.message);
|
||||
toast.error(t('pipelines.createError') + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,10 +193,10 @@ export default function PipelineFormComponent({
|
||||
.updatePipeline(pipelineId || '', pipeline)
|
||||
.then(() => {
|
||||
onFinish();
|
||||
toast.success('保存成功');
|
||||
toast.success(t('pipelines.saveSuccess'));
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('保存失败:' + err.message);
|
||||
toast.error(t('pipelines.saveError') + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -206,10 +213,10 @@ export default function PipelineFormComponent({
|
||||
if (stage.name === 'runner') {
|
||||
return (
|
||||
<div key={stage.name} className="space-y-4 mb-6">
|
||||
<div className="text-lg font-medium">{stage.label.zh_CN}</div>
|
||||
<div className="text-lg font-medium">{i18nObj(stage.label)}</div>
|
||||
{stage.description && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{stage.description.zh_CN}
|
||||
{i18nObj(stage.description)}
|
||||
</div>
|
||||
)}
|
||||
<DynamicFormComponent
|
||||
@@ -241,9 +248,11 @@ export default function PipelineFormComponent({
|
||||
|
||||
return (
|
||||
<div key={stage.name} className="space-y-4 mb-6">
|
||||
<div className="text-lg font-medium">{stage.label.zh_CN}</div>
|
||||
<div className="text-lg font-medium">{i18nObj(stage.label)}</div>
|
||||
{stage.description && (
|
||||
<div className="text-sm text-gray-500">{stage.description.zh_CN}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{i18nObj(stage.description)}
|
||||
</div>
|
||||
)}
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
@@ -270,10 +279,10 @@ export default function PipelineFormComponent({
|
||||
.deletePipeline(pipelineId || '')
|
||||
.then(() => {
|
||||
onFinish();
|
||||
toast.success('删除成功');
|
||||
toast.success(t('common.deleteSuccess'));
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('删除失败:' + err.message);
|
||||
toast.error(t('common.deleteError') + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -285,17 +294,17 @@ export default function PipelineFormComponent({
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>删除确认</DialogTitle>
|
||||
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
你确定要删除这个流水线吗?已绑定此流水线的机器人将无法使用。
|
||||
{t('pipelines.deleteConfirmation')}
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDeleteConfirmModal(false)}
|
||||
>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -304,7 +313,7 @@ export default function PipelineFormComponent({
|
||||
setShowDeleteConfirmModal(false);
|
||||
}}
|
||||
>
|
||||
确认删除
|
||||
{t('common.confirmDelete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -337,7 +346,8 @@ export default function PipelineFormComponent({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
名称<span className="text-red-500">*</span>
|
||||
{t('common.name')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
@@ -353,7 +363,8 @@ export default function PipelineFormComponent({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
描述<span className="text-red-500">*</span>
|
||||
{t('common.description')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
@@ -408,7 +419,7 @@ export default function PipelineFormComponent({
|
||||
<div className="flex justify-end items-center gap-2">
|
||||
{isEditMode && isDefaultPipeline && (
|
||||
<span className="text-gray-500 text-[0.7rem]">
|
||||
默认流水线不可删除
|
||||
{t('pipelines.defaultPipelineCannotDelete')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -421,12 +432,12 @@ export default function PipelineFormComponent({
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
删除
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="cursor-pointer">
|
||||
{isEditMode ? '保存' : '提交'}
|
||||
{isEditMode ? t('common.save') : t('common.submit')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -434,7 +445,7 @@ export default function PipelineFormComponent({
|
||||
onClick={onFinish}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export default function PluginConfigPage() {
|
||||
const { t } = useTranslation();
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [isEditForm, setIsEditForm] = useState(false);
|
||||
const [pipelineList, setPipelineList] = useState<PipelineCardVO[]>([]);
|
||||
@@ -53,7 +55,9 @@ export default function PluginConfigPage() {
|
||||
);
|
||||
|
||||
const lastUpdatedTimeAgoText =
|
||||
lastUpdatedTimeAgo > 0 ? ` ${lastUpdatedTimeAgo} 天前` : '今天';
|
||||
lastUpdatedTimeAgo > 0
|
||||
? ` ${lastUpdatedTimeAgo} ${t('pipelines.daysAgo')}`
|
||||
: t('pipelines.today');
|
||||
|
||||
return new PipelineCardVO({
|
||||
lastUpdatedTimeAgo: lastUpdatedTimeAgoText,
|
||||
@@ -67,7 +71,7 @@ export default function PluginConfigPage() {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error('获取流水线列表失败:' + error.message);
|
||||
toast.error(t('pipelines.getPipelineListError') + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,7 +98,9 @@ export default function PluginConfigPage() {
|
||||
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 pb-4">
|
||||
<DialogTitle>
|
||||
{isEditForm ? '编辑流水线' : '创建流水线'}
|
||||
{isEditForm
|
||||
? t('pipelines.editPipeline')
|
||||
: t('pipelines.createPipeline')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6">
|
||||
|
||||
@@ -20,6 +20,7 @@ import { GithubIcon } from 'lucide-react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
enum PluginInstallStatus {
|
||||
WAIT_INPUT = 'wait_input',
|
||||
@@ -28,6 +29,7 @@ enum PluginInstallStatus {
|
||||
}
|
||||
|
||||
export default function PluginConfigPage() {
|
||||
const { t } = useTranslation();
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [sortModalOpen, setSortModalOpen] = useState(false);
|
||||
const [pluginInstallStatus, setPluginInstallStatus] =
|
||||
@@ -61,7 +63,7 @@ export default function PluginConfigPage() {
|
||||
} else {
|
||||
// success
|
||||
if (!alreadySuccess) {
|
||||
toast.success('插件安装成功');
|
||||
toast.success(t('plugins.installSuccess'));
|
||||
alreadySuccess = true;
|
||||
}
|
||||
setGithubURL('');
|
||||
@@ -85,10 +87,10 @@ export default function PluginConfigPage() {
|
||||
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
||||
<TabsList className="shadow-md py-5 bg-[#f0f0f0]">
|
||||
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
|
||||
已安装
|
||||
{t('plugins.installed')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="market" className="px-6 py-4 cursor-pointer">
|
||||
插件市场
|
||||
{t('plugins.marketplace')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -100,7 +102,7 @@ export default function PluginConfigPage() {
|
||||
setSortModalOpen(true);
|
||||
}}
|
||||
>
|
||||
编排
|
||||
{t('plugins.arrange')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -112,7 +114,7 @@ export default function PluginConfigPage() {
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
安装
|
||||
{t('plugins.install')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,14 +138,14 @@ export default function PluginConfigPage() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-4">
|
||||
<GithubIcon className="size-6" />
|
||||
<span>从 GitHub 安装插件</span>
|
||||
<span>{t('plugins.installFromGithub')}</span>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2">目前仅支持从 GitHub 安装</p>
|
||||
<p className="mb-2">{t('plugins.onlySupportGithub')}</p>
|
||||
<Input
|
||||
placeholder="请输入插件的Github链接"
|
||||
placeholder={t('plugins.enterGithubLink')}
|
||||
value={githubURL}
|
||||
onChange={(e) => setGithubURL(e.target.value)}
|
||||
className="mb-4"
|
||||
@@ -152,12 +154,12 @@ export default function PluginConfigPage() {
|
||||
)}
|
||||
{pluginInstallStatus === PluginInstallStatus.INSTALLING && (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2">正在安装插件...</p>
|
||||
<p className="mb-2">{t('plugins.installing')}</p>
|
||||
</div>
|
||||
)}
|
||||
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
||||
<div className="mt-4">
|
||||
<p className="mb-2">插件安装失败:</p>
|
||||
<p className="mb-2">{t('plugins.installFailed')}</p>
|
||||
<p className="mb-2 text-red-500">{installError}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -165,14 +167,16 @@ export default function PluginConfigPage() {
|
||||
{pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setModalOpen(false)}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleModalConfirm}>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
<Button onClick={handleModalConfirm}>确认</Button>
|
||||
</>
|
||||
)}
|
||||
{pluginInstallStatus === PluginInstallStatus.ERROR && (
|
||||
<Button variant="default" onClick={() => setModalOpen(false)}>
|
||||
关闭
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
export interface PluginInstalledComponentRef {
|
||||
refreshPluginList: () => void;
|
||||
@@ -20,6 +22,7 @@ export interface PluginInstalledComponentRef {
|
||||
// eslint-disable-next-line react/display-name
|
||||
const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [pluginList, setPluginList] = useState<PluginCardVO[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginCardVO | null>(
|
||||
@@ -41,7 +44,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
value.plugins.map((plugin) => {
|
||||
return new PluginCardVO({
|
||||
author: plugin.author,
|
||||
description: plugin.description.zh_CN,
|
||||
description: i18nObj(plugin.description),
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
@@ -77,14 +80,14 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
>
|
||||
<path d="M7 5C7 2.79086 8.79086 1 11 1C13.2091 1 15 2.79086 15 5H20C20.5523 5 21 5.44772 21 6V10.1707C21 10.4953 20.8424 10.7997 20.5774 10.9872C20.3123 11.1746 19.9728 11.2217 19.6668 11.1135C19.4595 11.0403 19.2355 11 19 11C17.8954 11 17 11.8954 17 13C17 14.1046 17.8954 15 19 15C19.2355 15 19.4595 14.9597 19.6668 14.8865C19.9728 14.7783 20.3123 14.8254 20.5774 15.0128C20.8424 15.2003 21 15.5047 21 15.8293V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H7ZM11 3C9.89543 3 9 3.89543 9 5C9 5.23554 9.0403 5.45952 9.11355 5.66675C9.22172 5.97282 9.17461 6.31235 8.98718 6.57739C8.79974 6.84243 8.49532 7 8.17071 7H5V19H19V17C16.7909 17 15 15.2091 15 13C15 10.7909 16.7909 9 19 9V7H13.8293C13.5047 7 13.2003 6.84243 13.0128 6.57739C12.8254 6.31235 12.7783 5.97282 12.8865 5.66675C12.9597 5.45952 13 5.23555 13 5C13 3.89543 12.1046 3 11 3Z"></path>
|
||||
</svg>
|
||||
<div className="text-lg mb-2">暂未安装任何插件</div>
|
||||
<div className="text-lg mb-2">{t('plugins.noPluginInstalled')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 pb-2">
|
||||
<DialogTitle>插件配置</DialogTitle>
|
||||
<DialogTitle>{t('plugins.pluginConfig')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6">
|
||||
{selectedPlugin && (
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
enum PluginRemoveStatus {
|
||||
WAIT_INPUT = 'WAIT_INPUT',
|
||||
@@ -179,7 +180,7 @@ export default function PluginForm({
|
||||
<div className="space-y-2">
|
||||
<div className="text-lg font-medium">{pluginInfo.name}</div>
|
||||
<div className="text-sm text-gray-500 pb-2">
|
||||
{pluginInfo.description.zh_CN}
|
||||
{i18nObj(pluginInfo.description)}
|
||||
</div>
|
||||
{pluginInfo.config_schema.length > 0 && (
|
||||
<DynamicFormComponent
|
||||
|
||||
@@ -5,6 +5,7 @@ import styles from '@/app/home/plugins/plugins.module.css';
|
||||
import { PluginMarketCardVO } from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO';
|
||||
import PluginMarketCardComponent from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent';
|
||||
import { spaceClient } from '@/app/infra/http/HttpClient';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Pagination,
|
||||
@@ -27,6 +28,7 @@ export default function PluginMarketComponent({
|
||||
}: {
|
||||
askInstallPlugin: (githubURL: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [marketPluginList, setMarketPluginList] = useState<
|
||||
PluginMarketCardVO[]
|
||||
>([]);
|
||||
@@ -105,7 +107,7 @@ export default function PluginMarketComponent({
|
||||
console.log('market plugins:', res);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('获取插件列表失败:', error);
|
||||
console.error(t('plugins.getPluginListError'), error);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
@@ -131,7 +133,7 @@ export default function PluginMarketComponent({
|
||||
width: '300px',
|
||||
}}
|
||||
value={searchKeyword}
|
||||
placeholder="搜索插件"
|
||||
placeholder={t('plugins.searchPlugin')}
|
||||
onChange={(e) => onInputSearchKeyword(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -140,12 +142,16 @@ export default function PluginMarketComponent({
|
||||
onValueChange={handleSortChange}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] ml-2 cursor-pointer">
|
||||
<SelectValue placeholder="排序方式" />
|
||||
<SelectValue placeholder={t('plugins.sortBy')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="stars,DESC">最多星标</SelectItem>
|
||||
<SelectItem value="created_at,DESC">最近新增</SelectItem>
|
||||
<SelectItem value="pushed_at,DESC">最近更新</SelectItem>
|
||||
<SelectItem value="stars,DESC">{t('plugins.mostStars')}</SelectItem>
|
||||
<SelectItem value="created_at,DESC">
|
||||
{t('plugins.recentlyAdded')}
|
||||
</SelectItem>
|
||||
<SelectItem value="pushed_at,DESC">
|
||||
{t('plugins.recentlyUpdated')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -221,11 +227,11 @@ export default function PluginMarketComponent({
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{/* 加载中... */}
|
||||
{t('plugins.loading')}
|
||||
</div>
|
||||
) : marketPluginList.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{/* 没有找到匹配的插件 */}
|
||||
{t('plugins.noMatchingPlugins')}
|
||||
</div>
|
||||
) : (
|
||||
marketPluginList.map((vo, index) => (
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
interface PluginSortDialogProps {
|
||||
open: boolean;
|
||||
@@ -75,6 +77,7 @@ export default function PluginSortDialog({
|
||||
onOpenChange,
|
||||
onSortComplete,
|
||||
}: PluginSortDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [sortedPlugins, setSortedPlugins] = useState<PluginCardVO[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -84,7 +87,7 @@ export default function PluginSortDialog({
|
||||
value.plugins.map((plugin) => {
|
||||
return new PluginCardVO({
|
||||
author: plugin.author,
|
||||
description: plugin.description.zh_CN,
|
||||
description: i18nObj(plugin.description),
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
@@ -146,12 +149,12 @@ export default function PluginSortDialog({
|
||||
httpClient
|
||||
.reorderPlugins(reorderElements)
|
||||
.then(() => {
|
||||
toast.success('插件排序成功');
|
||||
toast.success(t('plugins.pluginSortSuccess'));
|
||||
onSortComplete();
|
||||
onOpenChange(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('排序失败:' + err.message);
|
||||
toast.error(t('plugins.pluginSortError') + err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
@@ -162,11 +165,11 @@ export default function PluginSortDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 pb-0">
|
||||
<DialogTitle>插件排序</DialogTitle>
|
||||
<DialogTitle>{t('plugins.pluginSort')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-0">
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
插件顺序会影响同一事件内的处理顺序,请拖动插件卡片排序
|
||||
{t('plugins.pluginSortDescription')}
|
||||
</p>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
@@ -194,10 +197,10 @@ export default function PluginSortDialog({
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isLoading}>
|
||||
{isLoading ? '保存中...' : '保存'}
|
||||
{isLoading ? t('common.saving') : t('common.save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import './global.css';
|
||||
import type { Metadata } from 'next';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import I18nProvider from '@/i18n/I18nProvider';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'LangBot',
|
||||
@@ -13,10 +14,12 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html>
|
||||
<body className={``}>
|
||||
{children}
|
||||
<Toaster />
|
||||
<I18nProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,13 @@ import {
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
@@ -19,23 +26,28 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Mail, Lock } from 'lucide-react';
|
||||
import { Mail, Lock, Globe } from 'lucide-react';
|
||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().email('请输入有效的邮箱地址'),
|
||||
password: z.string().min(1, '请输入密码'),
|
||||
});
|
||||
const formSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
email: z.string().email(t('common.invalidEmail')),
|
||||
password: z.string().min(1, t('common.emptyPassword')),
|
||||
});
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [currentLanguage, setCurrentLanguage] = useState<string>(i18n.language);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
|
||||
resolver: zodResolver(formSchema(t)),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
@@ -43,10 +55,34 @@ export default function Login() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
judgeLanguage();
|
||||
getIsInitialized();
|
||||
checkIfAlreadyLoggedIn();
|
||||
}, []);
|
||||
|
||||
const judgeLanguage = () => {
|
||||
// here's for user have never set the language
|
||||
// judge the language by the browser
|
||||
const language = navigator.language;
|
||||
if (language) {
|
||||
let lang = 'zh-Hans';
|
||||
if (language === 'zh-CN') {
|
||||
lang = 'zh-Hans';
|
||||
} else {
|
||||
lang = 'en-US';
|
||||
}
|
||||
i18n.changeLanguage(lang);
|
||||
setCurrentLanguage(lang);
|
||||
localStorage.setItem('langbot_language', lang);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChange = (value: string) => {
|
||||
i18n.changeLanguage(value);
|
||||
setCurrentLanguage(value);
|
||||
localStorage.setItem('langbot_language', value);
|
||||
};
|
||||
|
||||
function getIsInitialized() {
|
||||
httpClient
|
||||
.checkIfInited()
|
||||
@@ -73,7 +109,7 @@ export default function Login() {
|
||||
console.log('error at checkIfAlreadyLoggedIn: ', err);
|
||||
});
|
||||
}
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
function onSubmit(values: z.infer<ReturnType<typeof formSchema>>) {
|
||||
handleLogin(values.email, values.password);
|
||||
}
|
||||
|
||||
@@ -85,28 +121,45 @@ export default function Login() {
|
||||
localStorage.setItem('userEmail', username);
|
||||
console.log('login success: ', res);
|
||||
router.push('/home');
|
||||
toast.success('登录成功');
|
||||
toast.success(t('common.loginSuccess'));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('login error: ', err);
|
||||
|
||||
toast.error('登录失败,请检查邮箱和密码是否正确');
|
||||
toast.error(t('common.loginFailed'));
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<Card className="w-[360px]">
|
||||
<Card className="w-[375px]">
|
||||
<CardHeader>
|
||||
<div className="flex justify-end mb-6">
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onValueChange={handleLanguageChange}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<Globe className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder={t('common.language')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="zh-Hans">简体中文</SelectItem>
|
||||
<SelectItem value="en-US">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<img
|
||||
src={langbotIcon.src}
|
||||
alt="LangBot"
|
||||
className="w-16 h-16 mb-4 mx-auto"
|
||||
/>
|
||||
<CardTitle className="text-2xl text-center">
|
||||
欢迎回到 LangBot 👋
|
||||
{t('common.welcome')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-center">登录以继续</CardDescription>
|
||||
<CardDescription className="text-center">
|
||||
{t('common.continueToLogin')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
@@ -116,12 +169,12 @@ export default function Login() {
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>邮箱</FormLabel>
|
||||
<FormLabel>{t('common.email')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="输入邮箱地址"
|
||||
placeholder={t('common.enterEmail')}
|
||||
className="pl-10"
|
||||
{...field}
|
||||
/>
|
||||
@@ -137,13 +190,13 @@ export default function Login() {
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormLabel>{t('common.password')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="输入密码"
|
||||
placeholder={t('common.enterPassword')}
|
||||
className="pl-10"
|
||||
{...field}
|
||||
/>
|
||||
@@ -155,7 +208,7 @@ export default function Login() {
|
||||
/>
|
||||
|
||||
<Button type="submit" className="w-full mt-4 cursor-pointer">
|
||||
登录
|
||||
{t('common.login')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
20
web/src/i18n/I18nProvider.tsx
Normal file
20
web/src/i18n/I18nProvider.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import '@/i18n';
|
||||
import { I18nText } from '@/app/infra/entities/api';
|
||||
|
||||
interface I18nProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function I18nProvider({ children }: I18nProviderProps) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
export function i18nObj(i18nText: I18nText): string {
|
||||
const language = localStorage.getItem('langbot_language');
|
||||
if ((language === 'zh-Hans' && i18nText.zh_CN) || !i18nText.en_US) {
|
||||
return i18nText.zh_CN;
|
||||
}
|
||||
return i18nText.en_US;
|
||||
}
|
||||
34
web/src/i18n/index.ts
Normal file
34
web/src/i18n/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import enUS from './locales/en-US';
|
||||
import zhHans from './locales/zh-Hans';
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
'en-US': {
|
||||
translation: enUS,
|
||||
},
|
||||
'zh-Hans': {
|
||||
translation: zhHans,
|
||||
},
|
||||
},
|
||||
fallbackLng: 'zh-Hans',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes values
|
||||
},
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
lookupLocalStorage: 'langbot_language',
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
174
web/src/i18n/locales/en-US.ts
Normal file
174
web/src/i18n/locales/en-US.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
const enUS = {
|
||||
common: {
|
||||
login: 'Login',
|
||||
logout: 'Logout',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
welcome: 'Welcome back to LangBot 👋',
|
||||
continueToLogin: 'Login to continue',
|
||||
loginSuccess: 'Login successful',
|
||||
loginFailed: 'Login failed, please check your email and password',
|
||||
enterEmail: 'Enter email address',
|
||||
enterPassword: 'Enter password',
|
||||
invalidEmail: 'Please enter a valid email address',
|
||||
emptyPassword: 'Please enter your password',
|
||||
language: 'Language',
|
||||
helpDocs: 'Get Help',
|
||||
create: 'Create',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
add: 'Add',
|
||||
select: 'Select',
|
||||
cancel: 'Cancel',
|
||||
submit: 'Submit',
|
||||
error: 'Error',
|
||||
success: 'Success',
|
||||
save: 'Save',
|
||||
saving: 'Saving...',
|
||||
confirm: 'Confirm',
|
||||
confirmDelete: 'Confirm Delete',
|
||||
deleteConfirmation: 'Are you sure you want to delete this?',
|
||||
selectOption: 'Select an option',
|
||||
required: 'Required',
|
||||
enable: 'Enable',
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
close: 'Close',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Delete failed: ',
|
||||
addRound: 'Add Round',
|
||||
},
|
||||
models: {
|
||||
title: 'Models',
|
||||
description: 'Configure and manage models that can be used in pipelines',
|
||||
createModel: 'Create Model',
|
||||
editModel: 'Edit Model',
|
||||
getModelListError: 'Failed to get model list: ',
|
||||
modelName: 'Model Name',
|
||||
modelProvider: 'Model Provider',
|
||||
modelBaseURL: 'Base URL',
|
||||
modelAbilities: 'Model Abilities',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Save failed: ',
|
||||
createSuccess: 'Created successfully',
|
||||
createError: 'Creation failed: ',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Delete failed: ',
|
||||
deleteConfirmation: 'Are you sure you want to delete this model?',
|
||||
modelNameRequired: 'Model name cannot be empty',
|
||||
modelProviderRequired: 'Model provider cannot be empty',
|
||||
requestURLRequired: 'Request URL cannot be empty',
|
||||
apiKeyRequired: 'API Key cannot be empty',
|
||||
keyNameRequired: 'Key name cannot be empty',
|
||||
mustBeValidNumber: 'Must be a valid number',
|
||||
mustBeTrueOrFalse: 'Must be true or false',
|
||||
requestURL: 'Request URL',
|
||||
apiKey: 'API Key',
|
||||
abilities: 'Abilities',
|
||||
selectModelAbilities: 'Select model abilities',
|
||||
visionAbility: 'Vision Ability',
|
||||
functionCallAbility: 'Function Call',
|
||||
extraParameters: 'Extra Parameters',
|
||||
addParameter: 'Add Parameter',
|
||||
keyName: 'Key Name',
|
||||
type: 'Type',
|
||||
value: 'Value',
|
||||
string: 'String',
|
||||
number: 'Number',
|
||||
boolean: 'Boolean',
|
||||
extraParametersDescription:
|
||||
'Will be attached to the request body, such as max_tokens, temperature, top_p, etc.',
|
||||
selectModelProvider: 'Select Model Provider',
|
||||
modelProviderDescription:
|
||||
'Please fill in the model name provided by the supplier',
|
||||
selectModel: 'Select Model',
|
||||
},
|
||||
bots: {
|
||||
title: 'Bots',
|
||||
description:
|
||||
'Create and manage bots, which are the entry points for LangBot to connect with various platforms',
|
||||
createBot: 'Create Bot',
|
||||
editBot: 'Edit Bot',
|
||||
getBotListError: 'Failed to get bot list: ',
|
||||
botName: 'Bot Name',
|
||||
botDescription: 'Bot Description',
|
||||
botNameRequired: 'Bot name cannot be empty',
|
||||
botDescriptionRequired: 'Bot description cannot be empty',
|
||||
adapterRequired: 'Adapter cannot be empty',
|
||||
defaultDescription: 'A bot',
|
||||
getBotConfigError: 'Failed to get bot configuration: ',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Save failed: ',
|
||||
createSuccess:
|
||||
'Created successfully. Please enable or modify the bound pipeline',
|
||||
createError: 'Creation failed: ',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Delete failed: ',
|
||||
deleteConfirmation: 'Are you sure you want to delete this bot?',
|
||||
platformAdapter: 'Platform/Adapter Selection',
|
||||
selectAdapter: 'Select Adapter',
|
||||
adapterConfig: 'Adapter Configuration',
|
||||
bindPipeline: 'Bind Pipeline',
|
||||
selectPipeline: 'Select Pipeline',
|
||||
},
|
||||
plugins: {
|
||||
title: 'Plugins',
|
||||
description:
|
||||
'Install and configure plugins to extend LangBot functionality',
|
||||
createPlugin: 'Create Plugin',
|
||||
editPlugin: 'Edit Plugin',
|
||||
installed: 'Installed',
|
||||
marketplace: 'Marketplace',
|
||||
arrange: 'Sort Plugins',
|
||||
install: 'Install',
|
||||
installFromGithub: 'Install Plugin from GitHub',
|
||||
onlySupportGithub: 'Currently only supports installation from GitHub',
|
||||
enterGithubLink: 'Enter GitHub link of the plugin',
|
||||
installing: 'Installing plugin...',
|
||||
installSuccess: 'Plugin installed successfully',
|
||||
installFailed: 'Plugin installation failed:',
|
||||
searchPlugin: 'Search plugins',
|
||||
sortBy: 'Sort by',
|
||||
mostStars: 'Most stars',
|
||||
recentlyAdded: 'Recently added',
|
||||
recentlyUpdated: 'Recently updated',
|
||||
noMatchingPlugins: 'No matching plugins found',
|
||||
loading: 'Loading...',
|
||||
getPluginListError: 'Failed to get plugin list:',
|
||||
noPluginInstalled: 'No plugins installed',
|
||||
pluginConfig: 'Plugin Configuration',
|
||||
pluginSort: 'Plugin Sort',
|
||||
pluginSortDescription:
|
||||
'Plugin order affects the processing order within the same event, please drag the plugin card to sort',
|
||||
pluginSortSuccess: 'Plugin sort successful',
|
||||
pluginSortError: 'Plugin sort failed: ',
|
||||
},
|
||||
pipelines: {
|
||||
title: 'Pipelines',
|
||||
description:
|
||||
'Pipelines define the processing flow for message events, used to bind to bots',
|
||||
createPipeline: 'Create Pipeline',
|
||||
editPipeline: 'Edit Pipeline',
|
||||
getPipelineListError: 'Failed to get pipeline list: ',
|
||||
daysAgo: 'days ago',
|
||||
today: 'Today',
|
||||
updateTime: 'Updated ',
|
||||
defaultBadge: 'Default',
|
||||
basicInfo: 'Basic',
|
||||
aiCapabilities: 'AI',
|
||||
triggerConditions: 'Trigger',
|
||||
safetyControls: 'Safety',
|
||||
outputProcessing: 'Output',
|
||||
nameRequired: 'Name cannot be empty',
|
||||
descriptionRequired: 'Description cannot be empty',
|
||||
createSuccess: 'Created successfully. Please edit pipeline parameters',
|
||||
createError: 'Creation failed: ',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Save failed: ',
|
||||
deleteConfirmation:
|
||||
'Are you sure you want to delete this pipeline? Bots bound to this pipeline will not work.',
|
||||
defaultPipelineCannotDelete: 'Default pipeline cannot be deleted',
|
||||
},
|
||||
};
|
||||
|
||||
export default enUS;
|
||||
169
web/src/i18n/locales/zh-Hans.ts
Normal file
169
web/src/i18n/locales/zh-Hans.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
const zhHans = {
|
||||
common: {
|
||||
login: '登录',
|
||||
logout: '退出登录',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
welcome: '欢迎回到 LangBot 👋',
|
||||
continueToLogin: '登录以继续',
|
||||
loginSuccess: '登录成功',
|
||||
loginFailed: '登录失败,请检查邮箱和密码是否正确',
|
||||
enterEmail: '输入邮箱地址',
|
||||
enterPassword: '输入密码',
|
||||
invalidEmail: '请输入有效的邮箱地址',
|
||||
emptyPassword: '请输入密码',
|
||||
language: '语言',
|
||||
helpDocs: '帮助文档',
|
||||
create: '创建',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
add: '添加',
|
||||
select: '请选择',
|
||||
cancel: '取消',
|
||||
submit: '提交',
|
||||
error: '错误',
|
||||
success: '成功',
|
||||
save: '保存',
|
||||
saving: '保存中...',
|
||||
confirm: '确认',
|
||||
confirmDelete: '删除确认',
|
||||
deleteConfirmation: '你确定要删除这个吗?',
|
||||
selectOption: '选择一个选项',
|
||||
required: '必填',
|
||||
enable: '是否启用',
|
||||
name: '名称',
|
||||
description: '描述',
|
||||
close: '关闭',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteError: '删除失败:',
|
||||
addRound: '添加回合',
|
||||
},
|
||||
models: {
|
||||
title: '模型配置',
|
||||
description: '配置和管理可在流水线中使用的模型',
|
||||
createModel: '创建模型',
|
||||
editModel: '编辑模型',
|
||||
getModelListError: '获取模型列表失败:',
|
||||
modelName: '模型名称',
|
||||
modelProvider: '模型提供商',
|
||||
modelBaseURL: '基础 URL',
|
||||
modelAbilities: '模型能力',
|
||||
saveSuccess: '保存成功',
|
||||
saveError: '保存失败:',
|
||||
createSuccess: '创建成功',
|
||||
createError: '创建失败:',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteError: '删除失败:',
|
||||
deleteConfirmation: '你确定要删除这个模型吗?',
|
||||
modelNameRequired: '模型名称不能为空',
|
||||
modelProviderRequired: '模型供应商不能为空',
|
||||
requestURLRequired: '请求URL不能为空',
|
||||
apiKeyRequired: 'API Key不能为空',
|
||||
keyNameRequired: '键名不能为空',
|
||||
mustBeValidNumber: '必须是有效的数字',
|
||||
mustBeTrueOrFalse: '必须是 true 或 false',
|
||||
requestURL: '请求URL',
|
||||
apiKey: 'API Key',
|
||||
abilities: '能力',
|
||||
selectModelAbilities: '选择模型能力',
|
||||
visionAbility: '视觉能力',
|
||||
functionCallAbility: '函数调用',
|
||||
extraParameters: '额外参数',
|
||||
addParameter: '添加参数',
|
||||
keyName: '键名',
|
||||
type: '类型',
|
||||
value: '值',
|
||||
string: '字符串',
|
||||
number: '数字',
|
||||
boolean: '布尔值',
|
||||
extraParametersDescription:
|
||||
'将在请求时附加到请求体中,如 max_tokens, temperature, top_p 等',
|
||||
selectModelProvider: '选择模型供应商',
|
||||
modelProviderDescription: '请填写供应商向您提供的模型名称',
|
||||
selectModel: '请选择模型',
|
||||
},
|
||||
bots: {
|
||||
title: '机器人',
|
||||
description: '创建和管理机器人,这是 LangBot 与各个平台连接的入口',
|
||||
createBot: '创建机器人',
|
||||
editBot: '编辑机器人',
|
||||
getBotListError: '获取机器人列表失败:',
|
||||
botName: '机器人名称',
|
||||
botDescription: '机器人描述',
|
||||
botNameRequired: '机器人名称不能为空',
|
||||
botDescriptionRequired: '机器人描述不能为空',
|
||||
adapterRequired: '适配器不能为空',
|
||||
defaultDescription: '一个机器人',
|
||||
getBotConfigError: '获取机器人配置失败:',
|
||||
saveSuccess: '保存成功',
|
||||
saveError: '保存失败:',
|
||||
createSuccess: '创建成功 请启用或修改绑定流水线',
|
||||
createError: '创建失败:',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteError: '删除失败:',
|
||||
deleteConfirmation: '你确定要删除这个机器人吗?',
|
||||
platformAdapter: '平台/适配器选择',
|
||||
selectAdapter: '选择适配器',
|
||||
adapterConfig: '适配器配置',
|
||||
bindPipeline: '绑定流水线',
|
||||
selectPipeline: '选择流水线',
|
||||
},
|
||||
plugins: {
|
||||
title: '插件管理',
|
||||
description: '安装和配置用于扩展 LangBot 功能的插件',
|
||||
createPlugin: '创建插件',
|
||||
editPlugin: '编辑插件',
|
||||
installed: '已安装',
|
||||
marketplace: '插件市场',
|
||||
arrange: '编排',
|
||||
install: '安装',
|
||||
installFromGithub: '从 GitHub 安装插件',
|
||||
onlySupportGithub: '目前仅支持从 GitHub 安装',
|
||||
enterGithubLink: '请输入插件的Github链接',
|
||||
installing: '正在安装插件...',
|
||||
installSuccess: '插件安装成功',
|
||||
installFailed: '插件安装失败:',
|
||||
searchPlugin: '搜索插件',
|
||||
sortBy: '排序方式',
|
||||
mostStars: '最多星标',
|
||||
recentlyAdded: '最近新增',
|
||||
recentlyUpdated: '最近更新',
|
||||
noMatchingPlugins: '没有找到匹配的插件',
|
||||
loading: '加载中...',
|
||||
getPluginListError: '获取插件列表失败:',
|
||||
pluginConfig: '插件配置',
|
||||
noPluginInstalled: '暂未安装任何插件',
|
||||
pluginSort: '插件排序',
|
||||
pluginSortDescription:
|
||||
'插件顺序会影响同一事件内的处理顺序,请拖动插件卡片排序',
|
||||
pluginSortSuccess: '插件排序成功',
|
||||
pluginSortError: '插件排序失败:',
|
||||
},
|
||||
pipelines: {
|
||||
title: '流水线',
|
||||
description: '流水线定义了对消息事件的处理流程,用于绑定到机器人',
|
||||
createPipeline: '创建流水线',
|
||||
editPipeline: '编辑流水线',
|
||||
getPipelineListError: '获取流水线列表失败:',
|
||||
daysAgo: '天前',
|
||||
today: '今天',
|
||||
updateTime: '更新于',
|
||||
defaultBadge: '默认',
|
||||
basicInfo: '基础信息',
|
||||
aiCapabilities: 'AI 能力',
|
||||
triggerConditions: '触发条件',
|
||||
safetyControls: '安全控制',
|
||||
outputProcessing: '输出处理',
|
||||
nameRequired: '名称不能为空',
|
||||
descriptionRequired: '描述不能为空',
|
||||
createSuccess: '创建成功 请编辑流水线详细参数',
|
||||
createError: '创建失败:',
|
||||
saveSuccess: '保存成功',
|
||||
saveError: '保存失败:',
|
||||
deleteConfirmation:
|
||||
'你确定要删除这个流水线吗?已绑定此流水线的机器人将无法使用。',
|
||||
defaultPipelineCannotDelete: '默认流水线不可删除',
|
||||
},
|
||||
};
|
||||
|
||||
export default zhHans;
|
||||
Reference in New Issue
Block a user