diff --git a/package.json b/package.json index 5235d2f..08d0f01 100644 --- a/package.json +++ b/package.json @@ -31,12 +31,14 @@ "axios": "^1.6.7", "dayjs": "^1.11.10", "html-to-text": "^9.0.5", + "i18next": "^23.10.1", + "i18next-browser-languagedetector": "^7.2.0", "langchain": "^0.1.9", "lucide-react": "^0.350.0", - "plasmo": "0.84.1", "property-information": "^6.4.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-i18next": "^14.1.0", "react-markdown": "8.0.0", "react-router-dom": "6.10.0", "react-syntax-highlighter": "^15.5.0", diff --git a/src/assets/locale/en/option-playground.json b/src/assets/locale/en/option-playground.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/src/assets/locale/en/option-playground.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/assets/locale/en/option.json b/src/assets/locale/en/option.json new file mode 100644 index 0000000..55d3032 --- /dev/null +++ b/src/assets/locale/en/option.json @@ -0,0 +1,209 @@ +{ + "newChat": "New Chat", + "selectAModel": "Select a Model", + "selectAPrompt": "Select a Prompt", + "githubRepository": "GitHub Repository", + "settings": "Settings", + "sidebarTitle": "Chat History", + "error": "Error", + "somethingWentWrong": "Something went wrong", + "validationSelectModel": "Please select a model to continue", + "generalSettings": { + "title": "General Settings", + "heading": "Web UI Settings", + "settings": { + "speechRecognitionLang": { + "label": "Speech Recognition Language", + "placeholder": "Select a language" + }, + "darkMode": { + "label": "Change Theme", + "options": { + "light": "Light", + "dark": "Dark" + } + }, + "searchMode": { + "label": "Perform Simple Internet Search" + }, + "deleteChatHistory": { + "label": "Delete Chat History", + "button": "Delete", + "confirm": "Are you sure you want to delete your chat history? This action cannot be undone." + } + } + }, + "manageModels": { + "title": "Manage Models", + "addBtn": "Add New Model", + "columns": { + "name": "Name", + "digest": "Digest", + "modifiedAt": "Modified At", + "size": "Size", + "actions": "Actions" + }, + "expandedColumns": { + "parentModel": "Parent Model", + "format": "Format", + "family": "Family", + "parameterSize": "Parameter Size", + "quantizationLevel": "Quantization Level" + }, + "tooltip": { + "delete": "Delete Model", + "repull": "Re-Pull Model" + }, + "confirm": { + "delete": "Are you sure you want to delete this model?", + "repull": "Are you sure you want to re-pull this model?" + }, + "modal": { + "title": "Add New Model", + "placeholder": "Enter Model Name", + "pull": "Pull Model" + }, + "notification": { + "pullModel": "Pulling Model", + "pullModelDescription": "Pulling {{modelName}} model. For more details, check the extension icon.", + "success": "Success", + "error": "Error", + "successDescription": "Successfully pulled the model", + "successDeleteDescription": "Successfully deleted the model", + "someError": "Something went wrong. Please try again later" + } + }, + "managePrompts": { + "title": "Manage Prompts", + "addBtn": "Add New Prompt", + "columns": { + "title": "Title", + "prompt": "Prompt", + "type": "Prompt Type", + "actions": "Actions" + }, + "systemPrompt": "System Prompt", + "quickPrompt": "Quick Prompt", + "tooltip": { + "delete": "Delete Prompt", + "edit": "Edit Prompt" + }, + "confirm": { + "delete": "Are you sure you want to delete this prompt? This action cannot be undone." + }, + "modal": { + "addTitle": "Add New Prompt", + "editTitle": "Edit Prompt" + }, + "form": { + "title": { + "label": "Title", + "placeholder": "My Awesome Prompt", + "required": "Please enter a title" + }, + "prompt": { + "label": "Prompt", + "placeholder": "Enter Prompt", + "required": "Please enter a prompt", + "help": "You can use {key} as variable in your prompt." + }, + "isSystem": { + "label": "Is System Prompt" + }, + "btnSave": { + "saving": "Adding Prompt...", + "save": "Add Prompt" + }, + "btnEdit": { + "saving": "Updating Prompt...", + "save": "Update Prompt" + } + }, + "notification": { + "addSuccess": "Prompt Added", + "addSuccessDesc": "Prompt has been added successfully", + "error": "Error", + "someError": "Something went wrong. Please try again later", + "updatedSuccess": "Prompt Updated", + "updatedSuccessDesc": "Prompt has been updated successfully", + "deletedSuccess": "Prompt Deleted", + "deletedSuccessDesc": "Prompt has been deleted successfully" + } + }, + "manageShare": { + "title": "Manage Share", + "heading": "Configure Page Share URL", + "form": { + "url": { + "label": "Page Share URL", + "placeholder": "Enter Page Share URL", + "required": "Please input your Page Share URL!", + "help": "For privacy reasons, you can self-host the page share and provide the URL here. Learn More." + } + }, + "webshare": { + "heading": "Web Share", + "columns": { + "title": "Title", + "url": "URL", + "actions": "Actions" + }, + "tooltip": { + "delete": "Delete Share" + }, + "confirm": { + "delete": "Are you sure you want to delete this share? This action cannot be undone." + } + }, + "notification": { + "pageShareSuccess": "Page Share URL updated successfully", + "someError": "Something went wrong. Please try again later", + "webShareDeleteSuccess": "Web Share deleted successfully" + } + }, + "ollamaSettings": { + "title": "Ollama Settings", + "heading": "Configure Ollama", + "settings": { + "ollamaUrl": { + "label": "Ollama URL", + "placeholder": "Enter Ollama URL" + }, + "ragSettings": { + "label": "RAG Settings", + "model": { + "label": "Embedding Model", + "required": "Please select a model", + "help": "Highly recommended to use embedding models like `nomic-embed-text`.", + "placeholder": "Select a model" + }, + "chunkSize": { + "label": "Chunk Size", + "placeholder": "Enter Chunk Size", + "required": "Please enter a chunk size" + }, + "chunkOverlap": { + "label": "Chunk Overlap", + "placeholder": "Enter Chunk Overlap", + "required": "Please enter a chunk overlap" + } + }, + "prompt": { + "label": "Configure RAG Prompt", + "option1": "Normal", + "option2": "Web", + "alert": "Configuring the system prompt here is deprecated. Please use the Manage Prompts section to add or edit prompts. This section will be removed in a future release", + "systemPrompt": "System Prompt", + "systemPromptPlaceholder": "Enter System Prompt", + "webSearchPrompt": "Web Search Prompt", + "webSearchPromptHelp": "Do not remove `{search_results}` from the prompt.", + "webSearchPromptError": "Please enter a web search prompt", + "webSearchPromptPlaceholder": "Enter Web Search Prompt", + "webSearchFollowUpPrompt": "Web Search Follow Up Prompt", + "webSearchFollowUpPromptHelp": "Do not remove `{chat_history}` and `{question}` from the prompt.", + "webSearchFollowUpPromptError": "Please input your Web Search Follow Up Prompt!", + "webSearchFollowUpPromptPlaceholder": "Your Web Search Follow Up Prompt" + } + } + } +} \ No newline at end of file diff --git a/src/components/Layouts/Layout.tsx b/src/components/Layouts/Layout.tsx index 4a1a814..afe7412 100644 --- a/src/components/Layouts/Layout.tsx +++ b/src/components/Layouts/Layout.tsx @@ -17,6 +17,7 @@ import { } from "lucide-react" import { getAllPrompts } from "~/libs/db" import { ShareBtn } from "~/components/Common/ShareBtn" +import { useTranslation } from "react-i18next" export default function OptionLayout({ children @@ -24,6 +25,8 @@ export default function OptionLayout({ children: React.ReactNode }) { const [sidebarOpen, setSidebarOpen] = useState(false) + const { t } = useTranslation("option") + const { selectedModel, setSelectedModel, @@ -61,8 +64,8 @@ export default function OptionLayout({ if (prompt?.is_system) { setSelectedSystemPrompt(prompt.id) } else { - setSelectedQuickPrompt(prompt.content) - setSelectedSystemPrompt(null) + setSelectedQuickPrompt(prompt!.content) + setSelectedSystemPrompt("") } } @@ -93,7 +96,7 @@ export default function OptionLayout({ onClick={clearChat} className="inline-flex items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 "> - New Chat + {t("newChat")} @@ -106,12 +109,13 @@ export default function OptionLayout({ size="large" loading={isModelsLoading || isModelsFetching} filterOption={(input, option) => - option.label.toLowerCase().indexOf(input.toLowerCase()) >= + option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 || - option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 + option!.value.toLowerCase().indexOf(input.toLowerCase()) >= + 0 } showSearch - placeholder="Select a model" + placeholder={t("selectAModel")} className="w-64 " options={models?.map((model) => ({ label: model.name, @@ -127,12 +131,13 @@ export default function OptionLayout({ size="large" loading={isPromptLoading} showSearch - placeholder="Select a prompt" + placeholder={t("selectAPrompt")} className="w-60" allowClear onChange={handlePromptChange} value={selectedSystemPrompt} filterOption={(input, option) => + //@ts-ignore option.label.key .toLowerCase() .indexOf(input.toLowerCase()) >= 0 @@ -161,7 +166,8 @@ export default function OptionLayout({ {pathname === "/" && messages.length > 0 && !streaming && ( )} - + - + @@ -185,14 +192,12 @@ export default function OptionLayout({ setSidebarOpen(false)} open={sidebarOpen}> - setSidebarOpen(false)} - /> + setSidebarOpen(false)} /> ) diff --git a/src/components/Layouts/SettingsOptionLayout.tsx b/src/components/Layouts/SettingsOptionLayout.tsx index a53bfcb..627e295 100644 --- a/src/components/Layouts/SettingsOptionLayout.tsx +++ b/src/components/Layouts/SettingsOptionLayout.tsx @@ -5,6 +5,7 @@ import { Orbit, Share } from "lucide-react" +import { useTranslation } from "react-i18next" import { Link, useLocation } from "react-router-dom" function classNames(...classes: string[]) { @@ -44,6 +45,8 @@ const LinkComponent = (item: { export const SettingsLayout = ({ children }: { children: React.ReactNode }) => { const location = useLocation() + const { t } = useTranslation("option") + return ( <>
@@ -54,31 +57,31 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => { className="flex gap-x-3 gap-y-1 whitespace-nowrap lg:flex-col"> - diff --git a/src/components/Option/Models/index.tsx b/src/components/Option/Models/index.tsx index c142869..b39dd33 100644 --- a/src/components/Option/Models/index.tsx +++ b/src/components/Option/Models/index.tsx @@ -7,12 +7,14 @@ import relativeTime from "dayjs/plugin/relativeTime" import { useState } from "react" import { useForm } from "@mantine/form" import { Download, RotateCcw, Trash2 } from "lucide-react" +import { useTranslation } from "react-i18next" dayjs.extend(relativeTime) export const ModelsBody = () => { const queryClient = useQueryClient() const [open, setOpen] = useState(false) + const { t } = useTranslation("option") const form = useForm({ initialValues: { @@ -32,22 +34,24 @@ export const ModelsBody = () => { queryKey: ["fetchAllModels"] }) notification.success({ - message: "Model Deleted", - description: "Model has been deleted successfully" + message: t("manageModels.notification.success"), + description: t("manageModels.notification.successDeleteDescription") }) }, onError: (error) => { notification.error({ message: "Error", - description: error?.message || "Something went wrong" + description: error?.message || t("manageModels.notification.someError") }) } }) const pullModel = async (modelName: string) => { notification.info({ - message: "Pulling Model", - description: `Pulling ${modelName} model. For more details, check the extension icon.` + message: t("manageModels.notification.pullModel"), + description: t("manageModels.notification.pullModelDescription", { + modelName + }) }) setOpen(false) @@ -76,7 +80,7 @@ export const ModelsBody = () => {
@@ -88,12 +92,12 @@ export const ModelsBody = () => { ( @@ -105,28 +109,26 @@ export const ModelsBody = () => { ) }, { - title: "Modified", + title: t("manageModels.columns.modifiedAt"), dataIndex: "modified_at", key: "modified_at", render: (text: string) => dayjs(text).fromNow(true) }, { - title: "Size", + title: t("manageModels.columns.size"), dataIndex: "size", key: "size", render: (text: number) => bytePerSecondFormatter(text) }, { - title: "Action", + title: t("manageModels.columns.actions"), render: (_, record) => (
- + - + diff --git a/src/components/Option/Playground/PlaygroundNewChat.tsx b/src/components/Option/Playground/PlaygroundNewChat.tsx index 71d98b8..d9816af 100644 --- a/src/components/Option/Playground/PlaygroundNewChat.tsx +++ b/src/components/Option/Playground/PlaygroundNewChat.tsx @@ -1,8 +1,10 @@ import { PencilIcon } from "lucide-react" import { useMessage } from "../../../hooks/useMessage" +import { useTranslation } from 'react-i18next'; export const PlaygroundNewChat = () => { const { setHistory, setMessages, setHistoryId } = useMessage() + const { t } = useTranslation('optionChat') const handleClick = () => { setHistoryId(null) @@ -16,7 +18,7 @@ export const PlaygroundNewChat = () => { className="flex w-full border bg-transparent hover:bg-gray-200 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100 rounded-md p-2 dark:border-gray-800">
@@ -120,43 +124,41 @@ export const PromptBody = () => {
is_system ? ( - - System Prompt + + {t("managePrompts.systemPrompt")} ) : ( - Quick Prompt + {t("managePrompts.quickPrompt")} ) }, { - title: "Action", + title: t("managePrompts.columns.actions"), render: (_, record) => (
- + - +
setOpen(false)} footer={null}> @@ -198,25 +200,35 @@ export const PromptBody = () => { form={createForm}> - + label={t("managePrompts.form.title.label")} + rules={[ + { + required: true, + message: t("managePrompts.form.title.required") + } + ]}> + + label={t("managePrompts.form.prompt.label")} + rules={[ + { + required: true, + message: t("managePrompts.form.prompt.required") + } + ]} + help={t("managePrompts.form.prompt.help")}> @@ -225,14 +237,16 @@ export const PromptBody = () => { setOpenEdit(false)} footer={null}> @@ -242,25 +256,35 @@ export const PromptBody = () => { form={editForm}> - + label={t("managePrompts.form.title.label")} + rules={[ + { + required: true, + message: t("managePrompts.form.title.required") + } + ]}> + + label={t("managePrompts.form.prompt.label")} + rules={[ + { + required: true, + message: t("managePrompts.form.prompt.required") + } + ]} + help={t("managePrompts.form.prompt.help")}> @@ -269,7 +293,9 @@ export const PromptBody = () => { diff --git a/src/components/Option/Settings/ollama.tsx b/src/components/Option/Settings/ollama.tsx index c733d9b..af8ca63 100644 --- a/src/components/Option/Settings/ollama.tsx +++ b/src/components/Option/Settings/ollama.tsx @@ -12,16 +12,19 @@ import { setOllamaURL as saveOllamaURL } from "~/services/ollama" import { SettingPrompt } from "./prompt" +import { useTranslation } from "react-i18next" export const SettingsOllama = () => { const [ollamaURL, setOllamaURL] = useState("") + const { t } = useTranslation("option") + const { data: ollamaInfo, status } = useQuery({ queryKey: ["fetchOllamURL"], queryFn: async () => { const [ollamaURL, allModels, chunkOverlap, chunkSize, defaultEM] = await Promise.all([ getOllamaURL(), - getAllModels({returnEmpty: true}), + getAllModels({ returnEmpty: true }), defaultEmbeddingChunkOverlap(), defaultEmbeddingChunkSize(), defaultEmbeddingModelForRag() @@ -54,7 +57,7 @@ export const SettingsOllama = () => {

- Configure Ollama + {t("ollamaSettings.heading")}

@@ -62,7 +65,7 @@ export const SettingsOllama = () => { { onChange={(e) => { setOllamaURL(e.target.value) }} - placeholder="Your Ollama URL" + placeholder={t("ollamaSettings.settings.ollamaUrl.placeholder")} className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100" />
@@ -88,7 +91,7 @@ export const SettingsOllama = () => {

- Configure RAG + {t("ollamaSettings.settings.ragSettings.label")}

@@ -108,18 +111,26 @@ export const SettingsOllama = () => { }}> + label={t("ollamaSettings.settings.ragSettings.model.label")} + help={t("ollamaSettings.settings.ragSettings.model.help")} + rules={[ + { + required: true, + message: t( + "ollamaSettings.settings.ragSettings.model.required" + ) + } + ]}> - option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 || - option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 + option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 || + option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 } onChange={(value) => { setSpeechToTextLanguage(value) @@ -44,7 +47,9 @@ export const SettingOther = () => { />
- Change Theme + + {t("generalSettings.settings.darkMode.label")} +
- Delete Chat History + {t("generalSettings.settings.deleteChatHistory.label")}
diff --git a/src/components/Option/Settings/prompt.tsx b/src/components/Option/Settings/prompt.tsx index 1d743e2..4829b31 100644 --- a/src/components/Option/Settings/prompt.tsx +++ b/src/components/Option/Settings/prompt.tsx @@ -1,6 +1,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query" import { Skeleton, Radio, Form, Alert } from "antd" import React from "react" +import { useTranslation } from "react-i18next" import { SaveButton } from "~/components/Common/SaveButton" import { getWebSearchPrompt, @@ -11,6 +12,8 @@ import { } from "~/services/ollama" export const SettingPrompt = () => { + const { t } = useTranslation("option") + const [selectedValue, setSelectedValue] = React.useState<"normal" | "web">( "web" ) @@ -45,8 +48,12 @@ export const SettingPrompt = () => { setSelectedValue(e.target.value)}> - Normal - Web + + {t("ollamaSettings.settings.prompt.option1")} + + + {t("ollamaSettings.settings.prompt.option2")} + @@ -64,18 +71,22 @@ export const SettingPrompt = () => { }}> - +