Init support of i18n and English, Mandarin, Spanish, French (#1317)

* Init support of i18n and English and mandarin

* Update common.js (#1320)

* add General Appearance and Chat setting zh translate (#1414)

* add config zh translate (#1461)

* patch some translation pages

* Update locality fixes

* update: complete login page Mandarin translation. (#1709)

update: complete Mandarin translation.

* complete translation

* update github to run validator

* bump to test workflow failure

* bump to fix tests

* update workflow

* refactor lang selector support

* add Spanish and French

* add dictionaries

---------

Co-authored-by: GetOffer.help <13744916+getofferhelp@users.noreply.github.com>
Co-authored-by: AIR <129256286+KochabStar@users.noreply.github.com>
Co-authored-by: Ezio T <ezio5600@gmail.com>
This commit is contained in:
Timothy Carambat 2024-06-19 14:48:19 -07:00 committed by GitHub
parent 421c5c6b91
commit 610c87ce19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 2786 additions and 412 deletions

View File

@ -0,0 +1,37 @@
# This Github action is for validation of all languages which translations are offered for
# in the locales folder in `frontend/src`. All languages are compared to the EN translation
# schema since that is the fallback language setting. This workflow will run on all PRs that
# modify any files in the translation directory
name: Verify translations files
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "frontend/src/locales/**.js"
jobs:
run-script:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Run verifyTranslations.mjs script
run: |
cd frontend/src/locales
node verifyTranslations.mjs
- name: Fail job on error
if: failure()
run: exit 1

View File

@ -19,6 +19,8 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"he": "^1.2.0", "he": "^1.2.0",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"i18next": "^23.11.3",
"i18next-browser-languagedetector": "^7.2.1",
"js-levenshtein": "^1.1.6", "js-levenshtein": "^1.1.6",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
@ -27,6 +29,7 @@
"react-device-detect": "^2.2.2", "react-device-detect": "^2.2.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-i18next": "^14.1.1",
"react-loading-skeleton": "^3.1.0", "react-loading-skeleton": "^3.1.0",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-speech-recognition": "^3.10.0", "react-speech-recognition": "^3.10.0",

View File

@ -1,5 +1,6 @@
import React, { lazy, Suspense } from "react"; import React, { lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
import { I18nextProvider } from "react-i18next";
import { ContextWrapper } from "@/AuthContext"; import { ContextWrapper } from "@/AuthContext";
import PrivateRoute, { import PrivateRoute, {
AdminRoute, AdminRoute,
@ -9,6 +10,7 @@ import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
import Login from "@/pages/Login"; import Login from "@/pages/Login";
import OnboardingFlow from "@/pages/OnboardingFlow"; import OnboardingFlow from "@/pages/OnboardingFlow";
import i18n from "./i18n";
import { PfpProvider } from "./PfpContext"; import { PfpProvider } from "./PfpContext";
import { LogoProvider } from "./LogoContext"; import { LogoProvider } from "./LogoContext";
@ -61,6 +63,7 @@ export default function App() {
<ContextWrapper> <ContextWrapper>
<LogoProvider> <LogoProvider>
<PfpProvider> <PfpProvider>
<I18nextProvider i18n={i18n}>
<Routes> <Routes>
<Route path="/" element={<PrivateRoute Component={Main} />} /> <Route path="/" element={<PrivateRoute Component={Main} />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
@ -95,7 +98,9 @@ export default function App() {
/> />
<Route <Route
path="/settings/embedding-preference" path="/settings/embedding-preference"
element={<AdminRoute Component={GeneralEmbeddingPreference} />} element={
<AdminRoute Component={GeneralEmbeddingPreference} />
}
/> />
<Route <Route
path="/settings/text-splitter-preference" path="/settings/text-splitter-preference"
@ -164,6 +169,7 @@ export default function App() {
<Route path="/onboarding" element={<OnboardingFlow />} /> <Route path="/onboarding" element={<OnboardingFlow />} />
<Route path="/onboarding/:step" element={<OnboardingFlow />} /> <Route path="/onboarding/:step" element={<OnboardingFlow />} />
</Routes> </Routes>
</I18nextProvider>
<ToastContainer /> <ToastContainer />
</PfpProvider> </PfpProvider>
</LogoProvider> </LogoProvider>

View File

@ -1,5 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { X } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
export default function EditingChatBubble({ export default function EditingChatBubble({
message, message,
@ -11,11 +12,12 @@ export default function EditingChatBubble({
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [tempMessage, setTempMessage] = useState(message[type]); const [tempMessage, setTempMessage] = useState(message[type]);
const isUser = type === "user"; const isUser = type === "user";
const { t } = useTranslation();
return ( return (
<div> <div>
<p className={`text-xs text-[#D3D4D4] ${isUser ? "text-right" : ""}`}> <p className={`text-xs text-[#D3D4D4] ${isUser ? "text-right" : ""}`}>
{isUser ? "User" : "AnythingLLM Chat Assistant"} {isUser ? t("common.user") : t("appearance.message.assistant")}
</p> </p>
<div <div
className={`relative flex w-full mt-2 items-start ${ className={`relative flex w-full mt-2 items-start ${

View File

@ -1,9 +1,11 @@
import { useTranslation } from "react-i18next";
export default function NativeEmbeddingOptions() { export default function NativeEmbeddingOptions() {
const { t } = useTranslation();
return ( return (
<div className="w-full h-10 items-center flex"> <div className="w-full h-10 items-center flex">
<p className="text-sm font-base text-white text-opacity-60"> <p className="text-sm font-base text-white text-opacity-60">
There is no set up required when using AnythingLLM's native embedding {t("embedding.provider.description")}
engine.
</p> </p>
</div> </div>
); );

View File

@ -6,6 +6,7 @@ import showToast from "@/utils/toast";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal"; import { useModal } from "@/hooks/useModal";
import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal"; import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal";
import { useTranslation } from "react-i18next";
const RecoveryForm = ({ onSubmit, setShowRecoveryForm }) => { const RecoveryForm = ({ onSubmit, setShowRecoveryForm }) => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@ -160,6 +161,7 @@ const ResetPasswordForm = ({ onSubmit }) => {
}; };
export default function MultiUserAuth() { export default function MultiUserAuth() {
const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [recoveryCodes, setRecoveryCodes] = useState([]); const [recoveryCodes, setRecoveryCodes] = useState([]);
@ -279,14 +281,15 @@ export default function MultiUserAuth() {
<div className="flex items-center flex-col gap-y-4"> <div className="flex items-center flex-col gap-y-4">
<div className="flex gap-x-1"> <div className="flex gap-x-1">
<h3 className="text-md md:text-2xl font-bold text-white text-center white-space-nowrap hidden md:block"> <h3 className="text-md md:text-2xl font-bold text-white text-center white-space-nowrap hidden md:block">
Welcome to {t("login.multi-user.welcome")}
</h3> </h3>
<p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent"> <p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent">
{customAppName || "AnythingLLM"} {customAppName || "AnythingLLM"}
</p> </p>
</div> </div>
<p className="text-sm text-white/90 text-center"> <p className="text-sm text-white/90 text-center">
Sign in to your {customAppName || "AnythingLLM"} account. {t("login.sign-in.start")} {customAppName || "AnythingLLM"}{" "}
{t("login.sign-in.end")}
</p> </p>
</div> </div>
</div> </div>
@ -296,7 +299,7 @@ export default function MultiUserAuth() {
<input <input
name="username" name="username"
type="text" type="text"
placeholder="Username" placeholder={t("login.multi-user.placeholder-username")}
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-full h-[48px] md:w-[300px] md:h-[34px]" className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-full h-[48px] md:w-[300px] md:h-[34px]"
required={true} required={true}
autoComplete="off" autoComplete="off"
@ -306,7 +309,7 @@ export default function MultiUserAuth() {
<input <input
name="password" name="password"
type="password" type="password"
placeholder="Password" placeholder={t("login.multi-user.placeholder-password")}
className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-full h-[48px] md:w-[300px] md:h-[34px]" className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-full h-[48px] md:w-[300px] md:h-[34px]"
required={true} required={true}
autoComplete="off" autoComplete="off"
@ -321,14 +324,17 @@ export default function MultiUserAuth() {
type="submit" type="submit"
className="md:text-primary-button md:bg-transparent text-dark-text text-sm font-bold focus:ring-4 focus:outline-none rounded-md border-[1.5px] border-[#46C8FF] md:h-[34px] h-[48px] md:hover:text-white md:hover:bg-primary-button bg-primary-button focus:z-10 w-full" className="md:text-primary-button md:bg-transparent text-dark-text text-sm font-bold focus:ring-4 focus:outline-none rounded-md border-[1.5px] border-[#46C8FF] md:h-[34px] h-[48px] md:hover:text-white md:hover:bg-primary-button bg-primary-button focus:z-10 w-full"
> >
{loading ? "Validating..." : "Login"} {loading
? t("login.multi-user.validating")
: t("login.multi-user.login")}
</button> </button>
<button <button
type="button" type="button"
className="text-white text-sm flex gap-x-1 hover:text-primary-button hover:underline" className="text-white text-sm flex gap-x-1 hover:text-primary-button hover:underline"
onClick={handleResetPassword} onClick={handleResetPassword}
> >
Forgot password?<b>Reset</b> {t("login.multi-user.forgot-pass")}?
<b>{t("login.multi-user.reset")}</b>
</button> </button>
</div> </div>
</div> </div>

View File

@ -5,8 +5,10 @@ import paths from "../../../utils/paths";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal"; import { useModal } from "@/hooks/useModal";
import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal"; import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal";
import { useTranslation } from "react-i18next";
export default function SingleUserAuth() { export default function SingleUserAuth() {
const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [recoveryCodes, setRecoveryCodes] = useState([]); const [recoveryCodes, setRecoveryCodes] = useState([]);
@ -73,14 +75,15 @@ export default function SingleUserAuth() {
<div className="flex items-center flex-col gap-y-4"> <div className="flex items-center flex-col gap-y-4">
<div className="flex gap-x-1"> <div className="flex gap-x-1">
<h3 className="text-md md:text-2xl font-bold text-white text-center white-space-nowrap hidden md:block"> <h3 className="text-md md:text-2xl font-bold text-white text-center white-space-nowrap hidden md:block">
Welcome to {t("login.multi-user.welcome")}
</h3> </h3>
<p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent"> <p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent">
{customAppName || "AnythingLLM"} {customAppName || "AnythingLLM"}
</p> </p>
</div> </div>
<p className="text-sm text-white/90 text-center"> <p className="text-sm text-white/90 text-center">
Sign in to your {customAppName || "AnythingLLM"} instance. {t("login.sign-in.start")} {customAppName || "AnythingLLM"}{" "}
{t("login.sign-in.end")}
</p> </p>
</div> </div>
</div> </div>

View File

@ -29,8 +29,10 @@ import { USER_BACKGROUND_COLOR } from "@/utils/constants";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import Footer from "../Footer"; import Footer from "../Footer";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
export default function SettingsSidebar() { export default function SettingsSidebar() {
const { t } = useTranslation();
const { logo } = useLogo(); const { logo } = useLogo();
const { user } = useUser(); const { user } = useUser();
const sidebarRef = useRef(null); const sidebarRef = useRef(null);
@ -113,7 +115,7 @@ export default function SettingsSidebar() {
<div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-scroll no-scroll"> <div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-scroll no-scroll">
<div className="h-auto md:sidebar-items md:dark:sidebar-items"> <div className="h-auto md:sidebar-items md:dark:sidebar-items">
<div className="flex flex-col gap-y-4 pb-[60px] overflow-y-scroll no-scroll"> <div className="flex flex-col gap-y-4 pb-[60px] overflow-y-scroll no-scroll">
<SidebarOptions user={user} /> <SidebarOptions user={user} t={t} />
</div> </div>
</div> </div>
</div> </div>
@ -146,12 +148,12 @@ export default function SettingsSidebar() {
> >
<div className="w-full h-full flex flex-col overflow-x-hidden items-between min-w-[235px]"> <div className="w-full h-full flex flex-col overflow-x-hidden items-between min-w-[235px]">
<div className="text-white text-opacity-60 text-sm font-medium uppercase mt-[4px] mb-0 ml-2"> <div className="text-white text-opacity-60 text-sm font-medium uppercase mt-[4px] mb-0 ml-2">
Instance Settings {t("settings.title")}
</div> </div>
<div className="relative h-[calc(100%-60px)] flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll"> <div className="relative h-[calc(100%-60px)] flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll">
<div className="h-auto sidebar-items"> <div className="h-auto sidebar-items">
<div className="flex flex-col gap-y-2 pb-[60px] overflow-y-scroll no-scroll"> <div className="flex flex-col gap-y-2 pb-[60px] overflow-y-scroll no-scroll">
<SidebarOptions user={user} /> <SidebarOptions user={user} t={t} />
</div> </div>
</div> </div>
</div> </div>
@ -221,39 +223,39 @@ const Option = ({
); );
}; };
const SidebarOptions = ({ user = null }) => ( const SidebarOptions = ({ user = null, t }) => (
<> <>
<Option <Option
href={paths.settings.system()} href={paths.settings.system()}
btnText="System Preferences" btnText={t("settings.system")}
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />} icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
allowedRole={["admin", "manager"]} allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.invites()} href={paths.settings.invites()}
btnText="Invitation" btnText={t("settings.invites")}
icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />} icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
allowedRole={["admin", "manager"]} allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.users()} href={paths.settings.users()}
btnText="Users" btnText={t("settings.users")}
icon={<Users className="h-5 w-5 flex-shrink-0" />} icon={<Users className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
allowedRole={["admin", "manager"]} allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.workspaces()} href={paths.settings.workspaces()}
btnText="Workspaces" btnText={t("settings.workspaces")}
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />} icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
allowedRole={["admin", "manager"]} allowedRole={["admin", "manager"]}
/> />
<Option <Option
href={paths.settings.chats()} href={paths.settings.chats()}
btnText="Workspace Chat" btnText={t("settings.workspace-chats")}
icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />} icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -270,7 +272,7 @@ const SidebarOptions = ({ user = null }) => (
/> />
<Option <Option
href={paths.settings.appearance()} href={paths.settings.appearance()}
btnText="Appearance" btnText={t("settings.appearance")}
icon={<Eye className="h-5 w-5 flex-shrink-0" />} icon={<Eye className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -278,7 +280,7 @@ const SidebarOptions = ({ user = null }) => (
/> />
<Option <Option
href={paths.settings.apiKeys()} href={paths.settings.apiKeys()}
btnText="API Keys" btnText={t("settings.api-keys")}
icon={<Key className="h-5 w-5 flex-shrink-0" />} icon={<Key className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -286,7 +288,7 @@ const SidebarOptions = ({ user = null }) => (
/> />
<Option <Option
href={paths.settings.llmPreference()} href={paths.settings.llmPreference()}
btnText="LLM Preference" btnText={t("settings.llm")}
icon={<ChatText className="h-5 w-5 flex-shrink-0" />} icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -302,7 +304,7 @@ const SidebarOptions = ({ user = null }) => (
/> />
<Option <Option
href={paths.settings.transcriptionPreference()} href={paths.settings.transcriptionPreference()}
btnText="Transcription Model" btnText={t("settings.transcription")}
icon={<ClosedCaptioning className="h-5 w-5 flex-shrink-0" />} icon={<ClosedCaptioning className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -311,7 +313,7 @@ const SidebarOptions = ({ user = null }) => (
<Option <Option
href={paths.settings.embedder.modelPreference()} href={paths.settings.embedder.modelPreference()}
childLinks={[paths.settings.embedder.chunkingPreference()]} childLinks={[paths.settings.embedder.chunkingPreference()]}
btnText="Embedder Preferences" btnText={t("settings.embedder")}
icon={<FileCode className="h-5 w-5 flex-shrink-0" />} icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -320,7 +322,7 @@ const SidebarOptions = ({ user = null }) => (
<> <>
<Option <Option
href={paths.settings.embedder.chunkingPreference()} href={paths.settings.embedder.chunkingPreference()}
btnText="Text Splitter & Chunking" btnText={t("settings.text-splitting")}
icon={<SplitVertical className="h-5 w-5 flex-shrink-0" />} icon={<SplitVertical className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -331,7 +333,7 @@ const SidebarOptions = ({ user = null }) => (
/> />
<Option <Option
href={paths.settings.vectorDatabase()} href={paths.settings.vectorDatabase()}
btnText="Vector Database" btnText={t("settings.vector-database")}
icon={<Database className="h-5 w-5 flex-shrink-0" />} icon={<Database className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -340,7 +342,7 @@ const SidebarOptions = ({ user = null }) => (
<Option <Option
href={paths.settings.embedSetup()} href={paths.settings.embedSetup()}
childLinks={[paths.settings.embedChats()]} childLinks={[paths.settings.embedChats()]}
btnText="Chat Embed Widgets" btnText={t("settings.embeds")}
icon={<CodeBlock className="h-5 w-5 flex-shrink-0" />} icon={<CodeBlock className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -349,7 +351,7 @@ const SidebarOptions = ({ user = null }) => (
<> <>
<Option <Option
href={paths.settings.embedChats()} href={paths.settings.embedChats()}
btnText="Chat Embed History" btnText={t("settings.embed-chats")}
icon={<Barcode className="h-5 w-5 flex-shrink-0" />} icon={<Barcode className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -360,7 +362,7 @@ const SidebarOptions = ({ user = null }) => (
/> />
<Option <Option
href={paths.settings.security()} href={paths.settings.security()}
btnText="Security" btnText={t("settings.security")}
icon={<Lock className="h-5 w-5 flex-shrink-0" />} icon={<Lock className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -369,7 +371,7 @@ const SidebarOptions = ({ user = null }) => (
/> />
<Option <Option
href={paths.settings.logs()} href={paths.settings.logs()}
btnText="Event Logs" btnText={t("settings.event-logs")}
icon={<Notepad className="h-5 w-5 flex-shrink-0" />} icon={<Notepad className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}
@ -377,7 +379,7 @@ const SidebarOptions = ({ user = null }) => (
/> />
<Option <Option
href={paths.settings.privacy()} href={paths.settings.privacy()}
btnText="Privacy & Data" btnText={t("settings.privacy")}
icon={<EyeSlash className="h-5 w-5 flex-shrink-0" />} icon={<EyeSlash className="h-5 w-5 flex-shrink-0" />}
user={user} user={user}
flex={true} flex={true}

View File

@ -1,7 +1,9 @@
import { Gauge } from "@phosphor-icons/react";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Gauge } from "@phosphor-icons/react";
export default function NativeTranscriptionOptions({ settings }) { export default function NativeTranscriptionOptions({ settings }) {
const { t } = useTranslation();
const [model, setModel] = useState(settings?.WhisperModelPref); const [model, setModel] = useState(settings?.WhisperModelPref);
return ( return (
@ -10,7 +12,7 @@ export default function NativeTranscriptionOptions({ settings }) {
<div className="w-full flex items-center gap-4"> <div className="w-full flex items-center gap-4">
<div className="flex flex-col w-60"> <div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-4"> <label className="text-white text-sm font-semibold block mb-4">
Model Selection {t("common.selection")}
</label> </label>
<select <select
name="WhisperModelPref" name="WhisperModelPref"
@ -46,20 +48,19 @@ function LocalWarning({ model }) {
} }
function WhisperSmall() { function WhisperSmall() {
const { t } = useTranslation();
return ( return (
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2"> <div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
<div className="gap-x-2 flex items-center"> <div className="gap-x-2 flex items-center">
<Gauge size={25} /> <Gauge size={25} />
<p className="text-sm"> <p className="text-sm">
Running the <b>whisper-small</b> model on a machine with limited RAM {t("transcription.warn-start")}
or CPU can stall AnythingLLM when processing media files.
<br /> <br />
We recommend at least 2GB of RAM and upload files &lt;10Mb. {t("transcription.warn-recommend")}
<br /> <br />
<br /> <br />
<i> <i>{t("transcription.warn-end")} (250mb)</i>
This model will automatically download on the first use. (250mb)
</i>
</p> </p>
</div> </div>
</div> </div>
@ -67,21 +68,19 @@ function WhisperSmall() {
} }
function WhisperLarge() { function WhisperLarge() {
const { t } = useTranslation();
return ( return (
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2"> <div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
<div className="gap-x-2 flex items-center"> <div className="gap-x-2 flex items-center">
<Gauge size={25} /> <Gauge size={25} />
<p className="text-sm"> <p className="text-sm">
Using the <b>whisper-large</b> model on machines with limited RAM or {t("transcription.warn-start")}
CPU can stall AnythingLLM when processing media files. This model is
substantially larger than the whisper-small.
<br /> <br />
We recommend at least 8GB of RAM and upload files &lt;10Mb. {t("transcription.warn-recommend")}
<br /> <br />
<br /> <br />
<i> <i>{t("transcription.warn-end")} (1.56GB)</i>
This model will automatically download on the first use. (1.56GB)
</i>
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,3 +1,4 @@
import { useLanguageOptions } from "@/hooks/useLanguageOptions";
import usePfp from "@/hooks/usePfp"; import usePfp from "@/hooks/usePfp";
import System from "@/models/system"; import System from "@/models/system";
import { AUTH_USER } from "@/utils/constants"; import { AUTH_USER } from "@/utils/constants";
@ -147,6 +148,7 @@ export default function AccountModal({ user, hideModal }) {
placeholder={`${user.username}'s new password`} placeholder={`${user.username}'s new password`}
/> />
</div> </div>
<LanguagePreference />
</div> </div>
<div className="flex justify-between items-center border-t border-gray-500/50 pt-4 p-6"> <div className="flex justify-between items-center border-t border-gray-500/50 pt-4 p-6">
<button <button
@ -168,3 +170,37 @@ export default function AccountModal({ user, hideModal }) {
</div> </div>
); );
} }
function LanguagePreference() {
const {
currentLanguage,
supportedLanguages,
getLanguageName,
changeLanguage,
} = useLanguageOptions();
return (
<div>
<label
htmlFor="userLang"
className="block mb-2 text-sm font-medium text-white"
>
Preferred language
</label>
<select
name="userLang"
className="bg-zinc-900 w-fit mt-2 px-4 border-gray-500 text-white text-sm rounded-lg block py-2"
defaultValue={currentLanguage || "en"}
onChange={(e) => changeLanguage(e.target.value)}
>
{supportedLanguages.map((lang) => {
return (
<option key={lang} value={lang}>
{getLanguageName(lang)}
</option>
);
})}
</select>
</div>
);
}

View File

@ -1,8 +1,10 @@
import { useTranslation } from "react-i18next";
export default function LanceDBOptions() { export default function LanceDBOptions() {
const { t } = useTranslation();
return ( return (
<div className="w-full h-10 items-center flex"> <div className="w-full h-10 items-center flex">
<p className="text-sm font-base text-white text-opacity-60"> <p className="text-sm font-base text-white text-opacity-60">
There is no configuration needed for LanceDB. {t("vector.provider.description")}
</p> </p>
</div> </div>
); );

View File

@ -0,0 +1,20 @@
import i18n from "@/i18n";
import { resources as languages } from "@/locales/resources";
export function useLanguageOptions() {
const supportedLanguages = Object.keys(languages);
const languageNames = new Intl.DisplayNames(supportedLanguages, {
type: "language",
});
const changeLanguage = (newLang = "en") => {
if (!Object.keys(languages).includes(newLang)) return false;
i18n.changeLanguage(newLang);
};
return {
currentLanguage: i18n.language || "en",
supportedLanguages,
getLanguageName: (lang = "en") => languageNames.of(lang),
changeLanguage,
};
}

21
frontend/src/i18n.js Normal file
View File

@ -0,0 +1,21 @@
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { defaultNS, resources } from "./locales/resources";
i18next
// https://github.com/i18next/i18next-browser-languageDetector/blob/9efebe6ca0271c3797bc09b84babf1ba2d9b4dbb/src/index.js#L11
.use(initReactI18next) // Initialize i18n for React
.use(LanguageDetector)
.init({
fallbackLng: "en",
debug: true,
defaultNS,
resources,
lowerCaseLng: true,
interpolation: {
escapeValue: false,
},
});
export default i18next;

View File

@ -0,0 +1,448 @@
const TRANSLATIONS = {
common: {
"workspaces-name": "Workspaces Name",
error: "error",
success: "success",
user: "User",
selection: "Model Selection",
saving: "Saving...",
save: "Save changes",
previous: "Previous Page",
next: "Next Page",
},
// Setting Sidebar menu items.
settings: {
title: "Instance Settings",
system: "System Preferences",
invites: "Invitation",
users: "Users",
workspaces: "Workspaces",
"workspace-chats": "Workspace Chat",
appearance: "Appearance",
"api-keys": "API Keys",
llm: "LLM Preference",
transcription: "Transcription Model",
embedder: "Embedding Preferences",
"text-splitting": "Text Splitter & Chunking",
"vector-database": "Vector Database",
embeds: "Chat Embed Widgets",
"embed-chats": "Chat Embed History",
security: "Security",
"event-logs": "Event Logs",
privacy: "Privacy & Data",
},
// Page Definitions
login: {
"multi-user": {
welcome: "Welcome to",
"placeholder-username": "Username",
"placeholder-password": "Password",
login: "Login",
validating: "Validating...",
"forgot-pass": "Forgot password",
reset: "Reset",
},
"sign-in": {
start: "Sign in to your",
end: "account.",
},
},
// Workspace Settings menu items
"workspaces—settings": {
general: "General Settings",
chat: "Chat Settings",
vector: "Vector Database",
members: "Members",
agent: "Agent Configuration",
},
// General Appearance
general: {
vector: {
title: "Vector Count",
description: "Total number of vectors in your vector database.",
},
names: {
description: "This will only change the display name of your workspace.",
},
message: {
title: "Suggested Chat Messages",
description:
"Customize the messages that will be suggested to your workspace users.",
add: "Add new message",
save: "Save Messages",
heading: "Explain to me",
body: "the benefits of AnythingLLM",
},
pfp: {
title: "Assistant Profile Image",
description:
"Customize the profile image of the assistant for this workspace.",
image: "Workspace Image",
remove: "Remove Workspace Image",
},
delete: {
delete: "Delete Workspace",
deleting: "Deleting Workspace...",
"confirm-start": "You are about to delete your entire",
"confirm-end":
"workspace. This will remove all vector embeddings in your vector database.\n\nThe original source files will remain untouched. This action is irreversible.",
},
},
// Chat Settings
chat: {
llm: {
title: "Workspace LLM Provider",
description:
"The specific LLM provider & model that will be used for this workspace. By default, it uses the system LLM provider and settings.",
search: "Search all LLM providers",
},
model: {
title: "Workspace Chat model",
description:
"The specific chat model that will be used for this workspace. If empty, will use the system LLM preference.",
wait: "-- waiting for models --",
},
mode: {
title: "Chat mode",
chat: {
title: "Chat",
"desc-start": "will provide answers with the LLM's general knowledge",
and: "and",
"desc-end": "document context that is found.",
},
query: {
title: "Query",
"desc-start": "will provide answers",
only: "only",
"desc-end": "if document context is found.",
},
},
history: {
title: "Chat History",
"desc-start":
"The number of previous chats that will be included in the response's short-term memory.",
recommend: "Recommend 20. ",
"desc-end":
"AAnything more than 45 is likely to lead to continuous chat failures depending on message size.",
},
prompt: {
title: "Prompt",
description:
"The prompt that will be used on this workspace. Define the context and instructions for the AI to generate a response. You should to provide a carefully crafted prompt so the AI can generate a relevant and accurate response.",
},
refusal: {
title: "Query mode refusal response",
"desc-start": "When in",
query: "query",
"desc-end":
"mode, you may want to return a custom refusal response when no context is found.",
},
temperature: {
title: "LLM Temperature",
"desc-start":
'This setting controls how "creative" your LLM responses will be.',
"desc-end":
"The higher the number the more creative. For some models this can lead to incoherent responses when set too high.",
hint: "Most LLMs have various acceptable ranges of valid values. Consult your LLM provider for that information.",
},
},
// Vector Database
"vector-workspace": {
identifier: "Vector database identifier",
snippets: {
title: "Max Context Snippets",
description:
"This setting controls the maximum amount of context snippets the will be sent to the LLM for per chat or query.",
recommend: "Recommended: 4",
},
doc: {
title: "Document similarity threshold",
description:
"The minimum similarity score required for a source to be considered related to the chat. The higher the number, the more similar the source must be to the chat.",
zero: "No restriction",
low: "Low (similarity score ≥ .25)",
medium: "Medium (similarity score ≥ .50)",
high: "High (similarity score ≥ .75)",
},
reset: {
reset: "Reset Vector Database",
resetting: "Clearing vectors...",
confirm:
"You are about to reset this workspace's vector database. This will remove all vector embeddings currently embedded.\n\nThe original source files will remain untouched. This action is irreversible.",
error: "Workspace vector database could not be reset!",
success: "Workspace vector database was reset!",
},
},
// Agent Configuration
agent: {
"performance-warning":
"Performance of LLMs that do not explicitly support tool-calling is highly dependent on the model's capabilities and accuracy. Some abilities may be limited or non-functional.",
provider: {
title: "Workspace Agent LLM Provider",
description:
"The specific LLM provider & model that will be used for this workspace's @agent agent.",
},
mode: {
chat: {
title: "Workspace Agent Chat model",
description:
"The specific chat model that will be used for this workspace's @agent agent.",
},
title: "Workspace Agent model",
description:
"The specific LLM model that will be used for this workspace's @agent agent.",
wait: "-- waiting for models --",
},
skill: {
title: "Default agent skills",
description:
"Improve the natural abilities of the default agent with these pre-built skills. This set up applies to all workspaces.",
rag: {
title: "RAG & long-term memory",
description:
'Allow the agent to leverage your local documents to answer a query or ask the agent to "remember" pieces of content for long-term memory retrieval.',
},
view: {
title: "View & summarize documents",
description:
"Allow the agent to list and summarize the content of workspace files currently embedded.",
},
scrape: {
title: "Scrape websites",
description:
"Allow the agent to visit and scrape the content of websites.",
},
generate: {
title: "Generate charts",
description:
"Enable the default agent to generate various types of charts from data provided or given in chat.",
},
save: {
title: "Generate & save files to browser",
description:
"Enable the default agent to generate and write to files that save and can be downloaded in your browser.",
},
web: {
title: "Live web search and browsing",
"desc-start":
"Enable your agent to search the web to answer your questions by connecting to a web-search (SERP) provider.",
"desc-end":
"Web search during agent sessions will not work until this is set up.",
},
},
},
// Workspace Chats
recorded: {
title: "Workspace Chats",
description:
"These are all the recorded chats and messages that have been sent by users ordered by their creation date.",
export: "Export",
table: {
id: "Id",
by: "Sent By",
workspace: "Workspace",
prompt: "Prompt",
response: "Response",
at: "Sent At",
},
},
// Appearance
appearance: {
title: "Appearance",
description: "Customize the appearance settings of your platform.",
logo: {
title: "Customize Logo",
description: "Upload your custom logo to make your chatbot yours.",
add: "Add a custom logo",
recommended: "Recommended size: 800 x 200",
remove: "Remove",
replace: "Replace",
},
message: {
title: "Customize Messages",
description: "Customize the automatic messages displayed to your users.",
new: "New",
system: "system",
user: "user",
message: "message",
assistant: "AnythingLLM Chat Assistant",
"double-click": "Double click to edit...",
save: "Save Messages",
},
icons: {
title: "Custom Footer Icons",
description:
"Customize the footer icons displayed on the bottom of the sidebar.",
icon: "Icon",
link: "Link",
},
},
// API Keys
api: {
title: "API Keys",
description:
"API keys allow the holder to programmatically access and manage this AnythingLLM instance.",
link: "Read the API documentation",
generate: "Generate New API Key",
table: {
key: "API Key",
by: "Created By",
created: "Created",
},
},
llm: {
title: "LLM Preference",
description:
"These are the credentials and settings for your preferred LLM chat & embedding provider. Its important these keys are current and correct or else AnythingLLM will not function properly.",
provider: "LLM Provider",
},
transcription: {
title: "Transcription Model Preference",
description:
"These are the credentials and settings for your preferred transcription model provider. Its important these keys are current and correct or else media files and audio will not transcribe.",
provider: "Transcription Provider",
"warn-start":
"Using the local whisper model on machines with limited RAM or CPU can stall AnythingLLM when processing media files.",
"warn-recommend":
"We recommend at least 2GB of RAM and upload files <10Mb.",
"warn-end":
"The built-in model will automatically download on the first use.",
},
embedding: {
title: "Embedding Preference",
"desc-start":
"When using an LLM that does not natively support an embedding engine - you may need to additionally specify credentials to for embedding text.",
"desc-end":
"Embedding is the process of turning text into vectors. These credentials are required to turn your files and prompts into a format which AnythingLLM can use to process.",
provider: {
title: "Embedding Provider",
description:
"There is no set up required when using AnythingLLM's native embedding engine.",
},
},
text: {
title: "Text splitting & Chunking Preferences",
"desc-start":
"Sometimes, you may want to change the default way that new documents are split and chunked before being inserted into your vector database.",
"desc-end":
"You should only modify this setting if you understand how text splitting works and it's side effects.",
"warn-start": "Changes here will only apply to",
"warn-center": "newly embedded documents",
"warn-end": ", not existing documents.",
size: {
title: "Text Chunk Size",
description:
"This is the maximum length of characters that can be present in a single vector.",
recommend: "Embed model maximum length is",
},
overlap: {
title: "Text Chunk Overlap",
description:
"This is the maximum overlap of characters that occurs during chunking between two adjacent text chunks.",
},
},
// Vector Database
vector: {
title: "Vector Database",
description:
"These are the credentials and settings for how your AnythingLLM instance will function. It's important these keys are current and correct.",
provider: {
title: "Vector Database Provider",
description: "There is no configuration needed for LanceDB.",
},
},
// Embeddable Chat Widgets
embeddable: {
title: "Embeddable Chat Widgets",
description:
"Embeddable chat widgets are public facing chat interfaces that are tied to a single workspace. These allow you to build workspaces that then you can publish to the world.",
create: "Create embed",
table: {
workspace: "Workspace",
chats: "Sent Chats",
Active: "Active Domains",
},
},
"embed-chats": {
title: "Embed Chats",
description:
"These are all the recorded chats and messages from any embed that you have published.",
table: {
embed: "Embed",
sender: "Sender",
message: "Message",
response: "Response",
at: "Sent At",
},
},
multi: {
title: "Multi-User Mode",
description:
"Set up your instance to support your team by activating Multi-User Mode.",
enable: {
"is-enable": "Multi-User Mode is Enabled",
enable: "Enable Multi-User Mode",
description:
"By default, you will be the only admin. As an admin you will need to create accounts for all new users or admins. Do not lose your password as only an Admin user can reset passwords.",
username: "Admin account username",
password: "Admin account password",
},
password: {
title: "Password Protection",
description:
"Protect your AnythingLLM instance with a password. If you forget this there is no recovery method so ensure you save this password.",
},
instance: {
title: "Password Protect Instance",
description:
"By default, you will be the only admin. As an admin you will need to create accounts for all new users or admins. Do not lose your password as only an Admin user can reset passwords.",
password: "Instance password",
},
},
// Event Logs
event: {
title: "Event Logs",
description:
"View all actions and events happening on this instance for monitoring.",
clear: "Clear Event Logs",
table: {
type: "Event Type",
user: "User",
occurred: "Occurred At",
},
},
// Privacy & Data-Handling
privacy: {
title: "Privacy & Data-Handling",
description:
"This is your configuration for how connected third party providers and AnythingLLM handle your data.",
llm: "LLM Selection",
embedding: "Embedding Preference",
vector: "Vector Database",
anonymous: "Anonymous Telemetry Enabled",
},
};
export default TRANSLATIONS;

View File

@ -0,0 +1,440 @@
const TRANSLATIONS = {
common: {
"workspaces-name": "Nombre de espacios de trabajo",
error: "error",
success: "éxito",
user: "Usuario",
selection: "Selección de modelo",
saving: "Guardando...",
save: "Guardar cambios",
previous: "Página anterior",
next: "Página siguiente",
},
settings: {
title: "Configuración de instancia",
system: "Preferencias del sistema",
invites: "Invitación",
users: "Usuarios",
workspaces: "Espacios de trabajo",
"workspace-chats": "Chat del espacio de trabajo",
appearance: "Apariencia",
"api-keys": "Claves API",
llm: "Preferencia de LLM",
transcription: "Modelo de transcripción",
embedder: "Preferencias de incrustación",
"text-splitting": "Divisor y fragmentación de texto",
"vector-database": "Base de datos de vectores",
embeds: "Widgets de chat incrustados",
"embed-chats": "Historial de chats incrustados",
security: "Seguridad",
"event-logs": "Registros de eventos",
privacy: "Privacidad y datos",
},
login: {
"multi-user": {
welcome: "Bienvenido a",
"placeholder-username": "Nombre de usuario",
"placeholder-password": "Contraseña",
login: "Iniciar sesión",
validating: "Validando...",
"forgot-pass": "Olvidé mi contraseña",
reset: "Restablecer",
},
"sign-in": {
start: "Iniciar sesión en tu",
end: "cuenta.",
},
},
"workspaces—settings": {
general: "Configuración general",
chat: "Configuración de chat",
vector: "Base de datos de vectores",
members: "Miembros",
agent: "Configuración del agente",
},
general: {
vector: {
title: "Conteo de vectores",
description: "Número total de vectores en tu base de datos de vectores.",
},
names: {
description:
"Esto solo cambiará el nombre de visualización de tu espacio de trabajo.",
},
message: {
title: "Mensajes de chat sugeridos",
description:
"Personaliza los mensajes que se sugerirán a los usuarios de tu espacio de trabajo.",
add: "Agregar nuevo mensaje",
save: "Guardar mensajes",
heading: "Explícame",
body: "los beneficios de AnythingLLM",
},
pfp: {
title: "Imagen de perfil del asistente",
description:
"Personaliza la imagen de perfil del asistente para este espacio de trabajo.",
image: "Imagen del espacio de trabajo",
remove: "Eliminar imagen del espacio de trabajo",
},
delete: {
delete: "Eliminar espacio de trabajo",
deleting: "Eliminando espacio de trabajo...",
"confirm-start": "Estás a punto de eliminar tu",
"confirm-end":
"espacio de trabajo. Esto eliminará todas las incrustaciones de vectores en tu base de datos de vectores.\n\nLos archivos de origen originales permanecerán intactos. Esta acción es irreversible.",
},
},
chat: {
llm: {
title: "Proveedor LLM del espacio de trabajo",
description:
"El proveedor y modelo LLM específico que se utilizará para este espacio de trabajo. Por defecto, utiliza el proveedor y configuración del sistema LLM.",
search: "Buscar todos los proveedores LLM",
},
model: {
title: "Modelo de chat del espacio de trabajo",
description:
"El modelo de chat específico que se utilizará para este espacio de trabajo. Si está vacío, se utilizará la preferencia LLM del sistema.",
wait: "-- esperando modelos --",
},
mode: {
title: "Modo de chat",
chat: {
title: "Chat",
"desc-start":
"proporcionará respuestas con el conocimiento general del LLM",
and: "y",
"desc-end": "el contexto del documento que se encuentre.",
},
query: {
title: "Consulta",
"desc-start": "proporcionará respuestas",
only: "solo",
"desc-end": "si se encuentra el contexto del documento.",
},
},
history: {
title: "Historial de chat",
"desc-start":
"El número de chats anteriores que se incluirán en la memoria a corto plazo de la respuesta.",
recommend: "Recomendar 20. ",
"desc-end":
"Cualquier cosa más de 45 probablemente conducirá a fallos continuos en el chat dependiendo del tamaño del mensaje.",
},
prompt: {
title: "Prompt",
description:
"El prompt que se utilizará en este espacio de trabajo. Define el contexto y las instrucciones para que la IA genere una respuesta. Debes proporcionar un prompt cuidadosamente elaborado para que la IA pueda generar una respuesta relevante y precisa.",
},
refusal: {
title: "Respuesta de rechazo en modo consulta",
"desc-start": "Cuando esté en",
query: "consulta",
"desc-end":
"modo, es posible que desees devolver una respuesta de rechazo personalizada cuando no se encuentre contexto.",
},
temperature: {
title: "Temperatura de LLM",
"desc-start":
'Esta configuración controla cuán "creativas" serán las respuestas de tu LLM.',
"desc-end":
"Cuanto mayor sea el número, más creativas serán las respuestas. Para algunos modelos, esto puede llevar a respuestas incoherentes cuando se establece demasiado alto.",
hint: "La mayoría de los LLM tienen varios rangos aceptables de valores válidos. Consulta a tu proveedor de LLM para obtener esa información.",
},
},
"vector-workspace": {
identifier: "Identificador de la base de datos de vectores",
snippets: {
title: "Máximo de fragmentos de contexto",
description:
"Esta configuración controla la cantidad máxima de fragmentos de contexto que se enviarán al LLM por chat o consulta.",
recommend: "Recomendado: 4",
},
doc: {
title: "Umbral de similitud de documentos",
description:
"La puntuación mínima de similitud requerida para que una fuente se considere relacionada con el chat. Cuanto mayor sea el número, más similar debe ser la fuente al chat.",
zero: "Sin restricción",
low: "Bajo (puntuación de similitud ≥ .25)",
medium: "Medio (puntuación de similitud ≥ .50)",
high: "Alto (puntuación de similitud ≥ .75)",
},
reset: {
reset: "Restablecer la base de datos de vectores",
resetting: "Borrando vectores...",
confirm:
"Estás a punto de restablecer la base de datos de vectores de este espacio de trabajo. Esto eliminará todas las incrustaciones de vectores actualmente incrustadas.\n\nLos archivos de origen originales permanecerán intactos. Esta acción es irreversible.",
error:
"¡No se pudo restablecer la base de datos de vectores del espacio de trabajo!",
success:
"¡La base de datos de vectores del espacio de trabajo fue restablecida!",
},
},
agent: {
"performance-warning":
"El rendimiento de los LLM que no admiten explícitamente la llamada de herramientas depende en gran medida de las capacidades y la precisión del modelo. Algunas habilidades pueden estar limitadas o no funcionar.",
provider: {
title: "Proveedor de LLM del agente del espacio de trabajo",
description:
"El proveedor y modelo LLM específico que se utilizará para el agente @agent de este espacio de trabajo.",
},
mode: {
chat: {
title: "Modelo de chat del agente del espacio de trabajo",
description:
"El modelo de chat específico que se utilizará para el agente @agent de este espacio de trabajo.",
},
title: "Modelo del agente del espacio de trabajo",
description:
"El modelo LLM específico que se utilizará para el agente @agent de este espacio de trabajo.",
wait: "-- esperando modelos --",
},
skill: {
title: "Habilidades predeterminadas del agente",
description:
"Mejora las habilidades naturales del agente predeterminado con estas habilidades preconstruidas. Esta configuración se aplica a todos los espacios de trabajo.",
rag: {
title: "RAG y memoria a largo plazo",
description:
'Permitir que el agente aproveche tus documentos locales para responder a una consulta o pedirle al agente que "recuerde" piezas de contenido para la recuperación de memoria a largo plazo.',
},
view: {
title: "Ver y resumir documentos",
description:
"Permitir que el agente enumere y resuma el contenido de los archivos del espacio de trabajo actualmente incrustados.",
},
scrape: {
title: "Rastrear sitios web",
description:
"Permitir que el agente visite y rastree el contenido de sitios web.",
},
generate: {
title: "Generar gráficos",
description:
"Habilitar al agente predeterminado para generar varios tipos de gráficos a partir de datos proporcionados o dados en el chat.",
},
save: {
title: "Generar y guardar archivos en el navegador",
description:
"Habilitar al agente predeterminado para generar y escribir archivos que se guarden y puedan descargarse en tu navegador.",
},
web: {
title: "Búsqueda en vivo en la web y navegación",
"desc-start":
"Permitir que tu agente busque en la web para responder tus preguntas conectándose a un proveedor de búsqueda en la web (SERP).",
"desc-end":
"La búsqueda en la web durante las sesiones del agente no funcionará hasta que esto esté configurado.",
},
},
},
recorded: {
title: "Chats del espacio de trabajo",
description:
"Estos son todos los chats y mensajes grabados que han sido enviados por los usuarios ordenados por su fecha de creación.",
export: "Exportar",
table: {
id: "Id",
by: "Enviado por",
workspace: "Espacio de trabajo",
prompt: "Prompt",
response: "Respuesta",
at: "Enviado a",
},
},
appearance: {
title: "Apariencia",
description: "Personaliza la configuración de apariencia de tu plataforma.",
logo: {
title: "Personalizar logotipo",
description:
"Sube tu logotipo personalizado para hacer que tu chatbot sea tuyo.",
add: "Agregar un logotipo personalizado",
recommended: "Tamaño recomendado: 800 x 200",
remove: "Eliminar",
replace: "Reemplazar",
},
message: {
title: "Personalizar mensajes",
description:
"Personaliza los mensajes automáticos que se muestran a tus usuarios.",
new: "Nuevo",
system: "sistema",
user: "usuario",
message: "mensaje",
assistant: "Asistente de chat AnythingLLM",
"double-click": "Haz doble clic para editar...",
save: "Guardar mensajes",
},
icons: {
title: "Iconos de pie de página personalizados",
description:
"Personaliza los iconos de pie de página que se muestran en la parte inferior de la barra lateral.",
icon: "Icono",
link: "Enlace",
},
},
api: {
title: "Claves API",
description:
"Las claves API permiten al titular acceder y gestionar programáticamente esta instancia de AnythingLLM.",
link: "Leer la documentación de la API",
generate: "Generar nueva clave API",
table: {
key: "Clave API",
by: "Creado por",
created: "Creado",
},
},
llm: {
title: "Preferencia de LLM",
description:
"Estas son las credenciales y configuraciones para tu proveedor preferido de chat y incrustación de LLM. Es importante que estas claves estén actualizadas y correctas, de lo contrario AnythingLLM no funcionará correctamente.",
provider: "Proveedor de LLM",
},
transcription: {
title: "Preferencia de modelo de transcripción",
description:
"Estas son las credenciales y configuraciones para tu proveedor preferido de modelo de transcripción. Es importante que estas claves estén actualizadas y correctas, de lo contrario los archivos multimedia y de audio no se transcribirán.",
provider: "Proveedor de transcripción",
"warn-start":
"El uso del modelo local Whisper en máquinas con RAM o CPU limitadas puede bloquear AnythingLLM al procesar archivos multimedia.",
"warn-recommend":
"Recomendamos al menos 2GB de RAM y subir archivos <10Mb.",
"warn-end":
"El modelo incorporado se descargará automáticamente en el primer uso.",
},
embedding: {
title: "Preferencia de incrustación",
"desc-start":
"Cuando uses un LLM que no admita de forma nativa un motor de incrustación, es posible que necesites especificar credenciales adicionales para incrustar texto.",
"desc-end":
"La incrustación es el proceso de convertir texto en vectores. Estas credenciales son necesarias para convertir tus archivos y prompts en un formato que AnythingLLM pueda usar para procesar.",
provider: {
title: "Proveedor de incrustación",
description:
"No se requiere configuración cuando se utiliza el motor de incrustación nativo de AnythingLLM.",
},
},
text: {
title: "Preferencias de división y fragmentación de texto",
"desc-start":
"A veces, es posible que desees cambiar la forma predeterminada en que los nuevos documentos se dividen y fragmentan antes de ser insertados en tu base de datos de vectores.",
"desc-end":
"Solo debes modificar esta configuración si entiendes cómo funciona la división de texto y sus efectos secundarios.",
"warn-start": "Los cambios aquí solo se aplicarán a",
"warn-center": "documentos recién incrustados",
"warn-end": ", no a los documentos existentes.",
size: {
title: "Tamaño del fragmento de texto",
description:
"Esta es la longitud máxima de caracteres que puede estar presente en un solo vector.",
recommend: "La longitud máxima del modelo de incrustación es",
},
overlap: {
title: "Superposición de fragmentos de texto",
description:
"Esta es la superposición máxima de caracteres que ocurre durante la fragmentación entre dos fragmentos de texto adyacentes.",
},
},
vector: {
title: "Base de datos de vectores",
description:
"Estas son las credenciales y configuraciones para cómo funcionará tu instancia de AnythingLLM. Es importante que estas claves estén actualizadas y correctas.",
provider: {
title: "Proveedor de base de datos de vectores",
description: "No se necesita configuración para LanceDB.",
},
},
embeddable: {
title: "Widgets de chat incrustables",
description:
"Los widgets de chat incrustables son interfaces de chat de cara al público que están vinculadas a un solo espacio de trabajo. Esto te permite crear espacios de trabajo que luego puedes publicar al mundo.",
create: "Crear incrustación",
table: {
workspace: "Espacio de trabajo",
chats: "Chats enviados",
Active: "Dominios activos",
},
},
"embed-chats": {
title: "Incrustar chats",
description:
"Estos son todos los chats y mensajes grabados de cualquier incrustación que hayas publicado.",
table: {
embed: "Incrustar",
sender: "Remitente",
message: "Mensaje",
response: "Respuesta",
at: "Enviado a",
},
},
multi: {
title: "Modo multiusuario",
description:
"Configura tu instancia para admitir a tu equipo activando el modo multiusuario.",
enable: {
"is-enable": "El modo multiusuario está habilitado",
enable: "Habilitar modo multiusuario",
description:
"Por defecto, serás el único administrador. Como administrador, necesitarás crear cuentas para todos los nuevos usuarios o administradores. No pierdas tu contraseña ya que solo un usuario administrador puede restablecer las contraseñas.",
username: "Nombre de usuario de la cuenta de administrador",
password: "Contraseña de la cuenta de administrador",
},
password: {
title: "Protección con contraseña",
description:
"Protege tu instancia de AnythingLLM con una contraseña. Si olvidas esta contraseña, no hay método de recuperación, así que asegúrate de guardar esta contraseña.",
},
instance: {
title: "Proteger instancia con contraseña",
description:
"Por defecto, serás el único administrador. Como administrador, necesitarás crear cuentas para todos los nuevos usuarios o administradores. No pierdas tu contraseña ya que solo un usuario administrador puede restablecer las contraseñas.",
password: "Contraseña de la instancia",
},
},
event: {
title: "Registros de eventos",
description:
"Ver todas las acciones y eventos que ocurren en esta instancia para monitoreo.",
clear: "Borrar registros de eventos",
table: {
type: "Tipo de evento",
user: "Usuario",
occurred: "Ocurrido a",
},
},
privacy: {
title: "Privacidad y manejo de datos",
description:
"Esta es tu configuración para cómo los proveedores de terceros conectados y AnythingLLM manejan tus datos.",
llm: "Selección de LLM",
embedding: "Preferencia de incrustación",
vector: "Base de datos de vectores",
anonymous: "Telemetría anónima habilitada",
},
};
export default TRANSLATIONS;

View File

@ -0,0 +1,456 @@
const TRANSLATIONS = {
common: {
"workspaces-name": "Nom des espaces de travail",
error: "erreur",
success: "succès",
user: "Utilisateur",
selection: "Sélection du modèle",
saving: "Enregistrement...",
save: "Enregistrer les modifications",
previous: "Page précédente",
next: "Page suivante",
},
// Setting Sidebar menu items.
settings: {
title: "Paramètres de l'instance",
system: "Préférences système",
invites: "Invitation",
users: "Utilisateurs",
workspaces: "Espaces de travail",
"workspace-chats": "Chat de l'espace de travail",
appearance: "Apparence",
"api-keys": "Clés API",
llm: "Préférence LLM",
transcription: "Modèle de transcription",
embedder: "Préférences d'intégration",
"text-splitting": "Diviseur de texte et découpage",
"vector-database": "Base de données vectorielle",
embeds: "Widgets de chat intégrés",
"embed-chats": "Historique des chats intégrés",
security: "Sécurité",
"event-logs": "Journaux d'événements",
privacy: "Confidentialité et données",
},
// Page Definitions
login: {
"multi-user": {
welcome: "Bienvenue à",
"placeholder-username": "Nom d'utilisateur",
"placeholder-password": "Mot de passe",
login: "Connexion",
validating: "Validation...",
"forgot-pass": "Mot de passe oublié",
reset: "Réinitialiser",
},
"sign-in": {
start: "Connectez-vous à votre",
end: "compte.",
},
},
// Workspace Settings menu items
"workspaces—settings": {
general: "Paramètres généraux",
chat: "Paramètres de chat",
vector: "Base de données vectorielle",
members: "Membres",
agent: "Configuration de l'agent",
},
// General Appearance
general: {
vector: {
title: "Nombre de vecteurs",
description:
"Nombre total de vecteurs dans votre base de données vectorielle.",
},
names: {
description:
"Cela ne changera que le nom d'affichage de votre espace de travail.",
},
message: {
title: "Messages de chat suggérés",
description:
"Personnalisez les messages qui seront suggérés aux utilisateurs de votre espace de travail.",
add: "Ajouter un nouveau message",
save: "Enregistrer les messages",
heading: "Expliquez-moi",
body: "les avantages de AnythingLLM",
},
pfp: {
title: "Image de profil de l'assistant",
description:
"Personnalisez l'image de profil de l'assistant pour cet espace de travail.",
image: "Image de l'espace de travail",
remove: "Supprimer l'image de l'espace de travail",
},
delete: {
delete: "Supprimer l'espace de travail",
deleting: "Suppression de l'espace de travail...",
"confirm-start": "Vous êtes sur le point de supprimer votre",
"confirm-end":
"espace de travail. Cela supprimera toutes les intégrations vectorielles dans votre base de données vectorielle.\n\nLes fichiers source originaux resteront intacts. Cette action est irréversible.",
},
},
// Chat Settings
chat: {
llm: {
title: "Fournisseur LLM de l'espace de travail",
description:
"Le fournisseur et le modèle LLM spécifiques qui seront utilisés pour cet espace de travail. Par défaut, il utilise le fournisseur et les paramètres LLM du système.",
search: "Rechercher tous les fournisseurs LLM",
},
model: {
title: "Modèle de chat de l'espace de travail",
description:
"Le modèle de chat spécifique qui sera utilisé pour cet espace de travail. Si vide, utilisera la préférence LLM du système.",
wait: "-- en attente des modèles --",
},
mode: {
title: "Mode de chat",
chat: {
title: "Chat",
"desc-start":
"fournira des réponses avec les connaissances générales du LLM",
and: "et",
"desc-end": "le contexte du document trouvé.",
},
query: {
title: "Requête",
"desc-start": "fournira des réponses",
only: "uniquement",
"desc-end": "si un contexte de document est trouvé.",
},
},
history: {
title: "Historique des chats",
"desc-start":
"Le nombre de chats précédents qui seront inclus dans la mémoire à court terme de la réponse.",
recommend: "Recommandé: 20.",
"desc-end":
"Tout nombre supérieur à 45 risque de provoquer des échecs de chat continus en fonction de la taille du message.",
},
prompt: {
title: "Invite",
description:
"L'invite qui sera utilisée sur cet espace de travail. Définissez le contexte et les instructions pour que l'IA génère une réponse. Vous devez fournir une invite soigneusement conçue pour que l'IA puisse générer une réponse pertinente et précise.",
},
refusal: {
title: "Réponse de refus en mode requête",
"desc-start": "En mode",
query: "requête",
"desc-end":
", vous pouvez souhaiter retourner une réponse de refus personnalisée lorsque aucun contexte n'est trouvé.",
},
temperature: {
title: "Température LLM",
"desc-start":
"Ce paramètre contrôle le niveau de créativité des réponses de votre LLM.",
"desc-end":
"Plus le nombre est élevé, plus la réponse sera créative. Pour certains modèles, cela peut entraîner des réponses incohérentes si la valeur est trop élevée.",
hint: "La plupart des LLM ont diverses plages acceptables de valeurs valides. Consultez votre fournisseur LLM pour cette information.",
},
},
// Vector Database
"vector-workspace": {
identifier: "Identifiant de la base de données vectorielle",
snippets: {
title: "Nombre maximum de contextes",
description:
"Ce paramètre contrôle le nombre maximum de contextes qui seront envoyés au LLM par chat ou requête.",
recommend: "Recommandé: 4",
},
doc: {
title: "Seuil de similarité des documents",
description:
"Le score de similarité minimum requis pour qu'une source soit considérée comme liée au chat. Plus le nombre est élevé, plus la source doit être similaire au chat.",
zero: "Aucune restriction",
low: "Bas (score de similarité ≥ .25)",
medium: "Moyen (score de similarité ≥ .50)",
high: "Élevé (score de similarité ≥ .75)",
},
reset: {
reset: "Réinitialiser la base de données vectorielle",
resetting: "Effacement des vecteurs...",
confirm:
"Vous êtes sur le point de réinitialiser la base de données vectorielle de cet espace de travail. Cela supprimera toutes les intégrations vectorielles actuellement intégrées.\n\nLes fichiers source originaux resteront intacts. Cette action est irréversible.",
error:
"La base de données vectorielle de l'espace de travail n'a pas pu être réinitialisée !",
success:
"La base de données vectorielle de l'espace de travail a été réinitialisée !",
},
},
// Agent Configuration
agent: {
"performance-warning":
"La performance des LLM qui ne supportent pas explicitement l'appel d'outils dépend fortement des capacités et de la précision du modèle. Certaines capacités peuvent être limitées ou non fonctionnelles.",
provider: {
title: "Fournisseur LLM de l'agent de l'espace de travail",
description:
"Le fournisseur et le modèle LLM spécifiques qui seront utilisés pour l'agent @agent de cet espace de travail.",
},
mode: {
chat: {
title: "Modèle de chat de l'agent de l'espace de travail",
description:
"Le modèle de chat spécifique qui sera utilisé pour l'agent @agent de cet espace de travail.",
},
title: "Modèle de l'agent de l'espace de travail",
description:
"Le modèle LLM spécifique qui sera utilisé pour l'agent @agent de cet espace de travail.",
wait: "-- en attente des modèles --",
},
skill: {
title: "Compétences par défaut de l'agent",
description:
"Améliorez les capacités naturelles de l'agent par défaut avec ces compétences préconstruites. Cette configuration s'applique à tous les espaces de travail.",
rag: {
title: "RAG et mémoire à long terme",
description:
"Permettez à l'agent de s'appuyer sur vos documents locaux pour répondre à une requête ou demandez à l'agent de se souvenir de morceaux de contenu pour la récupération de mémoire à long terme.",
},
view: {
title: "Voir et résumer des documents",
description:
"Permettez à l'agent de lister et de résumer le contenu des fichiers de l'espace de travail actuellement intégrés.",
},
scrape: {
title: "Récupérer des sites web",
description:
"Permettez à l'agent de visiter et de récupérer le contenu des sites web.",
},
generate: {
title: "Générer des graphiques",
description:
"Activez l'agent par défaut pour générer différents types de graphiques à partir des données fournies ou données dans le chat.",
},
save: {
title: "Générer et sauvegarder des fichiers dans le navigateur",
description:
"Activez l'agent par défaut pour générer et écrire des fichiers qui peuvent être sauvegardés et téléchargés dans votre navigateur.",
},
web: {
title: "Recherche web en direct et navigation",
"desc-start":
"Permettez à votre agent de rechercher sur le web pour répondre à vos questions en se connectant à un fournisseur de recherche web (SERP).",
"desc-end":
"La recherche web pendant les sessions d'agent ne fonctionnera pas tant que cela ne sera pas configuré.",
},
},
},
// Workspace Chats
recorded: {
title: "Chats de l'espace de travail",
description:
"Voici tous les chats et messages enregistrés qui ont été envoyés par les utilisateurs, classés par date de création.",
export: "Exporter",
table: {
id: "Id",
by: "Envoyé par",
workspace: "Espace de travail",
prompt: "Invite",
response: "Réponse",
at: "Envoyé à",
},
},
// Appearance
appearance: {
title: "Apparence",
description:
"Personnalisez les paramètres d'apparence de votre plateforme.",
logo: {
title: "Personnaliser le logo",
description:
"Téléchargez votre logo personnalisé pour rendre votre chatbot unique.",
add: "Ajouter un logo personnalisé",
recommended: "Taille recommandée: 800 x 200",
remove: "Supprimer",
replace: "Remplacer",
},
message: {
title: "Personnaliser les messages",
description:
"Personnalisez les messages automatiques affichés à vos utilisateurs.",
new: "Nouveau",
system: "système",
user: "utilisateur",
message: "message",
assistant: "Assistant de chat AnythingLLM",
"double-click": "Double-cliquez pour modifier...",
save: "Enregistrer les messages",
},
icons: {
title: "Icônes de pied de page personnalisées",
description:
"Personnalisez les icônes de pied de page affichées en bas de la barre latérale.",
icon: "Icône",
link: "Lien",
},
},
// API Keys
api: {
title: "Clés API",
description:
"Les clés API permettent au titulaire d'accéder et de gérer de manière programmatique cette instance AnythingLLM.",
link: "Lisez la documentation de l'API",
generate: "Générer une nouvelle clé API",
table: {
key: "Clé API",
by: "Créé par",
created: "Créé",
},
},
llm: {
title: "Préférence LLM",
description:
"Voici les identifiants et les paramètres de votre fournisseur LLM de chat et d'intégration préféré. Il est important que ces clés soient actuelles et correctes, sinon AnythingLLM ne fonctionnera pas correctement.",
provider: "Fournisseur LLM",
},
transcription: {
title: "Préférence du modèle de transcription",
description:
"Voici les identifiants et les paramètres de votre fournisseur de modèle de transcription préféré. Il est important que ces clés soient actuelles et correctes, sinon les fichiers multimédias et audio ne seront pas transcrits.",
provider: "Fournisseur de transcription",
"warn-start":
"L'utilisation du modèle local whisper sur des machines avec une RAM ou un CPU limités peut bloquer AnythingLLM lors du traitement des fichiers multimédias.",
"warn-recommend":
"Nous recommandons au moins 2 Go de RAM et des fichiers téléchargés <10 Mo.",
"warn-end":
"Le modèle intégré se téléchargera automatiquement lors de la première utilisation.",
},
embedding: {
title: "Préférence d'intégration",
"desc-start":
"Lorsque vous utilisez un LLM qui ne supporte pas nativement un moteur d'intégration - vous devrez peut-être spécifier en plus des identifiants pour intégrer le texte.",
"desc-end":
"L'intégration est le processus de transformation du texte en vecteurs. Ces identifiants sont nécessaires pour transformer vos fichiers et invites en un format que AnythingLLM peut utiliser pour traiter.",
provider: {
title: "Fournisseur d'intégration",
description:
"Aucune configuration n'est nécessaire lors de l'utilisation du moteur d'intégration natif de AnythingLLM.",
},
},
text: {
title: "Préférences de division et de découpage du texte",
"desc-start":
"Parfois, vous voudrez peut-être changer la façon dont les nouveaux documents sont divisés et découpés avant d'être insérés dans votre base de données vectorielle.",
"desc-end":
"Vous ne devez modifier ce paramètre que si vous comprenez comment fonctionne la division du texte et ses effets secondaires.",
"warn-start": "Les changements ici s'appliqueront uniquement aux",
"warn-center": "nouveaux documents intégrés",
"warn-end": ", pas aux documents existants.",
size: {
title: "Taille des segments de texte",
description:
"C'est la longueur maximale de caractères pouvant être présents dans un seul vecteur.",
recommend: "Longueur maximale du modèle d'intégration est",
},
overlap: {
title: "Chevauchement des segments de texte",
description:
"C'est le chevauchement maximal de caractères qui se produit pendant le découpage entre deux segments de texte adjacents.",
},
},
// Vector Database
vector: {
title: "Base de données vectorielle",
description:
"Voici les identifiants et les paramètres de fonctionnement de votre instance AnythingLLM. Il est important que ces clés soient actuelles et correctes.",
provider: {
title: "Fournisseur de base de données vectorielle",
description: "Aucune configuration n'est nécessaire pour LanceDB.",
},
},
// Embeddable Chat Widgets
embeddable: {
title: "Widgets de chat intégrables",
description:
"Les widgets de chat intégrables sont des interfaces de chat publiques associées à un espace de travail unique. Ils vous permettent de créer des espaces de travail que vous pouvez ensuite publier dans le monde entier.",
create: "Créer un widget intégré",
table: {
workspace: "Espace de travail",
chats: "Chats envoyés",
Active: "Domaines actifs",
},
},
"embed-chats": {
title: "Chats intégrés",
description:
"Voici tous les chats et messages enregistrés de tout widget intégré que vous avez publié.",
table: {
embed: "Intégration",
sender: "Expéditeur",
message: "Message",
response: "Réponse",
at: "Envoyé à",
},
},
multi: {
title: "Mode multi-utilisateurs",
description:
"Configurez votre instance pour prendre en charge votre équipe en activant le mode multi-utilisateurs.",
enable: {
"is-enable": "Le mode multi-utilisateurs est activé",
enable: "Activer le mode multi-utilisateurs",
description:
"Par défaut, vous serez le seul administrateur. En tant qu'administrateur, vous devrez créer des comptes pour tous les nouveaux utilisateurs ou administrateurs. Ne perdez pas votre mot de passe car seul un utilisateur administrateur peut réinitialiser les mots de passe.",
username: "Nom d'utilisateur du compte administrateur",
password: "Mot de passe du compte administrateur",
},
password: {
title: "Protection par mot de passe",
description:
"Protégez votre instance AnythingLLM avec un mot de passe. Si vous oubliez ce mot de passe, il n'y a pas de méthode de récupération, donc assurez-vous de le sauvegarder.",
},
instance: {
title: "Protéger l'instance par mot de passe",
description:
"Par défaut, vous serez le seul administrateur. En tant qu'administrateur, vous devrez créer des comptes pour tous les nouveaux utilisateurs ou administrateurs. Ne perdez pas votre mot de passe car seul un utilisateur administrateur peut réinitialiser les mots de passe.",
password: "Mot de passe de l'instance",
},
},
// Event Logs
event: {
title: "Journaux d'événements",
description:
"Consultez toutes les actions et événements se produisant sur cette instance pour la surveillance.",
clear: "Effacer les journaux d'événements",
table: {
type: "Type d'événement",
user: "Utilisateur",
occurred: "Survenu à",
},
},
// Privacy & Data-Handling
privacy: {
title: "Confidentialité et gestion des données",
description:
"Voici votre configuration pour la gestion des données et des fournisseurs tiers connectés avec AnythingLLM.",
llm: "Sélection LLM",
embedding: "Préférence d'intégration",
vector: "Base de données vectorielle",
anonymous: "Télémétrie anonyme activée",
},
};
export default TRANSLATIONS;

View File

@ -0,0 +1,32 @@
// Looking for a language to translate AnythingLLM to?
// Create a `common.js` file in the language's ISO code https://www.w3.org/International/O-charset-lang.html
// eg: Spanish => es/common.js
// eg: French => fr/common.js
// You should copy the en/common.js file as your template and just translate every string in there.
// By default, we try to see what the browsers native language is set to and use that. If a string
// is not defined or is null in the translation file, it will fallback to the value in the en/common.js file
// RULES:
// The EN translation file is the ground-truth for what keys and options are available. DO NOT add a special key
// to a specific language file as this will break the other languages. Any new keys should be added to english
// and the language file you are working on.
import English from "./en/common.js";
import Spanish from "./es/common.js";
import French from "./fr/common.js";
import Mandarin from "./zh/common.js";
export const defaultNS = "common";
export const resources = {
en: {
common: English,
},
zh: {
common: Mandarin,
},
es: {
common: Spanish,
},
fr: {
common: French,
},
};

View File

@ -0,0 +1,96 @@
import { resources } from "./resources.js";
const languageNames = new Intl.DisplayNames(Object.keys(resources), {
type: "language",
});
function langDisplayName(lang) {
return languageNames.of(lang);
}
function compareStructures(lang, a, b, subdir = null) {
//if a and b aren't the same type, they can't be equal
if (typeof a !== typeof b) {
console.log("Invalid type comparison", [
{
lang,
a: typeof a,
b: typeof b,
...(!!subdir ? { subdir } : {}),
},
]);
return false;
}
// Need the truthy guard because
// typeof null === 'object'
if (a && typeof a === "object") {
var keysA = Object.keys(a).sort(),
keysB = Object.keys(b).sort();
//if a and b are objects with different no of keys, unequal
if (keysA.length !== keysB.length) {
console.log("Keys are missing!", {
[lang]: keysA,
en: keysB,
...(!!subdir ? { subdir } : {}),
diff: {
added: keysB.filter((key) => !keysA.includes(key)),
removed: keysA.filter((key) => !keysB.includes(key)),
},
});
return false;
}
//if keys aren't all the same, unequal
if (
!keysA.every(function (k, i) {
return k === keysB[i];
})
) {
console.log("Keys are not equal!", {
[lang]: keysA,
en: keysB,
...(!!subdir ? { subdir } : {}),
});
return false;
}
//recurse on the values for each key
return keysA.every(function (key) {
//if we made it here, they have identical keys
return compareStructures(lang, a[key], b[key], key);
});
//for primitives just ignore since we don't check values.
} else {
return true;
}
}
const failed = [];
const TRANSLATIONS = {};
for (const [lang, { common }] of Object.entries(resources))
TRANSLATIONS[lang] = common;
const PRIMARY = { ...TRANSLATIONS["en"] };
delete TRANSLATIONS["en"];
console.log(
`The following translation files will be verified: [${Object.keys(
TRANSLATIONS
).join(",")}]`
);
for (const [lang, translations] of Object.entries(TRANSLATIONS)) {
const passed = compareStructures(lang, translations, PRIMARY);
console.log(`${langDisplayName(lang)} (${lang}): ${passed ? "✅" : "❌"}`);
!passed && failed.push(lang);
}
if (failed.length !== 0)
throw new Error(
`The following translations files are INVALID and need fixing. Please see logs`,
failed
);
console.log(
`👍 All translation files located match the schema defined by the English file!`
);
process.exit(0);

View File

@ -0,0 +1,424 @@
// Anything with "null" requires a translation. Contribute to translation via a PR!
const TRANSLATIONS = {
common: {
"workspaces-name": "工作区名称",
error: "错误",
success: "成功",
user: "用户",
selection: "模型选择",
save: "保存更改",
saving: "保存中...",
previous: "上一页",
next: "下一页",
},
// Setting Sidebar menu items.
settings: {
title: "设置",
system: "系统",
invites: "邀请",
users: "用户",
workspaces: "工作区",
"workspace-chats": "对话历史记录", // "workspace-chats" should be "对话历史记录", means "chat history",or "chat history records"
appearance: "外观",
"api-keys": "API 密钥",
llm: "LLM 首选项",
transcription: "Transcription 模型",
embedder: "Embedder 首选项",
"text-splitting": "文本分割",
"vector-database": "向量数据库",
embeds: "嵌入式对话",
"embed-chats": "嵌入式对话历史",
security: "用户与安全",
"event-logs": "事件日志",
privacy: "隐私与数据",
},
// Page Definitions
login: {
"multi-user": {
welcome: "欢迎!",
"placeholder-username": "请输入用户名",
"placeholder-password": "请输入密码",
login: "登录",
validating: "登录",
"forgot-pass": "忘记密码",
reset: "重置",
},
"sign-in": {
start: "登录你的",
end: "账户",
},
},
// Workspace Settings menu items
"workspaces—settings": {
general: "通用设置",
chat: "聊天设置",
vector: "向量数据库",
members: "成员",
agent: "代理配置",
},
// General Appearance
general: {
vector: {
title: "向量数量",
description: "向量数据库中的总向量数。",
},
names: {
description: "这只会更改工作区的显示名称。",
},
message: {
title: "建议的聊天消息",
description: "自定义将向您的工作区用户建议的消息。",
add: "添加新消息",
save: "保存消息",
heading: "向我解释",
body: "AnythingLLM的好处",
},
pfp: {
title: "助理头像",
description: "为此工作区自定义助手的个人资料图像。",
image: "工作区图像",
remove: "移除工作区图像",
},
delete: {
delete: "删除工作区",
deleting: "正在删除工作区...",
"confirm-start": "您即将删除整个",
"confirm-end":
"工作区。这将删除矢量数据库中的所有矢量嵌入。\n\n原始源文件将保持不变。此操作是不可逆转的。",
},
},
// Chat Settings
chat: {
llm: {
title: "工作区LLM提供者",
description:
"将用于此工作区的特定 LLM 提供商和模型。默认情况下,它使用系统 LLM 提供程序和设置。",
search: "搜索所有 LLM 提供商",
},
model: {
title: "工作区聊天模型",
description:
"将用于此工作区的特定聊天模型。如果为空将使用系统LLM首选项。",
wait: "-- 等待模型 --",
},
mode: {
title: "聊天模式",
chat: {
title: "聊天",
"desc-start": "将提供法学硕士的一般知识",
and: "和",
"desc-end": "找到的文档上下文的答案。",
},
query: {
title: "查询",
"desc-start": "将",
only: "仅",
"desc-end": "提供找到的文档上下文的答案。",
},
},
history: {
title: "聊天历史记录",
"desc-start": "将包含在响应的短期记忆中的先前聊天的数量。",
recommend: "推荐 20。",
"desc-end":
"任何超过 45 的值都可能导致连续聊天失败,具体取决于消息大小。",
},
prompt: {
title: "聊天提示",
description:
"将在此工作区上使用的提示。定义 AI 生成响应的上下文和指令。您应该提供精心设计的提示,以便人工智能可以生成相关且准确的响应。",
},
refusal: {
title: "查询模式拒绝响应",
"desc-start": "当处于",
query: "查询",
"desc-end": "模式时,当未找到上下文时,您可能希望返回自定义拒绝响应。",
},
temperature: {
title: "LLM Temperature",
"desc-start": "此设置控制您的 LLM 回答的“创意”程度",
"desc-end":
"数字越高越有创意。对于某些模型,如果设置得太高,可能会导致响应不一致。",
hint: "大多数法学硕士都有各种可接受的有效值范围。请咨询您的法学硕士提供商以获取该信息。",
},
},
// Vector Database Settings
"vector-workspace": {
identifier: "向量数据库标识符",
snippets: {
title: "最大上下文片段",
description:
"此设置控制每次聊天或查询将发送到 LLM 的上下文片段的最大数量。",
recommend: "推荐: 4",
},
doc: {
title: "文档相似性阈值",
description:
"源被视为与聊天相关所需的最低相似度分数。数字越高,来源与聊天就越相似。",
zero: "无限制",
low: "低(相似度分数 ≥ .25",
medium: "中(相似度分数 ≥ .50",
high: "高(相似度分数 ≥ .75",
},
reset: {
reset: "重置向量数据库",
resetting: "清除向量...",
confirm:
"您将重置此工作区的矢量数据库。这将删除当前嵌入的所有矢量嵌入。\n\n原始源文件将保持不变。此操作是不可逆转的。",
success: "向量数据库已重置。",
error: "无法重置工作区向量数据库!",
},
},
// Agent Configuration
agent: {
"performance-warning":
"不明确支持工具调用的 LLMs 的性能高度依赖于模型的功能和准确性。有些能力可能受到限制或不起作用。",
provider: {
title: "工作区代理 LLM 提供商",
description: "将用于此工作区的 @agent 代理的特定 LLM 提供商和模型。",
},
mode: {
chat: {
title: "工作区代理聊天模型",
description: "将用于此工作区的 @agent 代理的特定聊天模型。",
},
title: "工作区代理模型",
description: "将用于此工作区的 @agent 代理的特定 LLM 模型。",
wait: "-- 等待模型 --",
},
skill: {
title: "默认代理技能",
description:
"使用这些预构建的技能提高默认代理的自然能力。此设置适用于所有工作区。",
rag: {
title: "RAG和长期记忆",
description:
'允许代理利用您的本地文档来回答查询,或要求代理"记住"长期记忆检索的内容片段。',
},
view: {
title: "查看和总结文档",
description: "允许代理列出和总结当前嵌入的工作区文件的内容。",
},
scrape: {
title: "抓取网站",
description: "允许代理访问和抓取网站的内容。",
},
generate: {
title: "生成图表",
description: "使默认代理能够从提供的数据或聊天中生成各种类型的图表。",
},
save: {
title: "生成并保存文件到浏览器",
description:
"使默认代理能够生成并写入文件,这些文件可以保存并在您的浏览器中下载。",
},
web: {
title: "实时网络搜索和浏览",
"desc-start":
"通过连接到网络搜索SERP提供者使您的代理能够搜索网络以回答您的问题。",
"desc-end": "在代理会话期间,网络搜索将不起作用,直到此设置完成。",
},
},
},
// Workspace Chat
recorded: {
title: "工作区聊天历史记录",
description: "这些是用户发送的所有聊天记录和消息,按创建日期排序。",
export: "导出",
table: {
id: "Id",
by: "Sent By",
workspace: "Workspace",
prompt: "Prompt",
response: "Response",
at: "Sent At",
},
},
appearance: {
title: "外观",
description: "自定义平台的外观设置。",
logo: {
title: "自定义图标",
description: "上传您的自定义图标,让您的聊天机器人成为您的。",
add: "添加自定义图标",
recommended: "建议尺寸800 x 200",
remove: "移除",
replace: "替换",
},
message: {
title: "自定义消息",
description: "自定义向用户显示的自动消息。",
new: "新建",
system: "系统",
user: "用户",
message: "消息",
assistant: "AnythingLLM 聊天助手",
"double-click": "双击以编辑...",
save: "保存消息",
},
icons: {
title: "自定义页脚图标",
description: "自定义侧边栏底部显示的页脚图标。",
icon: "图标",
link: "链接",
},
},
// API Keys
api: {
title: "API 密钥",
description: "API 密钥允许持有者以编程方式访问和管理此 AnythingLLM 实例。",
link: "阅读 API 文档",
generate: "生成新的 API 密钥",
table: {
key: "API 密钥",
by: "创建者",
created: "创建",
},
},
// LLM Preferences
llm: {
title: "LLM 偏好",
description:
"这些是您首选的 LLM 聊天和嵌入提供商的凭据和设置。重要的是,这些密钥是最新的和正确的,否则 AnythingLLM 将无法正常运行。",
provider: "LLM 提供商",
},
transcription: {
title: "转录模型偏好",
description:
"这些是您的首选转录模型提供商的凭据和设置。重要的是这些密钥是最新且正确的,否则媒体文件和音频将无法转录。",
provider: "转录提供商",
"warn-start":
"在 RAM 或 CPU 有限的计算机上使用本地耳语模型可能会在处理媒体文件时停止 AnythingLLM。",
"warn-recommend": "我们建议至少 2GB RAM 并上传 <10Mb 的文件。",
"warn-end": "内置模型将在首次使用时自动下载。",
},
embedding: {
title: "嵌入首选项",
"desc-start":
"当使用本身不支持嵌入引擎的 LLM 时,您可能需要额外指定用于嵌入文本的凭据。",
"desc-end":
"嵌入是将文本转换为矢量的过程。需要这些凭据才能将您的文件和提示转换为 AnythingLLM 可以用来处理的格式。",
provider: {
title: "嵌入引擎提供商",
description: "使用 AnythingLLM 的本机嵌入引擎时不需要设置。",
},
},
text: {
title: "文本拆分和分块首选项",
"desc-start":
"有时,您可能希望更改新文档在插入到矢量数据库之前拆分和分块的默认方式。",
"desc-end": "只有在了解文本拆分的工作原理及其副作用时,才应修改此设置。",
"warn-start": "此处的更改仅适用于",
"warn-center": "新嵌入的文档",
"warn-end": ",而不是现有文档。",
size: {
title: "文本块大小",
description: "这是单个向量中可以存在的字符的最大长度。",
recommend: "嵌入模型的最大长度为",
},
overlap: {
title: "文本块重叠",
description: "这是在两个相邻文本块之间分块期间发生的最大字符重叠。",
},
},
// Vector Database
vector: {
title: "向量数据库",
description:
"这些是 AnythingLLM 实例如何运行的凭据和设置。重要的是,这些密钥是最新的和正确的。",
provider: {
title: "向量数据库提供商",
description: "LanceDB 不需要任何配置。",
},
},
// Embeddable Chats
embeddable: {
title: "可嵌入的聊天小部件",
description:
"可嵌入的聊天小部件是与单个工作区绑定的面向公众的聊天界面。这些允许您构建工作区,然后您可以将其发布到全世界。",
create: "创建嵌入式对话",
table: {
workspace: "工作区",
chats: "已发送聊天",
Active: "活动域",
},
},
// Embeddable Chat History
"embed-chats": {
title: "嵌入聊天",
description: "这些是您发布的任何嵌入的所有记录的聊天和消息。",
table: {
embed: "嵌入",
sender: "发送者",
message: "消息",
response: "响应",
at: "发送于",
},
},
multi: {
title: "多用户模式",
description: "通过激活多用户模式来设置您的实例以支持您的团队。",
enable: {
"is-enable": "多用户模式已启用",
enable: "启用多用户模式",
description:
"默认情况下,您将是唯一的管理员。作为管理员,您需要为所有新用户或管理员创建账户。不要丢失您的密码,因为只有管理员用户可以重置密码。",
username: "管理员账户用户名",
password: "管理员账户密码",
},
password: {
title: "密码保护",
description:
"用密码保护您的AnythingLLM实例。如果您忘记了密码那么没有恢复方法所以请确保保存这个密码。",
},
instance: {
title: "实例密码保护",
description:
"默认情况下,您将是唯一的管理员。作为管理员,您需要为所有新用户或管理员创建账户。不要丢失您的密码,因为只有管理员用户可以重置密码。",
password: "实例密码",
},
},
// Event Logs
event: {
title: "事件日志",
description: "查看此实例上发生的所有操作和事件以进行监控。",
clear: "清除事件日志",
table: {
type: "事件类型",
user: "用户",
occurred: "发生时间",
},
},
// Privacy & Data-Handling
privacy: {
title: "隐私和数据处理",
description:
"这是您对如何处理连接的第三方提供商和AnythingLLM的数据的配置。",
llm: "LLM选择",
embedding: "嵌入偏好",
vector: "向量数据库",
anonymous: "启用匿名遥测",
},
};
export default TRANSLATIONS;

View File

@ -7,6 +7,7 @@ import * as Skeleton from "react-loading-skeleton";
import LogRow from "./LogRow"; import LogRow from "./LogRow";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import CTAButton from "@/components/lib/CTAButton"; import CTAButton from "@/components/lib/CTAButton";
import { useTranslation } from "react-i18next";
export default function AdminLogs() { export default function AdminLogs() {
const query = useQuery(); const query = useQuery();
@ -14,6 +15,7 @@ export default function AdminLogs() {
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [offset, setOffset] = useState(Number(query.get("offset") || 0)); const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false); const [canNext, setCanNext] = useState(false);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
async function fetchLogs() { async function fetchLogs() {
@ -62,12 +64,11 @@ export default function AdminLogs() {
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center"> <div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Event Logs {t("event.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
View all actions and events happening on this instance for {t("event.description")}
monitoring.
</p> </p>
</div> </div>
<div className="w-full justify-end flex"> <div className="w-full justify-end flex">
@ -75,7 +76,7 @@ export default function AdminLogs() {
onClick={handleResetLogs} onClick={handleResetLogs}
className="mt-3 mr-0 -mb-14 z-10" className="mt-3 mr-0 -mb-14 z-10"
> >
Clear Event Logs {t("event.clear")}
</CTAButton> </CTAButton>
</div> </div>
<LogsContainer <LogsContainer
@ -100,6 +101,7 @@ function LogsContainer({
handleNext, handleNext,
handlePrevious, handlePrevious,
}) { }) {
const { t } = useTranslation();
if (loading) { if (loading) {
return ( return (
<Skeleton.default <Skeleton.default
@ -120,13 +122,13 @@ function LogsContainer({
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60"> <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr> <tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg"> <th scope="col" className="px-6 py-3 rounded-tl-lg">
Event Type {t("event.table.type")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
User {t("event.table.user")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Occurred At {t("event.table.occurred")}
</th> </th>
<th scope="col" className="px-6 py-3 rounded-tr-lg"> <th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "} {" "}
@ -143,14 +145,14 @@ function LogsContainer({
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible" className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
disabled={offset === 0} disabled={offset === 0}
> >
Previous Page {t("common.previous")}
</button> </button>
<button <button
onClick={handleNext} onClick={handleNext}
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible" className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
disabled={!canNext} disabled={!canNext}
> >
Next Page {t("common.next")}
</button> </button>
</div> </div>
</> </>

View File

@ -1,9 +1,11 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { X } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react";
import Admin from "@/models/admin"; import Admin from "@/models/admin";
import { useTranslation } from "react-i18next";
export default function NewWorkspaceModal({ closeModal }) { export default function NewWorkspaceModal({ closeModal }) {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const { t } = useTranslation();
const handleCreate = async (e) => { const handleCreate = async (e) => {
setError(null); setError(null);
e.preventDefault(); e.preventDefault();
@ -37,7 +39,7 @@ export default function NewWorkspaceModal({ closeModal }) {
htmlFor="name" htmlFor="name"
className="block mb-2 text-sm font-medium text-white" className="block mb-2 text-sm font-medium text-white"
> >
Workspace name {t("common.workspaces-name")}
</label> </label>
<input <input
name="name" name="name"

View File

@ -13,10 +13,11 @@ import System from "@/models/system";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal"; import { useModal } from "@/hooks/useModal";
import CTAButton from "@/components/lib/CTAButton"; import CTAButton from "@/components/lib/CTAButton";
import { useTranslation } from "react-i18next";
export default function AdminApiKeys() { export default function AdminApiKeys() {
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
const { t } = useTranslation();
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar /> <Sidebar />
@ -27,11 +28,12 @@ export default function AdminApiKeys() {
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16"> <div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4"> <div className="items-center flex gap-x-4">
<p className="text-lg leading-6 font-bold text-white">API Keys</p> <p className="text-lg leading-6 font-bold text-white">
{t("api.title")}
</p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
API keys allow the holder to programmatically access and manage {t("api.description")}
this AnythingLLM instance.
</p> </p>
<a <a
href={paths.apiDocs()} href={paths.apiDocs()}
@ -39,13 +41,13 @@ export default function AdminApiKeys() {
rel="noreferrer" rel="noreferrer"
className="text-xs leading-[18px] font-base text-blue-300 hover:underline" className="text-xs leading-[18px] font-base text-blue-300 hover:underline"
> >
Read the API documentation &rarr; {t("api.link")} &rarr;
</a> </a>
</div> </div>
<div className="w-full justify-end flex"> <div className="w-full justify-end flex">
<CTAButton onClick={openModal} className="mt-3 mr-0 -mb-14 z-10"> <CTAButton onClick={openModal} className="mt-3 mr-0 -mb-14 z-10">
<PlusCircle className="h-4 w-4" weight="bold" /> Generate New API <PlusCircle className="h-4 w-4" weight="bold" />{" "}
Key {t("api.generate")}
</CTAButton> </CTAButton>
</div> </div>
<ApiKeysContainer /> <ApiKeysContainer />
@ -61,6 +63,7 @@ export default function AdminApiKeys() {
function ApiKeysContainer() { function ApiKeysContainer() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [apiKeys, setApiKeys] = useState([]); const [apiKeys, setApiKeys] = useState([]);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
async function fetchExistingKeys() { async function fetchExistingKeys() {
@ -92,13 +95,13 @@ function ApiKeysContainer() {
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60"> <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr> <tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg"> <th scope="col" className="px-6 py-3 rounded-tl-lg">
API Key {t("api.table.key")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Created By {t("api.table.by")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Created {t("api.table.created")}
</th> </th>
<th scope="col" className="px-6 py-3 rounded-tr-lg"> <th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "} {" "}

View File

@ -3,6 +3,7 @@ import System from "@/models/system";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Plus } from "@phosphor-icons/react"; import { Plus } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
export default function CustomLogo() { export default function CustomLogo() {
const { logo: _initLogo, setLogo: _setLogo } = useLogo(); const { logo: _initLogo, setLogo: _setLogo } = useLogo();
@ -65,15 +66,16 @@ export default function CustomLogo() {
const triggerFileInputClick = () => { const triggerFileInputClick = () => {
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
const { t } = useTranslation();
return ( return (
<div className="mt-6 mb-8"> <div className="mt-6 mb-8">
<div className="flex flex-col gap-y-1"> <div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white"> <h2 className="text-base leading-6 font-bold text-white">
Custom Logo {t("appearance.logo.title")}
</h2> </h2>
<p className="text-xs leading-[18px] font-base text-white/60"> <p className="text-xs leading-[18px] font-base text-white/60">
Upload your custom logo to make your chatbot yours. {t("appearance.logo.description")}
</p> </p>
</div> </div>
{isDefaultLogo ? ( {isDefaultLogo ? (
@ -99,10 +101,10 @@ export default function CustomLogo() {
<Plus className="w-6 h-6 text-black/80 m-2" /> <Plus className="w-6 h-6 text-black/80 m-2" />
</div> </div>
<div className="text-white text-opacity-80 text-sm font-semibold py-1"> <div className="text-white text-opacity-80 text-sm font-semibold py-1">
Add a custom logo {t("appearance.logo.add")}
</div> </div>
<div className="text-white text-opacity-60 text-xs font-medium py-1"> <div className="text-white text-opacity-60 text-xs font-medium py-1">
Recommended size: 800 x 200 {t("appearance.logo.recommended")}
</div> </div>
</div> </div>
</div> </div>
@ -123,7 +125,7 @@ export default function CustomLogo() {
onClick={triggerFileInputClick} onClick={triggerFileInputClick}
className="text-white text-base font-medium hover:text-opacity-60 mx-2" className="text-white text-base font-medium hover:text-opacity-60 mx-2"
> >
Replace {t("appearance.logo.replace")}
</button> </button>
<input <input
@ -138,7 +140,7 @@ export default function CustomLogo() {
onClick={handleRemoveLogo} onClick={handleRemoveLogo}
className="text-white text-base font-medium hover:text-opacity-60 mx-2" className="text-white text-base font-medium hover:text-opacity-60 mx-2"
> >
Remove {t("appearance.logo.remove")}
</button> </button>
</div> </div>
</div> </div>

View File

@ -3,10 +3,12 @@ import System from "@/models/system";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { Plus } from "@phosphor-icons/react"; import { Plus } from "@phosphor-icons/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
export default function CustomMessages() { export default function CustomMessages() {
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
async function fetchMessages() { async function fetchMessages() {
@ -20,12 +22,12 @@ export default function CustomMessages() {
if (type === "user") { if (type === "user") {
setMessages([ setMessages([
...messages, ...messages,
{ user: "Double click to edit...", response: "" }, { user: t("appearance.message.double-click"), response: "" },
]); ]);
} else { } else {
setMessages([ setMessages([
...messages, ...messages,
{ user: "", response: "Double click to edit..." }, { user: "", response: t("appearance.message.double-click") },
]); ]);
} }
}; };
@ -56,10 +58,10 @@ export default function CustomMessages() {
<div className="mb-8"> <div className="mb-8">
<div className="flex flex-col gap-y-1"> <div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white"> <h2 className="text-base leading-6 font-bold text-white">
Custom Messages {t("appearance.message.title")}
</h2> </h2>
<p className="text-xs leading-[18px] font-base text-white/60"> <p className="text-xs leading-[18px] font-base text-white/60">
Customize the automatic messages displayed to your users. {t("appearance.message.description")}
</p> </p>
</div> </div>
<div className="mt-3 flex flex-col gap-y-6 bg-dark-highlight rounded-lg pr-[31px] pl-[12px] pt-4 max-w-[700px]"> <div className="mt-3 flex flex-col gap-y-6 bg-dark-highlight rounded-lg pr-[31px] pl-[12px] pt-4 max-w-[700px]">
@ -93,8 +95,11 @@ export default function CustomMessages() {
<div className="flex items-center justify-start text-sm font-normal -ml-2"> <div className="flex items-center justify-start text-sm font-normal -ml-2">
<Plus className="m-2" size={16} weight="bold" /> <Plus className="m-2" size={16} weight="bold" />
<span className="leading-5"> <span className="leading-5">
New <span className="font-bold italic mr-1">system</span>{" "} {t("appearance.message.new")}{" "}
message <span className="font-bold italic mr-1">
{t("appearance.message.system")}
</span>{" "}
{t("appearance.message.message")}
</span> </span>
</div> </div>
</button> </button>
@ -105,7 +110,11 @@ export default function CustomMessages() {
<div className="flex items-center justify-start text-sm font-normal"> <div className="flex items-center justify-start text-sm font-normal">
<Plus className="m-2" size={16} weight="bold" /> <Plus className="m-2" size={16} weight="bold" />
<span className="leading-5"> <span className="leading-5">
New <span className="font-bold italic mr-1">user</span> message {t("appearance.message.new")}{" "}
<span className="font-bold italic mr-1">
{t("appearance.message.user")}
</span>{" "}
{t("appearance.message.message")}
</span> </span>
</div> </div>
</button> </button>
@ -117,7 +126,7 @@ export default function CustomMessages() {
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
onClick={handleMessageSave} onClick={handleMessageSave}
> >
Save Messages {t("appearance.message.save")}
</button> </button>
</div> </div>
)} )}

View File

@ -4,10 +4,11 @@ import { safeJsonParse } from "@/utils/request";
import NewIconForm from "./NewIconForm"; import NewIconForm from "./NewIconForm";
import Admin from "@/models/admin"; import Admin from "@/models/admin";
import System from "@/models/system"; import System from "@/models/system";
import { useTranslation } from "react-i18next";
export default function FooterCustomization() { export default function FooterCustomization() {
const [footerIcons, setFooterIcons] = useState(Array(3).fill(null)); const [footerIcons, setFooterIcons] = useState(Array(3).fill(null));
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
async function fetchFooterIcons() { async function fetchFooterIcons() {
const settings = (await Admin.systemPreferences())?.settings; const settings = (await Admin.systemPreferences())?.settings;
@ -52,15 +53,15 @@ export default function FooterCustomization() {
<div className="mb-8"> <div className="mb-8">
<div className="flex flex-col gap-y-1"> <div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white"> <h2 className="text-base leading-6 font-bold text-white">
Custom Footer Icons {t("appearance.icons.title")}
</h2> </h2>
<p className="text-xs leading-[18px] font-base text-white/60"> <p className="text-xs leading-[18px] font-base text-white/60">
Customize the footer icons displayed on the bottom of the sidebar. {t("appearance.icons.description")}
</p> </p>
</div> </div>
<div className="mt-3 flex gap-x-3 font-bold text-white text-sm"> <div className="mt-3 flex gap-x-3 font-bold text-white text-sm">
<div>Icon</div> <div>{t("appearance.icons.icon")}</div>
<div>Link</div> <div>{t("appearance.icons.link")}</div>
</div> </div>
<div className="mt-2 flex flex-col gap-y-[10px]"> <div className="mt-2 flex flex-col gap-y-[10px]">
{footerIcons.map((icon, index) => ( {footerIcons.map((icon, index) => (

View File

@ -0,0 +1,40 @@
import { useLanguageOptions } from "@/hooks/useLanguageOptions";
export default function LanguagePreference() {
const {
currentLanguage,
supportedLanguages,
getLanguageName,
changeLanguage,
} = useLanguageOptions();
return (
<>
<div className="flex flex-col gap-y-1">
<h2 className="text-base leading-6 font-bold text-white">
Display Language
</h2>
<p className="text-xs leading-[18px] font-base text-white/60">
Select the preferred language to render AnythingLLM's UI in, when
applicable.
</p>
</div>
<div className="flex items-center gap-x-4">
<select
name="userLang"
className="bg-zinc-900 w-fit mt-2 px-4 border-gray-500 text-white text-sm rounded-lg block py-2"
defaultValue={currentLanguage || "en"}
onChange={(e) => changeLanguage(e.target.value)}
>
{supportedLanguages.map((lang) => {
return (
<option key={lang} value={lang}>
{getLanguageName(lang)}
</option>
);
})}
</select>
</div>
</>
);
}

View File

@ -4,9 +4,12 @@ import FooterCustomization from "./FooterCustomization";
import SupportEmail from "./SupportEmail"; import SupportEmail from "./SupportEmail";
import CustomLogo from "./CustomLogo"; import CustomLogo from "./CustomLogo";
import CustomMessages from "./CustomMessages"; import CustomMessages from "./CustomMessages";
import { useTranslation } from "react-i18next";
import CustomAppName from "./CustomAppName"; import CustomAppName from "./CustomAppName";
import LanguagePreference from "./LanguagePreference";
export default function Appearance() { export default function Appearance() {
const { t } = useTranslation();
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar /> <Sidebar />
@ -18,13 +21,14 @@ export default function Appearance() {
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center"> <div className="items-center">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Appearance {t("appearance.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Customize the appearance settings of your platform. {t("appearance.description")}
</p> </p>
</div> </div>
<LanguagePreference />
<CustomLogo /> <CustomLogo />
<CustomAppName /> <CustomAppName />
<CustomMessages /> <CustomMessages />

View File

@ -9,6 +9,7 @@ import showToast from "@/utils/toast";
import System from "@/models/system"; import System from "@/models/system";
import { CaretDown, Download, Trash } from "@phosphor-icons/react"; import { CaretDown, Download, Trash } from "@phosphor-icons/react";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import { useTranslation } from "react-i18next";
const exportOptions = { const exportOptions = {
csv: { csv: {
@ -54,6 +55,7 @@ export default function WorkspaceChats() {
const [chats, setChats] = useState([]); const [chats, setChats] = useState([]);
const [offset, setOffset] = useState(Number(query.get("offset") || 0)); const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false); const [canNext, setCanNext] = useState(false);
const { t } = useTranslation();
const handleDumpChats = async (exportType) => { const handleDumpChats = async (exportType) => {
const chats = await System.exportChats(exportType); const chats = await System.exportChats(exportType);
@ -122,7 +124,7 @@ export default function WorkspaceChats() {
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center"> <div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Workspace Chats {t("recorded.title")}
</p> </p>
<div className="relative"> <div className="relative">
<button <button
@ -131,7 +133,7 @@ export default function WorkspaceChats() {
className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit" className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
> >
<Download size={18} weight="bold" /> <Download size={18} weight="bold" />
Export {t("recorded.export")}
<CaretDown size={18} weight="bold" /> <CaretDown size={18} weight="bold" />
</button> </button>
<div <div
@ -167,8 +169,7 @@ export default function WorkspaceChats() {
)} )}
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are all the recorded chats and messages that have been sent {t("recorded.description")}
by users ordered by their creation date.
</p> </p>
</div> </div>
<ChatsContainer <ChatsContainer
@ -178,6 +179,7 @@ export default function WorkspaceChats() {
offset={offset} offset={offset}
setOffset={setOffset} setOffset={setOffset}
canNext={canNext} canNext={canNext}
t={t}
/> />
</div> </div>
</div> </div>
@ -192,6 +194,7 @@ function ChatsContainer({
offset, offset,
setOffset, setOffset,
canNext, canNext,
t,
}) { }) {
const handlePrevious = () => { const handlePrevious = () => {
setOffset(Math.max(offset - 1, 0)); setOffset(Math.max(offset - 1, 0));
@ -225,22 +228,22 @@ function ChatsContainer({
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60"> <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr> <tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg"> <th scope="col" className="px-6 py-3 rounded-tl-lg">
Id {t("recorded.table.id")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Sent By {t("recorded.table.by")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Workspace {t("recorded.table.workspace")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Prompt {t("recorded.table.prompt")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Response {t("recorded.table.response")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Sent At {t("recorded.table.at")}
</th> </th>
<th scope="col" className="px-6 py-3 rounded-tr-lg"> <th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "} {" "}

View File

@ -6,9 +6,11 @@ import "react-loading-skeleton/dist/skeleton.css";
import useQuery from "@/hooks/useQuery"; import useQuery from "@/hooks/useQuery";
import ChatRow from "./ChatRow"; import ChatRow from "./ChatRow";
import Embed from "@/models/embed"; import Embed from "@/models/embed";
import { useTranslation } from "react-i18next";
export default function EmbedChats() { export default function EmbedChats() {
// TODO [FEAT]: Add export of embed chats // TODO [FEAT]: Add export of embed chats
const { t } = useTranslation();
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar /> <Sidebar />
@ -20,12 +22,11 @@ export default function EmbedChats() {
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center"> <div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Embed Chats {t("embed-chats.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are all the recorded chats and messages from any embed that {t("embed-chats.description")}
you have published.
</p> </p>
</div> </div>
<ChatsContainer /> <ChatsContainer />
@ -41,6 +42,7 @@ function ChatsContainer() {
const [chats, setChats] = useState([]); const [chats, setChats] = useState([]);
const [offset, setOffset] = useState(Number(query.get("offset") || 0)); const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false); const [canNext, setCanNext] = useState(false);
const { t } = useTranslation();
const handlePrevious = () => { const handlePrevious = () => {
setOffset(Math.max(offset - 1, 0)); setOffset(Math.max(offset - 1, 0));
@ -83,19 +85,19 @@ function ChatsContainer() {
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60"> <thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<tr> <tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg"> <th scope="col" className="px-6 py-3 rounded-tl-lg">
Embed {t("embed-chats.table.embed")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Sender {t("embed-chats.table.sender")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Message {t("embed-chats.table.message")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Response {t("embed-chats.table.response")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Sent At {t("embed-chats.table.at")}
</th> </th>
<th scope="col" className="px-6 py-3 rounded-tr-lg"> <th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "} {" "}
@ -116,14 +118,14 @@ function ChatsContainer() {
disabled={offset === 0} disabled={offset === 0}
> >
{" "} {" "}
Previous Page {t("common.previous")}
</button> </button>
<button <button
onClick={handleNext} onClick={handleNext}
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible" className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
disabled={!canNext} disabled={!canNext}
> >
Next Page {t("common.next")}
</button> </button>
</div> </div>
</> </>

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Sidebar from "@/components/SettingsSidebar"; import Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import * as Skeleton from "react-loading-skeleton"; import * as Skeleton from "react-loading-skeleton";
@ -13,7 +14,7 @@ import CTAButton from "@/components/lib/CTAButton";
export default function EmbedConfigs() { export default function EmbedConfigs() {
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
const { t } = useTranslation();
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar /> <Sidebar />
@ -25,18 +26,17 @@ export default function EmbedConfigs() {
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4"> <div className="items-center flex gap-x-4">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Embeddable Chat Widgets {t("embeddable.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Embeddable chat widgets are public facing chat interfaces that are {t("embeddable.description")}
tied to a single workspace. These allow you to build workspaces
that then you can publish to the world.
</p> </p>
</div> </div>
<div className="w-full justify-end flex"> <div className="w-full justify-end flex">
<CTAButton onClick={openModal} className="mt-3 mr-0 -mb-14 z-10"> <CTAButton onClick={openModal} className="mt-3 mr-0 -mb-14 z-10">
<CodeBlock className="h-4 w-4" weight="bold" /> Create embed <CodeBlock className="h-4 w-4" weight="bold" />{" "}
{t("embeddable.create")}
</CTAButton> </CTAButton>
</div> </div>
<EmbedContainer /> <EmbedContainer />
@ -52,6 +52,7 @@ export default function EmbedConfigs() {
function EmbedContainer() { function EmbedContainer() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [embeds, setEmbeds] = useState([]); const [embeds, setEmbeds] = useState([]);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
async function fetchUsers() { async function fetchUsers() {
@ -81,13 +82,13 @@ function EmbedContainer() {
<thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60"> <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60">
<tr> <tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg"> <th scope="col" className="px-6 py-3 rounded-tl-lg">
Workspace {t("embeddable.table.workspace")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Sent Chats {t("embeddable.table.chats")}
</th> </th>
<th scope="col" className="px-6 py-3"> <th scope="col" className="px-6 py-3">
Active Domains {t("embeddable.table.Active")}
</th> </th>
<th scope="col" className="px-6 py-3 rounded-tr-lg"> <th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "} {" "}

View File

@ -30,6 +30,7 @@ import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import { useModal } from "@/hooks/useModal"; import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import CTAButton from "@/components/lib/CTAButton"; import CTAButton from "@/components/lib/CTAButton";
import { useTranslation } from "react-i18next";
const EMBEDDERS = [ const EMBEDDERS = [
{ {
@ -112,6 +113,7 @@ export default function GeneralEmbeddingPreference() {
const [searchMenuOpen, setSearchMenuOpen] = useState(false); const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
const { t } = useTranslation();
function embedderModelChanged(formEl) { function embedderModelChanged(formEl) {
try { try {
@ -223,17 +225,13 @@ export default function GeneralEmbeddingPreference() {
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center"> <div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Embedding Preference {t("embedding.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
When using an LLM that does not natively support an embedding {t("embedding.desc-start")}
engine - you may need to additionally specify credentials to
for embedding text.
<br /> <br />
Embedding is the process of turning text into vectors. These {t("embedding.desc-end")}
credentials are required to turn your files and prompts into a
format which AnythingLLM can use to process.
</p> </p>
</div> </div>
<div className="w-full justify-end flex"> <div className="w-full justify-end flex">
@ -242,12 +240,12 @@ export default function GeneralEmbeddingPreference() {
onClick={() => handleSubmit()} onClick={() => handleSubmit()}
className="mt-3 mr-0 -mb-14 z-10" className="mt-3 mr-0 -mb-14 z-10"
> >
{saving ? "Saving..." : "Save changes"} {saving ? t("common.saving") : t("common.save")}
</CTAButton> </CTAButton>
)} )}
</div> </div>
<div className="text-base font-bold text-white mt-6 mb-4"> <div className="text-base font-bold text-white mt-6 mb-4">
Embedding Provider {t("embedding.provider.title")}
</div> </div>
<div className="relative"> <div className="relative">
{searchMenuOpen && ( {searchMenuOpen && (

View File

@ -6,6 +6,7 @@ import CTAButton from "@/components/lib/CTAButton";
import Admin from "@/models/admin"; import Admin from "@/models/admin";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { nFormatter, numberWithCommas } from "@/utils/numbers"; import { nFormatter, numberWithCommas } from "@/utils/numbers";
import { useTranslation } from "react-i18next";
function isNullOrNaN(value) { function isNullOrNaN(value) {
if (value === null) return true; if (value === null) return true;
@ -17,6 +18,7 @@ export default function EmbeddingTextSplitterPreference() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const { t } = useTranslation();
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -86,25 +88,22 @@ export default function EmbeddingTextSplitterPreference() {
<div className="w-full flex flex-col gap-y-1 pb-4 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-4 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center"> <div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Text splitting & Chunking Preferences {t("text.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Sometimes, you may want to change the default way that new {t("text.desc-start")} <br />
documents are split and chunked before being inserted into {t("text.desc-end")}
your vector database. <br />
You should only modify this setting if you understand how text
splitting works and it's side effects.
</p> </p>
<p className="text-xs leading-[18px] font-semibold text-white/80"> <p className="text-xs leading-[18px] font-semibold text-white/80">
Changes here will only apply to{" "} {t("text.warn-start")} <i>{t("text.warn-center")}</i>
<i>newly embedded documents</i>, not existing documents. {t("text.warn-end")}
</p> </p>
</div> </div>
<div className="w-full justify-end flex"> <div className="w-full justify-end flex">
{hasChanges && ( {hasChanges && (
<CTAButton className="mt-3 mr-0 -mb-14 z-10"> <CTAButton className="mt-3 mr-0 -mb-14 z-10">
{saving ? "Saving..." : "Save changes"} {saving ? t("common.saving") : t("common.save")}
</CTAButton> </CTAButton>
)} )}
</div> </div>
@ -113,11 +112,10 @@ export default function EmbeddingTextSplitterPreference() {
<div className="flex flex-col max-w-[300px]"> <div className="flex flex-col max-w-[300px]">
<div className="flex flex-col gap-y-2 mb-4"> <div className="flex flex-col gap-y-2 mb-4">
<label className="text-white text-sm font-semibold block"> <label className="text-white text-sm font-semibold block">
Text Chunk Size {t("text.size.title")}
</label> </label>
<p className="text-xs text-white/60"> <p className="text-xs text-white/60">
This is the maximum length of characters that can be {t("text.size.description")}
present in a single vector.
</p> </p>
</div> </div>
<input <input
@ -137,7 +135,7 @@ export default function EmbeddingTextSplitterPreference() {
autoComplete="off" autoComplete="off"
/> />
<p className="text-xs text-white/40"> <p className="text-xs text-white/40">
Embed model maximum length is{" "} {t("text.size.recommend")}{" "}
{numberWithCommas(settings?.max_embed_chunk_size || 1000)}. {numberWithCommas(settings?.max_embed_chunk_size || 1000)}.
</p> </p>
</div> </div>
@ -147,11 +145,10 @@ export default function EmbeddingTextSplitterPreference() {
<div className="flex flex-col max-w-[300px]"> <div className="flex flex-col max-w-[300px]">
<div className="flex flex-col gap-y-2 mb-4"> <div className="flex flex-col gap-y-2 mb-4">
<label className="text-white text-sm font-semibold block"> <label className="text-white text-sm font-semibold block">
Text Chunk Overlap {t("text.overlap.title")}
</label> </label>
<p className="text-xs text-white/60"> <p className="text-xs text-white/60">
This is the maximum overlap of characters that occurs {t("text.overlap.description")}
during chunking between two adjacent text chunks.
</p> </p>
</div> </div>
<input <input

View File

@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Sidebar from "@/components/SettingsSidebar"; import Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import System from "@/models/system"; import System from "@/models/system";
@ -232,6 +233,7 @@ export default function GeneralLLMPreference() {
const [searchMenuOpen, setSearchMenuOpen] = useState(false); const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
const isHosted = window.location.hostname.includes("useanything.com"); const isHosted = window.location.hostname.includes("useanything.com");
const { t } = useTranslation();
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -310,14 +312,11 @@ export default function GeneralLLMPreference() {
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center"> <div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
LLM Preference {t("llm.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are the credentials and settings for your preferred LLM {t("llm.description")}
chat & embedding provider. Its important these keys are
current and correct or else AnythingLLM will not function
properly.
</p> </p>
</div> </div>
<div className="w-full justify-end flex"> <div className="w-full justify-end flex">
@ -331,7 +330,7 @@ export default function GeneralLLMPreference() {
)} )}
</div> </div>
<div className="text-base font-bold text-white mt-6 mb-4"> <div className="text-base font-bold text-white mt-6 mb-4">
LLM Provider {t("llm.provider")}
</div> </div>
<div className="relative"> <div className="relative">
{searchMenuOpen && ( {searchMenuOpen && (

View File

@ -9,11 +9,12 @@ import {
LLM_SELECTION_PRIVACY, LLM_SELECTION_PRIVACY,
VECTOR_DB_PRIVACY, VECTOR_DB_PRIVACY,
} from "@/pages/OnboardingFlow/Steps/DataHandling"; } from "@/pages/OnboardingFlow/Steps/DataHandling";
import { useTranslation } from "react-i18next";
export default function PrivacyAndDataHandling() { export default function PrivacyAndDataHandling() {
const [settings, setSettings] = useState({}); const [settings, setSettings] = useState({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
async function fetchSettings() { async function fetchSettings() {
setLoading(true); setLoading(true);
@ -35,12 +36,11 @@ export default function PrivacyAndDataHandling() {
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4"> <div className="items-center flex gap-x-4">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Privacy & Data-Handling {t("privacy.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
This is your configuration for how connected third party providers {t("privacy.description")}
and AnythingLLM handle your data.
</p> </p>
</div> </div>
{loading ? ( {loading ? (
@ -65,12 +65,15 @@ function ThirdParty({ settings }) {
const llmChoice = settings?.LLMProvider || "openai"; const llmChoice = settings?.LLMProvider || "openai";
const embeddingEngine = settings?.EmbeddingEngine || "openai"; const embeddingEngine = settings?.EmbeddingEngine || "openai";
const vectorDb = settings?.VectorDB || "lancedb"; const vectorDb = settings?.VectorDB || "lancedb";
const { t } = useTranslation();
return ( return (
<div className="py-8 w-full flex items-start justify-center flex-col gap-y-6 border-b-2 border-white/10"> <div className="py-8 w-full flex items-start justify-center flex-col gap-y-6 border-b-2 border-white/10">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div className="flex flex-col gap-y-2 border-b border-zinc-500/50 pb-4"> <div className="flex flex-col gap-y-2 border-b border-zinc-500/50 pb-4">
<div className="text-white text-base font-bold">LLM Selection</div> <div className="text-white text-base font-bold">
{t("privacy.llm")}
</div>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<img <img
src={LLM_SELECTION_PRIVACY[llmChoice].logo} src={LLM_SELECTION_PRIVACY[llmChoice].logo}
@ -89,7 +92,7 @@ function ThirdParty({ settings }) {
</div> </div>
<div className="flex flex-col gap-y-2 border-b border-zinc-500/50 pb-4"> <div className="flex flex-col gap-y-2 border-b border-zinc-500/50 pb-4">
<div className="text-white text-base font-bold"> <div className="text-white text-base font-bold">
Embedding Preference {t("privacy.embedding")}
</div> </div>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<img <img
@ -111,7 +114,9 @@ function ThirdParty({ settings }) {
</div> </div>
<div className="flex flex-col gap-y-2 pb-4"> <div className="flex flex-col gap-y-2 pb-4">
<div className="text-white text-base font-bold">Vector Database</div> <div className="text-white text-base font-bold">
{t("privacy.vector")}
</div>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<img <img
src={VECTOR_DB_PRIVACY[vectorDb].logo} src={VECTOR_DB_PRIVACY[vectorDb].logo}
@ -137,6 +142,7 @@ function TelemetryLogs({ settings }) {
const [telemetry, setTelemetry] = useState( const [telemetry, setTelemetry] = useState(
settings?.DisableTelemetry !== "true" settings?.DisableTelemetry !== "true"
); );
const { t } = useTranslation();
async function toggleTelemetry() { async function toggleTelemetry() {
await System.updateSystem({ await System.updateSystem({
DisableTelemetry: !telemetry ? "false" : "true", DisableTelemetry: !telemetry ? "false" : "true",
@ -157,7 +163,7 @@ function TelemetryLogs({ settings }) {
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-4">
<div className=""> <div className="">
<label className="mb-2.5 block font-medium text-white"> <label className="mb-2.5 block font-medium text-white">
Anonymous Telemetry Enabled {t("privacy.anonymous")}
</label> </label>
<label className="relative inline-flex cursor-pointer items-center"> <label className="relative inline-flex cursor-pointer items-center">
<input <input

View File

@ -7,6 +7,7 @@ import paths from "@/utils/paths";
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants"; import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
import PreLoader from "@/components/Preloader"; import PreLoader from "@/components/Preloader";
import CTAButton from "@/components/lib/CTAButton"; import CTAButton from "@/components/lib/CTAButton";
import { useTranslation } from "react-i18next";
export default function GeneralSecurity() { export default function GeneralSecurity() {
return ( return (
@ -29,6 +30,7 @@ function MultiUserMode() {
const [useMultiUserMode, setUseMultiUserMode] = useState(false); const [useMultiUserMode, setUseMultiUserMode] = useState(false);
const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false); const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { t } = useTranslation();
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -90,12 +92,11 @@ function MultiUserMode() {
<div className="w-full flex flex-col gap-y-1"> <div className="w-full flex flex-col gap-y-1">
<div className="items-center flex gap-x-4"> <div className="items-center flex gap-x-4">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Multi-User Mode {t("multi.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Set up your instance to support your team by activating Multi-User {t("multi.description")}
Mode.
</p> </p>
</div> </div>
{hasChanges && ( {hasChanges && (
@ -104,7 +105,7 @@ function MultiUserMode() {
onClick={() => handleSubmit()} onClick={() => handleSubmit()}
className="mt-3 mr-0 -mb-20 z-10" className="mt-3 mr-0 -mb-20 z-10"
> >
{saving ? "Saving..." : "Save changes"} {saving ? t("common.saving") : t("common.save")}
</CTAButton> </CTAButton>
</div> </div>
)} )}
@ -116,8 +117,8 @@ function MultiUserMode() {
<div className=""> <div className="">
<label className="mb-2.5 block font-medium text-white"> <label className="mb-2.5 block font-medium text-white">
{multiUserModeEnabled {multiUserModeEnabled
? "Multi-User Mode is Enabled" ? t("multi.enable.is-enable")
: "Enable Multi-User Mode"} : t("multi.enable.enable")}
</label> </label>
<label className="relative inline-flex cursor-pointer items-center"> <label className="relative inline-flex cursor-pointer items-center">
@ -140,7 +141,7 @@ function MultiUserMode() {
htmlFor="username" htmlFor="username"
className="block mb-3 font-medium text-white" className="block mb-3 font-medium text-white"
> >
Admin account username {t("multi.enable.username")}
</label> </label>
<input <input
name="username" name="username"
@ -159,7 +160,7 @@ function MultiUserMode() {
htmlFor="password" htmlFor="password"
className="block mb-3 font-medium text-white" className="block mb-3 font-medium text-white"
> >
Admin account password {t("multi.enable.password")}
</label> </label>
<input <input
name="password" name="password"
@ -178,9 +179,7 @@ function MultiUserMode() {
</div> </div>
<div className="flex items-center justify-between space-x-14"> <div className="flex items-center justify-between space-x-14">
<p className="text-white/80 text-xs rounded-lg w-96"> <p className="text-white/80 text-xs rounded-lg w-96">
By default, you will be the only admin. As an admin you will {t("multi.enable.description")}
need to create accounts for all new users or admins. Do not lose
your password as only an Admin user can reset passwords.
</p> </p>
</div> </div>
</div> </div>
@ -197,6 +196,7 @@ function PasswordProtection() {
const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false); const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false);
const [usePassword, setUsePassword] = useState(false); const [usePassword, setUsePassword] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { t } = useTranslation();
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -269,12 +269,11 @@ function PasswordProtection() {
<div className="w-full flex flex-col gap-y-1"> <div className="w-full flex flex-col gap-y-1">
<div className="items-center flex gap-x-4"> <div className="items-center flex gap-x-4">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Password Protection {t("multi.password.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
Protect your AnythingLLM instance with a password. If you forget {t("multi.password.description")}
this there is no recovery method so ensure you save this password.
</p> </p>
</div> </div>
{hasChanges && ( {hasChanges && (
@ -283,7 +282,7 @@ function PasswordProtection() {
onClick={() => handleSubmit()} onClick={() => handleSubmit()}
className="mt-3 mr-0 -mb-20 z-10" className="mt-3 mr-0 -mb-20 z-10"
> >
{saving ? "Saving..." : "Save changes"} {saving ? t("common.saving") : t("common.save")}
</CTAButton> </CTAButton>
</div> </div>
)} )}
@ -294,7 +293,7 @@ function PasswordProtection() {
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-4">
<div className=""> <div className="">
<label className="mb-2.5 block font-medium text-white"> <label className="mb-2.5 block font-medium text-white">
Password Protect Instance {t("multi.instance.title")}
</label> </label>
<label className="relative inline-flex cursor-pointer items-center"> <label className="relative inline-flex cursor-pointer items-center">
@ -314,7 +313,7 @@ function PasswordProtection() {
htmlFor="password" htmlFor="password"
className="block mb-3 font-medium text-white" className="block mb-3 font-medium text-white"
> >
Instance password {t("multi.instance.password")}
</label> </label>
<input <input
name="password" name="password"
@ -333,9 +332,7 @@ function PasswordProtection() {
</div> </div>
<div className="flex items-center justify-between space-x-14"> <div className="flex items-center justify-between space-x-14">
<p className="text-white/80 text-xs rounded-lg w-96"> <p className="text-white/80 text-xs rounded-lg w-96">
By default, anyone with this password can log into the instance. {t("multi.instance.description")}
Do not lose this password as only the instance maintainer is
able to retrieve or reset the password once set.
</p> </p>
</div> </div>
</div> </div>

View File

@ -11,6 +11,7 @@ import NativeTranscriptionOptions from "@/components/TranscriptionSelection/Nati
import LLMItem from "@/components/LLMSelection/LLMItem"; import LLMItem from "@/components/LLMSelection/LLMItem";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import CTAButton from "@/components/lib/CTAButton"; import CTAButton from "@/components/lib/CTAButton";
import { useTranslation } from "react-i18next";
const PROVIDERS = [ const PROVIDERS = [
{ {
@ -39,6 +40,7 @@ export default function TranscriptionModelPreference() {
const [selectedProvider, setSelectedProvider] = useState(null); const [selectedProvider, setSelectedProvider] = useState(null);
const [searchMenuOpen, setSearchMenuOpen] = useState(false); const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
const { t } = useTranslation();
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -118,14 +120,11 @@ export default function TranscriptionModelPreference() {
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center"> <div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Transcription Model Preference {t("transcription.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are the credentials and settings for your preferred {t("transcription.description")}
transcription model provider. Its important these keys are
current and correct or else media files and audio will not
transcribe.
</p> </p>
</div> </div>
<div className="w-full justify-end flex"> <div className="w-full justify-end flex">
@ -139,7 +138,7 @@ export default function TranscriptionModelPreference() {
)} )}
</div> </div>
<div className="text-base font-bold text-white mt-6 mb-4"> <div className="text-base font-bold text-white mt-6 mb-4">
Transcription Provider {t("transcription.provider")}
</div> </div>
<div className="relative"> <div className="relative">
{searchMenuOpen && ( {searchMenuOpen && (

View File

@ -26,6 +26,7 @@ import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import AstraDBOptions from "@/components/VectorDBSelection/AstraDBOptions"; import AstraDBOptions from "@/components/VectorDBSelection/AstraDBOptions";
import CTAButton from "@/components/lib/CTAButton"; import CTAButton from "@/components/lib/CTAButton";
import { useTranslation } from "react-i18next";
export default function GeneralVectorDatabase() { export default function GeneralVectorDatabase() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -39,6 +40,7 @@ export default function GeneralVectorDatabase() {
const [searchMenuOpen, setSearchMenuOpen] = useState(false); const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
const { t } = useTranslation();
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -194,13 +196,11 @@ export default function GeneralVectorDatabase() {
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex gap-x-4 items-center"> <div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white"> <p className="text-lg leading-6 font-bold text-white">
Vector Database {t("vector.title")}
</p> </p>
</div> </div>
<p className="text-xs leading-[18px] font-base text-white text-opacity-60"> <p className="text-xs leading-[18px] font-base text-white text-opacity-60">
These are the credentials and settings for how your {t("vector.description")}
AnythingLLM instance will function. It's important these keys
are current and correct.
</p> </p>
</div> </div>
<div className="w-full justify-end flex"> <div className="w-full justify-end flex">
@ -209,12 +209,12 @@ export default function GeneralVectorDatabase() {
onClick={() => handleSubmit()} onClick={() => handleSubmit()}
className="mt-3 mr-0 -mb-14 z-10" className="mt-3 mr-0 -mb-14 z-10"
> >
{saving ? "Saving..." : "Save changes"} {saving ? t("common.saving") : t("common.save")}
</CTAButton> </CTAButton>
)} )}
</div> </div>
<div className="text-base font-bold text-white mt-6 mb-4"> <div className="text-base font-bold text-white mt-6 mb-4">
Vector Database Provider {t("vector.provider.title")}
</div> </div>
<div className="relative"> <div className="relative">
{searchMenuOpen && ( {searchMenuOpen && (

View File

@ -4,6 +4,7 @@ import paths from "@/utils/paths";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Workspace from "@/models/workspace"; import Workspace from "@/models/workspace";
import { useTranslation } from "react-i18next";
const TITLE = "Create your first workspace"; const TITLE = "Create your first workspace";
const DESCRIPTION = const DESCRIPTION =
@ -17,6 +18,7 @@ export default function CreateWorkspace({
const [workspaceName, setWorkspaceName] = useState(""); const [workspaceName, setWorkspaceName] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
const createWorkspaceRef = useRef(); const createWorkspaceRef = useRef();
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
setHeader({ title: TITLE, description: DESCRIPTION }); setHeader({ title: TITLE, description: DESCRIPTION });
@ -71,7 +73,7 @@ export default function CreateWorkspace({
htmlFor="name" htmlFor="name"
className="block mb-3 text-sm font-medium text-white" className="block mb-3 text-sm font-medium text-white"
> >
Workspace Name {t("common.workspaces-name")}
</label> </label>
<input <input
name="name" name="name"

View File

@ -4,6 +4,7 @@ import AgentLLMItem from "./AgentLLMItem";
import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference"; import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference";
import { CaretUpDown, Gauge, MagnifyingGlass, X } from "@phosphor-icons/react"; import { CaretUpDown, Gauge, MagnifyingGlass, X } from "@phosphor-icons/react";
import AgentModelSelection from "../AgentModelSelection"; import AgentModelSelection from "../AgentModelSelection";
import { useTranslation } from "react-i18next";
const ENABLED_PROVIDERS = [ const ENABLED_PROVIDERS = [
"openai", "openai",
@ -65,7 +66,7 @@ export default function AgentLLMSelection({
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [searchMenuOpen, setSearchMenuOpen] = useState(false); const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
const { t } = useTranslation();
function updateLLMChoice(selection) { function updateLLMChoice(selection) {
setSearchQuery(""); setSearchQuery("");
setSelectedLLM(selection); setSelectedLLM(selection);
@ -96,22 +97,17 @@ export default function AgentLLMSelection({
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2"> <div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
<div className="gap-x-2 flex items-center"> <div className="gap-x-2 flex items-center">
<Gauge className="shrink-0" size={25} /> <Gauge className="shrink-0" size={25} />
<p className="text-sm"> <p className="text-sm">{t("agent.performance-warning")}</p>
Performance of LLMs that do not explicitly support tool-calling is
highly dependent on the model's capabilities and accuracy. Some
abilities may be limited or non-functional.
</p>
</div> </div>
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Workspace Agent LLM Provider {t("agent.provider.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
The specific LLM provider & model that will be used for this {t("agent.provider.description")}
workspace's @agent agent.
</p> </p>
</div> </div>

View File

@ -1,6 +1,7 @@
import useGetProviderModels, { import useGetProviderModels, {
DISABLED_PROVIDERS, DISABLED_PROVIDERS,
} from "@/hooks/useGetProvidersModels"; } from "@/hooks/useGetProvidersModels";
import { useTranslation } from "react-i18next";
// These models do NOT support function calling // These models do NOT support function calling
function supportedModel(provider, model = "") { function supportedModel(provider, model = "") {
@ -19,6 +20,8 @@ export default function AgentModelSelection({
}) { }) {
const { defaultModels, customModels, loading } = const { defaultModels, customModels, loading } =
useGetProviderModels(provider); useGetProviderModels(provider);
const { t } = useTranslation();
if (DISABLED_PROVIDERS.includes(provider)) return null; if (DISABLED_PROVIDERS.includes(provider)) return null;
if (loading) { if (loading) {
@ -26,11 +29,10 @@ export default function AgentModelSelection({
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Workspace Agent Chat model {t("agent.mode.chat.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
The specific chat model that will be used for this workspace's {t("agent.mode.chat.description")}
@agent agent.
</p> </p>
</div> </div>
<select <select
@ -40,7 +42,7 @@ export default function AgentModelSelection({
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
> >
<option disabled={true} selected={true}> <option disabled={true} selected={true}>
-- waiting for models -- {t("agent.mode.wait")}
</option> </option>
</select> </select>
</div> </div>
@ -51,11 +53,10 @@ export default function AgentModelSelection({
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Workspace Agent model {t("agent.mode.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
The specific LLM model that will be used for this workspace's @agent {t("agent.mode.description")}
agent.
</p> </p>
</div> </div>

View File

@ -0,0 +1,204 @@
import React, { useEffect, useRef, useState } from "react";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import GoogleSearchIcon from "./icons/google.png";
import SerperDotDevIcon from "./icons/serper.png";
import BingSearchIcon from "./icons/bing.png";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import SearchProviderItem from "./SearchProviderItem";
import {
SerperDotDevOptions,
GoogleSearchOptions,
BingSearchOptions,
} from "./SearchProviderOptions";
import { useTranslation } from "react-i18next";
const SEARCH_PROVIDERS = [
{
name: "Please make a selection",
value: "none",
logo: AnythingLLMIcon,
options: () => <React.Fragment />,
description:
"Web search will be disabled until a provider and keys are provided.",
},
{
name: "Google Search Engine",
value: "google-search-engine",
logo: GoogleSearchIcon,
options: (settings) => <GoogleSearchOptions settings={settings} />,
description:
"Web search powered by a custom Google Search Engine. Free for 100 queries per day.",
},
{
name: "Serper.dev",
value: "serper-dot-dev",
logo: SerperDotDevIcon,
options: (settings) => <SerperDotDevOptions settings={settings} />,
description:
"Serper.dev web-search. Free account with a 2,500 calls, but then paid.",
},
{
name: "Bing Search",
value: "bing-search",
logo: BingSearchIcon,
options: (settings) => <BingSearchOptions settings={settings} />,
description:
"Web search powered by the Bing Search API. Free for 1000 queries per month.",
},
];
export default function AgentWebSearchSelection({
skill,
settings,
toggleSkill,
enabled = false,
}) {
const searchInputRef = useRef(null);
const [filteredResults, setFilteredResults] = useState([]);
const [selectedProvider, setSelectedProvider] = useState("none");
const [searchQuery, setSearchQuery] = useState("");
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const { t } = useTranslation();
function updateChoice(selection) {
setSearchQuery("");
setSelectedProvider(selection);
setSearchMenuOpen(false);
}
function handleXButton() {
if (searchQuery.length > 0) {
setSearchQuery("");
if (searchInputRef.current) searchInputRef.current.value = "";
} else {
setSearchMenuOpen(!searchMenuOpen);
}
}
useEffect(() => {
const filtered = SEARCH_PROVIDERS.filter((provider) =>
provider.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredResults(filtered);
}, [searchQuery, selectedProvider]);
useEffect(() => {
setSelectedProvider(settings?.preferences?.agent_search_provider ?? "none");
}, [settings?.preferences?.agent_search_provider]);
const selectedSearchProviderObject = SEARCH_PROVIDERS.find(
(provider) => provider.value === selectedProvider
);
return (
<div className="border-b border-white/40 pb-4">
<div className="flex flex-col">
<div className="flex w-full justify-between items-center">
<label htmlFor="name" className="block input-label">
{t("agent.skill.web.title")}
</label>
<label className="border-none relative inline-flex cursor-pointer items-center mt-2">
<input
type="checkbox"
className="peer sr-only"
checked={enabled}
onClick={() => toggleSkill(skill)}
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span>
</label>
</div>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{t("agent.skill.web.desc-start")}
<br />
{t("agent.skill.web.desc-end")}
</p>
</div>
<div hidden={!enabled}>
<div className="relative">
<input
type="hidden"
name="system::agent_search_provider"
value={selectedProvider}
/>
{searchMenuOpen && (
<div
className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
onClick={() => setSearchMenuOpen(false)}
/>
)}
{searchMenuOpen ? (
<div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] overflow-auto white-scrollbar min-h-[64px] bg-[#18181B] rounded-lg flex flex-col justify-between cursor-pointer border-2 border-[#46C8FF] z-20">
<div className="w-full flex flex-col gap-y-1">
<div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
<MagnifyingGlass
size={20}
weight="bold"
className="absolute left-4 z-30 text-white -ml-4 my-2"
/>
<input
type="text"
name="web-provider-search"
autoComplete="off"
placeholder="Search available web-search providers"
className="border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium"
onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<X
size={20}
weight="bold"
className="cursor-pointer text-white hover:text-[#9CA3AF]"
onClick={handleXButton}
/>
</div>
<div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4">
{filteredResults.map((provider) => {
return (
<SearchProviderItem
provider={provider}
key={provider.name}
checked={selectedProvider === provider.value}
onClick={() => updateChoice(provider.value)}
/>
);
})}
</div>
</div>
</div>
) : (
<button
className="w-full max-w-[640px] h-[64px] bg-[#18181B] rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-[#46C8FF] transition-all duration-300"
type="button"
onClick={() => setSearchMenuOpen(true)}
>
<div className="flex gap-x-4 items-center">
<img
src={selectedSearchProviderObject.logo}
alt={`${selectedSearchProviderObject.name} logo`}
className="w-10 h-10 rounded-md"
/>
<div className="flex flex-col text-left">
<div className="text-sm font-semibold text-white">
{selectedSearchProviderObject.name}
</div>
<div className="mt-1 text-xs text-[#D2D5DB]">
{selectedSearchProviderObject.description}
</div>
</div>
</div>
<CaretUpDown size={24} weight="bold" className="text-white" />
</button>
)}
</div>
{selectedProvider !== "none" && (
<div className="mt-4 flex flex-col gap-y-1">
{selectedSearchProviderObject.options(settings)}
</div>
)}
</div>
</div>
);
}

View File

@ -1,16 +1,16 @@
import { useTranslation } from "react-i18next";
export default function ChatHistorySettings({ workspace, setHasChanges }) { export default function ChatHistorySettings({ workspace, setHasChanges }) {
const { t } = useTranslation();
return ( return (
<div> <div>
<div className="flex flex-col gap-y-1 mb-4"> <div className="flex flex-col gap-y-1 mb-4">
<label htmlFor="name" className="block mb-2 input-label"> <label htmlFor="name" className="block mb-2 input-label">
Chat History {t("chat.history.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium"> <p className="text-white text-opacity-60 text-xs font-medium">
The number of previous chats that will be included in the {t("chat.history.desc-start")}
response&apos;s short-term memory. <i> {t("chat.history.recommend")} </i>
<i>Recommend 20. </i> {t("chat.history.desc-end")}
Anything more than 45 is likely to lead to continuous chat failures
depending on message size.
</p> </p>
</div> </div>
<input <input

View File

@ -1,12 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
export default function ChatModeSelection({ workspace, setHasChanges }) { export default function ChatModeSelection({ workspace, setHasChanges }) {
const [chatMode, setChatMode] = useState(workspace?.chatMode || "chat"); const [chatMode, setChatMode] = useState(workspace?.chatMode || "chat");
const { t } = useTranslation();
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="chatMode" className="block input-label"> <label htmlFor="chatMode" className="block input-label">
Chat mode {t("chat.mode.title")}
</label> </label>
</div> </div>
@ -22,7 +23,7 @@ export default function ChatModeSelection({ workspace, setHasChanges }) {
}} }}
className="transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md" className="transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md"
> >
Chat {t("chat.mode.chat.title")}
</button> </button>
<button <button
type="button" type="button"
@ -33,21 +34,23 @@ export default function ChatModeSelection({ workspace, setHasChanges }) {
}} }}
className="transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md" className="transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md"
> >
Query {t("chat.mode.query.title")}
</button> </button>
</div> </div>
<p className="text-sm text-white/60"> <p className="text-sm text-white/60">
{chatMode === "chat" ? ( {chatMode === "chat" ? (
<> <>
<b>Chat</b> will provide answers with the LLM's general knowledge{" "} <b>{t("chat.mode.chat.title")}</b>{" "}
<i className="font-semibold">and</i> document context that is {t("chat.mode.chat.desc-start")}{" "}
found. <i className="font-semibold">{t("chat.mode.chat.and")}</i>{" "}
{t("chat.mode.chat.desc-end")}
</> </>
) : ( ) : (
<> <>
<b>Query</b> will provide answers{" "} <b>{t("chat.mode.query.title")}</b>{" "}
<i className="font-semibold">only</i> if document context is {t("chat.mode.query.desc-start")}{" "}
found. <i className="font-semibold">{t("chat.mode.query.only")}</i>{" "}
{t("chat.mode.query.desc-end")}
</> </>
)} )}
</p> </p>

View File

@ -1,7 +1,7 @@
import useGetProviderModels, { import useGetProviderModels, {
DISABLED_PROVIDERS, DISABLED_PROVIDERS,
} from "@/hooks/useGetProvidersModels"; } from "@/hooks/useGetProvidersModels";
import { useTranslation } from "react-i18next";
export default function ChatModelSelection({ export default function ChatModelSelection({
provider, provider,
workspace, workspace,
@ -9,6 +9,7 @@ export default function ChatModelSelection({
}) { }) {
const { defaultModels, customModels, loading } = const { defaultModels, customModels, loading } =
useGetProviderModels(provider); useGetProviderModels(provider);
const { t } = useTranslation();
if (DISABLED_PROVIDERS.includes(provider)) return null; if (DISABLED_PROVIDERS.includes(provider)) return null;
if (loading) { if (loading) {
@ -16,11 +17,10 @@ export default function ChatModelSelection({
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Workspace Chat model {t("chat.model.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
The specific chat model that will be used for this workspace. If {t("chat.model.description")}
empty, will use the system LLM preference.
</p> </p>
</div> </div>
<select <select
@ -41,11 +41,10 @@ export default function ChatModelSelection({
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Workspace Chat model {t("chat.model.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
The specific chat model that will be used for this workspace. If {t("chat.model.description")}
empty, will use the system LLM preference.
</p> </p>
</div> </div>

View File

@ -1,17 +1,15 @@
import { chatPrompt } from "@/utils/chat"; import { chatPrompt } from "@/utils/chat";
import { useTranslation } from "react-i18next";
export default function ChatPromptSettings({ workspace, setHasChanges }) { export default function ChatPromptSettings({ workspace, setHasChanges }) {
const { t } = useTranslation();
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Prompt {t("chat.prompt.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
The prompt that will be used on this workspace. Define the context and {t("chat.prompt.description")}
instructions for the AI to generate a response. You should to provide
a carefully crafted prompt so the AI can generate a relevant and
accurate response.
</p> </p>
</div> </div>
<textarea <textarea

View File

@ -1,16 +1,19 @@
import { chatQueryRefusalResponse } from "@/utils/chat"; import { chatQueryRefusalResponse } from "@/utils/chat";
import { useTranslation } from "react-i18next";
export default function ChatQueryRefusalResponse({ workspace, setHasChanges }) { export default function ChatQueryRefusalResponse({ workspace, setHasChanges }) {
const { t } = useTranslation();
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Query mode refusal response {t("chat.refusal.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
When in <code className="bg-zinc-900 p-0.5 rounded-sm">query</code>{" "} {t("chat.refusal.desc-start")}{" "}
mode, you may want to return a custom refusal response when no context <code className="bg-zinc-900 p-0.5 rounded-sm">
is found. {t("chat.refusal.query")}
</code>{" "}
{t("chat.refusal.desc-end")}
</p> </p>
</div> </div>
<textarea <textarea

View File

@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
function recommendedSettings(provider = null) { function recommendedSettings(provider = null) {
switch (provider) { switch (provider) {
case "mistral": case "mistral":
@ -13,24 +14,20 @@ export default function ChatTemperatureSettings({
setHasChanges, setHasChanges,
}) { }) {
const defaults = recommendedSettings(settings?.LLMProvider); const defaults = recommendedSettings(settings?.LLMProvider);
const { t } = useTranslation();
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
LLM Temperature {t("chat.temperature.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
This setting controls how &quot;creative&quot; your LLM responses will {t("chat.temperature.desc-start")}
be.
<br /> <br />
The higher the number the more creative. For some models this can lead {t("chat.temperature.desc-end")}
to incoherent responses when set too high.
<br /> <br />
<br /> <br />
<i> <i>{t("chat.temperature.hint")}</i>
Most LLMs have various acceptable ranges of valid values. Consult
your LLM provider for that information.
</i>
</p> </p>
</div> </div>
<input <input

View File

@ -4,6 +4,7 @@ import WorkspaceLLMItem from "./WorkspaceLLMItem";
import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference"; import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import ChatModelSelection from "../ChatModelSelection"; import ChatModelSelection from "../ChatModelSelection";
import { useTranslation } from "react-i18next";
// Some providers can only be associated with a single model. // Some providers can only be associated with a single model.
// In that case there is no selection to be made so we can just move on. // In that case there is no selection to be made so we can just move on.
@ -34,7 +35,7 @@ export default function WorkspaceLLMSelection({
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [searchMenuOpen, setSearchMenuOpen] = useState(false); const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
const { t } = useTranslation();
function updateLLMChoice(selection) { function updateLLMChoice(selection) {
setSearchQuery(""); setSearchQuery("");
setSelectedLLM(selection); setSelectedLLM(selection);
@ -63,11 +64,10 @@ export default function WorkspaceLLMSelection({
<div className="border-b border-white/40 pb-8"> <div className="border-b border-white/40 pb-8">
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Workspace LLM Provider {t("chat.llm.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
The specific LLM provider & model that will be used for this {t("chat.llm.description")}
workspace. By default, it uses the system LLM provider and settings.
</p> </p>
</div> </div>
@ -92,7 +92,7 @@ export default function WorkspaceLLMSelection({
type="text" type="text"
name="llm-search" name="llm-search"
autoComplete="off" autoComplete="off"
placeholder="Search all LLM providers" placeholder={t("chat.llm.search")}
className="-ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium" className="-ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium"
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef} ref={searchInputRef}

View File

@ -3,12 +3,13 @@ import { useParams } from "react-router-dom";
import Workspace from "@/models/workspace"; import Workspace from "@/models/workspace";
import paths from "@/utils/paths"; import paths from "@/utils/paths";
import System from "@/models/system"; import System from "@/models/system";
import { useTranslation } from "react-i18next";
export default function DeleteWorkspace({ workspace }) { export default function DeleteWorkspace({ workspace }) {
const { slug } = useParams(); const { slug } = useParams();
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const [canDelete, setCanDelete] = useState(false); const [canDelete, setCanDelete] = useState(false);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
async function fetchKeys() { async function fetchKeys() {
const canDelete = await System.getCanDeleteWorkspaces(); const canDelete = await System.getCanDeleteWorkspaces();
@ -20,7 +21,9 @@ export default function DeleteWorkspace({ workspace }) {
const deleteWorkspace = async () => { const deleteWorkspace = async () => {
if ( if (
!window.confirm( !window.confirm(
`You are about to delete your entire ${workspace.name} workspace. This will remove all vector embeddings on your vector database.\n\nThe original source files will remain untouched. This action is irreversible.` `${t("general.delete.confirm-start")} ${workspace.name} ${t(
"general.delete.confirm-end"
)}`
) )
) )
return false; return false;
@ -46,7 +49,7 @@ export default function DeleteWorkspace({ workspace }) {
type="button" type="button"
className="w-60 mt-[40px] transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-red-500/25 text-red-200 hover:text-white hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse" className="w-60 mt-[40px] transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-red-500/25 text-red-200 hover:text-white hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse"
> >
{deleting ? "Deleting Workspace..." : "Delete Workspace"} {deleting ? t("general.delete.deleting") : t("general.delete.delete")}
</button> </button>
); );
} }

View File

@ -3,6 +3,7 @@ import Workspace from "@/models/workspace";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Plus, X } from "@phosphor-icons/react"; import { Plus, X } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
export default function SuggestedChatMessages({ slug }) { export default function SuggestedChatMessages({ slug }) {
const [suggestedMessages, setSuggestedMessages] = useState([]); const [suggestedMessages, setSuggestedMessages] = useState([]);
@ -10,7 +11,7 @@ export default function SuggestedChatMessages({ slug }) {
const [newMessage, setNewMessage] = useState({ heading: "", message: "" }); const [newMessage, setNewMessage] = useState({ heading: "", message: "" });
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
async function fetchWorkspace() { async function fetchWorkspace() {
if (!slug) return; if (!slug) return;
@ -45,8 +46,8 @@ export default function SuggestedChatMessages({ slug }) {
return; return;
} }
const defaultMessage = { const defaultMessage = {
heading: "Explain to me", heading: t("general.message.heading"),
message: "the benefits of AnythingLLM", message: t("general.message.body"),
}; };
setNewMessage(defaultMessage); setNewMessage(defaultMessage);
setSuggestedMessages([...suggestedMessages, { ...defaultMessage }]); setSuggestedMessages([...suggestedMessages, { ...defaultMessage }]);
@ -91,9 +92,11 @@ export default function SuggestedChatMessages({ slug }) {
if (loading) if (loading)
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<label className="block input-label">Suggested Chat Messages</label> <label className="block input-label">
{t("general.message.title")}
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
Customize the messages that will be suggested to your workspace users. {t("general.message.description")}
</p> </p>
<p className="text-white text-opacity-60 text-sm font-medium mt-6"> <p className="text-white text-opacity-60 text-sm font-medium mt-6">
<PreLoader size="4" /> <PreLoader size="4" />
@ -103,9 +106,11 @@ export default function SuggestedChatMessages({ slug }) {
return ( return (
<div className="w-screen mt-6"> <div className="w-screen mt-6">
<div className="flex flex-col"> <div className="flex flex-col">
<label className="block input-label">Suggested Chat Messages</label> <label className="block input-label">
{t("general.message.title")}
</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
Customize the messages that will be suggested to your workspace users. {t("general.message.description")}
</p> </p>
</div> </div>
@ -169,7 +174,8 @@ export default function SuggestedChatMessages({ slug }) {
onClick={addMessage} onClick={addMessage}
className="flex gap-x-2 items-center justify-center mt-6 text-white text-sm hover:text-sky-400 transition-all duration-300" className="flex gap-x-2 items-center justify-center mt-6 text-white text-sm hover:text-sky-400 transition-all duration-300"
> >
Add new message <Plus className="" size={24} weight="fill" /> {t("general.message.add")}{" "}
<Plus className="" size={24} weight="fill" />
</button> </button>
)} )}
@ -180,7 +186,7 @@ export default function SuggestedChatMessages({ slug }) {
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
onClick={handleSaveSuggestedMessages} onClick={handleSaveSuggestedMessages}
> >
Save Messages {t("general.message.save")}
</button> </button>
</div> </div>
)} )}

View File

@ -1,12 +1,15 @@
import { useTranslation } from "react-i18next";
export default function WorkspaceName({ workspace, setHasChanges }) { export default function WorkspaceName({ workspace, setHasChanges }) {
const { t } = useTranslation();
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Workspace Name {t("common.workspaces-name")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
This will only change the display name of your workspace. {t("general.names.description")}
</p> </p>
</div> </div>
<input <input

View File

@ -2,10 +2,11 @@ import Workspace from "@/models/workspace";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { Plus } from "@phosphor-icons/react"; import { Plus } from "@phosphor-icons/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
export default function WorkspacePfp({ workspace, slug }) { export default function WorkspacePfp({ workspace, slug }) {
const [pfp, setPfp] = useState(null); const [pfp, setPfp] = useState(null);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
async function fetchWorkspace() { async function fetchWorkspace() {
const pfpUrl = await Workspace.fetchPfp(slug); const pfpUrl = await Workspace.fetchPfp(slug);
@ -47,9 +48,9 @@ export default function WorkspacePfp({ workspace, slug }) {
return ( return (
<div className="mt-6"> <div className="mt-6">
<div className="flex flex-col"> <div className="flex flex-col">
<label className="block input-label">Assistant Profile Image</label> <label className="block input-label">{t("general.pfp.title")}</label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
Customize the profile image of the assistant for this workspace. {t("general.pfp.description")}
</p> </p>
</div> </div>
<div className="flex flex-col md:flex-row items-center gap-8"> <div className="flex flex-col md:flex-row items-center gap-8">
@ -72,7 +73,7 @@ export default function WorkspacePfp({ workspace, slug }) {
<div className="flex flex-col items-center justify-center p-3"> <div className="flex flex-col items-center justify-center p-3">
<Plus className="w-8 h-8 text-white/80 m-2" /> <Plus className="w-8 h-8 text-white/80 m-2" />
<span className="text-white text-opacity-80 text-xs font-semibold"> <span className="text-white text-opacity-80 text-xs font-semibold">
Workspace Image {t("general.pfp.image")}
</span> </span>
<span className="text-white text-opacity-60 text-xs"> <span className="text-white text-opacity-60 text-xs">
800 x 800 800 x 800
@ -86,7 +87,7 @@ export default function WorkspacePfp({ workspace, slug }) {
onClick={handleRemovePfp} onClick={handleRemovePfp}
className="mt-3 text-white text-opacity-60 text-sm font-medium hover:underline" className="mt-3 text-white text-opacity-60 text-sm font-medium hover:underline"
> >
Remove Workspace Image {t("general.pfp.remove")}
</button> </button>
)} )}
</div> </div>

View File

@ -1,17 +1,18 @@
import { useTranslation } from "react-i18next";
export default function DocumentSimilarityThreshold({ export default function DocumentSimilarityThreshold({
workspace, workspace,
setHasChanges, setHasChanges,
}) { }) {
const { t } = useTranslation();
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Document similarity threshold {t("vector-workspace.doc.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
The minimum similarity score required for a source to be considered {t("vector-workspace.doc.description")}
related to the chat. The higher the number, the more similar the
source must be to the chat.
</p> </p>
</div> </div>
<select <select
@ -21,10 +22,10 @@ export default function DocumentSimilarityThreshold({
onChange={() => setHasChanges(true)} onChange={() => setHasChanges(true)}
required={true} required={true}
> >
<option value={0.0}>No restriction</option> <option value={0.0}>{t("vector-workspace.doc.zero")}</option>
<option value={0.25}>Low (similarity score &ge; .25)</option> <option value={0.25}>{t("vector-workspace.doc.low")}</option>
<option value={0.5}>Medium (similarity score &ge; .50)</option> <option value={0.5}>{t("vector-workspace.doc.medium")}</option>
<option value={0.75}>High (similarity score &ge; .75)</option> <option value={0.75}>{t("vector-workspace.doc.high")}</option>
</select> </select>
</div> </div>
); );

View File

@ -1,15 +1,17 @@
import { useTranslation } from "react-i18next";
export default function MaxContextSnippets({ workspace, setHasChanges }) { export default function MaxContextSnippets({ workspace, setHasChanges }) {
const { t } = useTranslation();
return ( return (
<div> <div>
<div className="flex flex-col"> <div className="flex flex-col">
<label htmlFor="name" className="block input-label"> <label htmlFor="name" className="block input-label">
Max Context Snippets {t("vector-workspace.snippets.title")}
</label> </label>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
This setting controls the maximum amount of context snippets the will {t("vector-workspace.snippets.description")}
be sent to the LLM for per chat or query.
<br /> <br />
<i>Recommended: 4</i> <i>{t("vector-workspace.snippets.recommend")}</i>
</p> </p>
</div> </div>
<input <input

View File

@ -1,31 +1,35 @@
import { useState } from "react"; import { useState } from "react";
import Workspace from "@/models/workspace"; import Workspace from "@/models/workspace";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import { useTranslation } from "react-i18next";
export default function ResetDatabase({ workspace }) { export default function ResetDatabase({ workspace }) {
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const { t } = useTranslation();
const resetVectorDatabase = async () => { const resetVectorDatabase = async () => {
if ( if (!window.confirm(`${t("vector-workspace.reset.confirm")}`)) return false;
!window.confirm(
`You are about to reset this workspace's vector database. This will remove all vector embeddings currently embedded.\n\nThe original source files will remain untouched. This action is irreversible.`
)
)
return false;
setDeleting(true); setDeleting(true);
const success = await Workspace.wipeVectorDb(workspace.slug); const success = await Workspace.wipeVectorDb(workspace.slug);
if (!success) { if (!success) {
showToast("Workspace vector database could not be reset!", "error", { showToast(
t("vector-workspace.reset.error"),
t("vector-workspace.common.error"),
{
clear: true, clear: true,
}); }
);
setDeleting(false); setDeleting(false);
return; return;
} }
showToast("Workspace vector database was reset!", "success", { showToast(
t("vector-workspace.reset.success"),
t("vector-workspace.common.success"),
{
clear: true, clear: true,
}); }
);
setDeleting(false); setDeleting(false);
}; };
@ -36,7 +40,9 @@ export default function ResetDatabase({ workspace }) {
type="button" type="button"
className="border-none w-fit transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-red-500/25 text-red-200 hover:text-white hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse" className="border-none w-fit transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-red-500/25 text-red-200 hover:text-white hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse"
> >
{deleting ? "Clearing vectors..." : "Reset Workspace Vector Database"} {deleting
? t("vector-workspace.reset.resetting")
: t("vector-workspace.reset.reset")}
</button> </button>
); );
} }

View File

@ -1,9 +1,11 @@
import PreLoader from "@/components/Preloader"; import PreLoader from "@/components/Preloader";
import System from "@/models/system"; import System from "@/models/system";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
export default function VectorCount({ reload, workspace }) { export default function VectorCount({ reload, workspace }) {
const [totalVectors, setTotalVectors] = useState(null); const [totalVectors, setTotalVectors] = useState(null);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
async function fetchVectorCount() { async function fetchVectorCount() {
@ -16,9 +18,9 @@ export default function VectorCount({ reload, workspace }) {
if (totalVectors === null) if (totalVectors === null)
return ( return (
<div> <div>
<h3 className="input-label">Number of vectors</h3> <h3 className="input-label">{t("general.vector.title")}</h3>
<p className="text-white text-opacity-60 text-xs font-medium py-1"> <p className="text-white text-opacity-60 text-xs font-medium py-1">
Total number of vectors in your vector database. {t("general.vector.description")}
</p> </p>
<p className="text-white text-opacity-60 text-sm font-medium"> <p className="text-white text-opacity-60 text-sm font-medium">
<PreLoader size="4" /> <PreLoader size="4" />
@ -27,7 +29,7 @@ export default function VectorCount({ reload, workspace }) {
); );
return ( return (
<div> <div>
<h3 className="input-label">Number of vectors</h3> <h3 className="input-label">{t("general.vector.title")}</h3>
<p className="text-white text-opacity-60 text-sm font-medium"> <p className="text-white text-opacity-60 text-sm font-medium">
{totalVectors} {totalVectors}
</p> </p>

View File

@ -1,7 +1,10 @@
import { useTranslation } from "react-i18next";
export default function VectorDBIdentifier({ workspace }) { export default function VectorDBIdentifier({ workspace }) {
const { t } = useTranslation();
return ( return (
<div> <div>
<h3 className="input-label">Vector database identifier</h3> <h3 className="input-label">{t("vector-workspace.identifier")}</h3>
<p className="text-white/60 text-xs font-medium py-1"> </p> <p className="text-white/60 text-xs font-medium py-1"> </p>
<p className="text-white/60 text-sm">{workspace?.slug}</p> <p className="text-white/60 text-sm">{workspace?.slug}</p>
</div> </div>

View File

@ -22,6 +22,7 @@ import VectorDatabase from "./VectorDatabase";
import Members from "./Members"; import Members from "./Members";
import WorkspaceAgentConfiguration from "./AgentConfig"; import WorkspaceAgentConfiguration from "./AgentConfig";
import useUser from "@/hooks/useUser"; import useUser from "@/hooks/useUser";
import { useTranslation } from "react-i18next";
const TABS = { const TABS = {
"general-appearance": GeneralAppearance, "general-appearance": GeneralAppearance,
@ -43,6 +44,7 @@ export default function WorkspaceSettings() {
} }
function ShowWorkspaceChat() { function ShowWorkspaceChat() {
const { t } = useTranslation();
const { slug, tab } = useParams(); const { slug, tab } = useParams();
const { user } = useUser(); const { user } = useUser();
const [workspace, setWorkspace] = useState(null); const [workspace, setWorkspace] = useState(null);
@ -85,28 +87,28 @@ function ShowWorkspaceChat() {
<ArrowUUpLeft className="h-5 w-5" weight="fill" /> <ArrowUUpLeft className="h-5 w-5" weight="fill" />
</Link> </Link>
<TabItem <TabItem
title="General Settings" title={t("workspaces—settings.general")}
icon={<Wrench className="h-6 w-6" />} icon={<Wrench className="h-6 w-6" />}
to={paths.workspace.settings.generalAppearance(slug)} to={paths.workspace.settings.generalAppearance(slug)}
/> />
<TabItem <TabItem
title="Chat Settings" title={t("workspaces—settings.chat")}
icon={<ChatText className="h-6 w-6" />} icon={<ChatText className="h-6 w-6" />}
to={paths.workspace.settings.chatSettings(slug)} to={paths.workspace.settings.chatSettings(slug)}
/> />
<TabItem <TabItem
title="Vector Database" title={t("workspaces—settings.vector")}
icon={<Database className="h-6 w-6" />} icon={<Database className="h-6 w-6" />}
to={paths.workspace.settings.vectorDatabase(slug)} to={paths.workspace.settings.vectorDatabase(slug)}
/> />
<TabItem <TabItem
title="Members" title={t("workspaces—settings.members")}
icon={<User className="h-6 w-6" />} icon={<User className="h-6 w-6" />}
to={paths.workspace.settings.members(slug)} to={paths.workspace.settings.members(slug)}
visible={["admin", "manager"].includes(user?.role)} visible={["admin", "manager"].includes(user?.role)}
/> />
<TabItem <TabItem
title="Agent Configuration" title={t("workspaces—settings.agent")}
icon={<Robot className="h-6 w-6" />} icon={<Robot className="h-6 w-6" />}
to={paths.workspace.settings.agentConfig(slug)} to={paths.workspace.settings.agentConfig(slug)}
/> />

View File

@ -191,6 +191,13 @@
dependencies: dependencies:
regenerator-runtime "^0.14.0" regenerator-runtime "^0.14.0"
"@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9":
version "7.24.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c"
integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15": "@babel/template@^7.22.15":
version "7.22.15" version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
@ -1956,6 +1963,13 @@ highlight.js@^11.9.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw== integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
html-parse-stringify@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
dependencies:
void-elements "3.1.0"
html2canvas@^1.2.0: html2canvas@^1.2.0:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
@ -1974,6 +1988,20 @@ human-signals@^4.3.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"
integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==
i18next-browser-languagedetector@^7.2.1:
version "7.2.1"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz#1968196d437b4c8db847410c7c33554f6c448f6f"
integrity sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==
dependencies:
"@babel/runtime" "^7.23.2"
i18next@^23.11.3:
version "23.11.3"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.11.3.tgz#d269c9c15bae9d90ab291055cfc433089ca5f77b"
integrity sha512-Pq/aSKowir7JM0rj+Wa23Kb6KKDUGno/HjG+wRQu0PxoTbpQ4N89MAT0rFGvXmLkRLNMb1BbBOKGozl01dabzg==
dependencies:
"@babel/runtime" "^7.23.2"
ieee754@^1.2.1: ieee754@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -2807,6 +2835,14 @@ react-dropzone@^14.2.3:
file-selector "^0.6.0" file-selector "^0.6.0"
prop-types "^15.8.1" prop-types "^15.8.1"
react-i18next@^14.1.1:
version "14.1.1"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.1.1.tgz#3d942a99866555ae3552c40f9fddfa061e29d7f3"
integrity sha512-QSiKw+ihzJ/CIeIYWrarCmXJUySHDwQr5y8uaNIkbxoGRm/5DukkxZs+RPla79IKyyDPzC/DRlgQCABHtrQuQQ==
dependencies:
"@babel/runtime" "^7.23.9"
html-parse-stringify "^3.0.1"
react-is@^16.10.2, react-is@^16.13.1: react-is@^16.10.2, react-is@^16.13.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -3626,6 +3662,11 @@ vlq@^0.2.1:
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
void-elements@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
which-boxed-primitive@^1.0.2: which-boxed-primitive@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"

View File

@ -24,7 +24,8 @@
"prod:server": "cd server && yarn start", "prod:server": "cd server && yarn start",
"prod:frontend": "cd frontend && yarn build", "prod:frontend": "cd frontend && yarn build",
"generate:cloudformation": "node cloud-deployments/aws/cloudformation/generate.mjs", "generate:cloudformation": "node cloud-deployments/aws/cloudformation/generate.mjs",
"generate::gcp_deployment": "node cloud-deployments/gcp/deployment/generate.mjs" "generate::gcp_deployment": "node cloud-deployments/gcp/deployment/generate.mjs",
"verify:translations": "cd frontend/src/locales && node verifyTranslations.mjs"
}, },
"private": false "private": false
} }