From 93dd365fdab6dc3030bfbb4271396dd02700e486 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 16 Feb 2025 10:52:29 -0500 Subject: [PATCH 01/32] =?UTF-8?q?=F0=9F=90=9E=20fix:=20Add=20Null=20Checks?= =?UTF-8?q?=20for=20BaseURL=20in=20Agent=20Config=20(#5908)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server/controllers/agents/run.js | 2 +- api/server/services/Endpoints/openAI/llm.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/server/controllers/agents/run.js b/api/server/controllers/agents/run.js index 346b9e6df8..6c98a641db 100644 --- a/api/server/controllers/agents/run.js +++ b/api/server/controllers/agents/run.js @@ -45,7 +45,7 @@ async function createRun({ /** @type {'reasoning_content' | 'reasoning'} */ let reasoningKey; - if (llmConfig.configuration?.baseURL.includes(KnownEndpoints.openrouter)) { + if (llmConfig.configuration?.baseURL?.includes(KnownEndpoints.openrouter)) { reasoningKey = 'reasoning'; } if (/o1(?!-(?:mini|preview)).*$/.test(llmConfig.model)) { diff --git a/api/server/services/Endpoints/openAI/llm.js b/api/server/services/Endpoints/openAI/llm.js index 05b08b284b..c12f835f2f 100644 --- a/api/server/services/Endpoints/openAI/llm.js +++ b/api/server/services/Endpoints/openAI/llm.js @@ -58,7 +58,7 @@ function getLLMConfig(apiKey, options = {}) { /** @type {OpenAIClientOptions['configuration']} */ const configOptions = {}; - if (useOpenRouter || reverseProxyUrl.includes(KnownEndpoints.openrouter)) { + if (useOpenRouter || (reverseProxyUrl && reverseProxyUrl.includes(KnownEndpoints.openrouter))) { llmConfig.include_reasoning = true; configOptions.baseURL = reverseProxyUrl; configOptions.defaultHeaders = Object.assign( From a65647a7dea18ee58b0c64f578a1332f177e1162 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 16 Feb 2025 11:47:01 -0500 Subject: [PATCH 02/32] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20refactor:=20Enhance?= =?UTF-8?q?=20Logging,=20Navigation=20And=20Error=20Handling=20(#5910)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Ensure Axios Errors are less Verbose if No Response * refactor: Improve error handling in logAxiosError function * fix: Prevent ModelSelect from rendering for Agent Endpoints * refactor: Enhance logging functions with type parameter for better clarity * refactor: Update buildDefaultConvo function to use optional endpoint parameter since we pass a default value for undefined * refactor: Replace console logs with logger warnings and errors in useNavigateToConvo hook, and handle removed endpoint edge case * chore: import order --- api/utils/axios.js | 56 ++++++++----------- .../components/Chat/Input/HeaderOptions.tsx | 12 ++-- .../Conversations/useNavigateToConvo.tsx | 16 +++--- client/src/utils/buildDefaultConvo.ts | 8 +-- client/src/utils/logger.ts | 21 ++++--- 5 files changed, 57 insertions(+), 56 deletions(-) diff --git a/api/utils/axios.js b/api/utils/axios.js index 8b12a5ca99..acd23a184f 100644 --- a/api/utils/axios.js +++ b/api/utils/axios.js @@ -5,40 +5,32 @@ const { logger } = require('~/config'); * * @param {Object} options - The options object. * @param {string} options.message - The custom message to be logged. - * @param {Error} options.error - The Axios error object. + * @param {import('axios').AxiosError} options.error - The Axios error object. */ const logAxiosError = ({ message, error }) => { - const timedOutMessage = 'Cannot read properties of undefined (reading \'status\')'; - if (error.response) { - logger.error( - `${message} The request was made and the server responded with a status code that falls out of the range of 2xx: ${ - error.message ? error.message : '' - }. Error response data:\n`, - { - headers: error.response?.headers, - status: error.response?.status, - data: error.response?.data, - }, - ); - } else if (error.request) { - logger.error( - `${message} The request was made but no response was received: ${ - error.message ? error.message : '' - }. Error Request:\n`, - { - request: error.request, - }, - ); - } else if (error?.message?.includes(timedOutMessage)) { - logger.error( - `${message}\nThe request either timed out or was unsuccessful. Error message:\n`, - error, - ); - } else { - logger.error( - `${message}\nSomething happened in setting up the request. Error message:\n`, - error, - ); + try { + if (error.response?.status) { + const { status, headers, data } = error.response; + logger.error(`${message} The server responded with status ${status}: ${error.message}`, { + status, + headers, + data, + }); + } else if (error.request) { + const { method, url } = error.config || {}; + logger.error( + `${message} No response received for ${method ? method.toUpperCase() : ''} ${url || ''}: ${error.message}`, + { requestInfo: { method, url } }, + ); + } else if (error?.message?.includes('Cannot read properties of undefined (reading \'status\')')) { + logger.error( + `${message} It appears the request timed out or was unsuccessful: ${error.message}`, + ); + } else { + logger.error(`${message} An error occurred while setting up the request: ${error.message}`); + } + } catch (err) { + logger.error(`Error in logAxiosError: ${err.message}`); } }; diff --git a/client/src/components/Chat/Input/HeaderOptions.tsx b/client/src/components/Chat/Input/HeaderOptions.tsx index 0bd3326b53..5313f43b8d 100644 --- a/client/src/components/Chat/Input/HeaderOptions.tsx +++ b/client/src/components/Chat/Input/HeaderOptions.tsx @@ -1,8 +1,13 @@ import { useRecoilState } from 'recoil'; import { Settings2 } from 'lucide-react'; -import { Root, Anchor } from '@radix-ui/react-popover'; import { useState, useEffect, useMemo } from 'react'; -import { tConvoUpdateSchema, EModelEndpoint, isParamEndpoint } from 'librechat-data-provider'; +import { Root, Anchor } from '@radix-ui/react-popover'; +import { + EModelEndpoint, + isParamEndpoint, + isAgentsEndpoint, + tConvoUpdateSchema, +} from 'librechat-data-provider'; import type { TPreset, TInterfaceConfig } from 'librechat-data-provider'; import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints'; import { PluginStoreDialog, TooltipAnchor } from '~/components'; @@ -42,7 +47,6 @@ export default function HeaderOptions({ if (endpoint && noSettings[endpoint]) { setShowPopover(false); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [endpoint, noSettings]); const saveAsPreset = () => { @@ -67,7 +71,7 @@ export default function HeaderOptions({
- {interfaceConfig?.modelSelect === true && ( + {interfaceConfig?.modelSelect === true && !isAgentsEndpoint(endpoint) && ( { @@ -20,7 +20,7 @@ const useNavigateToConvo = (index = 0) => { invalidateMessages = false, ) => { if (!conversation) { - console.log('Conversation not provided'); + logger.warn('conversation', 'Conversation not provided to `navigateToConvo`'); return; } hasSetConversation.current = true; @@ -34,10 +34,10 @@ const useNavigateToConvo = (index = 0) => { } let convo = { ...conversation }; - if (!convo.endpoint) { - /* undefined endpoint edge case */ + const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); + if (!convo.endpoint || !endpointsConfig?.[convo.endpoint]) { + /* undefined/removed endpoint edge case */ const modelsConfig = queryClient.getQueryData([QueryKeys.models]); - const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); const defaultEndpoint = getDefaultEndpoint({ convoSetup: conversation, endpointsConfig, @@ -51,10 +51,10 @@ const useNavigateToConvo = (index = 0) => { const models = modelsConfig?.[defaultEndpoint ?? ''] ?? []; convo = buildDefaultConvo({ + models, conversation, endpoint: defaultEndpoint, lastConversationSetup: conversation, - models, }); } clearAllConversations(true); @@ -68,7 +68,7 @@ const useNavigateToConvo = (index = 0) => { invalidateMessages?: boolean, ) => { if (!conversation) { - console.log('Conversation not provided'); + logger.warn('conversation', 'Conversation not provided to `navigateToConvo`'); return; } // set conversation to the new conversation @@ -78,7 +78,7 @@ const useNavigateToConvo = (index = 0) => { lastSelectedTools = JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_TOOLS) ?? '') ?? []; } catch (e) { - // console.error(e); + logger.error('conversation', 'Error parsing last selected tools', e); } const hasTools = (conversation.tools?.length ?? 0) > 0; navigateToConvo( diff --git a/client/src/utils/buildDefaultConvo.ts b/client/src/utils/buildDefaultConvo.ts index 429e400cf8..7d60aecfda 100644 --- a/client/src/utils/buildDefaultConvo.ts +++ b/client/src/utils/buildDefaultConvo.ts @@ -8,14 +8,14 @@ import type { TConversation } from 'librechat-data-provider'; import { getLocalStorageItems } from './localStorage'; const buildDefaultConvo = ({ + models, conversation, endpoint = null, - models, lastConversationSetup, }: { - conversation: TConversation; - endpoint: EModelEndpoint | null; models: string[]; + conversation: TConversation; + endpoint?: EModelEndpoint | null; lastConversationSetup: TConversation | null; }): TConversation => { const { lastSelectedModel, lastSelectedTools } = getLocalStorageItems(); @@ -33,7 +33,7 @@ const buildDefaultConvo = ({ const model = lastConversationSetup?.model ?? lastSelectedModel?.[endpoint] ?? ''; const secondaryModel: string | null = endpoint === EModelEndpoint.gptPlugins - ? lastConversationSetup?.agentOptions?.model ?? lastSelectedModel?.secondaryModel ?? null + ? (lastConversationSetup?.agentOptions?.model ?? lastSelectedModel?.secondaryModel ?? null) : null; let possibleModels: string[], secondaryModels: string[]; diff --git a/client/src/utils/logger.ts b/client/src/utils/logger.ts index 413a96f17d..6bc1d21db6 100644 --- a/client/src/utils/logger.ts +++ b/client/src/utils/logger.ts @@ -4,12 +4,17 @@ const loggerFilter = import.meta.env.VITE_LOGGER_FILTER || ''; type LogFunction = (...args: unknown[]) => void; -const createLogFunction = (consoleMethod: LogFunction): LogFunction => { +const createLogFunction = ( + consoleMethod: LogFunction, + type?: 'log' | 'warn' | 'error' | 'info' | 'debug' | 'dir', +): LogFunction => { return (...args: unknown[]) => { if (isDevelopment || isLoggerEnabled) { const tag = typeof args[0] === 'string' ? args[0] : ''; if (shouldLog(tag)) { - if (tag && args.length > 1) { + if (tag && typeof args[1] === 'string' && type === 'error') { + consoleMethod(`[${tag}] ${args[1]}`, ...args.slice(2)); + } else if (tag && args.length > 1) { consoleMethod(`[${tag}]`, ...args.slice(1)); } else { consoleMethod(...args); @@ -20,12 +25,12 @@ const createLogFunction = (consoleMethod: LogFunction): LogFunction => { }; const logger = { - log: createLogFunction(console.log), - warn: createLogFunction(console.warn), - error: createLogFunction(console.error), - info: createLogFunction(console.info), - debug: createLogFunction(console.debug), - dir: createLogFunction(console.dir), + log: createLogFunction(console.log, 'log'), + dir: createLogFunction(console.dir, 'dir'), + warn: createLogFunction(console.warn, 'warn'), + info: createLogFunction(console.info, 'info'), + error: createLogFunction(console.error, 'error'), + debug: createLogFunction(console.debug, 'debug'), }; function shouldLog(tag: string): boolean { From 46ceae1a9361770dd81f25181e23ff1d36bf8bbf Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:39:46 +0100 Subject: [PATCH 03/32] =?UTF-8?q?=E2=9A=96=EF=B8=8F=20docs:=20Update=20LIC?= =?UTF-8?q?ENSE.md=20Year:=202024=20->=202025=20(#5915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 49a224977b..535850a920 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 LibreChat +Copyright (c) 2025 LibreChat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From f0f09138bd6db18ea05b44c2cdcff6e54b873032 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Tue, 18 Feb 2025 01:09:36 +0100 Subject: [PATCH 04/32] =?UTF-8?q?=F0=9F=94=92=20feat:=20Two-Factor=20Authe?= =?UTF-8?q?ntication=20with=20Backup=20Codes=20&=20QR=20support=20(#5685)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿ”’ feat: add Two-Factor Authentication (2FA) with backup codes & QR support (#5684) * working version for generating TOTP and authenticate. * better looking UI * refactored + better TOTP logic * fixed issue with UI * fixed issue: remove initial setup when closing window before completion. * added: onKeyDown for verify and disable * refactored some code and cleaned it up a bit. * refactored some code and cleaned it up a bit. * refactored some code and cleaned it up a bit. * refactored some code and cleaned it up a bit. * fixed issue after updating to new main branch * updated example * refactored controllers * removed `passport-totp` not used. * update the generateBackupCodes function to generate 10 codes by default: * update the backup codes to an object. * fixed issue with backup codes not working * be able to disable 2FA with backup codes. * removed new env. replaced with JWT_SECRET * โœจ style: improved a11y and style for TwoFactorAuthentication * ๐Ÿ”’ fix: small types checks * โœจ feat: improve 2FA UI components * fix: remove unnecessary console log * add option to disable 2FA with backup codes * - add option to refresh backup codes - (optional) maybe show the user which backup codes have already been used? * removed text to be able to merge the main. * removed eng tx to be able to merge * fix: migrated lang to new format. * feat: rewrote whole 2FA UI + refactored 2FA backend * chore: resolving conflicts * chore: resolving conflicts * fix: missing packages, because of resolving conflicts. * fix: UI issue and improved a11y * fix: 2FA backup code not working * fix: update localization keys for UI consistency * fix: update button label to use localized text * fix: refactor backup codes regeneration and update localization keys * fix: remove outdated translation for shared links management * fix: remove outdated 2FA code prompts from translation.json * fix: add cursor styles for backup codes item based on usage state * fix: resolve conflict issue * fix: resolve conflict issue * fix: resolve conflict issue * fix: missing packages in package-lock.json * fix: add disabled opacity to the verify button in TwoFactorScreen * โš™ fix: update 2FA logic to rely on backup codes instead of TOTP status * โš™๏ธ fix: Simplify user retrieval in 2FA logic by removing unnecessary TOTP secret query * โš™๏ธ test: Add unit tests for TwoFactorAuthController and twoFactorControllers * โš™๏ธ fix: Ensure backup codes are validated as an array before usage in 2FA components * โš™๏ธ fix: Update module path mappings in tests to use relative paths * โš™๏ธ fix: Update moduleNameMapper in jest.config.js to remove the caret from path mapping * โš™๏ธ refactor: Simplify import paths in TwoFactorAuthController and twoFactorControllers test files * โš™๏ธ test: Mock twoFactorService methods in twoFactorControllers tests * โš™๏ธ refactor: Comment out unused imports and mock setups in test files for two-factor authentication * โš™๏ธ refactor: removed files * refactor: Exclude totpSecret from user data retrieval in AuthController, LoginController, and jwtStrategy * refactor: Consolidate backup code verification to apply DRY and remove default array in user schema * refactor: Enhance two-factor authentication ux/flow with improved error handling and loading state management, prevent redirect to /login --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com> Co-authored-by: Danny Avila --- api/models/schema/userSchema.js | 13 +- api/server/controllers/AuthController.js | 2 +- api/server/controllers/TwoFactorController.js | 111 +++++++ api/server/controllers/UserController.js | 4 +- .../controllers/auth/LoginController.js | 8 +- .../auth/TwoFactorAuthController.js | 56 ++++ api/server/routes/auth.js | 14 + api/server/services/twoFactorService.js | 205 ++++++++++++ api/server/utils/crypto.js | 23 +- api/strategies/jwtStrategy.js | 2 +- client/package.json | 4 +- client/src/components/Auth/AuthLayout.tsx | 3 +- client/src/components/Auth/LoginForm.tsx | 4 +- .../src/components/Auth/TwoFactorScreen.tsx | 176 +++++++++++ client/src/components/Auth/index.ts | 1 + .../Chat/Input/Files/FileUpload.tsx | 6 +- .../Nav/SettingsTabs/Account/Account.tsx | 23 +- .../Nav/SettingsTabs/Account/Avatar.tsx | 10 +- .../SettingsTabs/Account/BackupCodesItem.tsx | 194 ++++++++++++ .../SettingsTabs/Account/DeleteAccount.tsx | 2 +- .../Account/DisableTwoFactorToggle.tsx | 36 +++ .../Account/TwoFactorAuthentication.tsx | 298 ++++++++++++++++++ .../Account/TwoFactorPhases/BackupPhase.tsx | 60 ++++ .../Account/TwoFactorPhases/DisablePhase.tsx | 88 ++++++ .../Account/TwoFactorPhases/QRPhase.tsx | 66 ++++ .../Account/TwoFactorPhases/SetupPhase.tsx | 42 +++ .../Account/TwoFactorPhases/VerifyPhase.tsx | 58 ++++ .../Account/TwoFactorPhases/index.ts | 5 + .../SettingsTabs/Data/ImportConversations.tsx | 4 +- .../Nav/SettingsTabs/Data/SharedLinks.tsx | 4 +- .../SettingsTabs/General/ArchivedChats.tsx | 2 +- client/src/components/ui/InputOTP.tsx | 68 ++++ client/src/components/ui/Progress.tsx | 22 ++ client/src/components/ui/index.ts | 2 + client/src/data-provider/Auth/mutations.ts | 90 +++++- client/src/hooks/AuthContext.tsx | 9 +- client/src/locales/ar/translation.json | 7 +- client/src/locales/de/translation.json | 7 +- client/src/locales/en/translation.json | 53 +++- client/src/locales/es/translation.json | 7 +- client/src/locales/et/translation.json | 3 +- client/src/locales/fi/translation.json | 7 +- client/src/locales/fr/translation.json | 7 +- client/src/locales/he/translation.json | 6 +- client/src/locales/id/translation.json | 7 +- client/src/locales/it/translation.json | 7 +- client/src/locales/ja/translation.json | 7 +- client/src/locales/ko/translation.json | 7 +- client/src/locales/nl/translation.json | 7 +- client/src/locales/pl/translation.json | 7 +- client/src/locales/ru/translation.json | 7 +- client/src/locales/sv/translation.json | 7 +- client/src/locales/tr/translation.json | 7 +- client/src/locales/vi/translation.json | 6 +- client/src/locales/zh-Hant/translation.json | 7 +- client/src/routes/Layouts/Startup.tsx | 1 + client/src/routes/index.tsx | 5 + package-lock.json | 107 +++++++ packages/data-provider/src/api-endpoints.ts | 8 + packages/data-provider/src/data-service.ts | 30 ++ packages/data-provider/src/keys.ts | 2 + packages/data-provider/src/request.ts | 3 + packages/data-provider/src/types.ts | 61 +++- 63 files changed, 1976 insertions(+), 129 deletions(-) create mode 100644 api/server/controllers/TwoFactorController.js create mode 100644 api/server/controllers/auth/TwoFactorAuthController.js create mode 100644 api/server/services/twoFactorService.js create mode 100644 client/src/components/Auth/TwoFactorScreen.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/DisableTwoFactorToggle.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/BackupPhase.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/DisablePhase.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/QRPhase.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/SetupPhase.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/VerifyPhase.tsx create mode 100644 client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/index.ts create mode 100644 client/src/components/ui/InputOTP.tsx create mode 100644 client/src/components/ui/Progress.tsx diff --git a/api/models/schema/userSchema.js b/api/models/schema/userSchema.js index f586553367..bebc7fea1e 100644 --- a/api/models/schema/userSchema.js +++ b/api/models/schema/userSchema.js @@ -39,6 +39,12 @@ const Session = mongoose.Schema({ }, }); +const backupCodeSchema = mongoose.Schema({ + codeHash: { type: String, required: true }, + used: { type: Boolean, default: false }, + usedAt: { type: Date, default: null }, +}); + /** @type {MongooseSchema} */ const userSchema = mongoose.Schema( { @@ -119,7 +125,12 @@ const userSchema = mongoose.Schema( }, plugins: { type: Array, - default: [], + }, + totpSecret: { + type: String, + }, + backupCodes: { + type: [backupCodeSchema], }, refreshToken: { type: [Session], diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index 71551ea867..7cdfaa9aaf 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -61,7 +61,7 @@ const refreshController = async (req, res) => { try { const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET); - const user = await getUserById(payload.id, '-password -__v'); + const user = await getUserById(payload.id, '-password -__v -totpSecret'); if (!user) { return res.status(401).redirect('/login'); } diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js new file mode 100644 index 0000000000..3e8d38ac12 --- /dev/null +++ b/api/server/controllers/TwoFactorController.js @@ -0,0 +1,111 @@ +const { + verifyTOTP, + verifyBackupCode, + generateTOTPSecret, + generateBackupCodes, +} = require('~/server/services/twoFactorService'); +const { updateUser, getUserById } = require('~/models'); +const { logger } = require('~/config'); + +const enable2FAController = async (req, res) => { + const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, ''); + + try { + const userId = req.user.id; + const secret = generateTOTPSecret(); + const { plainCodes, codeObjects } = await generateBackupCodes(); + + const user = await updateUser(userId, { totpSecret: secret, backupCodes: codeObjects }); + + const otpauthUrl = `otpauth://totp/${safeAppTitle}:${user.email}?secret=${secret}&issuer=${safeAppTitle}`; + + res.status(200).json({ + otpauthUrl, + backupCodes: plainCodes, + }); + } catch (err) { + logger.error('[enable2FAController]', err); + res.status(500).json({ message: err.message }); + } +}; + +const verify2FAController = async (req, res) => { + try { + const userId = req.user.id; + const { token, backupCode } = req.body; + const user = await getUserById(userId); + if (!user || !user.totpSecret) { + return res.status(400).json({ message: '2FA not initiated' }); + } + + let verified = false; + if (token && (await verifyTOTP(user.totpSecret, token))) { + return res.status(200).json(); + } else if (backupCode) { + verified = await verifyBackupCode({ user, backupCode }); + } + if (verified) { + return res.status(200).json(); + } + + return res.status(400).json({ message: 'Invalid token.' }); + } catch (err) { + logger.error('[verify2FAController]', err); + res.status(500).json({ message: err.message }); + } +}; + +const confirm2FAController = async (req, res) => { + try { + const userId = req.user.id; + const { token } = req.body; + const user = await getUserById(userId); + + if (!user || !user.totpSecret) { + return res.status(400).json({ message: '2FA not initiated' }); + } + + if (await verifyTOTP(user.totpSecret, token)) { + return res.status(200).json(); + } + + return res.status(400).json({ message: 'Invalid token.' }); + } catch (err) { + logger.error('[confirm2FAController]', err); + res.status(500).json({ message: err.message }); + } +}; + +const disable2FAController = async (req, res) => { + try { + const userId = req.user.id; + await updateUser(userId, { totpSecret: null, backupCodes: [] }); + res.status(200).json(); + } catch (err) { + logger.error('[disable2FAController]', err); + res.status(500).json({ message: err.message }); + } +}; + +const regenerateBackupCodesController = async (req, res) => { + try { + const userId = req.user.id; + const { plainCodes, codeObjects } = await generateBackupCodes(); + await updateUser(userId, { backupCodes: codeObjects }); + res.status(200).json({ + backupCodes: plainCodes, + backupCodesHash: codeObjects, + }); + } catch (err) { + logger.error('[regenerateBackupCodesController]', err); + res.status(500).json({ message: err.message }); + } +}; + +module.exports = { + enable2FAController, + verify2FAController, + confirm2FAController, + disable2FAController, + regenerateBackupCodesController, +}; diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 17089e8fdc..a331b8daae 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -19,7 +19,9 @@ const { Transaction } = require('~/models/Transaction'); const { logger } = require('~/config'); const getUserController = async (req, res) => { - res.status(200).send(req.user); + const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user }; + delete userData.totpSecret; + res.status(200).send(userData); }; const getTermsStatusController = async (req, res) => { diff --git a/api/server/controllers/auth/LoginController.js b/api/server/controllers/auth/LoginController.js index 1b543e9baf..8ab9a99ddb 100644 --- a/api/server/controllers/auth/LoginController.js +++ b/api/server/controllers/auth/LoginController.js @@ -1,3 +1,4 @@ +const { generate2FATempToken } = require('~/server/services/twoFactorService'); const { setAuthTokens } = require('~/server/services/AuthService'); const { logger } = require('~/config'); @@ -7,7 +8,12 @@ const loginController = async (req, res) => { return res.status(400).json({ message: 'Invalid credentials' }); } - const { password: _, __v, ...user } = req.user; + if (req.user.backupCodes != null && req.user.backupCodes.length > 0) { + const tempToken = generate2FATempToken(req.user._id); + return res.status(200).json({ twoFAPending: true, tempToken }); + } + + const { password: _p, totpSecret: _t, __v, ...user } = req.user; user.id = user._id.toString(); const token = await setAuthTokens(req.user._id, res); diff --git a/api/server/controllers/auth/TwoFactorAuthController.js b/api/server/controllers/auth/TwoFactorAuthController.js new file mode 100644 index 0000000000..37a8045829 --- /dev/null +++ b/api/server/controllers/auth/TwoFactorAuthController.js @@ -0,0 +1,56 @@ +const jwt = require('jsonwebtoken'); +const { verifyTOTP, verifyBackupCode } = require('~/server/services/twoFactorService'); +const { setAuthTokens } = require('~/server/services/AuthService'); +const { getUserById } = require('~/models/userMethods'); +const { logger } = require('~/config'); + +const verify2FA = async (req, res) => { + try { + const { tempToken, token, backupCode } = req.body; + if (!tempToken) { + return res.status(400).json({ message: 'Missing temporary token' }); + } + + let payload; + try { + payload = jwt.verify(tempToken, process.env.JWT_SECRET); + } catch (err) { + return res.status(401).json({ message: 'Invalid or expired temporary token' }); + } + + const user = await getUserById(payload.userId); + // Ensure that the user exists and has backup codes (i.e. 2FA enabled) + if (!user || !(user.backupCodes && user.backupCodes.length > 0)) { + return res.status(400).json({ message: '2FA is not enabled for this user' }); + } + + let verified = false; + + if (token && (await verifyTOTP(user.totpSecret, token))) { + verified = true; + } else if (backupCode) { + verified = await verifyBackupCode({ user, backupCode }); + } + + if (!verified) { + return res.status(401).json({ message: 'Invalid 2FA code or backup code' }); + } + + // Prepare user data for response. + // If the user is a plain object (from lean queries), we create a shallow copy. + const userData = user.toObject ? user.toObject() : { ...user }; + // Remove sensitive fields + delete userData.password; + delete userData.__v; + delete userData.totpSecret; + userData.id = user._id.toString(); + + const authToken = await setAuthTokens(user._id, res); + return res.status(200).json({ token: authToken, user: userData }); + } catch (err) { + logger.error('[verify2FA]', err); + return res.status(500).json({ message: 'Something went wrong' }); + } +}; + +module.exports = { verify2FA }; diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js index 3e86ffd868..03046d903f 100644 --- a/api/server/routes/auth.js +++ b/api/server/routes/auth.js @@ -7,6 +7,13 @@ const { } = require('~/server/controllers/AuthController'); const { loginController } = require('~/server/controllers/auth/LoginController'); const { logoutController } = require('~/server/controllers/auth/LogoutController'); +const { verify2FA } = require('~/server/controllers/auth/TwoFactorAuthController'); +const { + enable2FAController, + verify2FAController, + disable2FAController, + regenerateBackupCodesController, confirm2FAController, +} = require('~/server/controllers/TwoFactorController'); const { checkBan, loginLimiter, @@ -50,4 +57,11 @@ router.post( ); router.post('/resetPassword', checkBan, validatePasswordReset, resetPasswordController); +router.get('/2fa/enable', requireJwtAuth, enable2FAController); +router.post('/2fa/verify', requireJwtAuth, verify2FAController); +router.post('/2fa/verify-temp', checkBan, verify2FA); +router.post('/2fa/confirm', requireJwtAuth, confirm2FAController); +router.post('/2fa/disable', requireJwtAuth, disable2FAController); +router.post('/2fa/backup/regenerate', requireJwtAuth, regenerateBackupCodesController); + module.exports = router; diff --git a/api/server/services/twoFactorService.js b/api/server/services/twoFactorService.js new file mode 100644 index 0000000000..ac7247409c --- /dev/null +++ b/api/server/services/twoFactorService.js @@ -0,0 +1,205 @@ +const { sign } = require('jsonwebtoken'); +const { webcrypto } = require('node:crypto'); +const { hashBackupCode } = require('~/server/utils/crypto'); +const { updateUser } = require('~/models/userMethods'); + +const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + +/** + * Encodes a Buffer into a Base32 string using RFC 4648 alphabet. + * @param {Buffer} buffer - The buffer to encode. + * @returns {string} - The Base32 encoded string. + */ +const encodeBase32 = (buffer) => { + let bits = 0; + let value = 0; + let output = ''; + for (const byte of buffer) { + value = (value << 8) | byte; + bits += 8; + while (bits >= 5) { + output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31]; + bits -= 5; + } + } + if (bits > 0) { + output += BASE32_ALPHABET[(value << (5 - bits)) & 31]; + } + return output; +}; + +/** + * Decodes a Base32-encoded string back into a Buffer. + * @param {string} base32Str + * @returns {Buffer} + */ +const decodeBase32 = (base32Str) => { + const cleaned = base32Str.replace(/=+$/, '').toUpperCase(); + let bits = 0; + let value = 0; + const output = []; + for (const char of cleaned) { + const idx = BASE32_ALPHABET.indexOf(char); + if (idx === -1) { + continue; + } + value = (value << 5) | idx; + bits += 5; + if (bits >= 8) { + output.push((value >>> (bits - 8)) & 0xff); + bits -= 8; + } + } + return Buffer.from(output); +}; + +/** + * Generate a temporary token for 2FA verification. + * This token is signed with JWT_SECRET and expires in 5 minutes. + */ +const generate2FATempToken = (userId) => + sign({ userId, twoFAPending: true }, process.env.JWT_SECRET, { expiresIn: '5m' }); + +/** + * Generate a TOTP secret. + * Generates 10 random bytes using WebCrypto and encodes them into a Base32 string. + */ +const generateTOTPSecret = () => { + const randomArray = new Uint8Array(10); + webcrypto.getRandomValues(randomArray); + return encodeBase32(Buffer.from(randomArray)); +}; + +/** + * Generate a TOTP code based on the secret and current time. + * Uses a 30-second time step and generates a 6-digit code. + * + * @param {string} secret - Base32-encoded secret + * @param {number} [forTime=Date.now()] - Time in milliseconds + * @returns {Promise} - The 6-digit TOTP code. + */ +const generateTOTP = async (secret, forTime = Date.now()) => { + const timeStep = 30; // seconds + const counter = Math.floor(forTime / 1000 / timeStep); + const counterBuffer = new ArrayBuffer(8); + const counterView = new DataView(counterBuffer); + // Write counter into the last 4 bytes (big-endian) + counterView.setUint32(4, counter, false); + + // Decode the secret into an ArrayBuffer + const keyBuffer = decodeBase32(secret); + const keyArrayBuffer = keyBuffer.buffer.slice( + keyBuffer.byteOffset, + keyBuffer.byteOffset + keyBuffer.byteLength, + ); + + // Import the key for HMAC-SHA1 signing + const cryptoKey = await webcrypto.subtle.importKey( + 'raw', + keyArrayBuffer, + { name: 'HMAC', hash: 'SHA-1' }, + false, + ['sign'], + ); + + // Generate HMAC signature + const signatureBuffer = await webcrypto.subtle.sign('HMAC', cryptoKey, counterBuffer); + const hmac = new Uint8Array(signatureBuffer); + + const offset = hmac[hmac.length - 1] & 0xf; + const slice = hmac.slice(offset, offset + 4); + const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength); + const binaryCode = view.getUint32(0, false) & 0x7fffffff; + const code = (binaryCode % 1000000).toString().padStart(6, '0'); + return code; +}; + +/** + * Verify a provided TOTP token against the secret. + * Allows for a ยฑ1 time-step window. + * + * @param {string} secret + * @param {string} token + * @returns {Promise} + */ +const verifyTOTP = async (secret, token) => { + const timeStepMS = 30 * 1000; + const currentTime = Date.now(); + for (let offset = -1; offset <= 1; offset++) { + const expected = await generateTOTP(secret, currentTime + offset * timeStepMS); + if (expected === token) { + return true; + } + } + return false; +}; + +/** + * Generate backup codes. + * Generates `count` backup code objects and returns an object with both plain codes + * (for one-time download) and their objects (for secure storage). Uses WebCrypto for randomness and hashing. + * + * @param {number} count - Number of backup codes to generate (default: 10). + * @returns {Promise} - Contains `plainCodes` (array of strings) and `codeObjects` (array of objects). + */ +const generateBackupCodes = async (count = 10) => { + const plainCodes = []; + const codeObjects = []; + const encoder = new TextEncoder(); + for (let i = 0; i < count; i++) { + const randomArray = new Uint8Array(4); + webcrypto.getRandomValues(randomArray); + const code = Array.from(randomArray) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); // 8-character hex code + plainCodes.push(code); + + // Compute SHA-256 hash of the code using WebCrypto + const codeBuffer = encoder.encode(code); + const hashBuffer = await webcrypto.subtle.digest('SHA-256', codeBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const codeHash = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + codeObjects.push({ codeHash, used: false, usedAt: null }); + } + return { plainCodes, codeObjects }; +}; + +/** + * Verifies a backup code and updates the user's backup codes if valid + * @param {Object} params + * @param {TUser | undefined} [params.user] - The user object + * @param {string | undefined} [params.backupCode] - The backup code to verify + * @returns {Promise} - Whether the backup code was valid + */ +const verifyBackupCode = async ({ user, backupCode }) => { + if (!backupCode || !user || !Array.isArray(user.backupCodes)) { + return false; + } + + const hashedInput = await hashBackupCode(backupCode.trim()); + const matchingCode = user.backupCodes.find( + (codeObj) => codeObj.codeHash === hashedInput && !codeObj.used, + ); + + if (matchingCode) { + const updatedBackupCodes = user.backupCodes.map((codeObj) => + codeObj.codeHash === hashedInput && !codeObj.used + ? { ...codeObj, used: true, usedAt: new Date() } + : codeObj, + ); + + await updateUser(user._id, { backupCodes: updatedBackupCodes }); + return true; + } + + return false; +}; + +module.exports = { + verifyTOTP, + generateTOTP, + verifyBackupCode, + generateTOTPSecret, + generateBackupCodes, + generate2FATempToken, +}; diff --git a/api/server/utils/crypto.js b/api/server/utils/crypto.js index ea71df51ad..407fad62ac 100644 --- a/api/server/utils/crypto.js +++ b/api/server/utils/crypto.js @@ -112,4 +112,25 @@ async function getRandomValues(length) { return Buffer.from(randomValues).toString('hex'); } -module.exports = { encrypt, decrypt, encryptV2, decryptV2, hashToken, getRandomValues }; +/** + * Computes SHA-256 hash for the given input using WebCrypto + * @param {string} input + * @returns {Promise} - Hex hash string + */ +const hashBackupCode = async (input) => { + const encoder = new TextEncoder(); + const data = encoder.encode(input); + const hashBuffer = await webcrypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); +}; + +module.exports = { + encrypt, + decrypt, + encryptV2, + decryptV2, + hashToken, + hashBackupCode, + getRandomValues, +}; diff --git a/api/strategies/jwtStrategy.js b/api/strategies/jwtStrategy.js index e65b284950..ac19e92ac3 100644 --- a/api/strategies/jwtStrategy.js +++ b/api/strategies/jwtStrategy.js @@ -12,7 +12,7 @@ const jwtLogin = async () => }, async (payload, done) => { try { - const user = await getUserById(payload?.id, '-password -__v'); + const user = await getUserById(payload?.id, '-password -__v -totpSecret'); if (user) { user.id = user._id.toString(); if (!user.role) { diff --git a/client/package.json b/client/package.json index 22e9b1dd03..917333ce25 100644 --- a/client/package.json +++ b/client/package.json @@ -44,6 +44,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.0", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", @@ -66,6 +67,7 @@ "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.3", "js-cookie": "^3.0.5", + "input-otp": "^1.4.2", "librechat-data-provider": "*", "lodash": "^4.17.21", "lucide-react": "^0.394.0", @@ -142,4 +144,4 @@ "vite-plugin-node-polyfills": "^0.17.0", "vite-plugin-pwa": "^0.21.1" } -} +} \ No newline at end of file diff --git a/client/src/components/Auth/AuthLayout.tsx b/client/src/components/Auth/AuthLayout.tsx index a7e890517a..d90f0d3dfe 100644 --- a/client/src/components/Auth/AuthLayout.tsx +++ b/client/src/components/Auth/AuthLayout.tsx @@ -85,7 +85,8 @@ function AuthLayout({ )} {children} - {(pathname.includes('login') || pathname.includes('register')) && ( + {!pathname.includes('2fa') && + (pathname.includes('login') || pathname.includes('register')) && ( )} diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index 9f5bb46039..2cd62d08b9 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -166,9 +166,7 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, type="submit" className=" w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white - transition-colors hover:bg-green-700 focus:outline-none focus:ring-2 - focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50 - disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700 + transition-colors hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 " > {localize('com_auth_continue')} diff --git a/client/src/components/Auth/TwoFactorScreen.tsx b/client/src/components/Auth/TwoFactorScreen.tsx new file mode 100644 index 0000000000..04f89d7cea --- /dev/null +++ b/client/src/components/Auth/TwoFactorScreen.tsx @@ -0,0 +1,176 @@ +import React, { useState, useCallback } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useForm, Controller } from 'react-hook-form'; +import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'; +import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Label } from '~/components'; +import { useVerifyTwoFactorTempMutation } from '~/data-provider'; +import { useToastContext } from '~/Providers'; +import { useLocalize } from '~/hooks'; + +interface VerifyPayload { + tempToken: string; + token?: string; + backupCode?: string; +} + +type TwoFactorFormInputs = { + token?: string; + backupCode?: string; +}; + +const TwoFactorScreen: React.FC = React.memo(() => { + const [searchParams] = useSearchParams(); + const tempTokenRaw = searchParams.get('tempToken'); + const tempToken = tempTokenRaw !== null && tempTokenRaw !== '' ? tempTokenRaw : ''; + + const { + control, + handleSubmit, + formState: { errors }, + } = useForm(); + const localize = useLocalize(); + const { showToast } = useToastContext(); + const [useBackup, setUseBackup] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const { mutate: verifyTempMutate } = useVerifyTwoFactorTempMutation({ + onSuccess: (result) => { + if (result.token != null && result.token !== '') { + window.location.href = '/'; + } + }, + onMutate: () => { + setIsLoading(true); + }, + onError: (error: unknown) => { + setIsLoading(false); + const err = error as { response?: { data?: { message?: unknown } } }; + const errorMsg = + typeof err.response?.data?.message === 'string' + ? err.response.data.message + : 'Error verifying 2FA'; + showToast({ message: errorMsg, status: 'error' }); + }, + }); + + const onSubmit = useCallback( + (data: TwoFactorFormInputs) => { + const payload: VerifyPayload = { tempToken }; + if (useBackup && data.backupCode != null && data.backupCode !== '') { + payload.backupCode = data.backupCode; + } else if (data.token != null && data.token !== '') { + payload.token = data.token; + } + verifyTempMutate(payload); + }, + [tempToken, useBackup, verifyTempMutate], + ); + + const toggleBackupOn = useCallback(() => { + setUseBackup(true); + }, []); + + const toggleBackupOff = useCallback(() => { + setUseBackup(false); + }, []); + + return ( +
+
+ + {!useBackup && ( +
+ ( + + + + + + + + + + + + + + )} + /> + {errors.token && {errors.token.message}} +
+ )} + {useBackup && ( +
+ ( + + + + + + + + + + + + + )} + /> + {errors.backupCode && ( + {errors.backupCode.message} + )} +
+ )} +
+ +
+
+ {!useBackup ? ( + + ) : ( + + )} +
+
+
+ ); +}); + +export default TwoFactorScreen; diff --git a/client/src/components/Auth/index.ts b/client/src/components/Auth/index.ts index cd1ac1adce..afde148015 100644 --- a/client/src/components/Auth/index.ts +++ b/client/src/components/Auth/index.ts @@ -4,3 +4,4 @@ export { default as ResetPassword } from './ResetPassword'; export { default as VerifyEmail } from './VerifyEmail'; export { default as ApiErrorWatcher } from './ApiErrorWatcher'; export { default as RequestPasswordReset } from './RequestPasswordReset'; +export { default as TwoFactorScreen } from './TwoFactorScreen'; diff --git a/client/src/components/Chat/Input/Files/FileUpload.tsx b/client/src/components/Chat/Input/Files/FileUpload.tsx index 506f50c01d..723fa32e86 100644 --- a/client/src/components/Chat/Input/Files/FileUpload.tsx +++ b/client/src/components/Chat/Input/Files/FileUpload.tsx @@ -55,7 +55,7 @@ const FileUpload: React.FC = ({ let statusText: string; if (!status) { - statusText = text ?? localize('com_endpoint_import'); + statusText = text ?? localize('com_ui_import'); } else if (status === 'success') { statusText = successText ?? localize('com_ui_upload_success'); } else { @@ -72,12 +72,12 @@ const FileUpload: React.FC = ({ )} > - {statusText} + {statusText} diff --git a/client/src/components/Nav/SettingsTabs/Account/Account.tsx b/client/src/components/Nav/SettingsTabs/Account/Account.tsx index 374a6b996e..68168f7f72 100644 --- a/client/src/components/Nav/SettingsTabs/Account/Account.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/Account.tsx @@ -2,19 +2,36 @@ import React from 'react'; import DisplayUsernameMessages from './DisplayUsernameMessages'; import DeleteAccount from './DeleteAccount'; import Avatar from './Avatar'; +import EnableTwoFactorItem from './TwoFactorAuthentication'; +import BackupCodesItem from './BackupCodesItem'; +import { useAuthContext } from '~/hooks'; function Account() { + const user = useAuthContext(); + return (
+
+ +
+ {user?.user?.provider === 'local' && ( + <> +
+ +
+ {Array.isArray(user.user?.backupCodes) && user.user?.backupCodes.length > 0 && ( +
+ +
+ )} + + )}
-
- -
); } diff --git a/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx index 48f585bdbb..5ecdb5a990 100644 --- a/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx @@ -47,7 +47,7 @@ function Avatar() { const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({ onSuccess: (data) => { showToast({ message: localize('com_ui_upload_success') }); - setUser((prev) => ({ ...prev, avatar: data.url } as TUser)); + setUser((prev) => ({ ...prev, avatar: data.url }) as TUser); openButtonRef.current?.click(); }, onError: (error) => { @@ -133,9 +133,11 @@ function Avatar() { >
{localize('com_nav_profile_picture')} - - - {localize('com_nav_change_picture')} + +
diff --git a/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx b/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx new file mode 100644 index 0000000000..a034e2773a --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/BackupCodesItem.tsx @@ -0,0 +1,194 @@ +import React, { useState } from 'react'; +import { RefreshCcw, ShieldX } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { TBackupCode, TRegenerateBackupCodesResponse, type TUser } from 'librechat-data-provider'; +import { + OGDialog, + OGDialogContent, + OGDialogTitle, + OGDialogTrigger, + Button, + Label, + Spinner, + TooltipAnchor, +} from '~/components'; +import { useRegenerateBackupCodesMutation } from '~/data-provider'; +import { useAuthContext, useLocalize } from '~/hooks'; +import { useToastContext } from '~/Providers'; +import { useSetRecoilState } from 'recoil'; +import store from '~/store'; + +const BackupCodesItem: React.FC = () => { + const localize = useLocalize(); + const { user } = useAuthContext(); + const { showToast } = useToastContext(); + const setUser = useSetRecoilState(store.user); + const [isDialogOpen, setDialogOpen] = useState(false); + + const { mutate: regenerateBackupCodes, isLoading } = useRegenerateBackupCodesMutation(); + + const fetchBackupCodes = (auto: boolean = false) => { + regenerateBackupCodes(undefined, { + onSuccess: (data: TRegenerateBackupCodesResponse) => { + const newBackupCodes: TBackupCode[] = data.backupCodesHash.map((codeHash) => ({ + codeHash, + used: false, + usedAt: null, + })); + + setUser((prev) => ({ ...prev, backupCodes: newBackupCodes }) as TUser); + showToast({ + message: localize('com_ui_backup_codes_regenerated'), + status: 'success', + }); + + // Trigger file download only when user explicitly clicks the button. + if (!auto && newBackupCodes.length) { + const codesString = data.backupCodes.join('\n'); + const blob = new Blob([codesString], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'backup-codes.txt'; + a.click(); + URL.revokeObjectURL(url); + } + }, + onError: () => + showToast({ + message: localize('com_ui_backup_codes_regenerate_error'), + status: 'error', + }), + }); + }; + + const handleRegenerate = () => { + fetchBackupCodes(false); + }; + + return ( + +
+
+ +
+ + + +
+ + + + {localize('com_ui_backup_codes')} + + + + + {Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? ( + <> +
+ {user?.backupCodes.map((code, index) => { + const isUsed = code.used; + const description = `Backup code number ${index + 1}, ${ + isUsed + ? `used on ${code.usedAt ? new Date(code.usedAt).toLocaleDateString() : 'an unknown date'}` + : 'not used yet' + }`; + + return ( + { + const announcement = new CustomEvent('announce', { + detail: { message: description }, + }); + document.dispatchEvent(announcement); + }} + className={`flex flex-col rounded-xl border p-4 backdrop-blur-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${ + isUsed + ? 'border-red-200 bg-red-50/80 dark:border-red-800 dark:bg-red-900/20' + : 'border-green-200 bg-green-50/80 dark:border-green-800 dark:bg-green-900/20' + } `} + > + + + ); + })} +
+
+ +
+ + ) : ( +
+ +

{localize('com_ui_no_backup_codes')}

+ +
+ )} +
+
+
+
+ ); +}; + +export default React.memo(BackupCodesItem); diff --git a/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx b/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx index 1c1e207d58..b00e7498bc 100644 --- a/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/DeleteAccount.tsx @@ -57,7 +57,7 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea - + {localize('com_nav_delete_account_confirm')} diff --git a/client/src/components/Nav/SettingsTabs/Account/DisableTwoFactorToggle.tsx b/client/src/components/Nav/SettingsTabs/Account/DisableTwoFactorToggle.tsx new file mode 100644 index 0000000000..5dfad770d3 --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/DisableTwoFactorToggle.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { LockIcon, UnlockIcon } from 'lucide-react'; +import { Label, Button } from '~/components'; +import { useLocalize } from '~/hooks'; + +interface DisableTwoFactorToggleProps { + enabled: boolean; + onChange: () => void; + disabled?: boolean; +} + +export const DisableTwoFactorToggle: React.FC = ({ + enabled, + onChange, + disabled, +}) => { + const localize = useLocalize(); + + return ( +
+
+ +
+
+ +
+
+ ); +}; diff --git a/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx b/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx new file mode 100644 index 0000000000..bd46e80249 --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx @@ -0,0 +1,298 @@ +import React, { useCallback, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { SmartphoneIcon } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import type { TUser, TVerify2FARequest } from 'librechat-data-provider'; +import { OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle, Progress } from '~/components'; +import { SetupPhase, QRPhase, VerifyPhase, BackupPhase, DisablePhase } from './TwoFactorPhases'; +import { DisableTwoFactorToggle } from './DisableTwoFactorToggle'; +import { useAuthContext, useLocalize } from '~/hooks'; +import { useToastContext } from '~/Providers'; +import store from '~/store'; +import { + useConfirmTwoFactorMutation, + useDisableTwoFactorMutation, + useEnableTwoFactorMutation, + useVerifyTwoFactorMutation, +} from '~/data-provider'; + +export type Phase = 'setup' | 'qr' | 'verify' | 'backup' | 'disable'; + +const phaseVariants = { + initial: { opacity: 0, scale: 0.95 }, + animate: { opacity: 1, scale: 1, transition: { duration: 0.3, ease: 'easeOut' } }, + exit: { opacity: 0, scale: 0.95, transition: { duration: 0.3, ease: 'easeIn' } }, +}; + +const TwoFactorAuthentication: React.FC = () => { + const localize = useLocalize(); + const { user } = useAuthContext(); + const setUser = useSetRecoilState(store.user); + const { showToast } = useToastContext(); + + const [secret, setSecret] = useState(''); + const [otpauthUrl, setOtpauthUrl] = useState(''); + const [downloaded, setDownloaded] = useState(false); + const [disableToken, setDisableToken] = useState(''); + const [backupCodes, setBackupCodes] = useState([]); + const [isDialogOpen, setDialogOpen] = useState(false); + const [verificationToken, setVerificationToken] = useState(''); + const [phase, setPhase] = useState(Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? 'disable' : 'setup'); + + const { mutate: confirm2FAMutate } = useConfirmTwoFactorMutation(); + const { mutate: enable2FAMutate, isLoading: isGenerating } = useEnableTwoFactorMutation(); + const { mutate: verify2FAMutate, isLoading: isVerifying } = useVerifyTwoFactorMutation(); + const { mutate: disable2FAMutate, isLoading: isDisabling } = useDisableTwoFactorMutation(); + + const steps = ['Setup', 'Scan QR', 'Verify', 'Backup']; + const phasesLabel: Record = { + setup: 'Setup', + qr: 'Scan QR', + verify: 'Verify', + backup: 'Backup', + disable: '', + }; + + const currentStep = steps.indexOf(phasesLabel[phase]); + + const resetState = useCallback(() => { + if (Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 && otpauthUrl) { + disable2FAMutate(undefined, { + onError: () => + showToast({ message: localize('com_ui_2fa_disable_error'), status: 'error' }), + }); + } + + setOtpauthUrl(''); + setSecret(''); + setBackupCodes([]); + setVerificationToken(''); + setDisableToken(''); + setPhase(Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? 'disable' : 'setup'); + setDownloaded(false); + }, [user, otpauthUrl, disable2FAMutate, localize, showToast]); + + const handleGenerateQRCode = useCallback(() => { + enable2FAMutate(undefined, { + onSuccess: ({ otpauthUrl, backupCodes }) => { + setOtpauthUrl(otpauthUrl); + setSecret(otpauthUrl.split('secret=')[1].split('&')[0]); + setBackupCodes(backupCodes); + setPhase('qr'); + }, + onError: () => showToast({ message: localize('com_ui_2fa_generate_error'), status: 'error' }), + }); + }, [enable2FAMutate, localize, showToast]); + + const handleVerify = useCallback(() => { + if (!verificationToken) { + return; + } + + verify2FAMutate( + { token: verificationToken }, + { + onSuccess: () => { + showToast({ message: localize('com_ui_2fa_verified') }); + confirm2FAMutate( + { token: verificationToken }, + { + onSuccess: () => setPhase('backup'), + onError: () => + showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }), + }, + ); + }, + onError: () => showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }), + }, + ); + }, [verificationToken, verify2FAMutate, confirm2FAMutate, localize, showToast]); + + const handleDownload = useCallback(() => { + if (!backupCodes.length) { + return; + } + const blob = new Blob([backupCodes.join('\n')], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'backup-codes.txt'; + a.click(); + URL.revokeObjectURL(url); + setDownloaded(true); + }, [backupCodes]); + + const handleConfirm = useCallback(() => { + setDialogOpen(false); + setPhase('disable'); + showToast({ message: localize('com_ui_2fa_enabled') }); + setUser( + (prev) => + ({ + ...prev, + backupCodes: backupCodes.map((code) => ({ + code, + codeHash: code, + used: false, + usedAt: null, + })), + }) as TUser, + ); + }, [setUser, localize, showToast, backupCodes]); + + const handleDisableVerify = useCallback( + (token: string, useBackup: boolean) => { + // Validate: if not using backup, ensure token has at least 6 digits; + // if using backup, ensure backup code has at least 8 characters. + if (!useBackup && token.trim().length < 6) { + return; + } + + if (useBackup && token.trim().length < 8) { + return; + } + + const payload: TVerify2FARequest = {}; + if (useBackup) { + payload.backupCode = token.trim(); + } else { + payload.token = token.trim(); + } + + verify2FAMutate(payload, { + onSuccess: () => { + disable2FAMutate(undefined, { + onSuccess: () => { + showToast({ message: localize('com_ui_2fa_disabled') }); + setDialogOpen(false); + setUser( + (prev) => + ({ + ...prev, + totpSecret: '', + backupCodes: [], + }) as TUser, + ); + setPhase('setup'); + setOtpauthUrl(''); + }, + onError: () => + showToast({ message: localize('com_ui_2fa_disable_error'), status: 'error' }), + }); + }, + onError: () => showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }), + }); + }, + [disableToken, verify2FAMutate, disable2FAMutate, showToast, localize, setUser], + ); + + return ( + { + setDialogOpen(open); + if (!open) { + resetState(); + } + }} + > + 0} + onChange={() => setDialogOpen(true)} + disabled={isVerifying || isDisabling || isGenerating} + /> + + + + + + + + {Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? localize('com_ui_2fa_disable') : localize('com_ui_2fa_setup')} + + {Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 && phase !== 'disable' && ( +
+ +
+ {steps.map((step, index) => ( + = index ? 'var(--text-primary)' : 'var(--text-tertiary)', + }} + className="font-medium" + > + {step} + + ))} +
+
+ )} +
+ + + {phase === 'setup' && ( + setPhase('qr')} + onError={(error) => showToast({ message: error.message, status: 'error' })} + /> + )} + + {phase === 'qr' && ( + setPhase('verify')} + onError={(error) => showToast({ message: error.message, status: 'error' })} + /> + )} + + {phase === 'verify' && ( + showToast({ message: error.message, status: 'error' })} + /> + )} + + {phase === 'backup' && ( + showToast({ message: error.message, status: 'error' })} + /> + )} + + {phase === 'disable' && ( + showToast({ message: error.message, status: 'error' })} + /> + )} + +
+
+
+
+ ); +}; + +export default React.memo(TwoFactorAuthentication); diff --git a/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/BackupPhase.tsx b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/BackupPhase.tsx new file mode 100644 index 0000000000..67e05a1423 --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/BackupPhase.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Download } from 'lucide-react'; +import { Button, Label } from '~/components'; +import { useLocalize } from '~/hooks'; + +const fadeAnimation = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 }, + transition: { duration: 0.2 }, +}; + +interface BackupPhaseProps { + onNext: () => void; + onError: (error: Error) => void; + backupCodes: string[]; + onDownload: () => void; + downloaded: boolean; +} + +export const BackupPhase: React.FC = ({ + backupCodes, + onDownload, + downloaded, + onNext, +}) => { + const localize = useLocalize(); + + return ( + + +
+ {backupCodes.map((code, index) => ( + +
+ #{index + 1} + {code} +
+
+ ))} +
+
+ + +
+
+ ); +}; diff --git a/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/DisablePhase.tsx b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/DisablePhase.tsx new file mode 100644 index 0000000000..27422d26c3 --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/DisablePhase.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'; +import { + Button, + InputOTP, + InputOTPGroup, + InputOTPSlot, + InputOTPSeparator, + Spinner, +} from '~/components'; +import { useLocalize } from '~/hooks'; + +const fadeAnimation = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 }, + transition: { duration: 0.2 }, +}; + +interface DisablePhaseProps { + onSuccess?: () => void; + onError?: (error: Error) => void; + onDisable: (token: string, useBackup: boolean) => void; + isDisabling: boolean; +} + +export const DisablePhase: React.FC = ({ onDisable, isDisabling }) => { + const localize = useLocalize(); + const [token, setToken] = useState(''); + const [useBackup, setUseBackup] = useState(false); + + return ( + +
+ + {useBackup ? ( + + + + + + + + + + + ) : ( + <> + + + + + + + + + + + + + )} + +
+ + +
+ ); +}; diff --git a/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/QRPhase.tsx b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/QRPhase.tsx new file mode 100644 index 0000000000..7a0eccae3f --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/QRPhase.tsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { motion } from 'framer-motion'; +import { QRCodeSVG } from 'qrcode.react'; +import { Copy, Check } from 'lucide-react'; +import { Input, Button, Label } from '~/components'; +import { useLocalize } from '~/hooks'; +import { cn } from '~/utils'; + +const fadeAnimation = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 }, + transition: { duration: 0.2 }, +}; + +interface QRPhaseProps { + secret: string; + otpauthUrl: string; + onNext: () => void; + onSuccess?: () => void; + onError?: (error: Error) => void; +} + +export const QRPhase: React.FC = ({ secret, otpauthUrl, onNext }) => { + const localize = useLocalize(); + const [isCopying, setIsCopying] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(secret); + setIsCopying(true); + setTimeout(() => setIsCopying(false), 2000); + }; + + return ( + +
+ + + +
+ +
+ + +
+
+
+ +
+ ); +}; diff --git a/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/SetupPhase.tsx b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/SetupPhase.tsx new file mode 100644 index 0000000000..4fd2d1181d --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/SetupPhase.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { QrCode } from 'lucide-react'; +import { motion } from 'framer-motion'; +import { Button, Spinner } from '~/components'; +import { useLocalize } from '~/hooks'; + +const fadeAnimation = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 }, + transition: { duration: 0.2 }, +}; + +interface SetupPhaseProps { + onNext: () => void; + onError: (error: Error) => void; + isGenerating: boolean; + onGenerate: () => void; +} + +export const SetupPhase: React.FC = ({ isGenerating, onGenerate, onNext }) => { + const localize = useLocalize(); + + return ( + +
+

+ {localize('com_ui_2fa_account_security')} +

+ +
+
+ ); +}; diff --git a/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/VerifyPhase.tsx b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/VerifyPhase.tsx new file mode 100644 index 0000000000..e872dfa0d2 --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/VerifyPhase.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { Button, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from '~/components'; +import { REGEXP_ONLY_DIGITS } from 'input-otp'; +import { useLocalize } from '~/hooks'; + +const fadeAnimation = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 }, + transition: { duration: 0.2 }, +}; + +interface VerifyPhaseProps { + token: string; + onTokenChange: (value: string) => void; + isVerifying: boolean; + onNext: () => void; + onError: (error: Error) => void; +} + +export const VerifyPhase: React.FC = ({ + token, + onTokenChange, + isVerifying, + onNext, +}) => { + const localize = useLocalize(); + + return ( + +
+ + + {Array.from({ length: 3 }).map((_, i) => ( + + ))} + + + + {Array.from({ length: 3 }).map((_, i) => ( + + ))} + + +
+ +
+ ); +}; diff --git a/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/index.ts b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/index.ts new file mode 100644 index 0000000000..1cc474efef --- /dev/null +++ b/client/src/components/Nav/SettingsTabs/Account/TwoFactorPhases/index.ts @@ -0,0 +1,5 @@ +export * from './BackupPhase'; +export * from './QRPhase'; +export * from './VerifyPhase'; +export * from './SetupPhase'; +export * from './DisablePhase'; diff --git a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx index c39e8351e8..e3bafd9152 100644 --- a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx @@ -82,7 +82,7 @@ function ImportConversations() { onClick={handleImportClick} onKeyDown={handleKeyDown} disabled={!allowImport} - aria-label={localize('com_ui_import_conversation')} + aria-label={localize('com_ui_import')} className="btn btn-neutral relative" > {allowImport ? ( @@ -90,7 +90,7 @@ function ImportConversations() { ) : ( )} - {localize('com_ui_import_conversation')} + {localize('com_ui_import')} setIsOpen(true)}> - + , + React.ComponentPropsWithoutRef +>(({ className, containerClassName, ...props }, ref) => ( + +)); +InputOTP.displayName = 'InputOTP'; + +const InputOTPGroup = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> +>(({ className, ...props }, ref) => ( +
+)); +InputOTPGroup.displayName = 'InputOTPGroup'; + +const InputOTPSlot = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> & { index: number } +>(({ index, className, ...props }, ref) => { + const inputOTPContext = React.useContext(OTPInputContext); + const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]; + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ); +}); +InputOTPSlot.displayName = 'InputOTPSlot'; + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> +>(({ ...props }, ref) => ( +
+ +
+)); +InputOTPSeparator.displayName = 'InputOTPSeparator'; + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }; diff --git a/client/src/components/ui/Progress.tsx b/client/src/components/ui/Progress.tsx new file mode 100644 index 0000000000..e8e0b0f6b2 --- /dev/null +++ b/client/src/components/ui/Progress.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import * as ProgressPrimitive from '@radix-ui/react-progress'; +import { cn } from '~/utils'; + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; diff --git a/client/src/components/ui/index.ts b/client/src/components/ui/index.ts index d81a8cad5d..b0025d2536 100644 --- a/client/src/components/ui/index.ts +++ b/client/src/components/ui/index.ts @@ -24,6 +24,8 @@ export * from './Textarea'; export * from './TextareaAutosize'; export * from './Tooltip'; export * from './Pagination'; +export * from './Progress'; +export * from './InputOTP'; export { default as Combobox } from './Combobox'; export { default as Dropdown } from './Dropdown'; export { default as FileUpload } from './FileUpload'; diff --git a/client/src/data-provider/Auth/mutations.ts b/client/src/data-provider/Auth/mutations.ts index 49d4fbe1d0..eb09868ec6 100644 --- a/client/src/data-provider/Auth/mutations.ts +++ b/client/src/data-provider/Auth/mutations.ts @@ -1,6 +1,6 @@ import { useResetRecoilState, useSetRecoilState } from 'recoil'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { MutationKeys, dataService, request } from 'librechat-data-provider'; +import { MutationKeys, QueryKeys, dataService, request } from 'librechat-data-provider'; import type { UseMutationResult } from '@tanstack/react-query'; import type * as t from 'librechat-data-provider'; import useClearStates from '~/hooks/Config/useClearStates'; @@ -84,3 +84,91 @@ export const useDeleteUserMutation = ( }, }); }; + +// Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 + +export const useEnableTwoFactorMutation = (): UseMutationResult< + t.TEnable2FAResponse, + unknown, + void, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation(() => dataService.enableTwoFactor(), { + onSuccess: (data) => { + queryClient.setQueryData([QueryKeys.user, '2fa'], data); + }, + }); +}; + +export const useVerifyTwoFactorMutation = (): UseMutationResult< + t.TVerify2FAResponse, + unknown, + t.TVerify2FARequest, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation((payload: t.TVerify2FARequest) => dataService.verifyTwoFactor(payload), { + onSuccess: (data) => { + queryClient.setQueryData([QueryKeys.user, '2fa'], data); + }, + }); +}; + +export const useConfirmTwoFactorMutation = (): UseMutationResult< + t.TVerify2FAResponse, + unknown, + t.TVerify2FARequest, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation((payload: t.TVerify2FARequest) => dataService.confirmTwoFactor(payload), { + onSuccess: (data) => { + queryClient.setQueryData([QueryKeys.user, '2fa'], data); + }, + }); +}; + +export const useDisableTwoFactorMutation = (): UseMutationResult< + t.TDisable2FAResponse, + unknown, + void, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation(() => dataService.disableTwoFactor(), { + onSuccess: (data) => { + queryClient.setQueryData([QueryKeys.user, '2fa'], null); + }, + }); +}; + +export const useRegenerateBackupCodesMutation = (): UseMutationResult< + t.TRegenerateBackupCodesResponse, + unknown, + void, + unknown +> => { + const queryClient = useQueryClient(); + return useMutation(() => dataService.regenerateBackupCodes(), { + onSuccess: (data) => { + queryClient.setQueryData([QueryKeys.user, '2fa', 'backup'], data); + }, + }); +}; + +export const useVerifyTwoFactorTempMutation = ( + options?: t.MutationOptions, +): UseMutationResult => { + const queryClient = useQueryClient(); + return useMutation( + (payload: t.TVerify2FATempRequest) => dataService.verifyTwoFactorTemp(payload), + { + ...(options || {}), + onSuccess: (data, ...args) => { + queryClient.setQueryData([QueryKeys.user, '2fa'], data); + options?.onSuccess?.(data, ...args); + }, + }, + ); +}; diff --git a/client/src/hooks/AuthContext.tsx b/client/src/hooks/AuthContext.tsx index 3a680f30b8..3d000ff78e 100644 --- a/client/src/hooks/AuthContext.tsx +++ b/client/src/hooks/AuthContext.tsx @@ -70,7 +70,12 @@ const AuthContextProvider = ({ const loginUser = useLoginUserMutation({ onSuccess: (data: t.TLoginResponse) => { - const { user, token } = data; + const { user, token, twoFAPending, tempToken } = data; + if (twoFAPending) { + // Redirect to the two-factor authentication route. + navigate(`/login/2fa?tempToken=${tempToken}`, { replace: true }); + return; + } setError(undefined); setUserContext({ token, isAuthenticated: true, user, redirect: '/c/new' }); }, @@ -212,4 +217,4 @@ const useAuthContext = () => { return context; }; -export { AuthContextProvider, useAuthContext }; +export { AuthContextProvider, useAuthContext }; \ No newline at end of file diff --git a/client/src/locales/ar/translation.json b/client/src/locales/ar/translation.json index 54824fd1a7..f0f9f25ebd 100644 --- a/client/src/locales/ar/translation.json +++ b/client/src/locales/ar/translation.json @@ -178,7 +178,6 @@ "com_endpoint_google_temp": "ุงู„ู‚ูŠู… ุงู„ุฃุนู„ู‰ = ุฃูƒุซุฑ ุนุดูˆุงุฆูŠุฉุŒ ุจูŠู†ู…ุง ุงู„ู‚ูŠู… ุงู„ุฃู‚ู„ = ุฃูƒุซุฑ ุชุฑูƒูŠุฒู‹ุง ูˆุญุชู…ูŠุฉ. ู†ูˆุตูŠ ุจุชุบูŠูŠุฑ ู‡ุฐุง ุฃูˆ Top P ูˆู„ูƒู† ู„ูŠุณ ูƒู„ุงู‡ู…ุง.", "com_endpoint_google_topk": "Top-k ูŠุบูŠุฑ ูƒูŠููŠุฉ ุงุฎุชูŠุงุฑ ุงู„ู†ู…ูˆุฐุฌ ู„ู„ุฑู…ูˆุฒ ู„ู„ุฅุฎุฑุงุฌ. top-k ู…ู† 1 ูŠุนู†ูŠ ุฃู† ุงู„ุฑู…ุฒ ุงู„ู…ุญุฏุฏ ู‡ูˆ ุงู„ุฃูƒุซุฑ ุงุญุชู…ุงู„ูŠุฉ ุจูŠู† ุฌู…ูŠุน ุงู„ุฑู…ูˆุฒ ููŠ ู…ูุฑุฏุงุช ุงู„ู†ู…ูˆุฐุฌ (ูŠุณู…ู‰ ุฃูŠุถู‹ุง ุงู„ุชุฑู…ูŠุฒ ุงู„ุฌุดุนูŠ)ุŒ ุจูŠู†ู…ุง top-k ู…ู† 3 ูŠุนู†ูŠ ุฃู† ุงู„ุฑู…ุฒ ุงู„ุชุงู„ูŠ ูŠุชู… ุงุฎุชูŠุงุฑู‡ ู…ู† ุจูŠู† ุงู„ุฑู…ูˆุฒ ุงู„ุซู„ุงุซุฉ ุงู„ุฃูƒุซุฑ ุงุญุชู…ุงู„ูŠุฉ (ุจุงุณุชุฎุฏุงู… ุงู„ุญุฑุงุฑุฉ).", "com_endpoint_google_topp": "Top-p ูŠุบูŠุฑ ูƒูŠููŠุฉ ุงุฎุชูŠุงุฑ ุงู„ู†ู…ูˆุฐุฌ ู„ู„ุฑู…ูˆุฒ ู„ู„ุฅุฎุฑุงุฌ. ูŠุชู… ุงุฎุชูŠุงุฑ ุงู„ุฑู…ูˆุฒ ู…ู† ุงู„ุฃูƒุซุฑ K (ุงู†ุธุฑ ู…ุนู„ู…ุฉ topK) ุงุญุชู…ุงู„ู‹ุง ุฅู„ู‰ ุงู„ุฃู‚ู„ ุญุชู‰ ูŠุตุจุญ ู…ุฌู…ูˆุน ุงุญุชู…ุงู„ุงุชู‡ู… ูŠุณุงูˆูŠ ู‚ูŠู…ุฉ top-p.", - "com_endpoint_import": "ุงุณุชูŠุฑุงุฏ", "com_endpoint_instructions_assistants": "ุชุนู„ูŠู…ุงุช ุงู„ุชุฌุงูˆุฒ", "com_endpoint_instructions_assistants_placeholder": "ูŠุชุฌุงูˆุฒ ุงู„ุชุนู„ูŠู…ุงุช ุงู„ุฎุงุตุฉ ุจุงู„ู…ุณุงุนุฏ. ู‡ุฐุง ู…ููŠุฏ ู„ุชุนุฏูŠู„ ุงู„ุณู„ูˆูƒ ุนู„ู‰ ุฃุณุงุณ ูƒู„ ู…ุฑุฉ.", "com_endpoint_max_output_tokens": "ุงู„ุญุฏ ุงู„ุฃู‚ุตู‰ ู„ุนุฏุฏ ุงู„ุฑู…ูˆุฒ ุงู„ู…ู†ุชุฌุฉ", @@ -262,7 +261,6 @@ "com_nav_archive_name": "ุงู„ุงุณู…", "com_nav_archived_chats": "ุงู„ุฏุฑุฏุดุงุช ุงู„ู…ุคุฑุดูุฉ", "com_nav_archived_chats_empty": "ู„ูŠุณ ู„ุฏูŠูƒ ุฃูŠ ุฏุฑุฏุดุงุช ู…ุคุฑุดูุฉ.", - "com_nav_archived_chats_manage": "ุฅุฏุงุฑุฉ", "com_nav_at_command": "ุฃู…ุฑ-@", "com_nav_at_command_description": "ุชุจุฏูŠู„ ุงู„ุฃู…ุฑ \"@\" ู„ู„ุชู†ู‚ู„ ุจูŠู† ู†ู‚ุงุท ุงู„ู†ู‡ุงูŠุฉ ูˆุงู„ู†ู…ุงุฐุฌ ูˆุงู„ุฅุนุฏุงุฏุงุช ุงู„ู…ุณุจู‚ุฉ ูˆุบูŠุฑู‡ุง", "com_nav_audio_play_error": "ุฎุทุฃ ููŠ ุชุดุบูŠู„ ุงู„ุตูˆุช: {{0}}", @@ -383,7 +381,6 @@ "com_nav_setting_speech": "ุงู„ูƒู„ุงู…", "com_nav_settings": "ุงู„ุฅุนุฏุงุฏุงุช", "com_nav_shared_links": "ุฑูˆุงุจุท ู…ุดุชุฑูƒุฉ", - "com_nav_shared_links_manage": "ุงู„ุฅุฏุงุฑุฉ", "com_nav_show_code": "ุฅุธู‡ุงุฑ ุงู„ุดูุฑุฉ ุฏุงุฆู…ู‹ุง ุนู†ุฏ ุงุณุชุฎุฏุงู… ู…ูุณุฑ ุงู„ุดูุฑุฉ", "com_nav_slash_command": "/-ุงู„ุฃู…ุฑ", "com_nav_slash_command_description": "ุชุจุฏูŠู„ ุงู„ุฃู…ุฑ \"/\" ู„ุงุฎุชูŠุงุฑ ู…ูˆุฌู‡ ุนุจุฑ ู„ูˆุญุฉ ุงู„ู…ูุงุชูŠุญ", @@ -587,7 +584,7 @@ "com_ui_happy_birthday": "ุฅู†ู‡ ุนูŠุฏ ู…ูŠู„ุงุฏูŠ ุงู„ุฃูˆู„!", "com_ui_host": "ู…ูุถูŠู", "com_ui_image_gen": "ุชูˆู„ูŠุฏ ุงู„ุตูˆุฑ", - "com_ui_import_conversation": "ุงุณุชูŠุฑุงุฏ", + "com_ui_import": "ุงุณุชูŠุฑุงุฏ", "com_ui_import_conversation_error": "ุญุฏุซ ุฎุทุฃ ุฃุซู†ุงุก ุงุณุชูŠุฑุงุฏ ู…ุญุงุฏุซุงุชูƒ", "com_ui_import_conversation_file_type_error": "ู†ูˆุน ุงู„ู…ู„ู ุบูŠุฑ ู…ุฏุนูˆู… ู„ู„ุงุณุชูŠุฑุงุฏ", "com_ui_import_conversation_info": "ุงุณุชูŠุฑุงุฏ ู…ุญุงุฏุซุงุช ู…ู† ู…ู„ู JSON", @@ -719,4 +716,4 @@ "com_ui_zoom": "ุชูƒุจูŠุฑ", "com_user_message": "ุฃู†ุช", "com_warning_resubmit_unsupported": "ุฅุนุงุฏุฉ ุฅุฑุณุงู„ ุฑุณุงู„ุฉ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ ุบูŠุฑ ู…ุฏุนูˆู…ุฉ ู„ู†ู‚ุทุฉ ุงู„ู†ู‡ุงูŠุฉ ู‡ุฐู‡" -} \ No newline at end of file +} diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index e02657b4b4..98cd14b865 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -182,7 +182,6 @@ "com_endpoint_google_temp": "Hรถhere Werte = zufรคlliger, wรคhrend niedrigere Werte = fokussierter und deterministischer. Wir empfehlen, entweder dies oder Top P zu รคndern, aber nicht beides.", "com_endpoint_google_topk": "Top-k รคndert, wie das Modell Token fรผr die Antwort auswรคhlt. Ein Top-k von 1 bedeutet, dass das ausgewรคhlte Token das wahrscheinlichste unter allen Token im Vokabular des Modells ist (auch Greedy-Decoding genannt), wรคhrend ein Top-k von 3 bedeutet, dass das nรคchste Token aus den 3 wahrscheinlichsten Token ausgewรคhlt wird (unter Verwendung der Temperatur).", "com_endpoint_google_topp": "Top-p รคndert, wie das Modell Token fรผr die Antwort auswรคhlt. Token werden von den wahrscheinlichsten K (siehe topK-Parameter) bis zu den am wenigsten wahrscheinlichen ausgewรคhlt, bis die Summe ihrer Wahrscheinlichkeiten dem Top-p-Wert entspricht.", - "com_endpoint_import": "Importieren", "com_endpoint_instructions_assistants": "Anweisungen รผberschreiben", "com_endpoint_instructions_assistants_placeholder": "รœberschreibt die Anweisungen des Assistenten. Dies ist nรผtzlich, um das Verhalten auf Basis einzelner Ausfรผhrungen zu modifizieren.", "com_endpoint_max_output_tokens": "Max. Antwort-Token", @@ -268,7 +267,6 @@ "com_nav_archive_name": "Name", "com_nav_archived_chats": "Archivierte Chats", "com_nav_archived_chats_empty": "Du hast keine archivierten Chats.", - "com_nav_archived_chats_manage": "Verwalten", "com_nav_at_command": "@-Befehl", "com_nav_at_command_description": "Schaltet den Befehl \"@\" zum Wechseln von Endpunkten, Modellen, Voreinstellungen usw. um.", "com_nav_audio_play_error": "Fehler beim Abspielen des Audios: {{0}}", @@ -391,7 +389,6 @@ "com_nav_setting_speech": "Sprache", "com_nav_settings": "Einstellungen", "com_nav_shared_links": "Geteilte Links", - "com_nav_shared_links_manage": "Verwalten", "com_nav_show_code": "Code immer anzeigen, wenn der Code-Interpreter verwendet wird", "com_nav_show_thinking": "Denkprozess-Dropdowns standardmรครŸig รถffnen", "com_nav_slash_command": "/-Befehl", @@ -617,7 +614,7 @@ "com_ui_hide_qr": "QR-Code ausblenden", "com_ui_host": "Host", "com_ui_image_gen": "Bildgenerierung", - "com_ui_import_conversation": "Importieren", + "com_ui_import": "Importieren", "com_ui_import_conversation_error": "Beim Importieren Ihrer Konversationen ist ein Fehler aufgetreten", "com_ui_import_conversation_file_type_error": "Nicht unterstรผtzter Importtyp", "com_ui_import_conversation_info": "Konversationen aus einer JSON-Datei importieren", @@ -767,4 +764,4 @@ "com_ui_zoom": "Zoom", "com_user_message": "Du", "com_warning_resubmit_unsupported": "Das erneute Senden der KI-Nachricht wird fรผr diesen Endpunkt nicht unterstรผtzt." -} \ No newline at end of file +} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 1daa6794d6..63272a54f4 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1,6 +1,6 @@ { - "chat_direction_left_to_right": "something needs to go here. was empty", - "chat_direction_right_to_left": "something needs to go here. was empty", + "chat_direction_left_to_right": "Chat direction is now left to right", + "chat_direction_right_to_left": "Chat direction is now right to left", "com_a11y_ai_composing": "The AI is still composing.", "com_a11y_end": "The AI has finished their reply.", "com_a11y_start": "The AI has started their reply.", @@ -87,6 +87,7 @@ "com_auth_email_verification_redirecting": "Redirecting in {{0}} seconds...", "com_auth_email_verification_resend_prompt": "Didn't receive the email?", "com_auth_email_verification_success": "Email verified successfully", + "com_auth_email_verifying_ellipsis": "Verifying...", "com_auth_error_create": "There was an error attempting to register your account. Please try again.", "com_auth_error_invalid_reset_token": "This password reset token is no longer valid.", "com_auth_error_login": "Unable to login with the information provided. Please check your credentials and try again.", @@ -184,7 +185,6 @@ "com_endpoint_google_temp": "Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.", "com_endpoint_google_topk": "Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).", "com_endpoint_google_topp": "Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.", - "com_endpoint_import": "Import", "com_endpoint_instructions_assistants": "Override Instructions", "com_endpoint_instructions_assistants_placeholder": "Overrides the instructions of the assistant. This is useful for modifying the behavior on a per-run basis.", "com_endpoint_max_output_tokens": "Max Output Tokens", @@ -263,7 +263,7 @@ "com_files_filter": "Filter files...", "com_files_no_results": "No results.", "com_files_number_selected": "{{0}} of {{1}} items(s) selected", - "com_files_table": "something needs to go here. was empty", + "com_files_table": "Files Table", "com_generated_files": "Generated files:", "com_hide_examples": "Hide Examples", "com_nav_account_settings": "Account Settings", @@ -272,7 +272,6 @@ "com_nav_archive_name": "Name", "com_nav_archived_chats": "Archived chats", "com_nav_archived_chats_empty": "You have no archived conversations.", - "com_nav_archived_chats_manage": "Manage", "com_nav_at_command": "@-Command", "com_nav_at_command_description": "Toggle command \"@\" for switching endpoints, models, presets, etc.", "com_nav_audio_play_error": "Error playing audio: {{0}}", @@ -396,7 +395,6 @@ "com_nav_setting_speech": "Speech", "com_nav_settings": "Settings", "com_nav_shared_links": "Shared links", - "com_nav_shared_links_manage": "Manage", "com_nav_show_code": "Always show code when using code interpreter", "com_nav_show_thinking": "Open Thinking Dropdowns by Default", "com_nav_slash_command": "/-Command", @@ -437,6 +435,9 @@ "com_sidepanel_parameters": "Parameters", "com_sidepanel_select_agent": "Select an Agent", "com_sidepanel_select_assistant": "Select an Assistant", + "com_nav_2fa": "Two-Factor Authentication (2FA)", + "com_auth_verify_your_identity": "Verify Your Identity", + "com_auth_two_factor": "Check your preferred one-time password application for a code", "com_ui_accept": "I accept", "com_ui_add": "Add", "com_ui_add_model_preset": "Add a model or preset for an additional response", @@ -626,7 +627,7 @@ "com_ui_fork_split_target_setting": "Start fork from target message by default", "com_ui_fork_success": "Successfully forked conversation", "com_ui_fork_visible": "Visible messages only", - "com_ui_global_group": "something needs to go here. was empty", + "com_ui_global_group": "Global Group", "com_ui_go_back": "Go back", "com_ui_go_to_conversation": "Go to conversation", "com_ui_happy_birthday": "It's my 1st birthday!", @@ -634,7 +635,7 @@ "com_ui_host": "Host", "com_ui_idea": "Ideas", "com_ui_image_gen": "Image Gen", - "com_ui_import_conversation": "Import", + "com_ui_import": "Import", "com_ui_import_conversation_error": "There was an error importing your conversations", "com_ui_import_conversation_file_type_error": "Unsupported import type", "com_ui_import_conversation_info": "Import conversations from a JSON file", @@ -670,9 +671,9 @@ "com_ui_no_bookmarks": "it seems like you have no bookmarks yet. Click on a chat and add a new one", "com_ui_no_category": "No category", "com_ui_no_changes": "No changes to update", - "com_ui_no_data": "something needs to go here. was empty", + "com_ui_no_data": "No data", "com_ui_no_terms_content": "No terms and conditions content to display", - "com_ui_no_valid_items": "something needs to go here. was empty", + "com_ui_no_valid_items": "No valid items", "com_ui_none": "None", "com_ui_none_selected": "None selected", "com_ui_nothing_found": "Nothing found", @@ -795,6 +796,36 @@ "com_ui_write": "Writing", "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", + "com_ui_secret_key": "Secret Key", + "com_ui_2fa_account_security": "Two-factor authentication adds an extra layer of security to your account", + "com_ui_2fa_generate_error": "There was an error generating two-factor authentication settings", + "com_ui_backup_codes": "Backup Codes", + "com_ui_2fa_invalid": "Invalid two-factor authentication code", + "com_ui_2fa_setup": "Setup 2FA", + "com_ui_2fa_enable": "Enable 2FA", + "com_ui_2fa_disable": "Disable 2FA", + "com_ui_disabling": "Disabling...", + "com_ui_2fa_enabled": "2FA has been enabled", + "com_ui_2fa_disabled": "2FA has been disabled", + "com_ui_download_backup": "Download Backup Codes", + "com_ui_use_backup_code": "Use Backup Code Instead", + "com_ui_use_2fa_code": "Use 2FA Code Instead", + "com_ui_verify": "Verify", + "com_ui_2fa_disable_error": "There was an error disabling two-factor authentication", + "com_ui_2fa_verified": "Successfully verified Two-Factor Authentication", + "com_ui_generate_backup": "Generate Backup Codes", + "com_ui_regenerate_backup": "Regenerate Backup Codes", + "com_ui_regenerating": "Regenerating...", + "com_ui_used": "Used", + "com_ui_not_used": "Not Used", + "com_ui_backup_codes_regenerated": "Backup codes have been regenerated successfully", + "com_ui_backup_codes_regenerate_error": "There was an error regenerating backup codes", + "com_ui_no_backup_codes": "No backup codes available. Please generate new ones", + "com_ui_generating": "Generating...", + "com_ui_generate_qrcode": "Generate QR Code", + "com_ui_complete_setup": "Complete Setup", + "com_ui_download_backup_tooltip": "Before you continue, download your backup codes. You will need them to regain access if you lose your authenticator device", + "com_ui_show": "Show", "com_user_message": "You", "com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint." -} \ No newline at end of file +} diff --git a/client/src/locales/es/translation.json b/client/src/locales/es/translation.json index a662d76452..3355d79df4 100644 --- a/client/src/locales/es/translation.json +++ b/client/src/locales/es/translation.json @@ -178,7 +178,6 @@ "com_endpoint_google_temp": "Los valores mรกs altos = mรกs aleatorios, mientras que los valores mรกs bajos = mรกs enfocados y deterministas. Recomendamos alterar esto o Top P, pero no ambos.", "com_endpoint_google_topk": "Top-k cambia la forma en que el modelo selecciona tokens para la salida. Un top-k de 1 significa que el token seleccionado es el mรกs probable entre todos los tokens en el vocabulario del modelo (tambiรฉn llamado decodificaciรณn codiciosa), mientras que un top-k de 3 significa que el siguiente token se selecciona entre los 3 tokens mรกs probables (usando temperatura).", "com_endpoint_google_topp": "Top-p cambia la forma en que el modelo selecciona tokens para la salida. Los tokens se seleccionan desde los mรกs K (ver parรกmetro topK) probables hasta los menos probables hasta que la suma de sus probabilidades sea igual al valor top-p.", - "com_endpoint_import": "Importar", "com_endpoint_instructions_assistants": "Anular instrucciones", "com_endpoint_instructions_assistants_placeholder": "Anula las instrucciones del asistente. Esto es รบtil para modificar el comportamiento por ejecuciรณn.", "com_endpoint_max_output_tokens": "Tokens de Salida Mรกximos", @@ -262,7 +261,6 @@ "com_nav_archive_name": "Nombre", "com_nav_archived_chats": "Archivadas", "com_nav_archived_chats_empty": "No tienes conversaciones archivadas.", - "com_nav_archived_chats_manage": "Gestionar", "com_nav_at_command": "Comando @", "com_nav_at_command_description": "Alternar comando \"@\" para cambiar entre puntos de conexiรณn, modelos, ajustes predefinidos, etc.", "com_nav_audio_play_error": "Error al reproducir el audio: {{0}}", @@ -383,7 +381,6 @@ "com_nav_setting_speech": "Voz y habla", "com_nav_settings": "Configuraciรณn", "com_nav_shared_links": "Links Compartidos", - "com_nav_shared_links_manage": "Gerenciar", "com_nav_show_code": "Mostrar siempre el cรณdigo cuando se use el intรฉrprete de cรณdigo", "com_nav_slash_command": "Comando /", "com_nav_slash_command_description": "Alternar comando '/' para seleccionar un mensaje predefinido mediante el teclado", @@ -587,7 +584,7 @@ "com_ui_happy_birthday": "ยกEs mi primer cumpleaรฑos!", "com_ui_host": "Host", "com_ui_image_gen": "Gen Imรกgenes", - "com_ui_import_conversation": "Importar", + "com_ui_import": "Importar", "com_ui_import_conversation_error": "Hubo un error al importar tus chats", "com_ui_import_conversation_file_type_error": "com_ui_import_conversation_file_type_error: Tipo de archivo no compatible para importar", "com_ui_import_conversation_info": "Importar chats de un archivo JSON", @@ -719,4 +716,4 @@ "com_ui_zoom": "Zoom", "com_user_message": "Usted", "com_warning_resubmit_unsupported": "No se admite el reenvรญo del mensaje de IA para este punto de conexiรณn." -} \ No newline at end of file +} diff --git a/client/src/locales/et/translation.json b/client/src/locales/et/translation.json index cbf616cec1..84d40e80f6 100644 --- a/client/src/locales/et/translation.json +++ b/client/src/locales/et/translation.json @@ -396,7 +396,6 @@ "com_nav_setting_speech": "Kรตne", "com_nav_settings": "Seaded", "com_nav_shared_links": "Jagatud lingid", - "com_nav_shared_links_manage": "Halda", "com_nav_show_code": "Nรคita koodi alati, kui kasutatakse koodiinterpreteerijat", "com_nav_show_thinking": "Ava mรตtlemise rippmenรผรผd vaikimisi", "com_nav_slash_command": "/-kรคsk", @@ -787,4 +786,4 @@ "com_ui_zoom": "Suumi", "com_user_message": "Sina", "com_warning_resubmit_unsupported": "AI sรตnumi uuesti esitamine pole selle otspunkti jaoks toetatud." -} \ No newline at end of file +} diff --git a/client/src/locales/fi/translation.json b/client/src/locales/fi/translation.json index 3005dcfb6b..190d344026 100644 --- a/client/src/locales/fi/translation.json +++ b/client/src/locales/fi/translation.json @@ -149,7 +149,6 @@ "com_endpoint_google_temp": "Korkeampi arvo = satunnaisempi; matalampi arvo = keskittyneempi ja deterministisempi. Suosittelemme, ettรค muokkaat tรคtรค tai Top P:tรค, mutta ei molempia.", "com_endpoint_google_topk": "Top-k vaikuttaa siihen, miten malli valitsee tokeineita tulokseen. Jos Top-k on 1, valitaan se token, joka on kaikkien todennรคkรถisen mallin sanastossa (tunnetaan myรถs nimellรค ahne dekoodaus), kun taas top-k 3 tarkoittaisi, ettรค seuraavat token valitaan 3 todennรคkรถisimmรคn tokenin joukosta, lรคmpรถtilaa hyรถdyntรคen.", "com_endpoint_google_topp": "Top-P vaikuttaa siihen kuinka malli valitsee tokeneita tulokseen. Tokenit valitaan top-k:sta (ks. Top-k -parametri) todennรคkรถisimmistรค vรคhiten todennรคkรถseen, kunnes niiden todennรคkรถisyyksien summa ylittรครค Top-P -arvon.", - "com_endpoint_import": "Tuo", "com_endpoint_instructions_assistants": "Yliaja ohjeet", "com_endpoint_instructions_assistants_placeholder": "Yliajaa Avustajan ohjeet. Tรคtรค voi hyรถdyntรครค kรคytรถksen muuttamiseen keskustelukohtaisesti.", "com_endpoint_max_output_tokens": "Tulos-tokeneiden maksimimรครคrรค", @@ -220,7 +219,6 @@ "com_nav_archive_name": "Nimi", "com_nav_archived_chats": "Arkistoidut keskustelut", "com_nav_archived_chats_empty": "Sinulla ei ole arkistoituja keskusteluita.", - "com_nav_archived_chats_manage": "Hallinnoi", "com_nav_audio_play_error": "Virhe รครคntรค toistaessa: {{0}}", "com_nav_audio_process_error": "Virhe รครคntรค kรคsitellessรค: {{0}}", "com_nav_auto_scroll": "Vieritรค automaattisesti viimeisimpรครคn viestiin keskustelua avatessa", @@ -315,7 +313,6 @@ "com_nav_setting_speech": "Puhe", "com_nav_settings": "Asetukset", "com_nav_shared_links": "Jaetut linkit", - "com_nav_shared_links_manage": "Hallinnoi", "com_nav_show_code": "Kooditulkkia kรคyttรคessรค nรคytรค aina koodi", "com_nav_speech_to_text": "Puheesta tekstiksi", "com_nav_text_to_speech": "Tekstistรค puheeksi", @@ -461,7 +458,7 @@ "com_ui_happy_birthday": "On 1. syntymรคpรคivรคni!", "com_ui_host": "Host", "com_ui_image_gen": "Kuvanluonti", - "com_ui_import_conversation": "Tuo", + "com_ui_import": "Tuo", "com_ui_import_conversation_error": "Keskustelujesi tuonnissa tapahtui virhe", "com_ui_import_conversation_file_type_error": "Tiedostotyyppi ei ole tuettu tuonnissa", "com_ui_import_conversation_info": "Tuo keskusteluja JSON-tiedostosta", @@ -554,4 +551,4 @@ "com_ui_versions": "Versiot", "com_ui_yes": "Kyllรค", "com_user_message": "Sinรค" -} \ No newline at end of file +} diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index ccfae170d2..b1bd434c9d 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -179,7 +179,6 @@ "com_endpoint_google_temp": "Des valeurs plus รฉlevรฉes = plus alรฉatoires, tandis que des valeurs plus faibles = plus concentrรฉes et dรฉterministes. Nous vous recommandons de modifier ceci ou Top P mais pas les deux.", "com_endpoint_google_topk": "Top-k change la faรงon dont le modรจle sรฉlectionne les jetons pour la sortie. Un top-k de 1 signifie que le jeton sรฉlectionnรฉ est le plus probable parmi tous les jetons du vocabulaire du modรจle (รฉgalement appelรฉ dรฉcodage glouton), tandis qu'un top-k de 3 signifie que le jeton suivant est sรฉlectionnรฉ parmi les 3 jetons les plus probables (en utilisant la tempรฉrature).", "com_endpoint_google_topp": "Top-p change la faรงon dont le modรจle sรฉlectionne les jetons pour la sortie. Les jetons sont sรฉlectionnรฉs du plus K (voir le paramรจtre topK) probable au moins jusqu'ร  ce que la somme de leurs probabilitรฉs รฉgale la valeur top-p.", - "com_endpoint_import": "Importer", "com_endpoint_instructions_assistants": "Instructions de remplacement", "com_endpoint_instructions_assistants_placeholder": "Remplace les instructions de l'assistant. Cela est utile pour modifier le comportement au cas par cas.", "com_endpoint_max_output_tokens": "Nombre maximum de jetons en sortie", @@ -265,7 +264,6 @@ "com_nav_archive_name": "Nom", "com_nav_archived_chats": "Conversations archivรฉes", "com_nav_archived_chats_empty": "Vous n'avez aucune conversation archivรฉe.", - "com_nav_archived_chats_manage": "Gรฉrer", "com_nav_at_command": "Commande-@", "com_nav_at_command_description": "Basculer la commande \"@\" pour changer d'endpoints, de modรจles, de prรฉrรฉglages, etc.", "com_nav_audio_play_error": "Erreur de lecture audio : {{0}}", @@ -388,7 +386,6 @@ "com_nav_setting_speech": "Parole", "com_nav_settings": "Paramรจtres", "com_nav_shared_links": "Liens partagรฉs", - "com_nav_shared_links_manage": "Gรฉrer", "com_nav_show_code": "Toujours afficher le code lors de l'utilisation de l'interprรฉteur de code", "com_nav_show_thinking": "Ovrir les menus dรฉroulants de rรฉflexion par dรฉfaut", "com_nav_slash_command": "/-Commande", @@ -602,7 +599,7 @@ "com_ui_hide_qr": "Cacher le code QR", "com_ui_host": "Hรดte", "com_ui_image_gen": "Gรฉnรฉration d'image", - "com_ui_import_conversation": "Importer", + "com_ui_import": "Importer", "com_ui_import_conversation_error": "Une erreur s'est produite lors de l'importation de vos conversations", "com_ui_import_conversation_file_type_error": "Type de fichier non pris en charge pour l'importation", "com_ui_import_conversation_info": "Importer des conversations ร  partir d'un fichier JSON", @@ -736,4 +733,4 @@ "com_ui_zoom": "Zoom", "com_user_message": "Vous", "com_warning_resubmit_unsupported": "La resoumission du message IA n'est pas prise en charge pour ce point de terminaison." -} \ No newline at end of file +} diff --git a/client/src/locales/he/translation.json b/client/src/locales/he/translation.json index 3c2f1f6317..456d3d33e0 100644 --- a/client/src/locales/he/translation.json +++ b/client/src/locales/he/translation.json @@ -167,7 +167,6 @@ "com_nav_archive_name": "ืฉื", "com_nav_archived_chats": "ืฉื™ื—ื•ืช ืžืืจื›ื™ื•ืŸ", "com_nav_archived_chats_empty": "ืื™ืŸ ืฉื™ื—ื•ืช ืžืืจื›ื™ื•ืŸ.", - "com_nav_archived_chats_manage": "ื ื™ื”ื•ืœ", "com_nav_auto_scroll": "Auto-s ื’ืœื•ืœ ืืœ ื”ื›ื™ ื—ื“ืฉ ื‘ืคืชื™ื—ื”", "com_nav_balance": "ืœึฐืึทื–ึตืŸ", "com_nav_change_picture": "ืฉื ื” ืชืžื•ื ื”", @@ -231,7 +230,6 @@ "com_nav_setting_general": "ื›ืœืœื™", "com_nav_settings": "ื”ื’ื“ืจื•ืช", "com_nav_shared_links": "ืงื™ืฉื•ืจื™ื ืžืฉื•ืชืคื™ื", - "com_nav_shared_links_manage": "ื ื™ื”ื•ืœ", "com_nav_theme": "ื ื•ืฉื", "com_nav_theme_dark": "ื›ื”ื”", "com_nav_theme_light": "ืื•ืจ", @@ -293,7 +291,7 @@ "com_ui_error": "ืฉื’ื™ืื”", "com_ui_examples": "ื“ื•ื’ืžืื•ืช", "com_ui_happy_birthday": "ื–ื” ื™ื•ื ื”ื”ื•ืœื“ืช ื”ืจืืฉื•ืŸ ืฉืœื™!", - "com_ui_import_conversation": "ื™ื‘ื•ื", + "com_ui_import": "ื™ื‘ื•ื", "com_ui_import_conversation_error": "ืื™ืจืขื” ืฉื’ื™ืื” ื‘ืขืช ื™ื™ื‘ื•ื ื”ืฉื™ื—ื•ืช ืฉืœืš", "com_ui_import_conversation_info": "ื™ื™ื‘ื ืฉื™ื—ื•ืช ืžืงื•ื‘ืฅ JSON", "com_ui_import_conversation_success": "ื”ืฉื™ื—ื•ืช ื™ื•ื‘ืื• ื‘ื”ืฆืœื—ื”", @@ -333,4 +331,4 @@ "com_ui_upload_success": "ืงื•ื‘ืฅ ืฉื”ื•ืขืœื” ื‘ื”ืฆืœื—ื”", "com_ui_use_prompt": "ื”ืฉืชืžืฉ ื‘ื”ื•ื“ืขืช", "com_user_message": "ืืชื”" -} \ No newline at end of file +} diff --git a/client/src/locales/id/translation.json b/client/src/locales/id/translation.json index e5d1a23594..b6d7d57237 100644 --- a/client/src/locales/id/translation.json +++ b/client/src/locales/id/translation.json @@ -91,7 +91,6 @@ "com_endpoint_google_temp": "Nilai yang lebih tinggi = lebih acak, sedangkan nilai yang lebih rendah = lebih fokus dan deterministik. Kami merekomendasikan untuk mengubah ini atau Top P tetapi tidak keduanya.", "com_endpoint_google_topk": "Top-k mengubah cara model memilih token untuk output. Top-k 1 berarti token yang dipilih adalah yang paling mungkin di antara semua token dalam kosakata model (juga disebut decoding serakah), sedangkan top-k 3 berarti token berikutnya dipilih dari antara 3 token yang paling mungkin (menggunakan temperatur).", "com_endpoint_google_topp": "Top-p mengubah cara model memilih token untuk output. Token dipilih dari yang paling mungkin (lihat parameter topK) hingga yang paling tidak mungkin sampai jumlah probabilitas mereka sama dengan nilai top-p.", - "com_endpoint_import": "Impor", "com_endpoint_max_output_tokens": "Token Output Maks", "com_endpoint_message": "Pesan", "com_endpoint_message_not_appendable": "Edit pesan Anda atau Regenerasi.", @@ -142,7 +141,6 @@ "com_nav_archive_name": "Nama", "com_nav_archived_chats": "Percakapan Arsip", "com_nav_archived_chats_empty": "Tidak ada percakapan yang diarsipkan.", - "com_nav_archived_chats_manage": "Pengelolaan", "com_nav_auto_scroll": "Otomatis gulir ke Baru saat Buka", "com_nav_balance": "Keseimbangan", "com_nav_change_picture": "Ubah foto", @@ -205,7 +203,6 @@ "com_nav_setting_general": "Umum", "com_nav_settings": "Pengaturan", "com_nav_shared_links": "Link berbagi", - "com_nav_shared_links_manage": "Pengeluaran", "com_nav_theme": "Tema", "com_nav_theme_dark": "Gelap", "com_nav_theme_light": "Terang", @@ -250,7 +247,7 @@ "com_ui_enter": "Masuk", "com_ui_examples": "Contoh", "com_ui_happy_birthday": "Ini ulang tahun pertamaku!", - "com_ui_import_conversation": "Impor", + "com_ui_import": "Impor", "com_ui_import_conversation_error": "Terjadi kesalahan saat mengimpor percakapan Anda", "com_ui_import_conversation_info": "Impor percakapan dari file JSON", "com_ui_import_conversation_success": "Percakapan berhasil diimpor", @@ -287,4 +284,4 @@ "com_ui_upload_success": "Berhasil mengunggah file", "com_ui_use_prompt": "Gunakan petunjuk", "com_user_message": "Kamu" -} \ No newline at end of file +} diff --git a/client/src/locales/it/translation.json b/client/src/locales/it/translation.json index 45c26e11e2..d64fca28e5 100644 --- a/client/src/locales/it/translation.json +++ b/client/src/locales/it/translation.json @@ -179,7 +179,6 @@ "com_endpoint_google_temp": "Valori piรน alti = piรน casualitร , mentre valori piรน bassi = piรน focalizzati e deterministici. Consigliamo di modificare questo o Top P ma non entrambi.", "com_endpoint_google_topk": "Top-k cambia il modo in cui il modello seleziona i token per l'output. Un top-k di 1 significa che il token selezionato รจ il piรน probabile tra tutti i token nel vocabolario del modello (anche chiamato greedy decoding), mentre un top-k di 3 significa che il prossimo token รจ selezionato tra i 3 piรน probabili (usando la temperatura).", "com_endpoint_google_topp": "Top-p cambia il modo in cui il modello seleziona i token per l'output. I token vengono selezionati dai piรน probabili K (vedi parametro topK) ai meno probabili fino a quando la somma delle loro probabilitร  eguaglia il valore top-p.", - "com_endpoint_import": "Importa", "com_endpoint_instructions_assistants": "Sovrascrivi istruzioni", "com_endpoint_instructions_assistants_placeholder": "Sovrascrive le istruzioni dell'assistente. Utile per modificare il comportamento su base singola.", "com_endpoint_max_output_tokens": "Token di output massimi", @@ -265,7 +264,6 @@ "com_nav_archive_name": "Nome", "com_nav_archived_chats": "Chat archiviate", "com_nav_archived_chats_empty": "Non hai chat archiviate.", - "com_nav_archived_chats_manage": "Gestisci", "com_nav_at_command": "Comando @", "com_nav_at_command_description": "Attiva il comando \"@\" per cambiare endpoint, modelli, preset e altro", "com_nav_audio_play_error": "Errore durante la riproduzione audio: {{0}}", @@ -388,7 +386,6 @@ "com_nav_setting_speech": "Voce", "com_nav_settings": "Impostazioni", "com_nav_shared_links": "Link condivisi", - "com_nav_shared_links_manage": "Gestisci", "com_nav_show_code": "Mostra sempre il codice quando si usa l'interprete di codice", "com_nav_show_thinking": "Apri i menu a tendina del ragionamento per impostazione predefinita", "com_nav_slash_command": "/-Comando", @@ -603,7 +600,7 @@ "com_ui_hide_qr": "Nascondi codice QR", "com_ui_host": "Host", "com_ui_image_gen": "Generazione immagine", - "com_ui_import_conversation": "Importa", + "com_ui_import": "Importa", "com_ui_import_conversation_error": "Si รจ verificato un errore durante l'importazione delle conversazioni", "com_ui_import_conversation_file_type_error": "Tipo di importazione non supportato", "com_ui_import_conversation_info": "Importa conversazioni da un file JSON", @@ -747,4 +744,4 @@ "com_ui_zoom": "Zoom", "com_user_message": "Mostra nome utente nei messaggi", "com_warning_resubmit_unsupported": "Il reinvio del messaggio AI non รจ supportato per questo endpoint." -} \ No newline at end of file +} diff --git a/client/src/locales/ja/translation.json b/client/src/locales/ja/translation.json index 52846cd59a..965ca5699d 100644 --- a/client/src/locales/ja/translation.json +++ b/client/src/locales/ja/translation.json @@ -178,7 +178,6 @@ "com_endpoint_google_temp": "ๅคงใใ„ๅ€ค = ใƒฉใƒณใƒ€ใƒ ๆ€งใŒๅข—ใ—ใพใ™ใ€‚ไฝŽใ„ๅ€ค = ใ‚ˆใ‚Šๆฑบๅฎš่ซ–็š„ใซใชใ‚Šใพใ™ใ€‚ใ“ใฎๅ€คใ‚’ๅค‰ๆ›ดใ™ใ‚‹ใ‹ใ€Top P ใฎๅค‰ๆ›ดใ‚’ใŠใ™ใ™ใ‚ใ—ใพใ™ใŒใ€ไธกๆ–นใ‚’ๅค‰ๆ›ดใฏใŠใ™ใ™ใ‚ใ—ใพใ›ใ‚“ใ€‚", "com_endpoint_google_topk": "Top-k ใฏใƒขใƒ‡ใƒซใŒใƒˆใƒผใ‚ฏใƒณใ‚’ใฉใฎใ‚ˆใ†ใซ้ธๆŠžใ—ใฆๅ‡บๅŠ›ใ™ใ‚‹ใ‹ใ‚’ๅค‰ๆ›ดใ—ใพใ™ใ€‚top-kใŒ1ใฎๅ ดๅˆใฏใƒขใƒ‡ใƒซใฎ่ชžๅฝ™ใซๅซใพใ‚Œใ‚‹ใ™ในใฆใฎใƒˆใƒผใ‚ฏใƒณใฎไธญใงๆœ€ใ‚‚็ขบ็އใŒ้ซ˜ใ„1ใคใŒ้ธๆŠžใ•ใ‚Œใพใ™(greedy decodingใจๅ‘ผใฐใ‚Œใฆใ„ใ‚‹)ใ€‚top-kใŒ3ใฎๅ ดๅˆใฏไธŠไฝ3ใคใฎใƒˆใƒผใ‚ฏใƒณใฎไธญใ‹ใ‚‰้ธๆŠžใ•ใ‚Œใพใ™ใ€‚(temperatureใ‚’ไฝฟ็”จ)", "com_endpoint_google_topp": "Top-p ใฏใƒขใƒ‡ใƒซใŒใƒˆใƒผใ‚ฏใƒณใ‚’ใฉใฎใ‚ˆใ†ใซ้ธๆŠžใ—ใฆๅ‡บๅŠ›ใ™ใ‚‹ใ‹ใ‚’ๅค‰ๆ›ดใ—ใพใ™ใ€‚K(topKใ‚’ๅ‚็…ง)ใฎ็ขบ็އใฎๅˆ่จˆใŒtop-pใฎ็ขบ็އใจ็ญ‰ใ—ใใชใ‚‹ใพใงใฎใƒˆใƒผใ‚ฏใƒณใŒ้ธๆŠžใ•ใ‚Œใพใ™ใ€‚", - "com_endpoint_import": "ใ‚คใƒณใƒใƒผใƒˆ", "com_endpoint_instructions_assistants": "ๆŒ‡็คบใ‚’ใ‚ชใƒผใƒใƒผใƒฉใ‚คใƒ‰ใ™ใ‚‹", "com_endpoint_instructions_assistants_placeholder": "ใ‚ขใ‚ทใ‚นใ‚ฟใƒณใƒˆใฎๆŒ‡็คบใ‚’ไธŠๆ›ธใใ—ใพใ™ใ€‚ใ“ใ‚Œใฏใ€ๅฎŸ่กŒใ”ใจใซๅ‹•ไฝœใ‚’ๅค‰ๆ›ดใ™ใ‚‹ๅ ดๅˆใซไพฟๅˆฉใงใ™ใ€‚", "com_endpoint_max_output_tokens": "ๆœ€ๅคงๅ‡บๅŠ›ใƒˆใƒผใ‚ฏใƒณๆ•ฐ", @@ -262,7 +261,6 @@ "com_nav_archive_name": "ๅๅ‰", "com_nav_archived_chats": "ใ‚ขใƒผใ‚ซใ‚คใƒ–ใ•ใ‚ŒใŸใƒใƒฃใƒƒใƒˆ", "com_nav_archived_chats_empty": "ใ‚ขใƒผใ‚ซใ‚คใƒ–ใ•ใ‚ŒใŸใƒใƒฃใƒƒใƒˆใฏใ‚ใ‚Šใพใ›ใ‚“", - "com_nav_archived_chats_manage": "็ฎก็†", "com_nav_at_command": "@-Command", "com_nav_at_command_description": "ใ‚ณใƒžใƒณใƒ‰\"@\"ใงใ‚จใƒณใƒ‰ใƒใ‚คใƒณใƒˆใ€ใƒขใƒ‡ใƒซใ€ใƒ—ใƒชใ‚ปใƒƒใƒˆใ‚’ๅˆ‡ใ‚Šๆ›ฟใˆใ‚‹", "com_nav_audio_play_error": "ใ‚ชใƒผใƒ‡ใ‚ฃใ‚ชใฎๅ†็”Ÿใ‚จใƒฉใƒผ: {{0}}", @@ -383,7 +381,6 @@ "com_nav_setting_speech": "ใ‚นใƒ”ใƒผใƒ", "com_nav_settings": "่จญๅฎš", "com_nav_shared_links": "ๅ…ฑๆœ‰ใƒชใƒณใ‚ฏ", - "com_nav_shared_links_manage": "็ฎก็†", "com_nav_show_code": "Code Interpreter ใ‚’ไฝฟ็”จใ™ใ‚‹้š›ใฏๅธธใซใ‚ณใƒผใƒ‰ใ‚’่กจ็คบใ™ใ‚‹", "com_nav_slash_command": "/-Command", "com_nav_slash_command_description": "ใ‚ณใƒžใƒณใƒ‰\"/\"ใงใ‚ญใƒผใƒœใƒผใƒ‰ใงใƒ—ใƒญใƒณใƒ—ใƒˆใ‚’้ธๆŠžใ™ใ‚‹", @@ -587,7 +584,7 @@ "com_ui_happy_birthday": "ๅˆใ‚ใฆใฎ่ช•็”Ÿๆ—ฅใงใ™๏ผ", "com_ui_host": "ใƒ›ใ‚นใƒˆ", "com_ui_image_gen": "็”ปๅƒ็”Ÿๆˆ", - "com_ui_import_conversation": "ใ‚คใƒณใƒใƒผใƒˆ", + "com_ui_import": "ใ‚คใƒณใƒใƒผใƒˆ", "com_ui_import_conversation_error": "ไผš่ฉฑใฎใ‚คใƒณใƒใƒผใƒˆๆ™‚ใซใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸ", "com_ui_import_conversation_file_type_error": "ใ‚ตใƒใƒผใƒˆใ•ใ‚Œใฆใ„ใชใ„ใ‚คใƒณใƒใƒผใƒˆๅฝขๅผใงใ™", "com_ui_import_conversation_info": "JSONใƒ•ใ‚กใ‚คใƒซใ‹ใ‚‰ไผš่ฉฑใ‚’ใ‚คใƒณใƒใƒผใƒˆใ™ใ‚‹", @@ -719,4 +716,4 @@ "com_ui_zoom": "ใ‚บใƒผใƒ ", "com_user_message": "ใ‚ใชใŸ", "com_warning_resubmit_unsupported": "ใ“ใฎใ‚จใƒณใƒ‰ใƒใ‚คใƒณใƒˆใงใฏAIใƒกใƒƒใ‚ปใƒผใ‚ธใฎๅ†้€ไฟกใฏใ‚ตใƒใƒผใƒˆใ•ใ‚Œใฆใ„ใพใ›ใ‚“" -} \ No newline at end of file +} diff --git a/client/src/locales/ko/translation.json b/client/src/locales/ko/translation.json index 61571435c3..46eda9f9de 100644 --- a/client/src/locales/ko/translation.json +++ b/client/src/locales/ko/translation.json @@ -178,7 +178,6 @@ "com_endpoint_google_temp": "๋†’์€ ๊ฐ’ = ๋” ๋ฌด์ž‘์œ„, ๋‚ฎ์€ ๊ฐ’ = ๋” ์ง‘์ค‘์ ์ด๊ณ  ๊ฒฐ์ •์ ์ž…๋‹ˆ๋‹ค. ์ด ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ Top P ์ค‘ ํ•˜๋‚˜๋งŒ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.", "com_endpoint_google_topk": "Top-k๋Š” ๋ชจ๋ธ์ด ์ถœ๋ ฅ์— ์‚ฌ์šฉํ•  ํ† ํฐ์„ ์„ ํƒํ•˜๋Š” ๋ฐฉ์‹์„ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. top-k๊ฐ€ 1์ธ ๊ฒฝ์šฐ ๋ชจ๋ธ์˜ ์–ดํœ˜ ์ค‘ ๊ฐ€์žฅ ํ™•๋ฅ ์ด ๋†’์€ ํ† ํฐ์ด ์„ ํƒ๋ฉ๋‹ˆ๋‹ค(greedy decoding). top-k๊ฐ€ 3์ธ ๊ฒฝ์šฐ ๋‹ค์Œ ํ† ํฐ์€ ๊ฐ€์žฅ ํ™•๋ฅ ์ด ๋†’์€ 3๊ฐœ์˜ ํ† ํฐ ์ค‘์—์„œ ์„ ํƒ๋ฉ๋‹ˆ๋‹ค(temperature ์‚ฌ์šฉ).", "com_endpoint_google_topp": "Top-p๋Š” ๋ชจ๋ธ์ด ์ถœ๋ ฅ์— ์‚ฌ์šฉํ•  ํ† ํฐ์„ ์„ ํƒํ•˜๋Š” ๋ฐฉ์‹์„ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. ํ† ํฐ์€ ๊ฐ€์žฅ ๋†’์€ ํ™•๋ฅ ๋ถ€ํ„ฐ ๊ฐ€์žฅ ๋‚ฎ์€ ํ™•๋ฅ ๊นŒ์ง€ ์„ ํƒ๋ฉ๋‹ˆ๋‹ค. ์„ ํƒ๋œ ํ† ํฐ์˜ ํ™•๋ฅ ์˜ ํ•ฉ์ด top-p ๊ฐ’๊ณผ ๊ฐ™์•„์งˆ ๋•Œ๊นŒ์ง€ ์„ ํƒ๋ฉ๋‹ˆ๋‹ค.", - "com_endpoint_import": "๊ฐ€์ ธ์˜ค๊ธฐ", "com_endpoint_instructions_assistants": "์—์ด์ „ํŠธ ์ง€์นจ ์žฌ์ •์˜", "com_endpoint_instructions_assistants_placeholder": "์–ด์‹œ์Šคํ„ดํŠธ์˜ ์ง€์นจ์„ ์žฌ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์‹คํ–‰๋งˆ๋‹ค ๋™์ž‘์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", "com_endpoint_max_output_tokens": "์ตœ๋Œ€ ์ถœ๋ ฅ ํ† ํฐ ์ˆ˜", @@ -262,7 +261,6 @@ "com_nav_archive_name": "์ด๋ฆ„", "com_nav_archived_chats": "์•„์นด์ด๋ธŒ๋œ ์ฑ„ํŒ…", "com_nav_archived_chats_empty": "์•„์นด์ด๋ธŒ๋œ ์ฑ„ํŒ…์ด ์—†์Šต๋‹ˆ๋‹ค", - "com_nav_archived_chats_manage": "๊ด€๋ฆฌ", "com_nav_at_command": "@ ๋ช…๋ น์–ด", "com_nav_at_command_description": "์—”๋“œํฌ์ธํŠธ, ๋ชจ๋ธ, ํ”„๋ฆฌ์…‹ ๋“ฑ์„ ์ „ํ™˜ํ•˜๋Š” \"@\" ๋ช…๋ น์–ด ํ† ๊ธ€", "com_nav_audio_play_error": "์˜ค๋””์˜ค ์žฌ์ƒ ์˜ค๋ฅ˜: {{0}}", @@ -383,7 +381,6 @@ "com_nav_setting_speech": "์Œ์„ฑ", "com_nav_settings": "์„ค์ •", "com_nav_shared_links": "๊ณต์œ  ๋งํฌ", - "com_nav_shared_links_manage": "๊ด€๋ฆฌ", "com_nav_show_code": "์ฝ”๋“œ ์ธํ„ฐํ”„๋ฆฌํ„ฐ ์‚ฌ์šฉ ์‹œ ํ•ญ์ƒ ์ฝ”๋“œ ํ‘œ์‹œ", "com_nav_slash_command": "์Šฌ๋ž˜์‹œ ๋ช…๋ น์–ด", "com_nav_slash_command_description": "ํ‚ค๋ณด๋“œ๋กœ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์„ ํƒํ•˜๋ ค๋ฉด \"/\" ๋ช…๋ น์–ด ํ† ๊ธ€", @@ -587,7 +584,7 @@ "com_ui_happy_birthday": "๋‚ด ์ฒซ ์ƒ์ผ์ด์•ผ!", "com_ui_host": "ํ˜ธ์ŠคํŠธ", "com_ui_image_gen": "์ด๋ฏธ์ง€ ์ƒ์„ฑ", - "com_ui_import_conversation": "๊ฐ€์ ธ์˜ค๊ธฐ", + "com_ui_import": "๊ฐ€์ ธ์˜ค๊ธฐ", "com_ui_import_conversation_error": "๋Œ€ํ™”๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋™์•ˆ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค", "com_ui_import_conversation_file_type_error": "๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค", "com_ui_import_conversation_info": "JSON ํŒŒ์ผ์—์„œ ๋Œ€ํ™” ๊ฐ€์ ธ์˜ค๊ธฐ", @@ -719,4 +716,4 @@ "com_ui_zoom": "ํ™•๋Œ€/์ถ•์†Œ", "com_user_message": "๋‹น์‹ ", "com_warning_resubmit_unsupported": "์ด ์—”๋“œํฌ์ธํŠธ์—์„œ๋Š” AI ๋ฉ”์‹œ์ง€ ์žฌ์ „์†ก์ด ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" -} \ No newline at end of file +} diff --git a/client/src/locales/nl/translation.json b/client/src/locales/nl/translation.json index 55d96bb096..f6c99423fb 100644 --- a/client/src/locales/nl/translation.json +++ b/client/src/locales/nl/translation.json @@ -93,7 +93,6 @@ "com_endpoint_google_temp": "Hogere waarden = meer willekeurig, terwijl lagere waarden = meer gericht en deterministisch. We raden aan dit of Top P te wijzigen, maar niet beide.", "com_endpoint_google_topk": "Top-k verandert hoe het model tokens selecteert voor uitvoer. Een top-k van 1 betekent dat het geselecteerde token het meest waarschijnlijk is van alle tokens in de vocabulaire van het model (ook wel 'greedy decoding' genoemd), terwijl een top-k van 3 betekent dat het volgende token wordt geselecteerd uit de 3 meest waarschijnlijke tokens (met behulp van temperatuur).", "com_endpoint_google_topp": "Top-p verandert hoe het model tokens selecteert voor uitvoer. Tokens worden geselecteerd van meest K (zie topK-parameter) waarschijnlijk tot minst waarschijnlijk totdat de som van hun kansen gelijk is aan de top-p-waarde.", - "com_endpoint_import": "Importeren", "com_endpoint_max_output_tokens": "Max. uitvoertokens", "com_endpoint_my_preset": "Mijn voorinstelling", "com_endpoint_no_presets": "Nog geen voorinstellingen, gebruik de instellingenknop om er een te maken", @@ -127,7 +126,6 @@ "com_nav_archive_name": "Naam", "com_nav_archived_chats": "Gearchiveerde chats", "com_nav_archived_chats_empty": "Geen gearchiveerde chats", - "com_nav_archived_chats_manage": "Beheren", "com_nav_auto_scroll": "Automatisch scrollen naar Nieuwste bij openen", "com_nav_balance": "Evenwicht", "com_nav_clear_all_chats": "Alle chats wissen", @@ -182,7 +180,6 @@ "com_nav_setting_general": "Algemeen", "com_nav_settings": "Instellingen", "com_nav_shared_links": "Gedeelde links", - "com_nav_shared_links_manage": "Beheren", "com_nav_theme": "Thema", "com_nav_theme_dark": "Donker", "com_nav_theme_light": "Licht", @@ -222,7 +219,7 @@ "com_ui_enter": "Invoeren", "com_ui_examples": "Voorbeelden", "com_ui_happy_birthday": "Het is mijn eerste verjaardag!", - "com_ui_import_conversation": "Importeren", + "com_ui_import": "Importeren", "com_ui_import_conversation_error": "Er is een fout opgetreden bij het importeren van je gesprekken", "com_ui_import_conversation_info": "Gesprekken importeren vanuit een JSON-bestand", "com_ui_import_conversation_success": "Gesprekken succesvol geรฏmporteerd", @@ -253,4 +250,4 @@ "com_ui_unarchive_error": "Kan conversatie niet uit archiveren", "com_ui_upload_success": "Bestand succesvol geรผpload", "com_ui_use_prompt": "Gebruik prompt" -} \ No newline at end of file +} diff --git a/client/src/locales/pl/translation.json b/client/src/locales/pl/translation.json index e9ee3d2279..30483ba8b3 100644 --- a/client/src/locales/pl/translation.json +++ b/client/src/locales/pl/translation.json @@ -161,7 +161,6 @@ "com_endpoint_google_temp": "Wyลผsze wartoล›ci oznaczajฤ… wiฤ™kszฤ… losowoล›ฤ‡, natomiast niลผsze wartoล›ci prowadzฤ… do bardziej skoncentrowanych i deterministycznych wynikรณw. Zalecamy dostosowanie tej wartoล›ci lub Top P, ale nie obu jednoczeล›nie.", "com_endpoint_google_topk": "Top-k wpล‚ywa na sposรณb, w jaki model wybiera tokeny do wygenerowania odpowiedzi. Top-k 1 oznacza, ลผe wybrany token jest najbardziej prawdopodobny spoล›rรณd wszystkich tokenรณw w sล‚owniku modelu (nazywane teลผ dekodowaniem zachล‚annym), podczas gdy top-k 3 oznacza, ลผe nastฤ™pny token jest wybierany spoล›rรณd 3 najbardziej prawdopodobnych tokenรณw (z uwzglฤ™dnieniem temperatury).", "com_endpoint_google_topp": "Top-p wpล‚ywa na sposรณb, w jaki model wybiera tokeny do wygenerowania odpowiedzi. Tokeny sฤ… wybierane od najbardziej prawdopodobnych do najmniej, aลผ suma ich prawdopodobieล„stw osiฤ…gnie wartoล›ฤ‡ top-p.", - "com_endpoint_import": "Importuj", "com_endpoint_instructions_assistants": "Nadpisz instrukcje", "com_endpoint_max_output_tokens": "Maksymalna liczba tokenรณw wyjล›ciowych", "com_endpoint_message": "Wiadomoล›ฤ‡", @@ -238,7 +237,6 @@ "com_nav_archive_name": "Nazwa", "com_nav_archived_chats": "Zarchiwizowane rozmowy", "com_nav_archived_chats_empty": "Nie masz ลผadnych zarchiwizowanych rozmรณw.", - "com_nav_archived_chats_manage": "Zarzฤ…dzaj", "com_nav_at_command": "Polecenie @", "com_nav_at_command_description": "Przeล‚ฤ…cz polecenie \"@\" do przeล‚ฤ…czania punktรณw koล„cowych, modeli, presetรณw, itp.", "com_nav_audio_play_error": "Bล‚ฤ…d odtwarzania audio: {0}", @@ -355,7 +353,6 @@ "com_nav_setting_speech": "Mowa", "com_nav_settings": "Ustawienia", "com_nav_shared_links": "Linki udostฤ™pnione", - "com_nav_shared_links_manage": "Beheren", "com_nav_show_code": "Zawsze pokazuj kod podczas uลผywania interpretera kodu", "com_nav_show_thinking": "Domyล›lnie otwieraj rozwijane menu myล›lenia", "com_nav_slash_command": "Polecenie /", @@ -565,7 +562,7 @@ "com_ui_hide_qr": "Ukryj kod QR", "com_ui_host": "Host", "com_ui_image_gen": "Generowanie obrazu", - "com_ui_import_conversation": "Importuj", + "com_ui_import": "Importuj", "com_ui_import_conversation_error": "Wystฤ…piล‚ bล‚ฤ…d podczas importowania konwersacji", "com_ui_import_conversation_file_type_error": "Nieobsล‚ugiwany typ importu", "com_ui_import_conversation_info": "Importuj konwersacje z pliku JSON", @@ -707,4 +704,4 @@ "com_ui_zoom": "Powiฤ™ksz", "com_user_message": "Ty", "com_warning_resubmit_unsupported": "Ponowne przesyล‚anie wiadomoล›ci AI nie jest obsล‚ugiwane dla tego punktu koล„cowego." -} \ No newline at end of file +} diff --git a/client/src/locales/ru/translation.json b/client/src/locales/ru/translation.json index 9580d54f54..ee5e4d0e29 100644 --- a/client/src/locales/ru/translation.json +++ b/client/src/locales/ru/translation.json @@ -178,7 +178,6 @@ "com_endpoint_google_temp": "ะ‘ะพะปะตะต ะฒั‹ัะพะบะธะต ะทะฝะฐั‡ะตะฝะธั = ะฑะพะปะตะต ัะปัƒั‡ะฐะนะฝั‹ะต ั€ะตะทัƒะปัŒั‚ะฐั‚ั‹, ะฑะพะปะตะต ะฝะธะทะบะธะต ะทะฝะฐั‡ะตะฝะธั = ะฑะพะปะตะต ั„ะพะบัƒัะธั€ะพะฒะฐะฝะฝั‹ะต ะธ ะดะตั‚ะตั€ะผะธะฝะธั€ะพะฒะฐะฝะฝั‹ะต ั€ะตะทัƒะปัŒั‚ะฐั‚ั‹. ะœั‹ ั€ะตะบะพะผะตะฝะดัƒะตะผ ะธะทะผะตะฝัั‚ัŒ ัั‚ะพ ะธะปะธ Top P, ะฝะพ ะฝะต ะพะฑะฐ ะทะฝะฐั‡ะตะฝะธั ะพะดะฝะพะฒั€ะตะผะตะฝะฝะพ.", "com_endpoint_google_topk": "Top-k ะธะทะผะตะฝัะตั‚ ั‚ะพ, ะบะฐะบ ะผะพะดะตะปัŒ ะฒั‹ะฑะธั€ะฐะตั‚ ั‚ะพะบะตะฝั‹ ะดะปั ะฒั‹ะฒะพะดะฐ. Top-k ั€ะฐะฒะฝะพะต 1 ะพะทะฝะฐั‡ะฐะตั‚, ั‡ั‚ะพ ะฒั‹ะฑะธั€ะฐะตั‚ัั ะฝะฐะธะฑะพะปะตะต ะฒะตั€ะพัั‚ะฝั‹ะน ั‚ะพะบะตะฝ ะธะท ะฒัะตะณะพ ัะปะพะฒะฐั€ั ะผะพะดะตะปะธ (ั‚ะฐะบ ะฝะฐะทั‹ะฒะฐะตะผะพะต ะถะฐะดะฝะพะต ะดะตะบะพะดะธั€ะพะฒะฐะฝะธะต), ะฐ Top-k ั€ะฐะฒะฝะพะต 3 ะพะทะฝะฐั‡ะฐะตั‚, ั‡ั‚ะพ ัะปะตะดัƒัŽั‰ะธะน ั‚ะพะบะตะฝ ะฒั‹ะฑะธั€ะฐะตั‚ัั ะธะท ั‚ั€ะตั… ะฝะฐะธะฑะพะปะตะต ะฒะตั€ะพัั‚ะฝั‹ั… ั‚ะพะบะตะฝะพะฒ (ั ะธัะฟะพะปัŒะทะพะฒะฐะฝะธะตะผ ั‚ะตะผะฟะตั€ะฐั‚ัƒั€ั‹).", "com_endpoint_google_topp": "Top-p ะธะทะผะตะฝัะตั‚ ั‚ะพ, ะบะฐะบ ะผะพะดะตะปัŒ ะฒั‹ะฑะธั€ะฐะตั‚ ั‚ะพะบะตะฝั‹ ะดะปั ะฒั‹ะฒะพะดะฐ. ะขะพะบะตะฝั‹ ะฒั‹ะฑะธั€ะฐัŽั‚ัั ะธะท ะฝะฐะธะฑะพะปะตะต ะฒะตั€ะพัั‚ะฝั‹ั… K (ัะผ. ะฟะฐั€ะฐะผะตั‚ั€ topK) ะดะพ ะฝะฐะธะผะตะฝะตะต ะฒะตั€ะพัั‚ะฝั‹ั…, ะฟะพะบะฐ ััƒะผะผะฐ ะธั… ะฒะตั€ะพัั‚ะฝะพัั‚ะตะน ะฝะต ะดะพัั‚ะธะณะฝะตั‚ ะทะฝะฐั‡ะตะฝะธั top-p.", - "com_endpoint_import": "ะ˜ะผะฟะพั€ั‚", "com_endpoint_instructions_assistants": "ะ˜ะฝัั‚ั€ัƒะบั†ะธะธ ะดะปั ะฐััะธัั‚ะตะฝั‚ะพะฒ", "com_endpoint_instructions_assistants_placeholder": "ะŸะตั€ะตะพะฟั€ะตะดะตะปัะตั‚ ะธะฝัั‚ั€ัƒะบั†ะธะธ ะดะปั ะฐััะธัั‚ะตะฝั‚ะฐ. ะญั‚ะพ ะฟะพะปะตะทะฝะพ ะดะปั ะธะทะผะตะฝะตะฝะธั ะฟะพะฒะตะดะตะฝะธั ะดะปั ะพั‚ะดะตะปัŒะฝะพะณะพ ะทะฐะฟัƒัะบะฐ.", "com_endpoint_max_output_tokens": "ะœะฐะบัะธะผะฐะปัŒะฝะพะต ะบะพะปะธั‡ะตัั‚ะฒะพ ะฒั‹ะฒะพะดะธะผั‹ั… ั‚ะพะบะตะฝะพะฒ", @@ -262,7 +261,6 @@ "com_nav_archive_name": "ะ˜ะผั", "com_nav_archived_chats": "ะั€ั…ะธะฒะธั€ะพะฒะฐะฝะฝั‹ะต ั‡ะฐั‚ั‹", "com_nav_archived_chats_empty": "ะฃ ะฒะฐั ะฝะตั‚ ะฐั€ั…ะธะฒะธั€ะพะฒะฐะฝะฝั‹ั… ั‡ะฐั‚ะพะฒ.", - "com_nav_archived_chats_manage": "ะฃะฟั€ะฐะฒะปะตะฝะธะต", "com_nav_at_command": "@-ะบะพะผะฐะฝะดะฐ", "com_nav_at_command_description": "ะŸะตั€ะตะบะปัŽั‡ะตะฝะธะต ะบะพะผะฐะฝะดั‹ \"@\" ะดะปั ะฒั‹ะฑะพั€ะฐ ัะฝะดะฟะพะธะฝั‚ะพะฒ, ะผะพะดะตะปะตะน, ะฟั€ะตัะตั‚ะพะฒ ะธ ะดั€.", "com_nav_audio_play_error": "ะžัˆะธะฑะบะฐ ะฒะพัะฟั€ะพะธะทะฒะตะดะตะฝะธั ะฐัƒะดะธะพ: {{0}}", @@ -383,7 +381,6 @@ "com_nav_setting_speech": "ะ“ะพะปะพั", "com_nav_settings": "ะะฐัั‚ั€ะพะนะบะธ", "com_nav_shared_links": "ะกะฒัะทั‹ะฒะฐะตะผั‹ะต ััั‹ะปะบะธ", - "com_nav_shared_links_manage": "ะฃะฟั€ะฐะฒะปะตะฝะธะต", "com_nav_show_code": "ะ’ัะตะณะดะฐ ะฟะพะบะฐะทั‹ะฒะฐั‚ัŒ ะบะพะด ะฟั€ะธ ะธัะฟะพะปัŒะทะพะฒะฐะฝะธะธ ะธะฝั‚ะตั€ะฟั€ะตั‚ะฐั‚ะพั€ะฐ", "com_nav_slash_command": "/-ะšะพะผะฐะฝะดะฐ", "com_nav_slash_command_description": "ะ’ั‹ะทะพะฒ ะบะพะผะฐะฝะดะฝะพะน ัั‚ั€ะพะบะธ ะบะปะฐะฒะธัˆะตะน '/' ะดะปั ะฒั‹ะฑะพั€ะฐ ะฟั€ะพะผั‚ะฐ ั ะบะปะฐะฒะธะฐั‚ัƒั€ั‹", @@ -587,7 +584,7 @@ "com_ui_happy_birthday": "ะญั‚ะพ ะผะพะน ะฟะตั€ะฒั‹ะน ะดะตะฝัŒ ั€ะพะถะดะตะฝะธั!", "com_ui_host": "ะฅะพัั‚", "com_ui_image_gen": "ะ“ะตะฝะตั€ะฐั‚ะพั€ ะธะทะพะฑั€ะฐะถะตะฝะธะน", - "com_ui_import_conversation": "ะ˜ะผะฟะพั€ั‚ะธั€ะพะฒะฐั‚ัŒ", + "com_ui_import": "ะ˜ะผะฟะพั€ั‚ะธั€ะพะฒะฐั‚ัŒ", "com_ui_import_conversation_error": "ะŸั€ะธ ะธะผะฟะพั€ั‚ะต ะฑะตัะตะด ะฟั€ะพะธะทะพัˆะปะฐ ะพัˆะธะฑะบะฐ", "com_ui_import_conversation_file_type_error": "ะะตะฟะพะดะดะตั€ะถะธะฒะฐะตะผั‹ะน ั‚ะธะฟ ะธะผะฟะพั€ั‚ะฐ", "com_ui_import_conversation_info": "ะ˜ะผะฟะพั€ั‚ะธั€ะพะฒะฐั‚ัŒ ะฑะตัะตะดั‹ ะธะท ั„ะฐะนะปะฐ JSON", @@ -719,4 +716,4 @@ "com_ui_zoom": "ะœะฐััˆั‚ะฐะฑ", "com_user_message": "ะ’ั‹", "com_warning_resubmit_unsupported": "ะŸะพะฒั‚ะพั€ะฝะฐั ะพั‚ะฟั€ะฐะฒะบะฐ ัะพะพะฑั‰ะตะฝะธั ะ˜ะ˜ ะฝะต ะฟะพะดะดะตั€ะถะธะฒะฐะตั‚ัั ะดะปั ะดะฐะฝะฝะพะน ะบะพะฝะตั‡ะฝะพะน ั‚ะพั‡ะบะธ" -} \ No newline at end of file +} diff --git a/client/src/locales/sv/translation.json b/client/src/locales/sv/translation.json index 96e37de09e..26c8cc0c55 100644 --- a/client/src/locales/sv/translation.json +++ b/client/src/locales/sv/translation.json @@ -82,7 +82,6 @@ "com_endpoint_google_temp": "Hรถgre vรคrden = mer slumpmรคssigt, medan lรคgre vรคrden = mer fokuserat och bestรคmt. Vi rekommenderar att รคndra detta eller Top P men inte bรฅda.", "com_endpoint_google_topk": "Top-k รคndrar hur modellen vรคljer tokens fรถr utdata. Ett top-k av 1 innebรคr att den valda token รคr den mest sannolika bland alla tokens i modellens vokabulรคr (kallas ocksรฅ girig avkodning), medan ett top-k av 3 innebรคr att nรคsta token vรคljs bland de 3 mest sannolika tokens (med temperatur).", "com_endpoint_google_topp": "Top-p รคndrar hur modellen vรคljer tokens fรถr utdata. Tokens vรคljs frรฅn de mest K (se topK-parameter) sannolika till de minst tills summan av deras sannolikheter nรฅr top-p-vรคrdet.", - "com_endpoint_import": "Importera", "com_endpoint_max_output_tokens": "Max utdatatokens", "com_endpoint_my_preset": "Min fรถrinstรคllning", "com_endpoint_no_presets": "Ingen fรถrinstรคllning รคnnu", @@ -115,7 +114,6 @@ "com_nav_archive_name": "Namn", "com_nav_archived_chats": "Arkiverade chattar", "com_nav_archived_chats_empty": "Du har inga arkiverade chattar.", - "com_nav_archived_chats_manage": "Hantera", "com_nav_balance": "Balans", "com_nav_clear_all_chats": "Rensa alla chattar", "com_nav_clear_conversation": "Rensa konversationer", @@ -169,7 +167,6 @@ "com_nav_setting_general": "Allmรคnt", "com_nav_settings": "Instรคllningar", "com_nav_shared_links": "Delade lรคnkar", - "com_nav_shared_links_manage": "Hantera", "com_nav_theme": "Tema", "com_nav_theme_dark": "Mรถrkt", "com_nav_theme_light": "Ljust", @@ -209,7 +206,7 @@ "com_ui_enter": "Ange", "com_ui_examples": "Exempel", "com_ui_happy_birthday": "Det รคr min fรถrsta fรถdelsedag!", - "com_ui_import_conversation": "Importera", + "com_ui_import": "Importera", "com_ui_import_conversation_error": "Det uppstod ett fel vid import av dina konversationer", "com_ui_import_conversation_info": "Importera konversationer frรฅn en JSON-fil", "com_ui_import_conversation_success": "Konversationer har importerats framgรฅngsrikt", @@ -239,4 +236,4 @@ "com_ui_unarchive_error": "Kunde inte avarkivera chatt", "com_ui_upload_success": "Uppladdningen av filen lyckades", "com_ui_use_prompt": "Anvรคnd prompt" -} \ No newline at end of file +} diff --git a/client/src/locales/tr/translation.json b/client/src/locales/tr/translation.json index cbf93c3474..a75a5422ea 100644 --- a/client/src/locales/tr/translation.json +++ b/client/src/locales/tr/translation.json @@ -179,7 +179,6 @@ "com_endpoint_google_temp": "Yรผksek deฤŸerler = daha rastgele, dรผลŸรผk deฤŸerler = daha odaklฤฑ ve belirleyici. Bu parametre ile Olasฤฑlฤฑk Kรผtรผphanesini deฤŸiลŸtirmeyi รถneririz (ikisini birden deฤŸiลŸtirmemek).", "com_endpoint_google_topk": "Top-k, modelin รงฤฑktฤฑ iรงin token seรงimini nasฤฑl yaptฤฑฤŸฤฑnฤฑ deฤŸiลŸtirir. 1 olan bir top-k, modelin kelime haznesindeki en olasฤฑ tokenin seรงildiฤŸi (aรงgรถzlรผ kod รงรถzme olarak da adlandฤฑrฤฑlฤฑr) anlamฤฑna gelirken, 3 olan bir top-k, bir sonraki tokenin en olasฤฑ รผรง token arasฤฑndan (sฤฑcaklฤฑk kullanฤฑlarak) seรงileceฤŸi anlamฤฑna gelir.", "com_endpoint_google_topp": "Olasฤฑlฤฑk Kรผtรผphanesi, modelin รงฤฑktฤฑ iรงin token seรงme ลŸeklini deฤŸiลŸtirir. Tokenler, en olasฤฑlฤฑktan (bkz. topK parametresi) en az olasฤฑya kadar seรงilir ve olasฤฑlฤฑklarฤฑ toplamฤฑ, top-p deฤŸerine eลŸit olana kadar devam eder.", - "com_endpoint_import": "ฤฐthal et", "com_endpoint_instructions_assistants": "Talimatlarฤฑ Geรงersiz Kฤฑl", "com_endpoint_instructions_assistants_placeholder": "Asistanฤฑn talimatlarฤฑnฤฑ geรงersiz kฤฑlar. Bu, davranฤฑลŸฤฑ tek tek รงalฤฑลŸma bazฤฑnda deฤŸiลŸtirmek iรงin yararlฤฑdฤฑr.", "com_endpoint_max_output_tokens": "Maksimum ร‡ฤฑktฤฑ Tokenleri", @@ -265,7 +264,6 @@ "com_nav_archive_name": "Ad", "com_nav_archived_chats": "ArลŸivlenmiลŸ sohbetler", "com_nav_archived_chats_empty": "ArลŸivlenmiลŸ konuลŸmanฤฑz yok.", - "com_nav_archived_chats_manage": "Yรถnet", "com_nav_at_command": "@-Komutu", "com_nav_at_command_description": "Uรง noktalarฤฑ, modelleri, รถn ayarlarฤฑ vb. deฤŸiลŸtirmek iรงin \"@\" komutunu aรง/kapat", "com_nav_audio_play_error": "Ses oynatma hatasฤฑ: {{0}}", @@ -388,7 +386,6 @@ "com_nav_setting_speech": "KonuลŸma", "com_nav_settings": "Ayarlar", "com_nav_shared_links": "PaylaลŸฤฑlan baฤŸlantฤฑlar", - "com_nav_shared_links_manage": "Yรถnet", "com_nav_show_code": "Kod yorumlayฤฑcฤฑ kullanฤฑrken her zaman kodu gรถster", "com_nav_show_thinking": "DรผลŸรผnme Aรงฤฑlฤฑr Menรผlerini Varsayฤฑlan Olarak Aรง", "com_nav_slash_command": "/-Komutu", @@ -605,7 +602,7 @@ "com_ui_hide_qr": "QR Kodunu Gizle", "com_ui_host": "Host", "com_ui_image_gen": "Gรถrรผntรผ OluลŸtur", - "com_ui_import_conversation": "ฤฐรงe Aktar", + "com_ui_import": "ฤฐรงe Aktar", "com_ui_import_conversation_error": "KonuลŸmalarฤฑnฤฑzฤฑ iรงe aktarma sฤฑrasฤฑnda bir hata oluลŸtu", "com_ui_import_conversation_file_type_error": "Desteklenmeyen iรงe aktarma tรผrรผ", "com_ui_import_conversation_info": "JSON dosyasฤฑndan konuลŸmalarฤฑ iรงe aktar", @@ -750,4 +747,4 @@ "com_ui_zoom": "YakฤฑnlaลŸtฤฑr", "com_user_message": "Sen", "com_warning_resubmit_unsupported": "Bu uรง nokta iรงin yapay zeka mesajฤฑnฤฑ yeniden gรถnderme desteklenmiyor." -} \ No newline at end of file +} diff --git a/client/src/locales/vi/translation.json b/client/src/locales/vi/translation.json index b7b460d1ea..57f34eef0b 100644 --- a/client/src/locales/vi/translation.json +++ b/client/src/locales/vi/translation.json @@ -84,7 +84,6 @@ "com_endpoint_google_temp": "Giรก trแป‹ cao = ngแบซu nhiรชn hฦกn, trong khi giรก trแป‹ thแบฅp = tแบญp trung vร  xรกc ฤ‘แป‹nh hฦกn. Chรบng tรดi khuyแบฟn nghแป‹ thay ฤ‘แป•i giรก trแป‹ nร y hoแบทc Top P nhฦฐng khรดng phแบฃi cแบฃ hai.", "com_endpoint_google_topk": "Top-k thay ฤ‘แป•i cรกch mรด hรฌnh chแปn mรฃ thรดng bรกo ฤ‘แปƒ xuแบฅt. Top-k lร  1 cรณ nghฤฉa lร  mรฃ thรดng bรกo ฤ‘ฦฐแปฃc chแปn lร  phแป• biแบฟn nhแบฅt trong tแบฅt cแบฃ cรกc mรฃ thรดng bรกo trong bแบฃng tแปซ vแปฑng cแปงa mรด hรฌnh (cรฒn ฤ‘ฦฐแปฃc gแปi lร  giแบฃi mรฃ tham lam), trong khi top-k lร  3 cรณ nghฤฉa lร  mรฃ thรดng bรกo tiแบฟp theo ฤ‘ฦฐแปฃc chแปn tแปซ giแปฏa 3 mรฃ thรดng bรกo phแป• biแบฟn nhแบฅt (sแปญ dแปฅng nhiแป‡t ฤ‘แป™).", "com_endpoint_google_topp": "Top-p thay ฤ‘แป•i cรกch mรด hรฌnh chแปn mรฃ thรดng bรกo ฤ‘แปƒ xuแบฅt. Mรฃ thรดng bรกo ฤ‘ฦฐแปฃc chแปn tแปซ cฤƒn cแปฉ cรณ xรกc suแบฅt cao nhแบฅt ฤ‘แบฟn thแบฅp nhแบฅt cho ฤ‘แบฟn khi tแป•ng xรกc suแบฅt cแปงa chรบng bแบฑng giรก trแป‹ top-p.", - "com_endpoint_import": "Nhแบญp", "com_endpoint_max_output_tokens": "Sแป‘ mรฃ thรดng bรกo tแป‘i ฤ‘a", "com_endpoint_my_preset": "ฤแบทt sแบตn cแปงa tรดi", "com_endpoint_no_presets": "Chฦฐa cรณ ฤ‘แบทt sแบตn", @@ -167,7 +166,6 @@ "com_nav_setting_general": "Chung", "com_nav_settings": "Cร i ฤ‘แบทt", "com_nav_shared_links": "Liรชn kแบฟt ฤ‘ฦฐแปฃc chia sแบป", - "com_nav_shared_links_manage": "Quแบฃn l", "com_nav_theme": "Chแปง ฤ‘แป", "com_nav_theme_dark": "Tแป‘i", "com_nav_theme_light": "Sรกng", @@ -207,7 +205,7 @@ "com_ui_enter": "Nhแบญp", "com_ui_examples": "Vรญ dแปฅ", "com_ui_happy_birthday": "ฤรขy lร  sinh nhแบญt ฤ‘แบงu tiรชn cแปงa tรดi!", - "com_ui_import_conversation": "Nhแบญp khแบฉu", + "com_ui_import": "Nhแบญp khแบฉu", "com_ui_import_conversation_error": "ฤรฃ xแบฃy ra lแป—i khi nhแบญp khแบฉu cuแป™c trรฒ chuyแป‡n cแปงa bแบกn", "com_ui_import_conversation_info": "Nhแบญp khแบฉu cuแป™c trรฒ chuyแป‡n tแปซ mแป™t tแป‡p JSON", "com_ui_import_conversation_success": "ฤรฃ nhแบญp khแบฉu cuแป™c trรฒ chuyแป‡n thร nh cรดng", @@ -237,4 +235,4 @@ "com_ui_unarchive_error": "Khรดng thแปƒ bแป lฦฐu trแปฏ cuแป™c trรฒ chuyแป‡n", "com_ui_upload_success": "Tแบฃi tแป‡p thร nh cรดng", "com_ui_use_prompt": "Sแปญ dแปฅng gแปฃi รฝ" -} \ No newline at end of file +} diff --git a/client/src/locales/zh-Hant/translation.json b/client/src/locales/zh-Hant/translation.json index 1913f05f56..f46d94af2d 100644 --- a/client/src/locales/zh-Hant/translation.json +++ b/client/src/locales/zh-Hant/translation.json @@ -178,7 +178,6 @@ "com_endpoint_google_temp": "่ผƒ้ซ˜็š„ๅ€ผ่กจ็คบๆ›ด้šจๆฉŸ๏ผŒ่€Œ่ผƒไฝŽ็š„ๅ€ผ่กจ็คบๆ›ด้›†ไธญๅ’Œ็ขบๅฎšใ€‚ๆˆ‘ๅ€‘ๅปบ่ญฐไฟฎๆ”น้€™ๅ€‹ๆˆ– Top P๏ผŒไฝ†ไธๅปบ่ญฐๅ…ฉ่€…้ƒฝไฟฎๆ”นใ€‚", "com_endpoint_google_topk": "Top-k ่ชฟๆ•ดๆจกๅž‹ๅฆ‚ไฝ•้ธๅ–่ผธๅ‡บ็š„ tokenใ€‚็•ถ Top-k ่จญ็‚บ 1 ๆ™‚๏ผŒๆจกๅž‹ๆœƒ้ธๅ–ๅœจๅ…ถ่ฉžๅฝ™ๅบซไธญๆฉŸ็އๆœ€้ซ˜็š„ token ้€ฒ่กŒ่ผธๅ‡บ๏ผˆ้€™ไนŸ่ขซ็จฑ็‚บ่ฒชๅฉช่งฃ็ขผ๏ผ‰ใ€‚็›ธๅฐๅœฐ๏ผŒ็•ถ Top-k ่จญ็‚บ 3 ๆ™‚๏ผŒๆจกๅž‹ๆœƒๅพžๆฉŸ็އๆœ€้ซ˜็š„ไธ‰ๅ€‹ token ไธญ้ธๅ–ไธ‹ไธ€ๅ€‹่ผธๅ‡บ token๏ผˆ้€™ๆœƒๆถ‰ๅŠๅˆฐๆ‰€่ฌ‚็š„ใ€Œๆบซๅบฆใ€่ชฟๆ•ด๏ผ‰", "com_endpoint_google_topp": "Top-p ่ชฟๆ•ดๆจกๅž‹ๅœจ่ผธๅ‡บ token ๆ™‚็š„้ธๆ“‡ๆฉŸๅˆถใ€‚ๅพžๆœ€ๅฏ่ƒฝ็š„ K๏ผˆ่ฆ‹ topK ๅƒๆ•ธ๏ผ‰้–‹ๅง‹้ธๆ“‡ token๏ผŒ็›ดๅˆฐๅฎƒๅ€‘็š„ๆฉŸ็އไน‹ๅ’Œ้”ๅˆฐ top-p ๅ€ผใ€‚", - "com_endpoint_import": "ๅŒฏๅ…ฅ", "com_endpoint_instructions_assistants": "่ฆ†ๅฏซๆ็คบๆŒ‡ไปค", "com_endpoint_instructions_assistants_placeholder": "่ฆ†ๅฏซๅŠฉ็†็š„ๆ็คบๆŒ‡ไปคใ€‚้€™ๅฐๆ–ผๅœจๆฏๆฌกๅŸท่กŒๆ™‚ไฟฎๆ”น่กŒ็‚บๅพˆๆœ‰็”จใ€‚", "com_endpoint_max_output_tokens": "ๆœ€ๅคง่ผธๅ‡บ token ๆ•ธ", @@ -262,7 +261,6 @@ "com_nav_archive_name": "ๅ็จฑ", "com_nav_archived_chats": "ๅฐๅญ˜็š„ๅฐ่ฉฑ", "com_nav_archived_chats_empty": "ๆ‚จๆฒ’ๆœ‰ไปปไฝ•ๅฐๅญ˜็š„ๅฐ่ฉฑใ€‚", - "com_nav_archived_chats_manage": "็ฎก็†", "com_nav_at_command": "@-ๆŒ‡ไปค", "com_nav_at_command_description": "ไฝฟ็”จใ€Œ@ใ€ๆŒ‡ไปคๅˆ‡ๆ›็ซฏ้ปžใ€ๆจกๅž‹ๅ’Œ้ ่จญๅ€ผ็ญ‰", "com_nav_audio_play_error": "ๆ’ญๆ”พ้Ÿณ่จŠๆ™‚็™ผ็”Ÿ้Œฏ่ชค๏ผš{{0}}", @@ -383,7 +381,6 @@ "com_nav_setting_speech": "่ชž้Ÿณ", "com_nav_settings": "่จญๅฎš", "com_nav_shared_links": "ๅ…ฑไบซ้€ฃ็ต", - "com_nav_shared_links_manage": "็ฎก็†", "com_nav_show_code": "ไธ€ๅพ‹้กฏ็คบไฝฟ็”จ็จ‹ๅผ็ขผ่งฃ่ญฏๅ™จๆ™‚็š„็จ‹ๅผ็ขผ", "com_nav_slash_command": "/ๆŒ‡ไปค", "com_nav_slash_command_description": "ไฝฟ็”จ้ต็›คๆŒ‰ไธ‹ \"/\" ๅฟซ้€Ÿ้ธๆ“‡ๆ็คบ่ฉž", @@ -587,7 +584,7 @@ "com_ui_happy_birthday": "้€™ๆ˜ฏๆˆ‘็š„็ฌฌไธ€ๅ€‹็”Ÿๆ—ฅ๏ผ", "com_ui_host": "ไธปๆฉŸ", "com_ui_image_gen": "ๅฝฑๅƒ็”Ÿๆˆ", - "com_ui_import_conversation": "ๅŒฏๅ…ฅ", + "com_ui_import": "ๅŒฏๅ…ฅ", "com_ui_import_conversation_error": "ๅŒฏๅ…ฅๅฐ่ฉฑๆ™‚็™ผ็”Ÿ้Œฏ่ชค", "com_ui_import_conversation_file_type_error": "ไธๆ”ฏๆด็š„ๅŒฏๅ…ฅๆช”ๆกˆ้กžๅž‹", "com_ui_import_conversation_info": "ๅพž JSON ๆ–‡ไปถๅŒฏๅ…ฅๅฐ่ฉฑ", @@ -719,4 +716,4 @@ "com_ui_zoom": "็ธฎๆ”พ", "com_user_message": "ๆ‚จ", "com_warning_resubmit_unsupported": "ๆญค็ซฏ้ปžไธๆ”ฏๆด้‡ๆ–ฐ้€ๅ‡บ AI ่จŠๆฏใ€‚" -} \ No newline at end of file +} diff --git a/client/src/routes/Layouts/Startup.tsx b/client/src/routes/Layouts/Startup.tsx index b00c8bcdc0..9c9e0952dd 100644 --- a/client/src/routes/Layouts/Startup.tsx +++ b/client/src/routes/Layouts/Startup.tsx @@ -10,6 +10,7 @@ const headerMap: Record = { '/register': 'com_auth_create_account', '/forgot-password': 'com_auth_reset_password', '/reset-password': 'com_auth_reset_password', + '/login/2fa': 'com_auth_verify_your_identity', }; export default function StartupLayout({ isAuthenticated }: { isAuthenticated?: boolean }) { diff --git a/client/src/routes/index.tsx b/client/src/routes/index.tsx index 3cdfe3c46e..c8bc382a42 100644 --- a/client/src/routes/index.tsx +++ b/client/src/routes/index.tsx @@ -6,6 +6,7 @@ import { ResetPassword, VerifyEmail, ApiErrorWatcher, + TwoFactorScreen, } from '~/components/Auth'; import { AuthContextProvider } from '~/hooks/AuthContext'; import RouteErrorBoundary from './RouteErrorBoundary'; @@ -66,6 +67,10 @@ export const router = createBrowserRouter([ path: 'login', element: , }, + { + path: 'login/2fa', + element: , + }, ], }, dashboardRoutes, diff --git a/package-lock.json b/package-lock.json index dfae230c57..802cedd19e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1053,6 +1053,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.0", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", @@ -1074,6 +1075,7 @@ "html-to-image": "^1.11.11", "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.3", + "input-otp": "^1.4.2", "js-cookie": "^3.0.5", "librechat-data-provider": "*", "lodash": "^4.17.21", @@ -13744,6 +13746,101 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz", + "integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-radio-group": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", @@ -23618,6 +23715,16 @@ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==" }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", diff --git a/packages/data-provider/src/api-endpoints.ts b/packages/data-provider/src/api-endpoints.ts index 27cc221d72..142ed9ba20 100644 --- a/packages/data-provider/src/api-endpoints.ts +++ b/packages/data-provider/src/api-endpoints.ts @@ -237,3 +237,11 @@ export const addTagToConversation = (conversationId: string) => export const userTerms = () => '/api/user/terms'; export const acceptUserTerms = () => '/api/user/terms/accept'; export const banner = () => '/api/banner'; + +// Two-Factor Endpoints +export const enableTwoFactor = () => '/api/auth/2fa/enable'; +export const verifyTwoFactor = () => '/api/auth/2fa/verify'; +export const confirmTwoFactor = () => '/api/auth/2fa/confirm'; +export const disableTwoFactor = () => '/api/auth/2fa/disable'; +export const regenerateBackupCodes = () => '/api/auth/2fa/backup/regenerate'; +export const verifyTwoFactorTemp = () => '/api/auth/2fa/verify-temp'; \ No newline at end of file diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 5af00fdcb9..78700e7419 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -774,3 +774,33 @@ export function acceptTerms(): Promise { export function getBanner(): Promise { return request.get(endpoints.banner()); } + +export function enableTwoFactor(): Promise { + return request.get(endpoints.enableTwoFactor()); +} + +export function verifyTwoFactor( + payload: t.TVerify2FARequest, +): Promise { + return request.post(endpoints.verifyTwoFactor(), payload); +} + +export function confirmTwoFactor( + payload: t.TVerify2FARequest, +): Promise { + return request.post(endpoints.confirmTwoFactor(), payload); +} + +export function disableTwoFactor(): Promise { + return request.post(endpoints.disableTwoFactor()); +} + +export function regenerateBackupCodes(): Promise { + return request.post(endpoints.regenerateBackupCodes()); +} + +export function verifyTwoFactorTemp( + payload: t.TVerify2FATempRequest, +): Promise { + return request.post(endpoints.verifyTwoFactorTemp(), payload); +} \ No newline at end of file diff --git a/packages/data-provider/src/keys.ts b/packages/data-provider/src/keys.ts index c1e0c24557..fd5ee95087 100644 --- a/packages/data-provider/src/keys.ts +++ b/packages/data-provider/src/keys.ts @@ -67,4 +67,6 @@ export enum MutationKeys { deleteAgentAction = 'deleteAgentAction', deleteUser = 'deleteUser', updateRole = 'updateRole', + enableTwoFactor = 'enableTwoFactor', + verifyTwoFactor = 'verifyTwoFactor', } diff --git a/packages/data-provider/src/request.ts b/packages/data-provider/src/request.ts index 740e9cbe6c..e4dd53847b 100644 --- a/packages/data-provider/src/request.ts +++ b/packages/data-provider/src/request.ts @@ -91,6 +91,9 @@ axios.interceptors.response.use( return Promise.reject(error); } + if (originalRequest.url?.includes('/api/auth/2fa') === true) { + return Promise.reject(error); + } if (originalRequest.url?.includes('/api/auth/logout') === true) { return Promise.reject(error); } diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index bf31a48cc0..6771901267 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -100,6 +100,12 @@ export type TError = { }; }; +export type TBackupCode = { + codeHash: string; + used: boolean; + usedAt: Date | null; +}; + export type TUser = { id: string; username: string; @@ -109,6 +115,7 @@ export type TUser = { role: string; provider: string; plugins?: string[]; + backupCodes?: TBackupCode[]; createdAt: string; updatedAt: string; }; @@ -285,11 +292,61 @@ export type TRegisterUser = { export type TLoginUser = { email: string; password: string; + token?: string; + backupCode?: string; }; export type TLoginResponse = { - token: string; - user: TUser; + token?: string; + user?: TUser; + twoFAPending?: boolean; + tempToken?: string; +}; + +export type TEnable2FAResponse = { + otpauthUrl: string; + backupCodes: string[]; + message?: string; +}; + +export type TVerify2FARequest = { + token?: string; + backupCode?: string; +}; + +export type TVerify2FAResponse = { + message: string; +}; + +/** + * For verifying 2FA during login with a temporary token. + */ +export type TVerify2FATempRequest = { + tempToken: string; + token?: string; + backupCode?: string; +}; + +export type TVerify2FATempResponse = { + token?: string; + user?: TUser; + message?: string; +}; + +/** + * Response from disabling 2FA. + */ +export type TDisable2FAResponse = { + message: string; +}; + +/** + * Response from regenerating backup codes. + */ +export type TRegenerateBackupCodesResponse = { + message: string; + backupCodes: string[]; + backupCodesHash: string[]; }; export type TRequestPasswordReset = { From 964a74c73bb76c04da63a146190bf280b3c07d5e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 17 Feb 2025 19:37:03 -0500 Subject: [PATCH 05/32] =?UTF-8?q?=F0=9F=9B=A0=20refactor:=20Ensure=20File?= =?UTF-8?q?=20Deletions,=20File=20Naming,=20and=20Agent=20Resource=20Updat?= =?UTF-8?q?es=20(#5928)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Improve error logging for file upload and processing functions to prevent verbosity * refactor: Add uploads directory to Docker Compose to persist file uploads * refactor: `addAgentResourceFile` to handle edge case of non-existing `tool_resource` array * refactor: Remove version specification from deploy-compose.yml * refactor: Prefix filenames with file_id to ensure uniqueness in file uploads * refactor: Enhance error handling in deleteVectors to log warnings for non-404 errors * refactor: Limit file search results to top 5 based on relevance score * ๐ŸŒ i18n: Update translation.json with latest translations --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- api/app/clients/tools/util/fileSearch.js | 3 +- api/models/Agent.js | 16 +- api/models/Agent.spec.js | 160 ++++++++++++++++++++ api/server/services/Files/Code/crud.js | 7 +- api/server/services/Files/Code/process.js | 11 +- api/server/services/Files/VectorDB/crud.js | 9 +- api/server/services/Files/process.js | 7 +- client/src/locales/ar/translation.json | 3 +- client/src/locales/de/translation.json | 3 +- client/src/locales/en/translation.json | 80 +++++----- client/src/locales/es/translation.json | 3 +- client/src/locales/et/translation.json | 5 +- client/src/locales/fi/translation.json | 3 +- client/src/locales/fr/translation.json | 3 +- client/src/locales/he/translation.json | 3 +- client/src/locales/id/translation.json | 3 +- client/src/locales/it/translation.json | 3 +- client/src/locales/ja/translation.json | 3 +- client/src/locales/ko/translation.json | 3 +- client/src/locales/nl/translation.json | 3 +- client/src/locales/pl/translation.json | 3 +- client/src/locales/pt-BR/translation.json | 4 - client/src/locales/pt-PT/translation.json | 4 - client/src/locales/ru/translation.json | 3 +- client/src/locales/sv/translation.json | 3 +- client/src/locales/tr/translation.json | 3 +- client/src/locales/vi/translation.json | 3 +- client/src/locales/zh-Hans/translation.json | 4 - client/src/locales/zh-Hant/translation.json | 3 +- deploy-compose.yml | 2 +- docker-compose.yml | 1 + 31 files changed, 262 insertions(+), 102 deletions(-) create mode 100644 api/models/Agent.spec.js diff --git a/api/app/clients/tools/util/fileSearch.js b/api/app/clients/tools/util/fileSearch.js index 23ba58bb5a..c48adc2eb4 100644 --- a/api/app/clients/tools/util/fileSearch.js +++ b/api/app/clients/tools/util/fileSearch.js @@ -112,7 +112,8 @@ const createFileSearchTool = async ({ req, files, entity_id }) => { relevanceScore, })), ) - .sort((a, b) => b.relevanceScore - a.relevanceScore); + .sort((a, b) => b.relevanceScore - a.relevanceScore) + .slice(0, 5); const formattedString = formattedResults .map( diff --git a/api/models/Agent.js b/api/models/Agent.js index 6fa00f56bc..6ea203113c 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -97,11 +97,22 @@ const updateAgent = async (searchParameter, updateData) => { const addAgentResourceFile = async ({ agent_id, tool_resource, file_id }) => { const searchParameter = { id: agent_id }; - // build the update to push or create the file ids set const fileIdsPath = `tool_resources.${tool_resource}.file_ids`; + + await Agent.updateOne( + { + id: agent_id, + [`${fileIdsPath}`]: { $exists: false }, + }, + { + $set: { + [`${fileIdsPath}`]: [], + }, + }, + ); + const updateData = { $addToSet: { [fileIdsPath]: file_id } }; - // return the updated agent or throw if no agent matches const updatedAgent = await updateAgent(searchParameter, updateData); if (updatedAgent) { return updatedAgent; @@ -290,6 +301,7 @@ const updateAgentProjects = async ({ user, agentId, projectIds, removeProjectIds }; module.exports = { + Agent, getAgent, loadAgent, createAgent, diff --git a/api/models/Agent.spec.js b/api/models/Agent.spec.js new file mode 100644 index 0000000000..769eda2bb7 --- /dev/null +++ b/api/models/Agent.spec.js @@ -0,0 +1,160 @@ +const mongoose = require('mongoose'); +const { v4: uuidv4 } = require('uuid'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { Agent, addAgentResourceFile, removeAgentResourceFiles } = require('./Agent'); + +describe('Agent Resource File Operations', () => { + let mongoServer; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + await Agent.deleteMany({}); + }); + + const createBasicAgent = async () => { + const agentId = `agent_${uuidv4()}`; + const agent = await Agent.create({ + id: agentId, + name: 'Test Agent', + provider: 'test', + model: 'test-model', + author: new mongoose.Types.ObjectId(), + }); + return agent; + }; + + test('should handle concurrent file additions', async () => { + const agent = await createBasicAgent(); + const fileIds = Array.from({ length: 10 }, () => uuidv4()); + + // Concurrent additions + const additionPromises = fileIds.map((fileId) => + addAgentResourceFile({ + agent_id: agent.id, + tool_resource: 'test_tool', + file_id: fileId, + }), + ); + + await Promise.all(additionPromises); + + const updatedAgent = await Agent.findOne({ id: agent.id }); + expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined(); + expect(updatedAgent.tool_resources.test_tool.file_ids).toHaveLength(10); + expect(new Set(updatedAgent.tool_resources.test_tool.file_ids).size).toBe(10); + }); + + test('should handle concurrent additions and removals', async () => { + const agent = await createBasicAgent(); + const initialFileIds = Array.from({ length: 5 }, () => uuidv4()); + + await Promise.all( + initialFileIds.map((fileId) => + addAgentResourceFile({ + agent_id: agent.id, + tool_resource: 'test_tool', + file_id: fileId, + }), + ), + ); + + const newFileIds = Array.from({ length: 5 }, () => uuidv4()); + const operations = [ + ...newFileIds.map((fileId) => + addAgentResourceFile({ + agent_id: agent.id, + tool_resource: 'test_tool', + file_id: fileId, + }), + ), + ...initialFileIds.map((fileId) => + removeAgentResourceFiles({ + agent_id: agent.id, + files: [{ tool_resource: 'test_tool', file_id: fileId }], + }), + ), + ]; + + await Promise.all(operations); + + const updatedAgent = await Agent.findOne({ id: agent.id }); + expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined(); + expect(updatedAgent.tool_resources.test_tool.file_ids).toHaveLength(5); + }); + + test('should initialize array when adding to non-existent tool resource', async () => { + const agent = await createBasicAgent(); + const fileId = uuidv4(); + + const updatedAgent = await addAgentResourceFile({ + agent_id: agent.id, + tool_resource: 'new_tool', + file_id: fileId, + }); + + expect(updatedAgent.tool_resources.new_tool.file_ids).toBeDefined(); + expect(updatedAgent.tool_resources.new_tool.file_ids).toHaveLength(1); + expect(updatedAgent.tool_resources.new_tool.file_ids[0]).toBe(fileId); + }); + + test('should handle rapid sequential modifications to same tool resource', async () => { + const agent = await createBasicAgent(); + const fileId = uuidv4(); + + for (let i = 0; i < 10; i++) { + await addAgentResourceFile({ + agent_id: agent.id, + tool_resource: 'test_tool', + file_id: `${fileId}_${i}`, + }); + + if (i % 2 === 0) { + await removeAgentResourceFiles({ + agent_id: agent.id, + files: [{ tool_resource: 'test_tool', file_id: `${fileId}_${i}` }], + }); + } + } + + const updatedAgent = await Agent.findOne({ id: agent.id }); + expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined(); + expect(Array.isArray(updatedAgent.tool_resources.test_tool.file_ids)).toBe(true); + }); + + test('should handle multiple tool resources concurrently', async () => { + const agent = await createBasicAgent(); + const toolResources = ['tool1', 'tool2', 'tool3']; + const operations = []; + + toolResources.forEach((tool) => { + const fileIds = Array.from({ length: 5 }, () => uuidv4()); + fileIds.forEach((fileId) => { + operations.push( + addAgentResourceFile({ + agent_id: agent.id, + tool_resource: tool, + file_id: fileId, + }), + ); + }); + }); + + await Promise.all(operations); + + const updatedAgent = await Agent.findOne({ id: agent.id }); + toolResources.forEach((tool) => { + expect(updatedAgent.tool_resources[tool].file_ids).toBeDefined(); + expect(updatedAgent.tool_resources[tool].file_ids).toHaveLength(5); + }); + }); +}); diff --git a/api/server/services/Files/Code/crud.js b/api/server/services/Files/Code/crud.js index 076a4d9f13..7b26093d62 100644 --- a/api/server/services/Files/Code/crud.js +++ b/api/server/services/Files/Code/crud.js @@ -2,6 +2,7 @@ const axios = require('axios'); const FormData = require('form-data'); const { getCodeBaseURL } = require('@librechat/agents'); +const { logAxiosError } = require('~/utils'); const MAX_FILE_SIZE = 150 * 1024 * 1024; @@ -78,7 +79,11 @@ async function uploadCodeEnvFile({ req, stream, filename, apiKey, entity_id = '' return `${fileIdentifier}?entity_id=${entity_id}`; } catch (error) { - throw new Error(`Error uploading file: ${error.message}`); + logAxiosError({ + message: `Error uploading code environment file: ${error.message}`, + error, + }); + throw new Error(`Error uploading code environment file: ${error.message}`); } } diff --git a/api/server/services/Files/Code/process.js b/api/server/services/Files/Code/process.js index 2a941a4647..ce8acf4ad3 100644 --- a/api/server/services/Files/Code/process.js +++ b/api/server/services/Files/Code/process.js @@ -12,6 +12,7 @@ const { const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { convertImage } = require('~/server/services/Files/images/convert'); const { createFile, getFiles, updateFile } = require('~/models/File'); +const { logAxiosError } = require('~/utils'); const { logger } = require('~/config'); /** @@ -85,7 +86,10 @@ const processCodeOutput = async ({ /** Note: `messageId` & `toolCallId` are not part of file DB schema; message object records associated file ID */ return Object.assign(file, { messageId, toolCallId }); } catch (error) { - logger.error('Error downloading file:', error); + logAxiosError({ + message: 'Error downloading code environment file', + error, + }); } }; @@ -135,7 +139,10 @@ async function getSessionInfo(fileIdentifier, apiKey) { return response.data.find((file) => file.name.startsWith(path))?.lastModified; } catch (error) { - logger.error(`Error fetching session info: ${error.message}`, error); + logAxiosError({ + message: `Error fetching session info: ${error.message}`, + error, + }); return null; } } diff --git a/api/server/services/Files/VectorDB/crud.js b/api/server/services/Files/VectorDB/crud.js index d290eea4b1..37a1e81487 100644 --- a/api/server/services/Files/VectorDB/crud.js +++ b/api/server/services/Files/VectorDB/crud.js @@ -37,7 +37,14 @@ const deleteVectors = async (req, file) => { error, message: 'Error deleting vectors', }); - throw new Error(error.message || 'An error occurred during file deletion.'); + if ( + error.response && + error.response.status !== 404 && + (error.response.status < 200 || error.response.status >= 300) + ) { + logger.warn('Error deleting vectors, file will not be deleted'); + throw new Error(error.message || 'An error occurred during file deletion.'); + } } }; diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index a5d9c8c1e0..8744eb409b 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -347,8 +347,8 @@ const uploadImageBuffer = async ({ req, context, metadata = {}, resize = true }) req.app.locals.imageOutputType }`; } - - const filepath = await saveBuffer({ userId: req.user.id, fileName: filename, buffer }); + const fileName = `${file_id}-${filename}`; + const filepath = await saveBuffer({ userId: req.user.id, fileName, buffer }); return await createFile( { user: req.user.id, @@ -801,8 +801,7 @@ async function saveBase64Image( { req, file_id: _file_id, filename: _filename, endpoint, context, resolution = 'high' }, ) { const file_id = _file_id ?? v4(); - - let filename = _filename; + let filename = `${file_id}-${_filename}`; const { buffer: inputBuffer, type } = base64ToBuffer(url); if (!path.extname(_filename)) { const extension = mime.getExtension(type); diff --git a/client/src/locales/ar/translation.json b/client/src/locales/ar/translation.json index f0f9f25ebd..cfee892483 100644 --- a/client/src/locales/ar/translation.json +++ b/client/src/locales/ar/translation.json @@ -584,7 +584,6 @@ "com_ui_happy_birthday": "ุฅู†ู‡ ุนูŠุฏ ู…ูŠู„ุงุฏูŠ ุงู„ุฃูˆู„!", "com_ui_host": "ู…ูุถูŠู", "com_ui_image_gen": "ุชูˆู„ูŠุฏ ุงู„ุตูˆุฑ", - "com_ui_import": "ุงุณุชูŠุฑุงุฏ", "com_ui_import_conversation_error": "ุญุฏุซ ุฎุทุฃ ุฃุซู†ุงุก ุงุณุชูŠุฑุงุฏ ู…ุญุงุฏุซุงุชูƒ", "com_ui_import_conversation_file_type_error": "ู†ูˆุน ุงู„ู…ู„ู ุบูŠุฑ ู…ุฏุนูˆู… ู„ู„ุงุณุชูŠุฑุงุฏ", "com_ui_import_conversation_info": "ุงุณุชูŠุฑุงุฏ ู…ุญุงุฏุซุงุช ู…ู† ู…ู„ู JSON", @@ -716,4 +715,4 @@ "com_ui_zoom": "ุชูƒุจูŠุฑ", "com_user_message": "ุฃู†ุช", "com_warning_resubmit_unsupported": "ุฅุนุงุฏุฉ ุฅุฑุณุงู„ ุฑุณุงู„ุฉ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ ุบูŠุฑ ู…ุฏุนูˆู…ุฉ ู„ู†ู‚ุทุฉ ุงู„ู†ู‡ุงูŠุฉ ู‡ุฐู‡" -} +} \ No newline at end of file diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index 98cd14b865..de986df0b0 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -614,7 +614,6 @@ "com_ui_hide_qr": "QR-Code ausblenden", "com_ui_host": "Host", "com_ui_image_gen": "Bildgenerierung", - "com_ui_import": "Importieren", "com_ui_import_conversation_error": "Beim Importieren Ihrer Konversationen ist ein Fehler aufgetreten", "com_ui_import_conversation_file_type_error": "Nicht unterstรผtzter Importtyp", "com_ui_import_conversation_info": "Konversationen aus einer JSON-Datei importieren", @@ -764,4 +763,4 @@ "com_ui_zoom": "Zoom", "com_user_message": "Du", "com_warning_resubmit_unsupported": "Das erneute Senden der KI-Nachricht wird fรผr diesen Endpunkt nicht unterstรผtzt." -} +} \ No newline at end of file diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 63272a54f4..caa1496969 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1,6 +1,6 @@ { - "chat_direction_left_to_right": "Chat direction is now left to right", - "chat_direction_right_to_left": "Chat direction is now right to left", + "chat_direction_left_to_right": "something needs to go here. was empty", + "chat_direction_right_to_left": "something needs to go here. was empty", "com_a11y_ai_composing": "The AI is still composing.", "com_a11y_end": "The AI has finished their reply.", "com_a11y_start": "The AI has started their reply.", @@ -124,9 +124,11 @@ "com_auth_submit_registration": "Submit registration", "com_auth_to_reset_your_password": "to reset your password.", "com_auth_to_try_again": "to try again.", + "com_auth_two_factor": "Check your preferred one-time password application for a code", "com_auth_username": "Username (optional)", "com_auth_username_max_length": "Username must be less than 20 characters", "com_auth_username_min_length": "Username must be at least 2 characters", + "com_auth_verify_your_identity": "Verify Your Identity", "com_auth_welcome_back": "Welcome back", "com_click_to_download": "(click here to download)", "com_download_expired": "(download expired)", @@ -263,9 +265,10 @@ "com_files_filter": "Filter files...", "com_files_no_results": "No results.", "com_files_number_selected": "{{0}} of {{1}} items(s) selected", - "com_files_table": "Files Table", + "com_files_table": "something needs to go here. was empty", "com_generated_files": "Generated files:", "com_hide_examples": "Hide Examples", + "com_nav_2fa": "Two-Factor Authentication (2FA)", "com_nav_account_settings": "Account Settings", "com_nav_always_make_prod": "Always make new versions production", "com_nav_archive_created_at": "Date Archived", @@ -435,9 +438,16 @@ "com_sidepanel_parameters": "Parameters", "com_sidepanel_select_agent": "Select an Agent", "com_sidepanel_select_assistant": "Select an Assistant", - "com_nav_2fa": "Two-Factor Authentication (2FA)", - "com_auth_verify_your_identity": "Verify Your Identity", - "com_auth_two_factor": "Check your preferred one-time password application for a code", + "com_ui_2fa_account_security": "Two-factor authentication adds an extra layer of security to your account", + "com_ui_2fa_disable": "Disable 2FA", + "com_ui_2fa_disable_error": "There was an error disabling two-factor authentication", + "com_ui_2fa_disabled": "2FA has been disabled", + "com_ui_2fa_enable": "Enable 2FA", + "com_ui_2fa_enabled": "2FA has been enabled", + "com_ui_2fa_generate_error": "There was an error generating two-factor authentication settings", + "com_ui_2fa_invalid": "Invalid two-factor authentication code", + "com_ui_2fa_setup": "Setup 2FA", + "com_ui_2fa_verified": "Successfully verified Two-Factor Authentication", "com_ui_accept": "I accept", "com_ui_add": "Add", "com_ui_add_model_preset": "Add a model or preset for an additional response", @@ -488,6 +498,9 @@ "com_ui_azure": "Azure", "com_ui_back_to_chat": "Back to Chat", "com_ui_back_to_prompts": "Back to Prompts", + "com_ui_backup_codes": "Backup Codes", + "com_ui_backup_codes_regenerate_error": "There was an error regenerating backup codes", + "com_ui_backup_codes_regenerated": "Backup codes have been regenerated successfully", "com_ui_basic": "Basic", "com_ui_basic_auth_header": "Basic authorization header", "com_ui_bearer": "Bearer", @@ -524,6 +537,7 @@ "com_ui_collapse_chat": "Collapse Chat", "com_ui_command_placeholder": "Optional: Enter a command for the prompt or name will be used", "com_ui_command_usage_placeholder": "Select a Prompt by command or name", + "com_ui_complete_setup": "Complete Setup", "com_ui_confirm_action": "Confirm Action", "com_ui_confirm_admin_use_change": "Changing this setting will block access for admins, including yourself. Are you sure you want to proceed?", "com_ui_confirm_change": "Confirm Change", @@ -577,8 +591,11 @@ "com_ui_descending": "Desc", "com_ui_description": "Description", "com_ui_description_placeholder": "Optional: Enter a description to display for the prompt", + "com_ui_disabling": "Disabling...", "com_ui_download": "Download", "com_ui_download_artifact": "Download Artifact", + "com_ui_download_backup": "Download Backup Codes", + "com_ui_download_backup_tooltip": "Before you continue, download your backup codes. You will need them to regain access if you lose your authenticator device", "com_ui_download_error": "Error downloading file. The file may have been deleted.", "com_ui_drag_drop": "something needs to go here. was empty", "com_ui_dropdown_variables": "Dropdown variables:", @@ -627,7 +644,10 @@ "com_ui_fork_split_target_setting": "Start fork from target message by default", "com_ui_fork_success": "Successfully forked conversation", "com_ui_fork_visible": "Visible messages only", - "com_ui_global_group": "Global Group", + "com_ui_generate_backup": "Generate Backup Codes", + "com_ui_generate_qrcode": "Generate QR Code", + "com_ui_generating": "Generating...", + "com_ui_global_group": "something needs to go here. was empty", "com_ui_go_back": "Go back", "com_ui_go_to_conversation": "Go to conversation", "com_ui_happy_birthday": "It's my 1st birthday!", @@ -668,14 +688,16 @@ "com_ui_new_chat": "New chat", "com_ui_next": "Next", "com_ui_no": "No", + "com_ui_no_backup_codes": "No backup codes available. Please generate new ones", "com_ui_no_bookmarks": "it seems like you have no bookmarks yet. Click on a chat and add a new one", "com_ui_no_category": "No category", "com_ui_no_changes": "No changes to update", - "com_ui_no_data": "No data", + "com_ui_no_data": "something needs to go here. was empty", "com_ui_no_terms_content": "No terms and conditions content to display", - "com_ui_no_valid_items": "No valid items", + "com_ui_no_valid_items": "something needs to go here. was empty", "com_ui_none": "None", "com_ui_none_selected": "None selected", + "com_ui_not_used": "Not Used", "com_ui_nothing_found": "Nothing found", "com_ui_oauth": "OAuth", "com_ui_of": "of", @@ -703,6 +725,8 @@ "com_ui_read_aloud": "Read aloud", "com_ui_refresh_link": "Refresh link", "com_ui_regenerate": "Regenerate", + "com_ui_regenerate_backup": "Regenerate Backup Codes", + "com_ui_regenerating": "Regenerating...", "com_ui_region": "Region", "com_ui_rename": "Rename", "com_ui_rename_prompt": "Rename Prompt", @@ -725,6 +749,7 @@ "com_ui_schema": "Schema", "com_ui_scope": "Scope", "com_ui_search": "Search", + "com_ui_secret_key": "Secret Key", "com_ui_select": "Select", "com_ui_select_file": "Select a file", "com_ui_select_model": "Select a model", @@ -749,6 +774,7 @@ "com_ui_shared_link_not_found": "Shared link not found", "com_ui_shared_prompts": "Shared Prompts", "com_ui_shop": "Shopping", + "com_ui_show": "Show", "com_ui_show_all": "Show All", "com_ui_show_qr": "Show QR Code", "com_ui_sign_in_to_domain": "Sign-in to {{0}}", @@ -786,46 +812,20 @@ "com_ui_upload_invalid_var": "Invalid file for upload. Must be an image not exceeding {{0}} MB", "com_ui_upload_success": "Successfully uploaded file", "com_ui_upload_type": "Select Upload Type", + "com_ui_use_2fa_code": "Use 2FA Code Instead", + "com_ui_use_backup_code": "Use Backup Code Instead", "com_ui_use_micrphone": "Use microphone", "com_ui_use_prompt": "Use prompt", + "com_ui_used": "Used", "com_ui_variables": "Variables", "com_ui_variables_info": "Use double braces in your text to create variables, e.g. `{{example variable}}`, to later fill when using the prompt.", + "com_ui_verify": "Verify", "com_ui_version_var": "Version {{0}}", "com_ui_versions": "Versions", "com_ui_view_source": "View source chat", "com_ui_write": "Writing", "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", - "com_ui_secret_key": "Secret Key", - "com_ui_2fa_account_security": "Two-factor authentication adds an extra layer of security to your account", - "com_ui_2fa_generate_error": "There was an error generating two-factor authentication settings", - "com_ui_backup_codes": "Backup Codes", - "com_ui_2fa_invalid": "Invalid two-factor authentication code", - "com_ui_2fa_setup": "Setup 2FA", - "com_ui_2fa_enable": "Enable 2FA", - "com_ui_2fa_disable": "Disable 2FA", - "com_ui_disabling": "Disabling...", - "com_ui_2fa_enabled": "2FA has been enabled", - "com_ui_2fa_disabled": "2FA has been disabled", - "com_ui_download_backup": "Download Backup Codes", - "com_ui_use_backup_code": "Use Backup Code Instead", - "com_ui_use_2fa_code": "Use 2FA Code Instead", - "com_ui_verify": "Verify", - "com_ui_2fa_disable_error": "There was an error disabling two-factor authentication", - "com_ui_2fa_verified": "Successfully verified Two-Factor Authentication", - "com_ui_generate_backup": "Generate Backup Codes", - "com_ui_regenerate_backup": "Regenerate Backup Codes", - "com_ui_regenerating": "Regenerating...", - "com_ui_used": "Used", - "com_ui_not_used": "Not Used", - "com_ui_backup_codes_regenerated": "Backup codes have been regenerated successfully", - "com_ui_backup_codes_regenerate_error": "There was an error regenerating backup codes", - "com_ui_no_backup_codes": "No backup codes available. Please generate new ones", - "com_ui_generating": "Generating...", - "com_ui_generate_qrcode": "Generate QR Code", - "com_ui_complete_setup": "Complete Setup", - "com_ui_download_backup_tooltip": "Before you continue, download your backup codes. You will need them to regain access if you lose your authenticator device", - "com_ui_show": "Show", "com_user_message": "You", "com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint." -} +} \ No newline at end of file diff --git a/client/src/locales/es/translation.json b/client/src/locales/es/translation.json index 3355d79df4..fb1e0220d3 100644 --- a/client/src/locales/es/translation.json +++ b/client/src/locales/es/translation.json @@ -584,7 +584,6 @@ "com_ui_happy_birthday": "ยกEs mi primer cumpleaรฑos!", "com_ui_host": "Host", "com_ui_image_gen": "Gen Imรกgenes", - "com_ui_import": "Importar", "com_ui_import_conversation_error": "Hubo un error al importar tus chats", "com_ui_import_conversation_file_type_error": "com_ui_import_conversation_file_type_error: Tipo de archivo no compatible para importar", "com_ui_import_conversation_info": "Importar chats de un archivo JSON", @@ -716,4 +715,4 @@ "com_ui_zoom": "Zoom", "com_user_message": "Usted", "com_warning_resubmit_unsupported": "No se admite el reenvรญo del mensaje de IA para este punto de conexiรณn." -} +} \ No newline at end of file diff --git a/client/src/locales/et/translation.json b/client/src/locales/et/translation.json index 84d40e80f6..6eec2e8956 100644 --- a/client/src/locales/et/translation.json +++ b/client/src/locales/et/translation.json @@ -184,7 +184,6 @@ "com_endpoint_google_temp": "Kรตrgemad vรครคrtused = juhuslikum, samas kui madalamad vรครคrtused = keskendunum ja deterministlikum. Soovitame muuta kas seda vรตi Top P-d, aga mitte mรตlemat.", "com_endpoint_google_topk": "Top-k muudab seda, kuidas mudel valib vรคljundi jaoks mรคrgid. Top-k vรครคrtus 1 tรคhendab, et valitud mรคrk on kรตige tรตenรคolisem kรตigi mudeli sรตnavaras olevate mรคrkide seas (nimetatakse ka ahneks dekodeerimiseks), samas kui top-k vรครคrtus 3 tรคhendab, et jรคrgmine mรคrk valitakse 3 kรตige tรตenรคolisema mรคrgi seast (kasutades temperatuuri).", "com_endpoint_google_topp": "Top-p muudab seda, kuidas mudel valib vรคljundi jaoks mรคrgid. Mรคrgid valitakse kรตige tรตenรคolisemast K (vt parameetrit topK) kuni vรคhim tรตenรคoliseni, kuni nende tรตenรคosuste summa on vรตrdne top-p vรครคrtusega.", - "com_endpoint_import": "Impordi", "com_endpoint_instructions_assistants": "Tรผhista juhised", "com_endpoint_instructions_assistants_placeholder": "Tรผhistab assistendi juhised. See on kasulik kรคitumise muutmiseks kรคivituse kohta.", "com_endpoint_max_output_tokens": "Maksimaalsed vรคljundmรคrgid", @@ -272,7 +271,6 @@ "com_nav_archive_name": "Nimi", "com_nav_archived_chats": "Arhiveeritud vestlused", "com_nav_archived_chats_empty": "Sul ei ole arhiveeritud vestlusi.", - "com_nav_archived_chats_manage": "Halda", "com_nav_at_command": "@-kรคsk", "com_nav_at_command_description": "Lรผlita kรคsk \"@\" sisse/vรคlja lรตpp-punktide, mudelite, eelseadistuste jms vahetamiseks.", "com_nav_audio_play_error": "Viga heli esitamisel: {{0}}", @@ -630,7 +628,6 @@ "com_ui_hide_qr": "Peida QR-kood", "com_ui_host": "Host", "com_ui_image_gen": "Pildi genereerimine", - "com_ui_import_conversation": "Impordi", "com_ui_import_conversation_error": "Vestluste importimisel tekkis viga", "com_ui_import_conversation_file_type_error": "Toetamatu imporditรผรผp", "com_ui_import_conversation_info": "Impordi vestlused JSON-failist", @@ -786,4 +783,4 @@ "com_ui_zoom": "Suumi", "com_user_message": "Sina", "com_warning_resubmit_unsupported": "AI sรตnumi uuesti esitamine pole selle otspunkti jaoks toetatud." -} +} \ No newline at end of file diff --git a/client/src/locales/fi/translation.json b/client/src/locales/fi/translation.json index 190d344026..270672475b 100644 --- a/client/src/locales/fi/translation.json +++ b/client/src/locales/fi/translation.json @@ -458,7 +458,6 @@ "com_ui_happy_birthday": "On 1. syntymรคpรคivรคni!", "com_ui_host": "Host", "com_ui_image_gen": "Kuvanluonti", - "com_ui_import": "Tuo", "com_ui_import_conversation_error": "Keskustelujesi tuonnissa tapahtui virhe", "com_ui_import_conversation_file_type_error": "Tiedostotyyppi ei ole tuettu tuonnissa", "com_ui_import_conversation_info": "Tuo keskusteluja JSON-tiedostosta", @@ -551,4 +550,4 @@ "com_ui_versions": "Versiot", "com_ui_yes": "Kyllรค", "com_user_message": "Sinรค" -} +} \ No newline at end of file diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index b1bd434c9d..b06cb11e50 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -599,7 +599,6 @@ "com_ui_hide_qr": "Cacher le code QR", "com_ui_host": "Hรดte", "com_ui_image_gen": "Gรฉnรฉration d'image", - "com_ui_import": "Importer", "com_ui_import_conversation_error": "Une erreur s'est produite lors de l'importation de vos conversations", "com_ui_import_conversation_file_type_error": "Type de fichier non pris en charge pour l'importation", "com_ui_import_conversation_info": "Importer des conversations ร  partir d'un fichier JSON", @@ -733,4 +732,4 @@ "com_ui_zoom": "Zoom", "com_user_message": "Vous", "com_warning_resubmit_unsupported": "La resoumission du message IA n'est pas prise en charge pour ce point de terminaison." -} +} \ No newline at end of file diff --git a/client/src/locales/he/translation.json b/client/src/locales/he/translation.json index 456d3d33e0..4f8afc7b1d 100644 --- a/client/src/locales/he/translation.json +++ b/client/src/locales/he/translation.json @@ -291,7 +291,6 @@ "com_ui_error": "ืฉื’ื™ืื”", "com_ui_examples": "ื“ื•ื’ืžืื•ืช", "com_ui_happy_birthday": "ื–ื” ื™ื•ื ื”ื”ื•ืœื“ืช ื”ืจืืฉื•ืŸ ืฉืœื™!", - "com_ui_import": "ื™ื‘ื•ื", "com_ui_import_conversation_error": "ืื™ืจืขื” ืฉื’ื™ืื” ื‘ืขืช ื™ื™ื‘ื•ื ื”ืฉื™ื—ื•ืช ืฉืœืš", "com_ui_import_conversation_info": "ื™ื™ื‘ื ืฉื™ื—ื•ืช ืžืงื•ื‘ืฅ JSON", "com_ui_import_conversation_success": "ื”ืฉื™ื—ื•ืช ื™ื•ื‘ืื• ื‘ื”ืฆืœื—ื”", @@ -331,4 +330,4 @@ "com_ui_upload_success": "ืงื•ื‘ืฅ ืฉื”ื•ืขืœื” ื‘ื”ืฆืœื—ื”", "com_ui_use_prompt": "ื”ืฉืชืžืฉ ื‘ื”ื•ื“ืขืช", "com_user_message": "ืืชื”" -} +} \ No newline at end of file diff --git a/client/src/locales/id/translation.json b/client/src/locales/id/translation.json index b6d7d57237..7caba4d0ae 100644 --- a/client/src/locales/id/translation.json +++ b/client/src/locales/id/translation.json @@ -247,7 +247,6 @@ "com_ui_enter": "Masuk", "com_ui_examples": "Contoh", "com_ui_happy_birthday": "Ini ulang tahun pertamaku!", - "com_ui_import": "Impor", "com_ui_import_conversation_error": "Terjadi kesalahan saat mengimpor percakapan Anda", "com_ui_import_conversation_info": "Impor percakapan dari file JSON", "com_ui_import_conversation_success": "Percakapan berhasil diimpor", @@ -284,4 +283,4 @@ "com_ui_upload_success": "Berhasil mengunggah file", "com_ui_use_prompt": "Gunakan petunjuk", "com_user_message": "Kamu" -} +} \ No newline at end of file diff --git a/client/src/locales/it/translation.json b/client/src/locales/it/translation.json index d64fca28e5..062209cce9 100644 --- a/client/src/locales/it/translation.json +++ b/client/src/locales/it/translation.json @@ -600,7 +600,6 @@ "com_ui_hide_qr": "Nascondi codice QR", "com_ui_host": "Host", "com_ui_image_gen": "Generazione immagine", - "com_ui_import": "Importa", "com_ui_import_conversation_error": "Si รจ verificato un errore durante l'importazione delle conversazioni", "com_ui_import_conversation_file_type_error": "Tipo di importazione non supportato", "com_ui_import_conversation_info": "Importa conversazioni da un file JSON", @@ -744,4 +743,4 @@ "com_ui_zoom": "Zoom", "com_user_message": "Mostra nome utente nei messaggi", "com_warning_resubmit_unsupported": "Il reinvio del messaggio AI non รจ supportato per questo endpoint." -} +} \ No newline at end of file diff --git a/client/src/locales/ja/translation.json b/client/src/locales/ja/translation.json index 965ca5699d..a63fc80254 100644 --- a/client/src/locales/ja/translation.json +++ b/client/src/locales/ja/translation.json @@ -584,7 +584,6 @@ "com_ui_happy_birthday": "ๅˆใ‚ใฆใฎ่ช•็”Ÿๆ—ฅใงใ™๏ผ", "com_ui_host": "ใƒ›ใ‚นใƒˆ", "com_ui_image_gen": "็”ปๅƒ็”Ÿๆˆ", - "com_ui_import": "ใ‚คใƒณใƒใƒผใƒˆ", "com_ui_import_conversation_error": "ไผš่ฉฑใฎใ‚คใƒณใƒใƒผใƒˆๆ™‚ใซใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸ", "com_ui_import_conversation_file_type_error": "ใ‚ตใƒใƒผใƒˆใ•ใ‚Œใฆใ„ใชใ„ใ‚คใƒณใƒใƒผใƒˆๅฝขๅผใงใ™", "com_ui_import_conversation_info": "JSONใƒ•ใ‚กใ‚คใƒซใ‹ใ‚‰ไผš่ฉฑใ‚’ใ‚คใƒณใƒใƒผใƒˆใ™ใ‚‹", @@ -716,4 +715,4 @@ "com_ui_zoom": "ใ‚บใƒผใƒ ", "com_user_message": "ใ‚ใชใŸ", "com_warning_resubmit_unsupported": "ใ“ใฎใ‚จใƒณใƒ‰ใƒใ‚คใƒณใƒˆใงใฏAIใƒกใƒƒใ‚ปใƒผใ‚ธใฎๅ†้€ไฟกใฏใ‚ตใƒใƒผใƒˆใ•ใ‚Œใฆใ„ใพใ›ใ‚“" -} +} \ No newline at end of file diff --git a/client/src/locales/ko/translation.json b/client/src/locales/ko/translation.json index 46eda9f9de..0bfb21452e 100644 --- a/client/src/locales/ko/translation.json +++ b/client/src/locales/ko/translation.json @@ -584,7 +584,6 @@ "com_ui_happy_birthday": "๋‚ด ์ฒซ ์ƒ์ผ์ด์•ผ!", "com_ui_host": "ํ˜ธ์ŠคํŠธ", "com_ui_image_gen": "์ด๋ฏธ์ง€ ์ƒ์„ฑ", - "com_ui_import": "๊ฐ€์ ธ์˜ค๊ธฐ", "com_ui_import_conversation_error": "๋Œ€ํ™”๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋™์•ˆ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค", "com_ui_import_conversation_file_type_error": "๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค", "com_ui_import_conversation_info": "JSON ํŒŒ์ผ์—์„œ ๋Œ€ํ™” ๊ฐ€์ ธ์˜ค๊ธฐ", @@ -716,4 +715,4 @@ "com_ui_zoom": "ํ™•๋Œ€/์ถ•์†Œ", "com_user_message": "๋‹น์‹ ", "com_warning_resubmit_unsupported": "์ด ์—”๋“œํฌ์ธํŠธ์—์„œ๋Š” AI ๋ฉ”์‹œ์ง€ ์žฌ์ „์†ก์ด ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" -} +} \ No newline at end of file diff --git a/client/src/locales/nl/translation.json b/client/src/locales/nl/translation.json index f6c99423fb..35fec8640e 100644 --- a/client/src/locales/nl/translation.json +++ b/client/src/locales/nl/translation.json @@ -219,7 +219,6 @@ "com_ui_enter": "Invoeren", "com_ui_examples": "Voorbeelden", "com_ui_happy_birthday": "Het is mijn eerste verjaardag!", - "com_ui_import": "Importeren", "com_ui_import_conversation_error": "Er is een fout opgetreden bij het importeren van je gesprekken", "com_ui_import_conversation_info": "Gesprekken importeren vanuit een JSON-bestand", "com_ui_import_conversation_success": "Gesprekken succesvol geรฏmporteerd", @@ -250,4 +249,4 @@ "com_ui_unarchive_error": "Kan conversatie niet uit archiveren", "com_ui_upload_success": "Bestand succesvol geรผpload", "com_ui_use_prompt": "Gebruik prompt" -} +} \ No newline at end of file diff --git a/client/src/locales/pl/translation.json b/client/src/locales/pl/translation.json index 30483ba8b3..24fd96a70c 100644 --- a/client/src/locales/pl/translation.json +++ b/client/src/locales/pl/translation.json @@ -562,7 +562,6 @@ "com_ui_hide_qr": "Ukryj kod QR", "com_ui_host": "Host", "com_ui_image_gen": "Generowanie obrazu", - "com_ui_import": "Importuj", "com_ui_import_conversation_error": "Wystฤ…piล‚ bล‚ฤ…d podczas importowania konwersacji", "com_ui_import_conversation_file_type_error": "Nieobsล‚ugiwany typ importu", "com_ui_import_conversation_info": "Importuj konwersacje z pliku JSON", @@ -704,4 +703,4 @@ "com_ui_zoom": "Powiฤ™ksz", "com_user_message": "Ty", "com_warning_resubmit_unsupported": "Ponowne przesyล‚anie wiadomoล›ci AI nie jest obsล‚ugiwane dla tego punktu koล„cowego." -} +} \ No newline at end of file diff --git a/client/src/locales/pt-BR/translation.json b/client/src/locales/pt-BR/translation.json index 06a22184c5..a1e37608ff 100644 --- a/client/src/locales/pt-BR/translation.json +++ b/client/src/locales/pt-BR/translation.json @@ -163,7 +163,6 @@ "com_endpoint_google_temp": "Valores mais altos = mais aleatรณrio, enquanto valores mais baixos = mais focado e determinรญstico. Recomendamos alterar isso ou Top P, mas nรฃo ambos.", "com_endpoint_google_topk": "Top-k altera como o modelo seleciona tokens para saรญda. Um top-k de 1 significa que o token selecionado รฉ o mais provรกvel entre todos os tokens no vocabulรกrio do modelo (tambรฉm chamado de decodificaรงรฃo gananciosa), enquanto um top-k de 3 significa que o prรณximo token รฉ selecionado entre os 3 tokens mais provรกveis (usando temperatura).", "com_endpoint_google_topp": "Top-p altera como o modelo seleciona tokens para saรญda. Os tokens sรฃo selecionados dos mais provรกveis (veja o parรขmetro topK) atรฉ os menos provรกveis atรฉ que a soma de suas probabilidades atinja o valor top-p.", - "com_endpoint_import": "Importar", "com_endpoint_instructions_assistants": "Substituir Instruรงรตes", "com_endpoint_instructions_assistants_placeholder": "Substitui as instruรงรตes do assistente. Isso รฉ รบtil para modificar o comportamento em uma base por execuรงรฃo.", "com_endpoint_max_output_tokens": "Mรกximo de Tokens de Saรญda", @@ -237,7 +236,6 @@ "com_nav_archive_name": "Nome", "com_nav_archived_chats": "Chats Arquivados", "com_nav_archived_chats_empty": "Vocรช nรฃo tem conversas arquivadas.", - "com_nav_archived_chats_manage": "Gerenciar", "com_nav_at_command": "Comando @", "com_nav_at_command_description": "Alternar comando \"@\" para alternar endpoints, modelos, predefiniรงรตes, etc.", "com_nav_audio_play_error": "Erro ao reproduzir รกudio: {{0}}", @@ -353,7 +351,6 @@ "com_nav_setting_speech": "Fala", "com_nav_settings": "Configuraรงรตes", "com_nav_shared_links": "Links compartilhados", - "com_nav_shared_links_manage": "Gerenciar", "com_nav_show_code": "Sempre mostrar cรณdigo ao usar o interpretador de cรณdigo", "com_nav_slash_command": "Comando /", "com_nav_slash_command_description": "Alternar comando \"/\" para selecionar um prompt via teclado", @@ -530,7 +527,6 @@ "com_ui_happy_birthday": "ร‰ meu 1ยบ aniversรกrio!", "com_ui_host": "Host", "com_ui_image_gen": "Geraรงรฃo de Imagem", - "com_ui_import_conversation": "Importar", "com_ui_import_conversation_error": "Houve um erro ao importar suas conversas", "com_ui_import_conversation_file_type_error": "Tipo de importaรงรฃo nรฃo suportado", "com_ui_import_conversation_info": "Importar conversas de um arquivo JSON", diff --git a/client/src/locales/pt-PT/translation.json b/client/src/locales/pt-PT/translation.json index c687f7b2e0..730ce2ac2f 100644 --- a/client/src/locales/pt-PT/translation.json +++ b/client/src/locales/pt-PT/translation.json @@ -182,7 +182,6 @@ "com_endpoint_google_temp": "Valores mais altos = mais aleatรณrio, enquanto valores mais baixos = mais focado e determinรญstico. Recomendamos alterar isso ou Top P, mas nรฃo ambos.", "com_endpoint_google_topk": "Top-k altera como o modelo seleciona tokens para saรญda. Um top-k de 1 significa que o token selecionado รฉ o mais provรกvel entre todos os tokens no vocabulรกrio do modelo (tambรฉm chamado de decodificaรงรฃo gananciosa), enquanto um top-k de 3 significa que o prรณximo token รฉ selecionado entre os 3 tokens mais provรกveis (usando temperatura).", "com_endpoint_google_topp": "Top-p altera como o modelo seleciona tokens para saรญda. Os tokens sรฃo selecionados dos mais provรกveis (veja o parรขmetro topK) atรฉ os menos provรกveis atรฉ que a soma de suas probabilidades atinja o valor top-p.", - "com_endpoint_import": "Importar", "com_endpoint_instructions_assistants": "Substituir Instruรงรตes", "com_endpoint_instructions_assistants_placeholder": "Substitui as instruรงรตes do assistente. Isso รฉ รบtil para modificar o comportamento em uma base por execuรงรฃo.", "com_endpoint_max_output_tokens": "Mรกximo de Tokens de Saรญda", @@ -268,7 +267,6 @@ "com_nav_archive_name": "Nome", "com_nav_archived_chats": "Chats Arquivados", "com_nav_archived_chats_empty": "Vocรช nรฃo tem conversas arquivadas.", - "com_nav_archived_chats_manage": "Gerenciar", "com_nav_at_command": "Comando @", "com_nav_at_command_description": "Alternar comando \"@\" para alternar endpoints, modelos, predefiniรงรตes, etc.", "com_nav_audio_play_error": "Erro ao reproduzir รกudio: {{0}}", @@ -391,7 +389,6 @@ "com_nav_setting_speech": "Fala", "com_nav_settings": "Configuraรงรตes", "com_nav_shared_links": "Links compartilhados", - "com_nav_shared_links_manage": "Gerenciar", "com_nav_show_code": "Sempre mostrar cรณdigo ao usar o interpretador de cรณdigo", "com_nav_show_thinking": "Abrir Dropdown de lรณgica por defeito.", "com_nav_slash_command": "Comando /", @@ -626,7 +623,6 @@ "com_ui_host": "Host", "com_ui_idea": "Ideias", "com_ui_image_gen": "Geraรงรฃo de Imagem", - "com_ui_import_conversation": "Importar", "com_ui_import_conversation_error": "Houve um erro ao importar suas conversas", "com_ui_import_conversation_file_type_error": "Tipo de importaรงรฃo nรฃo suportado", "com_ui_import_conversation_info": "Importar conversas de um arquivo JSON", diff --git a/client/src/locales/ru/translation.json b/client/src/locales/ru/translation.json index ee5e4d0e29..76962c09b2 100644 --- a/client/src/locales/ru/translation.json +++ b/client/src/locales/ru/translation.json @@ -584,7 +584,6 @@ "com_ui_happy_birthday": "ะญั‚ะพ ะผะพะน ะฟะตั€ะฒั‹ะน ะดะตะฝัŒ ั€ะพะถะดะตะฝะธั!", "com_ui_host": "ะฅะพัั‚", "com_ui_image_gen": "ะ“ะตะฝะตั€ะฐั‚ะพั€ ะธะทะพะฑั€ะฐะถะตะฝะธะน", - "com_ui_import": "ะ˜ะผะฟะพั€ั‚ะธั€ะพะฒะฐั‚ัŒ", "com_ui_import_conversation_error": "ะŸั€ะธ ะธะผะฟะพั€ั‚ะต ะฑะตัะตะด ะฟั€ะพะธะทะพัˆะปะฐ ะพัˆะธะฑะบะฐ", "com_ui_import_conversation_file_type_error": "ะะตะฟะพะดะดะตั€ะถะธะฒะฐะตะผั‹ะน ั‚ะธะฟ ะธะผะฟะพั€ั‚ะฐ", "com_ui_import_conversation_info": "ะ˜ะผะฟะพั€ั‚ะธั€ะพะฒะฐั‚ัŒ ะฑะตัะตะดั‹ ะธะท ั„ะฐะนะปะฐ JSON", @@ -716,4 +715,4 @@ "com_ui_zoom": "ะœะฐััˆั‚ะฐะฑ", "com_user_message": "ะ’ั‹", "com_warning_resubmit_unsupported": "ะŸะพะฒั‚ะพั€ะฝะฐั ะพั‚ะฟั€ะฐะฒะบะฐ ัะพะพะฑั‰ะตะฝะธั ะ˜ะ˜ ะฝะต ะฟะพะดะดะตั€ะถะธะฒะฐะตั‚ัั ะดะปั ะดะฐะฝะฝะพะน ะบะพะฝะตั‡ะฝะพะน ั‚ะพั‡ะบะธ" -} +} \ No newline at end of file diff --git a/client/src/locales/sv/translation.json b/client/src/locales/sv/translation.json index 26c8cc0c55..4497ecc857 100644 --- a/client/src/locales/sv/translation.json +++ b/client/src/locales/sv/translation.json @@ -206,7 +206,6 @@ "com_ui_enter": "Ange", "com_ui_examples": "Exempel", "com_ui_happy_birthday": "Det รคr min fรถrsta fรถdelsedag!", - "com_ui_import": "Importera", "com_ui_import_conversation_error": "Det uppstod ett fel vid import av dina konversationer", "com_ui_import_conversation_info": "Importera konversationer frรฅn en JSON-fil", "com_ui_import_conversation_success": "Konversationer har importerats framgรฅngsrikt", @@ -236,4 +235,4 @@ "com_ui_unarchive_error": "Kunde inte avarkivera chatt", "com_ui_upload_success": "Uppladdningen av filen lyckades", "com_ui_use_prompt": "Anvรคnd prompt" -} +} \ No newline at end of file diff --git a/client/src/locales/tr/translation.json b/client/src/locales/tr/translation.json index a75a5422ea..e4b7bd8ce5 100644 --- a/client/src/locales/tr/translation.json +++ b/client/src/locales/tr/translation.json @@ -602,7 +602,6 @@ "com_ui_hide_qr": "QR Kodunu Gizle", "com_ui_host": "Host", "com_ui_image_gen": "Gรถrรผntรผ OluลŸtur", - "com_ui_import": "ฤฐรงe Aktar", "com_ui_import_conversation_error": "KonuลŸmalarฤฑnฤฑzฤฑ iรงe aktarma sฤฑrasฤฑnda bir hata oluลŸtu", "com_ui_import_conversation_file_type_error": "Desteklenmeyen iรงe aktarma tรผrรผ", "com_ui_import_conversation_info": "JSON dosyasฤฑndan konuลŸmalarฤฑ iรงe aktar", @@ -747,4 +746,4 @@ "com_ui_zoom": "YakฤฑnlaลŸtฤฑr", "com_user_message": "Sen", "com_warning_resubmit_unsupported": "Bu uรง nokta iรงin yapay zeka mesajฤฑnฤฑ yeniden gรถnderme desteklenmiyor." -} +} \ No newline at end of file diff --git a/client/src/locales/vi/translation.json b/client/src/locales/vi/translation.json index 57f34eef0b..d4cff9b40e 100644 --- a/client/src/locales/vi/translation.json +++ b/client/src/locales/vi/translation.json @@ -205,7 +205,6 @@ "com_ui_enter": "Nhแบญp", "com_ui_examples": "Vรญ dแปฅ", "com_ui_happy_birthday": "ฤรขy lร  sinh nhแบญt ฤ‘แบงu tiรชn cแปงa tรดi!", - "com_ui_import": "Nhแบญp khแบฉu", "com_ui_import_conversation_error": "ฤรฃ xแบฃy ra lแป—i khi nhแบญp khแบฉu cuแป™c trรฒ chuyแป‡n cแปงa bแบกn", "com_ui_import_conversation_info": "Nhแบญp khแบฉu cuแป™c trรฒ chuyแป‡n tแปซ mแป™t tแป‡p JSON", "com_ui_import_conversation_success": "ฤรฃ nhแบญp khแบฉu cuแป™c trรฒ chuyแป‡n thร nh cรดng", @@ -235,4 +234,4 @@ "com_ui_unarchive_error": "Khรดng thแปƒ bแป lฦฐu trแปฏ cuแป™c trรฒ chuyแป‡n", "com_ui_upload_success": "Tแบฃi tแป‡p thร nh cรดng", "com_ui_use_prompt": "Sแปญ dแปฅng gแปฃi รฝ" -} +} \ No newline at end of file diff --git a/client/src/locales/zh-Hans/translation.json b/client/src/locales/zh-Hans/translation.json index 6570bf1ae5..cca6ac9b8e 100644 --- a/client/src/locales/zh-Hans/translation.json +++ b/client/src/locales/zh-Hans/translation.json @@ -182,7 +182,6 @@ "com_endpoint_google_temp": "ๅ€ผ่ถŠ้ซ˜่กจ็คบ่พ“ๅ‡บ่ถŠ้šๆœบ๏ผŒๅ€ผ่ถŠไฝŽ่กจ็คบ่พ“ๅ‡บ่ถŠ็กฎๅฎšใ€‚ๅปบ่ฎฎไธ่ฆๅŒๆ—ถๆ”นๅ˜ๆญคๅ€ผๅ’Œ Top-pใ€‚", "com_endpoint_google_topk": "top-k ไผšๆ”นๅ˜ๆจกๅž‹้€‰ๆ‹ฉ่พ“ๅ‡บ่ฏ็š„ๆ–นๅผใ€‚top-k ไธบ 1 ๆ„ๅ‘ณ็€ๆ‰€้€‰่ฏๆ˜ฏๆจกๅž‹่ฏๆฑ‡ไธญๆฆ‚็އๆœ€ๅคง็š„๏ผˆไนŸ็งฐไธบ่ดชๅฟƒ่งฃ็ ๏ผ‰๏ผŒ่€Œ top-k ไธบ 3 ๆ„ๅ‘ณ็€ไธ‹ไธ€ไธช่ฏๆ˜ฏไปŽ 3 ไธชๆฆ‚็އๆœ€ๅคง็š„่ฏไธญ้€‰ๅ‡บ็š„๏ผˆไฝฟ็”จ้šๆœบๆ€ง๏ผ‰ใ€‚", "com_endpoint_google_topp": "top-p๏ผˆๆ ธ้‡‡ๆ ท๏ผ‰ไผšๆ”นๅ˜ๆจกๅž‹้€‰ๆ‹ฉ่พ“ๅ‡บ่ฏ็š„ๆ–นๅผใ€‚ไปŽๆฆ‚็އๆœ€ๅคง็š„ K๏ผˆๅ‚่งtopKๅ‚ๆ•ฐ๏ผ‰ๅ‘ๆœ€ๅฐ็š„ K ้€‰ๆ‹ฉ๏ผŒ็›ดๅˆฐๅฎƒไปฌ็š„ๆฆ‚็އไน‹ๅ’Œ็ญ‰ไบŽ top-p ๅ€ผใ€‚", - "com_endpoint_import": "ๅฏผๅ…ฅ", "com_endpoint_instructions_assistants": "่ฆ†ๅ†™ๆŒ‡ไปค", "com_endpoint_instructions_assistants_placeholder": "่ฆ†็›–ๅŠฉๆ‰‹็š„ๆŒ‡ไปคใ€‚่ฟ™ๅฏนไบŽ้œ€่ฆ้€ๆฌกไฟฎๆ”น่กŒไธบ้žๅธธๆœ‰็”จใ€‚", "com_endpoint_max_output_tokens": "ๆœ€ๅคง่พ“ๅ‡บ่ฏๅ…ƒๆ•ฐ", @@ -267,7 +266,6 @@ "com_nav_archive_name": "ๅ็งฐ", "com_nav_archived_chats": "ๅฝ’ๆกฃ็š„ๅฏน่ฏ", "com_nav_archived_chats_empty": "ๆ‚จๆฒกๆœ‰ๅฝ’ๆกฃ็š„ๅฏน่ฏใ€‚", - "com_nav_archived_chats_manage": "็ฎก็†", "com_nav_at_command": "@-ๅ‘ฝไปค", "com_nav_at_command_description": "ๅˆ‡ๆข่‡ณๅ‘ฝไปค โ€œ@โ€ ไปฅๆ›ดๆ”น็ซฏ็‚นใ€ๆจกๅž‹ใ€้ข„่ฎพ็ญ‰", "com_nav_audio_play_error": "ๆ’ญๆ”พ้Ÿณ้ข‘ๆ—ถๅ‘็”Ÿ้”™่ฏฏ๏ผš{{0}}", @@ -390,7 +388,6 @@ "com_nav_setting_speech": "่ฏญ้Ÿณ", "com_nav_settings": "่ฎพ็ฝฎ", "com_nav_shared_links": "ๅ…ฑไบซ้“พๆŽฅ", - "com_nav_shared_links_manage": "็ฎก็†", "com_nav_show_code": "ไฝฟ็”จไปฃ็ ่งฃ้‡Šๅ™จๆ—ถๅง‹็ปˆๆ˜พ็คบไปฃ็ ", "com_nav_slash_command": "/-ๅ‘ฝไปค", "com_nav_slash_command_description": "ๅˆ‡ๆข่‡ณๅ‘ฝไปค โ€œ/โ€ ไปฅ้€š่ฟ‡้”ฎ็›˜้€‰ๆ‹ฉๆ็คบ่ฏ", @@ -611,7 +608,6 @@ "com_ui_hide_qr": "้š่—ไบŒ็ปด็ ", "com_ui_host": "ไธปๆœบ", "com_ui_image_gen": "ๅ›พ็‰‡็”Ÿๆˆ", - "com_ui_import_conversation": "ๅฏผๅ…ฅ", "com_ui_import_conversation_error": "ๅฏผๅ…ฅๅฏน่ฏๆ—ถๅ‘็”Ÿ้”™่ฏฏ", "com_ui_import_conversation_file_type_error": "ไธๆ”ฏๆŒ็š„ๅฏผๅ…ฅ็ฑปๅž‹", "com_ui_import_conversation_info": "ไปŽ JSON ๆ–‡ไปถๅฏผๅ…ฅๅฏน่ฏ", diff --git a/client/src/locales/zh-Hant/translation.json b/client/src/locales/zh-Hant/translation.json index f46d94af2d..0aaf22594d 100644 --- a/client/src/locales/zh-Hant/translation.json +++ b/client/src/locales/zh-Hant/translation.json @@ -584,7 +584,6 @@ "com_ui_happy_birthday": "้€™ๆ˜ฏๆˆ‘็š„็ฌฌไธ€ๅ€‹็”Ÿๆ—ฅ๏ผ", "com_ui_host": "ไธปๆฉŸ", "com_ui_image_gen": "ๅฝฑๅƒ็”Ÿๆˆ", - "com_ui_import": "ๅŒฏๅ…ฅ", "com_ui_import_conversation_error": "ๅŒฏๅ…ฅๅฐ่ฉฑๆ™‚็™ผ็”Ÿ้Œฏ่ชค", "com_ui_import_conversation_file_type_error": "ไธๆ”ฏๆด็š„ๅŒฏๅ…ฅๆช”ๆกˆ้กžๅž‹", "com_ui_import_conversation_info": "ๅพž JSON ๆ–‡ไปถๅŒฏๅ…ฅๅฐ่ฉฑ", @@ -716,4 +715,4 @@ "com_ui_zoom": "็ธฎๆ”พ", "com_user_message": "ๆ‚จ", "com_warning_resubmit_unsupported": "ๆญค็ซฏ้ปžไธๆ”ฏๆด้‡ๆ–ฐ้€ๅ‡บ AI ่จŠๆฏใ€‚" -} +} \ No newline at end of file diff --git a/deploy-compose.yml b/deploy-compose.yml index 3f1e14ee54..ae61265a05 100644 --- a/deploy-compose.yml +++ b/deploy-compose.yml @@ -1,4 +1,3 @@ -version: "3.8" services: api: # build: @@ -29,6 +28,7 @@ services: source: ./librechat.yaml target: /app/librechat.yaml - ./images:/app/client/public/images + - ./uploads:/app/uploads - ./logs:/app/api/logs client: diff --git a/docker-compose.yml b/docker-compose.yml index e863965342..e16f93f4c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,7 @@ services: source: ./.env target: /app/.env - ./images:/app/client/public/images + - ./uploads:/app/uploads - ./logs:/app/api/logs mongodb: container_name: chat-mongodb From ecddffa7b241502c0a4342f4b1c1c65467e754dd Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 18 Feb 2025 08:14:19 -0500 Subject: [PATCH 06/32] =?UTF-8?q?=F0=9F=90=9B=20fix:=20RAG=20Results=20Sor?= =?UTF-8?q?ted=20By=20Distance=20(#5931)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Extract file unlinking logic into a separate function and don't throw error * fix: RAG results are actually in distance, not score --- api/app/clients/tools/util/fileSearch.js | 12 +++++++----- api/server/services/Files/Local/crud.js | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/api/app/clients/tools/util/fileSearch.js b/api/app/clients/tools/util/fileSearch.js index c48adc2eb4..54da483362 100644 --- a/api/app/clients/tools/util/fileSearch.js +++ b/api/app/clients/tools/util/fileSearch.js @@ -106,19 +106,21 @@ const createFileSearchTool = async ({ req, files, entity_id }) => { const formattedResults = validResults .flatMap((result) => - result.data.map(([docInfo, relevanceScore]) => ({ + result.data.map(([docInfo, distance]) => ({ filename: docInfo.metadata.source.split('/').pop(), content: docInfo.page_content, - relevanceScore, + distance, })), ) - .sort((a, b) => b.relevanceScore - a.relevanceScore) - .slice(0, 5); + // TODO: results should be sorted by relevance, not distance + .sort((a, b) => a.distance - b.distance) + // TODO: make this configurable + .slice(0, 10); const formattedString = formattedResults .map( (result) => - `File: ${result.filename}\nRelevance: ${result.relevanceScore.toFixed(4)}\nContent: ${ + `File: ${result.filename}\nRelevance: ${1.0 - result.distance.toFixed(4)}\nContent: ${ result.content }\n`, ) diff --git a/api/server/services/Files/Local/crud.js b/api/server/services/Files/Local/crud.js index e004eab79e..97a067d794 100644 --- a/api/server/services/Files/Local/crud.js +++ b/api/server/services/Files/Local/crud.js @@ -175,6 +175,17 @@ const isValidPath = (req, base, subfolder, filepath) => { return normalizedFilepath.startsWith(normalizedBase); }; +/** + * @param {string} filepath + */ +const unlinkFile = async (filepath) => { + try { + await fs.promises.unlink(filepath); + } catch (error) { + logger.error('Error deleting file:', error); + } +}; + /** * Deletes a file from the filesystem. This function takes a file object, constructs the full path, and * verifies the path's validity before deleting the file. If the path is invalid, an error is thrown. @@ -217,7 +228,7 @@ const deleteLocalFile = async (req, file) => { throw new Error(`Invalid file path: ${file.filepath}`); } - await fs.promises.unlink(filepath); + await unlinkFile(filepath); return; } @@ -233,7 +244,7 @@ const deleteLocalFile = async (req, file) => { throw new Error('Invalid file path'); } - await fs.promises.unlink(filepath); + await unlinkFile(filepath); }; /** From 06282b584f0f960221220affd3db09d75cb782a3 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Tue, 18 Feb 2025 14:35:43 +0100 Subject: [PATCH 07/32] =?UTF-8?q?=F0=9F=93=9C=20ci:=20Automate`CHANGELOG.m?= =?UTF-8?q?d`=20(#5838)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: started with automated CHANGELOG.md * fix: no `configuration.json` found * refactor: `configuration.json` * fix: missing label `configuration.json` * fix: missing label `configuration.json` * fix: missing label `configuration.json` * fix: missing label `configuration.json` * fix: missing label `configuration.json` * ci: test new workflow action * ci: test new workflow action * ci: test new workflow action * feat: working CHANGELOG.md generation * feat: working CHANGELOG.md generation * feat: working CHANGELOG.md generation * feat: working CHANGELOG.md generation * feat: working CHANGELOG.md generation * feat: working CHANGELOG.md generation * feat: working CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * fix: separate release and Unreleased workflows CHANGELOG.md generation * refactor: only trigger the `unreleased-changelog` action on push to `main` and `generate-release-changelog` only when pushing a tag with `v*.*.*` * refactor: Runs only every Monday at 00:00 UTC --- .github/configuration-release.json | 60 ++++++++++ .github/configuration-unreleased.json | 68 +++++++++++ .../generate-release-changelog-pr.yml | 94 ++++++++++++++++ .../generate-unreleased-changelog-pr.yml | 106 ++++++++++++++++++ 4 files changed, 328 insertions(+) create mode 100644 .github/configuration-release.json create mode 100644 .github/configuration-unreleased.json create mode 100644 .github/workflows/generate-release-changelog-pr.yml create mode 100644 .github/workflows/generate-unreleased-changelog-pr.yml diff --git a/.github/configuration-release.json b/.github/configuration-release.json new file mode 100644 index 0000000000..68fe80ed8f --- /dev/null +++ b/.github/configuration-release.json @@ -0,0 +1,60 @@ +{ + "categories": [ + { + "title": "### โœจ New Features", + "labels": ["feat"] + }, + { + "title": "### ๐ŸŒ Internationalization", + "labels": ["i18n"] + }, + { + "title": "### ๐Ÿ‘ Accessibility", + "labels": ["a11y"] + }, + { + "title": "### ๐Ÿ”ง Fixes", + "labels": ["Fix", "fix"] + }, + { + "title": "### โš™๏ธ Other Changes", + "labels": ["ci", "style", "docs", "refactor", "chore"] + } + ], + "ignore_labels": [ + "๐Ÿ” duplicate", + "๐Ÿ“Š analytics", + "๐ŸŒฑ good first issue", + "๐Ÿ” investigation", + "๐Ÿ™ help wanted", + "โŒ invalid", + "โ“ question", + "๐Ÿšซ wontfix", + "๐Ÿš€ release", + "version" + ], + "base_branches": ["main"], + "sort": { + "order": "ASC", + "on_property": "mergedAt" + }, + "label_extractor": [ + { + "pattern": "^(?:[^A-Za-z0-9]*)(feat|fix|chore|docs|refactor|ci|style|a11y|i18n)\\s*:", + "target": "$1", + "flags": "i", + "on_property": "title", + "method": "match" + }, + { + "pattern": "^(?:[^A-Za-z0-9]*)(v\\d+\\.\\d+\\.\\d+(?:-rc\\d+)?).*", + "target": "version", + "flags": "i", + "on_property": "title", + "method": "match" + } + ], + "template": "## [#{{TO_TAG}}] - #{{TO_TAG_DATE}}\n\nChanges from #{{FROM_TAG}} to #{{TO_TAG}}.\n\n#{{CHANGELOG}}\n\n[See full release details][release-#{{TO_TAG}}]\n\n[release-#{{TO_TAG}}]: https://github.com/#{{OWNER}}/#{{REPO}}/releases/tag/#{{TO_TAG}}\n\n---", + "pr_template": "- #{{TITLE}} by **@#{{AUTHOR}}** in [##{{NUMBER}}](#{{URL}})", + "empty_template": "- no changes" +} \ No newline at end of file diff --git a/.github/configuration-unreleased.json b/.github/configuration-unreleased.json new file mode 100644 index 0000000000..29eaf5e13b --- /dev/null +++ b/.github/configuration-unreleased.json @@ -0,0 +1,68 @@ +{ + "categories": [ + { + "title": "### โœจ New Features", + "labels": ["feat"] + }, + { + "title": "### ๐ŸŒ Internationalization", + "labels": ["i18n"] + }, + { + "title": "### ๐Ÿ‘ Accessibility", + "labels": ["a11y"] + }, + { + "title": "### ๐Ÿ”ง Fixes", + "labels": ["Fix", "fix"] + }, + { + "title": "### โš™๏ธ Other Changes", + "labels": ["ci", "style", "docs", "refactor", "chore"] + } + ], + "ignore_labels": [ + "๐Ÿ” duplicate", + "๐Ÿ“Š analytics", + "๐ŸŒฑ good first issue", + "๐Ÿ” investigation", + "๐Ÿ™ help wanted", + "โŒ invalid", + "โ“ question", + "๐Ÿšซ wontfix", + "๐Ÿš€ release", + "version", + "action" + ], + "base_branches": ["main"], + "sort": { + "order": "ASC", + "on_property": "mergedAt" + }, + "label_extractor": [ + { + "pattern": "^(?:[^A-Za-z0-9]*)(feat|fix|chore|docs|refactor|ci|style|a11y|i18n)\\s*:", + "target": "$1", + "flags": "i", + "on_property": "title", + "method": "match" + }, + { + "pattern": "^(?:[^A-Za-z0-9]*)(v\\d+\\.\\d+\\.\\d+(?:-rc\\d+)?).*", + "target": "version", + "flags": "i", + "on_property": "title", + "method": "match" + }, + { + "pattern": "^(?:[^A-Za-z0-9]*)(action)\\b.*", + "target": "action", + "flags": "i", + "on_property": "title", + "method": "match" + } + ], + "template": "## [Unreleased]\n\n#{{CHANGELOG}}\n\n---", + "pr_template": "- #{{TITLE}} by **@#{{AUTHOR}}** in [##{{NUMBER}}](#{{URL}})", + "empty_template": "- no changes" +} \ No newline at end of file diff --git a/.github/workflows/generate-release-changelog-pr.yml b/.github/workflows/generate-release-changelog-pr.yml new file mode 100644 index 0000000000..c3bceae9de --- /dev/null +++ b/.github/workflows/generate-release-changelog-pr.yml @@ -0,0 +1,94 @@ +name: Generate Release Changelog PR + +on: + push: + tags: + - 'v*.*.*' + +jobs: + generate-release-changelog-pr: + permissions: + contents: write # Needed for pushing commits and creating branches. + pull-requests: write + runs-on: ubuntu-latest + steps: + # 1. Checkout the repository (with full history). + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # 2. Generate the release changelog using our custom configuration. + - name: Generate Release Changelog + id: generate_release + uses: mikepenz/release-changelog-builder-action@v5.1.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + configuration: ".github/configuration-release.json" + owner: ${{ github.repository_owner }} + repo: ${{ github.event.repository.name }} + outputFile: CHANGELOG-release.md + + # 3. Update the main CHANGELOG.md: + # - If it doesn't exist, create it with a basic header. + # - Remove the "Unreleased" section (if present). + # - Prepend the new release changelog above previous releases. + # - Remove all temporary files before committing. + - name: Update CHANGELOG.md + run: | + # Determine the release tag, e.g. "v1.2.3" + TAG=${GITHUB_REF##*/} + echo "Using release tag: $TAG" + + # Ensure CHANGELOG.md exists; if not, create a basic header. + if [ ! -f CHANGELOG.md ]; then + echo "# Changelog" > CHANGELOG.md + echo "" >> CHANGELOG.md + echo "All notable changes to this project will be documented in this file." >> CHANGELOG.md + echo "" >> CHANGELOG.md + fi + + echo "Updating CHANGELOG.mdโ€ฆ" + + # Remove the "Unreleased" section (from "## [Unreleased]" until the first occurrence of '---') if it exists. + if grep -q "^## \[Unreleased\]" CHANGELOG.md; then + awk '/^## \[Unreleased\]/{flag=1} flag && /^---/{flag=0; next} !flag' CHANGELOG.md > CHANGELOG.cleaned + else + cp CHANGELOG.md CHANGELOG.cleaned + fi + + # Split the cleaned file into: + # - header.md: content before the first release header ("## [v..."). + # - tail.md: content from the first release header onward. + awk '/^## \[v/{exit} {print}' CHANGELOG.cleaned > header.md + awk 'f{print} /^## \[v/{f=1; print}' CHANGELOG.cleaned > tail.md + + # Combine header, the new release changelog, and the tail. + echo "Combining updated changelog parts..." + cat header.md CHANGELOG-release.md > CHANGELOG.md.new + echo "" >> CHANGELOG.md.new + cat tail.md >> CHANGELOG.md.new + + mv CHANGELOG.md.new CHANGELOG.md + + # Remove temporary files. + rm -f CHANGELOG.cleaned header.md tail.md CHANGELOG-release.md + + echo "Final CHANGELOG.md content:" + cat CHANGELOG.md + + # 4. Create (or update) the Pull Request with the updated CHANGELOG.md. + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + sign-commits: true + commit-message: "chore: update CHANGELOG for release ${GITHUB_REF##*/}" + base: main + branch: "changelog/${GITHUB_REF##*/}" + reviewers: danny-avila + title: "chore: update CHANGELOG for release ${GITHUB_REF##*/}" + body: | + **Description**: + - This PR updates the CHANGELOG.md by removing the "Unreleased" section and adding new release notes for release ${GITHUB_REF##*/} above previous releases. \ No newline at end of file diff --git a/.github/workflows/generate-unreleased-changelog-pr.yml b/.github/workflows/generate-unreleased-changelog-pr.yml new file mode 100644 index 0000000000..b130e4fb33 --- /dev/null +++ b/.github/workflows/generate-unreleased-changelog-pr.yml @@ -0,0 +1,106 @@ +name: Generate Unreleased Changelog PR + +on: + schedule: + - cron: "0 0 * * 1" # Runs every Monday at 00:00 UTC + +jobs: + generate-unreleased-changelog-pr: + permissions: + contents: write # Needed for pushing commits and creating branches. + pull-requests: write + runs-on: ubuntu-latest + steps: + # 1. Checkout the repository on main. + - name: Checkout Repository on Main + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + + # 4. Get the latest version tag. + - name: Get Latest Tag + id: get_latest_tag + run: | + LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1) || echo "none") + echo "Latest tag: $LATEST_TAG" + echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT + + # 5. Generate the Unreleased changelog. + - name: Generate Unreleased Changelog + id: generate_unreleased + uses: mikepenz/release-changelog-builder-action@v5.1.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + configuration: ".github/configuration-unreleased.json" + owner: ${{ github.repository_owner }} + repo: ${{ github.event.repository.name }} + outputFile: CHANGELOG-unreleased.md + fromTag: ${{ steps.get_latest_tag.outputs.tag }} + toTag: main + + # 7. Update CHANGELOG.md with the new Unreleased section. + - name: Update CHANGELOG.md + id: update_changelog + run: | + # Create CHANGELOG.md if it doesn't exist. + if [ ! -f CHANGELOG.md ]; then + echo "# Changelog" > CHANGELOG.md + echo "" >> CHANGELOG.md + echo "All notable changes to this project will be documented in this file." >> CHANGELOG.md + echo "" >> CHANGELOG.md + fi + + echo "Updating CHANGELOG.mdโ€ฆ" + + # Extract content before the "## [Unreleased]" (or first version header if missing). + if grep -q "^## \[Unreleased\]" CHANGELOG.md; then + awk '/^## \[Unreleased\]/{exit} {print}' CHANGELOG.md > CHANGELOG_TMP.md + else + awk '/^## \[v/{exit} {print}' CHANGELOG.md > CHANGELOG_TMP.md + fi + + # Append the generated Unreleased changelog. + echo "" >> CHANGELOG_TMP.md + cat CHANGELOG-unreleased.md >> CHANGELOG_TMP.md + echo "" >> CHANGELOG_TMP.md + + # Append the remainder of the original changelog (starting from the first version header). + awk 'f{print} /^## \[v/{f=1; print}' CHANGELOG.md >> CHANGELOG_TMP.md + + # Replace the old file with the updated file. + mv CHANGELOG_TMP.md CHANGELOG.md + + # Remove the temporary generated file. + rm -f CHANGELOG-unreleased.md + + echo "Final CHANGELOG.md:" + cat CHANGELOG.md + + # 8. Check if CHANGELOG.md has any updates. + - name: Check for CHANGELOG.md changes + id: changelog_changes + run: | + if git diff --quiet CHANGELOG.md; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + # 9. Create (or update) the Pull Request only if there are changes. + - name: Create Pull Request + if: steps.changelog_changes.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + base: main + branch: "changelog/unreleased-update" + sign-commits: true + commit-message: "action: update Unreleased changelog" + title: "action: update Unreleased changelog" + body: | + **Description**: + - This PR updates the Unreleased section in CHANGELOG.md. + - It compares the current main branch with the latest version tag (determined as ${{ steps.get_latest_tag.outputs.tag }}), + regenerates the Unreleased changelog, removes any old Unreleased block, and inserts the new content. \ No newline at end of file From 538a2a144a7075c89574e2c87234ddc86ca80e4d Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Wed, 19 Feb 2025 19:33:29 +0100 Subject: [PATCH 08/32] =?UTF-8?q?=F0=9F=94=92=20fix:=202FA=20Encrypt=20TOT?= =?UTF-8?q?P=20Secrets=20&=20Improve=20Docs=20(#5933)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿ”’ fix: Integrate TOTP secret retrieval and encryption in Two-Factor Authentication * ๐Ÿ”’ refactor: Simplify TOTP verification by removing commented-out code --- api/server/controllers/TwoFactorController.js | 24 +++-- .../auth/TwoFactorAuthController.js | 10 +- api/server/services/twoFactorService.js | 91 +++++++++++++------ 3 files changed, 84 insertions(+), 41 deletions(-) diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js index 3e8d38ac12..f145d69d92 100644 --- a/api/server/controllers/TwoFactorController.js +++ b/api/server/controllers/TwoFactorController.js @@ -3,9 +3,11 @@ const { verifyBackupCode, generateTOTPSecret, generateBackupCodes, + getTOTPSecret, } = require('~/server/services/twoFactorService'); const { updateUser, getUserById } = require('~/models'); const { logger } = require('~/config'); +const { encryptV2 } = require('~/server/utils/crypto'); const enable2FAController = async (req, res) => { const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, ''); @@ -15,7 +17,8 @@ const enable2FAController = async (req, res) => { const secret = generateTOTPSecret(); const { plainCodes, codeObjects } = await generateBackupCodes(); - const user = await updateUser(userId, { totpSecret: secret, backupCodes: codeObjects }); + const encryptedSecret = await encryptV2(secret); + const user = await updateUser(userId, { totpSecret: encryptedSecret, backupCodes: codeObjects }); const otpauthUrl = `otpauth://totp/${safeAppTitle}:${user.email}?secret=${secret}&issuer=${safeAppTitle}`; @@ -38,14 +41,16 @@ const verify2FAController = async (req, res) => { return res.status(400).json({ message: '2FA not initiated' }); } - let verified = false; - if (token && (await verifyTOTP(user.totpSecret, token))) { + // Retrieve the plain TOTP secret using getTOTPSecret. + const secret = await getTOTPSecret(user.totpSecret); + + if (token && (await verifyTOTP(secret, token))) { return res.status(200).json(); } else if (backupCode) { - verified = await verifyBackupCode({ user, backupCode }); - } - if (verified) { - return res.status(200).json(); + const verified = await verifyBackupCode({ user, backupCode }); + if (verified) { + return res.status(200).json(); + } } return res.status(400).json({ message: 'Invalid token.' }); @@ -65,7 +70,10 @@ const confirm2FAController = async (req, res) => { return res.status(400).json({ message: '2FA not initiated' }); } - if (await verifyTOTP(user.totpSecret, token)) { + // Retrieve the plain TOTP secret using getTOTPSecret. + const secret = await getTOTPSecret(user.totpSecret); + + if (await verifyTOTP(secret, token)) { return res.status(200).json(); } diff --git a/api/server/controllers/auth/TwoFactorAuthController.js b/api/server/controllers/auth/TwoFactorAuthController.js index 37a8045829..78c5c0314e 100644 --- a/api/server/controllers/auth/TwoFactorAuthController.js +++ b/api/server/controllers/auth/TwoFactorAuthController.js @@ -1,5 +1,5 @@ const jwt = require('jsonwebtoken'); -const { verifyTOTP, verifyBackupCode } = require('~/server/services/twoFactorService'); +const { verifyTOTP, verifyBackupCode, getTOTPSecret } = require('~/server/services/twoFactorService'); const { setAuthTokens } = require('~/server/services/AuthService'); const { getUserById } = require('~/models/userMethods'); const { logger } = require('~/config'); @@ -24,9 +24,11 @@ const verify2FA = async (req, res) => { return res.status(400).json({ message: '2FA is not enabled for this user' }); } - let verified = false; + // Use the new getTOTPSecret function to retrieve (and decrypt if necessary) the TOTP secret. + const secret = await getTOTPSecret(user.totpSecret); - if (token && (await verifyTOTP(user.totpSecret, token))) { + let verified = false; + if (token && (await verifyTOTP(secret, token))) { verified = true; } else if (backupCode) { verified = await verifyBackupCode({ user, backupCode }); @@ -39,7 +41,7 @@ const verify2FA = async (req, res) => { // Prepare user data for response. // If the user is a plain object (from lean queries), we create a shallow copy. const userData = user.toObject ? user.toObject() : { ...user }; - // Remove sensitive fields + // Remove sensitive fields. delete userData.password; delete userData.__v; delete userData.totpSecret; diff --git a/api/server/services/twoFactorService.js b/api/server/services/twoFactorService.js index ac7247409c..e48b2ac938 100644 --- a/api/server/services/twoFactorService.js +++ b/api/server/services/twoFactorService.js @@ -1,14 +1,15 @@ const { sign } = require('jsonwebtoken'); const { webcrypto } = require('node:crypto'); -const { hashBackupCode } = require('~/server/utils/crypto'); +const { hashBackupCode, decryptV2 } = require('~/server/utils/crypto'); const { updateUser } = require('~/models/userMethods'); const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; /** - * Encodes a Buffer into a Base32 string using RFC 4648 alphabet. + * Encodes a Buffer into a Base32 string using the RFC 4648 alphabet. + * * @param {Buffer} buffer - The buffer to encode. - * @returns {string} - The Base32 encoded string. + * @returns {string} The Base32 encoded string. */ const encodeBase32 = (buffer) => { let bits = 0; @@ -30,8 +31,9 @@ const encodeBase32 = (buffer) => { /** * Decodes a Base32-encoded string back into a Buffer. - * @param {string} base32Str - * @returns {Buffer} + * + * @param {string} base32Str - The Base32-encoded string. + * @returns {Buffer} The decoded buffer. */ const decodeBase32 = (base32Str) => { const cleaned = base32Str.replace(/=+$/, '').toUpperCase(); @@ -54,15 +56,20 @@ const decodeBase32 = (base32Str) => { }; /** - * Generate a temporary token for 2FA verification. - * This token is signed with JWT_SECRET and expires in 5 minutes. + * Generates a temporary token for 2FA verification. + * The token is signed with the JWT_SECRET and expires in 5 minutes. + * + * @param {string} userId - The unique identifier of the user. + * @returns {string} The signed JWT token. */ const generate2FATempToken = (userId) => sign({ userId, twoFAPending: true }, process.env.JWT_SECRET, { expiresIn: '5m' }); /** - * Generate a TOTP secret. - * Generates 10 random bytes using WebCrypto and encodes them into a Base32 string. + * Generates a TOTP secret. + * Creates 10 random bytes using WebCrypto and encodes them into a Base32 string. + * + * @returns {string} A Base32-encoded secret for TOTP. */ const generateTOTPSecret = () => { const randomArray = new Uint8Array(10); @@ -71,12 +78,12 @@ const generateTOTPSecret = () => { }; /** - * Generate a TOTP code based on the secret and current time. - * Uses a 30-second time step and generates a 6-digit code. + * Generates a Time-based One-Time Password (TOTP) based on the provided secret and time. + * This implementation uses a 30-second time step and produces a 6-digit code. * - * @param {string} secret - Base32-encoded secret - * @param {number} [forTime=Date.now()] - Time in milliseconds - * @returns {Promise} - The 6-digit TOTP code. + * @param {string} secret - The Base32-encoded TOTP secret. + * @param {number} [forTime=Date.now()] - The time (in milliseconds) for which to generate the TOTP. + * @returns {Promise} A promise that resolves to the 6-digit TOTP code. */ const generateTOTP = async (secret, forTime = Date.now()) => { const timeStep = 30; // seconds @@ -106,6 +113,7 @@ const generateTOTP = async (secret, forTime = Date.now()) => { const signatureBuffer = await webcrypto.subtle.sign('HMAC', cryptoKey, counterBuffer); const hmac = new Uint8Array(signatureBuffer); + // Dynamic truncation as per RFC 4226 const offset = hmac[hmac.length - 1] & 0xf; const slice = hmac.slice(offset, offset + 4); const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength); @@ -115,12 +123,12 @@ const generateTOTP = async (secret, forTime = Date.now()) => { }; /** - * Verify a provided TOTP token against the secret. - * Allows for a ยฑ1 time-step window. + * Verifies a provided TOTP token against the secret. + * It allows for a ยฑ1 time-step window to account for slight clock discrepancies. * - * @param {string} secret - * @param {string} token - * @returns {Promise} + * @param {string} secret - The Base32-encoded TOTP secret. + * @param {string} token - The TOTP token provided by the user. + * @returns {Promise} A promise that resolves to true if the token is valid; otherwise, false. */ const verifyTOTP = async (secret, token) => { const timeStepMS = 30 * 1000; @@ -135,12 +143,13 @@ const verifyTOTP = async (secret, token) => { }; /** - * Generate backup codes. - * Generates `count` backup code objects and returns an object with both plain codes - * (for one-time download) and their objects (for secure storage). Uses WebCrypto for randomness and hashing. + * Generates backup codes for two-factor authentication. + * Each backup code is an 8-character hexadecimal string along with its SHA-256 hash. + * The plain codes are returned for one-time download, while the hashed objects are meant for secure storage. * - * @param {number} count - Number of backup codes to generate (default: 10). - * @returns {Promise} - Contains `plainCodes` (array of strings) and `codeObjects` (array of objects). + * @param {number} [count=10] - The number of backup codes to generate. + * @returns {Promise<{ plainCodes: string[], codeObjects: Array<{ codeHash: string, used: boolean, usedAt: Date | null }> }>} + * A promise that resolves to an object containing both plain backup codes and their corresponding code objects. */ const generateBackupCodes = async (count = 10) => { const plainCodes = []; @@ -165,11 +174,12 @@ const generateBackupCodes = async (count = 10) => { }; /** - * Verifies a backup code and updates the user's backup codes if valid - * @param {Object} params - * @param {TUser | undefined} [params.user] - The user object - * @param {string | undefined} [params.backupCode] - The backup code to verify - * @returns {Promise} - Whether the backup code was valid + * Verifies a backup code for a user and updates its status as used if valid. + * + * @param {Object} params - The parameters object. + * @param {TUser | undefined} [params.user] - The user object containing backup codes. + * @param {string | undefined} [params.backupCode] - The backup code to verify. + * @returns {Promise} A promise that resolves to true if the backup code is valid and updated; otherwise, false. */ const verifyBackupCode = async ({ user, backupCode }) => { if (!backupCode || !user || !Array.isArray(user.backupCodes)) { @@ -195,9 +205,32 @@ const verifyBackupCode = async ({ user, backupCode }) => { return false; }; +/** + * Retrieves and, if necessary, decrypts a stored TOTP secret. + * If the secret contains a colon, it is assumed to be in the format "iv:encryptedData" and will be decrypted. + * If the secret is exactly 16 characters long, it is assumed to be a legacy plain secret. + * + * @param {string|null} storedSecret - The stored TOTP secret (which may be encrypted). + * @returns {Promise} A promise that resolves to the plain TOTP secret, or null if none is provided. + */ +const getTOTPSecret = async (storedSecret) => { + if (!storedSecret) { return null; } + // Check for a colon marker (encrypted secrets are stored as "iv:encryptedData") + if (storedSecret.includes(':')) { + return await decryptV2(storedSecret); + } + // If it's exactly 16 characters, assume it's already plain (legacy secret) + if (storedSecret.length === 16) { + return storedSecret; + } + // Fallback in case it doesn't meet our criteria. + return storedSecret; +}; + module.exports = { verifyTOTP, generateTOTP, + getTOTPSecret, verifyBackupCode, generateTOTPSecret, generateBackupCodes, From fdb3cf3f583c63c7bef67c267878d61eeec0553e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 19 Feb 2025 14:53:22 -0500 Subject: [PATCH 09/32] =?UTF-8?q?=F0=9F=94=A7=20fix:=20Resizable=20Panel?= =?UTF-8?q?=20Unmount=20Error=20&=20Code=20Env.=20File=20Re-Upload=20(#594?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿ”ง refactor: handle full path for code env. file re-upload * fix: update react-resizable-panels to version 2.1.7 to resolve error thrown on unmount of artifacts; ref: https://github.com/bvaughn/react-resizable-panels/issues/372 * refactor: replace promptPrefix with systemMessage in GoogleClient for improved clarity, and to prevent saving LibreChat feature-specific instructions to the user's custom instructions --- api/app/clients/GoogleClient.js | 20 +++++++++++--------- api/server/services/Files/Code/process.js | 2 +- api/server/services/Files/Firebase/crud.js | 3 ++- api/server/services/Files/Local/crud.js | 22 +++++++++++++++++++++- client/package.json | 6 +++--- package-lock.json | 20 ++++++++++---------- 6 files changed, 48 insertions(+), 25 deletions(-) diff --git a/api/app/clients/GoogleClient.js b/api/app/clients/GoogleClient.js index 03461a6796..a2ca02558b 100644 --- a/api/app/clients/GoogleClient.js +++ b/api/app/clients/GoogleClient.js @@ -51,7 +51,7 @@ class GoogleClient extends BaseClient { const serviceKey = creds[AuthKeys.GOOGLE_SERVICE_KEY] ?? {}; this.serviceKey = - serviceKey && typeof serviceKey === 'string' ? JSON.parse(serviceKey) : serviceKey ?? {}; + serviceKey && typeof serviceKey === 'string' ? JSON.parse(serviceKey) : (serviceKey ?? {}); /** @type {string | null | undefined} */ this.project_id = this.serviceKey.project_id; this.client_email = this.serviceKey.client_email; @@ -73,6 +73,8 @@ class GoogleClient extends BaseClient { * @type {string} */ this.outputTokensKey = 'output_tokens'; this.visionMode = VisionModes.generative; + /** @type {string} */ + this.systemMessage; if (options.skipSetOptions) { return; } @@ -184,7 +186,7 @@ class GoogleClient extends BaseClient { if (typeof this.options.artifactsPrompt === 'string' && this.options.artifactsPrompt) { promptPrefix = `${promptPrefix ?? ''}\n${this.options.artifactsPrompt}`.trim(); } - this.options.promptPrefix = promptPrefix; + this.systemMessage = promptPrefix; this.initializeClient(); return this; } @@ -314,7 +316,7 @@ class GoogleClient extends BaseClient { } this.augmentedPrompt = await this.contextHandlers.createContext(); - this.options.promptPrefix = this.augmentedPrompt + this.options.promptPrefix; + this.systemMessage = this.augmentedPrompt + this.systemMessage; } } @@ -361,8 +363,8 @@ class GoogleClient extends BaseClient { throw new Error('[GoogleClient] PaLM 2 and Codey models are no longer supported.'); } - if (this.options.promptPrefix) { - const instructionsTokenCount = this.getTokenCount(this.options.promptPrefix); + if (this.systemMessage) { + const instructionsTokenCount = this.getTokenCount(this.systemMessage); this.maxContextTokens = this.maxContextTokens - instructionsTokenCount; if (this.maxContextTokens < 0) { @@ -417,8 +419,8 @@ class GoogleClient extends BaseClient { ], }; - if (this.options.promptPrefix) { - payload.instances[0].context = this.options.promptPrefix; + if (this.systemMessage) { + payload.instances[0].context = this.systemMessage; } logger.debug('[GoogleClient] buildMessages', payload); @@ -464,7 +466,7 @@ class GoogleClient extends BaseClient { identityPrefix = `${identityPrefix}\nYou are ${this.options.modelLabel}`; } - let promptPrefix = (this.options.promptPrefix ?? '').trim(); + let promptPrefix = (this.systemMessage ?? '').trim(); if (identityPrefix) { promptPrefix = `${identityPrefix}${promptPrefix}`; @@ -648,7 +650,7 @@ class GoogleClient extends BaseClient { generationConfig: googleGenConfigSchema.parse(this.modelOptions), }; - const promptPrefix = (this.options.promptPrefix ?? '').trim(); + const promptPrefix = (this.systemMessage ?? '').trim(); if (promptPrefix.length) { requestOptions.systemInstruction = { parts: [ diff --git a/api/server/services/Files/Code/process.js b/api/server/services/Files/Code/process.js index ce8acf4ad3..c92e628589 100644 --- a/api/server/services/Files/Code/process.js +++ b/api/server/services/Files/Code/process.js @@ -209,7 +209,7 @@ const primeFiles = async (options, apiKey) => { const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions( FileSources.execute_code, ); - const stream = await getDownloadStream(file.filepath); + const stream = await getDownloadStream(options.req, file.filepath); const fileIdentifier = await uploadCodeEnvFile({ req: options.req, stream, diff --git a/api/server/services/Files/Firebase/crud.js b/api/server/services/Files/Firebase/crud.js index 76a6c1d8d4..8319f908ef 100644 --- a/api/server/services/Files/Firebase/crud.js +++ b/api/server/services/Files/Firebase/crud.js @@ -224,10 +224,11 @@ async function uploadFileToFirebase({ req, file, file_id }) { /** * Retrieves a readable stream for a file from Firebase storage. * + * @param {ServerRequest} _req * @param {string} filepath - The filepath. * @returns {Promise} A readable stream of the file. */ -async function getFirebaseFileStream(filepath) { +async function getFirebaseFileStream(_req, filepath) { try { const storage = getFirebaseStorage(); if (!storage) { diff --git a/api/server/services/Files/Local/crud.js b/api/server/services/Files/Local/crud.js index 97a067d794..c2bb75c125 100644 --- a/api/server/services/Files/Local/crud.js +++ b/api/server/services/Files/Local/crud.js @@ -286,11 +286,31 @@ async function uploadLocalFile({ req, file, file_id }) { /** * Retrieves a readable stream for a file from local storage. * + * @param {ServerRequest} req - The request object from Express * @param {string} filepath - The filepath. * @returns {ReadableStream} A readable stream of the file. */ -function getLocalFileStream(filepath) { +function getLocalFileStream(req, filepath) { try { + if (filepath.includes('/uploads/')) { + const basePath = filepath.split('/uploads/')[1]; + + if (!basePath) { + logger.warn(`Invalid base path: ${filepath}`); + throw new Error(`Invalid file path: ${filepath}`); + } + + const fullPath = path.join(req.app.locals.paths.uploads, basePath); + const uploadsDir = req.app.locals.paths.uploads; + + const rel = path.relative(uploadsDir, fullPath); + if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) { + logger.warn(`Invalid relative file path: ${filepath}`); + throw new Error(`Invalid file path: ${filepath}`); + } + + return fs.createReadStream(fullPath); + } return fs.createReadStream(filepath); } catch (error) { logger.error('Error getting local file stream:', error); diff --git a/client/package.json b/client/package.json index 917333ce25..df85c2521c 100644 --- a/client/package.json +++ b/client/package.json @@ -66,8 +66,8 @@ "html-to-image": "^1.11.11", "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.3", - "js-cookie": "^3.0.5", "input-otp": "^1.4.2", + "js-cookie": "^3.0.5", "librechat-data-provider": "*", "lodash": "^4.17.21", "lucide-react": "^0.394.0", @@ -86,7 +86,7 @@ "react-i18next": "^15.4.0", "react-lazy-load-image-component": "^1.6.0", "react-markdown": "^9.0.1", - "react-resizable-panels": "^2.1.1", + "react-resizable-panels": "^2.1.7", "react-router-dom": "^6.11.2", "react-speech-recognition": "^3.10.0", "react-textarea-autosize": "^8.4.0", @@ -144,4 +144,4 @@ "vite-plugin-node-polyfills": "^0.17.0", "vite-plugin-pwa": "^0.21.1" } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 802cedd19e..ef573d7010 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1095,7 +1095,7 @@ "react-i18next": "^15.4.0", "react-lazy-load-image-component": "^1.6.0", "react-markdown": "^9.0.1", - "react-resizable-panels": "^2.1.1", + "react-resizable-panels": "^2.1.7", "react-router-dom": "^6.11.2", "react-speech-recognition": "^3.10.0", "react-textarea-autosize": "^8.4.0", @@ -1609,6 +1609,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "client/node_modules/react-resizable-panels": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", + "integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "client/node_modules/rollup": { "version": "4.34.6", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.6.tgz", @@ -31919,15 +31928,6 @@ } } }, - "node_modules/react-resizable-panels": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.1.tgz", - "integrity": "sha512-+cUV/yZBYfiBj+WJtpWDJ3NtR4zgDZfHt3+xtaETKE+FCvp+RK/NJxacDQKxMHgRUTSkfA6AnGljQ5QZNsCQoA==", - "peerDependencies": { - "react": "^16.14.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-router": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz", From fe7013562b215e574e2ab245c58a6e89ede84017 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:17:43 +0100 Subject: [PATCH 10/32] =?UTF-8?q?=E2=9C=A8=20style:=20Enhance=20Styling=20?= =?UTF-8?q?&=20Accessibility=20(#5956)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * โœจ feat: Enhance UI Components with Shadows and Accessibility Improvements * ๐Ÿ”ง fix: Correct Category Labels and Values in API Model & Adjust Button Class in Prompt List --- api/models/Categories.js | 36 ++++++++-------- client/src/components/Prompts/Command.tsx | 2 +- client/src/components/Prompts/Description.tsx | 2 +- client/src/components/Prompts/Groups/List.tsx | 2 +- .../src/components/Prompts/PromptEditor.tsx | 3 +- client/src/components/Prompts/PromptForm.tsx | 3 +- .../components/Prompts/PromptVariables.tsx | 4 +- .../SidePanel/Agents/ModelPanel.tsx | 13 +++++- client/src/components/ui/SelectDropDown.tsx | 41 ++++++++++--------- 9 files changed, 59 insertions(+), 47 deletions(-) diff --git a/api/models/Categories.js b/api/models/Categories.js index 605b68d176..6fb88fb995 100644 --- a/api/models/Categories.js +++ b/api/models/Categories.js @@ -3,40 +3,40 @@ const { logger } = require('~/config'); const options = [ { - label: 'idea', - value: 'com_ui_idea', + label: 'com_ui_idea', + value: 'idea', }, { - label: 'travel', - value: 'com_ui_travel', + label: 'com_ui_travel', + value: 'travel', }, { - label: 'teach_or_explain', - value: 'com_ui_teach_or_explain', + label: 'com_ui_teach_or_explain', + value: 'teach_or_explain', }, { - label: 'write', - value: 'com_ui_write', + label: 'com_ui_write', + value: 'write', }, { - label: 'shop', - value: 'com_ui_shop', + label: 'com_ui_shop', + value: 'shop', }, { - label: 'code', - value: 'com_ui_code', + label: 'com_ui_code', + value: 'code', }, { - label: 'misc', - value: 'com_ui_misc', + label: 'com_ui_misc', + value: 'misc', }, { - label: 'roleplay', - value: 'com_ui_roleplay', + label: 'com_ui_roleplay', + value: 'roleplay', }, { - label: 'finance', - value: 'com_ui_finance', + label: 'com_ui_finance', + value: 'finance', }, ]; diff --git a/client/src/components/Prompts/Command.tsx b/client/src/components/Prompts/Command.tsx index f64fee4546..e670410ae8 100644 --- a/client/src/components/Prompts/Command.tsx +++ b/client/src/components/Prompts/Command.tsx @@ -44,7 +44,7 @@ const Command = ({ } return ( -
+