From 373946ef3fd9427ff1788325de590eac75465db3 Mon Sep 17 00:00:00 2001 From: balibabu <cike8899@users.noreply.github.com> Date: Sun, 7 Apr 2024 17:41:29 +0800 Subject: [PATCH] change language #245 (#246) ### What problem does this PR solve? change language Issue link: #245 - [x] New Feature (non-breaking change which adds functionality) --- web/src/app.tsx | 32 +- .../components/chunk-method-modal/index.tsx | 39 +- web/src/components/max-token-number.tsx | 12 +- web/src/components/rename-modal/index.tsx | 8 +- web/src/global.ts | 1 - web/src/hooks/commonHooks.tsx | 4 + web/src/hooks/logicHooks.ts | 14 + web/src/hooks/userSettingHook.ts | 2 +- .../components/right-toolbar/index.tsx | 43 +- web/src/locales/config.ts | 4 +- web/src/locales/en.json | 122 ----- web/src/locales/en.ts | 433 ++++++++++++++++++ web/src/locales/zh.json | 23 - web/src/locales/zh.ts | 417 +++++++++++++++++ .../components/chunk-creating-modal/index.tsx | 16 +- .../components/chunk-toolbar/index.tsx | 21 +- .../components/knowledge-chunk/index.tsx | 4 +- .../components/knowledge-chunk/model.ts | 3 +- .../knowledge-upload-file/index.tsx | 17 +- .../components/knowledge-file/index.tsx | 4 +- .../components/knowledge-file/model.ts | 17 +- .../parsing-status-cell/index.tsx | 8 +- .../knowledge-setting/category-panel.tsx | 28 +- .../knowledge-setting/configuration.tsx | 6 +- .../components/knowledge-setting/model.ts | 5 +- web/src/pages/add-knowledge/index.tsx | 3 +- .../assistant-setting.tsx | 35 +- .../chat/chat-configuration-modal/index.tsx | 38 +- .../model-setting.tsx | 54 +-- .../prompt-engine.tsx | 32 +- web/src/pages/chat/chat-container/index.tsx | 6 +- web/src/pages/chat/index.tsx | 11 +- web/src/pages/chat/model.ts | 9 +- web/src/pages/login/index.tsx | 2 +- web/src/pages/login/model.ts | 7 +- web/src/pages/login/right-panel.tsx | 9 +- .../components/setting-title/index.tsx | 5 +- web/src/pages/user-setting/model.ts | 10 +- .../setting-model/api-key-modal/index.tsx | 14 +- .../user-setting/setting-model/index.tsx | 16 +- .../system-model-setting-modal/index.tsx | 27 +- .../user-setting/setting-password/index.tsx | 26 +- .../user-setting/setting-profile/index.tsx | 68 +-- .../pages/user-setting/setting-team/index.tsx | 8 +- web/src/pages/user-setting/sidebar/index.tsx | 43 +- web/src/utils/authorizationUtil.ts | 6 + web/src/utils/request.ts | 39 +- 47 files changed, 1297 insertions(+), 454 deletions(-) delete mode 100644 web/src/global.ts delete mode 100644 web/src/locales/en.json create mode 100644 web/src/locales/en.ts delete mode 100644 web/src/locales/zh.json create mode 100644 web/src/locales/zh.ts diff --git a/web/src/app.tsx b/web/src/app.tsx index 746b649..e33201a 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -1,7 +1,26 @@ -import { App, ConfigProvider } from 'antd'; -import { ReactNode } from 'react'; +import i18next from '@/locales/config'; +import { App, ConfigProvider, ConfigProviderProps } from 'antd'; +import enUS from 'antd/locale/en_US'; +import zhCN from 'antd/locale/zh_CN'; +import React, { ReactNode, useEffect, useState } from 'react'; +import storage from './utils/authorizationUtil'; + +type Locale = ConfigProviderProps['locale']; + +const RootProvider = ({ children }: React.PropsWithChildren) => { + const getLocale = (lng: string) => (lng === 'zh' ? zhCN : enUS); + + const [locale, setLocal] = useState<Locale>(getLocale(storage.getLanguage())); + + i18next.on('languageChanged', function (lng: string) { + storage.setLanguage(lng); + setLocal(getLocale(lng)); + }); + + useEffect(() => { + i18next.changeLanguage(storage.getLanguage()); + }, [locale]); -export function rootContainer(container: ReactNode) { return ( <ConfigProvider theme={{ @@ -9,8 +28,13 @@ export function rootContainer(container: ReactNode) { fontFamily: 'Inter', }, }} + locale={locale} > - <App> {container}</App> + <App> {children}</App> </ConfigProvider> ); +}; + +export function rootContainer(container: ReactNode) { + return <RootProvider>{container}</RootProvider>; } diff --git a/web/src/components/chunk-method-modal/index.tsx b/web/src/components/chunk-method-modal/index.tsx index f4ecbd9..7f592b5 100644 --- a/web/src/components/chunk-method-modal/index.tsx +++ b/web/src/components/chunk-method-modal/index.tsx @@ -134,12 +134,8 @@ const ChunkMethodModal: React.FC<IProps> = ({ {showPages && ( <> <Space> - <p>Page Ranges:</p> - <Tooltip - title={ - 'page ranges: Define the page ranges that need to be parsed. The pages that not included in these ranges will be ignored.' - } - > + <p>{t('pageRanges')}:</p> + <Tooltip title={t('pageRangesTip')}> <QuestionCircleOutlined className={styles.questionIcon} ></QuestionCircleOutlined> @@ -163,7 +159,7 @@ const ChunkMethodModal: React.FC<IProps> = ({ rules={[ { required: true, - message: 'Missing start page number', + message: t('fromMessage'), }, ({ getFieldValue }) => ({ validator(_, value) { @@ -175,16 +171,14 @@ const ChunkMethodModal: React.FC<IProps> = ({ return Promise.resolve(); } return Promise.reject( - new Error( - 'The current value must be greater than the previous to!', - ), + new Error(t('greaterThanPrevious')), ); }, }), ]} > <InputNumber - placeholder="from" + placeholder={t('fromPlaceholder')} min={0} precision={0} className={styles.pageInputNumber} @@ -197,7 +191,7 @@ const ChunkMethodModal: React.FC<IProps> = ({ rules={[ { required: true, - message: 'Missing end page number(excluded)', + message: t('toMessage'), }, ({ getFieldValue }) => ({ validator(_, value) { @@ -208,16 +202,14 @@ const ChunkMethodModal: React.FC<IProps> = ({ return Promise.resolve(); } return Promise.reject( - new Error( - 'The current value must be greater than to!', - ), + new Error(t('greaterThan')), ); }, }), ]} > <InputNumber - placeholder="to" + placeholder={t('toPlaceholder')} min={0} precision={0} className={styles.pageInputNumber} @@ -235,7 +227,7 @@ const ChunkMethodModal: React.FC<IProps> = ({ block icon={<PlusOutlined />} > - Add page + {t('addPage')} </Button> </Form.Item> </> @@ -246,12 +238,10 @@ const ChunkMethodModal: React.FC<IProps> = ({ {showOne && ( <Form.Item name={['parser_config', 'layout_recognize']} - label="Layout recognize" + label={t('layoutRecognize')} initialValue={true} valuePropName="checked" - tooltip={ - 'Use visual models for layout analysis to better identify document structure, find where the titles, text blocks, images, and tables are. Without this feature, only the plain text of the PDF can be obtained.' - } + tooltip={t('layoutRecognizeTip')} > <Switch /> </Form.Item> @@ -265,14 +255,13 @@ const ChunkMethodModal: React.FC<IProps> = ({ getFieldValue(['parser_config', 'layout_recognize']) && ( <Form.Item name={['parser_config', 'task_page_size']} - label="Task page size" - tooltip={`If using layout recognize, the PDF file will be split into groups of successive. Layout analysis will be performed parallelly between groups to increase the processing speed. - The 'Task page size' determines the size of groups. The larger the page size is, the lower the chance of splitting continuous text between pages into different chunks.`} + label={t('taskPageSize')} + tooltip={t('taskPageSizeTip')} initialValue={12} rules={[ { required: true, - message: 'Please input your task page size!', + message: t('taskPageSizeMessage'), }, ]} > diff --git a/web/src/components/max-token-number.tsx b/web/src/components/max-token-number.tsx index 43618be..9913b03 100644 --- a/web/src/components/max-token-number.tsx +++ b/web/src/components/max-token-number.tsx @@ -1,18 +1,18 @@ +import { useTranslate } from '@/hooks/commonHooks'; import { Flex, Form, InputNumber, Slider } from 'antd'; const MaxTokenNumber = () => { + const { t } = useTranslate('knowledgeConfiguration'); + return ( - <Form.Item - label="Chunk token number" - tooltip="It determine the token number of a chunk approximately." - > + <Form.Item label={t('chunkTokenNumber')} tooltip={t('chunkTokenNumberTip')}> <Flex gap={20} align="center"> <Flex flex={1}> <Form.Item name={['parser_config', 'chunk_token_num']} noStyle initialValue={128} - rules={[{ required: true, message: 'Province is required' }]} + rules={[{ required: true, message: t('chunkTokenNumberMessage') }]} > <Slider max={2048} style={{ width: '100%' }} /> </Form.Item> @@ -20,7 +20,7 @@ const MaxTokenNumber = () => { <Form.Item name={['parser_config', 'chunk_token_num']} noStyle - rules={[{ required: true, message: 'Street is required' }]} + rules={[{ required: true, message: t('chunkTokenNumberMessage') }]} > <InputNumber max={2048} min={0} /> </Form.Item> diff --git a/web/src/components/rename-modal/index.tsx b/web/src/components/rename-modal/index.tsx index 9802ff8..afe5b7f 100644 --- a/web/src/components/rename-modal/index.tsx +++ b/web/src/components/rename-modal/index.tsx @@ -1,3 +1,4 @@ +import { useTranslate } from '@/hooks/commonHooks'; import { Form, Input, Modal } from 'antd'; import { useEffect } from 'react'; import { IModalManagerChildrenProps } from '../modal-manager'; @@ -17,6 +18,7 @@ const RenameModal = ({ onOk, }: IProps) => { const [form] = Form.useForm(); + const { t } = useTranslate('common'); type FieldType = { name?: string; @@ -48,7 +50,7 @@ const RenameModal = ({ return ( <Modal - title="Rename" + title={t('rename')} open={visible} onOk={handleOk} onCancel={handleCancel} @@ -66,9 +68,9 @@ const RenameModal = ({ form={form} > <Form.Item<FieldType> - label="Name" + label={t('name')} name="name" - rules={[{ required: true, message: 'Please input name!' }]} + rules={[{ required: true, message: t('namePlaceholder') }]} > <Input /> </Form.Item> diff --git a/web/src/global.ts b/web/src/global.ts deleted file mode 100644 index d025e4a..0000000 --- a/web/src/global.ts +++ /dev/null @@ -1 +0,0 @@ -import '@/locales/config'; diff --git a/web/src/hooks/commonHooks.tsx b/web/src/hooks/commonHooks.tsx index 4125cd7..03a1650 100644 --- a/web/src/hooks/commonHooks.tsx +++ b/web/src/hooks/commonHooks.tsx @@ -119,3 +119,7 @@ export const useShowDeleteConfirm = () => { export const useTranslate = (keyPrefix: string) => { return useTranslation('translation', { keyPrefix }); }; + +export const useCommonTranslation = () => { + return useTranslation('translation', { keyPrefix: 'common' }); +}; diff --git a/web/src/hooks/logicHooks.ts b/web/src/hooks/logicHooks.ts index 42dea0f..23833db 100644 --- a/web/src/hooks/logicHooks.ts +++ b/web/src/hooks/logicHooks.ts @@ -1,9 +1,11 @@ import { IKnowledgeFile } from '@/interfaces/database/knowledge'; import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; import { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useSetModalState } from './commonHooks'; import { useSetDocumentParser } from './documentHooks'; import { useOneNamespaceEffectsLoading } from './storeHooks'; +import { useSaveSetting } from './userSettingHook'; export const useChangeDocumentParser = (documentId: string) => { const setDocumentParser = useSetDocumentParser(); @@ -45,3 +47,15 @@ export const useSetSelectedRecord = <T = IKnowledgeFile>() => { return { currentRecord, setRecord }; }; + +export const useChangeLanguage = () => { + const { i18n } = useTranslation(); + const saveSetting = useSaveSetting(); + + const changeLanguage = (lng: string) => { + i18n.changeLanguage(lng === 'Chinese' ? 'zh' : 'en'); + saveSetting({ language: lng }); + }; + + return changeLanguage; +}; diff --git a/web/src/hooks/userSettingHook.ts b/web/src/hooks/userSettingHook.ts index c6d5065..79f1381 100644 --- a/web/src/hooks/userSettingHook.ts +++ b/web/src/hooks/userSettingHook.ts @@ -84,7 +84,7 @@ export const useSaveSetting = () => { const dispatch = useDispatch(); const saveSetting = useCallback( - (userInfo: { new_password: string } | IUserInfo): number => { + (userInfo: { new_password: string } | Partial<IUserInfo>): number => { return dispatch<any>({ type: 'settingModel/setting', payload: userInfo }); }, [dispatch], diff --git a/web/src/layouts/components/right-toolbar/index.tsx b/web/src/layouts/components/right-toolbar/index.tsx index e5df898..faca5c9 100644 --- a/web/src/layouts/components/right-toolbar/index.tsx +++ b/web/src/layouts/components/right-toolbar/index.tsx @@ -1,11 +1,19 @@ +import { ReactComponent as TranslationIcon } from '@/assets/svg/translation.svg'; +import { useTranslate } from '@/hooks/commonHooks'; import { GithubOutlined } from '@ant-design/icons'; -import { Space } from 'antd'; +import { Dropdown, MenuProps, Space } from 'antd'; import React from 'react'; import User from '../user'; + +import { useChangeLanguage } from '@/hooks/logicHooks'; import styled from './index.less'; -const Circle = ({ children }: React.PropsWithChildren) => { - return <div className={styled.circle}>{children}</div>; +const Circle = ({ children, ...restProps }: React.PropsWithChildren) => { + return ( + <div {...restProps} className={styled.circle}> + {children} + </div> + ); }; const handleGithubCLick = () => { @@ -13,17 +21,38 @@ const handleGithubCLick = () => { }; const RightToolBar = () => { + const { t } = useTranslate('common'); + const changeLanguage = useChangeLanguage(); + + const handleItemClick: MenuProps['onClick'] = ({ key }) => { + changeLanguage(key); + }; + + const items: MenuProps['items'] = [ + { + key: 'English', + label: <span>{t('english')}</span>, + }, + { type: 'divider' }, + { + key: 'Chinese', + label: <span>{t('chinese')}</span>, + }, + ]; + return ( <div className={styled.toolbarWrapper}> <Space wrap size={16}> <Circle> <GithubOutlined onClick={handleGithubCLick} /> </Circle> + <Dropdown menu={{ items, onClick: handleItemClick }} placement="bottom"> + <Circle> + <TranslationIcon /> + </Circle> + </Dropdown> {/* <Circle> - <TranslationIcon /> - </Circle> - <Circle> - <MoonIcon /> + <MonIcon /> </Circle> */} <User></User> </Space> diff --git a/web/src/locales/config.ts b/web/src/locales/config.ts index 311f5d2..17b7a7f 100644 --- a/web/src/locales/config.ts +++ b/web/src/locales/config.ts @@ -1,8 +1,8 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; -import translation_en from './en.json'; -import translation_zh from './zh.json'; +import translation_en from './en'; +import translation_zh from './zh'; const resources = { en: translation_en, diff --git a/web/src/locales/en.json b/web/src/locales/en.json deleted file mode 100644 index 027d7b7..0000000 --- a/web/src/locales/en.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "translation": { - "common": { - "delete": "Delete", - "deleteModalTitle": "Are you sure delete this item?", - "ok": "Yes", - "cancel": "No", - "total": "Total", - "rename": "Rename", - "name": "Name", - "namePlaceholder": "Please input name" - }, - "login": { - "login": "Sign in", - "signUp": "Sign up", - "loginDescription": "We’re so excited to see you again!", - "registerDescription": "Glad to have you on board!", - "emailLabel": "Email", - "emailPlaceholder": "Please input email", - "passwordLabel": "Password", - "passwordPlaceholder": "Please input password", - "rememberMe": "Remember me", - "signInTip": "Don’t have an account?", - "signUpTip": "Already have an account?", - "nicknameLabel": "Nickname", - "nicknamePlaceholder": "Please input nickname", - "register": "Create an account", - "continue": "Continue" - }, - "header": { - "knowledgeBase": "Knowledge Base", - "chat": "Chat", - "register": "Register", - "signin": "Sign in", - "home": "Home", - "setting": "用户设置", - "logout": "登出" - }, - "knowledgeList": { - "welcome": "Welcome back", - "description": "Which database are we going to use today?", - "createKnowledgeBase": "Create knowledge base", - "name": "Name", - "namePlaceholder": "Please input name!", - "doc": "Docs" - }, - "knowledgeDetails": { - "dataset": "Dataset", - "testing": "Retrieval testing", - "configuration": "Configuration", - "name": "Name", - "namePlaceholder": "Please input name!", - "doc": "Docs", - "datasetDescription": "Hey, don't forget to adjust the chunk after adding the dataset! 😉", - "addFile": "Add file", - "searchFiles": "Search your files", - "localFiles": "Local files", - "emptyFiles": "Create empty file", - "chunkNumber": "Chunk Number", - "uploadDate": "Upload Date", - "chunkMethod": "Chunk Method", - "enabled": "Enabled", - "action": "Action", - "parsingStatus": "Parsing Status", - "processBeginAt": "Process Begin At", - "processDuration": "Process Duration", - "progressMsg": "Progress Msg", - "testingDescription": "Final step! After success, leave the rest to Infiniflow AI.", - "topK": "Top K", - "topKTip": "For the computaion cost, not all the retrieved chunk will be computed vector cosine similarity with query. The bigger the 'Top K' is, the higher the recall rate is, the slower the retrieval speed is.", - "similarityThreshold": "Similarity threshold", - "similarityThresholdTip": "We use hybrid similarity score to evaluate distance between two lines of text. It's weighted keywords similarity and vector cosine similarity. If the similarity between query and chunk is less than this threshold, the chunk will be filtered out.", - "vectorSimilarityWeight": "Vector similarity weight", - "vectorSimilarityWeightTip": "We use hybrid similarity score to evaluate distance between two lines of text. It's weighted keywords similarity and vector cosine similarity. The sum of both weights is 1.0.", - "testText": "Test text", - "testTextPlaceholder": "Please input your question!", - "testingLabel": "Testing", - "similarity": "Hybrid Similarity", - "termSimilarity": "Term Similarity", - "vectorSimilarity": "Vector Similarity", - "hits": "Hits", - "view": "View", - "filesSelected": "Files Selected" - }, - "knowledgeConfiguration": { - "titleDescription": "Update your knowledge base details especially parsing method here.", - "name": "Knowledge base name", - "photo": "Knowledge base photo", - "description": "Description", - "language": "Language", - "languageMessage": "Please input your language!", - "languagePlaceholder": "Please input your language!", - "permissions": "Permissions", - "embeddingModel": "Embedding model", - "chunkTokenNumber": "Chunk token number", - "embeddingModelTip": "The embedding model used to embedding chunks. It's unchangable once the knowledgebase has chunks. You need to delete all the chunks if you want to change it.", - "permissionsTip": "If the permission is 'Team', all the team member can manipulate the knowledgebase.", - "chunkTokenNumberTip": "It determine the token number of a chunk approximately.", - "chunkMethodTip": "The instruction is at right.", - "upload": "Upload", - "english": "English", - "chinese": "Chinese", - "embeddingModelPlaceholder": "Please select a embedding model", - "chunkMethodPlaceholder": "Please select a chunk method", - "save": "Save", - "me": "Only me", - "team": "Team", - "cancel": "Cancel" - }, - "footer": { - "detail": "All rights reserved @ React" - }, - "layout": { - "file": "file", - "knowledge": "knowledge", - "chat": "chat" - }, - "setting": { - "btn": "en" - } - } -} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts new file mode 100644 index 0000000..487b5c3 --- /dev/null +++ b/web/src/locales/en.ts @@ -0,0 +1,433 @@ +export default { + translation: { + common: { + delete: 'Delete', + deleteModalTitle: 'Are you sure delete this item?', + ok: 'Yes', + cancel: 'No', + total: 'Total', + rename: 'Rename', + name: 'Name', + save: 'Save', + namePlaceholder: 'Please input name', + next: 'Next', + create: 'Create', + edit: 'Edit', + upload: 'Upload', + english: 'English', + chinese: 'Chinese', + language: 'Language', + languageMessage: 'Please input your language!', + languagePlaceholder: 'select your language', + }, + login: { + login: 'Sign in', + signUp: 'Sign up', + loginDescription: 'We’re so excited to see you again!', + registerDescription: 'Glad to have you on board!', + emailLabel: 'Email', + emailPlaceholder: 'Please input email', + passwordLabel: 'Password', + passwordPlaceholder: 'Please input password', + rememberMe: 'Remember me', + signInTip: 'Don’t have an account?', + signUpTip: 'Already have an account?', + nicknameLabel: 'Nickname', + nicknamePlaceholder: 'Please input nickname', + register: 'Create an account', + continue: 'Continue', + title: 'Start building your smart assistants.', + description: + 'Sign up for free to explore top RAG technology. Create knowledge bases and AIs to empower your business.', + review: 'from 500+ reviews', + }, + header: { + knowledgeBase: 'Knowledge Base', + chat: 'Chat', + register: 'Register', + signin: 'Sign in', + home: 'Home', + setting: '用户设置', + logout: '登出', + }, + knowledgeList: { + welcome: 'Welcome back', + description: 'Which knowledge base are we going to use today?', + createKnowledgeBase: 'Create knowledge base', + name: 'Name', + namePlaceholder: 'Please input name!', + doc: 'Docs', + }, + knowledgeDetails: { + dataset: 'Dataset', + testing: 'Retrieval testing', + files: 'files', + configuration: 'Configuration', + name: 'Name', + namePlaceholder: 'Please input name!', + doc: 'Docs', + datasetDescription: + "Hey, don't forget to adjust the chunk after adding the dataset! 😉", + addFile: 'Add file', + searchFiles: 'Search your files', + localFiles: 'Local files', + emptyFiles: 'Create empty file', + chunkNumber: 'Chunk Number', + uploadDate: 'Upload Date', + chunkMethod: 'Chunk Method', + enabled: 'Enabled', + action: 'Action', + parsingStatus: 'Parsing Status', + processBeginAt: 'Process Begin At', + processDuration: 'Process Duration', + progressMsg: 'Progress Msg', + testingDescription: + 'Final step! After success, leave the rest to Infiniflow AI.', + topK: 'Top K', + topKTip: + "For the computaion cost, not all the retrieved chunk will be computed vector cosine similarity with query. The bigger the 'Top K' is, the higher the recall rate is, the slower the retrieval speed is.", + similarityThreshold: 'Similarity threshold', + similarityThresholdTip: + "We use hybrid similarity score to evaluate distance between two lines of text. It's weighted keywords similarity and vector cosine similarity. If the similarity between query and chunk is less than this threshold, the chunk will be filtered out.", + vectorSimilarityWeight: 'Vector similarity weight', + vectorSimilarityWeightTip: + "We use hybrid similarity score to evaluate distance between two lines of text. It's weighted keywords similarity and vector cosine similarity. The sum of both weights is 1.0.", + testText: 'Test text', + testTextPlaceholder: 'Please input your question!', + testingLabel: 'Testing', + similarity: 'Hybrid Similarity', + termSimilarity: 'Term Similarity', + vectorSimilarity: 'Vector Similarity', + hits: 'Hits', + view: 'View', + filesSelected: 'Files Selected', + upload: 'Upload', + runningStatus0: 'UNSTART', + runningStatus1: 'Parsing', + runningStatus2: 'CANCEL', + runningStatus3: 'SUCCESS', + runningStatus4: 'FAIL', + pageRanges: 'Page Ranges', + pageRangesTip: + 'page ranges: Define the page ranges that need to be parsed. The pages that not included in these ranges will be ignored.', + fromPlaceholder: 'from', + fromMessage: 'Missing start page number', + toPlaceholder: 'to', + toMessage: 'Missing end page number(excluded)', + layoutRecognize: 'Layout recognize', + layoutRecognizeTip: + 'Use visual models for layout analysis to better identify document structure, find where the titles, text blocks, images, and tables are. Without this feature, only the plain text of the PDF can be obtained.', + taskPageSize: 'Task page size', + taskPageSizeMessage: 'Please input your task page size!', + taskPageSizeTip: `If using layout recognize, the PDF file will be split into groups of successive. Layout analysis will be performed parallelly between groups to increase the processing speed. The 'Task page size' determines the size of groups. The larger the page size is, the lower the chance of splitting continuous text between pages into different chunks.`, + addPage: 'Add page', + greaterThan: 'The current value must be greater than to!', + greaterThanPrevious: + 'The current value must be greater than the previous to!', + selectFiles: 'Select files', + changeSpecificCategory: 'Change specific category', + uploadTitle: 'Click or drag file to this area to upload', + uploadDescription: + 'Support for a single or bulk upload. Strictly prohibited from uploading company data or other banned files.', + }, + knowledgeConfiguration: { + titleDescription: + 'Update your knowledge base details especially parsing method here.', + name: 'Knowledge base name', + photo: 'Knowledge base photo', + description: 'Description', + language: 'Language', + languageMessage: 'Please input your language!', + languagePlaceholder: 'Please input your language!', + permissions: 'Permissions', + embeddingModel: 'Embedding model', + chunkTokenNumber: 'Chunk token number', + chunkTokenNumberMessage: 'Chunk token number is required', + embeddingModelTip: + "The embedding model used to embedding chunks. It's unchangable once the knowledgebase has chunks. You need to delete all the chunks if you want to change it.", + permissionsTip: + "If the permission is 'Team', all the team member can manipulate the knowledgebase.", + chunkTokenNumberTip: + 'It determine the token number of a chunk approximately.', + chunkMethod: 'Chunk method', + chunkMethodTip: 'The instruction is at right.', + upload: 'Upload', + english: 'English', + chinese: 'Chinese', + embeddingModelPlaceholder: 'Please select a embedding model', + chunkMethodPlaceholder: 'Please select a chunk method', + save: 'Save', + me: 'Only me', + team: 'Team', + cancel: 'Cancel', + methodTitle: 'Chunking Method Description', + methodExamples: 'Examples', + methodExamplesDescription: + 'This visual guides is in order to make understanding easier for you.', + dialogueExamplesTitle: 'Dialogue Examples', + methodEmpty: + 'This will display a visual explanation of the knowledge base categories', + book: `<p>Supported file formats are <b>DOCX</b>, <b>PDF</b>, <b>TXT</b>.</p><p> + Since a book is long and not all the parts are useful, if it's a PDF, + please setup the <i>page ranges</i> for every book in order eliminate negative effects and save computing time for analyzing.</p>`, + laws: `<p>Supported file formats are <b>DOCX</b>, <b>PDF</b>, <b>TXT</b>.</p><p> + Legal documents have a very rigorous writing format. We use text feature to detect split point. + </p><p> + The chunk granularity is consistent with 'ARTICLE', and all the upper level text will be included in the chunk. + </p>`, + manual: `<p>Only <b>PDF</b> is supported.</p><p> + We assume manual has hierarchical section structure. We use the lowest section titles as pivots to slice documents. + So, the figures and tables in the same section will not be sliced apart, and chunk size might be large. + </p>`, + naive: `<p>Supported file formats are <b>DOCX, EXCEL, PPT, IMAGE, PDF, TXT</b>.</p> + <p>This method apply the naive ways to chunk files: </p> + <p> + <li>Successive text will be sliced into pieces using vision detection model.</li> + <li>Next, these successive pieces are merge into chunks whose token number is no more than 'Token number'.</li></p>`, + paper: `<p>Only <b>PDF</b> file is supported.</p><p> + If our model works well, the paper will be sliced by it's sections, like <i>abstract, 1.1, 1.2</i>, etc. </p><p> + The benefit of doing this is that LLM can better summarize the content of relevant sections in the paper, + resulting in more comprehensive answers that help readers better understand the paper. + The downside is that it increases the context of the LLM conversation and adds computational cost, + so during the conversation, you can consider reducing the ‘<b>topN</b>’ setting.</p>`, + presentation: `<p>The supported file formats are <b>PDF</b>, <b>PPTX</b>.</p><p> + Every page will be treated as a chunk. And the thumbnail of every page will be stored.</p><p> + <i>All the PPT files you uploaded will be chunked by using this method automatically, setting-up for every PPT file is not necessary.</i></p>`, + qa: `<p><b>EXCEL</b> and <b>CSV/TXT</b> files are supported.</p><p> + If the file is in excel format, there should be 2 columns question and answer without header. + And question column is ahead of answer column. + And it's O.K if it has multiple sheets as long as the columns are rightly composed.</p><p> + + If it's in csv format, it should be UTF-8 encoded. Use TAB as delimiter to separate question and answer.</p><p> + + <i>All the deformed lines will be ignored. + Every pair of Q&A will be treated as a chunk.</i></p>`, + resume: `<p>The supported file formats are <b>DOCX</b>, <b>PDF</b>, <b>TXT</b>. + </p><p> + The résumé comes in a variety of formats, just like a person’s personality, but we often have to organize them into structured data that makes it easy to search. + </p><p> + Instead of chunking the résumé, we parse the résumé into structured data. As a HR, you can dump all the résumé you have, + the you can list all the candidates that match the qualifications just by talk with <i>'RAGFlow'</i>. + </p> + `, + table: `<p><b>EXCEL</b> and <b>CSV/TXT</b> format files are supported.</p><p> + Here're some tips: + <ul> + <li>For csv or txt file, the delimiter between columns is <em><b>TAB</b></em>.</li> + <li>The first line must be column headers.</li> + <li>Column headers must be meaningful terms in order to make our LLM understanding. + It's good to enumerate some synonyms using slash <i>'/'</i> to separate, and even better to + enumerate values using brackets like <i>'gender/sex(male, female)'</i>.<p> + Here are some examples for headers:<ol> + <li>supplier/vendor<b>'TAB'</b>color(yellow, red, brown)<b>'TAB'</b>gender/sex(male, female)<b>'TAB'</b>size(M,L,XL,XXL)</li> + <li>姓å/åå—<b>'TAB'</b>电è¯/手机/微信<b>'TAB'</b>最高å¦åŽ†ï¼ˆé«˜ä¸ï¼ŒèŒé«˜ï¼Œç¡•å£«ï¼Œæœ¬ç§‘,åšå£«ï¼Œåˆä¸ï¼Œä¸æŠ€ï¼Œä¸ä¸“,专科,专å‡æœ¬ï¼ŒMPA,MBA,EMBA)</li> + </ol> + </p> + </li> + <li>Every row in table will be treated as a chunk.</li> + </ul>`, + picture: ` + <p>Image files are supported. Video is coming soon.</p><p> + If the picture has text in it, OCR is applied to extract the text as its text description. + </p><p> + If the text extracted by OCR is not enough, visual LLM is used to get the descriptions. + </p>`, + one: ` + <p>Supported file formats are <b>DOCX, EXCEL, PDF, TXT</b>. + </p><p> + For a document, it will be treated as an entire chunk, no split at all. + </p><p> + If you want to summarize something that needs all the context of an article and the selected LLM's context length covers the document length, you can try this method. + </p>`, + }, + chunk: { + chunk: 'Chunk', + bulk: 'Bulk', + selectAll: 'Select All', + enabledSelected: 'Enable Selected', + disabledSelected: 'Disable Selected', + deleteSelected: 'Delete Selected', + search: 'Search', + all: 'All', + enabled: 'Enabled', + disabled: 'Disabled', + keyword: 'Keyword', + function: 'Function', + chunkMessage: 'Please input value!', + }, + chat: { + assistantSetting: 'Assistant Setting', + promptEngine: 'Prompt Engine', + modelSetting: 'Model Setting', + chat: 'Chat', + newChat: 'New chat', + send: 'Send', + sendPlaceholder: 'Message Resume Assistant...', + chatConfiguration: 'Chat Configuration', + chatConfigurationDescription: + ' Here, dress up a dedicated assistant for your special knowledge bases! 💕', + assistantName: 'Assistant name', + namePlaceholder: 'e.g. Resume Jarvis', + assistantAvatar: 'Assistant avatar', + language: 'Language', + emptyResponse: 'Empty response', + emptyResponseTip: `If nothing is retrieved with user's question in the knowledgebase, it will use this as an answer. If you want LLM comes up with its own opinion when nothing is retrieved, leave this blank.`, + setAnOpener: 'Set an opener', + setAnOpenerInitial: `Hi! I'm your assistant, what can I do for you?`, + setAnOpenerTip: 'How do you want to welcome your clients?', + knowledgeBases: 'Knowledgebases', + knowledgeBasesMessage: 'Please select', + knowledgeBasesTip: 'Select knowledgebases associated.', + system: 'System', + systemInitialValue: `You are an intelligent assistant. Please summarize the content of the knowledge base to answer the question. Please list the data in the knowledge base and answer in detail. When all knowledge base content is irrelevant to the question, your answer must include the sentence "The answer you are looking for is not found in the knowledge base!" Answers need to consider chat history. + Here is the knowledge base: + {knowledge} + The above is the knowledge base.`, + systemMessage: 'Please input!', + systemTip: + 'Instructions you need LLM to follow when LLM answers questions, like charactor design, answer length and answer language etc.', + topN: 'Top N', + topNTip: `Not all the chunks whose similarity score is above the 'simialrity threashold' will be feed to LLMs. LLM can only see these 'Top N' chunks.`, + variable: 'Variable', + variableTip: `If you use dialog APIs, the varialbes might help you chat with your clients with different strategies. + The variables are used to fill-in the 'System' part in prompt in order to give LLM a hint. + The 'knowledge' is a very special variable which will be filled-in with the retrieved chunks. + All the variables in 'System' should be curly bracketed.`, + add: 'Add', + key: 'key', + optional: 'Optional', + operation: 'operation', + model: 'Model', + modelTip: 'Large language chat model', + modelMessage: 'Please select!', + freedom: 'Freedom', + improvise: 'Improvise', + precise: 'Precise', + balance: 'Balance', + freedomTip: `'Precise' means the LLM will be conservative and answer your question cautiously. 'Improvise' means the you want LLM talk much and freely. 'Balance' is between cautiously and freely.`, + temperature: 'Temperature', + temperatureMessage: 'Temperature is required', + temperatureTip: + 'This parameter controls the randomness of predictions by the model. A lower temperature makes the model more confident in its responses, while a higher temperature makes it more creative and diverse.', + topP: 'Top P', + topPMessage: 'Top P is required', + topPTip: + 'Also known as “nucleus sampling,†this parameter sets a threshold to select a smaller set of words to sample from. It focuses on the most likely words, cutting off the less probable ones.', + presencePenalty: 'Presence Penalty', + presencePenaltyMessage: 'Presence Penalty is required', + presencePenaltyTip: + 'This discourages the model from repeating the same information by penalizing words that have already appeared in the conversation.', + frequencyPenalty: 'Frequency Penalty', + frequencyPenaltyMessage: 'Frequency Penalty is required', + frequencyPenaltyTip: + 'Similar to the presence penalty, this reduces the model’s tendency to repeat the same words frequently.', + maxTokens: 'Max Tokens', + maxTokensMessage: 'Max Tokens is required', + maxTokensTip: + 'This sets the maximum length of the model’s output, measured in the number of tokens (words or pieces of words).', + }, + setting: { + profile: 'Profile', + profileDescription: 'Update your photo and personal details here.', + password: 'Password', + passwordDescription: + 'Please enter your current password to change your password.', + model: 'Model Providers', + modelDescription: 'Manage your account settings and preferences here.', + team: 'Team', + logout: 'Log out', + username: 'Username', + usernameMessage: 'Please input your username!', + photo: 'Your photo', + photoDescription: 'This will be displayed on your profile.', + colorSchema: 'Color schema', + colorSchemaMessage: 'Please select your color schema!', + colorSchemaPlaceholder: 'select your color schema', + bright: 'Bright', + dark: 'Dark', + timezone: 'Timezone', + timezoneMessage: 'Please input your timezone!', + timezonePlaceholder: 'select your timezone', + email: 'Email address', + emailDescription: 'Once registered, E-mail cannot be changed.', + currentPassword: 'Current password', + currentPasswordMessage: 'Please input your password!', + newPassword: 'New password', + newPasswordMessage: 'Please input your password!', + newPasswordDescription: + 'Your new password must be more than 8 characters.', + confirmPassword: 'Confirm new password', + confirmPasswordMessage: 'Please confirm your password!', + confirmPasswordNonMatchMessage: + 'The new password that you entered do not match!', + cancel: 'Cancel', + addedModels: 'Added models', + modelsToBeAdded: 'Models to be added', + addTheModel: 'Add the model', + apiKey: 'API-Key', + apiKeyMessage: 'Please input api key!', + apiKeyTip: + 'The API key can be obtained by registering the corresponding LLM supplier.', + showMoreModels: 'Show more models', + baseUrl: 'Base-Url', + baseUrlTip: + 'If your API key is from OpenAI, just ignore it. Any other intermediate providers will give this base url with the API key.', + modify: 'Modify', + systemModelSettings: 'System Model Settings', + chatModel: 'Chat model', + chatModelTip: + 'The default chat LLM all the newly created knowledgebase will use.', + embeddingModel: 'Embedding model', + embeddingModelTip: + 'The default embedding model all the newly created knowledgebase will use.', + img2txtModel: 'Img2txt model', + img2txtModelTip: + 'The default multi-module model all the newly created knowledgebase will use. It can describe a picture or video.', + sequence2txtModel: 'Img2txt model', + sequence2txtModelTip: + 'The default ASR model all the newly created knowledgebase will use. Use this model to translate voices to corresponding text.', + workspace: 'Workspace', + upgrade: 'Upgrade', + }, + message: { + registered: 'Registered!', + logout: 'logout', + logged: 'logged!', + pleaseSelectChunk: 'Please select chunk!', + modified: 'Modified', + created: 'Created', + deleted: 'Deleted', + renamed: 'Renamed', + operated: 'Operated', + updated: 'Updated', + 200: 'The server successfully returns the requested data.', + 201: 'Create or modify data successfully.', + 202: 'A request has been queued in the background (asynchronous task).', + 204: 'Data deleted successfully.', + 400: 'There was an error in the request issued, and the server did not create or modify data.', + 401: 'The user does not have permissions (wrong token, username, password).', + 403: 'The user is authorized, but access is prohibited.', + 404: 'The request was made for a record that does not exist, and the server did not perform the operation.', + 406: 'The requested format is not available.', + 410: 'The requested resource has been permanently deleted and will not be available again.', + 422: 'When creating an object, a validation error occurred.', + 500: 'A server error occurred, please check the server.', + 502: 'Gateway error.', + 503: 'The service is unavailable and the server is temporarily overloaded or undergoing maintenance.', + 504: 'Gateway timeout.', + requestError: 'Request error', + networkAnomalyDescription: + 'There is an abnormality in your network and you cannot connect to the server.', + networkAnomaly: 'network anomaly', + hint: 'hint', + }, + footer: { + profile: 'All rights reserved @ React', + }, + layout: { + file: 'file', + knowledge: 'knowledge', + chat: 'chat', + }, + }, +}; diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json deleted file mode 100644 index a6fd835..0000000 --- a/web/src/locales/zh.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "translation": { - "login": { "login": "登录" }, - "header": { - "register": "注册", - "signin": "登陆", - "home": "首页", - "setting": "user setting", - "logout": "logout" - }, - "footer": { - "detail": "版æƒæ‰€æœ‰ @ React" - }, - "layout": { - "file": "文件", - "knowledge": "知识库", - "chat": "èŠå¤©" - }, - "setting": { - "btn": "ä¸æ–‡" - } - } -} diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts new file mode 100644 index 0000000..aa79895 --- /dev/null +++ b/web/src/locales/zh.ts @@ -0,0 +1,417 @@ +export default { + translation: { + common: { + delete: 'åˆ é™¤', + deleteModalTitle: 'ç¡®å®šåˆ é™¤å—?', + ok: '是', + cancel: 'å¦', + total: '总共', + rename: 'é‡å‘½å', + name: 'å称', + save: 'ä¿å˜', + namePlaceholder: '请输入å称', + next: '下一æ¥', + create: '创建', + edit: '编辑', + upload: 'ä¸Šä¼ ', + english: '英文', + chinese: 'ä¸æ–‡', + language: 'è¯è¨€', + languageMessage: '请输入è¯è¨€', + languagePlaceholder: '请选择è¯è¨€', + }, + login: { + login: '登录', + signUp: '注册', + loginDescription: '很高兴å†æ¬¡è§åˆ°æ‚¨ï¼', + registerDescription: 'å¾ˆé«˜å…´æ‚¨åŠ å…¥ï¼', + emailLabel: '邮箱', + emailPlaceholder: '请输入邮箱地å€', + passwordLabel: '密ç ', + passwordPlaceholder: '请输入密ç ', + rememberMe: 'è®°ä½æˆ‘', + signInTip: '没有å¸æˆ·ï¼Ÿ', + signUpTip: 'å·²ç»æœ‰å¸æˆ·ï¼Ÿ', + nicknameLabel: 'å称', + nicknamePlaceholder: '请输入å称', + register: '创建账户', + continue: '继ç»', + title: '开始构建您的智能助手', + description: + 'å…费注册以探索顶级 RAG 技术。 创建知识库和人工智能æ¥å¢žå¼ºæ‚¨çš„业务', + review: 'æ¥è‡ª 500 多æ¡è¯„论', + }, + header: { + knowledgeBase: '知识库', + chat: 'èŠå¤©', + register: '注册', + signin: '登录', + home: '首页', + setting: '用户设置', + logout: '登出', + }, + knowledgeList: { + welcome: '欢迎回æ¥', + description: '今天我们è¦ä½¿ç”¨å“ªä¸ªçŸ¥è¯†åº“?', + createKnowledgeBase: '创建知识库', + name: 'å称', + namePlaceholder: '请输入å称', + doc: '文档', + }, + knowledgeDetails: { + dataset: 'æ•°æ®é›†', + testing: '检索测试', + configuration: 'é…ç½®', + files: '文件', + name: 'å称', + namePlaceholder: '请输入å称', + doc: '文档', + datasetDescription: 'å˜¿ï¼Œæ·»åŠ æ•°æ®é›†åŽåˆ«å¿˜äº†è°ƒæ•´è§£æžå—ï¼ ðŸ˜‰', + addFile: '新增文件', + searchFiles: 'æœç´¢æ–‡ä»¶', + localFiles: '本地文件', + emptyFiles: '新建空文件', + chunkNumber: '模å—æ•°', + uploadDate: 'ä¸Šä¼ æ—¥æœŸ', + chunkMethod: '解æžæ–¹æ³•', + enabled: 'å¯ç”¨', + action: '动作', + parsingStatus: '解æžçŠ¶æ€', + processBeginAt: 'æµç¨‹å¼€å§‹äºŽ', + processDuration: '过程æŒç»æ—¶é—´', + progressMsg: '进度消æ¯', + testingDescription: '最åŽä¸€æ¥ï¼ æˆåŠŸåŽï¼Œå‰©ä¸‹çš„就交给Infiniflow AIå§ã€‚', + topK: 'Top K', + topKTip: + '对于计算æˆæœ¬ï¼Œå¹¶éžæ‰€æœ‰æ£€ç´¢åˆ°çš„å—都会计算与查询的å‘é‡ä½™å¼¦ç›¸ä¼¼åº¦ã€‚ Top K越大,å¬å›žçŽ‡è¶Šé«˜ï¼Œæ£€ç´¢é€Ÿåº¦è¶Šæ…¢ã€‚', + similarityThreshold: '相似度阈值', + similarityThresholdTip: + '我们使用混åˆç›¸ä¼¼åº¦å¾—分æ¥è¯„估两行文本之间的è·ç¦»ã€‚ å®ƒæ˜¯åŠ æƒå…³é”®è¯ç›¸ä¼¼åº¦å’Œå‘é‡ä½™å¼¦ç›¸ä¼¼åº¦ã€‚ 如果查询和å—之间的相似度å°äºŽæ¤é˜ˆå€¼ï¼Œåˆ™è¯¥å—将被过滤掉。', + vectorSimilarityWeight: 'å‘é‡ç›¸ä¼¼åº¦æƒé‡', + vectorSimilarityWeightTip: + '我们使用混åˆç›¸ä¼¼åº¦å¾—分æ¥è¯„估两行文本之间的è·ç¦»ã€‚ å®ƒæ˜¯åŠ æƒå…³é”®è¯ç›¸ä¼¼åº¦å’Œå‘é‡ä½™å¼¦ç›¸ä¼¼åº¦ã€‚ 两个æƒé‡ä¹‹å’Œä¸º 1.0。', + testText: '测试文本', + testTextPlaceholder: '请输入您的问题ï¼', + testingLabel: '测试', + similarity: 'æ··åˆç›¸ä¼¼åº¦', + termSimilarity: '术è¯ç›¸ä¼¼åº¦', + vectorSimilarity: 'å‘é‡ç›¸ä¼¼åº¦', + hits: '命ä¸æ•°', + view: '看法', + filesSelected: '选定的文件', + upload: 'ä¸Šä¼ ', + runningStatus0: '未å¯åŠ¨', + runningStatus1: '解æžä¸', + runningStatus2: 'å–消', + runningStatus3: 'æˆåŠŸ', + runningStatus4: '失败', + pageRanges: '页ç 范围', + pageRangesTip: + '页ç 范围:定义需è¦è§£æžçš„页é¢èŒƒå›´ã€‚ ä¸åŒ…å«åœ¨è¿™äº›èŒƒå›´å†…的页é¢å°†è¢«å¿½ç•¥ã€‚', + fromPlaceholder: '从', + fromMessage: '缺少起始页ç ', + toPlaceholder: '到', + toMessage: '缺少结æŸé¡µç (ä¸åŒ…å«ï¼‰', + layoutRecognize: '布局识别', + layoutRecognizeTip: + '使用视觉模型进行布局分æžï¼Œä»¥æ›´å¥½åœ°è¯†åˆ«æ–‡æ¡£ç»“æž„ï¼Œæ‰¾åˆ°æ ‡é¢˜ã€æ–‡æœ¬å—ã€å›¾åƒå’Œè¡¨æ ¼çš„ä½ç½®ã€‚ 如果没有æ¤åŠŸèƒ½ï¼Œåˆ™åªèƒ½èŽ·å– PDF 的纯文本。', + taskPageSize: '任务页é¢å¤§å°', + taskPageSizeMessage: '请输入您的任务页é¢å¤§å°ï¼', + taskPageSizeTip: `如果使用布局识别,PDF 文件将被分æˆè¿žç»çš„组。 布局分æžå°†åœ¨ç»„之间并行执行,以æ高处ç†é€Ÿåº¦ã€‚ “任务页é¢å¤§å°â€å†³å®šç»„的大å°ã€‚ 页é¢å¤§å°è¶Šå¤§ï¼Œå°†é¡µé¢ä¹‹é—´çš„è¿žç»æ–‡æœ¬åˆ†å‰²æˆä¸åŒå—的机会就越低。`, + addPage: '新增页é¢', + greaterThan: '当å‰å€¼å¿…须大于起始值ï¼', + greaterThanPrevious: '当å‰å€¼å¿…须大于之å‰çš„值ï¼', + selectFiles: '选择文件', + changeSpecificCategory: '更改特定类别', + uploadTitle: '点击或拖拽文件至æ¤åŒºåŸŸå³å¯ä¸Šä¼ ', + uploadDescription: + '支æŒå•æ¬¡æˆ–批é‡ä¸Šä¼ 。 严ç¦ä¸Šä¼ å…¬å¸æ•°æ®æˆ–其他è¿ç¦æ–‡ä»¶ã€‚', + }, + knowledgeConfiguration: { + titleDescription: '在这里更新您的知识库详细信æ¯ï¼Œå°¤å…¶æ˜¯è§£æžæ–¹æ³•ã€‚', + name: '知识库å称', + photo: '知识库图片', + description: 'æè¿°', + language: 'è¯è¨€', + languageMessage: '请输入è¯è¨€', + languagePlaceholder: '请输入è¯è¨€', + permissions: 'æƒé™', + embeddingModel: '嵌入模型', + chunkTokenNumber: 'å—令牌数', + chunkTokenNumberMessage: 'å—令牌数是必填项', + embeddingModelTip: + '用于嵌入å—的嵌入模型。 一旦知识库有了å—ï¼Œå®ƒå°±æ— æ³•æ›´æ”¹ã€‚ å¦‚æžœä½ æƒ³æ”¹å˜å®ƒï¼Œä½ 需è¦åˆ 除所有的å—。', + permissionsTip: '如果æƒé™æ˜¯â€œå›¢é˜Ÿâ€ï¼Œåˆ™æ‰€æœ‰å›¢é˜Ÿæˆå‘˜éƒ½å¯ä»¥æ“作知识库。', + chunkTokenNumberTip: '它大致确定了一个å—的令牌数é‡ã€‚', + chunkMethod: '解æžæ–¹æ³•', + chunkMethodTip: '说明ä½äºŽå³ä¾§ã€‚', + upload: 'ä¸Šä¼ ', + english: '英文', + chinese: 'ä¸æ–‡', + embeddingModelPlaceholder: '请选择嵌入模型', + chunkMethodPlaceholder: '请选择分å—方法', + save: 'ä¿å˜', + me: 'åªæœ‰æˆ‘', + team: '团队', + cancel: 'å–消', + methodTitle: '分å—方法说明', + methodExamples: '示例', + methodExamplesDescription: '这个视觉指å—是为了让您更容易ç†è§£ã€‚', + dialogueExamplesTitle: '对è¯ç¤ºä¾‹', + methodEmpty: '这将显示知识库类别的å¯è§†åŒ–解释', + book: `<p>支æŒçš„æ–‡ä»¶æ ¼å¼ä¸º<b>DOCX</b>ã€<b>PDF</b>ã€<b>TXT</b>。</p><p> + 由于一本书很长,并ä¸æ˜¯æ‰€æœ‰éƒ¨åˆ†éƒ½æœ‰ç”¨ï¼Œå¦‚果是 PDF, + 请为æ¯æœ¬ä¹¦è®¾ç½®<i>页é¢èŒƒå›´</i>,以消除负é¢å½±å“并节çœåˆ†æžè®¡ç®—时间。</p>`, + laws: `<p>支æŒçš„æ–‡ä»¶æ ¼å¼ä¸º<b>DOCX</b>ã€<b>PDF</b>ã€<b>TXT</b>。</p><p> + 法律文件有éžå¸¸ä¸¥æ ¼çš„ä¹¦å†™æ ¼å¼ã€‚ 我们使用文本特å¾æ¥æ£€æµ‹åˆ†å‰²ç‚¹ã€‚ + </p><p> + chunk的粒度与'ARTICLE'一致,所有上层文本都会包å«åœ¨chunkä¸ã€‚ + </p>`, + manual: `<p>仅支æŒ<b>PDF</b>。</p><p> + 我们å‡è®¾æ‰‹å†Œå…·æœ‰åˆ†å±‚部分结构。 æˆ‘ä»¬ä½¿ç”¨æœ€ä½Žçš„éƒ¨åˆ†æ ‡é¢˜ä½œä¸ºå¯¹æ–‡æ¡£è¿›è¡Œåˆ‡ç‰‡çš„æž¢è½´ã€‚ + å› æ¤ï¼ŒåŒä¸€éƒ¨åˆ†ä¸çš„图和表ä¸ä¼šè¢«åˆ†å‰²ï¼Œå¹¶ä¸”å—大å°å¯èƒ½ä¼šå¾ˆå¤§ã€‚ + </p>`, + naive: `<p>支æŒçš„æ–‡ä»¶æ ¼å¼ä¸º<b>DOCXã€EXCELã€PPTã€IMAGEã€PDFã€TXT</b>。</p> + <p>æ¤æ–¹æ³•å°†ç®€å•çš„方法应用于å—文件:</p> + <p> + <li>系统将使用视觉检测模型将连ç»æ–‡æœ¬åˆ†å‰²æˆå¤šä¸ªç‰‡æ®µã€‚</li> + <li>接下æ¥ï¼Œè¿™äº›è¿žç»çš„片段被åˆå¹¶æˆä»¤ç‰Œæ•°ä¸è¶…过“令牌数â€çš„å—。</li></p>`, + paper: `<p>仅支æŒ<b>PDF</b>文件。</p><p> + 如果我们的模型è¿è¡Œè‰¯å¥½ï¼Œè®ºæ–‡å°†æŒ‰å…¶éƒ¨åˆ†è¿›è¡Œåˆ‡ç‰‡ï¼Œä¾‹å¦‚<i>摘è¦ã€1.1ã€1.2</i>ç‰ã€‚</p><p> + è¿™æ ·åšçš„好处是LLMå¯ä»¥æ›´å¥½çš„概括论文ä¸ç›¸å…³ç« 节的内容, + 产生更全é¢çš„ç”案,帮助读者更好地ç†è§£è®ºæ–‡ã€‚ + ç¼ºç‚¹æ˜¯å®ƒå¢žåŠ äº† LLM 对è¯çš„èƒŒæ™¯å¹¶å¢žåŠ äº†è®¡ç®—æˆæœ¬ï¼Œ + 所以在对è¯è¿‡ç¨‹ä¸ï¼Œä½ å¯ä»¥è€ƒè™‘å‡å°‘‘<b>topN</b>’的设置。</p>`, + presentation: `<p>支æŒçš„æ–‡ä»¶æ ¼å¼ä¸º<b>PDF</b>ã€<b>PPTX</b>。</p><p> + æ¯ä¸ªé¡µé¢éƒ½å°†è¢«è§†ä¸ºä¸€ä¸ªå—。 并且æ¯ä¸ªé¡µé¢çš„缩略图都会被å˜å‚¨ã€‚</p><p> + <i>æ‚¨ä¸Šä¼ çš„æ‰€æœ‰PPT文件都会使用æ¤æ–¹æ³•è‡ªåŠ¨åˆ†å—ï¼Œæ— éœ€ä¸ºæ¯ä¸ªPPT文件进行设置。</i></p>`, + qa: `支æŒ<p><b>EXCEL</b>å’Œ<b>CSV/TXT</b>文件。</p><p> + 如果文件是Excelæ ¼å¼ï¼Œåº”该有2列问题和ç”æ¡ˆï¼Œæ²¡æœ‰æ ‡é¢˜ã€‚ + 问题æ ä½äºŽç”案æ 之å‰ã€‚ + 如果有多个工作表也没关系,åªè¦åˆ—的组åˆæ£ç¡®å³å¯ã€‚</p><p> + + 如果是 csv æ ¼å¼ï¼Œåˆ™åº”采用 UTF-8 ç¼–ç 。 使用 TAB 作为分隔符æ¥åˆ†éš”问题和ç”案。</p><p> + + <i>所有å˜å½¢çš„线都将被忽略。 + æ¯å¯¹é—®ç”都将被视为一个å—。</i></p>`, + resume: `<p>支æŒçš„æ–‡ä»¶æ ¼å¼ä¸º<b>DOCX</b>ã€<b>PDF</b>ã€<b>TXT</b>。 + </p><p> + 简历有多ç§æ ¼å¼ï¼Œå°±åƒä¸€ä¸ªäººçš„ä¸ªæ€§ä¸€æ ·ï¼Œä½†æˆ‘ä»¬ç»å¸¸å¿…须将它们组织æˆç»“构化数æ®ï¼Œä»¥ä¾¿äºŽæœç´¢ã€‚ + </p><p> + 我们ä¸æ˜¯å°†ç®€åŽ†åˆ†å—,而是将简历解æžä¸ºç»“构化数æ®ã€‚ 作为HRï¼Œä½ å¯ä»¥æ‰”掉所有的简历, + 您åªéœ€ä¸Ž<i>'RAGFlow'</i>交谈å³å¯åˆ—出所有符åˆèµ„æ ¼çš„å€™é€‰äººã€‚ + </p> + `, + table: `支æŒ<p><b>EXCEL</b>å’Œ<b>CSV/TXT</b>æ ¼å¼æ–‡ä»¶ã€‚</p><p> + 以下是一些æ示: + <ul> + <li>对于 csv 或 txt 文件,列之间的分隔符为 <em><b>TAB</b></em>。</li> + <li>ç¬¬ä¸€è¡Œå¿…é¡»æ˜¯åˆ—æ ‡é¢˜ã€‚</li> + <li>åˆ—æ ‡é¢˜å¿…é¡»æ˜¯æœ‰æ„义的术è¯ï¼Œä»¥ä¾¿æˆ‘们的法å¦ç¡•å£«èƒ½å¤Ÿç†è§£ã€‚ + 列举一些åŒä¹‰è¯æ—¶æœ€å¥½ä½¿ç”¨æ–œæ <i>'/'</i>æ¥åˆ†éš”,甚至更好 + 使用方括å·æžšä¸¾å€¼ï¼Œä¾‹å¦‚ <i>'gender/sex(male,female)'</i>.<p> + ä»¥ä¸‹æ˜¯æ ‡é¢˜çš„ä¸€äº›ç¤ºä¾‹ï¼š<ol> + <li>供应商/供货商<b>'TAB'</b>颜色(黄色ã€çº¢è‰²ã€æ£•è‰²ï¼‰<b>'TAB'</b>性别(男ã€å¥³ï¼‰<b>'TAB'</ b>å°ºç (Mã€Lã€XLã€XXL)</li> + <li>姓å/åå—<b>'TAB'</b>电è¯/手机/微信<b>'TAB'</b>最高å¦åŽ†ï¼ˆé«˜ä¸ï¼ŒèŒé«˜ï¼Œç¡•å£«ï¼Œæœ¬ç§‘,åšå£«ï¼Œåˆä¸ï¼Œä¸æŠ€ï¼Œä¸ 专,专科,专å‡æœ¬ï¼ŒMPA,MBA,EMBA)</li> + </ol> + </p> + </li> + <li>表ä¸çš„æ¯ä¸€è¡Œéƒ½å°†è¢«è§†ä¸ºä¸€ä¸ªå—。</li> + </ul>`, + picture: ` + <p>支æŒå›¾åƒæ–‡ä»¶ã€‚ 视频å³å°†æŽ¨å‡ºã€‚</p><p> + 如果图片ä¸æœ‰æ–‡å—,则应用 OCR æå–æ–‡å—作为其文å—æ述。 + </p><p> + 如果OCRæå–的文本ä¸å¤Ÿï¼Œå¯ä»¥ä½¿ç”¨è§†è§‰LLMæ¥èŽ·å–æ述。 + </p>`, + one: ` + <p>支æŒçš„æ–‡ä»¶æ ¼å¼ä¸º<b>DOCXã€EXCELã€PDFã€TXT</b>。 + </p><p> + 对于一个文档,它将被视为一个完整的å—ï¼Œæ ¹æœ¬ä¸ä¼šè¢«åˆ†å‰²ã€‚ + </p><p> + å¦‚æžœä½ è¦æ€»ç»“的东西需è¦ä¸€ç¯‡æ–‡ç« 的全部上下文,并且所选LLMçš„ä¸Šä¸‹æ–‡é•¿åº¦è¦†ç›–äº†æ–‡æ¡£é•¿åº¦ï¼Œä½ å¯ä»¥å°è¯•è¿™ç§æ–¹æ³•ã€‚ + </p>`, + }, + chunk: { + chunk: '解æžå—', + bulk: '批é‡', + selectAll: '选择所有', + enabledSelected: 'å¯ç”¨é€‰å®šçš„', + disabledSelected: 'ç¦ç”¨é€‰å®šçš„', + deleteSelected: 'åˆ é™¤é€‰å®šçš„', + search: 'æœç´¢', + all: '所有', + enabled: 'å¯ç”¨', + disabled: 'ç¦ç”¨çš„', + keyword: '关键è¯', + function: '函数', + chunkMessage: '请输入值ï¼', + }, + chat: { + assistantSetting: '助ç†è®¾ç½®', + promptEngine: 'æ示引擎', + modelSetting: '模型设置', + chat: 'èŠå¤©', + newChat: '新建èŠå¤©', + send: 'å‘é€', + sendPlaceholder: '消æ¯æ¦‚è¦åŠ©æ‰‹...', + chatConfiguration: 'èŠå¤©é…ç½®', + chatConfigurationDescription: 'åœ¨è¿™é‡Œï¼Œä¸ºä½ çš„ä¸“ä¸šçŸ¥è¯†åº“è£…æ‰®ä¸“å±žåŠ©æ‰‹ï¼ ðŸ’•', + assistantName: '助ç†å§“å', + namePlaceholder: '例如 贾维斯简历', + assistantAvatar: '助ç†å¤´åƒ', + language: 'è¯è¨€', + emptyResponse: '空回å¤', + emptyResponseTip: `如果在知识库ä¸æ²¡æœ‰æ£€ç´¢åˆ°ç”¨æˆ·çš„问题,它将使用它作为ç”案。 如果您希望 LLM 在未检索到任何内容时æ出自己的æ„è§ï¼Œè¯·å°†æ¤ç•™ç©ºã€‚`, + setAnOpener: '设置开场白', + setAnOpenerInitial: `ä½ å¥½ï¼ æˆ‘æ˜¯ä½ çš„åŠ©ç†ï¼Œæœ‰ä»€ä¹ˆå¯ä»¥å¸®åˆ°ä½ çš„å—?`, + setAnOpenerTip: '您想如何欢迎您的客户?', + knowledgeBases: '知识库', + knowledgeBasesMessage: '请选择', + knowledgeBasesTip: '选择关è”的知识库。', + system: '系统', + systemInitialValue: `ä½ æ˜¯ä¸€ä¸ªæ™ºèƒ½åŠ©æ‰‹ï¼Œè¯·æ€»ç»“çŸ¥è¯†åº“çš„å†…å®¹æ¥å›žç”问题,请列举知识库ä¸çš„æ•°æ®è¯¦ç»†å›žç”ã€‚å½“æ‰€æœ‰çŸ¥è¯†åº“å†…å®¹éƒ½ä¸Žé—®é¢˜æ— å…³æ—¶ï¼Œä½ çš„å›žç”必须包括“知识库ä¸æœªæ‰¾åˆ°æ‚¨è¦çš„ç”案ï¼â€è¿™å¥è¯ã€‚回ç”需è¦è€ƒè™‘èŠå¤©åŽ†å²ã€‚ + 以下是知识库: + {knowledge} + 以上是知识库。`, + systemMessage: '请输入', + systemTip: + '当LLM回ç”é—®é¢˜æ—¶ï¼Œä½ éœ€è¦LLMéµå¾ªçš„说明,比如角色设计ã€ç”案长度和ç”案è¯è¨€ç‰ã€‚', + topN: 'Top N', + topNTip: `并éžæ‰€æœ‰ç›¸ä¼¼åº¦å¾—分高于“相似度阈值â€çš„å—都会被æ供给法å¦ç¡•å£«ã€‚ LLM åªèƒ½çœ‹åˆ°è¿™äº›â€œTop Nâ€å—。`, + variable: 'å˜é‡', + variableTip: `å¦‚æžœæ‚¨ä½¿ç”¨å¯¹è¯ API,å˜é‡å¯èƒ½ä¼šå¸®åŠ©æ‚¨ä½¿ç”¨ä¸åŒçš„ç–略与客户èŠå¤©ã€‚ + 这些å˜é‡ç”¨äºŽå¡«å†™æ示ä¸çš„“系统â€éƒ¨åˆ†ï¼Œä»¥ä¾¿ç»™LLM一个æ示。 + “知识â€æ˜¯ä¸€ä¸ªéžå¸¸ç‰¹æ®Šçš„å˜é‡ï¼Œå®ƒå°†ç”¨æ£€ç´¢åˆ°çš„å—填充。 + “Systemâ€ä¸çš„所有å˜é‡éƒ½åº”该用大括å·æ‹¬èµ·æ¥ã€‚`, + add: '新增', + key: '关键å—', + optional: 'å¯é€‰çš„', + operation: 'æ“作', + model: '模型', + modelTip: '大è¯è¨€èŠå¤©æ¨¡åž‹', + modelMessage: '请选择', + freedom: '自由', + improvise: 'å³å…´åˆ›ä½œ', + precise: '精确', + balance: '平衡', + freedomTip: `“精确â€æ„味ç€æ³•å¦ç¡•å£«ä¼šä¿å®ˆå¹¶è°¨æ…Žåœ°å›žç”ä½ çš„é—®é¢˜ã€‚ “å³å…´å‘挥â€æ„味ç€ä½ 希望法å¦ç¡•å£«èƒ½å¤Ÿè‡ªç”±åœ°ç•…所欲言。 “平衡â€æ˜¯è°¨æ…Žä¸Žè‡ªç”±ä¹‹é—´çš„平衡。`, + temperature: '温度', + temperatureMessage: '温度是必填项', + temperatureTip: + '该å‚数控制模型预测的éšæœºæ€§ã€‚ 较低的温度使模型对其å“åº”æ›´æœ‰ä¿¡å¿ƒï¼Œè€Œè¾ƒé«˜çš„æ¸©åº¦åˆ™ä½¿å…¶æ›´å…·åˆ›é€ æ€§å’Œå¤šæ ·æ€§ã€‚', + topP: 'Top P', + topPMessage: 'Top P 是必填项', + topPTip: + '该å‚æ•°ä¹Ÿç§°ä¸ºâ€œæ ¸å¿ƒé‡‡æ ·â€ï¼Œå®ƒè®¾ç½®ä¸€ä¸ªé˜ˆå€¼æ¥é€‰æ‹©è¾ƒå°çš„å•è¯é›†è¿›è¡Œé‡‡æ ·ã€‚ 它专注于最å¯èƒ½çš„å•è¯ï¼Œå‰”除ä¸å¤ªå¯èƒ½çš„å•è¯ã€‚', + presencePenalty: '出å¸å¤„罚', + presencePenaltyMessage: '出å¸å¤„罚是必填项', + presencePenaltyTip: + '这会通过惩罚对è¯ä¸å·²ç»å‡ºçŽ°çš„å•è¯æ¥é˜»æ¢æ¨¡åž‹é‡å¤ç›¸åŒçš„ä¿¡æ¯ã€‚', + frequencyPenalty: '频率惩罚', + frequencyPenaltyMessage: '频率惩罚是必填项', + frequencyPenaltyTip: + '与å˜åœ¨æƒ©ç½šç±»ä¼¼ï¼Œè¿™å‡å°‘了模型频ç¹é‡å¤ç›¸åŒå•è¯çš„倾å‘。', + maxTokens: '最大tokenæ•°', + maxTokensMessage: '最大token数是必填项', + maxTokensTip: + 'è¿™è®¾ç½®äº†æ¨¡åž‹è¾“å‡ºçš„æœ€å¤§é•¿åº¦ï¼Œä»¥æ ‡è®°ï¼ˆå•è¯æˆ–å•è¯ç‰‡æ®µï¼‰çš„æ•°é‡æ¥è¡¡é‡ã€‚', + }, + setting: { + profile: '概è¦', + profileDescription: '在æ¤æ›´æ–°æ‚¨çš„照片和个人详细信æ¯ã€‚', + password: '密ç ', + passwordDescription: '请输入您当å‰çš„密ç 以更改您的密ç 。', + model: '模型æ供商', + modelDescription: '在æ¤ç®¡ç†æ‚¨çš„å¸æˆ·è®¾ç½®å’Œé¦–选项。', + team: '团队', + logout: '登出', + username: '用户å', + usernameMessage: '请输入用户å', + photo: '头åƒ', + photoDescription: '这将显示在您的个人资料上。', + colorSchema: '主题', + colorSchemaMessage: '请选择您的主题ï¼', + colorSchemaPlaceholder: '请选择您的主题ï¼', + bright: '明亮', + dark: '暗色', + timezone: '时区', + timezoneMessage: '请选择时区', + timezonePlaceholder: '请选择时区', + email: '邮箱地å€', + emailDescription: '一旦注册,电åé‚®ä»¶å°†æ— æ³•æ›´æ”¹ã€‚', + currentPassword: '当å‰å¯†ç ', + currentPasswordMessage: '请输入当å‰å¯†ç ', + newPassword: '新密ç ', + newPasswordMessage: '请输入新密ç ', + newPasswordDescription: '您的新密ç 必须超过 8 个å—符。', + confirmPassword: '确认新密ç ', + confirmPasswordMessage: '请确认新密ç ', + confirmPasswordNonMatchMessage: '您输入的新密ç ä¸åŒ¹é…ï¼', + cancel: 'å–消', + addedModels: 'æ·»åŠ äº†çš„æ¨¡åž‹', + modelsToBeAdded: 'å¾…æ·»åŠ çš„æ¨¡åž‹', + addTheModel: 'æ·»åŠ æ¨¡åž‹', + apiKey: 'API-Key', + apiKeyMessage: '请输入 api key!', + apiKeyTip: 'API keyå¯ä»¥é€šè¿‡æ³¨å†Œç›¸åº”çš„LLM供应商æ¥èŽ·å–。', + showMoreModels: '展示更多模型', + baseUrl: 'Base-Url', + baseUrlTip: + '如果您的 API 密钥æ¥è‡ª OpenAI,请忽略它。 任何其他ä¸é—´æ供商都会æ供带有 API 密钥的基本 URL。', + modify: '修改', + systemModelSettings: '系统模型设置', + chatModel: 'èŠå¤©æ¨¡åž‹', + chatModelTip: '所有新创建的知识库都会使用默认的èŠå¤©LLM。', + embeddingModel: '嵌入模型', + embeddingModelTip: '所有新创建的知识库都将使用的默认嵌入模型。', + img2txtModel: 'Img2txt模型', + img2txtModelTip: + '所有新创建的知识库都将使用默认的多模å—模型。 它å¯ä»¥æ述图片或视频。', + sequence2txtModel: 'Img2txt模型', + sequence2txtModelTip: + '所有新创建的知识库都将使用默认的 ASR 模型。 使用æ¤æ¨¡åž‹å°†è¯éŸ³ç¿»è¯‘为相应的文本。', + workspace: '工作空间', + upgrade: 'å‡çº§', + }, + message: { + registered: '注册æˆåŠŸ', + logout: '登出æˆåŠŸ', + logged: '登录æˆåŠŸ', + pleaseSelectChunk: '请选择解æžå—', + modified: 'æ›´æ–°æˆåŠŸ', + created: '创建æˆåŠŸ', + deleted: 'åˆ é™¤æˆåŠŸ', + renamed: 'é‡å‘½åæˆåŠŸ', + operated: 'æ“作æˆåŠŸ', + updated: 'æ›´æ–°æˆåŠŸ', + 200: 'æœåŠ¡å™¨æˆåŠŸè¿”回请求的数æ®ã€‚', + 201: '新建或修改数æ®æˆåŠŸã€‚', + 202: '一个请求已ç»è¿›å…¥åŽå°æŽ’队(异æ¥ä»»åŠ¡ï¼‰ã€‚', + 204: 'åˆ é™¤æ•°æ®æˆåŠŸã€‚', + 400: 'å‘出的请求有错误,æœåŠ¡å™¨æ²¡æœ‰è¿›è¡Œæ–°å»ºæˆ–修改数æ®çš„æ“作。', + 401: '用户没有æƒé™ï¼ˆä»¤ç‰Œã€ç”¨æˆ·åã€å¯†ç 错误)。', + 403: '用户得到授æƒï¼Œä½†æ˜¯è®¿é—®æ˜¯è¢«ç¦æ¢çš„。', + 404: 'å‘出的请求针对的是ä¸å˜åœ¨çš„记录,æœåŠ¡å™¨æ²¡æœ‰è¿›è¡Œæ“作。', + 406: 'è¯·æ±‚çš„æ ¼å¼ä¸å¯å¾—。', + 410: '请求的资æºè¢«æ°¸ä¹…åˆ é™¤ï¼Œä¸”ä¸ä¼šå†å¾—到的。', + 422: '当创建一个对象时,å‘生一个验è¯é”™è¯¯ã€‚', + 500: 'æœåŠ¡å™¨å‘生错误,请检查æœåŠ¡å™¨ã€‚', + 502: '网关错误。', + 503: 'æœåŠ¡ä¸å¯ç”¨ï¼ŒæœåŠ¡å™¨æš‚时过载或维护。', + 504: '网关超时。', + requestError: '请求错误', + networkAnomalyDescription: '您的网络å‘ç”Ÿå¼‚å¸¸ï¼Œæ— æ³•è¿žæŽ¥æœåŠ¡å™¨', + networkAnomaly: '网络异常', + hint: 'æ示', + }, + footer: { + profile: 'All rights reserved @ React', + }, + layout: { + file: 'file', + knowledge: 'knowledge', + chat: 'chat', + }, + }, +}; diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx index 15f84ac..93f2382 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx @@ -3,6 +3,7 @@ import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; import { DeleteOutlined } from '@ant-design/icons'; import { Checkbox, Form, Input, Modal, Space } from 'antd'; import React, { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'umi'; import EditTag from '../edit-tag'; @@ -24,6 +25,7 @@ const ChunkCreatingModal: React.FC<kFProps> = ({ doc_id, chunkId }) => { const [keywords, setKeywords] = useState<string[]>([]); const loading = useOneNamespaceEffectsLoading('chunkModel', ['create_chunk']); const { removeChunk } = useDeleteChunkByIds(); + const { t } = useTranslation(); const handleCancel = () => { dispatch({ @@ -87,7 +89,7 @@ const ChunkCreatingModal: React.FC<kFProps> = ({ doc_id, chunkId }) => { return ( <Modal - title={`${chunkId ? 'Edit' : 'Create'} Chunk`} + title={`${chunkId ? t('common.edit') : t('common.create')} ${t('chunk.chunk')}`} open={isShowCreateModal} onOk={handleOk} onCancel={handleCancel} @@ -100,27 +102,27 @@ const ChunkCreatingModal: React.FC<kFProps> = ({ doc_id, chunkId }) => { layout={'vertical'} > <Form.Item<FieldType> - label="Chunk" + label={t('chunk.chunk')} name="content" - rules={[{ required: true, message: 'Please input value!' }]} + rules={[{ required: true, message: t('chunk.chunkMessage') }]} > <Input.TextArea autoSize={{ minRows: 4, maxRows: 10 }} /> </Form.Item> </Form> <section> - <p>Keyword*</p> + <p>{t('chunk.keyword')} *</p> <EditTag tags={keywords} setTags={setKeywords} /> </section> {chunkId && ( <section> - <p>Function*</p> + <p>{t('chunk.function')} *</p> <Space size={'large'}> <Checkbox onChange={handleCheck} checked={checked}> - Enabled + {t('chunk.enabled')} </Checkbox> <span onClick={handleRemove}> - <DeleteOutlined /> Delete + <DeleteOutlined /> {t('common.delete')} </span> </Space> </section> diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx index fd02a1b..ead23c4 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx @@ -1,5 +1,6 @@ import { ReactComponent as FilterIcon } from '@/assets/filter.svg'; import { KnowledgeRouteKey } from '@/constants/knowledge'; +import { useTranslate } from '@/hooks/commonHooks'; import { useKnowledgeBaseId } from '@/hooks/knowledgeHook'; import { ArrowLeftOutlined, @@ -49,6 +50,7 @@ const ChunkToolBar = ({ const dispatch = useDispatch(); const knowledgeBaseId = useKnowledgeBaseId(); const [isShowSearchBox, setIsShowSearchBox] = useState(false); + const { t } = useTranslate('chunk'); const handleSelectAllCheck = useCallback( (e: any) => { @@ -95,7 +97,7 @@ const ChunkToolBar = ({ label: ( <> <Checkbox onChange={handleSelectAllCheck} checked={checked}> - <b>Select All</b> + <b>{t('selectAll')}</b> </Checkbox> </> ), @@ -106,7 +108,7 @@ const ChunkToolBar = ({ label: ( <Space onClick={handleEnabledClick}> <CheckCircleOutlined /> - <b>Enabled Selected</b> + <b>{t('enabledSelected')}</b> </Space> ), }, @@ -115,7 +117,7 @@ const ChunkToolBar = ({ label: ( <Space onClick={handleDisabledClick}> <CloseCircleOutlined /> - <b>Disabled Selected</b> + <b>{t('disabledSelected')}</b> </Space> ), }, @@ -125,7 +127,7 @@ const ChunkToolBar = ({ label: ( <Space onClick={handleDelete}> <DeleteOutlined /> - <b>Delete Selected</b> + <b>{t('deleteSelected')}</b> </Space> ), }, @@ -136,6 +138,7 @@ const ChunkToolBar = ({ handleDelete, handleEnabledClick, handleDisabledClick, + t, ]); const content = ( @@ -151,9 +154,9 @@ const ChunkToolBar = ({ const filterContent = ( <Radio.Group onChange={handleFilterChange} value={available}> <Space direction="vertical"> - <Radio value={undefined}>All</Radio> - <Radio value={1}>Enabled</Radio> - <Radio value={0}>Disabled</Radio> + <Radio value={undefined}>{t('all')}</Radio> + <Radio value={1}>{t('enabled')}</Radio> + <Radio value={0}>{t('disabled')}</Radio> </Space> </Radio.Group> ); @@ -172,14 +175,14 @@ const ChunkToolBar = ({ <Space> <Popover content={content} placement="bottom" arrow={false}> <Button> - Bulk + {t('bulk')} <DownOutlined /> </Button> </Popover> {isShowSearchBox ? ( <Input size="middle" - placeholder="Search" + placeholder={t('search')} prefix={<SearchOutlined />} allowClear onChange={handleSearchChange} diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx index d63e70e..dc60291 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx @@ -16,6 +16,7 @@ import { } from './hooks'; import { ChunkModelState } from './model'; +import { useTranslation } from 'react-i18next'; import styles from './index.less'; const Chunk = () => { @@ -33,6 +34,7 @@ const Chunk = () => { const documentInfo = useSelectDocumentInfo(); const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick(); const isPdf = documentInfo.type === 'pdf'; + const { t } = useTranslation(); const getChunkList = useFetchChunkList(); @@ -86,7 +88,7 @@ const Chunk = () => { [], ); const showSelectedChunkWarning = () => { - message.warning('Please select chunk!'); + message.warning(t('message.pleaseSelectChunk')); }; const handleRemoveChunk = useCallback(async () => { diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/model.ts b/web/src/pages/add-knowledge/components/knowledge-chunk/model.ts index 5d9089a..93187b1 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/model.ts +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/model.ts @@ -4,6 +4,7 @@ import kbService from '@/services/kbService'; import { message } from 'antd'; import { pick } from 'lodash'; // import { delay } from '@/utils/storeUtil'; +import i18n from '@/locales/config'; import { DvaModel } from 'umi'; export interface ChunkModelState extends BaseState { @@ -102,7 +103,7 @@ const model: DvaModel<ChunkModelState> = { const { data } = yield call(kbService.switch_chunk, payload); const { retcode } = data; if (retcode === 0) { - message.success('Modified successfully ï¼'); + message.success(i18n.t('message.modified')); } return retcode; }, diff --git a/web/src/pages/add-knowledge/components/knowledge-dataset/knowledge-upload-file/index.tsx b/web/src/pages/add-knowledge/components/knowledge-dataset/knowledge-upload-file/index.tsx index aaff97c..263457a 100644 --- a/web/src/pages/add-knowledge/components/knowledge-dataset/knowledge-upload-file/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-dataset/knowledge-upload-file/index.tsx @@ -40,6 +40,7 @@ import classNames from 'classnames'; import { ReactElement, useCallback, useMemo, useRef, useState } from 'react'; import { Link, useNavigate } from 'umi'; +import { useTranslate } from '@/hooks/commonHooks'; import styles from './index.less'; const { Dragger } = Upload; @@ -135,6 +136,7 @@ const KnowledgeUploadFile = () => { const documentList = useSelectDocumentList(); const runDocumentByIds = useRunDocument(); const uploadDocument = useUploadDocument(); + const { t } = useTranslate('knowledgeDetails'); const enabled = useMemo(() => { if (isUpload) { @@ -257,10 +259,10 @@ const KnowledgeUploadFile = () => { </Flex> <Flex justify="space-around"> <p className={styles.selectFilesText}> - <b>Select files</b> + <b>{t('selectFiles')}</b> </p> <p className={styles.changeSpecificCategoryText}> - <b>Change specific category</b> + <b>{t('changeSpecificCategory')}</b> </p> </Flex> </div> @@ -275,13 +277,8 @@ const KnowledgeUploadFile = () => { <Button className={styles.uploaderButton}> <CloudUploadOutlined className={styles.uploaderIcon} /> </Button> - <p className="ant-upload-text"> - Click or drag file to this area to upload - </p> - <p className="ant-upload-hint"> - Support for a single or bulk upload. Strictly prohibited from - uploading company data or other banned files. - </p> + <p className="ant-upload-text">{t('uploadTitle')}</p> + <p className="ant-upload-hint">{t('uploadDescription')}</p> </Dragger> </section> <section className={styles.footer}> @@ -292,7 +289,7 @@ const KnowledgeUploadFile = () => { disabled={!enabled} size="large" > - Next + {t('next', { keyPrefix: 'common' })} </Button> </section> </div> diff --git a/web/src/pages/add-knowledge/components/knowledge-file/index.tsx b/web/src/pages/add-knowledge/components/knowledge-file/index.tsx index a520a90..096b22d 100644 --- a/web/src/pages/add-knowledge/components/knowledge-file/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-file/index.tsx @@ -204,7 +204,9 @@ const KnowledgeFile = () => { <div className={styles.filter}> <Space> <h3>{t('total', { keyPrefix: 'common' })}</h3> - <Tag color="purple">{total} files</Tag> + <Tag color="purple"> + {total} {t('files')} + </Tag> </Space> <Space> <Input diff --git a/web/src/pages/add-knowledge/components/knowledge-file/model.ts b/web/src/pages/add-knowledge/components/knowledge-file/model.ts index f946c9d..490fa85 100644 --- a/web/src/pages/add-knowledge/components/knowledge-file/model.ts +++ b/web/src/pages/add-knowledge/components/knowledge-file/model.ts @@ -1,5 +1,6 @@ import { BaseState } from '@/interfaces/common'; import { IKnowledgeFile } from '@/interfaces/database/knowledge'; +import i18n from '@/locales/config'; import kbService, { getDocumentFile } from '@/services/kbService'; import { message } from 'antd'; import omit from 'lodash/omit'; @@ -54,14 +55,14 @@ const model: DvaModel<KFModelState> = { const { data } = yield call(kbService.createKb, payload); const { retcode } = data; if (retcode === 0) { - message.success('Created!'); + message.success(i18n.t('message.created')); } }, *updateKf({ payload = {} }, { call }) { const { data } = yield call(kbService.updateKb, payload); const { retcode } = data; if (retcode === 0) { - message.success('Modified!'); + message.success(i18n.t('message.modified')); } }, *getKfDetail({ payload = {} }, { call }) { @@ -109,7 +110,7 @@ const model: DvaModel<KFModelState> = { ); const { retcode } = data; if (retcode === 0) { - message.success('Modified!'); + message.success(i18n.t('message.modified')); yield put({ type: 'getKfList', payload: { kb_id: payload.kb_id }, @@ -122,7 +123,7 @@ const model: DvaModel<KFModelState> = { }); const { retcode } = data; if (retcode === 0) { - message.success('Deleted!'); + message.success(i18n.t('message.deleted')); yield put({ type: 'getKfList', payload: { kb_id: payload.kb_id }, @@ -137,7 +138,7 @@ const model: DvaModel<KFModelState> = { ); const { retcode } = data; if (retcode === 0) { - message.success('rename successï¼'); + message.success(i18n.t('message.renamed')); yield put({ type: 'getKfList', @@ -156,7 +157,7 @@ const model: DvaModel<KFModelState> = { payload: { kb_id: payload.kb_id }, }); - message.success('Created!'); + message.success(i18n.t('message.created')); } return retcode; }, @@ -173,7 +174,7 @@ const model: DvaModel<KFModelState> = { payload: { kb_id: payload.knowledgeBaseId }, }); } - message.success('Operation successfully ï¼'); + message.success(i18n.t('message.operated')); } return retcode; }, @@ -189,7 +190,7 @@ const model: DvaModel<KFModelState> = { payload: { kb_id: payload.kb_id }, }); - message.success('Modified!'); + message.success(i18n.t('message.modified')); } return retcode; }, diff --git a/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx b/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx index aa6c586..d5ca83e 100644 --- a/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx @@ -4,6 +4,7 @@ import { useTranslate } from '@/hooks/commonHooks'; import { IKnowledgeFile } from '@/interfaces/database/knowledge'; import { CloseCircleOutlined } from '@ant-design/icons'; import { Badge, DescriptionsProps, Flex, Popover, Space, Tag } from 'antd'; +import { useTranslation } from 'react-i18next'; import reactStringReplace from 'react-string-replace'; import { useDispatch } from 'umi'; import { RunningStatus, RunningStatusMap } from '../constant'; @@ -80,11 +81,14 @@ export const ParsingStatusCell = ({ record }: IProps) => { const dispatch = useDispatch(); const text = record.run; const runningStatus = RunningStatusMap[text]; + const { t } = useTranslation(); const isRunning = isParserRunning(text); const OperationIcon = iconMap[text]; + const label = t(`knowledgeDetails.runningStatus${text}`); + const handleOperationIconClick = () => { dispatch({ type: 'kFModel/document_run', @@ -103,11 +107,11 @@ export const ParsingStatusCell = ({ record }: IProps) => { {isRunning ? ( <Space> <Badge color={runningStatus.color} /> - {runningStatus.label} + {label} <span>{(record.progress * 100).toFixed(2)}%</span> </Space> ) : ( - runningStatus.label + label )} </Tag> </Popover> diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/category-panel.tsx b/web/src/pages/add-knowledge/components/knowledge-setting/category-panel.tsx index 8b6168e..d7f6a6c 100644 --- a/web/src/pages/add-knowledge/components/knowledge-setting/category-panel.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-setting/category-panel.tsx @@ -1,25 +1,27 @@ import SvgIcon from '@/components/svg-icon'; +import { useTranslate } from '@/hooks/commonHooks'; import { useSelectParserList } from '@/hooks/userSettingHook'; import { Col, Divider, Empty, Row, Typography } from 'antd'; import { useMemo } from 'react'; import styles from './index.less'; -import { ImageMap, TextMap } from './utils'; +import { ImageMap } from './utils'; const { Title, Text } = Typography; const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => { const parserList = useSelectParserList(); + const { t } = useTranslate('knowledgeConfiguration'); const item = useMemo(() => { const item = parserList.find((x) => x.value === chunkMethod); if (item) { return { title: item.label, - description: TextMap[item.value as keyof typeof TextMap]?.description, + description: t(item.value), }; } return { title: '', description: '' }; - }, [parserList, chunkMethod]); + }, [parserList, chunkMethod, t]); const imageList = useMemo(() => { if (chunkMethod in ImageMap) { @@ -33,18 +35,17 @@ const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => { {imageList.length > 0 ? ( <> <Title level={5} className={styles.topTitle}> - "{item.title}" Chunking Method Description + "{item.title}" {t('methodTitle')} </Title> <p dangerouslySetInnerHTML={{ __html: item.description, }} ></p> - <Title level={5}>"{item.title}" Examples</Title> - <Text> - This visual guides is in order to make understanding easier - for you. - </Text> + <Title level={5}> + "{item.title}" {t('methodExamples')} + </Title> + <Text>{t('methodExamplesDescription')}</Text> <Row gutter={[10, 10]} className={styles.imageRow}> {imageList.map((x) => ( <Col span={12} key={x}> @@ -56,15 +57,14 @@ const CategoryPanel = ({ chunkMethod }: { chunkMethod: string }) => { </Col> ))} </Row> - <Title level={5}>{item.title} Dialogue Examples</Title> + <Title level={5}> + {item.title} {t('dialogueExamplesTitle')} + </Title> <Divider></Divider> </> ) : ( <Empty description={''} image={null}> - <p> - This will display a visual explanation of the knowledge base - categories - </p> + <p>{t('methodEmpty')}</p> <SvgIcon name={'chunk-method/chunk-empty'} width={'100%'}></SvgIcon> </Empty> )} diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx b/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx index b55164c..e50156e 100644 --- a/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx @@ -59,7 +59,7 @@ const ConfigurationForm = ({ form }: { form: FormInstance }) => { </Form.Item> <Form.Item name="permission" - label="Permissions" + label={t('permissions')} tooltip={t('permissionsTip')} rules={[{ required: true }]} > @@ -70,7 +70,7 @@ const ConfigurationForm = ({ form }: { form: FormInstance }) => { </Form.Item> <Form.Item name="embd_id" - label="Embedding model" + label={t('embeddingModel')} rules={[{ required: true }]} tooltip={t('embeddingModelTip')} > @@ -82,7 +82,7 @@ const ConfigurationForm = ({ form }: { form: FormInstance }) => { </Form.Item> <Form.Item name="parser_id" - label="Chunk method" + label={t('chunkMethod')} tooltip={t('chunkMethodTip')} rules={[{ required: true }]} > diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/model.ts b/web/src/pages/add-knowledge/components/knowledge-setting/model.ts index a24aea8..a2eb9e1 100644 --- a/web/src/pages/add-knowledge/components/knowledge-setting/model.ts +++ b/web/src/pages/add-knowledge/components/knowledge-setting/model.ts @@ -1,4 +1,5 @@ import { IKnowledge } from '@/interfaces/database/knowledge'; +import i18n from '@/locales/config'; import kbService from '@/services/kbService'; import { message } from 'antd'; import { DvaModel } from 'umi'; @@ -32,7 +33,7 @@ const model: DvaModel<KSModelState> = { const { data } = yield call(kbService.createKb, payload); const { retcode } = data; if (retcode === 0) { - message.success('Created!'); + message.success(i18n.t('message.created')); } return data; }, @@ -41,7 +42,7 @@ const model: DvaModel<KSModelState> = { const { retcode } = data; if (retcode === 0) { yield put({ type: 'getKbDetail', payload: { kb_id: payload.kb_id } }); - message.success('Updated!'); + message.success(i18n.t('message.updated')); } }, *getKbDetail({ payload = {} }, { call, put }) { diff --git a/web/src/pages/add-knowledge/index.tsx b/web/src/pages/add-knowledge/index.tsx index abe0e2e..68df603 100644 --- a/web/src/pages/add-knowledge/index.tsx +++ b/web/src/pages/add-knowledge/index.tsx @@ -14,7 +14,6 @@ import { KnowledgeDatasetRouteKey, KnowledgeRouteKey, datasetRouteMap, - routeMap, } from './constant'; import styles from './index.less'; @@ -49,7 +48,7 @@ const KnowledgeAdding = () => { {t(`knowledgeDetails.${activeKey}`)} </Link> ) : ( - routeMap[activeKey] + t(`knowledgeDetails.${activeKey}`) ), }, ]; diff --git a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx index bca69b6..4dfdfe6 100644 --- a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx +++ b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx @@ -4,6 +4,7 @@ import { Form, Input, Select, Upload } from 'antd'; import classNames from 'classnames'; import { ISegmentedContentProps } from '../interface'; +import { useTranslate } from '@/hooks/commonHooks'; import styles from './index.less'; const AssistantSetting = ({ show }: ISegmentedContentProps) => { @@ -12,6 +13,7 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => { label: x.name, value: x.id, })); + const { t } = useTranslate('chat'); const normFile = (e: any) => { if (Array.isArray(e)) { @@ -28,14 +30,14 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => { > <Form.Item name={'name'} - label="Assistant name" + label={t('assistantName')} rules={[{ required: true }]} > <Input placeholder="e.g. Resume Jarvis" /> </Form.Item> <Form.Item name="icon" - label="Assistant avatar" + label={t('assistantAvatar')} valuePropName="fileList" getValueFromEvent={normFile} > @@ -46,44 +48,45 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => { > <button style={{ border: 0, background: 'none' }} type="button"> <PlusOutlined /> - <div style={{ marginTop: 8 }}>Upload</div> + <div style={{ marginTop: 8 }}> + {t('upload', { keyPrefix: 'common' })} + </div> </button> </Upload> </Form.Item> <Form.Item name={'language'} - label="Language" + label={t('language')} initialValue={'Chinese'} tooltip="coming soon" - style={{display:'none'}} + style={{ display: 'none' }} > <Select options={[ - { value: 'Chinese', label: 'Chinese' }, - { value: 'English', label: 'English' }, + { value: 'Chinese', label: t('chinese', { keyPrefix: 'common' }) }, + { value: 'English', label: t('english', { keyPrefix: 'common' }) }, ]} /> </Form.Item> <Form.Item name={['prompt_config', 'empty_response']} - label="Empty response" - tooltip="If nothing is retrieved with user's question in the knowledgebase, it will use this as an answer. - If you want LLM comes up with its own opinion when nothing is retrieved, leave this blank." + label={t('emptyResponse')} + tooltip={t('emptyResponseTip')} > <Input placeholder="" /> </Form.Item> <Form.Item name={['prompt_config', 'prologue']} - label="Set an opener" - tooltip="How do you want to welcome your clients?" - initialValue={"Hi! I'm your assistant, what can I do for you?"} + label={t('setAnOpener')} + tooltip={t('setAnOpenerTip')} + initialValue={t('setAnOpenerInitial')} > <Input.TextArea autoSize={{ minRows: 5 }} /> </Form.Item> <Form.Item - label="Knowledgebases" + label={t('knowledgeBases')} name="kb_ids" - tooltip="Select knowledgebases associated." + tooltip={t('knowledgeBasesTip')} rules={[ { required: true, @@ -95,7 +98,7 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => { <Select mode="multiple" options={knowledgeOptions} - placeholder="Please select" + placeholder={t('knowledgeBasesMessage')} ></Select> </Form.Item> </section> diff --git a/web/src/pages/chat/chat-configuration-modal/index.tsx b/web/src/pages/chat/chat-configuration-modal/index.tsx index 91281e5..8ddec39 100644 --- a/web/src/pages/chat/chat-configuration-modal/index.tsx +++ b/web/src/pages/chat/chat-configuration-modal/index.tsx @@ -7,6 +7,7 @@ import { import { IDialog } from '@/interfaces/database/chat'; import { Divider, Flex, Form, Modal, Segmented, UploadFile } from 'antd'; import { SegmentedValue } from 'antd/es/segmented'; +import camelCase from 'lodash/camelCase'; import omit from 'lodash/omit'; import { useEffect, useRef, useState } from 'react'; import { variableEnabledFieldMap } from '../constants'; @@ -17,20 +18,9 @@ import { useFetchModelId } from './hooks'; import ModelSetting from './model-setting'; import PromptEngine from './prompt-engine'; +import { useTranslate } from '@/hooks/commonHooks'; import styles from './index.less'; -enum ConfigurationSegmented { - AssistantSetting = 'Assistant Setting', - PromptEngine = 'Prompt Engine', - ModelSetting = 'Model Setting', -} - -const segmentedMap = { - [ConfigurationSegmented.AssistantSetting]: AssistantSetting, - [ConfigurationSegmented.ModelSetting]: ModelSetting, - [ConfigurationSegmented.PromptEngine]: PromptEngine, -}; - const layout = { labelCol: { span: 7 }, wrapperCol: { span: 17 }, @@ -47,6 +37,18 @@ const validateMessages = { }, }; +enum ConfigurationSegmented { + AssistantSetting = 'Assistant Setting', + PromptEngine = 'Prompt Engine', + ModelSetting = 'Model Setting', +} + +const segmentedMap = { + [ConfigurationSegmented.AssistantSetting]: AssistantSetting, + [ConfigurationSegmented.ModelSetting]: ModelSetting, + [ConfigurationSegmented.PromptEngine]: PromptEngine, +}; + interface IProps extends IModalManagerChildrenProps { initialDialog: IDialog; loading: boolean; @@ -63,11 +65,13 @@ const ChatConfigurationModal = ({ clearDialog, }: IProps) => { const [form] = Form.useForm(); + const [value, setValue] = useState<ConfigurationSegmented>( ConfigurationSegmented.AssistantSetting, ); const promptEngineRef = useRef<Array<IPromptConfigParameters>>([]); const modelId = useFetchModelId(visible); + const { t } = useTranslate('chat'); const handleOk = async () => { const values = await form.validateFields(); @@ -115,10 +119,9 @@ const ChatConfigurationModal = ({ <Flex gap={16}> <ChatConfigurationAtom></ChatConfigurationAtom> <div> - <b>Chat Configuration</b> + <b>{t('chatConfiguration')}</b> <div className={styles.chatConfigurationDescription}> - Here, dress up a dedicated assistant for your special knowledge bases! - 💕 + {t('chatConfigurationDescription')} </div> </div> </Flex> @@ -158,7 +161,10 @@ const ChatConfigurationModal = ({ size={'large'} value={value} onChange={handleSegmentedChange} - options={Object.values(ConfigurationSegmented)} + options={Object.values(ConfigurationSegmented).map((x) => ({ + label: t(camelCase(x)), + value: x, + }))} block /> <Divider></Divider> diff --git a/web/src/pages/chat/chat-configuration-modal/model-setting.tsx b/web/src/pages/chat/chat-configuration-modal/model-setting.tsx index 87390ab..6f2ec77 100644 --- a/web/src/pages/chat/chat-configuration-modal/model-setting.tsx +++ b/web/src/pages/chat/chat-configuration-modal/model-setting.tsx @@ -5,16 +5,19 @@ import { } from '@/constants/knowledge'; import { Divider, Flex, Form, InputNumber, Select, Slider, Switch } from 'antd'; import classNames from 'classnames'; +import camelCase from 'lodash/camelCase'; import { useEffect } from 'react'; import { ISegmentedContentProps } from '../interface'; +import { useTranslate } from '@/hooks/commonHooks'; import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks'; import { variableEnabledFieldMap } from '../constants'; import styles from './index.less'; const ModelSetting = ({ show, form }: ISegmentedContentProps) => { + const { t } = useTranslate('chat'); const parameterOptions = Object.values(ModelVariableType).map((x) => ({ - label: x, + label: t(camelCase(x)), value: x, })); @@ -44,18 +47,18 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { })} > <Form.Item - label="Model" + label={t('model')} name="llm_id" - tooltip="Large language chat model" - rules={[{ required: true, message: 'Please select!' }]} + tooltip={t('modelTip')} + rules={[{ required: true, message: t('modelMessage') }]} > <Select options={modelOptions} showSearch /> </Form.Item> <Divider></Divider> <Form.Item - label="Freedom" + label={t('freedom')} name="parameters" - tooltip="'Precise' means the LLM will be conservative and answer your question cautiously. 'Improvise' means the you want LLM talk much and freely. 'Balance' is between cautiously and freely." + tooltip={t('freedomTip')} initialValue={ModelVariableType.Precise} // rules={[{ required: true, message: 'Please input!' }]} > @@ -64,7 +67,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { onChange={handleParametersChange} /> </Form.Item> - <Form.Item label="Temperature" tooltip={'This parameter controls the randomness of predictions by the model. A lower temperature makes the model more confident in its responses, while a higher temperature makes it more creative and diverse.'}> + <Form.Item label={t('temperature')} tooltip={t('temperatureTip')}> <Flex gap={20} align="center"> <Form.Item name={'temperatureEnabled'} @@ -77,7 +80,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { <Form.Item name={['llm_setting', 'temperature']} noStyle - rules={[{ required: true, message: 'Temperature is required' }]} + rules={[{ required: true, message: t('temperatureMessage') }]} > <Slider className={styles.variableSlider} max={1} step={0.01} /> </Form.Item> @@ -85,7 +88,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { <Form.Item name={['llm_setting', 'temperature']} noStyle - rules={[{ required: true, message: 'Temperature is required' }]} + rules={[{ required: true, message: t('temperatureMessage') }]} > <InputNumber className={styles.sliderInputNumber} @@ -96,7 +99,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { </Form.Item> </Flex> </Form.Item> - <Form.Item label="Top P" tooltip={'Also known as “nucleus sampling,†this parameter sets a threshold to select a smaller set of words to sample from. It focuses on the most likely words, cutting off the less probable ones.'}> + <Form.Item label={t('topP')} tooltip={t('topPTip')}> <Flex gap={20} align="center"> <Form.Item name={'topPEnabled'} valuePropName="checked" noStyle> <Switch size="small" /> @@ -105,7 +108,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { <Form.Item name={['llm_setting', 'top_p']} noStyle - rules={[{ required: true, message: 'Top_p is required' }]} + rules={[{ required: true, message: t('topPMessage') }]} > <Slider className={styles.variableSlider} max={1} step={0.01} /> </Form.Item> @@ -113,7 +116,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { <Form.Item name={['llm_setting', 'top_p']} noStyle - rules={[{ required: true, message: 'Top_p is required' }]} + rules={[{ required: true, message: t('topPMessage') }]} > <InputNumber className={styles.sliderInputNumber} @@ -124,7 +127,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { </Form.Item> </Flex> </Form.Item> - <Form.Item label="Presence Penalty" tooltip={'This discourages the model from repeating the same information by penalizing words that have already appeared in the conversation.'}> + <Form.Item label={t('presencePenalty')} tooltip={t('presencePenaltyTip')}> <Flex gap={20} align="center"> <Form.Item name={'presencePenaltyEnabled'} @@ -137,9 +140,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { <Form.Item name={['llm_setting', 'presence_penalty']} noStyle - rules={[ - { required: true, message: 'Presence Penalty is required' }, - ]} + rules={[{ required: true, message: t('presencePenaltyMessage') }]} > <Slider className={styles.variableSlider} max={1} step={0.01} /> </Form.Item> @@ -147,9 +148,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { <Form.Item name={['llm_setting', 'presence_penalty']} noStyle - rules={[ - { required: true, message: 'Presence Penalty is required' }, - ]} + rules={[{ required: true, message: t('presencePenaltyMessage') }]} > <InputNumber className={styles.sliderInputNumber} @@ -160,7 +159,10 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { </Form.Item> </Flex> </Form.Item> - <Form.Item label="Frequency Penalty" tooltip={'Similar to the presence penalty, this reduces the model’s tendency to repeat the same words frequently.'}> + <Form.Item + label={t('frequencyPenalty')} + tooltip={t('frequencyPenaltyTip')} + > <Flex gap={20} align="center"> <Form.Item name={'frequencyPenaltyEnabled'} @@ -174,7 +176,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { name={['llm_setting', 'frequency_penalty']} noStyle rules={[ - { required: true, message: 'Frequency Penalty is required' }, + { required: true, message: t('frequencyPenaltyMessage') }, ]} > <Slider className={styles.variableSlider} max={1} step={0.01} /> @@ -183,9 +185,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { <Form.Item name={['llm_setting', 'frequency_penalty']} noStyle - rules={[ - { required: true, message: 'Frequency Penalty is required' }, - ]} + rules={[{ required: true, message: t('frequencyPenaltyMessage') }]} > <InputNumber className={styles.sliderInputNumber} @@ -196,7 +196,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { </Form.Item> </Flex> </Form.Item> - <Form.Item label="Max Tokens" tooltip={'This sets the maximum length of the model’s output, measured in the number of tokens (words or pieces of words).'}> + <Form.Item label={t('maxTokens')} tooltip={t('maxTokensTip')}> <Flex gap={20} align="center"> <Form.Item name={'maxTokensEnabled'} valuePropName="checked" noStyle> <Switch size="small" /> @@ -205,7 +205,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { <Form.Item name={['llm_setting', 'max_tokens']} noStyle - rules={[{ required: true, message: 'Max Tokens is required' }]} + rules={[{ required: true, message: t('maxTokensMessage') }]} > <Slider className={styles.variableSlider} max={2048} /> </Form.Item> @@ -213,7 +213,7 @@ const ModelSetting = ({ show, form }: ISegmentedContentProps) => { <Form.Item name={['llm_setting', 'max_tokens']} noStyle - rules={[{ required: true, message: 'Max Tokens is required' }]} + rules={[{ required: true, message: t('maxTokensMessage') }]} > <InputNumber className={styles.sliderInputNumber} diff --git a/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx b/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx index 1fc6e10..1e49bc9 100644 --- a/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx +++ b/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx @@ -29,6 +29,7 @@ import { } from '../interface'; import { EditableCell, EditableRow } from './editable-cell'; +import { useTranslate } from '@/hooks/commonHooks'; import { useSelectPromptConfigParameters } from '../hooks'; import styles from './index.less'; @@ -44,6 +45,7 @@ const PromptEngine = ( ) => { const [dataSource, setDataSource] = useState<DataType[]>([]); const parameters = useSelectPromptConfigParameters(); + const { t } = useTranslate('chat'); const components = { body: { @@ -102,7 +104,7 @@ const PromptEngine = ( const columns: TableProps<DataType>['columns'] = [ { - title: 'key', + title: t('key'), dataIndex: 'variable', key: 'variable', onCell: (record: DataType) => ({ @@ -114,7 +116,7 @@ const PromptEngine = ( }), }, { - title: 'optional', + title: t('optional'), dataIndex: 'optional', key: 'optional', width: 40, @@ -130,7 +132,7 @@ const PromptEngine = ( }, }, { - title: 'operation', + title: t('operation'), dataIndex: 'operation', width: 30, key: 'operation', @@ -152,24 +154,21 @@ const PromptEngine = ( })} > <Form.Item - label="System" - rules={[{ required: true, message: 'Please input!' }]} - tooltip="Instructions you need LLM to follow when LLM answers questions, like charactor design, answer length and answer language etc." + label={t('system')} + rules={[{ required: true, message: t('systemMessage') }]} + tooltip={t('systemTip')} name={['prompt_config', 'system']} - initialValue={`ä½ æ˜¯ä¸€ä¸ªæ™ºèƒ½åŠ©æ‰‹ï¼Œè¯·æ€»ç»“çŸ¥è¯†åº“çš„å†…å®¹æ¥å›žç”问题,请列举知识库ä¸çš„æ•°æ®è¯¦ç»†å›žç”ã€‚å½“æ‰€æœ‰çŸ¥è¯†åº“å†…å®¹éƒ½ä¸Žé—®é¢˜æ— å…³æ—¶ï¼Œä½ çš„å›žç”必须包括“知识库ä¸æœªæ‰¾åˆ°æ‚¨è¦çš„ç”案ï¼â€è¿™å¥è¯ã€‚回ç”需è¦è€ƒè™‘èŠå¤©åŽ†å²ã€‚ - 以下是知识库: - {knowledge} - 以上是知识库。`} + initialValue={t('systemInitialValue')} > <Input.TextArea autoSize={{ maxRows: 8, minRows: 5 }} /> </Form.Item> <Divider></Divider> <SimilaritySlider isTooltipShown></SimilaritySlider> <Form.Item<FieldType> - label="Top N" + label={t('topN')} name={'top_n'} initialValue={8} - tooltip={`Not all the chunks whose similarity score is above the 'simialrity threashold' will be feed to LLMs. LLM can only see these 'Top N' chunks.`} + tooltip={t('topNTip')} > <Slider max={30} /> </Form.Item> @@ -177,18 +176,15 @@ const PromptEngine = ( <Row align={'middle'} justify="end"> <Col span={7} className={styles.variableAlign}> <label className={styles.variableLabel}> - Variables - <Tooltip title="If you use dialog APIs, the varialbes might help you chat with your clients with different strategies. - The variables are used to fill-in the 'System' part in prompt in order to give LLM a hint. - The 'knowledge' is a very special variable which will be filled-in with the retrieved chunks. - All the variables in 'System' should be curly bracketed."> + {t('variable')} + <Tooltip title={t('variableTip')}> <QuestionCircleOutlined className={styles.variableIcon} /> </Tooltip> </label> </Col> <Col span={17} className={styles.variableAlign}> <Button size="small" onClick={handleAdd}> - Add + {t('add')} </Button> </Col> </Row> diff --git a/web/src/pages/chat/chat-container/index.tsx b/web/src/pages/chat/chat-container/index.tsx index 887e92f..128bbe6 100644 --- a/web/src/pages/chat/chat-container/index.tsx +++ b/web/src/pages/chat/chat-container/index.tsx @@ -37,6 +37,7 @@ import { } from '../hooks'; import SvgIcon from '@/components/svg-icon'; +import { useTranslate } from '@/hooks/commonHooks'; import { getExtension, isPdf } from '@/utils/documentUtils'; import styles from './index.less'; @@ -298,6 +299,7 @@ const ChatContainer = () => { const disabled = useGetSendButtonDisabled(); useGetFileIcon(); const loading = useSelectConversationLoading(); + const { t } = useTranslate('chat'); return ( <> @@ -328,7 +330,7 @@ const ChatContainer = () => { </Flex> <Input size="large" - placeholder="Message Resume Assistant..." + placeholder={t('sendPlaceholder')} value={value} disabled={disabled} suffix={ @@ -338,7 +340,7 @@ const ChatContainer = () => { loading={sendLoading} disabled={disabled} > - Send + {t('send')} </Button> } onPressEnter={handlePressEnter} diff --git a/web/src/pages/chat/index.tsx b/web/src/pages/chat/index.tsx index 1839140..ba18215 100644 --- a/web/src/pages/chat/index.tsx +++ b/web/src/pages/chat/index.tsx @@ -35,6 +35,7 @@ import { useSelectFirstDialogOnMount, } from './hooks'; +import { useTranslate } from '@/hooks/commonHooks'; import styles from './index.less'; const Chat = () => { @@ -71,6 +72,7 @@ const Chat = () => { } = useEditDialog(); const dialogLoading = useSelectDialogListLoading(); const conversationLoading = useSelectConversationListLoading(); + const { t } = useTranslate('chat'); useFetchDialogOnMount(dialogId, true); @@ -132,7 +134,8 @@ const Chat = () => { onClick: handleCreateTemporaryConversation, label: ( <Space> - <EditOutlined /> New chat + <EditOutlined /> + {t('newChat')} </Space> ), }, @@ -146,7 +149,7 @@ const Chat = () => { label: ( <Space> <EditOutlined /> - Edit + {t('edit', { keyPrefix: 'common' })} </Space> ), }, @@ -157,7 +160,7 @@ const Chat = () => { label: ( <Space> <DeleteOutlined /> - Delete chat + {t('delete', { keyPrefix: 'common' })} </Space> ), }, @@ -250,7 +253,7 @@ const Chat = () => { className={styles.chatTitle} > <Space> - <b>Chat</b> + <b>{t('chat')}</b> <Tag>{conversationList.length}</Tag> </Space> <Dropdown menu={{ items }}> diff --git a/web/src/pages/chat/model.ts b/web/src/pages/chat/model.ts index 22bf7b4..1e94be0 100644 --- a/web/src/pages/chat/model.ts +++ b/web/src/pages/chat/model.ts @@ -1,4 +1,5 @@ import { IConversation, IDialog, Message } from '@/interfaces/database/chat'; +import i18n from '@/locales/config'; import chatService from '@/services/chatService'; import { message } from 'antd'; import { DvaModel } from 'umi'; @@ -77,7 +78,9 @@ const model: DvaModel<ChatModelState> = { const { data } = yield call(chatService.setDialog, payload); if (data.retcode === 0) { yield put({ type: 'listDialog' }); - message.success(payload.dialog_id ? 'Modified!' : 'Created!'); + message.success( + i18n.t(`message.${payload.dialog_id ? 'modified' : 'created'}`), + ); } return data.retcode; }, @@ -85,7 +88,7 @@ const model: DvaModel<ChatModelState> = { const { data } = yield call(chatService.removeDialog, payload); if (data.retcode === 0) { yield put({ type: 'listDialog' }); - message.success('Deleted successfully !'); + message.success(i18n.t('message.deleted')); } return data.retcode; }, @@ -152,7 +155,7 @@ const model: DvaModel<ChatModelState> = { type: 'listConversation', payload: { dialog_id: payload.dialog_id }, }); - message.success('Deleted successfully !'); + message.success(i18n.t('message.deleted')); } return data.retcode; }, diff --git a/web/src/pages/login/index.tsx b/web/src/pages/login/index.tsx index 1de7e93..98700dc 100644 --- a/web/src/pages/login/index.tsx +++ b/web/src/pages/login/index.tsx @@ -75,7 +75,7 @@ const Login = () => { <div className={styles.loginLeft}> <div className={styles.leftContainer}> <div className={styles.loginTitle}> - <div>{title === 'login' ? t('login') : 'Create an account'}</div> + <div>{title === 'login' ? t('login') : t('register')}</div> <span> {title === 'login' ? t('loginDescription') diff --git a/web/src/pages/login/model.ts b/web/src/pages/login/model.ts index eaa92fb..38865b1 100644 --- a/web/src/pages/login/model.ts +++ b/web/src/pages/login/model.ts @@ -1,4 +1,5 @@ import { Authorization } from '@/constants/authorization'; +import i18n from '@/locales/config'; import userService from '@/services/userService'; import authorizationUtil from '@/utils/authorizationUtil'; import { message } from 'antd'; @@ -31,7 +32,7 @@ const model: DvaModel<LoginModelState> = { const { retcode, data: res } = data; const authorization = response.headers.get(Authorization); if (retcode === 0) { - message.success('logged!'); + message.success(i18n.t('message.logged')); const token = res.access_token; const userInfo = { avatar: res.avatar, @@ -51,7 +52,7 @@ const model: DvaModel<LoginModelState> = { console.log(); const { retcode } = data; if (retcode === 0) { - message.success('Registered!'); + message.success(i18n.t('message.registered')); } return retcode; }, @@ -59,7 +60,7 @@ const model: DvaModel<LoginModelState> = { const { data } = yield call(userService.logout, payload); const { retcode } = data; if (retcode === 0) { - message.success('logout'); + message.success(i18n.t('message.logout')); } return retcode; }, diff --git a/web/src/pages/login/right-panel.tsx b/web/src/pages/login/right-panel.tsx index f407256..9643513 100644 --- a/web/src/pages/login/right-panel.tsx +++ b/web/src/pages/login/right-panel.tsx @@ -3,11 +3,13 @@ import SvgIcon from '@/components/svg-icon'; import { Flex, Rate, Space, Typography } from 'antd'; import classNames from 'classnames'; +import { useTranslate } from '@/hooks/commonHooks'; import styles from './index.less'; const { Title, Text } = Typography; const LoginRightPanel = () => { + const { t } = useTranslate('login'); return ( <section className={styles.rightPanel}> <SvgIcon name="login-star" width={80}></SvgIcon> @@ -16,11 +18,10 @@ const LoginRightPanel = () => { level={1} className={classNames(styles.white, styles.loginTitle)} > - Start building your smart assisstants. + {t('title')} </Title> <Text className={classNames(styles.pink, styles.loginDescription)}> - Sign up for free to explore top RAG technology. Create knowledge bases - and AIs to empower your business. + {t('description')} </Text> <Flex align="center" gap={16}> <Avatars></Avatars> @@ -34,7 +35,7 @@ const LoginRightPanel = () => { </span> </Space> <span className={classNames(styles.pink, styles.loginRateReviews)}> - from 500+ reviews + {t('review')} </span> </Flex> </Flex> diff --git a/web/src/pages/user-setting/components/setting-title/index.tsx b/web/src/pages/user-setting/components/setting-title/index.tsx index 8ad153f..f5f8a79 100644 --- a/web/src/pages/user-setting/components/setting-title/index.tsx +++ b/web/src/pages/user-setting/components/setting-title/index.tsx @@ -1,3 +1,4 @@ +import { useTranslate } from '@/hooks/commonHooks'; import { SettingOutlined } from '@ant-design/icons'; import { Button, Flex, Typography } from 'antd'; @@ -16,6 +17,8 @@ const SettingTitle = ({ clickButton, showRightButton = false, }: IProps) => { + const { t } = useTranslate('setting'); + return ( <Flex align="center" justify={'space-between'}> <div> @@ -24,7 +27,7 @@ const SettingTitle = ({ </div> {showRightButton && ( <Button type={'primary'} onClick={clickButton}> - <SettingOutlined></SettingOutlined> System Model Settings + <SettingOutlined></SettingOutlined> {t('systemModelSettings')} </Button> )} </Flex> diff --git a/web/src/pages/user-setting/model.ts b/web/src/pages/user-setting/model.ts index 0caa12f..5fffd8f 100644 --- a/web/src/pages/user-setting/model.ts +++ b/web/src/pages/user-setting/model.ts @@ -5,6 +5,7 @@ import { IThirdOAIModelCollection as IThirdAiModelCollection, } from '@/interfaces/database/llm'; import { IUserInfo } from '@/interfaces/database/userSetting'; +import i18n from '@/locales/config'; import userService from '@/services/userService'; import { message } from 'antd'; import { DvaModel } from 'umi'; @@ -47,7 +48,8 @@ const model: DvaModel<SettingModelState> = { const { data } = yield call(userService.setting, payload); const { retcode } = data; if (retcode === 0) { - message.success('Modified!'); + message.success(i18n.t('message.modified')); + yield put({ type: 'getUserInfo', }); @@ -89,7 +91,8 @@ const model: DvaModel<SettingModelState> = { const { data } = yield call(userService.set_tenant_info, payload); const { retcode } = data; if (retcode === 0) { - message.success('Modified!'); + message.success(i18n.t('message.modified')); + yield put({ type: 'getTenantInfo', }); @@ -137,7 +140,8 @@ const model: DvaModel<SettingModelState> = { const { data } = yield call(userService.set_api_key, payload); const { retcode } = data; if (retcode === 0) { - message.success('Modified!'); + message.success(i18n.t('message.modified')); + yield put({ type: 'my_llm' }); yield put({ type: 'factories_list' }); yield put({ diff --git a/web/src/pages/user-setting/setting-model/api-key-modal/index.tsx b/web/src/pages/user-setting/setting-model/api-key-modal/index.tsx index e4270e4..7a48507 100644 --- a/web/src/pages/user-setting/setting-model/api-key-modal/index.tsx +++ b/web/src/pages/user-setting/setting-model/api-key-modal/index.tsx @@ -1,4 +1,5 @@ import { IModalManagerChildrenProps } from '@/components/modal-manager'; +import { useTranslate } from '@/hooks/commonHooks'; import { Form, Input, Modal } from 'antd'; import { useEffect } from 'react'; @@ -24,6 +25,7 @@ const ApiKeyModal = ({ onOk, }: IProps) => { const [form] = Form.useForm(); + const { t } = useTranslate('setting'); const handleOk = async () => { const ret = await form.validateFields(); @@ -49,7 +51,7 @@ const ApiKeyModal = ({ return ( <Modal - title="Modify" + title={t('modify')} open={visible} onOk={handleOk} onCancel={handleCancel} @@ -67,18 +69,18 @@ const ApiKeyModal = ({ form={form} > <Form.Item<FieldType> - label="Api-Key" + label={t('apiKey')} name="api_key" - tooltip="The API key can be obtained by registering the corresponding LLM supplier." - rules={[{ required: true, message: 'Please input api key!' }]} + tooltip={t('apiKeyTip')} + rules={[{ required: true, message: t('apiKeyMessage') }]} > <Input /> </Form.Item> {llmFactory === 'OpenAI' && ( <Form.Item<FieldType> - label="Base-Url" + label={t('baseUrl')} name="base_url" - tooltip="If your API key is from OpenAI, just ignore it. Any other intermediate providers will give this base url with the API key." + tooltip={t('baseUrlTip')} > <Input placeholder="https://api.openai.com/v1" /> </Form.Item> diff --git a/web/src/pages/user-setting/setting-model/index.tsx b/web/src/pages/user-setting/setting-model/index.tsx index 4c8d2e8..03dd050 100644 --- a/web/src/pages/user-setting/setting-model/index.tsx +++ b/web/src/pages/user-setting/setting-model/index.tsx @@ -1,5 +1,5 @@ import { ReactComponent as MoreModelIcon } from '@/assets/svg/more-model.svg'; -import { useSetModalState } from '@/hooks/commonHooks'; +import { useSetModalState, useTranslate } from '@/hooks/commonHooks'; import { LlmItem, useFetchLlmFactoryListOnMount, @@ -64,6 +64,7 @@ interface IModelCardProps { const ModelCard = ({ item, clickApiKey }: IModelCardProps) => { const { visible, switchVisible } = useSetModalState(); + const { t } = useTranslate('setting'); const handleApiKeyClick = () => { clickApiKey(item.name); @@ -94,7 +95,7 @@ const ModelCard = ({ item, clickApiKey }: IModelCardProps) => { </Button> <Button onClick={handleShowMoreClick}> <Flex gap={'small'}> - Show more models + {t('showMoreModels')} <MoreModelIcon /> </Flex> </Button> @@ -140,6 +141,7 @@ const UserSettingModel = () => { hideSystemSettingModal, showSystemSettingModal, } = useSubmitSystemModelSetting(); + const { t } = useTranslate('setting'); const handleApiKeyClick = useCallback( (llmFactory: string) => { @@ -155,7 +157,7 @@ const UserSettingModel = () => { const items: CollapseProps['items'] = [ { key: '1', - label: 'Added models', + label: t('addedModels'), children: ( <List grid={{ gutter: 16, column: 1 }} @@ -168,7 +170,7 @@ const UserSettingModel = () => { }, { key: '2', - label: 'Models to be added', + label: t('modelsToBeAdded'), children: ( <List grid={{ @@ -193,7 +195,7 @@ const UserSettingModel = () => { </Flex> <Divider></Divider> <Button type="link" onClick={handleAddModel(item.name)}> - Add the model + {t('addTheModel')} </Button> </Card> </List.Item> @@ -208,8 +210,8 @@ const UserSettingModel = () => { <Spin spinning={loading}> <section className={styles.modelWrapper}> <SettingTitle - title="Model Setting" - description="Manage your account settings and preferences here." + title={t('model')} + description={t('profileDescription')} showRightButton clickButton={showSystemSettingModal} ></SettingTitle> diff --git a/web/src/pages/user-setting/setting-model/system-model-setting-modal/index.tsx b/web/src/pages/user-setting/setting-model/system-model-setting-modal/index.tsx index 1deb482..53ee206 100644 --- a/web/src/pages/user-setting/setting-model/system-model-setting-modal/index.tsx +++ b/web/src/pages/user-setting/setting-model/system-model-setting-modal/index.tsx @@ -1,5 +1,6 @@ import { IModalManagerChildrenProps } from '@/components/modal-manager'; import { LlmModelType } from '@/constants/knowledge'; +import { useTranslate } from '@/hooks/commonHooks'; import { ISystemModelSettingSavingParams } from '@/hooks/llmHooks'; import { Form, Modal, Select } from 'antd'; import { useEffect } from 'react'; @@ -21,6 +22,7 @@ const SystemModelSettingModal = ({ const [form] = Form.useForm(); const { systemSetting: initialValues, allOptions } = useFetchSystemModelSettingOnMount(visible); + const { t } = useTranslate('setting'); const handleOk = async () => { const values = await form.validateFields(); @@ -35,7 +37,7 @@ const SystemModelSettingModal = ({ return ( <Modal - title="System Model Settings" + title={t('systemModelSettings')} open={visible} onOk={handleOk} onCancel={hideModal} @@ -43,25 +45,32 @@ const SystemModelSettingModal = ({ confirmLoading={loading} > <Form form={form} onValuesChange={onFormLayoutChange} layout={'vertical'}> - - <Form.Item label="Chat model" name="llm_id" tooltip="The default chat LLM all the newly created knowledgebase will use."> + <Form.Item + label={t('chatModel')} + name="llm_id" + tooltip={t('chatModelTip')} + > <Select options={allOptions[LlmModelType.Chat]} /> </Form.Item> - <Form.Item label="Embedding model" name="embd_id" tooltip="The default embedding model all the newly created knowledgebase will use."> + <Form.Item + label={t('embeddingModel')} + name="embd_id" + tooltip={t('embeddingModelTip')} + > <Select options={allOptions[LlmModelType.Embedding]} /> </Form.Item> <Form.Item - label="Img2txt model" + label={t('img2txtModel')} name="img2txt_id" - tooltip="The default multi-module model all the newly created knowledgebase will use. It can describe a picture or video." + tooltip={t('img2txtModelTip')} > <Select options={allOptions[LlmModelType.Image2text]} /> </Form.Item> - + <Form.Item - label="Sequence2txt model" + label={t('sequence2txtModel')} name="asr_id" - tooltip="The default ASR model all the newly created knowledgebase will use. Use this model to translate voices to corresponding text." + tooltip={t('sequence2txtModelTip')} > <Select options={allOptions[LlmModelType.Speech2text]} /> </Form.Item> diff --git a/web/src/pages/user-setting/setting-password/index.tsx b/web/src/pages/user-setting/setting-password/index.tsx index 919af72..66e744a 100644 --- a/web/src/pages/user-setting/setting-password/index.tsx +++ b/web/src/pages/user-setting/setting-password/index.tsx @@ -5,6 +5,7 @@ import { Button, Divider, Form, Input, Space } from 'antd'; import SettingTitle from '../components/setting-title'; import { useValidateSubmittable } from '../hooks'; +import { useTranslate } from '@/hooks/commonHooks'; import parentStyles from '../index.less'; import styles from './index.less'; @@ -22,6 +23,7 @@ const UserSettingPassword = () => { const loading = useOneNamespaceEffectsLoading('settingModel', ['setting']); const { form, submittable } = useValidateSubmittable(); const saveSetting = useSaveSetting(); + const { t } = useTranslate('setting'); const onFinish = (values: any) => { const password = rsaPsw(values.password) as string; @@ -37,8 +39,8 @@ const UserSettingPassword = () => { return ( <section className={styles.passwordWrapper}> <SettingTitle - title="Password" - description="Please enter your current password to change your password." + title={t('password')} + description={t('passwordDescription')} ></SettingTitle> <Divider /> <Form @@ -56,12 +58,12 @@ const UserSettingPassword = () => { // requiredMark={'optional'} > <Form.Item<FieldType> - label="Current password" + label={t('currentPassword')} name="password" rules={[ { required: true, - message: 'Please input your password!', + message: t('currentPasswordMessage'), whitespace: true, }, ]} @@ -69,14 +71,14 @@ const UserSettingPassword = () => { <Input.Password /> </Form.Item> <Divider /> - <Form.Item label="New password" required> + <Form.Item label={t('newPassword')} required> <Form.Item<FieldType> noStyle name="new_password" rules={[ { required: true, - message: 'Please input your password!', + message: t('newPasswordMessage'), whitespace: true, }, ]} @@ -84,18 +86,18 @@ const UserSettingPassword = () => { <Input.Password /> </Form.Item> <p className={parentStyles.itemDescription}> - Your new password must be more than 8 characters. + {t('newPasswordDescription')} </p> </Form.Item> <Divider /> <Form.Item<FieldType> - label="Confirm new password" + label={t('confirmPassword')} name="confirm_password" dependencies={['new_password']} rules={[ { required: true, - message: 'Please confirm your password!', + message: t('confirmPasswordMessage'), whitespace: true, }, ({ getFieldValue }) => ({ @@ -104,7 +106,7 @@ const UserSettingPassword = () => { return Promise.resolve(); } return Promise.reject( - new Error('The new password that you entered do not match!'), + new Error(t('confirmPasswordNonMatchMessage')), ); }, }), @@ -120,14 +122,14 @@ const UserSettingPassword = () => { } > <Space> - <Button htmlType="button">Cancel</Button> + <Button htmlType="button">{t('cancel')}</Button> <Button type="primary" htmlType="submit" disabled={!submittable} loading={loading} > - Save + {t('save', { keyPrefix: 'common' })} </Button> </Space> </Form.Item> diff --git a/web/src/pages/user-setting/setting-profile/index.tsx b/web/src/pages/user-setting/setting-profile/index.tsx index 952dc7b..627af93 100644 --- a/web/src/pages/user-setting/setting-profile/index.tsx +++ b/web/src/pages/user-setting/setting-profile/index.tsx @@ -29,6 +29,8 @@ import { useValidateSubmittable, } from '../hooks'; +import { useTranslate } from '@/hooks/commonHooks'; +import { useChangeLanguage } from '@/hooks/logicHooks'; import parentStyles from '../index.less'; import styles from './index.less'; @@ -54,6 +56,8 @@ const UserSettingProfile = () => { const { form, submittable } = useValidateSubmittable(); const loading = useSelectUserInfoLoading(); useFetchUserInfo(); + const { t } = useTranslate('setting'); + const changeLanguage = useChangeLanguage(); const onFinish = async (values: any) => { const avatar = await getBase64FromUploadFileList(values.avatar); @@ -72,8 +76,8 @@ const UserSettingProfile = () => { return ( <section className={styles.profileWrapper}> <SettingTitle - title="Profile" - description="Update your photo and personal details here." + title={t('profile')} + description={t('profileDescription')} ></SettingTitle> <Divider /> <Spin spinning={loading}> @@ -91,12 +95,12 @@ const UserSettingProfile = () => { autoComplete="off" > <Form.Item<FieldType> - label="Username" + label={t('username')} name="nickname" rules={[ { required: true, - message: 'Please input your username!', + message: t('usernameMessage'), whitespace: true, }, ]} @@ -107,8 +111,8 @@ const UserSettingProfile = () => { <Form.Item<FieldType> label={ <div> - <Space>Your photo</Space> - <div>This will be displayed on your profile.</div> + <Space>{t('photo')}</Space> + <div>{t('photoDescription')}</div> </div> } name="avatar" @@ -126,41 +130,53 @@ const UserSettingProfile = () => { > <button style={{ border: 0, background: 'none' }} type="button"> <PlusOutlined /> - <div style={{ marginTop: 8 }}>Upload</div> + <div style={{ marginTop: 8 }}> + {t('upload', { keyPrefix: 'common' })} + </div> </button> </Upload> </Form.Item> <Divider /> <Form.Item<FieldType> - label="Color schema" + label={t('colorSchema')} name="color_schema" - rules={[ - { required: true, message: 'Please select your color schema!' }, - ]} + rules={[{ required: true, message: t('colorSchemaMessage') }]} > - <Select placeholder="select your color schema"> - <Option value="Bright">Bright</Option> - <Option value="Dark">Dark</Option> + <Select placeholder={t('colorSchemaPlaceholder')}> + <Option value="Bright">{t('bright')}</Option> + <Option value="Dark">{t('dark')}</Option> </Select> </Form.Item> <Divider /> <Form.Item<FieldType> - label="Language" + label={t('language', { keyPrefix: 'common' })} name="language" - rules={[{ required: true, message: 'Please input your language!' }]} + rules={[ + { + required: true, + message: t('languageMessage', { keyPrefix: 'common' }), + }, + ]} > - <Select placeholder="select your language"> - <Option value="English">English</Option> - <Option value="Chinese">Chinese</Option> + <Select + placeholder={t('languagePlaceholder', { keyPrefix: 'common' })} + onChange={changeLanguage} + > + <Option value="English"> + {t('english', { keyPrefix: 'common' })} + </Option> + <Option value="Chinese"> + {t('chinese', { keyPrefix: 'common' })} + </Option> </Select> </Form.Item> <Divider /> <Form.Item<FieldType> - label="Timezone" + label={t('timezone')} name="timezone" - rules={[{ required: true, message: 'Please input your timezone!' }]} + rules={[{ required: true, message: t('timezoneMessage') }]} > - <Select placeholder="select your timezone" showSearch> + <Select placeholder={t('timezonePlaceholder')} showSearch> {TimezoneList.map((x) => ( <Option value={x} key={x}> {x} @@ -169,12 +185,12 @@ const UserSettingProfile = () => { </Select> </Form.Item> <Divider /> - <Form.Item label="Email address"> + <Form.Item label={t('email')}> <Form.Item<FieldType> name="email" noStyle> <Input disabled /> </Form.Item> <p className={parentStyles.itemDescription}> - Once registered, E-mail cannot be changed. + {t('emailDescription')} </p> </Form.Item> <Form.Item @@ -184,14 +200,14 @@ const UserSettingProfile = () => { } > <Space> - <Button htmlType="button">Cancel</Button> + <Button htmlType="button">{t('cancel')}</Button> <Button type="primary" htmlType="submit" disabled={!submittable} loading={submitLoading} > - Save + {t('save', { keyPrefix: 'common' })} </Button> </Space> </Form.Item> diff --git a/web/src/pages/user-setting/setting-team/index.tsx b/web/src/pages/user-setting/setting-team/index.tsx index 48dab39..c2445f0 100644 --- a/web/src/pages/user-setting/setting-team/index.tsx +++ b/web/src/pages/user-setting/setting-team/index.tsx @@ -1,17 +1,21 @@ import { Button, Card, Flex } from 'antd'; +import { useTranslate } from '@/hooks/commonHooks'; import { useSelectUserInfo } from '@/hooks/userSettingHook'; import styles from './index.less'; const UserSettingTeam = () => { const userInfo = useSelectUserInfo(); + const { t } = useTranslate('setting'); return ( <div className={styles.teamWrapper}> <Card className={styles.teamCard}> <Flex align="center" justify={'space-between'}> - <span>{userInfo.nickname} Workspace</span> - <Button type="primary">Upgrade</Button> + <span> + {userInfo.nickname} {t('workspace')} + </span> + <Button type="primary">{t('upgrade')}</Button> </Flex> </Card> </div> diff --git a/web/src/pages/user-setting/sidebar/index.tsx b/web/src/pages/user-setting/sidebar/index.tsx index 097ab5f..0945e31 100644 --- a/web/src/pages/user-setting/sidebar/index.tsx +++ b/web/src/pages/user-setting/sidebar/index.tsx @@ -7,38 +7,39 @@ import { UserSettingBaseKey, UserSettingIconMap, UserSettingRouteKey, - UserSettingRouteMap, } from '../constants'; +import { useTranslate } from '@/hooks/commonHooks'; import { useLogout } from '@/hooks/userSettingHook'; import styles from './index.less'; type MenuItem = Required<MenuProps>['items'][number]; -function getItem( - label: React.ReactNode, - key: React.Key, - icon?: React.ReactNode, - children?: MenuItem[], - type?: 'group', -): MenuItem { - return { - key, - icon, - children, - label, - type, - } as MenuItem; -} - -const items: MenuItem[] = Object.values(UserSettingRouteKey).map((value) => - getItem(UserSettingRouteMap[value], value, UserSettingIconMap[value]), -); - const SideBar = () => { const navigate = useNavigate(); const pathName = useSecondPathName(); const logout = useLogout(); + const { t } = useTranslate('setting'); + + function getItem( + label: string, + key: React.Key, + icon?: React.ReactNode, + children?: MenuItem[], + type?: 'group', + ): MenuItem { + return { + key, + icon, + children, + label: t(label), + type, + } as MenuItem; + } + + const items: MenuItem[] = Object.values(UserSettingRouteKey).map((value) => + getItem(value, value, UserSettingIconMap[value]), + ); const handleMenuClick: MenuProps['onClick'] = ({ key }) => { if (key === UserSettingRouteKey.Logout) { diff --git a/web/src/utils/authorizationUtil.ts b/web/src/utils/authorizationUtil.ts index a2b3d06..f4b8b5e 100644 --- a/web/src/utils/authorizationUtil.ts +++ b/web/src/utils/authorizationUtil.ts @@ -38,6 +38,12 @@ const storage = { localStorage.removeItem(x); }); }, + setLanguage: (lng: string) => { + localStorage.setItem('lng', lng); + }, + getLanguage: (): string => { + return localStorage.getItem('lng') as string; + }, }; export default storage; diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index adc77c5..c5fd867 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -1,4 +1,5 @@ import { Authorization } from '@/constants/authorization'; +import i18n from '@/locales/config'; import authorizationUtil from '@/utils/authorizationUtil'; import { message, notification } from 'antd'; import { history } from 'umi'; @@ -7,21 +8,21 @@ import { RequestMethod, extend } from 'umi-request'; const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动ä¸æ–请求。errorHandler 抛出的error message const RetcodeMessage = { - 200: 'æœåŠ¡å™¨æˆåŠŸè¿”回请求的数æ®ã€‚', - 201: '新建或修改数æ®æˆåŠŸã€‚', - 202: '一个请求已ç»è¿›å…¥åŽå°æŽ’队(异æ¥ä»»åŠ¡ï¼‰ã€‚', - 204: 'åˆ é™¤æ•°æ®æˆåŠŸã€‚', - 400: 'å‘出的请求有错误,æœåŠ¡å™¨æ²¡æœ‰è¿›è¡Œæ–°å»ºæˆ–修改数æ®çš„æ“作。', - 401: '用户没有æƒé™ï¼ˆä»¤ç‰Œã€ç”¨æˆ·åã€å¯†ç 错误)。', - 403: '用户得到授æƒï¼Œä½†æ˜¯è®¿é—®æ˜¯è¢«ç¦æ¢çš„。', - 404: 'å‘出的请求针对的是ä¸å˜åœ¨çš„记录,æœåŠ¡å™¨æ²¡æœ‰è¿›è¡Œæ“作。', - 406: 'è¯·æ±‚çš„æ ¼å¼ä¸å¯å¾—。', - 410: '请求的资æºè¢«æ°¸ä¹…åˆ é™¤ï¼Œä¸”ä¸ä¼šå†å¾—到的。', - 422: '当创建一个对象时,å‘生一个验è¯é”™è¯¯ã€‚', - 500: 'æœåŠ¡å™¨å‘生错误,请检查æœåŠ¡å™¨ã€‚', - 502: '网关错误。', - 503: 'æœåŠ¡ä¸å¯ç”¨ï¼ŒæœåŠ¡å™¨æš‚时过载或维护。', - 504: '网关超时。', + 200: i18n.t('message.200'), + 201: i18n.t('message.201'), + 202: i18n.t('message.202'), + 204: i18n.t('message.204'), + 400: i18n.t('message.400'), + 401: i18n.t('message.401'), + 403: i18n.t('message.403'), + 404: i18n.t('message.404'), + 406: i18n.t('message.406'), + 410: i18n.t('message.410'), + 422: i18n.t('message.422'), + 500: i18n.t('message.500'), + 502: i18n.t('message.502'), + 503: i18n.t('message.503'), + 504: i18n.t('message.504'), }; type ResultCode = | 200 @@ -62,13 +63,13 @@ const errorHandler = (error: { RetcodeMessage[response.status as ResultCode] || response.statusText; const { status, url } = response; notification.error({ - message: `请求错误 ${status}: ${url}`, + message: `${i18n.t('message.requestError')} ${status}: ${url}`, description: errorText, }); } else if (!response) { notification.error({ - description: '您的网络å‘ç”Ÿå¼‚å¸¸ï¼Œæ— æ³•è¿žæŽ¥æœåŠ¡å™¨', - message: '网络异常', + description: i18n.t('message.networkAnomalyDescription'), + message: i18n.t('message.networkAnomaly'), }); } } @@ -123,7 +124,7 @@ request.interceptors.response.use(async (response: any, options) => { message.error(data.retmsg); } else { notification.error({ - message: `æ示 : ${data.retcode}`, + message: `${i18n.t('message.hint')} : ${data.retcode}`, description: data.retmsg, duration: 3, }); -- GitLab