From 01470ef9fd6a7282c957b7a43b0b7b2b2b78b2b3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 24 Jul 2025 10:38:26 -0400 Subject: [PATCH 001/224] =?UTF-8?q?=F0=9F=94=84=20refactor:=20Default=20Co?= =?UTF-8?q?mpletion=20Title=20Prompt=20and=20Title=20Model=20Selection=20(?= =?UTF-8?q?#8646)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: prefer `agent.model` (user-facing value) over `agent.model_parameters.model` to ensure Azure mapping * chore: update @librechat/agents to version 2.4.68 to use new default title prompt for completion title method --- api/package.json | 2 +- api/server/controllers/agents/client.js | 2 +- package-lock.json | 10 +++++----- packages/api/package.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/package.json b/api/package.json index 2cfecff47..d94aa8f53 100644 --- a/api/package.json +++ b/api/package.json @@ -49,7 +49,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.67", + "@librechat/agents": "^2.4.68", "@librechat/api": "*", "@librechat/data-schemas": "*", "@node-saml/passport-saml": "^5.0.0", diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 3a91693ab..f9dacbe5a 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -1022,7 +1022,7 @@ class AgentClient extends BaseClient { /** @type {import('@librechat/agents').ClientOptions} */ let clientOptions = { maxTokens: 75, - model: agent.model_parameters.model, + model: agent.model || agent.model_parameters.model, }; let titleProviderConfig = await getProviderConfig(endpoint); diff --git a/package-lock.json b/package-lock.json index 5b1324f0d..9fda62f0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.67", + "@librechat/agents": "^2.4.68", "@librechat/api": "*", "@librechat/data-schemas": "*", "@node-saml/passport-saml": "^5.0.0", @@ -21468,9 +21468,9 @@ } }, "node_modules/@librechat/agents": { - "version": "2.4.67", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.67.tgz", - "integrity": "sha512-GHGTdRmBTpfI/Ps3/is1h4hTEX0rijFdhj6LIqXQzHx6Nnv2nNIIK8tMW/0oPVHcdKuRGrR6Nt6BLpAJMfckgg==", + "version": "2.4.68", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.68.tgz", + "integrity": "sha512-05UhnUJJ6/I8KVkhJ9NrQcm3UKhA/cXG8yT2VU+QQRJoDf7qnt47DRBP87ZEWRGMLh2civq1OWQPW2BHf2eL4A==", "license": "MIT", "dependencies": { "@langchain/anthropic": "^0.3.24", @@ -48617,7 +48617,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.67", + "@librechat/agents": "^2.4.68", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.16.0", "axios": "^1.8.2", diff --git a/packages/api/package.json b/packages/api/package.json index 8b746f4ca..8ba82f34f 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -70,7 +70,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.67", + "@librechat/agents": "^2.4.68", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.16.0", "axios": "^1.8.2", From 1fe977e48fdef75313674c4671cecc50cad76973 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Thu, 24 Jul 2025 07:44:58 -0700 Subject: [PATCH 002/224] =?UTF-8?q?=F0=9F=90=9B=20fix:=20MCP=20Name=20Norm?= =?UTF-8?q?alization=20breaking=20User=20Provided=20Variables=20(#8644)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server/services/Config/getCustomConfig.js | 5 - api/server/services/MCP.js | 1 + packages/api/src/mcp/auth.test.ts | 168 ++++++++++++++++++ packages/api/src/mcp/auth.ts | 11 +- 4 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 packages/api/src/mcp/auth.test.ts diff --git a/api/server/services/Config/getCustomConfig.js b/api/server/services/Config/getCustomConfig.js index 7495ce1e2..a7cb74de5 100644 --- a/api/server/services/Config/getCustomConfig.js +++ b/api/server/services/Config/getCustomConfig.js @@ -3,7 +3,6 @@ const { isEnabled, getUserMCPAuthMap } = require('@librechat/api'); const { CacheKeys, EModelEndpoint } = require('librechat-data-provider'); const { normalizeEndpointName } = require('~/server/utils'); const loadCustomConfig = require('./loadCustomConfig'); -const { getCachedTools } = require('./getCachedTools'); const getLogStores = require('~/cache/getLogStores'); /** @@ -66,13 +65,9 @@ async function getMCPAuthMap({ userId, tools, findPluginAuthsByKeys }) { if (!tools || tools.length === 0) { return; } - const appTools = await getCachedTools({ - userId, - }); return await getUserMCPAuthMap({ tools, userId, - appTools, findPluginAuthsByKeys, }); } catch (err) { diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 3f0a4d618..997098182 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -235,6 +235,7 @@ async function createMCPTool({ req, res, toolKey, provider: _provider }) { responseFormat: AgentConstants.CONTENT_AND_ARTIFACT, }); toolInstance.mcp = true; + toolInstance.mcpRawServerName = serverName; return toolInstance; } diff --git a/packages/api/src/mcp/auth.test.ts b/packages/api/src/mcp/auth.test.ts new file mode 100644 index 000000000..7bfb40ae9 --- /dev/null +++ b/packages/api/src/mcp/auth.test.ts @@ -0,0 +1,168 @@ +import type { PluginAuthMethods } from '@librechat/data-schemas'; +import type { GenericTool } from '@librechat/agents'; +import { getPluginAuthMap } from '~/agents/auth'; +import { getUserMCPAuthMap } from './auth'; + +jest.mock('~/agents/auth', () => ({ + getPluginAuthMap: jest.fn(), +})); + +const mockGetPluginAuthMap = getPluginAuthMap as jest.MockedFunction; + +const createMockTool = ( + name: string, + mcpRawServerName?: string, + mcp = true, +): GenericTool & { mcpRawServerName?: string; mcp?: boolean } => + ({ + name, + mcpRawServerName, + mcp, + description: 'Mock tool', + }) as GenericTool & { mcpRawServerName?: string; mcp?: boolean }; + +const mockFindPluginAuthsByKeys: PluginAuthMethods['findPluginAuthsByKeys'] = jest.fn(); + +describe('getUserMCPAuthMap', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Core Functionality', () => { + it('should handle server names with various special characters and spaces', async () => { + const testCases = [ + { + originalName: 'Connector: Company', + normalizedToolName: 'tool_mcp_Connector__Company', + }, + { + originalName: 'Server (Production) @ Company.com', + normalizedToolName: 'tool_mcp_Server__Production____Company.com', + }, + { + originalName: '🌟 Testing Server™ (α-β) 测试服务器', + normalizedToolName: 'tool_mcp_____Testing_Server_________', + }, + ]; + + const tools = testCases.map((testCase) => + createMockTool(testCase.normalizedToolName, testCase.originalName), + ); + + const expectedKeys = testCases.map((tc) => `mcp_${tc.originalName}`); + mockGetPluginAuthMap.mockResolvedValue({}); + + await getUserMCPAuthMap({ + userId: 'user123', + tools, + findPluginAuthsByKeys: mockFindPluginAuthsByKeys, + }); + + expect(mockGetPluginAuthMap).toHaveBeenCalledWith({ + userId: 'user123', + pluginKeys: expectedKeys, + throwError: false, + findPluginAuthsByKeys: mockFindPluginAuthsByKeys, + }); + }); + }); + + describe('Edge Cases', () => { + it('should return empty object when no tools have mcpRawServerName', async () => { + const tools = [ + createMockTool('regular_tool', undefined, false), + createMockTool('another_tool', undefined, false), + createMockTool('test_mcp_Server_no_raw_name', undefined), + ]; + + const result = await getUserMCPAuthMap({ + userId: 'user123', + tools, + findPluginAuthsByKeys: mockFindPluginAuthsByKeys, + }); + + expect(result).toEqual({}); + expect(mockGetPluginAuthMap).not.toHaveBeenCalled(); + }); + + it('should handle empty or undefined tools array', async () => { + let result = await getUserMCPAuthMap({ + userId: 'user123', + tools: [], + findPluginAuthsByKeys: mockFindPluginAuthsByKeys, + }); + expect(result).toEqual({}); + expect(mockGetPluginAuthMap).not.toHaveBeenCalled(); + + result = await getUserMCPAuthMap({ + userId: 'user123', + tools: undefined, + findPluginAuthsByKeys: mockFindPluginAuthsByKeys, + }); + expect(result).toEqual({}); + expect(mockGetPluginAuthMap).not.toHaveBeenCalled(); + }); + + it('should handle database errors gracefully', async () => { + const tools = [createMockTool('test_mcp_Server1', 'Server1')]; + const dbError = new Error('Database connection failed'); + + mockGetPluginAuthMap.mockRejectedValue(dbError); + + const result = await getUserMCPAuthMap({ + userId: 'user123', + tools, + findPluginAuthsByKeys: mockFindPluginAuthsByKeys, + }); + + expect(result).toEqual({}); + }); + + it('should handle non-Error exceptions gracefully', async () => { + const tools = [createMockTool('test_mcp_Server1', 'Server1')]; + + mockGetPluginAuthMap.mockRejectedValue('String error'); + + const result = await getUserMCPAuthMap({ + userId: 'user123', + tools, + findPluginAuthsByKeys: mockFindPluginAuthsByKeys, + }); + + expect(result).toEqual({}); + }); + }); + + describe('Integration', () => { + it('should handle complete workflow with normalized tool names and original server names', async () => { + const originalServerName = 'Connector: Company'; + const toolName = 'test_auth_mcp_Connector__Company'; + + const tools = [createMockTool(toolName, originalServerName)]; + + const mockCustomUserVars = { + 'mcp_Connector: Company': { + API_KEY: 'test123', + SECRET_TOKEN: 'secret456', + }, + }; + + mockGetPluginAuthMap.mockResolvedValue(mockCustomUserVars); + + const result = await getUserMCPAuthMap({ + userId: 'user123', + tools, + findPluginAuthsByKeys: mockFindPluginAuthsByKeys, + }); + + expect(mockGetPluginAuthMap).toHaveBeenCalledWith({ + userId: 'user123', + pluginKeys: ['mcp_Connector: Company'], + throwError: false, + findPluginAuthsByKeys: mockFindPluginAuthsByKeys, + }); + + expect(result).toEqual(mockCustomUserVars); + }); + }); +}); diff --git a/packages/api/src/mcp/auth.ts b/packages/api/src/mcp/auth.ts index 7f6f6001f..8221278fd 100644 --- a/packages/api/src/mcp/auth.ts +++ b/packages/api/src/mcp/auth.ts @@ -3,17 +3,14 @@ import { Constants } from 'librechat-data-provider'; import type { PluginAuthMethods } from '@librechat/data-schemas'; import type { GenericTool } from '@librechat/agents'; import { getPluginAuthMap } from '~/agents/auth'; -import { mcpToolPattern } from './utils'; export async function getUserMCPAuthMap({ userId, tools, - appTools, findPluginAuthsByKeys, }: { userId: string; tools: GenericTool[] | undefined; - appTools: Record; findPluginAuthsByKeys: PluginAuthMethods['findPluginAuthsByKeys']; }) { if (!tools || tools.length === 0) { @@ -23,11 +20,9 @@ export async function getUserMCPAuthMap({ const uniqueMcpServers = new Set(); for (const tool of tools) { - const toolKey = tool.name; - if (toolKey && appTools[toolKey] && mcpToolPattern.test(toolKey)) { - const parts = toolKey.split(Constants.mcp_delimiter); - const serverName = parts[parts.length - 1]; - uniqueMcpServers.add(`${Constants.mcp_prefix}${serverName}`); + const mcpTool = tool as GenericTool & { mcpRawServerName?: string }; + if (mcpTool.mcpRawServerName) { + uniqueMcpServers.add(`${Constants.mcp_prefix}${mcpTool.mcpRawServerName}`); } } From ec67cf2d3a447ec19a7a62557462ab7c20f00844 Mon Sep 17 00:00:00 2001 From: Sebastien Bruel <93573440+sbruel@users.noreply.github.com> Date: Fri, 25 Jul 2025 23:17:33 +0900 Subject: [PATCH 003/224] =?UTF-8?q?=F0=9F=9A=87=20chore:=20Remove=20Overri?= =?UTF-8?q?dden=20Transport=20Error=20Listener=20(#8656)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/src/mcp/connection.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/api/src/mcp/connection.ts b/packages/api/src/mcp/connection.ts index baa69e93a..52189e2ed 100644 --- a/packages/api/src/mcp/connection.ts +++ b/packages/api/src/mcp/connection.ts @@ -211,11 +211,6 @@ export class MCPConnection extends EventEmitter { this.emit('connectionChange', 'disconnected'); }; - transport.onerror = (error) => { - logger.error(`${this.getLogPrefix()} SSE transport error:`, error); - this.emitError(error, 'SSE transport error:'); - }; - transport.onmessage = (message) => { logger.info(`${this.getLogPrefix()} Message received: ${JSON.stringify(message)}`); }; @@ -253,11 +248,6 @@ export class MCPConnection extends EventEmitter { this.emit('connectionChange', 'disconnected'); }; - transport.onerror = (error: Error | unknown) => { - logger.error(`${this.getLogPrefix()} Streamable-http transport error:`, error); - this.emitError(error, 'Streamable-http transport error:'); - }; - transport.onmessage = (message: JSONRPCMessage) => { logger.info(`${this.getLogPrefix()} Message received: ${JSON.stringify(message)}`); }; From 3dc9e85fabc000a5efe9e0e213a888416aeba0e2 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Fri, 25 Jul 2025 07:21:10 -0700 Subject: [PATCH 004/224] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Display=20OAuth?= =?UTF-8?q?=20MCP=20servers=20according=20to=20Chat=20Menu=20Setting=20(#8?= =?UTF-8?q?643)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: chatMenu not being respected in MCPSelect * fix: chatMenu not being respected in MCPSubMenu --- api/server/routes/config.js | 1 + client/src/components/Chat/Input/MCPSelect.tsx | 9 +++++++-- client/src/hooks/Plugins/useMCPSelect.ts | 17 ++++++++++++++--- packages/data-provider/src/config.ts | 1 + 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/api/server/routes/config.js b/api/server/routes/config.js index dd93037dd..55d4cc306 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -106,6 +106,7 @@ router.get('/', async function (req, res) { const serverConfig = config.mcpServers[serverName]; payload.mcpServers[serverName] = { customUserVars: serverConfig?.customUserVars || {}, + chatMenu: serverConfig?.chatMenu, }; } } diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index fd38e73c1..a3916def6 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -17,9 +17,14 @@ function MCPSelect() { const { mcpSelect, startupConfig } = useBadgeRowContext(); const { mcpValues, setMCPValues, mcpToolDetails, isPinned } = mcpSelect; - // Get all configured MCP servers from config + // Get all configured MCP servers from config that allow chat menu const configuredServers = useMemo(() => { - return Object.keys(startupConfig?.mcpServers || {}); + if (!startupConfig?.mcpServers) { + return []; + } + return Object.entries(startupConfig.mcpServers) + .filter(([, config]) => config.chatMenu !== false) + .map(([serverName]) => serverName); }, [startupConfig?.mcpServers]); const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); diff --git a/client/src/hooks/Plugins/useMCPSelect.ts b/client/src/hooks/Plugins/useMCPSelect.ts index a76ccdced..a817a8aa4 100644 --- a/client/src/hooks/Plugins/useMCPSelect.ts +++ b/client/src/hooks/Plugins/useMCPSelect.ts @@ -2,7 +2,7 @@ import { useRef, useEffect, useCallback, useMemo } from 'react'; import { useRecoilState } from 'recoil'; import { Constants, LocalStorageKeys, EModelEndpoint } from 'librechat-data-provider'; import type { TPlugin } from 'librechat-data-provider'; -import { useAvailableToolsQuery } from '~/data-provider'; +import { useAvailableToolsQuery, useGetStartupConfig } from '~/data-provider'; import useLocalStorage from '~/hooks/useLocalStorageAlt'; import { ephemeralAgentByConvoId } from '~/store'; @@ -28,12 +28,13 @@ export function useMCPSelect({ conversationId }: UseMCPSelectOptions) { const key = conversationId ?? Constants.NEW_CONVO; const hasSetFetched = useRef(null); const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key)); - const { data: mcpToolDetails, isFetched } = useAvailableToolsQuery(EModelEndpoint.agents, { + const { data: startupConfig } = useGetStartupConfig(); + const { data: rawMcpTools, isFetched } = useAvailableToolsQuery(EModelEndpoint.agents, { select: (data: TPlugin[]) => { const mcpToolsMap = new Map(); data.forEach((tool) => { const isMCP = tool.pluginKey.includes(Constants.mcp_delimiter); - if (isMCP && tool.chatMenu !== false) { + if (isMCP) { const parts = tool.pluginKey.split(Constants.mcp_delimiter); const serverName = parts[parts.length - 1]; if (!mcpToolsMap.has(serverName)) { @@ -50,6 +51,16 @@ export function useMCPSelect({ conversationId }: UseMCPSelectOptions) { }, }); + const mcpToolDetails = useMemo(() => { + if (!rawMcpTools || !startupConfig?.mcpServers) { + return rawMcpTools; + } + return rawMcpTools.filter((tool) => { + const serverConfig = startupConfig?.mcpServers?.[tool.name]; + return serverConfig?.chatMenu !== false; + }); + }, [rawMcpTools, startupConfig?.mcpServers]); + const mcpState = useMemo(() => { return ephemeralAgent?.mcp ?? []; }, [ephemeralAgent?.mcp]); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index f97f19350..a1510d99a 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -612,6 +612,7 @@ export type TStartupConfig = { description: string; } >; + chatMenu?: boolean; } >; mcpPlaceholder?: string; From 21005b66cc0dabe86a41fe920589575188edf92d Mon Sep 17 00:00:00 2001 From: "Theo N. Truong" <644650+nhtruong@users.noreply.github.com> Date: Fri, 25 Jul 2025 08:23:36 -0600 Subject: [PATCH 005/224] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20support=20for?= =?UTF-8?q?=20forced=20in-memory=20cache=20namespaces=20configuration=20(#?= =?UTF-8?q?8586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: Add support for forced in-memory cache keys configuration * refactor: Update cache keys to use uppercase constants and moved cache for `librechat.yaml` into its own cache namespace (STATIC_CONFIG) and with a more descriptive key (LIBRECHAT_YAML_CONFIG) --- .env.example | 4 ++ api/cache/cacheConfig.js | 20 +++++++ api/cache/cacheConfig.spec.js | 34 ++++++++++++ api/cache/cacheFactory.js | 5 +- api/cache/cacheFactory.spec.js | 26 +++++++++ api/cache/getLogStores.js | 1 + api/server/services/Config/getCustomConfig.js | 4 +- .../services/Config/loadCustomConfig.js | 4 +- packages/data-provider/src/config.ts | 53 ++++++++++--------- 9 files changed, 121 insertions(+), 30 deletions(-) diff --git a/.env.example b/.env.example index 396dcdf21..e375c60a8 100644 --- a/.env.example +++ b/.env.example @@ -627,6 +627,10 @@ HELP_AND_FAQ_URL=https://librechat.ai # Redis connection limits # REDIS_MAX_LISTENERS=40 +# Force specific cache namespaces to use in-memory storage even when Redis is enabled +# Comma-separated list of CacheKeys (e.g., STATIC_CONFIG,ROLES,MESSAGES) +# FORCED_IN_MEMORY_CACHE_NAMESPACES=STATIC_CONFIG,ROLES + #==================================================# # Others # #==================================================# diff --git a/api/cache/cacheConfig.js b/api/cache/cacheConfig.js index 534b3f4b3..551107f7a 100644 --- a/api/cache/cacheConfig.js +++ b/api/cache/cacheConfig.js @@ -1,5 +1,6 @@ const fs = require('fs'); const { math, isEnabled } = require('@librechat/api'); +const { CacheKeys } = require('librechat-data-provider'); // To ensure that different deployments do not interfere with each other's cache, we use a prefix for the Redis keys. // This prefix is usually the deployment ID, which is often passed to the container or pod as an env var. @@ -15,7 +16,26 @@ if (USE_REDIS && !process.env.REDIS_URI) { throw new Error('USE_REDIS is enabled but REDIS_URI is not set.'); } +// Comma-separated list of cache namespaces that should be forced to use in-memory storage +// even when Redis is enabled. This allows selective performance optimization for specific caches. +const FORCED_IN_MEMORY_CACHE_NAMESPACES = process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES + ? process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES.split(',').map((key) => key.trim()) + : []; + +// Validate against CacheKeys enum +if (FORCED_IN_MEMORY_CACHE_NAMESPACES.length > 0) { + const validKeys = Object.values(CacheKeys); + const invalidKeys = FORCED_IN_MEMORY_CACHE_NAMESPACES.filter((key) => !validKeys.includes(key)); + + if (invalidKeys.length > 0) { + throw new Error( + `Invalid cache keys in FORCED_IN_MEMORY_CACHE_NAMESPACES: ${invalidKeys.join(', ')}. Valid keys: ${validKeys.join(', ')}`, + ); + } +} + const cacheConfig = { + FORCED_IN_MEMORY_CACHE_NAMESPACES, USE_REDIS, REDIS_URI: process.env.REDIS_URI, REDIS_USERNAME: process.env.REDIS_USERNAME, diff --git a/api/cache/cacheConfig.spec.js b/api/cache/cacheConfig.spec.js index d931bf0ce..0809d6f62 100644 --- a/api/cache/cacheConfig.spec.js +++ b/api/cache/cacheConfig.spec.js @@ -14,6 +14,7 @@ describe('cacheConfig', () => { delete process.env.REDIS_KEY_PREFIX_VAR; delete process.env.REDIS_KEY_PREFIX; delete process.env.USE_REDIS; + delete process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES; // Clear require cache jest.resetModules(); @@ -105,4 +106,37 @@ describe('cacheConfig', () => { expect(cacheConfig.REDIS_CA).toBeNull(); }); }); + + describe('FORCED_IN_MEMORY_CACHE_NAMESPACES validation', () => { + test('should parse comma-separated cache keys correctly', () => { + process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = ' ROLES, STATIC_CONFIG ,MESSAGES '; + + const { cacheConfig } = require('./cacheConfig'); + expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([ + 'ROLES', + 'STATIC_CONFIG', + 'MESSAGES', + ]); + }); + + test('should throw error for invalid cache keys', () => { + process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'INVALID_KEY,ROLES'; + + expect(() => { + require('./cacheConfig'); + }).toThrow('Invalid cache keys in FORCED_IN_MEMORY_CACHE_NAMESPACES: INVALID_KEY'); + }); + + test('should handle empty string gracefully', () => { + process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = ''; + + const { cacheConfig } = require('./cacheConfig'); + expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([]); + }); + + test('should handle undefined env var gracefully', () => { + const { cacheConfig } = require('./cacheConfig'); + expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([]); + }); + }); }); diff --git a/api/cache/cacheFactory.js b/api/cache/cacheFactory.js index b4cbd2ef4..b9739f4d3 100644 --- a/api/cache/cacheFactory.js +++ b/api/cache/cacheFactory.js @@ -16,7 +16,10 @@ const { RedisStore } = require('rate-limit-redis'); * @returns {Keyv} Cache instance. */ const standardCache = (namespace, ttl = undefined, fallbackStore = undefined) => { - if (cacheConfig.USE_REDIS) { + if ( + cacheConfig.USE_REDIS && + !cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES?.includes(namespace) + ) { const keyvRedis = new KeyvRedis(keyvRedisClient); const cache = new Keyv(keyvRedis, { namespace, ttl }); keyvRedis.namespace = cacheConfig.REDIS_KEY_PREFIX; diff --git a/api/cache/cacheFactory.spec.js b/api/cache/cacheFactory.spec.js index 4f9840b81..76d01a915 100644 --- a/api/cache/cacheFactory.spec.js +++ b/api/cache/cacheFactory.spec.js @@ -31,6 +31,7 @@ jest.mock('./cacheConfig', () => ({ cacheConfig: { USE_REDIS: false, REDIS_KEY_PREFIX: 'test', + FORCED_IN_MEMORY_CACHE_NAMESPACES: [], }, })); @@ -63,6 +64,7 @@ describe('cacheFactory', () => { // Reset cache config mock cacheConfig.USE_REDIS = false; cacheConfig.REDIS_KEY_PREFIX = 'test'; + cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = []; }); describe('redisCache', () => { @@ -116,6 +118,30 @@ describe('cacheFactory', () => { expect(mockKeyv).toHaveBeenCalledWith({ namespace: undefined, ttl: undefined }); }); + + it('should use fallback when namespace is in FORCED_IN_MEMORY_CACHE_NAMESPACES', () => { + cacheConfig.USE_REDIS = true; + cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = ['forced-memory']; + const namespace = 'forced-memory'; + const ttl = 3600; + + standardCache(namespace, ttl); + + expect(require('@keyv/redis').default).not.toHaveBeenCalled(); + expect(mockKeyv).toHaveBeenCalledWith({ namespace, ttl }); + }); + + it('should use Redis when namespace is not in FORCED_IN_MEMORY_CACHE_NAMESPACES', () => { + cacheConfig.USE_REDIS = true; + cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = ['other-namespace']; + const namespace = 'test-namespace'; + const ttl = 3600; + + standardCache(namespace, ttl); + + expect(require('@keyv/redis').default).toHaveBeenCalledWith(mockKeyvRedisClient); + expect(mockKeyv).toHaveBeenCalledWith(mockKeyvRedis, { namespace, ttl }); + }); }); describe('violationCache', () => { diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js index aca53fcfc..675e16a76 100644 --- a/api/cache/getLogStores.js +++ b/api/cache/getLogStores.js @@ -33,6 +33,7 @@ const namespaces = { [CacheKeys.ROLES]: standardCache(CacheKeys.ROLES), [CacheKeys.MCP_TOOLS]: standardCache(CacheKeys.MCP_TOOLS), [CacheKeys.CONFIG_STORE]: standardCache(CacheKeys.CONFIG_STORE), + [CacheKeys.STATIC_CONFIG]: standardCache(CacheKeys.STATIC_CONFIG), [CacheKeys.PENDING_REQ]: standardCache(CacheKeys.PENDING_REQ), [CacheKeys.ENCODED_DOMAINS]: new Keyv({ store: keyvMongo, namespace: CacheKeys.ENCODED_DOMAINS }), [CacheKeys.ABORT_KEYS]: standardCache(CacheKeys.ABORT_KEYS, Time.TEN_MINUTES), diff --git a/api/server/services/Config/getCustomConfig.js b/api/server/services/Config/getCustomConfig.js index a7cb74de5..2b9f658b4 100644 --- a/api/server/services/Config/getCustomConfig.js +++ b/api/server/services/Config/getCustomConfig.js @@ -11,8 +11,8 @@ const getLogStores = require('~/cache/getLogStores'); * @returns {Promise} * */ async function getCustomConfig() { - const cache = getLogStores(CacheKeys.CONFIG_STORE); - return (await cache.get(CacheKeys.CUSTOM_CONFIG)) || (await loadCustomConfig()); + const cache = getLogStores(CacheKeys.STATIC_CONFIG); + return (await cache.get(CacheKeys.LIBRECHAT_YAML_CONFIG)) || (await loadCustomConfig()); } /** diff --git a/api/server/services/Config/loadCustomConfig.js b/api/server/services/Config/loadCustomConfig.js index 2ff7d5373..efa5e8654 100644 --- a/api/server/services/Config/loadCustomConfig.js +++ b/api/server/services/Config/loadCustomConfig.js @@ -120,8 +120,8 @@ https://www.librechat.ai/docs/configuration/stt_tts`); .forEach((endpoint) => parseCustomParams(endpoint.name, endpoint.customParams)); if (customConfig.cache) { - const cache = getLogStores(CacheKeys.CONFIG_STORE); - await cache.set(CacheKeys.CUSTOM_CONFIG, customConfig); + const cache = getLogStores(CacheKeys.STATIC_CONFIG); + await cache.set(CacheKeys.LIBRECHAT_YAML_CONFIG, customConfig); } if (result.data.modelSpecs) { diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index a1510d99a..89a3aa138 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1122,85 +1122,88 @@ export enum CacheKeys { /** * Key for the config store namespace. */ - CONFIG_STORE = 'configStore', + CONFIG_STORE = 'CONFIG_STORE', /** - * Key for the config store namespace. + * Key for the roles cache. */ - ROLES = 'roles', + ROLES = 'ROLES', /** * Key for the plugins cache. */ - PLUGINS = 'plugins', + PLUGINS = 'PLUGINS', /** * Key for the title generation cache. */ - GEN_TITLE = 'genTitle', - /** + GEN_TITLE = 'GEN_TITLE', /** * Key for the tools cache. */ - TOOLS = 'tools', + TOOLS = 'TOOLS', /** * Key for the model config cache. */ - MODELS_CONFIG = 'modelsConfig', + MODELS_CONFIG = 'MODELS_CONFIG', /** * Key for the model queries cache. */ - MODEL_QUERIES = 'modelQueries', + MODEL_QUERIES = 'MODEL_QUERIES', /** * Key for the default startup config cache. */ - STARTUP_CONFIG = 'startupConfig', + STARTUP_CONFIG = 'STARTUP_CONFIG', /** * Key for the default endpoint config cache. */ - ENDPOINT_CONFIG = 'endpointsConfig', + ENDPOINT_CONFIG = 'ENDPOINT_CONFIG', /** * Key for accessing the model token config cache. */ - TOKEN_CONFIG = 'tokenConfig', + TOKEN_CONFIG = 'TOKEN_CONFIG', /** - * Key for the custom config cache. + * Key for the librechat yaml config cache. */ - CUSTOM_CONFIG = 'customConfig', + LIBRECHAT_YAML_CONFIG = 'LIBRECHAT_YAML_CONFIG', + /** + * Key for the static config namespace. + */ + STATIC_CONFIG = 'STATIC_CONFIG', /** * Key for accessing Abort Keys */ - ABORT_KEYS = 'abortKeys', + ABORT_KEYS = 'ABORT_KEYS', /** * Key for the override config cache. */ - OVERRIDE_CONFIG = 'overrideConfig', + OVERRIDE_CONFIG = 'OVERRIDE_CONFIG', /** * Key for the bans cache. */ - BANS = 'bans', + BANS = 'BANS', /** * Key for the encoded domains cache. * Used by Azure OpenAI Assistants. */ - ENCODED_DOMAINS = 'encoded_domains', + ENCODED_DOMAINS = 'ENCODED_DOMAINS', /** * Key for the cached audio run Ids. */ - AUDIO_RUNS = 'audioRuns', + AUDIO_RUNS = 'AUDIO_RUNS', /** * Key for in-progress messages. */ - MESSAGES = 'messages', + MESSAGES = 'MESSAGES', /** * Key for in-progress flow states. */ - FLOWS = 'flows', + FLOWS = 'FLOWS', /** * Key for individual MCP Tool Manifests. */ - MCP_TOOLS = 'mcp_tools', + MCP_TOOLS = 'MCP_TOOLS', /** * Key for pending chat requests (concurrency check) */ - PENDING_REQ = 'pending_req', + PENDING_REQ = 'PENDING_REQ', /** * Key for s3 check intervals per user */ @@ -1212,11 +1215,11 @@ export enum CacheKeys { /** * Key for OpenID session. */ - OPENID_SESSION = 'openid_session', + OPENID_SESSION = 'OPENID_SESSION', /** * Key for SAML session. */ - SAML_SESSION = 'saml_session', + SAML_SESSION = 'SAML_SESSION', } /** From deb928bf809a3a3f67646ca2bdf0a69acb612c04 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:36:14 -0400 Subject: [PATCH 006/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#8664)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/lv/translation.json | 44 ++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/client/src/locales/lv/translation.json b/client/src/locales/lv/translation.json index 3cf815139..4144546e5 100644 --- a/client/src/locales/lv/translation.json +++ b/client/src/locales/lv/translation.json @@ -160,6 +160,7 @@ "com_endpoint_anthropic_thinking_budget": "Nosaka maksimālo žetonu skaitu, ko Claude drīkst izmantot savā iekšējā domāšanas procesā. Lielāki budžeti var uzlabot atbilžu kvalitāti, nodrošinot rūpīgāku analīzi sarežģītām problēmām, lai gan Claude var neizmantot visu piešķirto budžetu, īpaši diapazonos virs 32 000. Šim iestatījumam jābūt zemākam par \"Maksimālie izvades tokeni\".", "com_endpoint_anthropic_topk": "Top-k maina to, kā modelis atlasa marķierus izvadei. Ja top-k ir 1, tas nozīmē, ka atlasītais marķieris ir visticamākais starp visiem modeļa vārdu krājumā esošajiem marķieriem (to sauc arī par alkatīgo dekodēšanu), savukārt, ja top-k ir 3, tas nozīmē, ka nākamais marķieris tiek izvēlēts no 3 visticamākajiem marķieriem (izmantojot temperatūru).", "com_endpoint_anthropic_topp": "`Top-p` maina to, kā modelis atlasa marķierus izvadei. Marķieri tiek atlasīti no K (skatīt parametru topK) ticamākās līdz vismazāk ticamajai, līdz to varbūtību summa ir vienāda ar `top-p` vērtību.", + "com_endpoint_anthropic_use_web_search": "Iespējojiet tīmekļa meklēšanas funkcionalitāti, izmantojot Anthropic iebūvētās meklēšanas iespējas. Tas ļauj modelim meklēt tīmeklī jaunāko informāciju un sniegt precīzākas un aktuālākas atbildes.", "com_endpoint_assistant": "Asistents", "com_endpoint_assistant_model": "Asistenta modelis", "com_endpoint_assistant_placeholder": "Lūdzu, labajā sānu panelī atlasiet asistentu.", @@ -197,6 +198,8 @@ "com_endpoint_deprecated": "Novecojis", "com_endpoint_deprecated_info": "Šis galapunkts ir novecojis un var tikt noņemts turpmākajās versijās; lūdzu, tā vietā izmantojiet aģenta galapunktu.", "com_endpoint_deprecated_info_a11y": "Spraudņa galapunkts ir novecojis un var tikt noņemts turpmākajās versijās; lūdzu, tā vietā izmantojiet aģenta galapunktu.", + "com_endpoint_disable_streaming": "Izslēgt atbilžu straumēšanu un saņemt visu atbildi uzreiz. Noderīgi tādiem modeļiem kā o3, kas pieprasa organizācijas pārbaudi straumēšanai.", + "com_endpoint_disable_streaming_label": "Atspējot straumēšanu", "com_endpoint_examples": " Iepriekšiestatījumi", "com_endpoint_export": "Eksportēt", "com_endpoint_export_share": "Eksportēt/kopīgot", @@ -328,11 +331,11 @@ "com_nav_balance_auto_refill_settings": "Automātiskās bilances papildināšanas iestatījumi", "com_nav_balance_day": "diena", "com_nav_balance_days": "dienas", - "com_nav_balance_every": "Katru", + "com_nav_balance_every": "Katras", "com_nav_balance_hour": "stunda", "com_nav_balance_hours": "stundas", "com_nav_balance_interval": "Intervāls:", - "com_nav_balance_last_refill": "Pēdējā bilances papildišanā:", + "com_nav_balance_last_refill": "Pēdējā bilances papildišana:", "com_nav_balance_minute": "minūte", "com_nav_balance_minutes": "minūtes", "com_nav_balance_month": "mēnesis", @@ -441,6 +444,8 @@ "com_nav_log_out": "Izrakstīties", "com_nav_long_audio_warning": "Garāku tekstu apstrāde prasīs ilgāku laiku.", "com_nav_maximize_chat_space": "Maksimāli izmantojiet sarunas telpu", + "com_nav_mcp_configure_server": "Konfigurēt {{0}}", + "com_nav_mcp_status_connecting": "{{0}} - Savienojas", "com_nav_mcp_vars_update_error": "Kļūda, atjauninot MCP pielāgotos lietotāja parametrus: {{0}}", "com_nav_mcp_vars_updated": "MCP pielāgotie lietotāja mainīgie ir veiksmīgi atjaunināti.", "com_nav_modular_chat": "Iespējot galapunktu pārslēgšanu sarunas laikā", @@ -521,6 +526,7 @@ "com_ui_2fa_verified": "Divfaktoru autentifikācija veiksmīgi verificēta", "com_ui_accept": "Es piekrītu", "com_ui_action_button": "Darbības poga", + "com_ui_active": "Aktīvais", "com_ui_add": "Pievienot", "com_ui_add_mcp": "Pievienot MCP", "com_ui_add_mcp_server": "Pievienot MCP serveri", @@ -573,6 +579,7 @@ "com_ui_archive_error": "Neizdevās arhivēt sarunu.", "com_ui_artifact_click": "Noklikšķiniet, lai atvērtu", "com_ui_artifacts": "Artefakti", + "com_ui_artifacts_options": "Artefaktu opcijas", "com_ui_artifacts_toggle": "Pārslēgt artefaktu lietotāja saskarni", "com_ui_artifacts_toggle_agent": "Iespējot artefaktus", "com_ui_ascending": "Augošā", @@ -590,6 +597,7 @@ "com_ui_attachment": "Pielikums", "com_ui_auth_type": "Autorizācijas veids", "com_ui_auth_url": "Autorizācijas URL", + "com_ui_authenticate": "Autentificēt", "com_ui_authentication": "Autentifikācija", "com_ui_authentication_type": "Autentifikācijas veids", "com_ui_auto": "Auto", @@ -647,8 +655,10 @@ "com_ui_confirm_action": "Apstiprināt darbību", "com_ui_confirm_admin_use_change": "Mainot šo iestatījumu, administratoriem, tostarp jums, tiks liegta piekļuve. Vai tiešām vēlaties turpināt?", "com_ui_confirm_change": "Apstiprināt izmaiņas", + "com_ui_connecting": "Savienojas", "com_ui_context": "Konteksts", "com_ui_continue": "Turpināt", + "com_ui_continue_oauth": "Turpināt ar OAuth", "com_ui_controls": "Pārvaldība", "com_ui_convo_delete_error": "Neizdevās izdzēst sarunu", "com_ui_copied": "Nokopēts!", @@ -699,11 +709,14 @@ "com_ui_delete_mcp_error": "Neizdevās izdzēst MCP serveri.", "com_ui_delete_mcp_success": "MCP serveris veiksmīgi izdzēsts", "com_ui_delete_memory": "Dzēst atmiņu", + "com_ui_delete_not_allowed": "Dzēšanas darbība nav atļauta", "com_ui_delete_prompt": "Vai dzēst uzvedni?", "com_ui_delete_shared_link": "Vai dzēst koplietoto saiti?", + "com_ui_delete_success": "Veiksmīgi dzēsts", "com_ui_delete_tool": "Dzēst rīku", "com_ui_delete_tool_confirm": "Vai tiešām vēlaties dzēst šo rīku?", "com_ui_deleted": "Dzēsts", + "com_ui_deleting_file": "Tiek dzēsts fails...", "com_ui_descending": "Dilstošs", "com_ui_description": "Apraksts", "com_ui_description_placeholder": "Pēc izvēles: ievadiet aprakstu, kas jāparāda uzvednē", @@ -755,6 +768,7 @@ "com_ui_feedback_tag_missing_image": "Tika gaidīts attēls", "com_ui_feedback_tag_not_helpful": "Trūka noderīgas informācijas", "com_ui_feedback_tag_not_matched": "Neatbilda manam pieprasījumam", + "com_ui_feedback_tag_one": " ", "com_ui_feedback_tag_other": "Cita problēma", "com_ui_feedback_tag_unjustified_refusal": "Atteicās bez iemesla", "com_ui_feedback_tag_zero": "Cita problēma", @@ -771,6 +785,7 @@ "com_ui_fork_change_default": "Noklusējuma atzarojuma opcija", "com_ui_fork_default": "Izmantot noklusējuma atzarojuma opciju", "com_ui_fork_error": "Sarunas atzarošanas laikā radās kļūda.", + "com_ui_fork_error_rate_limit": "Pārāk daudz atzaru pieprasījumu. Lūdzu, mēģiniet vēlreiz vēlāk", "com_ui_fork_from_message": "Izvēlieties atzarojuma opciju", "com_ui_fork_info_1": "Izmantojiet šo iestatījumu, lai atdalītu ziņu ar vēlamo darbību.", "com_ui_fork_info_2": "\"Sadalīšana\" attiecas uz jaunas sarunas izveidi, kas sākas/beidzas ar konkrētām ziņām pašreizējā sarunā, izveidojot kopiju atbilstoši atlasītajām opcijām.", @@ -803,6 +818,7 @@ "com_ui_good_morning": "Labrīt", "com_ui_happy_birthday": "Man šodien ir pirmā dzimšanas diena!", "com_ui_hide_image_details": "Slēpt attēla detaļas", + "com_ui_hide_password": "Paslēpt paroli", "com_ui_hide_qr": "Slēpt QR kodu", "com_ui_high": "Augsts", "com_ui_host": "Serveris", @@ -834,10 +850,20 @@ "com_ui_low": "Zems", "com_ui_manage": "Pārvaldīt", "com_ui_max_tags": "Maksimālais atļautais skaits ir {{0}}, izmantojot jaunākās vērtības.", + "com_ui_mcp_authenticated_success": "MCP serveris '{{0}}' veiksmīgi autentificēts", "com_ui_mcp_dialog_desc": "Lūdzu, ievadiet nepieciešamo informāciju zemāk.", "com_ui_mcp_enter_var": "Ievadiet vērtību {{0}}", + "com_ui_mcp_init_failed": "Neizdevās inicializēt MCP serveri", + "com_ui_mcp_initialize": "Inicializēt", + "com_ui_mcp_initialized_success": "MCP serveris '{{0}}' veiksmīgi inicializēts", + "com_ui_mcp_not_authenticated": "{{0}} nav autentificēts (nepieciešams OAuth).", + "com_ui_mcp_not_initialized": "{{0}} nav inicializēts", + "com_ui_mcp_oauth_cancelled": "OAuth pieteikšanās atcelta {{0}}", + "com_ui_mcp_oauth_no_url": "Nepieciešama OAuth autentifikācija, bet URL nav padots", + "com_ui_mcp_oauth_timeout": "OAuth pieteikšanās beidzās priekš {{0}}", "com_ui_mcp_server_not_found": "Serveris nav atrasts.", "com_ui_mcp_servers": "MCP serveri", + "com_ui_mcp_update_var": "Atjaunināt {{0}}", "com_ui_mcp_url": "MCP servera URL", "com_ui_medium": "Vidējs", "com_ui_memories": "Atmiņas", @@ -848,12 +874,17 @@ "com_ui_memories_allow_use": "Atļaut izmantot atmiņas", "com_ui_memories_filter": "Filtrēt atmiņas...", "com_ui_memory": "Atmiņa", + "com_ui_memory_already_exceeded": "Atmiņas krātuve jau ir pilna - tokeni pārsniegti par {{tokens}}. Izdzēsiet esošās atmiņas, pirms pievienojat jaunas.", "com_ui_memory_created": "Atmiņa veiksmīgi izveidota", "com_ui_memory_deleted": "Atmiņa izdzēsta", "com_ui_memory_deleted_items": "Dzēstās atmiņas", + "com_ui_memory_error": "Atmiņas kļūda", "com_ui_memory_key_exists": "Atmiņa ar šo atslēgu jau pastāv. Lūdzu, izmantojiet citu atslēgu.", + "com_ui_memory_key_validation": "Atmiņas atslēgā drīkst būt tikai mazie burti un pasvītrojumi.", + "com_ui_memory_storage_full": "Atmiņas krātuve ir pilna", "com_ui_memory_updated": "Atjaunināta saglabātā atmiņa", "com_ui_memory_updated_items": "Atjauninātas atmiņas", + "com_ui_memory_would_exceed": "Nevar saglabāt - pārsniegtu tokenu limitu par {{tokens}}. Izdzēsiet esošās atmiņas, lai atbrīvotu vietu.", "com_ui_mention": "Pieminiet galapunktu, assistentu vai sākotnējo iestatījumu, lai ātri uz to pārslēgtos", "com_ui_min_tags": "Nevar noņemt vairāk vērtību, vismaz {{0}} ir nepieciešamas.", "com_ui_misc": "Dažādi", @@ -887,10 +918,12 @@ "com_ui_oauth_error_missing_code": "Trūkst autorizācijas koda. Lūdzu, mēģiniet vēlreiz.", "com_ui_oauth_error_missing_state": "Trūkst stāvokļa parametrs. Lūdzu, mēģiniet vēlreiz.", "com_ui_oauth_error_title": "Autentifikācija neizdevās", + "com_ui_oauth_flow_desc": "Pabeidziet OAuth plūsmu jaunajā logā un pēc tam atgriezieties šeit.", "com_ui_oauth_success_description": "Jūsu autentifikācija bija veiksmīga. Šis logs aizvērsies pēc", "com_ui_oauth_success_title": "Autentifikācija veiksmīga", "com_ui_of": "no", "com_ui_off": "Izslēgts", + "com_ui_offline": "Bezsaistē", "com_ui_on": "Ieslēgts", "com_ui_openai": "OpenAI", "com_ui_optional": "(pēc izvēles)", @@ -923,6 +956,7 @@ "com_ui_regenerate_backup": "Atjaunot rezerves kodus", "com_ui_regenerating": "Atjaunošanās...", "com_ui_region": "Reģions", + "com_ui_reinitialize": "Reinicializēt", "com_ui_rename": "Pārdēvēt", "com_ui_rename_conversation": "Pārdēvēt sarunu", "com_ui_rename_failed": "Neizdevās pārdēvēt sarunu", @@ -962,6 +996,7 @@ "com_ui_select_search_plugin": "Meklēt spraudni pēc nosaukuma", "com_ui_select_search_provider": "Meklēšanas pakalpojumu sniedzējs pēc nosaukuma", "com_ui_select_search_region": "Meklēt reģionu pēc nosaukuma", + "com_ui_set": "Uzlikts", "com_ui_share": "Kopīgot", "com_ui_share_create_message": "Jūsu vārds un visas ziņas, ko pievienojat pēc kopīgošanas, paliek privātas.", "com_ui_share_delete_error": "Dzēšot koplietoto saiti, radās kļūda.", @@ -979,6 +1014,7 @@ "com_ui_show": "Rādīt", "com_ui_show_all": "Rādīt visu", "com_ui_show_image_details": "Rādīt attēla detaļas", + "com_ui_show_password": "Rādīt paroli", "com_ui_show_qr": "Rādīt QR kodu", "com_ui_sign_in_to_domain": "Pierakstīties {{0}}", "com_ui_simple": "Vienkāršs", @@ -1013,6 +1049,7 @@ "com_ui_unarchive": "Atarhivēt", "com_ui_unarchive_error": "Neizdevās atarhivēt sarunu", "com_ui_unknown": "Nezināms", + "com_ui_unset": "Neuzlikts", "com_ui_untitled": "Bez nosaukuma", "com_ui_update": "Atjauninājums", "com_ui_update_mcp_error": "Izveidojot vai atjauninot MCP, radās kļūda.", @@ -1052,6 +1089,7 @@ "com_ui_web_search_jina_key": "Ievadiet Jina API atslēgu", "com_ui_web_search_processing": "Rezultātu apstrāde", "com_ui_web_search_provider": "Meklēšanas nodrošinātājs", + "com_ui_web_search_provider_searxng": "SearXNG", "com_ui_web_search_provider_serper": "Serper API", "com_ui_web_search_provider_serper_key": "Iegūstiet savu Serper API atslēgu", "com_ui_web_search_reading": "Rezultātu lasīšana", @@ -1063,6 +1101,8 @@ "com_ui_web_search_scraper": "Scraper", "com_ui_web_search_scraper_firecrawl": "Firecrawl API", "com_ui_web_search_scraper_firecrawl_key": "Iegūstiet savu Firecrawl API atslēgu", + "com_ui_web_search_searxng_api_key": "Ievadiet SearXNG API atslēgu (pēc izvēles)", + "com_ui_web_search_searxng_instance_url": "SearXNG Instance URL", "com_ui_web_searching": "Meklēšana tīmeklī", "com_ui_web_searching_again": "Vēlreiz meklē tīmeklī", "com_ui_weekend_morning": "Priecīgu nedēļas nogali", From b050a0bf1ef91f2b46fb55e79a82138f2317bbb1 Mon Sep 17 00:00:00 2001 From: "Theo N. Truong" <644650+nhtruong@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:00:02 -0600 Subject: [PATCH 007/224] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20Redis=20Ping?= =?UTF-8?q?=20Interval=20Configuration=20(#8648)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danny Avila --- .env.example | 5 +++++ api/cache/cacheConfig.js | 1 + api/cache/cacheConfig.spec.js | 15 +++++++++++++++ api/cache/redisClients.js | 25 +++++++++++++++++-------- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index e375c60a8..23777fe26 100644 --- a/.env.example +++ b/.env.example @@ -627,6 +627,11 @@ HELP_AND_FAQ_URL=https://librechat.ai # Redis connection limits # REDIS_MAX_LISTENERS=40 +# Redis ping interval in seconds (0 = disabled, >0 = enabled) +# When set to a positive integer, Redis clients will ping the server at this interval to keep connections alive +# When unset or 0, no pinging is performed (recommended for most use cases) +# REDIS_PING_INTERVAL=300 + # Force specific cache namespaces to use in-memory storage even when Redis is enabled # Comma-separated list of CacheKeys (e.g., STATIC_CONFIG,ROLES,MESSAGES) # FORCED_IN_MEMORY_CACHE_NAMESPACES=STATIC_CONFIG,ROLES diff --git a/api/cache/cacheConfig.js b/api/cache/cacheConfig.js index 551107f7a..87c403bae 100644 --- a/api/cache/cacheConfig.js +++ b/api/cache/cacheConfig.js @@ -43,6 +43,7 @@ const cacheConfig = { REDIS_CA: process.env.REDIS_CA ? fs.readFileSync(process.env.REDIS_CA, 'utf8') : null, REDIS_KEY_PREFIX: process.env[REDIS_KEY_PREFIX_VAR] || REDIS_KEY_PREFIX || '', REDIS_MAX_LISTENERS: math(process.env.REDIS_MAX_LISTENERS, 40), + REDIS_PING_INTERVAL: math(process.env.REDIS_PING_INTERVAL, 0), CI: isEnabled(process.env.CI), DEBUG_MEMORY_CACHE: isEnabled(process.env.DEBUG_MEMORY_CACHE), diff --git a/api/cache/cacheConfig.spec.js b/api/cache/cacheConfig.spec.js index 0809d6f62..7d4078a84 100644 --- a/api/cache/cacheConfig.spec.js +++ b/api/cache/cacheConfig.spec.js @@ -14,6 +14,7 @@ describe('cacheConfig', () => { delete process.env.REDIS_KEY_PREFIX_VAR; delete process.env.REDIS_KEY_PREFIX; delete process.env.USE_REDIS; + delete process.env.REDIS_PING_INTERVAL; delete process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES; // Clear require cache @@ -107,6 +108,20 @@ describe('cacheConfig', () => { }); }); + describe('REDIS_PING_INTERVAL configuration', () => { + test('should default to 0 when REDIS_PING_INTERVAL is not set', () => { + const { cacheConfig } = require('./cacheConfig'); + expect(cacheConfig.REDIS_PING_INTERVAL).toBe(0); + }); + + test('should use provided REDIS_PING_INTERVAL value', () => { + process.env.REDIS_PING_INTERVAL = '300'; + + const { cacheConfig } = require('./cacheConfig'); + expect(cacheConfig.REDIS_PING_INTERVAL).toBe(300); + }); + }); + describe('FORCED_IN_MEMORY_CACHE_NAMESPACES validation', () => { test('should parse comma-separated cache keys correctly', () => { process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = ' ROLES, STATIC_CONFIG ,MESSAGES '; diff --git a/api/cache/redisClients.js b/api/cache/redisClients.js index 1a653ba13..5a633b196 100644 --- a/api/cache/redisClients.js +++ b/api/cache/redisClients.js @@ -25,10 +25,13 @@ if (cacheConfig.USE_REDIS) { ? new IoRedis(cacheConfig.REDIS_URI, redisOptions) : new IoRedis.Cluster(cacheConfig.REDIS_URI, { redisOptions }); - // Pinging the Redis server every 5 minutes to keep the connection alive - const pingInterval = setInterval(() => ioredisClient.ping(), 5 * 60 * 1000); - ioredisClient.on('close', () => clearInterval(pingInterval)); - ioredisClient.on('end', () => clearInterval(pingInterval)); + // Pinging the Redis server to keep the connection alive (if enabled) + let pingInterval = null; + if (cacheConfig.REDIS_PING_INTERVAL > 0) { + pingInterval = setInterval(() => ioredisClient.ping(), cacheConfig.REDIS_PING_INTERVAL * 1000); + ioredisClient.on('close', () => clearInterval(pingInterval)); + ioredisClient.on('end', () => clearInterval(pingInterval)); + } } /** @type {import('@keyv/redis').RedisClient | import('@keyv/redis').RedisCluster | null} */ @@ -48,10 +51,16 @@ if (cacheConfig.USE_REDIS) { keyvRedisClient.setMaxListeners(cacheConfig.REDIS_MAX_LISTENERS); - // Pinging the Redis server every 5 minutes to keep the connection alive - const keyvPingInterval = setInterval(() => keyvRedisClient.ping(), 5 * 60 * 1000); - keyvRedisClient.on('disconnect', () => clearInterval(keyvPingInterval)); - keyvRedisClient.on('end', () => clearInterval(keyvPingInterval)); + // Pinging the Redis server to keep the connection alive (if enabled) + let pingInterval = null; + if (cacheConfig.REDIS_PING_INTERVAL > 0) { + pingInterval = setInterval( + () => keyvRedisClient.ping(), + cacheConfig.REDIS_PING_INTERVAL * 1000, + ); + keyvRedisClient.on('disconnect', () => clearInterval(pingInterval)); + keyvRedisClient.on('end', () => clearInterval(pingInterval)); + } } module.exports = { ioredisClient, keyvRedisClient, GLOBAL_PREFIX_SEPARATOR }; From 1636af1f27e6c4458a58a51cd9f90994f0ad0776 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 25 Jul 2025 11:23:38 -0400 Subject: [PATCH 008/224] =?UTF-8?q?=F0=9F=93=A6=20chore:=20Bump=20`mongodb?= =?UTF-8?q?-memory-server`=20to=20v10.1.4=20(#8669)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/package.json | 2 +- package-lock.json | 111 ++++++++++++++++++++-------------------------- 2 files changed, 49 insertions(+), 64 deletions(-) diff --git a/api/package.json b/api/package.json index d94aa8f53..1b1d05b22 100644 --- a/api/package.json +++ b/api/package.json @@ -119,7 +119,7 @@ }, "devDependencies": { "jest": "^29.7.0", - "mongodb-memory-server": "^10.1.3", + "mongodb-memory-server": "^10.1.4", "nodemon": "^3.0.3", "supertest": "^7.1.0" } diff --git a/package-lock.json b/package-lock.json index 9fda62f0e..af46a0f8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -135,7 +135,7 @@ }, "devDependencies": { "jest": "^29.7.0", - "mongodb-memory-server": "^10.1.3", + "mongodb-memory-server": "^10.1.4", "nodemon": "^3.0.3", "supertest": "^7.1.0" } @@ -2453,45 +2453,6 @@ "node": ">=16" } }, - "api/node_modules/mongodb-memory-server": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-10.1.3.tgz", - "integrity": "sha512-QCUjsIIXSYv/EgkpDAjfhlqRKo6N+qR6DD43q4lyrCVn24xQmvlArdWHW/Um5RS4LkC9YWC3XveSncJqht2Hbg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "mongodb-memory-server-core": "10.1.3", - "tslib": "^2.7.0" - }, - "engines": { - "node": ">=16.20.1" - } - }, - "api/node_modules/mongodb-memory-server-core": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-10.1.3.tgz", - "integrity": "sha512-ayBQHeV74wRHhgcAKpxHYI4th9Ufidy/m3XhJnLFRufKsOyDsyHYU3Zxv5Fm4hxsWE6wVd0GAVcQ7t7XNkivOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-mutex": "^0.5.0", - "camelcase": "^6.3.0", - "debug": "^4.3.7", - "find-cache-dir": "^3.3.2", - "follow-redirects": "^1.15.9", - "https-proxy-agent": "^7.0.5", - "mongodb": "^6.9.0", - "new-find-package-json": "^2.0.0", - "semver": "^7.6.3", - "tar-stream": "^3.1.7", - "tslib": "^2.7.0", - "yauzl": "^3.1.3" - }, - "engines": { - "node": ">=16.20.1" - } - }, "api/node_modules/mongoose": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", @@ -28900,6 +28861,7 @@ "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.4.0" } @@ -29000,10 +28962,11 @@ } }, "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", - "dev": true + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/babel-jest": { "version": "29.7.0", @@ -29233,6 +29196,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", + "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -29551,6 +29522,7 @@ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -32905,7 +32877,8 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -39478,9 +39451,9 @@ } }, "node_modules/mongodb-memory-server-core/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -39603,6 +39576,7 @@ "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.4" }, @@ -40778,7 +40752,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", @@ -42458,12 +42433,6 @@ } ] }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -45079,13 +45048,17 @@ } }, "node_modules/streamx": { - "version": "2.15.7", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.7.tgz", - "integrity": "sha512-NPEKS5+yjyo597eafGbKW5ujh7Sm6lDLHZQd/lRSz6S0VarpADBJItqfB4PnwpS+472oob1GX5cCY9vzfJpHUA==", + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", "dev": true, + "license": "MIT", "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" } }, "node_modules/strict-event-emitter": { @@ -45711,6 +45684,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, + "license": "MIT", "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", @@ -45890,6 +45864,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -48514,10 +48498,11 @@ } }, "node_modules/yauzl": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", - "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", + "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" From 26f23c6aaf741c4a2255d549ccf6c1c58f63641b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 25 Jul 2025 11:26:20 -0400 Subject: [PATCH 009/224] =?UTF-8?q?=F0=9F=93=A6=20chore:=20Bump=20`@node-s?= =?UTF-8?q?aml/passport-saml`=20to=20v5.1.0=20(#8670)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/package.json | 2 +- package-lock.json | 268 ++++++++++++++++++++++++---------------------- 2 files changed, 141 insertions(+), 129 deletions(-) diff --git a/api/package.json b/api/package.json index 1b1d05b22..066e02428 100644 --- a/api/package.json +++ b/api/package.json @@ -52,7 +52,7 @@ "@librechat/agents": "^2.4.68", "@librechat/api": "*", "@librechat/data-schemas": "*", - "@node-saml/passport-saml": "^5.0.0", + "@node-saml/passport-saml": "^5.1.0", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", "bcryptjs": "^2.4.3", diff --git a/package-lock.json b/package-lock.json index af46a0f8c..1e7cd1e36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "@librechat/agents": "^2.4.68", "@librechat/api": "*", "@librechat/data-schemas": "*", - "@node-saml/passport-saml": "^5.0.0", + "@node-saml/passport-saml": "^5.1.0", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", "bcryptjs": "^2.4.3", @@ -1336,6 +1336,64 @@ } } }, + "api/node_modules/@node-saml/node-saml": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.1.0.tgz", + "integrity": "sha512-t3cJnZ4aC7HhPZ6MGylGZULvUtBOZ6FzuUndaHGXjmIZHXnLfC/7L8a57O9Q9V7AxJGKAiRM5zu2wNm9EsvQpw==", + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.12", + "@types/qs": "^6.9.18", + "@types/xml-encryption": "^1.2.4", + "@types/xml2js": "^0.4.14", + "@xmldom/is-dom-node": "^1.0.1", + "@xmldom/xmldom": "^0.8.10", + "debug": "^4.4.0", + "xml-crypto": "^6.1.2", + "xml-encryption": "^3.1.0", + "xml2js": "^0.6.2", + "xmlbuilder": "^15.1.1", + "xpath": "^0.0.34" + }, + "engines": { + "node": ">= 18" + } + }, + "api/node_modules/@node-saml/passport-saml": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.1.0.tgz", + "integrity": "sha512-pBm+iFjv9eihcgeJuSUs4c0AuX1QEFdHwP8w1iaWCfDzXdeWZxUBU5HT2bY2S4dvNutcy+A9hYsH7ZLBGtgwDg==", + "license": "MIT", + "dependencies": { + "@node-saml/node-saml": "^5.1.0", + "@types/express": "^4.17.23", + "@types/passport": "^1.0.17", + "@types/passport-strategy": "^0.2.38", + "passport": "^0.7.0", + "passport-strategy": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "api/node_modules/@node-saml/passport-saml/node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, "api/node_modules/@smithy/abort-controller": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", @@ -2081,6 +2139,36 @@ "node": ">=18.0.0" } }, + "api/node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "api/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "api/node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, "api/node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -2635,6 +2723,15 @@ "winston": "^3" } }, + "api/node_modules/xpath": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", + "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==", + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, "client": { "name": "@librechat/frontend", "version": "v0.7.9", @@ -22646,133 +22743,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@node-saml/node-saml": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.0.0.tgz", - "integrity": "sha512-4JGubfHgL5egpXiuo9bupSGn6mgpfOQ/brZZvv2Qiho5aJmW7O1khbjdB7tsTsCvNFtLLjQqm3BmvcRicJyA2g==", - "dependencies": { - "@types/debug": "^4.1.12", - "@types/qs": "^6.9.11", - "@types/xml-encryption": "^1.2.4", - "@types/xml2js": "^0.4.14", - "@xmldom/is-dom-node": "^1.0.1", - "@xmldom/xmldom": "^0.8.10", - "debug": "^4.3.4", - "xml-crypto": "^6.0.0", - "xml-encryption": "^3.0.2", - "xml2js": "^0.6.2", - "xmlbuilder": "^15.1.1", - "xpath": "^0.0.34" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@node-saml/node-saml/node_modules/xml-encryption": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.1.0.tgz", - "integrity": "sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q==", - "dependencies": { - "@xmldom/xmldom": "^0.8.5", - "escape-html": "^1.0.3", - "xpath": "0.0.32" - } - }, - "node_modules/@node-saml/node-saml/node_modules/xml-encryption/node_modules/xpath": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", - "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/@node-saml/node-saml/node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@node-saml/node-saml/node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@node-saml/node-saml/node_modules/xpath": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", - "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==", - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/@node-saml/passport-saml": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.0.0.tgz", - "integrity": "sha512-7miY7Id6UkP39+6HO68e3/V6eJwszytEQl+oCh0R/gbzp5nHA/WI1mvrI6NNUVq5gC5GEnDS8GTw7oj+Kx499w==", - "license": "MIT", - "dependencies": { - "@node-saml/node-saml": "^5.0.0", - "@types/express": "^4.17.21", - "@types/passport": "^1.0.16", - "@types/passport-strategy": "^0.2.38", - "passport": "^0.7.0", - "passport-strategy": "^1.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@node-saml/passport-saml/node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@node-saml/passport-saml/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@node-saml/passport-saml/node_modules/passport": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", - "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", - "license": "MIT", - "dependencies": { - "passport-strategy": "1.x.x", - "pause": "0.0.1", - "utils-merge": "^1.0.1" - }, - "engines": { - "node": ">= 0.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -48381,6 +48351,26 @@ "node": ">=16" } }, + "node_modules/xml-encryption": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.1.0.tgz", + "integrity": "sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.5", + "escape-html": "^1.0.3", + "xpath": "0.0.32" + } + }, + "node_modules/xml-encryption/node_modules/xpath": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", + "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -48390,6 +48380,28 @@ "node": ">=12" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", From 52512463132bfd5daf5c78a020e134dc395db7a9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 25 Jul 2025 12:33:05 -0400 Subject: [PATCH 010/224] =?UTF-8?q?=F0=9F=93=B1=20refactor:=20Redis=20Clie?= =?UTF-8?q?nt=20Error=20Logging=20and=20Ping=20only=20when=20Ready=20(#867?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📱 refactor: Redis Client Error Logging and Ping only when Ready * chore: intellisense for warning comment for Keyv Redis client regarding prefix support --- api/cache/redisClients.js | 58 +++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/api/cache/redisClients.js b/api/cache/redisClients.js index 5a633b196..46c2813e9 100644 --- a/api/cache/redisClients.js +++ b/api/cache/redisClients.js @@ -1,6 +1,7 @@ const IoRedis = require('ioredis'); -const { cacheConfig } = require('./cacheConfig'); +const { logger } = require('@librechat/data-schemas'); const { createClient, createCluster } = require('@keyv/redis'); +const { cacheConfig } = require('./cacheConfig'); const GLOBAL_PREFIX_SEPARATOR = '::'; @@ -25,20 +26,37 @@ if (cacheConfig.USE_REDIS) { ? new IoRedis(cacheConfig.REDIS_URI, redisOptions) : new IoRedis.Cluster(cacheConfig.REDIS_URI, { redisOptions }); - // Pinging the Redis server to keep the connection alive (if enabled) + ioredisClient.on('error', (err) => { + logger.error('ioredis client error:', err); + }); + + /** Ping Interval to keep the Redis server connection alive (if enabled) */ let pingInterval = null; + const clearPingInterval = () => { + if (pingInterval) { + clearInterval(pingInterval); + pingInterval = null; + } + }; + if (cacheConfig.REDIS_PING_INTERVAL > 0) { - pingInterval = setInterval(() => ioredisClient.ping(), cacheConfig.REDIS_PING_INTERVAL * 1000); - ioredisClient.on('close', () => clearInterval(pingInterval)); - ioredisClient.on('end', () => clearInterval(pingInterval)); + pingInterval = setInterval(() => { + if (ioredisClient && ioredisClient.status === 'ready') { + ioredisClient.ping(); + } + }, cacheConfig.REDIS_PING_INTERVAL * 1000); + ioredisClient.on('close', clearPingInterval); + ioredisClient.on('end', clearPingInterval); } } /** @type {import('@keyv/redis').RedisClient | import('@keyv/redis').RedisCluster | null} */ let keyvRedisClient = null; if (cacheConfig.USE_REDIS) { - // ** WARNING ** Keyv Redis client does not support Prefix like ioredis above. - // The prefix feature will be handled by the Keyv-Redis store in cacheFactory.js + /** + * ** WARNING ** Keyv Redis client does not support Prefix like ioredis above. + * The prefix feature will be handled by the Keyv-Redis store in cacheFactory.js + */ const redisOptions = { username, password, socket: { tls: ca != null, ca } }; keyvRedisClient = @@ -51,15 +69,27 @@ if (cacheConfig.USE_REDIS) { keyvRedisClient.setMaxListeners(cacheConfig.REDIS_MAX_LISTENERS); - // Pinging the Redis server to keep the connection alive (if enabled) + keyvRedisClient.on('error', (err) => { + logger.error('@keyv/redis client error:', err); + }); + + /** Ping Interval to keep the Redis server connection alive (if enabled) */ let pingInterval = null; + const clearPingInterval = () => { + if (pingInterval) { + clearInterval(pingInterval); + pingInterval = null; + } + }; + if (cacheConfig.REDIS_PING_INTERVAL > 0) { - pingInterval = setInterval( - () => keyvRedisClient.ping(), - cacheConfig.REDIS_PING_INTERVAL * 1000, - ); - keyvRedisClient.on('disconnect', () => clearInterval(pingInterval)); - keyvRedisClient.on('end', () => clearInterval(pingInterval)); + pingInterval = setInterval(() => { + if (keyvRedisClient && keyvRedisClient.isReady) { + keyvRedisClient.ping(); + } + }, cacheConfig.REDIS_PING_INTERVAL * 1000); + keyvRedisClient.on('disconnect', clearPingInterval); + keyvRedisClient.on('end', clearPingInterval); } } From e75beb92b3b17ffebd2d5e70c713cb812fc5f1fa Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 25 Jul 2025 13:45:22 -0400 Subject: [PATCH 011/224] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20chore:=20Remove?= =?UTF-8?q?=20Workflows=20for=20Changelogs=20(#8673)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generate-release-changelog-pr.yml | 95 ---------------- .../generate-unreleased-changelog-pr.yml | 107 ------------------ 2 files changed, 202 deletions(-) delete mode 100644 .github/workflows/generate-release-changelog-pr.yml delete mode 100644 .github/workflows/generate-unreleased-changelog-pr.yml diff --git a/.github/workflows/generate-release-changelog-pr.yml b/.github/workflows/generate-release-changelog-pr.yml deleted file mode 100644 index 405f0ca6d..000000000 --- a/.github/workflows/generate-release-changelog-pr.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Generate Release Changelog PR - -on: - push: - tags: - - 'v*.*.*' - workflow_dispatch: - -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_name }}" - base: main - branch: "changelog/${{ github.ref_name }}" - reviewers: danny-avila - title: "📜 docs: Changelog for release ${{ github.ref_name }}" - body: | - **Description**: - - This PR updates the CHANGELOG.md by removing the "Unreleased" section and adding new release notes for release ${{ github.ref_name }} above previous releases. diff --git a/.github/workflows/generate-unreleased-changelog-pr.yml b/.github/workflows/generate-unreleased-changelog-pr.yml deleted file mode 100644 index 133e19f1e..000000000 --- a/.github/workflows/generate-unreleased-changelog-pr.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: Generate Unreleased Changelog PR - -on: - schedule: - - cron: "0 0 * * 1" # Runs every Monday at 00:00 UTC - workflow_dispatch: - -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: "📜 docs: 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. From cd436dc6a8078a04d58d27a453e7cba9a5481f3c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 25 Jul 2025 14:06:16 -0400 Subject: [PATCH 012/224] =?UTF-8?q?=F0=9F=93=A6=20chore:=20Update=20`@mode?= =?UTF-8?q?lcontextprotocol/sdk`=20to=20v1.17.0=20(#8674)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📦 chore: Update `@modelcontextprotocol/sdk` to v1.17.0 * refactor: unused package detection by extracting workspace dependencies in GitHub Actions workflow * chore: Enhance unused package detection by including peerDependencies extraction in GitHub Actions workflow * fix: Ensure safe extraction of dependencies and peerDependencies in unused package detection workflow --- .github/workflows/unused-packages.yml | 48 +++++++++++++++++++++++++-- api/package.json | 1 + package-lock.json | 39 ++++------------------ packages/api/package.json | 2 +- 4 files changed, 54 insertions(+), 36 deletions(-) diff --git a/.github/workflows/unused-packages.yml b/.github/workflows/unused-packages.yml index dc6ce3ba5..5429a1abd 100644 --- a/.github/workflows/unused-packages.yml +++ b/.github/workflows/unused-packages.yml @@ -79,12 +79,52 @@ jobs: extract_deps_from_code "client" client_used_code.txt extract_deps_from_code "api" api_used_code.txt + - name: Extract Workspace Dependencies + id: extract-workspace-deps + run: | + # Function to get dependencies from a workspace package that are used by another package + get_workspace_package_deps() { + local package_json=$1 + local output_file=$2 + + # Get all workspace dependencies (starting with @librechat/) + if [[ -f "$package_json" ]]; then + local workspace_deps=$(jq -r '.dependencies // {} | to_entries[] | select(.key | startswith("@librechat/")) | .key' "$package_json" 2>/dev/null || echo "") + + # For each workspace dependency, get its dependencies + for dep in $workspace_deps; do + # Convert @librechat/api to packages/api + local workspace_path=$(echo "$dep" | sed 's/@librechat\//packages\//') + local workspace_package_json="${workspace_path}/package.json" + + if [[ -f "$workspace_package_json" ]]; then + # Extract all dependencies from the workspace package + jq -r '.dependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file" + # Also extract peerDependencies + jq -r '.peerDependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file" + fi + done + fi + + if [[ -f "$output_file" ]]; then + sort -u "$output_file" -o "$output_file" + else + touch "$output_file" + fi + } + + # Get workspace dependencies for each package + get_workspace_package_deps "package.json" root_workspace_deps.txt + get_workspace_package_deps "client/package.json" client_workspace_deps.txt + get_workspace_package_deps "api/package.json" api_workspace_deps.txt + - name: Run depcheck for root package.json id: check-root run: | if [[ -f "package.json" ]]; then UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "") - UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat root_used_deps.txt root_used_code.txt | sort) || echo "") + # Exclude dependencies used in scripts, code, and workspace packages + UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat root_used_deps.txt root_used_code.txt root_workspace_deps.txt | sort) || echo "") echo "ROOT_UNUSED<> $GITHUB_ENV echo "$UNUSED" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV @@ -97,7 +137,8 @@ jobs: chmod -R 755 client cd client UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "") - UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt | sort) || echo "") + # Exclude dependencies used in scripts, code, and workspace packages + UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt ../client_workspace_deps.txt | sort) || echo "") # Filter out false positives UNUSED=$(echo "$UNUSED" | grep -v "^micromark-extension-llm-math$" || echo "") echo "CLIENT_UNUSED<> $GITHUB_ENV @@ -113,7 +154,8 @@ jobs: chmod -R 755 api cd api UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "") - UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt | sort) || echo "") + # Exclude dependencies used in scripts, code, and workspace packages + UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt ../api_workspace_deps.txt | sort) || echo "") echo "API_UNUSED<> $GITHUB_ENV echo "$UNUSED" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV diff --git a/api/package.json b/api/package.json index 066e02428..229f9527c 100644 --- a/api/package.json +++ b/api/package.json @@ -52,6 +52,7 @@ "@librechat/agents": "^2.4.68", "@librechat/api": "*", "@librechat/data-schemas": "*", + "@modelcontextprotocol/sdk": "^1.17.0", "@node-saml/passport-saml": "^5.1.0", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", diff --git a/package-lock.json b/package-lock.json index 1e7cd1e36..0c610c2f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,7 @@ "@librechat/agents": "^2.4.68", "@librechat/api": "*", "@librechat/data-schemas": "*", + "@modelcontextprotocol/sdk": "^1.17.0", "@node-saml/passport-saml": "^5.1.0", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", @@ -22391,11 +22392,10 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.16.0.tgz", - "integrity": "sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.0.tgz", + "integrity": "sha512-qFfbWFA7r1Sd8D697L7GkTd36yqDuTkvz0KfOGkgXR8EUhQn3/EDNIR/qUdQNMT8IjmasBvHWuXeisxtXTQT2g==", "license": "MIT", - "peer": true, "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", @@ -22419,7 +22419,6 @@ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", - "peer": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -22433,7 +22432,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -22450,7 +22448,6 @@ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -22471,7 +22468,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -22484,7 +22480,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.6.0" } @@ -22494,7 +22489,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -22537,7 +22531,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -22555,7 +22548,6 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -22565,7 +22557,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -22577,15 +22568,13 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -22595,7 +22584,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -22608,7 +22596,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -22618,7 +22605,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -22631,7 +22617,6 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -22641,7 +22626,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "side-channel": "^1.1.0" }, @@ -22657,7 +22641,6 @@ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -22673,7 +22656,6 @@ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", @@ -22696,7 +22678,6 @@ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", - "peer": true, "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -22712,7 +22693,6 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", - "peer": true, "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -32764,7 +32744,6 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "peer": true, "engines": { "node": ">= 16" }, @@ -35349,8 +35328,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-reference": { "version": "1.2.1", @@ -40635,7 +40613,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=16" } @@ -40767,7 +40744,6 @@ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.20.0" } @@ -44214,7 +44190,6 @@ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -48616,7 +48591,7 @@ "@langchain/core": "^0.3.62", "@librechat/agents": "^2.4.68", "@librechat/data-schemas": "*", - "@modelcontextprotocol/sdk": "^1.16.0", + "@modelcontextprotocol/sdk": "^1.17.0", "axios": "^1.8.2", "diff": "^7.0.0", "eventsource": "^3.0.2", diff --git a/packages/api/package.json b/packages/api/package.json index 8ba82f34f..62f16900d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -72,7 +72,7 @@ "@langchain/core": "^0.3.62", "@librechat/agents": "^2.4.68", "@librechat/data-schemas": "*", - "@modelcontextprotocol/sdk": "^1.16.0", + "@modelcontextprotocol/sdk": "^1.17.0", "axios": "^1.8.2", "diff": "^7.0.0", "eventsource": "^3.0.2", From 545a9099537c85ba1bd942b9f99215ecf5e6ee15 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:51:42 -0700 Subject: [PATCH 013/224] =?UTF-8?q?=F0=9F=97=82=EF=B8=8F=20refactor:=20Mak?= =?UTF-8?q?e=20`MCPSubMenu`=20consistent=20with=20`MCPSelect`=20(#8650)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactored MCPSelect and MCPSubMenu components to utilize a new custom hook, `useMCPServerManager`, for improved state management and server initialization logic. - Added functionality to handle simultaneous MCP server initialization requests, including cancellation and user notifications. - Updated translation files to include new messages for initialization cancellation. - Improved the configuration dialog handling for MCP servers, streamlining the user experience when managing server settings. --- api/server/routes/mcp.js | 3 +- .../src/components/Chat/Input/MCPSelect.tsx | 315 ++--------------- .../src/components/Chat/Input/MCPSubMenu.tsx | 172 +++++---- .../components/Chat/Input/ToolsDropdown.tsx | 30 +- .../hooks/MCP/useMCPServerInitialization.ts | 78 +++-- client/src/hooks/MCP/useMCPServerManager.ts | 328 ++++++++++++++++++ client/src/locales/en/translation.json | 1 + 7 files changed, 503 insertions(+), 424 deletions(-) create mode 100644 client/src/hooks/MCP/useMCPServerManager.ts diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index 5e1db18e6..c49ba4cc3 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -331,7 +331,8 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`); - const config = await loadCustomConfig(); + const printConfig = false; + const config = await loadCustomConfig(printConfig); if (!config || !config.mcpServers || !config.mcpServers[serverName]) { return res.status(404).json({ error: `MCP server '${serverName}' not found in configuration`, diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index a3916def6..2f9b4071c 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -1,114 +1,21 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { Constants, QueryKeys } from 'librechat-data-provider'; -import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider'; -import React, { memo, useCallback, useState, useMemo, useRef } from 'react'; -import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query'; -import MCPConfigDialog, { ConfigFieldDetail } from '~/components/ui/MCP/MCPConfigDialog'; -import { useMCPServerInitialization } from '~/hooks/MCP/useMCPServerInitialization'; +import React, { memo, useCallback } from 'react'; +import MCPConfigDialog from '~/components/ui/MCP/MCPConfigDialog'; import MCPServerStatusIcon from '~/components/ui/MCP/MCPServerStatusIcon'; -import { useToastContext, useBadgeRowContext } from '~/Providers'; import MultiSelect from '~/components/ui/MultiSelect'; import { MCPIcon } from '~/components/svg'; -import { useLocalize } from '~/hooks'; +import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager'; function MCPSelect() { - const localize = useLocalize(); - const { showToast } = useToastContext(); - const { mcpSelect, startupConfig } = useBadgeRowContext(); - const { mcpValues, setMCPValues, mcpToolDetails, isPinned } = mcpSelect; - - // Get all configured MCP servers from config that allow chat menu - const configuredServers = useMemo(() => { - if (!startupConfig?.mcpServers) { - return []; - } - return Object.entries(startupConfig.mcpServers) - .filter(([, config]) => config.chatMenu !== false) - .map(([serverName]) => serverName); - }, [startupConfig?.mcpServers]); - - const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); - const [selectedToolForConfig, setSelectedToolForConfig] = useState(null); - const previousFocusRef = useRef(null); - - const queryClient = useQueryClient(); - - const updateUserPluginsMutation = useUpdateUserPluginsMutation({ - onSuccess: async () => { - showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' }); - - // tools so we dont leave tools available for use in chat if we revoke and thus kill mcp server - // auth values so customUserVars flags are updated in customUserVarsSection - // connection status so connection indicators are updated in the dropdown - await Promise.all([ - queryClient.refetchQueries([QueryKeys.tools]), - queryClient.refetchQueries([QueryKeys.mcpAuthValues]), - queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]), - ]); - }, - onError: (error: unknown) => { - console.error('Error updating MCP auth:', error); - showToast({ - message: localize('com_nav_mcp_vars_update_error'), - status: 'error', - }); - }, - }); - - // Use the shared initialization hook - const { initializeServer, isInitializing, connectionStatus, cancelOAuthFlow, isCancellable } = - useMCPServerInitialization({ - onSuccess: (serverName) => { - // Add to selected values after successful initialization - const currentValues = mcpValues ?? []; - if (!currentValues.includes(serverName)) { - setMCPValues([...currentValues, serverName]); - } - }, - onError: (serverName) => { - // Find the tool/server configuration - const tool = mcpToolDetails?.find((t) => t.name === serverName); - const serverConfig = startupConfig?.mcpServers?.[serverName]; - const serverStatus = connectionStatus[serverName]; - - // Check if this server would show a config button - const hasAuthConfig = - (tool?.authConfig && tool.authConfig.length > 0) || - (serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0); - - // Only open dialog if the server would have shown a config button - // (disconnected/error states always show button, connected only shows if hasAuthConfig) - const wouldShowButton = - !serverStatus || - serverStatus.connectionState === 'disconnected' || - serverStatus.connectionState === 'error' || - (serverStatus.connectionState === 'connected' && hasAuthConfig); - - if (!wouldShowButton) { - return; // Don't open dialog if no button would be shown - } - - // Create tool object if it doesn't exist - const configTool = tool || { - name: serverName, - pluginKey: `${Constants.mcp_prefix}${serverName}`, - authConfig: serverConfig?.customUserVars - ? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({ - authField: key, - label: config.title, - description: config.description, - })) - : [], - authenticated: false, - }; - - previousFocusRef.current = document.activeElement as HTMLElement; - - // Open the config dialog on error - setSelectedToolForConfig(configTool); - setIsConfigModalOpen(true); - }, - }); + const { + configuredServers, + mcpValues, + isPinned, + placeholderText, + batchToggleServers, + getServerStatusIconProps, + getConfigDialogProps, + localize, + } = useMCPServerManager(); const renderSelectedValues = useCallback( (values: string[], placeholder?: string) => { @@ -123,137 +30,9 @@ function MCPSelect() { [localize], ); - const handleConfigSave = useCallback( - (targetName: string, authData: Record) => { - if (selectedToolForConfig && selectedToolForConfig.name === targetName) { - // Use the pluginKey directly since it's already in the correct format - console.log( - `[MCP Select] Saving config for ${targetName}, pluginKey: ${`${Constants.mcp_prefix}${targetName}`}`, - ); - const payload: TUpdateUserPlugins = { - pluginKey: `${Constants.mcp_prefix}${targetName}`, - action: 'install', - auth: authData, - }; - updateUserPluginsMutation.mutate(payload); - } - }, - [selectedToolForConfig, updateUserPluginsMutation], - ); - - const handleConfigRevoke = useCallback( - (targetName: string) => { - if (selectedToolForConfig && selectedToolForConfig.name === targetName) { - // Use the pluginKey directly since it's already in the correct format - const payload: TUpdateUserPlugins = { - pluginKey: `${Constants.mcp_prefix}${targetName}`, - action: 'uninstall', - auth: {}, - }; - updateUserPluginsMutation.mutate(payload); - - // Remove the server from selected values after revoke - const currentValues = mcpValues ?? []; - const filteredValues = currentValues.filter((name) => name !== targetName); - setMCPValues(filteredValues); - } - }, - [selectedToolForConfig, updateUserPluginsMutation, mcpValues, setMCPValues], - ); - - const handleSave = useCallback( - (authData: Record) => { - if (selectedToolForConfig) { - handleConfigSave(selectedToolForConfig.name, authData); - } - }, - [selectedToolForConfig, handleConfigSave], - ); - - const handleRevoke = useCallback(() => { - if (selectedToolForConfig) { - handleConfigRevoke(selectedToolForConfig.name); - } - }, [selectedToolForConfig, handleConfigRevoke]); - - const handleDialogOpenChange = useCallback((open: boolean) => { - setIsConfigModalOpen(open); - - // Restore focus when dialog closes - if (!open && previousFocusRef.current) { - // Use setTimeout to ensure the dialog has fully closed before restoring focus - setTimeout(() => { - if (previousFocusRef.current && typeof previousFocusRef.current.focus === 'function') { - previousFocusRef.current.focus(); - } - previousFocusRef.current = null; - }, 0); - } - }, []); - - // Get connection status for all MCP servers (now from hook) - // Remove the duplicate useMCPConnectionStatusQuery since it's in the hook - - // Modified setValue function that attempts to initialize disconnected servers - const filteredSetMCPValues = useCallback( - (values: string[]) => { - // Separate connected and disconnected servers - const connectedServers: string[] = []; - const disconnectedServers: string[] = []; - - values.forEach((serverName) => { - const serverStatus = connectionStatus[serverName]; - if (serverStatus?.connectionState === 'connected') { - connectedServers.push(serverName); - } else { - disconnectedServers.push(serverName); - } - }); - - // Only set connected servers as selected values - setMCPValues(connectedServers); - - // Attempt to initialize each disconnected server (once) - disconnectedServers.forEach((serverName) => { - initializeServer(serverName); - }); - }, - [connectionStatus, setMCPValues, initializeServer], - ); - const renderItemContent = useCallback( (serverName: string, defaultContent: React.ReactNode) => { - const tool = mcpToolDetails?.find((t) => t.name === serverName); - const serverStatus = connectionStatus[serverName]; - const serverConfig = startupConfig?.mcpServers?.[serverName]; - - const handleConfigClick = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - - previousFocusRef.current = document.activeElement as HTMLElement; - - const configTool = tool || { - name: serverName, - pluginKey: `${Constants.mcp_prefix}${serverName}`, - authConfig: serverConfig?.customUserVars - ? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({ - authField: key, - label: config.title, - description: config.description, - })) - : [], - authenticated: false, - }; - setSelectedToolForConfig(configTool); - setIsConfigModalOpen(true); - }; - - const handleCancelClick = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - cancelOAuthFlow(serverName); - }; + const statusIconProps = getServerStatusIconProps(serverName); // Common wrapper for the main content (check mark + text) // Ensures Check & Text are adjacent and the group takes available space. @@ -267,22 +46,7 @@ function MCPSelect() { ); - // Check if this server has customUserVars to configure - const hasCustomUserVars = - serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0; - - const statusIcon = ( - - ); + const statusIcon = statusIconProps && ; if (statusIcon) { return ( @@ -295,14 +59,7 @@ function MCPSelect() { return mainContentWrapper; }, - [ - isInitializing, - isCancellable, - mcpToolDetails, - cancelOAuthFlow, - connectionStatus, - startupConfig?.mcpServers, - ], + [getServerStatusIconProps], ); // Don't render if no servers are selected and not pinned @@ -315,14 +72,14 @@ function MCPSelect() { return null; } - const placeholderText = - startupConfig?.interface?.mcpServers?.placeholder || localize('com_ui_mcp_servers'); + const configDialogProps = getConfigDialogProps(); + return ( <> - {selectedToolForConfig && ( - { - const schema: Record = {}; - if (selectedToolForConfig?.authConfig) { - selectedToolForConfig.authConfig.forEach((field) => { - schema[field.authField] = { - title: field.label, - description: field.description, - }; - }); - } - return schema; - })()} - initialValues={(() => { - const initial: Record = {}; - // Note: Actual initial values might need to be fetched if they are stored user-specifically - if (selectedToolForConfig?.authConfig) { - selectedToolForConfig.authConfig.forEach((field) => { - initial[field.authField] = ''; // Or fetched value - }); - } - return initial; - })()} - onSave={handleSave} - onRevoke={handleRevoke} - isSubmitting={updateUserPluginsMutation.isLoading} - /> - )} + {configDialogProps && } ); } diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx index fd6bd7ad4..8f271c0b6 100644 --- a/client/src/components/Chat/Input/MCPSubMenu.tsx +++ b/client/src/components/Chat/Input/MCPSubMenu.tsx @@ -2,28 +2,26 @@ import React from 'react'; import * as Ariakit from '@ariakit/react'; import { ChevronRight } from 'lucide-react'; import { PinIcon, MCPIcon } from '~/components/svg'; -import { useLocalize } from '~/hooks'; +import MCPConfigDialog from '~/components/ui/MCP/MCPConfigDialog'; +import MCPServerStatusIcon from '~/components/ui/MCP/MCPServerStatusIcon'; +import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager'; import { cn } from '~/utils'; interface MCPSubMenuProps { - isMCPPinned: boolean; - setIsMCPPinned: (value: boolean) => void; - mcpValues?: string[]; - mcpServerNames: string[]; - handleMCPToggle: (serverName: string) => void; placeholder?: string; } -const MCPSubMenu = ({ - mcpValues, - isMCPPinned, - mcpServerNames, - setIsMCPPinned, - handleMCPToggle, - placeholder, - ...props -}: MCPSubMenuProps) => { - const localize = useLocalize(); +const MCPSubMenu = ({ placeholder, ...props }: MCPSubMenuProps) => { + const { + configuredServers, + mcpValues, + isPinned, + setIsPinned, + placeholderText, + toggleServerSelection, + getServerStatusIconProps, + getConfigDialogProps, + } = useMCPServerManager(); const menuStore = Ariakit.useMenuStore({ focusLoop: true, @@ -31,72 +29,96 @@ const MCPSubMenu = ({ placement: 'right', }); + // Don't render if no MCP servers are configured + if (!configuredServers || configuredServers.length === 0) { + return null; + } + + const configDialogProps = getConfigDialogProps(); + return ( - - ) => { - e.stopPropagation(); - menuStore.toggle(); - }} - className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover" - /> - } - > -
- - {placeholder || localize('com_ui_mcp_servers')} - -
- -
- - {mcpServerNames.map((serverName) => ( - { - event.preventDefault(); - handleMCPToggle(serverName); + + + + {configuredServers.map((serverName) => { + const statusIconProps = getServerStatusIconProps(serverName); + const isSelected = mcpValues?.includes(serverName) ?? false; + + const statusIcon = statusIconProps && ; + + return ( + { + event.preventDefault(); + toggleServerSelection(serverName); + }} + className={cn( + 'flex items-center gap-2 rounded-lg px-2 py-1.5 text-text-primary hover:cursor-pointer', + 'scroll-m-1 outline-none transition-colors', + 'hover:bg-black/[0.075] dark:hover:bg-white/10', + 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', + 'w-full min-w-0 justify-between text-sm', + )} + > + + {statusIcon &&
{statusIcon}
} +
+ ); + })} +
+
+ {configDialogProps && } + ); }; diff --git a/client/src/components/Chat/Input/ToolsDropdown.tsx b/client/src/components/Chat/Input/ToolsDropdown.tsx index 56c7db1cb..159fe455d 100644 --- a/client/src/components/Chat/Input/ToolsDropdown.tsx +++ b/client/src/components/Chat/Input/ToolsDropdown.tsx @@ -55,12 +55,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { } = codeInterpreter; const { isPinned: isFileSearchPinned, setIsPinned: setIsFileSearchPinned } = fileSearch; const { isPinned: isArtifactsPinned, setIsPinned: setIsArtifactsPinned } = artifacts; - const { - mcpValues, - mcpServerNames, - isPinned: isMCPPinned, - setIsPinned: setIsMCPPinned, - } = mcpSelect; + const { mcpServerNames } = mcpSelect; const canUseWebSearch = useHasAccess({ permissionType: PermissionTypes.WEB_SEARCH, @@ -130,17 +125,6 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { } }, [artifacts]); - const handleMCPToggle = useCallback( - (serverName: string) => { - const currentValues = mcpSelect.mcpValues ?? []; - const newValues = currentValues.includes(serverName) - ? currentValues.filter((v) => v !== serverName) - : [...currentValues, serverName]; - mcpSelect.setMCPValues(newValues); - }, - [mcpSelect], - ); - const mcpPlaceholder = startupConfig?.interface?.mcpServers?.placeholder; const dropdownItems: MenuItemProps[] = []; @@ -305,17 +289,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { if (mcpServerNames && mcpServerNames.length > 0) { dropdownItems.push({ hideOnClick: false, - render: (props) => ( - - ), + render: (props) => , }); } diff --git a/client/src/hooks/MCP/useMCPServerInitialization.ts b/client/src/hooks/MCP/useMCPServerInitialization.ts index 17db7d908..ec5ab2cc0 100644 --- a/client/src/hooks/MCP/useMCPServerInitialization.ts +++ b/client/src/hooks/MCP/useMCPServerInitialization.ts @@ -37,6 +37,9 @@ export function useMCPServerInitialization(options?: UseMCPServerInitializationO // Main initialization mutation const reinitializeMutation = useReinitializeMCPServerMutation(); + // Track which server is currently being processed + const [currentProcessingServer, setCurrentProcessingServer] = useState(null); + // Cancel OAuth mutation const cancelOAuthMutation = useCancelMCPOAuthMutation(); @@ -184,12 +187,32 @@ export function useMCPServerInitialization(options?: UseMCPServerInitializationO return; } + if (connectionStatus[serverName]?.requiresOAuth) { + setCancellableServers((prev) => new Set(prev).add(serverName)); + } + // Add to initializing set setInitializingServers((prev) => new Set(prev).add(serverName)); - // Trigger initialization + // If there's already a server being processed, that one will be cancelled + if (currentProcessingServer && currentProcessingServer !== serverName) { + // Clean up the cancelled server's state immediately + showToast({ + message: localize('com_ui_mcp_init_cancelled', { 0: currentProcessingServer }), + status: 'warning', + }); + + cleanupOAuthState(currentProcessingServer); + } + + // Track the current server being processed + setCurrentProcessingServer(serverName); + reinitializeMutation.mutate(serverName, { onSuccess: (response: any) => { + // Clear current processing server + setCurrentProcessingServer(null); + if (response.success) { if (response.oauthRequired && response.oauthUrl) { // OAuth required - store URL and start polling @@ -238,40 +261,45 @@ export function useMCPServerInitialization(options?: UseMCPServerInitializationO } }, onError: (error: any) => { - console.error('Error initializing MCP server:', error); - showToast({ - message: localize('com_ui_mcp_init_failed'), - status: 'error', - }); - // Remove from initializing on error - setInitializingServers((prev) => { - const newSet = new Set(prev); - newSet.delete(serverName); - return newSet; - }); - // Remove from OAuth tracking - setOauthPollingServers((prev) => { - const newMap = new Map(prev); - newMap.delete(serverName); - return newMap; - }); - setOauthStartTimes((prev) => { - const newMap = new Map(prev); - newMap.delete(serverName); - return newMap; - }); + console.error(`Error initializing MCP server ${serverName}:`, error); + setCurrentProcessingServer(null); + + const isCancelled = + error?.name === 'CanceledError' || + error?.code === 'ERR_CANCELED' || + error?.message?.includes('cancel') || + error?.message?.includes('abort'); + + if (isCancelled) { + showToast({ + message: localize('com_ui_mcp_init_cancelled', { 0: serverName }), + status: 'warning', + }); + } else { + showToast({ + message: localize('com_ui_mcp_init_failed'), + status: 'error', + }); + } + + // Clean up OAuth state using helper function + cleanupOAuthState(serverName); + // Call optional error callback options?.onError?.(serverName, error); }, }); }, [ + initializingServers, + connectionStatus, + currentProcessingServer, reinitializeMutation, showToast, localize, - handleSuccessfulConnection, - initializingServers, + cleanupOAuthState, options, + handleSuccessfulConnection, ], ); diff --git a/client/src/hooks/MCP/useMCPServerManager.ts b/client/src/hooks/MCP/useMCPServerManager.ts new file mode 100644 index 000000000..95891b47e --- /dev/null +++ b/client/src/hooks/MCP/useMCPServerManager.ts @@ -0,0 +1,328 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { Constants, QueryKeys } from 'librechat-data-provider'; +import { useCallback, useState, useMemo, useRef } from 'react'; +import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query'; +import { useMCPServerInitialization } from '~/hooks/MCP/useMCPServerInitialization'; +import type { ConfigFieldDetail } from '~/components/ui/MCP/MCPConfigDialog'; +import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider'; +import { useToastContext, useBadgeRowContext } from '~/Providers'; +import { useLocalize } from '~/hooks'; + +export function useMCPServerManager() { + const localize = useLocalize(); + const { showToast } = useToastContext(); + const { mcpSelect, startupConfig } = useBadgeRowContext(); + const { mcpValues, setMCPValues, mcpToolDetails, isPinned, setIsPinned } = mcpSelect; + + const configuredServers = useMemo(() => { + if (!startupConfig?.mcpServers) { + return []; + } + return Object.entries(startupConfig.mcpServers) + .filter(([, config]) => config.chatMenu !== false) + .map(([serverName]) => serverName); + }, [startupConfig?.mcpServers]); + + const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); + const [selectedToolForConfig, setSelectedToolForConfig] = useState(null); + const previousFocusRef = useRef(null); + + const queryClient = useQueryClient(); + + const updateUserPluginsMutation = useUpdateUserPluginsMutation({ + onSuccess: async () => { + showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' }); + + await Promise.all([ + queryClient.refetchQueries([QueryKeys.tools]), + queryClient.refetchQueries([QueryKeys.mcpAuthValues]), + queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]), + ]); + }, + onError: (error: unknown) => { + console.error('Error updating MCP auth:', error); + showToast({ + message: localize('com_nav_mcp_vars_update_error'), + status: 'error', + }); + }, + }); + + const { initializeServer, isInitializing, connectionStatus, cancelOAuthFlow, isCancellable } = + useMCPServerInitialization({ + onSuccess: (serverName) => { + const currentValues = mcpValues ?? []; + if (!currentValues.includes(serverName)) { + setMCPValues([...currentValues, serverName]); + } + }, + onError: (serverName) => { + const tool = mcpToolDetails?.find((t) => t.name === serverName); + const serverConfig = startupConfig?.mcpServers?.[serverName]; + const serverStatus = connectionStatus[serverName]; + + const hasAuthConfig = + (tool?.authConfig && tool.authConfig.length > 0) || + (serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0); + + const wouldShowButton = + !serverStatus || + serverStatus.connectionState === 'disconnected' || + serverStatus.connectionState === 'error' || + (serverStatus.connectionState === 'connected' && hasAuthConfig); + + if (!wouldShowButton) { + return; + } + + const configTool = tool || { + name: serverName, + pluginKey: `${Constants.mcp_prefix}${serverName}`, + authConfig: serverConfig?.customUserVars + ? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({ + authField: key, + label: config.title, + description: config.description, + })) + : [], + authenticated: false, + }; + + previousFocusRef.current = document.activeElement as HTMLElement; + + setSelectedToolForConfig(configTool); + setIsConfigModalOpen(true); + }, + }); + + const handleConfigSave = useCallback( + (targetName: string, authData: Record) => { + if (selectedToolForConfig && selectedToolForConfig.name === targetName) { + const payload: TUpdateUserPlugins = { + pluginKey: `${Constants.mcp_prefix}${targetName}`, + action: 'install', + auth: authData, + }; + updateUserPluginsMutation.mutate(payload); + } + }, + [selectedToolForConfig, updateUserPluginsMutation], + ); + + const handleConfigRevoke = useCallback( + (targetName: string) => { + if (selectedToolForConfig && selectedToolForConfig.name === targetName) { + const payload: TUpdateUserPlugins = { + pluginKey: `${Constants.mcp_prefix}${targetName}`, + action: 'uninstall', + auth: {}, + }; + updateUserPluginsMutation.mutate(payload); + + const currentValues = mcpValues ?? []; + const filteredValues = currentValues.filter((name) => name !== targetName); + setMCPValues(filteredValues); + } + }, + [selectedToolForConfig, updateUserPluginsMutation, mcpValues, setMCPValues], + ); + + const handleSave = useCallback( + (authData: Record) => { + if (selectedToolForConfig) { + handleConfigSave(selectedToolForConfig.name, authData); + } + }, + [selectedToolForConfig, handleConfigSave], + ); + + const handleRevoke = useCallback(() => { + if (selectedToolForConfig) { + handleConfigRevoke(selectedToolForConfig.name); + } + }, [selectedToolForConfig, handleConfigRevoke]); + + const handleDialogOpenChange = useCallback((open: boolean) => { + setIsConfigModalOpen(open); + + if (!open && previousFocusRef.current) { + setTimeout(() => { + if (previousFocusRef.current && typeof previousFocusRef.current.focus === 'function') { + previousFocusRef.current.focus(); + } + previousFocusRef.current = null; + }, 0); + } + }, []); + + const toggleServerSelection = useCallback( + (serverName: string) => { + const currentValues = mcpValues ?? []; + const serverStatus = connectionStatus[serverName]; + + if (currentValues.includes(serverName)) { + const filteredValues = currentValues.filter((name) => name !== serverName); + setMCPValues(filteredValues); + } else { + if (serverStatus?.connectionState === 'connected') { + setMCPValues([...currentValues, serverName]); + } else { + initializeServer(serverName); + } + } + }, + [connectionStatus, mcpValues, setMCPValues, initializeServer], + ); + + const batchToggleServers = useCallback( + (serverNames: string[]) => { + const connectedServers: string[] = []; + const disconnectedServers: string[] = []; + + serverNames.forEach((serverName) => { + const serverStatus = connectionStatus[serverName]; + if (serverStatus?.connectionState === 'connected') { + connectedServers.push(serverName); + } else { + disconnectedServers.push(serverName); + } + }); + + setMCPValues(connectedServers); + + disconnectedServers.forEach((serverName) => { + initializeServer(serverName); + }); + }, + [connectionStatus, setMCPValues, initializeServer], + ); + + const getServerStatusIconProps = useCallback( + (serverName: string) => { + const tool = mcpToolDetails?.find((t) => t.name === serverName); + const serverStatus = connectionStatus[serverName]; + const serverConfig = startupConfig?.mcpServers?.[serverName]; + + const handleConfigClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + previousFocusRef.current = document.activeElement as HTMLElement; + + const configTool = tool || { + name: serverName, + pluginKey: `${Constants.mcp_prefix}${serverName}`, + authConfig: serverConfig?.customUserVars + ? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({ + authField: key, + label: config.title, + description: config.description, + })) + : [], + authenticated: false, + }; + setSelectedToolForConfig(configTool); + setIsConfigModalOpen(true); + }; + + const handleCancelClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + cancelOAuthFlow(serverName); + }; + + const hasCustomUserVars = + serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0; + + return { + serverName, + serverStatus, + tool, + onConfigClick: handleConfigClick, + isInitializing: isInitializing(serverName), + canCancel: isCancellable(serverName), + onCancel: handleCancelClick, + hasCustomUserVars, + }; + }, + [ + mcpToolDetails, + connectionStatus, + startupConfig?.mcpServers, + isInitializing, + isCancellable, + cancelOAuthFlow, + ], + ); + + const placeholderText = useMemo( + () => startupConfig?.interface?.mcpServers?.placeholder || localize('com_ui_mcp_servers'), + [startupConfig?.interface?.mcpServers?.placeholder, localize], + ); + + const getConfigDialogProps = useCallback(() => { + if (!selectedToolForConfig) return null; + + const fieldsSchema: Record = {}; + if (selectedToolForConfig?.authConfig) { + selectedToolForConfig.authConfig.forEach((field) => { + fieldsSchema[field.authField] = { + title: field.label || field.authField, + description: field.description, + }; + }); + } + + const initialValues: Record = {}; + if (selectedToolForConfig?.authConfig) { + selectedToolForConfig.authConfig.forEach((field) => { + initialValues[field.authField] = ''; + }); + } + + return { + serverName: selectedToolForConfig.name, + serverStatus: connectionStatus[selectedToolForConfig.name], + isOpen: isConfigModalOpen, + onOpenChange: handleDialogOpenChange, + fieldsSchema, + initialValues, + onSave: handleSave, + onRevoke: handleRevoke, + isSubmitting: updateUserPluginsMutation.isLoading, + }; + }, [ + selectedToolForConfig, + connectionStatus, + isConfigModalOpen, + handleDialogOpenChange, + handleSave, + handleRevoke, + updateUserPluginsMutation.isLoading, + ]); + + return { + // Data + configuredServers, + mcpValues, + mcpToolDetails, + isPinned, + setIsPinned, + startupConfig, + connectionStatus, + placeholderText, + + // Handlers + toggleServerSelection, + batchToggleServers, + getServerStatusIconProps, + + // Dialog state + selectedToolForConfig, + isConfigModalOpen, + getConfigDialogProps, + + // Utilities + localize, + }; +} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 0c5572475..95aa05fee 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -863,6 +863,7 @@ "com_ui_mcp_servers": "MCP Servers", "com_ui_mcp_update_var": "Update {{0}}", "com_ui_mcp_url": "MCP Server URL", + "com_ui_mcp_init_cancelled": "MCP server '{{0}}' initialization was cancelled due to simultaneous request", "com_ui_medium": "Medium", "com_ui_memories": "Memories", "com_ui_memories_allow_create": "Allow creating Memories", From f4facb7d35be01be897231d69714c72676f60c8d Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 26 Jul 2025 20:11:20 -0400 Subject: [PATCH 014/224] =?UTF-8?q?=F0=9F=AA=B5=20refactor:=20Dynamic=20`g?= =?UTF-8?q?etLogDirectory`=20utility=20for=20Loggers=20(#8686)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 2 +- packages/data-schemas/package.json | 2 +- .../data-schemas/src/config/meiliLogger.ts | 4 +- packages/data-schemas/src/config/utils.ts | 37 +++++++++++++++++++ packages/data-schemas/src/config/winston.ts | 4 +- 5 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 packages/data-schemas/src/config/utils.ts diff --git a/package-lock.json b/package-lock.json index 0c610c2f2..d9c3ea5f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48787,7 +48787,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.12", + "version": "0.0.13", "license": "MIT", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", diff --git a/packages/data-schemas/package.json b/packages/data-schemas/package.json index 6f299e3f0..651f82e22 100644 --- a/packages/data-schemas/package.json +++ b/packages/data-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/data-schemas", - "version": "0.0.12", + "version": "0.0.13", "description": "Mongoose schemas and models for LibreChat", "type": "module", "main": "dist/index.cjs", diff --git a/packages/data-schemas/src/config/meiliLogger.ts b/packages/data-schemas/src/config/meiliLogger.ts index 0d4d39475..572e87707 100644 --- a/packages/data-schemas/src/config/meiliLogger.ts +++ b/packages/data-schemas/src/config/meiliLogger.ts @@ -1,8 +1,8 @@ -import path from 'path'; import winston from 'winston'; import 'winston-daily-rotate-file'; +import { getLogDirectory } from './utils'; -const logDir = path.join(__dirname, '..', '..', '..', 'api', 'logs'); +const logDir = getLogDirectory(); const { NODE_ENV, DEBUG_LOGGING = 'false' } = process.env; diff --git a/packages/data-schemas/src/config/utils.ts b/packages/data-schemas/src/config/utils.ts new file mode 100644 index 000000000..323334207 --- /dev/null +++ b/packages/data-schemas/src/config/utils.ts @@ -0,0 +1,37 @@ +import path from 'path'; + +/** + * Determine the log directory in a cross-compatible way. + * Priority: + * 1. LIBRECHAT_LOG_DIR environment variable + * 2. If running within LibreChat monorepo (when cwd ends with /api), use api/logs + * 3. If api/logs exists relative to cwd, use that (for running from project root) + * 4. Otherwise, use logs directory relative to process.cwd() + * + * This avoids using __dirname which is not available in ESM modules + */ +export const getLogDirectory = (): string => { + if (process.env.LIBRECHAT_LOG_DIR) { + return process.env.LIBRECHAT_LOG_DIR; + } + + const cwd = process.cwd(); + + // Check if we're running from within the api directory + if (cwd.endsWith('/api') || cwd.endsWith('\\api')) { + return path.join(cwd, 'logs'); + } + + // Check if api/logs exists relative to current directory (running from project root) + // We'll just use the path and let the file system create it if needed + const apiLogsPath = path.join(cwd, 'api', 'logs'); + + // For LibreChat project structure, use api/logs + // For external consumers, they should set LIBRECHAT_LOG_DIR + if (cwd.includes('LibreChat')) { + return apiLogsPath; + } + + // Default to logs directory relative to current working directory + return path.join(cwd, 'logs'); +}; diff --git a/packages/data-schemas/src/config/winston.ts b/packages/data-schemas/src/config/winston.ts index 7e5287296..0141094bd 100644 --- a/packages/data-schemas/src/config/winston.ts +++ b/packages/data-schemas/src/config/winston.ts @@ -1,9 +1,9 @@ -import path from 'path'; import winston from 'winston'; import 'winston-daily-rotate-file'; import { redactFormat, redactMessage, debugTraverse, jsonTruncateFormat } from './parsers'; +import { getLogDirectory } from './utils'; -const logDir = path.join(__dirname, '..', '..', '..', 'api', 'logs'); +const logDir = getLogDirectory(); const { NODE_ENV, DEBUG_LOGGING, CONSOLE_JSON, DEBUG_CONSOLE } = process.env; From d6a65f5a08b1808c8a0ec0d3b18a90b46e70cf6f Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Sun, 27 Jul 2025 08:42:35 -0700 Subject: [PATCH 015/224] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Temporary=20Chats?= =?UTF-8?q?=20Still=20Visible=20in=20Sidebar=20(#8688)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: Fix import error causing temporary chats to still display in sidebar * refactor: Update import path for `getCustomConfig` in Conversation and Message models * chore: eslint warnings * ci: add tests for Conversation and Message models --------- Co-authored-by: Danny Avila --- api/models/Conversation.js | 2 +- api/models/Conversation.spec.js | 572 ++++++++++++++++++++++++++++++++ api/models/Message.js | 2 +- api/models/Message.spec.js | 273 ++++++++++++++- 4 files changed, 843 insertions(+), 6 deletions(-) create mode 100644 api/models/Conversation.spec.js diff --git a/api/models/Conversation.js b/api/models/Conversation.js index b237c41e9..8a529dd10 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -1,6 +1,6 @@ const { logger } = require('@librechat/data-schemas'); const { createTempChatExpirationDate } = require('@librechat/api'); -const getCustomConfig = require('~/server/services/Config/getCustomConfig'); +const { getCustomConfig } = require('~/server/services/Config/getCustomConfig'); const { getMessages, deleteMessages } = require('./Message'); const { Conversation } = require('~/db/models'); diff --git a/api/models/Conversation.spec.js b/api/models/Conversation.spec.js new file mode 100644 index 000000000..1acdb7750 --- /dev/null +++ b/api/models/Conversation.spec.js @@ -0,0 +1,572 @@ +const mongoose = require('mongoose'); +const { v4: uuidv4 } = require('uuid'); +const { EModelEndpoint } = require('librechat-data-provider'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { + deleteNullOrEmptyConversations, + searchConversation, + getConvosByCursor, + getConvosQueried, + getConvoFiles, + getConvoTitle, + deleteConvos, + saveConvo, + getConvo, +} = require('./Conversation'); +jest.mock('~/server/services/Config/getCustomConfig'); +jest.mock('./Message'); +const { getCustomConfig } = require('~/server/services/Config/getCustomConfig'); +const { getMessages, deleteMessages } = require('./Message'); + +const { Conversation } = require('~/db/models'); + +describe('Conversation Operations', () => { + let mongoServer; + let mockReq; + let mockConversationData; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + // Clear database + await Conversation.deleteMany({}); + + // Reset mocks + jest.clearAllMocks(); + + // Default mock implementations + getMessages.mockResolvedValue([]); + deleteMessages.mockResolvedValue({ deletedCount: 0 }); + + mockReq = { + user: { id: 'user123' }, + body: {}, + }; + + mockConversationData = { + conversationId: uuidv4(), + title: 'Test Conversation', + endpoint: EModelEndpoint.openAI, + }; + }); + + describe('saveConvo', () => { + it('should save a conversation for an authenticated user', async () => { + const result = await saveConvo(mockReq, mockConversationData); + + expect(result.conversationId).toBe(mockConversationData.conversationId); + expect(result.user).toBe('user123'); + expect(result.title).toBe('Test Conversation'); + expect(result.endpoint).toBe(EModelEndpoint.openAI); + + // Verify the conversation was actually saved to the database + const savedConvo = await Conversation.findOne({ + conversationId: mockConversationData.conversationId, + user: 'user123', + }); + expect(savedConvo).toBeTruthy(); + expect(savedConvo.title).toBe('Test Conversation'); + }); + + it('should query messages when saving a conversation', async () => { + // Mock messages as ObjectIds + const mongoose = require('mongoose'); + const mockMessages = [new mongoose.Types.ObjectId(), new mongoose.Types.ObjectId()]; + getMessages.mockResolvedValue(mockMessages); + + await saveConvo(mockReq, mockConversationData); + + // Verify that getMessages was called with correct parameters + expect(getMessages).toHaveBeenCalledWith( + { conversationId: mockConversationData.conversationId }, + '_id', + ); + }); + + it('should handle newConversationId when provided', async () => { + const newConversationId = uuidv4(); + const result = await saveConvo(mockReq, { + ...mockConversationData, + newConversationId, + }); + + expect(result.conversationId).toBe(newConversationId); + }); + + it('should handle unsetFields metadata', async () => { + const metadata = { + unsetFields: { someField: 1 }, + }; + + await saveConvo(mockReq, mockConversationData, metadata); + + const savedConvo = await Conversation.findOne({ + conversationId: mockConversationData.conversationId, + }); + expect(savedConvo.someField).toBeUndefined(); + }); + }); + + describe('isTemporary conversation handling', () => { + it('should save a conversation with expiredAt when isTemporary is true', async () => { + // Mock custom config with 24 hour retention + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 24, + }, + }); + + mockReq.body = { isTemporary: true }; + + const beforeSave = new Date(); + const result = await saveConvo(mockReq, mockConversationData); + const afterSave = new Date(); + + expect(result.conversationId).toBe(mockConversationData.conversationId); + expect(result.expiredAt).toBeDefined(); + expect(result.expiredAt).toBeInstanceOf(Date); + + // Verify expiredAt is approximately 24 hours in the future + const expectedExpirationTime = new Date(beforeSave.getTime() + 24 * 60 * 60 * 1000); + const actualExpirationTime = new Date(result.expiredAt); + + expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( + expectedExpirationTime.getTime() - 1000, + ); + expect(actualExpirationTime.getTime()).toBeLessThanOrEqual( + new Date(afterSave.getTime() + 24 * 60 * 60 * 1000 + 1000).getTime(), + ); + }); + + it('should save a conversation without expiredAt when isTemporary is false', async () => { + mockReq.body = { isTemporary: false }; + + const result = await saveConvo(mockReq, mockConversationData); + + expect(result.conversationId).toBe(mockConversationData.conversationId); + expect(result.expiredAt).toBeNull(); + }); + + it('should save a conversation without expiredAt when isTemporary is not provided', async () => { + // No isTemporary in body + mockReq.body = {}; + + const result = await saveConvo(mockReq, mockConversationData); + + expect(result.conversationId).toBe(mockConversationData.conversationId); + expect(result.expiredAt).toBeNull(); + }); + + it('should use custom retention period from config', async () => { + // Mock custom config with 48 hour retention + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 48, + }, + }); + + mockReq.body = { isTemporary: true }; + + const beforeSave = new Date(); + const result = await saveConvo(mockReq, mockConversationData); + + expect(result.expiredAt).toBeDefined(); + + // Verify expiredAt is approximately 48 hours in the future + const expectedExpirationTime = new Date(beforeSave.getTime() + 48 * 60 * 60 * 1000); + const actualExpirationTime = new Date(result.expiredAt); + + expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( + expectedExpirationTime.getTime() - 1000, + ); + expect(actualExpirationTime.getTime()).toBeLessThanOrEqual( + expectedExpirationTime.getTime() + 1000, + ); + }); + + it('should handle minimum retention period (1 hour)', async () => { + // Mock custom config with less than minimum retention + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 0.5, // Half hour - should be clamped to 1 hour + }, + }); + + mockReq.body = { isTemporary: true }; + + const beforeSave = new Date(); + const result = await saveConvo(mockReq, mockConversationData); + + expect(result.expiredAt).toBeDefined(); + + // Verify expiredAt is approximately 1 hour in the future (minimum) + const expectedExpirationTime = new Date(beforeSave.getTime() + 1 * 60 * 60 * 1000); + const actualExpirationTime = new Date(result.expiredAt); + + expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( + expectedExpirationTime.getTime() - 1000, + ); + expect(actualExpirationTime.getTime()).toBeLessThanOrEqual( + expectedExpirationTime.getTime() + 1000, + ); + }); + + it('should handle maximum retention period (8760 hours)', async () => { + // Mock custom config with more than maximum retention + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 10000, // Should be clamped to 8760 hours + }, + }); + + mockReq.body = { isTemporary: true }; + + const beforeSave = new Date(); + const result = await saveConvo(mockReq, mockConversationData); + + expect(result.expiredAt).toBeDefined(); + + // Verify expiredAt is approximately 8760 hours (1 year) in the future + const expectedExpirationTime = new Date(beforeSave.getTime() + 8760 * 60 * 60 * 1000); + const actualExpirationTime = new Date(result.expiredAt); + + expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( + expectedExpirationTime.getTime() - 1000, + ); + expect(actualExpirationTime.getTime()).toBeLessThanOrEqual( + expectedExpirationTime.getTime() + 1000, + ); + }); + + it('should handle getCustomConfig errors gracefully', async () => { + // Mock getCustomConfig to throw an error + getCustomConfig.mockRejectedValue(new Error('Config service unavailable')); + + mockReq.body = { isTemporary: true }; + + const result = await saveConvo(mockReq, mockConversationData); + + // Should still save the conversation but with expiredAt as null + expect(result.conversationId).toBe(mockConversationData.conversationId); + expect(result.expiredAt).toBeNull(); + }); + + it('should use default retention when config is not provided', async () => { + // Mock getCustomConfig to return empty config + getCustomConfig.mockResolvedValue({}); + + mockReq.body = { isTemporary: true }; + + const beforeSave = new Date(); + const result = await saveConvo(mockReq, mockConversationData); + + expect(result.expiredAt).toBeDefined(); + + // Default retention is 30 days (720 hours) + const expectedExpirationTime = new Date(beforeSave.getTime() + 30 * 24 * 60 * 60 * 1000); + const actualExpirationTime = new Date(result.expiredAt); + + expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( + expectedExpirationTime.getTime() - 1000, + ); + expect(actualExpirationTime.getTime()).toBeLessThanOrEqual( + expectedExpirationTime.getTime() + 1000, + ); + }); + + it('should update expiredAt when saving existing temporary conversation', async () => { + // First save a temporary conversation + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 24, + }, + }); + + mockReq.body = { isTemporary: true }; + const firstSave = await saveConvo(mockReq, mockConversationData); + const originalExpiredAt = firstSave.expiredAt; + + // Wait a bit to ensure time difference + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Save again with same conversationId but different title + const updatedData = { ...mockConversationData, title: 'Updated Title' }; + const secondSave = await saveConvo(mockReq, updatedData); + + // Should update title and create new expiredAt + expect(secondSave.title).toBe('Updated Title'); + expect(secondSave.expiredAt).toBeDefined(); + expect(new Date(secondSave.expiredAt).getTime()).toBeGreaterThan( + new Date(originalExpiredAt).getTime(), + ); + }); + + it('should not set expiredAt when updating non-temporary conversation', async () => { + // First save a non-temporary conversation + mockReq.body = { isTemporary: false }; + const firstSave = await saveConvo(mockReq, mockConversationData); + expect(firstSave.expiredAt).toBeNull(); + + // Update without isTemporary flag + mockReq.body = {}; + const updatedData = { ...mockConversationData, title: 'Updated Title' }; + const secondSave = await saveConvo(mockReq, updatedData); + + expect(secondSave.title).toBe('Updated Title'); + expect(secondSave.expiredAt).toBeNull(); + }); + + it('should filter out expired conversations in getConvosByCursor', async () => { + // Create some test conversations + const nonExpiredConvo = await Conversation.create({ + conversationId: uuidv4(), + user: 'user123', + title: 'Non-expired', + endpoint: EModelEndpoint.openAI, + expiredAt: null, + updatedAt: new Date(), + }); + + await Conversation.create({ + conversationId: uuidv4(), + user: 'user123', + title: 'Future expired', + endpoint: EModelEndpoint.openAI, + expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours from now + updatedAt: new Date(), + }); + + // Mock Meili search + Conversation.meiliSearch = jest.fn().mockResolvedValue({ hits: [] }); + + const result = await getConvosByCursor('user123'); + + // Should only return conversations with null or non-existent expiredAt + expect(result.conversations).toHaveLength(1); + expect(result.conversations[0].conversationId).toBe(nonExpiredConvo.conversationId); + }); + + it('should filter out expired conversations in getConvosQueried', async () => { + // Create test conversations + const nonExpiredConvo = await Conversation.create({ + conversationId: uuidv4(), + user: 'user123', + title: 'Non-expired', + endpoint: EModelEndpoint.openAI, + expiredAt: null, + }); + + const expiredConvo = await Conversation.create({ + conversationId: uuidv4(), + user: 'user123', + title: 'Expired', + endpoint: EModelEndpoint.openAI, + expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + }); + + const convoIds = [ + { conversationId: nonExpiredConvo.conversationId }, + { conversationId: expiredConvo.conversationId }, + ]; + + const result = await getConvosQueried('user123', convoIds); + + // Should only return the non-expired conversation + expect(result.conversations).toHaveLength(1); + expect(result.conversations[0].conversationId).toBe(nonExpiredConvo.conversationId); + expect(result.convoMap[nonExpiredConvo.conversationId]).toBeDefined(); + expect(result.convoMap[expiredConvo.conversationId]).toBeUndefined(); + }); + }); + + describe('searchConversation', () => { + it('should find a conversation by conversationId', async () => { + await Conversation.create({ + conversationId: mockConversationData.conversationId, + user: 'user123', + title: 'Test', + endpoint: EModelEndpoint.openAI, + }); + + const result = await searchConversation(mockConversationData.conversationId); + + expect(result).toBeTruthy(); + expect(result.conversationId).toBe(mockConversationData.conversationId); + expect(result.user).toBe('user123'); + expect(result.title).toBeUndefined(); // Only returns conversationId and user + }); + + it('should return null if conversation not found', async () => { + const result = await searchConversation('non-existent-id'); + expect(result).toBeNull(); + }); + }); + + describe('getConvo', () => { + it('should retrieve a conversation for a user', async () => { + await Conversation.create({ + conversationId: mockConversationData.conversationId, + user: 'user123', + title: 'Test Conversation', + endpoint: EModelEndpoint.openAI, + }); + + const result = await getConvo('user123', mockConversationData.conversationId); + + expect(result.conversationId).toBe(mockConversationData.conversationId); + expect(result.user).toBe('user123'); + expect(result.title).toBe('Test Conversation'); + }); + + it('should return null if conversation not found', async () => { + const result = await getConvo('user123', 'non-existent-id'); + expect(result).toBeNull(); + }); + }); + + describe('getConvoTitle', () => { + it('should return the conversation title', async () => { + await Conversation.create({ + conversationId: mockConversationData.conversationId, + user: 'user123', + title: 'Test Title', + endpoint: EModelEndpoint.openAI, + }); + + const result = await getConvoTitle('user123', mockConversationData.conversationId); + expect(result).toBe('Test Title'); + }); + + it('should return null if conversation has no title', async () => { + await Conversation.create({ + conversationId: mockConversationData.conversationId, + user: 'user123', + title: null, + endpoint: EModelEndpoint.openAI, + }); + + const result = await getConvoTitle('user123', mockConversationData.conversationId); + expect(result).toBeNull(); + }); + + it('should return "New Chat" if conversation not found', async () => { + const result = await getConvoTitle('user123', 'non-existent-id'); + expect(result).toBe('New Chat'); + }); + }); + + describe('getConvoFiles', () => { + it('should return conversation files', async () => { + const files = ['file1', 'file2']; + await Conversation.create({ + conversationId: mockConversationData.conversationId, + user: 'user123', + endpoint: EModelEndpoint.openAI, + files, + }); + + const result = await getConvoFiles(mockConversationData.conversationId); + expect(result).toEqual(files); + }); + + it('should return empty array if no files', async () => { + await Conversation.create({ + conversationId: mockConversationData.conversationId, + user: 'user123', + endpoint: EModelEndpoint.openAI, + }); + + const result = await getConvoFiles(mockConversationData.conversationId); + expect(result).toEqual([]); + }); + + it('should return empty array if conversation not found', async () => { + const result = await getConvoFiles('non-existent-id'); + expect(result).toEqual([]); + }); + }); + + describe('deleteConvos', () => { + it('should delete conversations and associated messages', async () => { + await Conversation.create({ + conversationId: mockConversationData.conversationId, + user: 'user123', + title: 'To Delete', + endpoint: EModelEndpoint.openAI, + }); + + deleteMessages.mockResolvedValue({ deletedCount: 5 }); + + const result = await deleteConvos('user123', { + conversationId: mockConversationData.conversationId, + }); + + expect(result.deletedCount).toBe(1); + expect(result.messages.deletedCount).toBe(5); + expect(deleteMessages).toHaveBeenCalledWith({ + conversationId: { $in: [mockConversationData.conversationId] }, + }); + + // Verify conversation was deleted + const deletedConvo = await Conversation.findOne({ + conversationId: mockConversationData.conversationId, + }); + expect(deletedConvo).toBeNull(); + }); + + it('should throw error if no conversations found', async () => { + await expect(deleteConvos('user123', { conversationId: 'non-existent' })).rejects.toThrow( + 'Conversation not found or already deleted.', + ); + }); + }); + + describe('deleteNullOrEmptyConversations', () => { + it('should delete conversations with null, empty, or missing conversationIds', async () => { + // Since conversationId is required by the schema, we can't create documents with null/missing IDs + // This test should verify the function works when such documents exist (e.g., from data corruption) + + // For this test, let's create a valid conversation and verify the function doesn't delete it + await Conversation.create({ + conversationId: mockConversationData.conversationId, + user: 'user4', + endpoint: EModelEndpoint.openAI, + }); + + deleteMessages.mockResolvedValue({ deletedCount: 0 }); + + const result = await deleteNullOrEmptyConversations(); + + expect(result.conversations.deletedCount).toBe(0); // No invalid conversations to delete + expect(result.messages.deletedCount).toBe(0); + + // Verify valid conversation remains + const remainingConvos = await Conversation.find({}); + expect(remainingConvos).toHaveLength(1); + expect(remainingConvos[0].conversationId).toBe(mockConversationData.conversationId); + }); + }); + + describe('Error Handling', () => { + it('should handle database errors in saveConvo', async () => { + // Force a database error by disconnecting + await mongoose.disconnect(); + + const result = await saveConvo(mockReq, mockConversationData); + + expect(result).toEqual({ message: 'Error saving conversation' }); + + // Reconnect for other tests + await mongoose.connect(mongoServer.getUri()); + }); + }); +}); diff --git a/api/models/Message.js b/api/models/Message.js index 3d5eee6ec..5a3f84a8e 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -1,7 +1,7 @@ const { z } = require('zod'); const { logger } = require('@librechat/data-schemas'); const { createTempChatExpirationDate } = require('@librechat/api'); -const getCustomConfig = require('~/server/services/Config/getCustomConfig'); +const { getCustomConfig } = require('~/server/services/Config/getCustomConfig'); const { Message } = require('~/db/models'); const idSchema = z.string().uuid(); diff --git a/api/models/Message.spec.js b/api/models/Message.spec.js index aebaebb44..8e954a12b 100644 --- a/api/models/Message.spec.js +++ b/api/models/Message.spec.js @@ -1,17 +1,21 @@ const mongoose = require('mongoose'); -const { MongoMemoryServer } = require('mongodb-memory-server'); const { v4: uuidv4 } = require('uuid'); const { messageSchema } = require('@librechat/data-schemas'); +const { MongoMemoryServer } = require('mongodb-memory-server'); const { saveMessage, getMessages, updateMessage, deleteMessages, + bulkSaveMessages, updateMessageText, deleteMessagesSince, } = require('./Message'); +jest.mock('~/server/services/Config/getCustomConfig'); +const { getCustomConfig } = require('~/server/services/Config/getCustomConfig'); + /** * @type {import('mongoose').Model} */ @@ -117,21 +121,21 @@ describe('Message Operations', () => { const conversationId = uuidv4(); // Create multiple messages in the same conversation - const message1 = await saveMessage(mockReq, { + await saveMessage(mockReq, { messageId: 'msg1', conversationId, text: 'First message', user: 'user123', }); - const message2 = await saveMessage(mockReq, { + await saveMessage(mockReq, { messageId: 'msg2', conversationId, text: 'Second message', user: 'user123', }); - const message3 = await saveMessage(mockReq, { + await saveMessage(mockReq, { messageId: 'msg3', conversationId, text: 'Third message', @@ -314,4 +318,265 @@ describe('Message Operations', () => { expect(messages[0].text).toBe('Victim message'); }); }); + + describe('isTemporary message handling', () => { + beforeEach(() => { + // Reset mocks before each test + jest.clearAllMocks(); + }); + + it('should save a message with expiredAt when isTemporary is true', async () => { + // Mock custom config with 24 hour retention + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 24, + }, + }); + + mockReq.body = { isTemporary: true }; + + const beforeSave = new Date(); + const result = await saveMessage(mockReq, mockMessageData); + const afterSave = new Date(); + + expect(result.messageId).toBe('msg123'); + expect(result.expiredAt).toBeDefined(); + expect(result.expiredAt).toBeInstanceOf(Date); + + // Verify expiredAt is approximately 24 hours in the future + const expectedExpirationTime = new Date(beforeSave.getTime() + 24 * 60 * 60 * 1000); + const actualExpirationTime = new Date(result.expiredAt); + + expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( + expectedExpirationTime.getTime() - 1000, + ); + expect(actualExpirationTime.getTime()).toBeLessThanOrEqual( + new Date(afterSave.getTime() + 24 * 60 * 60 * 1000 + 1000).getTime(), + ); + }); + + it('should save a message without expiredAt when isTemporary is false', async () => { + mockReq.body = { isTemporary: false }; + + const result = await saveMessage(mockReq, mockMessageData); + + expect(result.messageId).toBe('msg123'); + expect(result.expiredAt).toBeNull(); + }); + + it('should save a message without expiredAt when isTemporary is not provided', async () => { + // No isTemporary in body + mockReq.body = {}; + + const result = await saveMessage(mockReq, mockMessageData); + + expect(result.messageId).toBe('msg123'); + expect(result.expiredAt).toBeNull(); + }); + + it('should use custom retention period from config', async () => { + // Mock custom config with 48 hour retention + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 48, + }, + }); + + mockReq.body = { isTemporary: true }; + + const beforeSave = new Date(); + const result = await saveMessage(mockReq, mockMessageData); + + expect(result.expiredAt).toBeDefined(); + + // Verify expiredAt is approximately 48 hours in the future + const expectedExpirationTime = new Date(beforeSave.getTime() + 48 * 60 * 60 * 1000); + const actualExpirationTime = new Date(result.expiredAt); + + expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( + expectedExpirationTime.getTime() - 1000, + ); + expect(actualExpirationTime.getTime()).toBeLessThanOrEqual( + expectedExpirationTime.getTime() + 1000, + ); + }); + + it('should handle minimum retention period (1 hour)', async () => { + // Mock custom config with less than minimum retention + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 0.5, // Half hour - should be clamped to 1 hour + }, + }); + + mockReq.body = { isTemporary: true }; + + const beforeSave = new Date(); + const result = await saveMessage(mockReq, mockMessageData); + + expect(result.expiredAt).toBeDefined(); + + // Verify expiredAt is approximately 1 hour in the future (minimum) + const expectedExpirationTime = new Date(beforeSave.getTime() + 1 * 60 * 60 * 1000); + const actualExpirationTime = new Date(result.expiredAt); + + expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( + expectedExpirationTime.getTime() - 1000, + ); + expect(actualExpirationTime.getTime()).toBeLessThanOrEqual( + expectedExpirationTime.getTime() + 1000, + ); + }); + + it('should handle maximum retention period (8760 hours)', async () => { + // Mock custom config with more than maximum retention + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 10000, // Should be clamped to 8760 hours + }, + }); + + mockReq.body = { isTemporary: true }; + + const beforeSave = new Date(); + const result = await saveMessage(mockReq, mockMessageData); + + expect(result.expiredAt).toBeDefined(); + + // Verify expiredAt is approximately 8760 hours (1 year) in the future + const expectedExpirationTime = new Date(beforeSave.getTime() + 8760 * 60 * 60 * 1000); + const actualExpirationTime = new Date(result.expiredAt); + + expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( + expectedExpirationTime.getTime() - 1000, + ); + expect(actualExpirationTime.getTime()).toBeLessThanOrEqual( + expectedExpirationTime.getTime() + 1000, + ); + }); + + it('should handle getCustomConfig errors gracefully', async () => { + // Mock getCustomConfig to throw an error + getCustomConfig.mockRejectedValue(new Error('Config service unavailable')); + + mockReq.body = { isTemporary: true }; + + const result = await saveMessage(mockReq, mockMessageData); + + // Should still save the message but with expiredAt as null + expect(result.messageId).toBe('msg123'); + expect(result.expiredAt).toBeNull(); + }); + + it('should use default retention when config is not provided', async () => { + // Mock getCustomConfig to return empty config + getCustomConfig.mockResolvedValue({}); + + mockReq.body = { isTemporary: true }; + + const beforeSave = new Date(); + const result = await saveMessage(mockReq, mockMessageData); + + expect(result.expiredAt).toBeDefined(); + + // Default retention is 30 days (720 hours) + const expectedExpirationTime = new Date(beforeSave.getTime() + 30 * 24 * 60 * 60 * 1000); + const actualExpirationTime = new Date(result.expiredAt); + + expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual( + expectedExpirationTime.getTime() - 1000, + ); + expect(actualExpirationTime.getTime()).toBeLessThanOrEqual( + expectedExpirationTime.getTime() + 1000, + ); + }); + + it('should not update expiredAt on message update', async () => { + // First save a temporary message + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 24, + }, + }); + + mockReq.body = { isTemporary: true }; + const savedMessage = await saveMessage(mockReq, mockMessageData); + const originalExpiredAt = savedMessage.expiredAt; + + // Now update the message without isTemporary flag + mockReq.body = {}; + const updatedMessage = await updateMessage(mockReq, { + messageId: 'msg123', + text: 'Updated text', + }); + + // expiredAt should not be in the returned updated message object + expect(updatedMessage.expiredAt).toBeUndefined(); + + // Verify in database that expiredAt wasn't changed + const dbMessage = await Message.findOne({ messageId: 'msg123', user: 'user123' }); + expect(dbMessage.expiredAt).toEqual(originalExpiredAt); + }); + + it('should preserve expiredAt when saving existing temporary message', async () => { + // First save a temporary message + getCustomConfig.mockResolvedValue({ + interface: { + temporaryChatRetention: 24, + }, + }); + + mockReq.body = { isTemporary: true }; + const firstSave = await saveMessage(mockReq, mockMessageData); + const originalExpiredAt = firstSave.expiredAt; + + // Wait a bit to ensure time difference + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Save again with same messageId but different text + const updatedData = { ...mockMessageData, text: 'Updated text' }; + const secondSave = await saveMessage(mockReq, updatedData); + + // Should update text but create new expiredAt + expect(secondSave.text).toBe('Updated text'); + expect(secondSave.expiredAt).toBeDefined(); + expect(new Date(secondSave.expiredAt).getTime()).toBeGreaterThan( + new Date(originalExpiredAt).getTime(), + ); + }); + + it('should handle bulk operations with temporary messages', async () => { + // This test verifies bulkSaveMessages doesn't interfere with expiredAt + const messages = [ + { + messageId: 'bulk1', + conversationId: uuidv4(), + text: 'Bulk message 1', + user: 'user123', + expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + }, + { + messageId: 'bulk2', + conversationId: uuidv4(), + text: 'Bulk message 2', + user: 'user123', + expiredAt: null, + }, + ]; + + await bulkSaveMessages(messages); + + const savedMessages = await Message.find({ + messageId: { $in: ['bulk1', 'bulk2'] }, + }).lean(); + + expect(savedMessages).toHaveLength(2); + + const bulk1 = savedMessages.find((m) => m.messageId === 'bulk1'); + const bulk2 = savedMessages.find((m) => m.messageId === 'bulk2'); + + expect(bulk1.expiredAt).toBeDefined(); + expect(bulk2.expiredAt).toBeNull(); + }); + }); }); From 97e1cdd224bb2f98093b62fda7ab89a940abca1b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 27 Jul 2025 11:47:37 -0400 Subject: [PATCH 016/224] =?UTF-8?q?=F0=9F=A7=97=20refactor:=20Replace=20`t?= =?UTF-8?q?raverse`=20package=20with=20Minimal=20Traversal=20for=20Logging?= =?UTF-8?q?=20(#8687)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📦 chore: Remove `keyv` from peerDependencies in package.json and package-lock.json for data-schemas * refactor: replace traverse import with custom object-traverse utility for better control and error handling during logging * chore(data-schemas): bump version to 0.0.15 and remove unused dependencies * refactor: optimize message construction in debugTraverse * chore: update Node.js version to 20.x in data-schemas workflow --- .github/workflows/data-schemas.yml | 2 +- package-lock.json | 118 +++++++----- packages/data-schemas/package.json | 5 +- packages/data-schemas/src/config/parsers.ts | 90 +++++---- .../data-schemas/src/utils/object-traverse.ts | 178 ++++++++++++++++++ 5 files changed, 299 insertions(+), 94 deletions(-) create mode 100644 packages/data-schemas/src/utils/object-traverse.ts diff --git a/.github/workflows/data-schemas.yml b/.github/workflows/data-schemas.yml index fee72fbe0..ee2d9c30d 100644 --- a/.github/workflows/data-schemas.yml +++ b/.github/workflows/data-schemas.yml @@ -22,7 +22,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '18.x' + node-version: '20.x' - name: Install dependencies run: cd packages/data-schemas && npm ci diff --git a/package-lock.json b/package-lock.json index d9c3ea5f4..97df522f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27800,12 +27800,6 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" }, - "node_modules/@types/traverse": { - "version": "0.6.37", - "resolved": "https://registry.npmjs.org/@types/traverse/-/traverse-0.6.37.tgz", - "integrity": "sha512-c90MVeDiUI1FhOZ6rLQ3kDWr50YE8+paDpM+5zbHjbmsqEp2DlMYkqnZnwbK9oI+NvDe8yRajup4jFwnVX6xsA==", - "dev": true - }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -28569,6 +28563,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -28716,6 +28711,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", @@ -28801,6 +28797,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -28871,6 +28868,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -29559,6 +29557,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -30835,6 +30834,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -30852,6 +30852,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -30869,6 +30870,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -31037,6 +31039,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -31065,6 +31068,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -31589,6 +31593,7 @@ "version": "1.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -31767,6 +31772,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7", @@ -33284,6 +33290,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -33491,6 +33498,7 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -33511,6 +33519,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -33695,6 +33704,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -33797,6 +33807,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -34019,6 +34030,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -34035,6 +34047,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -34046,6 +34059,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" @@ -34830,6 +34844,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -34947,6 +34962,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -34970,6 +34986,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, "license": "MIT", "dependencies": { "async-function": "^1.0.0", @@ -34989,6 +35006,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" @@ -35015,6 +35033,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -35056,6 +35075,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -35082,6 +35102,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -35099,6 +35120,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -35147,6 +35169,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -35184,6 +35207,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -35236,6 +35260,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -35278,6 +35303,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -35343,6 +35369,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -35370,6 +35397,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -35382,6 +35410,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -35408,6 +35437,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -35424,6 +35454,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -35441,6 +35472,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -35456,6 +35488,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -35468,6 +35501,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -35483,6 +35517,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -39939,6 +39974,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -39947,6 +39983,7 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -40183,6 +40220,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", @@ -40859,6 +40897,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -43110,6 +43149,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -43166,6 +43206,7 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -44239,6 +44280,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -44258,6 +44300,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, "license": "MIT" }, "node_modules/safe-buffer": { @@ -44283,6 +44326,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -44299,12 +44343,14 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, "license": "MIT" }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -44468,6 +44514,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -44484,6 +44531,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -44499,6 +44547,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -45172,6 +45221,7 @@ "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -45193,6 +45243,7 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -45211,6 +45262,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -46245,6 +46297,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -46259,6 +46312,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -46278,6 +46332,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -46299,6 +46354,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -46321,28 +46377,6 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, - "node_modules/typedarray.prototype.slice": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.5.tgz", - "integrity": "sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==", - "peer": true, - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "math-intrinsics": "^1.1.0", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-offset": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -46433,6 +46467,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -47536,6 +47571,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", @@ -47555,6 +47591,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -47582,12 +47619,14 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, "license": "MIT" }, "node_modules/which-collection": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, "license": "MIT", "dependencies": { "is-map": "^2.0.3", @@ -47606,6 +47645,7 @@ "version": "1.1.18", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -48787,7 +48827,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.13", + "version": "0.0.15", "license": "MIT", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", @@ -48801,7 +48841,6 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.0", - "@types/traverse": "^0.6.37", "jest": "^29.5.0", "jest-junit": "^16.0.0", "mongodb-memory-server": "^10.1.4", @@ -48815,14 +48854,12 @@ }, "peerDependencies": { "jsonwebtoken": "^9.0.2", - "keyv": "^5.3.2", "klona": "^2.0.6", "librechat-data-provider": "*", "lodash": "^4.17.21", "meilisearch": "^0.38.0", "mongoose": "^8.12.1", "nanoid": "^3.3.7", - "traverse": "^0.6.11", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" } @@ -49014,23 +49051,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "packages/data-schemas/node_modules/traverse": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.11.tgz", - "integrity": "sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==", - "peer": true, - "dependencies": { - "gopd": "^1.2.0", - "typedarray.prototype.slice": "^1.0.5", - "which-typed-array": "^1.1.18" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "packages/data-schemas/node_modules/winston": { "version": "3.17.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", diff --git a/packages/data-schemas/package.json b/packages/data-schemas/package.json index 651f82e22..71c5ccc63 100644 --- a/packages/data-schemas/package.json +++ b/packages/data-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/data-schemas", - "version": "0.0.13", + "version": "0.0.15", "description": "Mongoose schemas and models for LibreChat", "type": "module", "main": "dist/index.cjs", @@ -48,7 +48,6 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.0", - "@types/traverse": "^0.6.37", "jest": "^29.5.0", "jest-junit": "^16.0.0", "mongodb-memory-server": "^10.1.4", @@ -62,14 +61,12 @@ }, "peerDependencies": { "jsonwebtoken": "^9.0.2", - "keyv": "^5.3.2", "klona": "^2.0.6", "librechat-data-provider": "*", "lodash": "^4.17.21", "meilisearch": "^0.38.0", "mongoose": "^8.12.1", "nanoid": "^3.3.7", - "traverse": "^0.6.11", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, diff --git a/packages/data-schemas/src/config/parsers.ts b/packages/data-schemas/src/config/parsers.ts index 306484481..c74e03a22 100644 --- a/packages/data-schemas/src/config/parsers.ts +++ b/packages/data-schemas/src/config/parsers.ts @@ -1,6 +1,7 @@ import { klona } from 'klona'; import winston from 'winston'; -import traverse from 'traverse'; +import traverse from '../utils/object-traverse'; +import type { TraverseContext } from '../utils/object-traverse'; const SPLAT_SYMBOL = Symbol.for('splat'); const MESSAGE_SYMBOL = Symbol.for('message'); @@ -123,15 +124,17 @@ const debugTraverse = winston.format.printf( return `${timestamp} ${level}: ${JSON.stringify(message)}`; } - let msg = `${timestamp} ${level}: ${truncateLongStrings(message.trim(), 150)}`; + const msgParts: string[] = [ + `${timestamp} ${level}: ${truncateLongStrings(message.trim(), 150)}`, + ]; try { if (level !== 'debug') { - return msg; + return msgParts[0]; } if (!metadata) { - return msg; + return msgParts[0]; } // Type-safe access to SPLAT_SYMBOL using bracket notation @@ -140,59 +143,66 @@ const debugTraverse = winston.format.printf( const debugValue = Array.isArray(splatArray) ? splatArray[0] : undefined; if (!debugValue) { - return msg; + return msgParts[0]; } if (debugValue && Array.isArray(debugValue)) { - msg += `\n${JSON.stringify(debugValue.map(condenseArray))}`; - return msg; + msgParts.push(`\n${JSON.stringify(debugValue.map(condenseArray))}`); + return msgParts.join(''); } if (typeof debugValue !== 'object') { - return (msg += ` ${debugValue}`); + msgParts.push(` ${debugValue}`); + return msgParts.join(''); } - msg += '\n{'; + msgParts.push('\n{'); const copy = klona(metadata); + try { + const traversal = traverse(copy); + traversal.forEach(function (this: TraverseContext, value: unknown) { + if (typeof this?.key === 'symbol') { + return; + } - traverse(copy).forEach(function (this: traverse.TraverseContext, value: unknown) { - if (typeof this?.key === 'symbol') { - return; - } + let _parentKey = ''; + const parent = this.parent; - let _parentKey = ''; - const parent = this.parent; + if (typeof parent?.key !== 'symbol' && parent?.key !== undefined) { + _parentKey = String(parent.key); + } - if (typeof parent?.key !== 'symbol' && parent?.key) { - _parentKey = parent.key; - } + const parentKey = `${parent && parent.notRoot ? _parentKey + '.' : ''}`; + const tabs = `${parent && parent.notRoot ? ' ' : ' '}`; + const currentKey = this?.key ?? 'unknown'; - const parentKey = `${parent && parent.notRoot ? _parentKey + '.' : ''}`; - const tabs = `${parent && parent.notRoot ? ' ' : ' '}`; - const currentKey = this?.key ?? 'unknown'; + if (this.isLeaf && typeof value === 'string') { + const truncatedText = truncateLongStrings(value); + msgParts.push(`\n${tabs}${parentKey}${currentKey}: ${JSON.stringify(truncatedText)},`); + } else if (this.notLeaf && Array.isArray(value) && value.length > 0) { + const currentMessage = `\n${tabs}// ${value.length} ${String(currentKey).replace(/s$/, '')}(s)`; + this.update(currentMessage); + msgParts.push(currentMessage); + const stringifiedArray = value.map(condenseArray); + msgParts.push(`\n${tabs}${parentKey}${currentKey}: [${stringifiedArray}],`); + } else if (this.isLeaf && typeof value === 'function') { + msgParts.push(`\n${tabs}${parentKey}${currentKey}: function,`); + } else if (this.isLeaf) { + msgParts.push(`\n${tabs}${parentKey}${currentKey}: ${value},`); + } + }); + } catch (e: unknown) { + const errorMessage = e instanceof Error ? e.message : 'Unknown error'; + msgParts.push(`\n[LOGGER TRAVERSAL ERROR] ${errorMessage}`); + } - if (this.isLeaf && typeof value === 'string') { - const truncatedText = truncateLongStrings(value); - msg += `\n${tabs}${parentKey}${currentKey}: ${JSON.stringify(truncatedText)},`; - } else if (this.notLeaf && Array.isArray(value) && value.length > 0) { - const currentMessage = `\n${tabs}// ${value.length} ${currentKey.replace(/s$/, '')}(s)`; - this.update(currentMessage, true); - msg += currentMessage; - const stringifiedArray = value.map(condenseArray); - msg += `\n${tabs}${parentKey}${currentKey}: [${stringifiedArray}],`; - } else if (this.isLeaf && typeof value === 'function') { - msg += `\n${tabs}${parentKey}${currentKey}: function,`; - } else if (this.isLeaf) { - msg += `\n${tabs}${parentKey}${currentKey}: ${value},`; - } - }); - - msg += '\n}'; - return msg; + msgParts.push('\n}'); + return msgParts.join(''); } catch (e: unknown) { const errorMessage = e instanceof Error ? e.message : 'Unknown error'; - return (msg += `\n[LOGGER PARSING ERROR] ${errorMessage}`); + msgParts.push(`\n[LOGGER PARSING ERROR] ${errorMessage}`); + return msgParts.join(''); } }, ); diff --git a/packages/data-schemas/src/utils/object-traverse.ts b/packages/data-schemas/src/utils/object-traverse.ts new file mode 100644 index 000000000..836f59a34 --- /dev/null +++ b/packages/data-schemas/src/utils/object-traverse.ts @@ -0,0 +1,178 @@ +/** + * ESM-native object traversal utility + * Simplified implementation focused on the forEach use case + */ + +export interface TraverseContext { + node: unknown; + path: (string | number)[]; + parent: TraverseContext | undefined; + key: string | number | undefined; + isLeaf: boolean; + notLeaf: boolean; + isRoot: boolean; + notRoot: boolean; + level: number; + circular: TraverseContext | null; + update: (value: unknown, stopHere?: boolean) => void; + remove: () => void; +} + +type ForEachCallback = (this: TraverseContext, value: unknown) => void; + +// Type guards for proper typing +type TraversableObject = Record | unknown[]; + +function isObject(value: unknown): value is TraversableObject { + if (value === null || typeof value !== 'object') { + return false; + } + + // Treat these built-in types as leaf nodes, not objects to traverse + if (value instanceof Date) return false; + if (value instanceof RegExp) return false; + if (value instanceof Error) return false; + if (value instanceof URL) return false; + + // Check for Buffer (Node.js) + if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return false; + + // Check for TypedArrays and ArrayBuffer + if (ArrayBuffer.isView(value)) return false; + if (value instanceof ArrayBuffer) return false; + if (value instanceof SharedArrayBuffer) return false; + + // Check for other built-in types that shouldn't be traversed + if (value instanceof Promise) return false; + if (value instanceof WeakMap) return false; + if (value instanceof WeakSet) return false; + if (value instanceof Map) return false; + if (value instanceof Set) return false; + + // Check if it's a primitive wrapper object + const stringTag = Object.prototype.toString.call(value); + if ( + stringTag === '[object Boolean]' || + stringTag === '[object Number]' || + stringTag === '[object String]' + ) { + return false; + } + + return true; +} + +// Helper to safely set a property on an object or array +function setProperty(obj: TraversableObject, key: string | number, value: unknown): void { + if (Array.isArray(obj) && typeof key === 'number') { + obj[key] = value; + } else if (!Array.isArray(obj) && typeof key === 'string') { + obj[key] = value; + } else if (!Array.isArray(obj) && typeof key === 'number') { + // Handle numeric keys on objects + obj[key] = value; + } +} + +// Helper to safely delete a property from an object +function deleteProperty(obj: TraversableObject, key: string | number): void { + if (Array.isArray(obj) && typeof key === 'number') { + // For arrays, we should use splice, but this is handled in remove() + // This function is only called for non-array deletion + return; + } + + if (!Array.isArray(obj)) { + delete obj[key]; + } +} + +function forEach(obj: unknown, callback: ForEachCallback): void { + const visited = new WeakSet(); + + function walk(node: unknown, path: (string | number)[] = [], parent?: TraverseContext): void { + // Check for circular references + let circular: TraverseContext | null = null; + if (isObject(node)) { + if (visited.has(node)) { + // Find the circular reference in the parent chain + let p = parent; + while (p) { + if (p.node === node) { + circular = p; + break; + } + p = p.parent; + } + return; // Skip circular references + } + visited.add(node); + } + + const key = path.length > 0 ? path[path.length - 1] : undefined; + const isRoot = path.length === 0; + const level = path.length; + + // Determine if this is a leaf node + const isLeaf = + !isObject(node) || + (Array.isArray(node) && node.length === 0) || + Object.keys(node).length === 0; + + // Create context + const context: TraverseContext = { + node, + path: [...path], + parent, + key, + isLeaf, + notLeaf: !isLeaf, + isRoot, + notRoot: !isRoot, + level, + circular, + update(value: unknown) { + if (!isRoot && parent && key !== undefined && isObject(parent.node)) { + setProperty(parent.node, key, value); + } + this.node = value; + }, + remove() { + if (!isRoot && parent && key !== undefined && isObject(parent.node)) { + if (Array.isArray(parent.node) && typeof key === 'number') { + parent.node.splice(key, 1); + } else { + deleteProperty(parent.node, key); + } + } + }, + }; + + // Call the callback with the context + callback.call(context, node); + + // Traverse children if not circular and is an object + if (!circular && isObject(node) && !isLeaf) { + if (Array.isArray(node)) { + for (let i = 0; i < node.length; i++) { + walk(node[i], [...path, i], context); + } + } else { + for (const [childKey, childValue] of Object.entries(node)) { + walk(childValue, [...path, childKey], context); + } + } + } + } + + walk(obj); +} + +// Main traverse function that returns an object with forEach method +export default function traverse(obj: unknown) { + return { + forEach(callback: ForEachCallback): void { + forEach(obj, callback); + }, + }; +} From 79197454f8296ebbb7423b961f9db6fcff688b6e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 27 Jul 2025 12:19:01 -0400 Subject: [PATCH 017/224] =?UTF-8?q?=F0=9F=93=A6=20feat:=20Move=20Shared=20?= =?UTF-8?q?Components=20to=20`@librechat/client`=20(#8685)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: init @librechat/client * feat: Add common types and interfaces for accessibility, agents, artifacts, assistants, and tools * feat: Add jotai as a peer dependency * fix build client package * feat: cleanup unused types from common/index.ts - Remove 104 unused type exports from packages/client/src/common/index.ts - Keep only 7 actually used exports (93% reduction) - Add cleanup script with enhanced import pattern detection - Support both named imports and namespace imports (* as t) - Create automatic backups and comprehensive documentation - Maintain type safety with build verification - No breaking changes to existing code Kept exports: - TShowToast, Option, OptionWithIcon, DropdownValueSetter - MentionOption, NotificationSeverity, MenuItemProps Scripts: cleanup-common-types-safe.js, README-CLEANUP.md * fix: cleanup * fix: package; refactor: tsconfig * feat: add back `recoil` * fix: move dependencies to peerDependencies in client package * feat: add @librechat/client as a dependency in package.json and package-lock.json * feat: update client package configuration and dependencies - Added new dependencies for Rollup plugins and updated existing ones in package.json and package-lock.json. - Introduced a new Rollup configuration file for building the client package. - Refactored build scripts to include a dedicated build command for the client. - Updated TypeScript configuration for improved module resolution and type declaration output. - Integrated a Toast component from the client package into the main App component. * feat: enhance Rollup configuration for client package - Updated terser plugin settings to preserve directives like 'use client'. - Added custom warning handler to ignore "use client" directive warnings during the build process. * chore: rename package/client build script command * feat: update client package dependencies and Rollup configuration - Added rollup-plugin-postcss to package.json and updated package-lock.json. - Enhanced Rollup configuration to include postcss plugin for CSS handling. - Updated index.ts to export all components from the components directory for better modularity. * feat: add client package directory to update configuration - Included the 'client' package directory in the update.js configuration to ensure it is recognized during updates. * feat: export Toast component in client package - Added export for the Toast component in index.ts to enhance modularity and accessibility of components. * feat: /client transition to @librechat/client * chore: fixed formatting issues * fix: update peer dependencies in @librechat/client to prevent bundling them * fix: correct useSprings implementation in SplitText component * fix: circular dependencies in DataTable * fix: add remaining peer dependencies and match actual versions previously used in `client/package.json` * fix: correct frontend:ci script to include client package build * chore: enhance unused package detection for @librechat/client and improve dependency extraction * fix: add missing peer dependency for @radix-ui/react-collapsible * chore: include "packages/client" in unused i18next keys detection * test: update AgentFooter tests to use document.querySelector for spinner checks test: mock window.matchMedia in setupTests.js for consistent test environment * feat: add react-hook-form dependency and update FormInput component to use its types * chore: linting * refactor: remove unused defaultSelectedValues prop from MCPSelect and MultiSelect components * chore: linting * feat: update GitHub Actions workflow to publish @librechat/client * chore: update GitHub Actions workflow to install and build data-provider and client dependencies * chore: add missing @testing-library/react dependency to client package * chore: update tsconfig.json to exclude additional test files * chore: fix build issues, resolve latest LC changes * chore: move MCP components outside of `~/components/ui` * feat: implement dynamic theme system with environment variable support and Tailwind CSS integration * chore: remove unnecessary logging of sttExternal and ttsExternal in Speech component * chore: squashed cleanup commits chore: move @tanstack/react-virtual to dependencies and remove recoil from package.json chore: move dependencies to peerDependencies in package.json feat: update package.json and rollup.config.js to include jotai and enhance bundling configuration feat: update package.json and rollup.config.js to include jotai and enhance bundling configuration refactor: reorganize exports in index.ts for improved clarity refactor: remove unused types and interfaces from common files refactor: update peer dependencies and improve component typings - Removed duplicate peer dependencies from package.json and organized them. - Updated rollup.config.js to disable TypeScript checking during the build process. - Modified AnimatedTabs component to use React.ReactNode for label and content types, and added TypeScript workarounds for compatibility. - Enhanced Label and Separator components to accept an optional className prop and improved prop spreading. - Updated Slider component to include an optional className prop and refined prop handling for better type safety. refactor: clean up client workflow and update package dependencies refactor: update package dependencies and improve PostCSS and Rollup configurations chore: bump version to 0.1.2 in package.json chore: bump client version to 0.1.2 in package-lock.json chore: bump client version to 0.1.3 and update dependencies chore: bump client version to 0.1.4 and update @react-spring dependencies chore: update package version to 0.1.5 and adjust peer dependencies - Bump version in package.json from 0.1.4 to 0.1.5. - Update peer dependency for @tanstack/react-query to allow version 5.0.0. - Add @tanstack/react-table and @tanstack/react-virtual as dependencies. - Update various dependencies to their latest compatible versions. - Simplify postcss.config.js by removing unnecessary options. - Clean up rollup.config.js by removing ignored PostCSS warnings. - Update CheckboxButton component to cast icon as React JSX element. - Adjust Combobox component's class names for better styling. - Change DropdownPopup component to use React's namespace import. - Modify InputOTP component to use 'any' type for OTPInputContext. - Ensure displayLabel and value in ModelParameters are converted to strings. - Update MultiSearch component's placeholder to ensure it's a string. - Cast selectIcon in MultiSelect as React JSX element for consistency. - Update OGDialogTemplate to cast selectText as React JSX element. - Initialize animationRef in PixelCard with undefined for clarity. - Add TypeScript ignore comments in Select and SelectDropDown components for Radix UI type conflicts. - Ensure title in SelectDropDown is a string and adjust rendering of options. - Update useLocalize hook to cast options as any for compatibility. refactor: code structure; chore: translations cleanup chore: remove unused imports and clean up code in NewChat component refactor: enhance Menu component to support custom render functions for menu items style: update itemClassName in ToolsDropdown for improved UI consistency fix: merge conflicts chore: update @radix-ui/react-accordion to version 1.2.11 * refactor: remove unnecessary TypeScript type assertions in AnimatedTabs, Label, Separator, and Slider components * feat: enhance theme system with localStorage persistence and new theme atoms * chore: bump version of @librechat/client to 0.1.7 * chore: fix ci/cd warnings/errors related to linting and unused localization keys * chore: update dependencies for class-variance-authority, clsx, and match-sorter * chore: bump @librechat/client to v0.1.8 * feat: add utility colors for theme customization and remove unused tailwindConfig * v0.1.9 --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com> --- .github/workflows/client.yml | 46 +- .github/workflows/i18n-unused-keys.yml | 3 +- .github/workflows/unused-packages.yml | 53 +- bun.lockb | Bin 1131200 -> 1113680 bytes client/package.json | 8 +- client/src/App.jsx | 20 +- client/src/Providers/index.ts | 2 - client/src/a11y/LiveAnnouncer.tsx | 3 +- client/src/common/menus.ts | 1 - client/src/common/types.ts | 15 +- client/src/components/Artifacts/Artifacts.tsx | 2 +- client/src/components/Artifacts/Code.tsx | 7 +- .../components/Artifacts/DownloadArtifact.tsx | 2 +- client/src/components/Artifacts/Mermaid.tsx | 3 +- client/src/components/Audio/TTS.tsx | 2 +- client/src/components/Audio/Voices.tsx | 2 +- client/src/components/Auth/AuthLayout.tsx | 4 +- client/src/components/Auth/Login.tsx | 4 +- client/src/components/Auth/LoginForm.tsx | 6 +- client/src/components/Auth/Registration.tsx | 4 +- .../components/Auth/RequestPasswordReset.tsx | 4 +- client/src/components/Auth/ResetPassword.tsx | 2 +- .../src/components/Auth/SocialLoginRender.tsx | 2 +- .../src/components/Auth/TwoFactorScreen.tsx | 4 +- client/src/components/Auth/VerifyEmail.tsx | 5 +- .../Bookmarks/BookmarkEditDialog.tsx | 4 +- .../src/components/Bookmarks/BookmarkForm.tsx | 3 +- .../src/components/Bookmarks/BookmarkItem.tsx | 2 +- .../Bookmarks/DeleteBookmarkButton.tsx | 13 +- .../Bookmarks/EditBookmarkButton.tsx | 4 +- client/src/components/Chat/AddMultiConvo.tsx | 2 +- client/src/components/Chat/ChatView.tsx | 2 +- .../components/Chat/ExportAndShareMenu.tsx | 4 +- client/src/components/Chat/Header.tsx | 3 +- .../src/components/Chat/Input/Artifacts.tsx | 2 +- .../Chat/Input/ArtifactsSubMenu.tsx | 2 +- .../components/Chat/Input/AudioRecorder.tsx | 5 +- client/src/components/Chat/Input/BadgeRow.tsx | 2 +- client/src/components/Chat/Input/ChatForm.tsx | 2 +- .../components/Chat/Input/CircleRender.tsx | 2 +- .../components/Chat/Input/CodeInterpreter.tsx | 2 +- .../components/Chat/Input/CollapseChat.tsx | 2 +- .../src/components/Chat/Input/EditBadges.tsx | 2 +- .../src/components/Chat/Input/FileSearch.tsx | 3 +- .../Chat/Input/Files/AttachFile.tsx | 2 +- .../Chat/Input/Files/AttachFileMenu.tsx | 6 +- .../Chat/Input/Files/DragDropModal.tsx | 2 +- .../Chat/Input/Files/FilePreview.tsx | 4 +- .../components/Chat/Input/Files/FileRow.tsx | 4 +- .../components/Chat/Input/Files/FilesView.tsx | 2 +- .../Chat/Input/Files/ImagePreview.tsx | 8 +- .../Chat/Input/Files/Table/Columns.tsx | 11 +- .../Chat/Input/Files/Table/DataTable.tsx | 13 +- .../Input/Files/Table/SortFilterHeader.tsx | 4 +- .../Chat/Input/Files/Table/TemplateTable.tsx | 2 +- .../components/Chat/Input/HeaderOptions.tsx | 6 +- .../components/Chat/Input/MCPConfigDialog.tsx | 121 + .../src/components/Chat/Input/MCPSelect.tsx | 8 +- .../src/components/Chat/Input/MCPSubMenu.tsx | 6 +- client/src/components/Chat/Input/Mention.tsx | 5 +- .../components/Chat/Input/OptionsPopover.tsx | 5 +- .../components/Chat/Input/PopoverButtons.tsx | 3 +- .../components/Chat/Input/PromptsCommand.tsx | 4 +- .../src/components/Chat/Input/SendButton.tsx | 3 +- .../src/components/Chat/Input/StopButton.tsx | 2 +- .../components/Chat/Input/ToolsDropdown.tsx | 5 +- .../src/components/Chat/Input/WebSearch.tsx | 2 +- client/src/components/Chat/Landing.tsx | 2 +- .../components/Chat/Menus/BookmarkMenu.tsx | 4 +- .../Menus/Bookmarks/BookmarkMenuItems.tsx | 2 +- .../Endpoints/components/EndpointItem.tsx | 2 +- .../components/Chat/Menus/HeaderNewChat.tsx | 3 +- .../src/components/Chat/Menus/OpenSidebar.tsx | 3 +- .../Chat/Menus/Presets/EditPresetDialog.tsx | 16 +- .../Chat/Menus/Presets/PresetItems.tsx | 14 +- .../src/components/Chat/Menus/PresetsMenu.tsx | 2 +- .../components/Chat/Menus/UI/TitleButton.tsx | 2 +- .../Chat/Messages/Content/ContentParts.tsx | 2 +- .../Chat/Messages/Content/DialogImage.tsx | 2 +- .../Chat/Messages/Content/EditMessage.tsx | 4 +- .../Chat/Messages/Content/Image.tsx | 2 +- .../Messages/Content/MarkdownComponents.tsx | 5 +- .../Chat/Messages/Content/MessageContent.tsx | 2 +- .../Messages/Content/Parts/EditTextPart.tsx | 2 +- .../Chat/Messages/Content/Parts/LogLink.tsx | 2 +- .../Parts/OpenAIImageGen/OpenAIImageGen.tsx | 2 +- .../Chat/Messages/Content/ProgressText.tsx | 2 +- .../Chat/Messages/Content/SearchContent.tsx | 2 +- .../Chat/Messages/Content/ToolCall.tsx | 2 +- .../src/components/Chat/Messages/Feedback.tsx | 16 +- client/src/components/Chat/Messages/Fork.tsx | 2 +- .../components/Chat/Messages/HoverButtons.tsx | 2 +- .../components/Chat/Messages/MessageParts.tsx | 1 - .../Chat/Messages/MinimalHoverButtons.tsx | 2 +- .../components/Chat/Messages/MultiMessage.tsx | 3 - client/src/components/Chat/TemporaryChat.tsx | 2 +- .../Conversations/Conversations.tsx | 4 +- client/src/components/Conversations/Convo.tsx | 4 +- .../ConvoOptions/ConvoOptions.tsx | 4 +- .../ConvoOptions/DeleteButton.tsx | 6 +- .../ConvoOptions/ShareButton.tsx | 3 +- .../ConvoOptions/SharedLinkButton.tsx | 12 +- client/src/components/Endpoints/Icon.tsx | 22 +- .../Endpoints/MessageEndpointIcon.tsx | 2 +- .../src/components/Endpoints/MinimalIcon.tsx | 2 +- .../Endpoints/SaveAsPresetDialog.tsx | 10 +- .../Endpoints/Settings/Advanced.tsx | 10 +- .../Endpoints/Settings/AgentSettings.tsx | 4 +- .../Endpoints/Settings/Assistants.tsx | 7 +- .../Endpoints/Settings/Examples.tsx | 6 +- .../components/Endpoints/Settings/Google.tsx | 15 +- .../Endpoints/Settings/OptionHover.tsx | 4 +- .../components/Endpoints/Settings/Plugins.tsx | 10 +- client/src/components/Files/ActionButton.tsx | 2 +- .../src/components/Files/DeleteIconButton.tsx | 3 +- .../Files/FileList/DataTableFile.tsx | 11 +- .../Files/FileList/FileListItem.tsx | 5 +- .../Files/FileList/FileListItem2.tsx | 7 +- .../components/Files/FileList/FilePreview.tsx | 9 +- .../Files/FileList/FileSidePanel.tsx | 5 +- .../Files/FileList/FileTableColumns.tsx | 9 +- .../Files/FileList/UploadFileButton.tsx | 4 +- .../Files/FileList/UploadFileModal.tsx | 3 +- .../Files/VectorStore/VectorStoreButton.tsx | 4 +- .../Files/VectorStore/VectorStoreListItem.tsx | 3 +- .../Files/VectorStore/VectorStorePreview.tsx | 5 +- .../VectorStore/VectorStoreSidePanel.tsx | 9 +- .../Input/Generations/Regenerate.tsx | 4 +- .../src/components/Input/Generations/Stop.tsx | 6 +- .../Input/ModelSelect/Anthropic.tsx | 2 +- .../components/Input/ModelSelect/ChatGPT.tsx | 2 +- .../components/Input/ModelSelect/Google.tsx | 2 +- .../ModelSelect}/MultiSelectDropDown.tsx | 10 +- .../ModelSelect}/MultiSelectPop.tsx | 12 +- .../components/Input/ModelSelect/OpenAI.tsx | 2 +- .../Input/ModelSelect/PluginsByIndex.tsx | 11 +- .../ModelSelect}/SelectDropDownPop.tsx | 2 +- .../Input/SetKeyDialog/GoogleConfig.tsx | 2 +- .../Input/SetKeyDialog/InputWithLabel.tsx | 2 +- .../Input/SetKeyDialog/SetKeyDialog.tsx | 8 +- .../{ui => }/MCP/CustomUserVarsSection.tsx | 4 +- .../{ui => }/MCP/MCPConfigDialog.tsx | 10 +- .../{ui => }/MCP/MCPServerStatusIcon.tsx | 0 .../MCP/ServerInitializationSection.tsx | 7 +- .../components/Messages/Content/CodeBlock.tsx | 7 +- .../src/components/Messages/Content/Error.tsx | 2 +- .../components/Messages/Content/Plugin.tsx | 10 +- .../components/Messages/Content/RunCode.tsx | 9 +- client/src/components/Nav/AccountSettings.tsx | 3 +- .../components/Nav/Bookmarks/BookmarkNav.tsx | 2 +- .../Nav/ExportConversation/ExportModal.tsx | 11 +- client/src/components/Nav/Nav.tsx | 2 +- client/src/components/Nav/NavToggle.tsx | 4 +- client/src/components/Nav/NewChat.tsx | 7 +- client/src/components/Nav/Settings.tsx | 29 +- .../Nav/SettingsTabs/Account/Avatar.tsx | 6 +- .../SettingsTabs/Account/BackupCodesItem.tsx | 4 +- .../SettingsTabs/Account/DeleteAccount.tsx | 4 +- .../Account/DisableTwoFactorToggle.tsx | 2 +- .../Account/DisplayUsernameMessages.tsx | 2 +- .../Account/TwoFactorAuthentication.tsx | 18 +- .../Account/TwoFactorPhases/BackupPhase.tsx | 2 +- .../Account/TwoFactorPhases/DisablePhase.tsx | 2 +- .../Account/TwoFactorPhases/QRPhase.tsx | 2 +- .../Account/TwoFactorPhases/SetupPhase.tsx | 2 +- .../Account/TwoFactorPhases/VerifyPhase.tsx | 8 +- .../Balance/AutoRefillSettings.tsx | 4 +- .../SettingsTabs/Balance/TokenCreditsItem.tsx | 4 +- .../Nav/SettingsTabs/Chat/ChatDirection.tsx | 2 +- .../SettingsTabs/Chat/FontSizeSelector.tsx | 3 +- .../Nav/SettingsTabs/Chat/ForkSettings.tsx | 4 +- .../Nav/SettingsTabs/Chat/SaveBadgesState.tsx | 4 +- .../Nav/SettingsTabs/Chat/ShowThinking.tsx | 4 +- .../SettingsTabs/Commands/AtCommandSwitch.tsx | 2 +- .../Commands/PlusCommandSwitch.tsx | 2 +- .../Commands/SlashCommandSwitch.tsx | 2 +- .../Nav/SettingsTabs/DangerButton.tsx | 5 +- .../Nav/SettingsTabs/Data/ClearChats.tsx | 12 +- .../components/Nav/SettingsTabs/Data/Data.tsx | 2 +- .../Nav/SettingsTabs/Data/DeleteCache.tsx | 13 +- .../SettingsTabs/Data/ImportConversations.tsx | 3 +- .../Nav/SettingsTabs/Data/RevokeAllKeys.tsx | 2 +- .../SettingsTabs/Data/RevokeKeysButton.tsx | 12 +- .../Nav/SettingsTabs/Data/SharedLinks.tsx | 19 +- .../SettingsTabs/General/ArchivedChats.tsx | 3 +- .../General/ArchivedChatsTable.tsx | 16 +- .../Nav/SettingsTabs/General/General.tsx | 4 +- .../Nav/SettingsTabs/HoverCardSettings.tsx | 9 +- .../Nav/SettingsTabs/Personalization.tsx | 3 +- .../Speech/ConversationModeSwitch.tsx | 2 +- .../Speech/STT/AutoSendTextSelector.tsx | 2 +- .../Speech/STT/AutoTranscribeAudioSwitch.tsx | 2 +- .../Speech/STT/DecibelSelector.tsx | 8 +- .../Speech/STT/EngineSTTDropdown.tsx | 2 +- .../Speech/STT/LanguageSTTDropdown.tsx | 2 +- .../Speech/STT/SpeechToTextSwitch.tsx | 2 +- .../Nav/SettingsTabs/Speech/Speech.tsx | 7 +- .../Speech/TTS/AutomaticPlaybackSwitch.tsx | 2 +- .../Speech/TTS/CacheTTSSwitch.tsx | 2 +- .../Speech/TTS/CloudBrowserVoicesSwitch.tsx | 2 +- .../Speech/TTS/EngineTTSDropdown.tsx | 2 +- .../SettingsTabs/Speech/TTS/PlaybackRate.tsx | 8 +- .../Speech/TTS/TextToSpeechSwitch.tsx | 2 +- .../SettingsTabs/Speech/TTS/VoiceDropdown.tsx | 2 +- .../Nav/SettingsTabs/ToggleSwitch.tsx | 7 +- .../Plugins/Store/PluginAuthForm.tsx | 2 +- .../Plugins/Store/PluginTooltip.tsx | 2 +- .../src/components/Prompts/AdminSettings.tsx | 10 +- client/src/components/Prompts/BackToChat.tsx | 4 +- client/src/components/Prompts/Command.tsx | 2 +- .../src/components/Prompts/DeleteVersion.tsx | 3 +- client/src/components/Prompts/Description.tsx | 2 +- .../Prompts/Groups/AlwaysMakeProd.tsx | 2 +- .../Prompts/Groups/AutoSendPrompt.tsx | 2 +- .../Prompts/Groups/CategorySelector.tsx | 4 +- .../Prompts/Groups/ChatGroupItem.tsx | 4 +- .../Prompts/Groups/CreatePromptForm.tsx | 4 +- .../Prompts/Groups/DashGroupItem.tsx | 14 +- .../Prompts/Groups/FilterPrompts.tsx | 8 +- .../Prompts/Groups/GroupSidePanel.tsx | 3 +- client/src/components/Prompts/Groups/List.tsx | 2 +- .../components/Prompts/Groups/ListCard.tsx | 5 +- .../Prompts/Groups/NoPromptGroup.tsx | 2 +- .../Prompts/Groups/PanelNavigation.tsx | 2 +- .../Prompts/Groups/VariableDialog.tsx | 2 +- .../Prompts/Groups/VariableForm.tsx | 2 +- .../src/components/Prompts/ManagePrompts.tsx | 2 +- .../src/components/Prompts/PreviewLabels.tsx | 2 +- .../src/components/Prompts/PreviewPrompt.tsx | 2 +- .../src/components/Prompts/PromptDetails.tsx | 2 +- .../src/components/Prompts/PromptEditor.tsx | 17 +- client/src/components/Prompts/PromptForm.tsx | 29 +- client/src/components/Prompts/PromptName.tsx | 2 +- .../components/Prompts/PromptVariables.tsx | 2 +- .../src/components/Prompts/PromptVersions.tsx | 2 +- client/src/components/Prompts/SharePrompt.tsx | 14 +- .../src/components/Prompts/SkeletonForm.tsx | 2 +- .../components/Prompts/VariablesDropdown.tsx | 2 +- client/src/components/Share/MessageIcon.tsx | 2 +- client/src/components/Share/ShareView.tsx | 2 +- .../SidePanel/Agents/ActionsInput.tsx | 7 +- .../SidePanel/Agents/ActionsPanel.tsx | 15 +- .../SidePanel/Agents/AdminSettings.tsx | 13 +- .../Agents/Advanced/AdvancedButton.tsx | 2 +- .../SidePanel/Agents/Advanced/AgentChain.tsx | 11 +- .../Agents/Advanced/MaxAgentSteps.tsx | 6 +- .../SidePanel/Agents/AgentAvatar.tsx | 4 +- .../SidePanel/Agents/AgentConfig.tsx | 3 +- .../SidePanel/Agents/AgentFooter.tsx | 4 +- .../SidePanel/Agents/AgentPanel.tsx | 3 +- .../SidePanel/Agents/AgentPanelSkeleton.tsx | 2 +- .../SidePanel/Agents/AgentSelect.tsx | 2 +- .../components/SidePanel/Agents/AgentTool.tsx | 18 +- .../components/SidePanel/Agents/Artifacts.tsx | 6 +- .../SidePanel/Agents/Code/Action.tsx | 6 +- .../SidePanel/Agents/Code/ApiKeyDialog.tsx | 5 +- .../SidePanel/Agents/Code/Files.tsx | 2 +- .../SidePanel/Agents/DeleteButton.tsx | 13 +- .../SidePanel/Agents/DuplicateAgent.tsx | 2 +- .../SidePanel/Agents/FileContext.tsx | 10 +- .../SidePanel/Agents/FileSearch.tsx | 2 +- .../SidePanel/Agents/FileSearchCheckbox.tsx | 6 +- .../SidePanel/Agents/ImageVision.tsx | 6 +- .../SidePanel/Agents/Instructions.tsx | 6 +- .../components/SidePanel/Agents/MCPIcon.tsx | 2 +- .../components/SidePanel/Agents/MCPInput.tsx | 6 +- .../components/SidePanel/Agents/MCPPanel.tsx | 24 +- .../SidePanel/Agents/MCPSection.tsx | 2 +- .../SidePanel/Agents/ModelPanel.tsx | 4 +- .../components/SidePanel/Agents/Retrieval.tsx | 4 +- .../SidePanel/Agents/Search/Action.tsx | 6 +- .../SidePanel/Agents/Search/ApiKeyDialog.tsx | 3 +- .../SidePanel/Agents/Search/InputSection.tsx | 7 +- .../SidePanel/Agents/ShareAgent.tsx | 6 +- .../Agents/Version/VersionButton.tsx | 4 +- .../Agents/Version/VersionContent.tsx | 6 +- .../SidePanel/Agents/Version/VersionPanel.tsx | 5 +- .../Version/__tests__/VersionContent.spec.tsx | 2 +- .../Agents/__tests__/AgentFooter.spec.tsx | 6 +- .../SidePanel/Bookmarks/BookmarkTable.tsx | 4 +- .../SidePanel/Bookmarks/BookmarkTableRow.tsx | 3 +- .../components/SidePanel/Builder/Action.tsx | 2 +- .../SidePanel/Builder/ActionCallback.tsx | 3 +- .../SidePanel/Builder/ActionsAuth.tsx | 2 +- .../SidePanel/Builder/ActionsInput.tsx | 6 +- .../SidePanel/Builder/ActionsPanel.tsx | 17 +- .../SidePanel/Builder/AppendDateCheckbox.tsx | 8 +- .../SidePanel/Builder/AssistantAvatar.tsx | 9 +- .../Builder/AssistantConversationStarters.tsx | 2 +- .../SidePanel/Builder/AssistantPanel.tsx | 7 +- .../SidePanel/Builder/AssistantSelect.tsx | 8 +- .../SidePanel/Builder/AssistantTool.tsx | 14 +- .../src/components/SidePanel/Builder/Code.tsx | 8 +- .../SidePanel/Builder/CodeFiles.tsx | 2 +- .../SidePanel/Builder/ContextButton.tsx | 15 +- .../SidePanel/Builder/ImageVision.tsx | 6 +- .../SidePanel/Builder/Knowledge.tsx | 2 +- .../src/components/SidePanel/Builder/MCP.tsx | 3 +- .../components/SidePanel/Builder/MCPAuth.tsx | 2 +- .../SidePanel/Builder/Retrieval.tsx | 6 +- .../SidePanel/Files/PanelColumns.tsx | 5 +- .../components/SidePanel/Files/PanelTable.tsx | 24 +- .../src/components/SidePanel/MCP/MCPPanel.tsx | 11 +- .../SidePanel/MCP/MCPPanelSkeleton.tsx | 2 +- .../SidePanel/Memories/AdminSettings.tsx | 13 +- .../SidePanel/Memories/MemoryCreateDialog.tsx | 12 +- .../SidePanel/Memories/MemoryEditDialog.tsx | 12 +- .../SidePanel/Memories/MemoryViewer.tsx | 8 +- client/src/components/SidePanel/Nav.tsx | 8 +- .../SidePanel/Parameters/DynamicCheckbox.tsx | 10 +- .../SidePanel/Parameters/DynamicCombobox.tsx | 19 +- .../SidePanel/Parameters/DynamicDropdown.tsx | 16 +- .../SidePanel/Parameters/DynamicInput.tsx | 2 +- .../SidePanel/Parameters/DynamicSlider.tsx | 2 +- .../SidePanel/Parameters/DynamicSwitch.tsx | 2 +- .../SidePanel/Parameters/DynamicTags.tsx | 6 +- .../SidePanel/Parameters/DynamicTextarea.tsx | 18 +- .../SidePanel/Parameters/OptionHover.tsx | 2 +- .../components/SidePanel/Parameters/Panel.tsx | 4 +- client/src/components/SidePanel/SidePanel.tsx | 4 +- .../components/SidePanel/SidePanelGroup.tsx | 8 +- client/src/components/Web/Sources.tsx | 14 +- .../components/svg/{Files => }/CodePaths.tsx | 0 .../components/svg/{Files => }/FileIcon.tsx | 0 .../components/svg/{Files => }/FilePaths.tsx | 0 .../components/svg/{Files => }/SheetPaths.tsx | 0 .../components/svg/{Files => }/TextPaths.tsx | 0 client/src/components/svg/index.ts | 71 +- .../src/components/ui/AnimatedSearchInput.tsx | 111 - client/src/components/ui/DelayedRender.tsx | 5 - client/src/components/ui/ModelParameters.tsx | 186 - client/src/components/ui/Prompt.tsx | 22 - client/src/components/ui/Slider.tsx | 26 - .../components/ui/TermsAndConditionsModal.tsx | 6 +- client/src/components/ui/index.ts | 49 +- client/src/data-provider/Files/mutations.ts | 2 +- client/src/hooks/Conversations/usePresets.ts | 5 +- client/src/hooks/Endpoint/Icons.tsx | 6 +- client/src/hooks/Endpoint/UnknownIcon.tsx | 2 +- .../src/hooks/Files/useDelayedUploadToast.ts | 4 +- client/src/hooks/Files/useFileHandling.ts | 2 +- client/src/hooks/Input/index.ts | 1 - .../src/hooks/Input/useSpeechToTextBrowser.ts | 2 +- .../hooks/Input/useSpeechToTextExternal.ts | 5 +- .../hooks/Input/useTextToSpeechExternal.ts | 6 +- client/src/hooks/Input/useTextarea.ts | 2 +- .../hooks/MCP/useMCPServerInitialization.ts | 4 +- client/src/hooks/MCP/useMCPServerManager.ts | 10 +- client/src/hooks/Nav/useSideNavLinks.ts | 2 +- client/src/hooks/Plugins/useAuthCodeTool.ts | 2 - client/src/hooks/Prompts/useCategories.tsx | 2 +- client/src/hooks/ScreenshotContext.tsx | 2 +- client/src/hooks/index.ts | 6 +- client/src/locales/Translation.spec.ts | 7 +- client/src/locales/en/translation.json | 3 +- client/src/routes/ChatRoute.tsx | 2 +- client/src/routes/Layouts/DashBreadcrumb.tsx | 2 +- client/src/routes/Root.tsx | 2 +- client/src/routes/RouteErrorBoundary.tsx | 3 +- client/src/routes/Search.tsx | 6 +- client/src/style.css | 3 + client/src/utils/files.ts | 5 +- client/src/utils/getThemeFromEnv.js | 58 + client/src/utils/index.ts | 2 +- client/tailwind.config.cjs | 12 +- client/test/setupTests.js | 15 + client/tsconfig.json | 2 +- config/update.js | 1 + package-lock.json | 3738 +++++++++++++++-- package.json | 5 +- packages/client/package.json | 92 + packages/client/rollup.config.js | 84 + .../client}/src/Providers/ToastContext.tsx | 4 +- packages/client/src/Providers/index.ts | 2 + packages/client/src/common/index.ts | 11 + packages/client/src/common/menus.ts | 24 + packages/client/src/common/types.ts | 33 + .../client/src/components}/Accordion.tsx | 3 +- .../client/src/components}/AlertDialog.tsx | 3 +- .../src/components/AnimatedSearchInput.tsx | 79 + .../client/src/components}/AnimatedTabs.css | 2 +- .../client/src/components}/AnimatedTabs.tsx | 10 +- .../client/src/components}/Badge.tsx | 9 +- .../client/src/components}/Breadcrumb.tsx | 7 +- .../client/src/components}/Button.tsx | 0 .../client/src/components}/Checkbox.tsx | 4 +- .../client/src/components}/CheckboxButton.tsx | 4 +- .../client/src/components}/Collapsible.tsx | 0 .../client/src/components}/Combobox.tsx | 14 +- .../src/components}/ControlCombobox.tsx | 0 .../client/src/components}/DataTable.tsx | 39 +- .../src/components}/DataTableColumnHeader.tsx | 15 +- .../client/src/components/DelayedRender.tsx | 12 + .../client/src/components}/Dialog.tsx | 4 +- .../src/components}/DialogTemplate.spec.tsx | 17 +- .../client/src/components}/DialogTemplate.tsx | 6 +- .../client/src/components}/Dropdown.tsx | 0 .../client/src/components}/DropdownMenu.tsx | 0 .../src/components}/DropdownNoState.tsx | 6 +- .../client/src/components}/DropdownPopup.tsx | 1 - .../client/src/components}/FileUpload.tsx | 0 .../client/src/components}/FormInput.tsx | 11 +- .../client/src/components}/HoverCard.tsx | 3 +- .../client/src/components}/Input.tsx | 1 - .../client/src/components}/InputCombobox.tsx | 0 .../client/src/components}/InputNumber.tsx | 0 .../client/src/components}/InputOTP.tsx | 5 + .../src/components}/InputWithDropDown.tsx | 6 +- .../client/src/components}/Label.tsx | 17 +- .../client/src/components}/MultiSearch.tsx | 8 +- .../client/src/components}/MultiSelect.tsx | 12 +- .../src/components}/OGDialogTemplate.tsx | 8 +- .../client/src/components}/OriginalDialog.tsx | 0 .../client/src/components}/Pagination.tsx | 0 .../client/src/components}/PixelCard.tsx | 14 +- .../client/src/components}/Progress.tsx | 0 .../client/src/components}/QuestionMark.tsx | 1 + .../client/src/components}/Resizable.tsx | 8 +- .../client/src/components}/Select.tsx | 3 + .../client/src/components}/SelectDropDown.tsx | 15 +- .../client/src/components}/Separator.tsx | 20 +- .../client/src/components}/Skeleton.tsx | 0 packages/client/src/components/Slider.tsx | 38 + .../client/src/components}/SplitText.spec.tsx | 0 .../client/src/components}/SplitText.tsx | 51 +- .../client/src/components}/Switch.tsx | 0 .../client/src/components}/Table.tsx | 0 .../client/src/components}/Tabs.tsx | 0 .../client/src/components}/Tag.tsx | 22 +- .../client/src/components}/Textarea.tsx | 3 +- .../src/components}/TextareaAutosize.tsx | 6 +- .../client/src/components}/ThemeSelector.tsx | 10 +- .../client/src/components}/Toast.tsx | 2 +- .../client/src/components}/Tooltip.tsx | 0 packages/client/src/components/index.ts | 51 + .../client/src/hooks/ThemeContext.old.tsx | 14 +- packages/client/src/hooks/index.ts | 10 + .../client/src/hooks}/useCombobox.ts | 0 .../client}/src/hooks/useDelayedRender.tsx | 0 packages/client/src/hooks/useLocalize.ts | 21 + .../client}/src/hooks/useMediaQuery.tsx | 0 .../client}/src/hooks/useOnClickOutside.ts | 0 .../client}/src/hooks/useToast.ts | 8 +- packages/client/src/index.ts | 24 + .../client/src/locales/Translation.spec.ts | 47 + .../client/src/locales/ar/translation.json | 3 + .../client/src/locales/ca/translation.json | 3 + .../client/src/locales/cs/translation.json | 3 + .../client/src/locales/da/translation.json | 3 + .../client/src/locales/de/translation.json | 3 + .../client/src/locales/en/translation.json | 3 + .../client/src/locales/es/translation.json | 3 + .../client/src/locales/et/translation.json | 3 + .../client/src/locales/fa/translation.json | 3 + .../client/src/locales/fi/translation.json | 3 + .../client/src/locales/fr/translation.json | 3 + .../client/src/locales/he/translation.json | 3 + .../client/src/locales/hu/translation.json | 3 + packages/client/src/locales/i18n.ts | 87 + .../client/src/locales/id/translation.json | 3 + .../client/src/locales/it/translation.json | 3 + .../client/src/locales/ja/translation.json | 3 + .../client/src/locales/ka/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 | 3 + .../client/src/locales/pt-PT/translation.json | 3 + .../client/src/locales/ru/translation.json | 3 + .../client/src/locales/sv/translation.json | 3 + .../client/src/locales/th/translation.json | 3 + .../client/src/locales/tr/translation.json | 3 + .../client/src/locales/vi/translation.json | 3 + .../src/locales/zh-Hans/translation.json | 3 + .../src/locales/zh-Hant/translation.json | 3 + packages/client/src/store.ts | 20 + .../client/src/svgs}/AnthropicIcon.tsx | 0 .../client/src/svgs}/AnthropicMinimalIcon.tsx | 0 .../client/src/svgs}/AppleIcon.tsx | 2 +- .../client/src/svgs}/ArchiveIcon.tsx | 0 .../client/src/svgs}/AssistantIcon.tsx | 0 .../client/src/svgs}/AttachmentIcon.tsx | 0 .../client/src/svgs}/AzureMinimalIcon.tsx | 1 - .../client/src/svgs}/BedrockIcon.tsx | 0 .../client/src/svgs}/BirthdayIcon.tsx | 0 .../client/src/svgs}/Blocks.tsx | 0 .../client/src/svgs}/CautionIcon.tsx | 0 .../client/src/svgs}/ChatGPTMinimalIcon.tsx | 0 .../client/src/svgs}/ChatIcon.tsx | 0 .../client/src/svgs}/CheckMark.tsx | 0 .../client/src/svgs}/CircleHelpIcon.tsx | 0 .../client/src/svgs}/Clipboard.tsx | 0 .../client/src/svgs}/CodeyIcon.tsx | 0 .../client/src/svgs}/ContinueIcon.tsx | 0 .../client/src/svgs}/ConvoIcon.tsx | 0 .../client/src/svgs}/CrossIcon.tsx | 0 .../client/src/svgs}/CustomMinimalIcon.tsx | 0 .../client/src/svgs}/DarkModeIcon.tsx | 0 .../client/src/svgs}/DataIcon.tsx | 0 .../client/src/svgs}/DiscordIcon.tsx | 0 .../client/src/svgs}/DislikeIcon.tsx | 0 .../client/src/svgs}/DotsIcon.tsx | 0 .../client/src/svgs}/EditIcon.tsx | 0 .../client/src/svgs}/ExperimentIcon.tsx | 0 .../client/src/svgs}/FacebookIcon.tsx | 0 .../client/src/svgs}/GPTIcon.tsx | 0 .../client/src/svgs}/GearIcon.tsx | 0 .../client/src/svgs}/GeminiIcon.tsx | 0 .../client/src/svgs}/GithubIcon.tsx | 0 .../client/src/svgs}/GoogleIcon.tsx | 0 .../client/src/svgs}/GoogleIconChat.tsx | 0 .../client/src/svgs}/GoogleMinimalIcon.tsx | 0 .../client/src/svgs}/LightModeIcon.tsx | 0 .../client/src/svgs}/LightningIcon.tsx | 0 .../client/src/svgs}/LikeIcon.tsx | 0 .../client/src/svgs}/LinkIcon.tsx | 0 .../client/src/svgs}/ListeningIcon.tsx | 6 +- .../client/src/svgs}/LockIcon.tsx | 0 .../client/src/svgs}/LogOutIcon.tsx | 0 .../client/src/svgs}/MCPIcon.tsx | 0 .../client/src/svgs}/MessagesSquared.tsx | 0 .../client/src/svgs}/MinimalPlugin.tsx | 0 .../client/src/svgs}/MobileSidebar.tsx | 0 .../client/src/svgs}/NewChatIcon.tsx | 0 .../client/src/svgs}/OpenAIMinimalIcon.tsx | 0 .../client/src/svgs}/OpenIDIcon.tsx | 0 .../client/src/svgs}/PaLMIcon.tsx | 0 .../client/src/svgs}/PaLMinimalIcon.tsx | 0 .../client/src/svgs}/PersonalizationIcon.tsx | 0 .../client/src/svgs}/PinIcon.tsx | 0 .../client/src/svgs}/Plugin.tsx | 0 .../client/src/svgs}/RegenerateIcon.tsx | 0 .../client/src/svgs}/RenameIcon.tsx | 0 .../client/src/svgs}/SamlIcon.tsx | 0 .../client/src/svgs}/SaveIcon.tsx | 7 +- .../client/src/svgs}/SendIcon.tsx | 0 .../client/src/svgs}/SendMessageIcon.tsx | 0 .../client/src/svgs}/Sidebar.tsx | 0 .../client/src/svgs}/Sparkles.tsx | 0 .../client/src/svgs}/SpeechIcon.tsx | 6 +- .../client/src/svgs}/Spinner.tsx | 0 .../client/src/svgs}/SquirclePlusIcon.tsx | 0 .../client/src/svgs}/StopGeneratingIcon.tsx | 0 .../client/src/svgs}/SunIcon.tsx | 0 .../client/src/svgs}/SwitchIcon.tsx | 7 +- .../client/src/svgs}/ThumbDownIcon.tsx | 2 - .../client/src/svgs}/ThumbUpIcon.tsx | 2 - .../client/src/svgs}/TrashIcon.tsx | 0 .../client/src/svgs}/UserIcon.tsx | 0 .../client/src/svgs}/VectorIcon.tsx | 0 .../client/src/svgs}/VolumeIcon.tsx | 0 .../client/src/svgs}/VolumeMuteIcon.tsx | 0 .../client/src/svgs}/XAIcon.tsx | 0 packages/client/src/svgs/index.ts | 67 + packages/client/src/theme/README.md | 473 +++ packages/client/src/theme/atoms/themeAtoms.ts | 36 + .../src/theme/context/ThemeProvider.tsx | 165 + packages/client/src/theme/index.ts | 14 + packages/client/src/theme/themes/dark.ts | 72 + packages/client/src/theme/themes/default.ts | 72 + packages/client/src/theme/themes/index.ts | 2 + packages/client/src/theme/types/index.ts | 189 + packages/client/src/theme/utils/applyTheme.ts | 115 + .../src/theme/utils/createTailwindColors.js | 86 + packages/client/src/utils/index.ts | 2 + .../client}/src/utils/theme.ts | 2 +- packages/client/src/utils/utils.ts | 7 + packages/client/tailwind.config.js | 13 + packages/client/tsconfig.json | 34 + 569 files changed, 7010 insertions(+), 1848 deletions(-) create mode 100644 client/src/components/Chat/Input/MCPConfigDialog.tsx rename client/src/components/{ui => Input/ModelSelect}/MultiSelectDropDown.tsx (97%) rename client/src/components/{ui => Input/ModelSelect}/MultiSelectPop.tsx (95%) rename client/src/components/{ui => Input/ModelSelect}/SelectDropDownPop.tsx (98%) rename client/src/components/{ui => }/MCP/CustomUserVarsSection.tsx (98%) rename client/src/components/{ui => }/MCP/MCPConfigDialog.tsx (97%) rename client/src/components/{ui => }/MCP/MCPServerStatusIcon.tsx (100%) rename client/src/components/{ui => }/MCP/ServerInitializationSection.tsx (96%) rename client/src/components/svg/{Files => }/CodePaths.tsx (100%) rename client/src/components/svg/{Files => }/FileIcon.tsx (100%) rename client/src/components/svg/{Files => }/FilePaths.tsx (100%) rename client/src/components/svg/{Files => }/SheetPaths.tsx (100%) rename client/src/components/svg/{Files => }/TextPaths.tsx (100%) delete mode 100644 client/src/components/ui/AnimatedSearchInput.tsx delete mode 100644 client/src/components/ui/DelayedRender.tsx delete mode 100644 client/src/components/ui/ModelParameters.tsx delete mode 100644 client/src/components/ui/Prompt.tsx delete mode 100644 client/src/components/ui/Slider.tsx create mode 100644 client/src/utils/getThemeFromEnv.js create mode 100644 packages/client/package.json create mode 100644 packages/client/rollup.config.js rename {client => packages/client}/src/Providers/ToastContext.tsx (77%) create mode 100644 packages/client/src/Providers/index.ts create mode 100644 packages/client/src/common/index.ts create mode 100644 packages/client/src/common/menus.ts create mode 100644 packages/client/src/common/types.ts rename {client/src/components/ui => packages/client/src/components}/Accordion.tsx (94%) rename {client/src/components/ui => packages/client/src/components}/AlertDialog.tsx (99%) create mode 100644 packages/client/src/components/AnimatedSearchInput.tsx rename {client/src/components/ui => packages/client/src/components}/AnimatedTabs.css (96%) rename {client/src/components/ui => packages/client/src/components}/AnimatedTabs.tsx (93%) rename {client/src/components/ui => packages/client/src/components}/Badge.tsx (92%) rename {client/src/components/ui => packages/client/src/components}/Breadcrumb.tsx (91%) rename {client/src/components/ui => packages/client/src/components}/Button.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/Checkbox.tsx (96%) rename {client/src/components/ui => packages/client/src/components}/CheckboxButton.tsx (98%) rename {client/src/components/ui => packages/client/src/components}/Collapsible.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/Combobox.tsx (92%) rename {client/src/components/ui => packages/client/src/components}/ControlCombobox.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/DataTable.tsx (94%) rename {client/src/components/ui => packages/client/src/components}/DataTableColumnHeader.tsx (83%) create mode 100644 packages/client/src/components/DelayedRender.tsx rename {client/src/components/ui => packages/client/src/components}/Dialog.tsx (99%) rename {client/src/components/ui => packages/client/src/components}/DialogTemplate.spec.tsx (92%) rename {client/src/components/ui => packages/client/src/components}/DialogTemplate.tsx (95%) rename {client/src/components/ui => packages/client/src/components}/Dropdown.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/DropdownMenu.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/DropdownNoState.tsx (95%) rename {client/src/components/ui => packages/client/src/components}/DropdownPopup.tsx (98%) rename {client/src/components/ui => packages/client/src/components}/FileUpload.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/FormInput.tsx (79%) rename {client/src/components/ui => packages/client/src/components}/HoverCard.tsx (97%) rename {client/src/components/ui => packages/client/src/components}/Input.tsx (99%) rename {client/src/components/ui => packages/client/src/components}/InputCombobox.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/InputNumber.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/InputOTP.tsx (95%) rename {client/src/components/ui => packages/client/src/components}/InputWithDropDown.tsx (95%) rename {client/src/components/ui => packages/client/src/components}/Label.tsx (51%) rename {client/src/components/ui => packages/client/src/components}/MultiSearch.tsx (94%) rename {client/src/components/ui => packages/client/src/components}/MultiSelect.tsx (92%) rename {client/src/components/ui => packages/client/src/components}/OGDialogTemplate.tsx (93%) rename {client/src/components/ui => packages/client/src/components}/OriginalDialog.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/Pagination.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/PixelCard.tsx (96%) rename {client/src/components/ui => packages/client/src/components}/Progress.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/QuestionMark.tsx (99%) rename {client/src/components/ui => packages/client/src/components}/Resizable.tsx (75%) rename {client/src/components/ui => packages/client/src/components}/Select.tsx (97%) rename {client/src/components/ui => packages/client/src/components}/SelectDropDown.tsx (96%) rename {client/src/components/ui => packages/client/src/components}/Separator.tsx (67%) rename {client/src/components/ui => packages/client/src/components}/Skeleton.tsx (100%) create mode 100644 packages/client/src/components/Slider.tsx rename {client/src/components/ui => packages/client/src/components}/SplitText.spec.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/SplitText.tsx (77%) rename {client/src/components/ui => packages/client/src/components}/Switch.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/Table.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/Tabs.tsx (100%) rename {client/src/components/ui => packages/client/src/components}/Tag.tsx (76%) rename {client/src/components/ui => packages/client/src/components}/Textarea.tsx (96%) rename {client/src/components/ui => packages/client/src/components}/TextareaAutosize.tsx (75%) rename {client/src/components/ui => packages/client/src/components}/ThemeSelector.tsx (91%) rename {client/src/components/ui => packages/client/src/components}/Toast.tsx (98%) rename {client/src/components/ui => packages/client/src/components}/Tooltip.tsx (100%) create mode 100644 packages/client/src/components/index.ts rename client/src/hooks/ThemeContext.tsx => packages/client/src/hooks/ThemeContext.old.tsx (89%) create mode 100644 packages/client/src/hooks/index.ts rename {client/src/hooks/Input => packages/client/src/hooks}/useCombobox.ts (100%) rename {client => packages/client}/src/hooks/useDelayedRender.tsx (100%) create mode 100644 packages/client/src/hooks/useLocalize.ts rename {client => packages/client}/src/hooks/useMediaQuery.tsx (100%) rename {client => packages/client}/src/hooks/useOnClickOutside.ts (100%) rename {client => packages/client}/src/hooks/useToast.ts (87%) create mode 100644 packages/client/src/index.ts create mode 100644 packages/client/src/locales/Translation.spec.ts create mode 100644 packages/client/src/locales/ar/translation.json create mode 100644 packages/client/src/locales/ca/translation.json create mode 100644 packages/client/src/locales/cs/translation.json create mode 100644 packages/client/src/locales/da/translation.json create mode 100644 packages/client/src/locales/de/translation.json create mode 100644 packages/client/src/locales/en/translation.json create mode 100644 packages/client/src/locales/es/translation.json create mode 100644 packages/client/src/locales/et/translation.json create mode 100644 packages/client/src/locales/fa/translation.json create mode 100644 packages/client/src/locales/fi/translation.json create mode 100644 packages/client/src/locales/fr/translation.json create mode 100644 packages/client/src/locales/he/translation.json create mode 100644 packages/client/src/locales/hu/translation.json create mode 100644 packages/client/src/locales/i18n.ts create mode 100644 packages/client/src/locales/id/translation.json create mode 100644 packages/client/src/locales/it/translation.json create mode 100644 packages/client/src/locales/ja/translation.json create mode 100644 packages/client/src/locales/ka/translation.json create mode 100644 packages/client/src/locales/ko/translation.json create mode 100644 packages/client/src/locales/nl/translation.json create mode 100644 packages/client/src/locales/pl/translation.json create mode 100644 packages/client/src/locales/pt-BR/translation.json create mode 100644 packages/client/src/locales/pt-PT/translation.json create mode 100644 packages/client/src/locales/ru/translation.json create mode 100644 packages/client/src/locales/sv/translation.json create mode 100644 packages/client/src/locales/th/translation.json create mode 100644 packages/client/src/locales/tr/translation.json create mode 100644 packages/client/src/locales/vi/translation.json create mode 100644 packages/client/src/locales/zh-Hans/translation.json create mode 100644 packages/client/src/locales/zh-Hant/translation.json create mode 100644 packages/client/src/store.ts rename {client/src/components/svg => packages/client/src/svgs}/AnthropicIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/AnthropicMinimalIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/AppleIcon.tsx (99%) rename {client/src/components/svg => packages/client/src/svgs}/ArchiveIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/AssistantIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/AttachmentIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/AzureMinimalIcon.tsx (99%) rename {client/src/components/svg => packages/client/src/svgs}/BedrockIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/BirthdayIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/Blocks.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/CautionIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/ChatGPTMinimalIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/ChatIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/CheckMark.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/CircleHelpIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/Clipboard.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/CodeyIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/ContinueIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/ConvoIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/CrossIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/CustomMinimalIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/DarkModeIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/DataIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/DiscordIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/DislikeIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/DotsIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/EditIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/ExperimentIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/FacebookIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/GPTIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/GearIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/GeminiIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/GithubIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/GoogleIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/GoogleIconChat.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/GoogleMinimalIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/LightModeIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/LightningIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/LikeIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/LinkIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/ListeningIcon.tsx (78%) rename {client/src/components/svg => packages/client/src/svgs}/LockIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/LogOutIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/MCPIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/MessagesSquared.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/MinimalPlugin.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/MobileSidebar.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/NewChatIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/OpenAIMinimalIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/OpenIDIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/PaLMIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/PaLMinimalIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/PersonalizationIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/PinIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/Plugin.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/RegenerateIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/RenameIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/SamlIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/SaveIcon.tsx (82%) rename {client/src/components/svg => packages/client/src/svgs}/SendIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/SendMessageIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/Sidebar.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/Sparkles.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/SpeechIcon.tsx (80%) rename {client/src/components/svg => packages/client/src/svgs}/Spinner.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/SquirclePlusIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/StopGeneratingIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/SunIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/SwitchIcon.tsx (79%) rename {client/src/components/svg => packages/client/src/svgs}/ThumbDownIcon.tsx (98%) rename {client/src/components/svg => packages/client/src/svgs}/ThumbUpIcon.tsx (98%) rename {client/src/components/svg => packages/client/src/svgs}/TrashIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/UserIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/VectorIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/VolumeIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/VolumeMuteIcon.tsx (100%) rename {client/src/components/svg => packages/client/src/svgs}/XAIcon.tsx (100%) create mode 100644 packages/client/src/svgs/index.ts create mode 100644 packages/client/src/theme/README.md create mode 100644 packages/client/src/theme/atoms/themeAtoms.ts create mode 100644 packages/client/src/theme/context/ThemeProvider.tsx create mode 100644 packages/client/src/theme/index.ts create mode 100644 packages/client/src/theme/themes/dark.ts create mode 100644 packages/client/src/theme/themes/default.ts create mode 100644 packages/client/src/theme/themes/index.ts create mode 100644 packages/client/src/theme/types/index.ts create mode 100644 packages/client/src/theme/utils/applyTheme.ts create mode 100644 packages/client/src/theme/utils/createTailwindColors.js create mode 100644 packages/client/src/utils/index.ts rename {client => packages/client}/src/utils/theme.ts (95%) create mode 100644 packages/client/src/utils/utils.ts create mode 100644 packages/client/tailwind.config.js create mode 100644 packages/client/tsconfig.json diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 1f0e2fd86..6b97a1e61 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -1,6 +1,11 @@ name: Publish `@librechat/client` to NPM on: + push: + branches: + - main + paths: + - 'packages/client/package.json' workflow_dispatch: inputs: reason: @@ -17,16 +22,37 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '18.x' - - - name: Check if client package exists + node-version: '20.x' + + - name: Install client dependencies + run: cd packages/client && npm ci + + - name: Build client + run: cd packages/client && npm run build + + - name: Set up npm authentication + run: echo "//registry.npmjs.org/:_authToken=${{ secrets.PUBLISH_NPM_TOKEN }}" > ~/.npmrc + + - name: Check version change + id: check + working-directory: packages/client run: | - if [ -d "packages/client" ]; then - echo "Client package directory found" + PACKAGE_VERSION=$(node -p "require('./package.json').version") + PUBLISHED_VERSION=$(npm view @librechat/client version 2>/dev/null || echo "0.0.0") + if [ "$PACKAGE_VERSION" = "$PUBLISHED_VERSION" ]; then + echo "No version change, skipping publish" + echo "skip=true" >> $GITHUB_OUTPUT else - echo "Client package directory not found - workflow ready for future use" - exit 0 + echo "Version changed, proceeding with publish" + echo "skip=false" >> $GITHUB_OUTPUT fi - - - name: Placeholder for future publishing - run: echo "Client package publishing workflow is ready" \ No newline at end of file + + - name: Pack package + if: steps.check.outputs.skip != 'true' + working-directory: packages/client + run: npm pack + + - name: Publish + if: steps.check.outputs.skip != 'true' + working-directory: packages/client + run: npm publish *.tgz --access public \ No newline at end of file diff --git a/.github/workflows/i18n-unused-keys.yml b/.github/workflows/i18n-unused-keys.yml index 07cc77a1a..9fb2fd683 100644 --- a/.github/workflows/i18n-unused-keys.yml +++ b/.github/workflows/i18n-unused-keys.yml @@ -6,6 +6,7 @@ on: - "client/src/**" - "api/**" - "packages/data-provider/src/**" + - "packages/client/**" jobs: detect-unused-i18n-keys: @@ -23,7 +24,7 @@ jobs: # Define paths I18N_FILE="client/src/locales/en/translation.json" - SOURCE_DIRS=("client/src" "api" "packages/data-provider/src") + SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client") # Check if translation file exists if [[ ! -f "$I18N_FILE" ]]; then diff --git a/.github/workflows/unused-packages.yml b/.github/workflows/unused-packages.yml index 5429a1abd..442925b69 100644 --- a/.github/workflows/unused-packages.yml +++ b/.github/workflows/unused-packages.yml @@ -7,6 +7,7 @@ on: - 'package-lock.json' - 'client/**' - 'api/**' + - 'packages/client/**' jobs: detect-unused-packages: @@ -28,7 +29,7 @@ jobs: - name: Validate JSON files run: | - for FILE in package.json client/package.json api/package.json; do + for FILE in package.json client/package.json api/package.json packages/client/package.json; do if [[ -f "$FILE" ]]; then jq empty "$FILE" || (echo "::error title=Invalid JSON::$FILE is invalid" && exit 1) fi @@ -63,12 +64,31 @@ jobs: local folder=$1 local output_file=$2 if [[ -d "$folder" ]]; then - grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" --include=\*.{js,ts,mjs,cjs} | \ + # Extract require() statements + grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" --include=\*.{js,ts,tsx,jsx,mjs,cjs} | \ sed -E "s/require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)/\1/" > "$output_file" - grep -rEho "import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{js,ts,mjs,cjs} | \ + # Extract ES6 imports - various patterns + # import x from 'module' + grep -rEho "import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{js,ts,tsx,jsx,mjs,cjs} | \ sed -E "s/import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" + + # import 'module' (side-effect imports) + grep -rEho "import ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{js,ts,tsx,jsx,mjs,cjs} | \ + sed -E "s/import ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" + + # export { x } from 'module' or export * from 'module' + grep -rEho "export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{js,ts,tsx,jsx,mjs,cjs} | \ + sed -E "s/export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" + + # import type { x } from 'module' (TypeScript) + grep -rEho "import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{ts,tsx} | \ + sed -E "s/import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" + # Remove subpath imports but keep the base package + # e.g., '@tanstack/react-query/devtools' becomes '@tanstack/react-query' + sed -i -E 's|^(@?[a-zA-Z0-9-]+(/[a-zA-Z0-9-]+)?)/.*|\1|' "$output_file" + sort -u "$output_file" -o "$output_file" else touch "$output_file" @@ -78,6 +98,33 @@ jobs: extract_deps_from_code "." root_used_code.txt extract_deps_from_code "client" client_used_code.txt extract_deps_from_code "api" api_used_code.txt + + # Extract dependencies used by @librechat/client package + extract_deps_from_code "packages/client" packages_client_used_code.txt + + - name: Get @librechat/client dependencies + id: get-librechat-client-deps + run: | + if [[ -f "packages/client/package.json" ]]; then + # Get all dependencies from @librechat/client (dependencies, devDependencies, and peerDependencies) + DEPS=$(jq -r '.dependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "") + DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "") + PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "") + + # Combine all dependencies + echo "$DEPS" > librechat_client_deps.txt + echo "$DEV_DEPS" >> librechat_client_deps.txt + echo "$PEER_DEPS" >> librechat_client_deps.txt + + # Also include dependencies that are imported in packages/client + cat packages_client_used_code.txt >> librechat_client_deps.txt + + # Remove empty lines and sort + grep -v '^$' librechat_client_deps.txt | sort -u > temp_deps.txt + mv temp_deps.txt librechat_client_deps.txt + else + touch librechat_client_deps.txt + fi - name: Extract Workspace Dependencies id: extract-workspace-deps diff --git a/bun.lockb b/bun.lockb index 61118178fd40c4cda7f4ba0f1055623a2ae2722e..77b01df558deaabf076fb3025af57bd314ef37ef 100755 GIT binary patch delta 272436 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 z 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

WaGVoOq+~GW>&`F^}5dhLyen_W5qyU-APV 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%0AZ?-=^ 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 Ox&8iaXyiZH&;JiGgf311 diff --git a/client/package.json b/client/package.json index d0c7c54e1..f1d68d27e 100644 --- a/client/package.json +++ b/client/package.json @@ -34,6 +34,7 @@ "@dicebear/collection": "^9.2.2", "@dicebear/core": "^9.2.2", "@headlessui/react": "^2.1.2", + "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.2", @@ -56,8 +57,8 @@ "@react-spring/web": "^9.7.5", "@tanstack/react-query": "^4.28.0", "@tanstack/react-table": "^8.11.7", - "class-variance-authority": "^0.6.0", - "clsx": "^1.2.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", "cross-env": "^7.0.3", "date-fns": "^3.3.1", @@ -70,11 +71,12 @@ "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.3", "input-otp": "^1.4.2", + "jotai": "^2.12.5", "js-cookie": "^3.0.5", "librechat-data-provider": "*", "lodash": "^4.17.21", "lucide-react": "^0.394.0", - "match-sorter": "^6.3.4", + "match-sorter": "^8.1.0", "micromark-extension-llm-math": "^3.1.0", "qrcode.react": "^4.2.0", "rc-input-number": "^7.4.2", diff --git a/client/src/App.jsx b/client/src/App.jsx index 38e568e42..d329bdd9b 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -4,10 +4,10 @@ import { RouterProvider } from 'react-router-dom'; import * as RadixToast from '@radix-ui/react-toast'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { Toast, ThemeProvider, ToastProvider } from '@librechat/client'; import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query'; -import { ScreenshotProvider, ThemeProvider, useApiErrorBoundary } from './hooks'; -import { ToastProvider } from './Providers'; -import Toast from './components/ui/Toast'; +import { ScreenshotProvider, useApiErrorBoundary } from './hooks'; +import { getThemeFromEnv } from './utils/getThemeFromEnv'; import { LiveAnnouncer } from '~/a11y'; import { router } from './routes'; @@ -24,11 +24,23 @@ const App = () => { }), }); + // Load theme from environment variables if available + const envTheme = getThemeFromEnv(); + return ( - + + {/* The ThemeProvider will automatically: + 1. Apply dark/light mode classes + 2. Apply custom theme colors if envTheme is provided + 3. Otherwise use stored theme preferences from localStorage + 4. Fall back to default theme colors if nothing is stored */} diff --git a/client/src/Providers/index.ts b/client/src/Providers/index.ts index caf5b82b7..7300813ae 100644 --- a/client/src/Providers/index.ts +++ b/client/src/Providers/index.ts @@ -1,11 +1,9 @@ export { default as AssistantsProvider } from './AssistantsContext'; export { default as AgentsProvider } from './AgentsContext'; -export { default as ToastProvider } from './ToastContext'; export * from './ActivePanelContext'; export * from './AgentPanelContext'; export * from './ChatContext'; export * from './ShareContext'; -export * from './ToastContext'; export * from './FileMapContext'; export * from './AddedChatContext'; export * from './EditorContext'; diff --git a/client/src/a11y/LiveAnnouncer.tsx b/client/src/a11y/LiveAnnouncer.tsx index 29912b49a..9a0271155 100644 --- a/client/src/a11y/LiveAnnouncer.tsx +++ b/client/src/a11y/LiveAnnouncer.tsx @@ -1,8 +1,7 @@ -// client/src/a11y/LiveAnnouncer.tsx import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import type { AnnounceOptions } from '~/common'; import AnnouncerContext from '~/Providers/AnnouncerContext'; -import useLocalize from '~/hooks/useLocalize'; +import { useLocalize } from '~/hooks'; import Announcer from './Announcer'; interface LiveAnnouncerProps { diff --git a/client/src/common/menus.ts b/client/src/common/menus.ts index c46ad3f8b..4d70f282c 100644 --- a/client/src/common/menus.ts +++ b/client/src/common/menus.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ export type RenderProp< P = React.HTMLAttributes & { ref?: React.Ref; diff --git a/client/src/common/types.ts b/client/src/common/types.ts index a222cb29b..214dc349b 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -206,7 +206,9 @@ export type AgentPanelProps = { setActivePanel: React.Dispatch>; setMcp: React.Dispatch>; setAction: React.Dispatch>; + endpointsConfig?: t.TEndpointsConfig; setCurrentAgentId: React.Dispatch>; + agentsConfig?: t.TAgentsEndpoint | null; }; export type AgentPanelContextType = { @@ -217,14 +219,12 @@ export type AgentPanelContextType = { mcps?: t.MCP[]; setMcp: React.Dispatch>; setMcps: React.Dispatch>; + groupedTools: Record; tools: t.AgentToolType[]; activePanel?: string; setActivePanel: React.Dispatch>; setCurrentAgentId: React.Dispatch>; - groupedTools?: Record; agent_id?: string; - agentsConfig?: t.TAgentsEndpoint | null; - endpointsConfig?: t.TEndpointsConfig | null; }; export type AgentModelPanelProps = { @@ -336,16 +336,13 @@ export type TAskProps = { export type TOptions = { editedMessageId?: string | null; editedText?: string | null; - editedContent?: { - index: number; - text: string; - type: 'text' | 'think'; - }; isRegenerate?: boolean; isContinued?: boolean; isEdited?: boolean; overrideMessages?: t.TMessage[]; - /** Currently only utilized when resubmitting user-created message, uses that message's currently attached files */ + /** This value is only true when the user submits a message with "Save & Submit" for a user-created message */ + isResubmission?: boolean; + /** Currently only utilized when `isResubmission === true`, uses that message's currently attached files */ overrideFiles?: t.TMessage['files']; }; diff --git a/client/src/components/Artifacts/Artifacts.tsx b/client/src/components/Artifacts/Artifacts.tsx index 4268ef5fe..41c620102 100644 --- a/client/src/components/Artifacts/Artifacts.tsx +++ b/client/src/components/Artifacts/Artifacts.tsx @@ -6,9 +6,9 @@ import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-re import useArtifacts from '~/hooks/Artifacts/useArtifacts'; import DownloadArtifact from './DownloadArtifact'; import { useEditorContext } from '~/Providers'; -import useLocalize from '~/hooks/useLocalize'; import ArtifactTabs from './ArtifactTabs'; import { CopyCodeButton } from './Code'; +import { useLocalize } from '~/hooks'; import store from '~/store'; export default function Artifacts() { diff --git a/client/src/components/Artifacts/Code.tsx b/client/src/components/Artifacts/Code.tsx index 21db2055d..4b6d1a006 100644 --- a/client/src/components/Artifacts/Code.tsx +++ b/client/src/components/Artifacts/Code.tsx @@ -1,12 +1,11 @@ import React, { memo, useEffect, useRef, useState } from 'react'; +import copy from 'copy-to-clipboard'; import rehypeKatex from 'rehype-katex'; import ReactMarkdown from 'react-markdown'; import rehypeHighlight from 'rehype-highlight'; -import copy from 'copy-to-clipboard'; +import { Clipboard, CheckMark } from '@librechat/client'; import { handleDoubleClick, langSubset } from '~/utils'; -import Clipboard from '~/components/svg/Clipboard'; -import CheckMark from '~/components/svg/CheckMark'; -import useLocalize from '~/hooks/useLocalize'; +import { useLocalize } from '~/hooks'; type TCodeProps = { inline: boolean; diff --git a/client/src/components/Artifacts/DownloadArtifact.tsx b/client/src/components/Artifacts/DownloadArtifact.tsx index a4b6f5031..afced5a37 100644 --- a/client/src/components/Artifacts/DownloadArtifact.tsx +++ b/client/src/components/Artifacts/DownloadArtifact.tsx @@ -1,9 +1,9 @@ import React, { useState } from 'react'; import { Download } from 'lucide-react'; import type { Artifact } from '~/common'; +import { CheckMark } from '@librechat/client'; import useArtifactProps from '~/hooks/Artifacts/useArtifactProps'; import { useEditorContext } from '~/Providers'; -import { CheckMark } from '~/components/svg'; import { useLocalize } from '~/hooks'; const DownloadArtifact = ({ diff --git a/client/src/components/Artifacts/Mermaid.tsx b/client/src/components/Artifacts/Mermaid.tsx index 551a5a5a7..f7291998a 100644 --- a/client/src/components/Artifacts/Mermaid.tsx +++ b/client/src/components/Artifacts/Mermaid.tsx @@ -1,8 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import mermaid from 'mermaid'; +import { Button } from '@librechat/client'; import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'; -// import { Button } from '/components/ui/Button'; // Live component -import { Button } from '~/components/ui/Button'; import { ZoomIn, ZoomOut, RefreshCw } from 'lucide-react'; interface MermaidDiagramProps { diff --git a/client/src/components/Audio/TTS.tsx b/client/src/components/Audio/TTS.tsx index 9343b483d..0d9351703 100644 --- a/client/src/components/Audio/TTS.tsx +++ b/client/src/components/Audio/TTS.tsx @@ -2,8 +2,8 @@ import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; import type { TMessageAudio } from '~/common'; +import { VolumeIcon, VolumeMuteIcon, Spinner } from '@librechat/client'; import { useLocalize, useTTSBrowser, useTTSExternal } from '~/hooks'; -import { VolumeIcon, VolumeMuteIcon, Spinner } from '~/components'; import { logger } from '~/utils'; import store from '~/store'; diff --git a/client/src/components/Audio/Voices.tsx b/client/src/components/Audio/Voices.tsx index 985e5e32d..6064a1662 100644 --- a/client/src/components/Audio/Voices.tsx +++ b/client/src/components/Audio/Voices.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { useRecoilState } from 'recoil'; +import { Dropdown } from '@librechat/client'; import type { Option } from '~/common'; import { useLocalize, useTTSBrowser, useTTSExternal } from '~/hooks'; -import { Dropdown } from '~/components/ui'; import { logger } from '~/utils'; import store from '~/store'; diff --git a/client/src/components/Auth/AuthLayout.tsx b/client/src/components/Auth/AuthLayout.tsx index 02b380291..c49ba2a7d 100644 --- a/client/src/components/Auth/AuthLayout.tsx +++ b/client/src/components/Auth/AuthLayout.tsx @@ -1,9 +1,9 @@ -import { TranslationKeys, useLocalize } from '~/hooks'; +import { ThemeSelector } from '@librechat/client'; import { TStartupConfig } from 'librechat-data-provider'; import { ErrorMessage } from '~/components/Auth/ErrorMessage'; +import { TranslationKeys, useLocalize } from '~/hooks'; import SocialLoginRender from './SocialLoginRender'; import { BlinkAnimation } from './BlinkAnimation'; -import { ThemeSelector } from '~/components'; import { Banner } from '../Banners'; import Footer from './Footer'; diff --git a/client/src/components/Auth/Login.tsx b/client/src/components/Auth/Login.tsx index ee0b87246..39406e300 100644 --- a/client/src/components/Auth/Login.tsx +++ b/client/src/components/Auth/Login.tsx @@ -1,10 +1,10 @@ import { useOutletContext, useSearchParams } from 'react-router-dom'; import { useEffect, useState } from 'react'; -import { useAuthContext } from '~/hooks/AuthContext'; +import { OpenIDIcon } from '@librechat/client'; import type { TLoginLayoutContext } from '~/common'; import { ErrorMessage } from '~/components/Auth/ErrorMessage'; import SocialButton from '~/components/Auth/SocialButton'; -import { OpenIDIcon } from '~/components'; +import { useAuthContext } from '~/hooks/AuthContext'; import { getLoginError } from '~/utils'; import { useLocalize } from '~/hooks'; import LoginForm from './LoginForm'; diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index c13535db2..c7a469fc2 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -1,11 +1,11 @@ -import { useForm } from 'react-hook-form'; import React, { useState, useEffect, useContext } from 'react'; +import { useForm } from 'react-hook-form'; import { Turnstile } from '@marsidev/react-turnstile'; +import { ThemeContext, Spinner, Button } from '@librechat/client'; import type { TLoginUser, TStartupConfig } from 'librechat-data-provider'; import type { TAuthContext } from '~/common'; import { useResendVerificationEmail, useGetStartupConfig } from '~/data-provider'; -import { ThemeContext, useLocalize } from '~/hooks'; -import { Spinner, Button } from '~/components'; +import { useLocalize } from '~/hooks'; type TLoginFormProps = { onSubmit: (data: TLoginUser) => void; diff --git a/client/src/components/Auth/Registration.tsx b/client/src/components/Auth/Registration.tsx index b193cff11..2e915e013 100644 --- a/client/src/components/Auth/Registration.tsx +++ b/client/src/components/Auth/Registration.tsx @@ -1,12 +1,12 @@ import { useForm } from 'react-hook-form'; import React, { useContext, useState } from 'react'; import { Turnstile } from '@marsidev/react-turnstile'; +import { ThemeContext, Spinner, Button } from '@librechat/client'; import { useNavigate, useOutletContext, useLocation } from 'react-router-dom'; import { useRegisterUserMutation } from 'librechat-data-provider/react-query'; import type { TRegisterUser, TError } from 'librechat-data-provider'; -import { useLocalize, TranslationKeys, ThemeContext } from '~/hooks'; import type { TLoginLayoutContext } from '~/common'; -import { Spinner, Button } from '~/components'; +import { useLocalize, TranslationKeys } from '~/hooks'; import { ErrorMessage } from './ErrorMessage'; const Registration: React.FC = () => { diff --git a/client/src/components/Auth/RequestPasswordReset.tsx b/client/src/components/Auth/RequestPasswordReset.tsx index c0ef5751c..9f50a7e42 100644 --- a/client/src/components/Auth/RequestPasswordReset.tsx +++ b/client/src/components/Auth/RequestPasswordReset.tsx @@ -1,11 +1,11 @@ import { useForm } from 'react-hook-form'; import { useState, ReactNode } from 'react'; +import { Spinner, Button } from '@librechat/client'; import { useOutletContext } from 'react-router-dom'; import { useRequestPasswordResetMutation } from 'librechat-data-provider/react-query'; import type { TRequestPasswordReset, TRequestPasswordResetResponse } from 'librechat-data-provider'; -import type { FC } from 'react'; import type { TLoginLayoutContext } from '~/common'; -import { Spinner, Button } from '~/components'; +import type { FC } from 'react'; import { useLocalize } from '~/hooks'; const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => { diff --git a/client/src/components/Auth/ResetPassword.tsx b/client/src/components/Auth/ResetPassword.tsx index 231c501cb..2882e1dc5 100644 --- a/client/src/components/Auth/ResetPassword.tsx +++ b/client/src/components/Auth/ResetPassword.tsx @@ -1,10 +1,10 @@ import { useForm } from 'react-hook-form'; +import { Spinner, Button } from '@librechat/client'; import { useOutletContext } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useResetPasswordMutation } from 'librechat-data-provider/react-query'; import type { TResetPassword } from 'librechat-data-provider'; import type { TLoginLayoutContext } from '~/common'; -import { Spinner, Button } from '~/components'; import { useLocalize } from '~/hooks'; function ResetPassword() { diff --git a/client/src/components/Auth/SocialLoginRender.tsx b/client/src/components/Auth/SocialLoginRender.tsx index 55a8ade6b..ad76354a5 100644 --- a/client/src/components/Auth/SocialLoginRender.tsx +++ b/client/src/components/Auth/SocialLoginRender.tsx @@ -6,7 +6,7 @@ import { DiscordIcon, AppleIcon, SamlIcon, -} from '~/components'; +} from '@librechat/client'; import SocialButton from './SocialButton'; diff --git a/client/src/components/Auth/TwoFactorScreen.tsx b/client/src/components/Auth/TwoFactorScreen.tsx index 04f89d7ce..11eab660c 100644 --- a/client/src/components/Auth/TwoFactorScreen.tsx +++ b/client/src/components/Auth/TwoFactorScreen.tsx @@ -1,10 +1,10 @@ import React, { useState, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { useToastContext } from '@librechat/client'; 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 { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Label } from '@librechat/client'; import { useVerifyTwoFactorTempMutation } from '~/data-provider'; -import { useToastContext } from '~/Providers'; import { useLocalize } from '~/hooks'; interface VerifyPayload { diff --git a/client/src/components/Auth/VerifyEmail.tsx b/client/src/components/Auth/VerifyEmail.tsx index 9c143224b..fb5c78719 100644 --- a/client/src/components/Auth/VerifyEmail.tsx +++ b/client/src/components/Auth/VerifyEmail.tsx @@ -1,8 +1,7 @@ -import { useSearchParams, useNavigate } from 'react-router-dom'; import { useState, useEffect, useMemo, useCallback } from 'react'; +import { Spinner, ThemeSelector } from '@librechat/client'; +import { useSearchParams, useNavigate } from 'react-router-dom'; import { useVerifyEmailMutation, useResendVerificationEmail } from '~/data-provider'; -import { ThemeSelector } from '~/components/ui'; -import { Spinner } from '~/components/svg'; import { useLocalize } from '~/hooks'; function RequestPasswordReset() { diff --git a/client/src/components/Bookmarks/BookmarkEditDialog.tsx b/client/src/components/Bookmarks/BookmarkEditDialog.tsx index ae81c3b81..7c0b494d7 100644 --- a/client/src/components/Bookmarks/BookmarkEditDialog.tsx +++ b/client/src/components/Bookmarks/BookmarkEditDialog.tsx @@ -1,10 +1,8 @@ import React, { useRef, Dispatch, SetStateAction } from 'react'; import { TConversationTag } from 'librechat-data-provider'; -import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; +import { OGDialogTemplate, OGDialog, Button, Spinner, useToastContext } from '@librechat/client'; import { useConversationTagMutation } from '~/data-provider'; -import { OGDialog, Button, Spinner } from '~/components'; import { NotificationSeverity } from '~/common'; -import { useToastContext } from '~/Providers'; import BookmarkForm from './BookmarkForm'; import { useLocalize } from '~/hooks'; import { logger } from '~/utils'; diff --git a/client/src/components/Bookmarks/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm.tsx index c866216be..857eacb10 100644 --- a/client/src/components/Bookmarks/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm.tsx @@ -2,11 +2,10 @@ import React, { useEffect } from 'react'; import { QueryKeys } from 'librechat-data-provider'; import { Controller, useForm } from 'react-hook-form'; import { useQueryClient } from '@tanstack/react-query'; +import { Checkbox, Label, TextareaAutosize, Input, useToastContext } from '@librechat/client'; import type { TConversationTag, TConversationTagRequest } from 'librechat-data-provider'; -import { Checkbox, Label, TextareaAutosize, Input } from '~/components'; import { useBookmarkContext } from '~/Providers/BookmarkContext'; import { useConversationTagMutation } from '~/data-provider'; -import { useToastContext } from '~/Providers'; import { useLocalize } from '~/hooks'; import { cn, logger } from '~/utils'; diff --git a/client/src/components/Bookmarks/BookmarkItem.tsx b/client/src/components/Bookmarks/BookmarkItem.tsx index 60698a316..b12a5b5ac 100644 --- a/client/src/components/Bookmarks/BookmarkItem.tsx +++ b/client/src/components/Bookmarks/BookmarkItem.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; +import { Spinner } from '@librechat/client'; import { MenuItem } from '@headlessui/react'; import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons'; import type { FC } from 'react'; -import { Spinner } from '~/components/svg'; type MenuItemProps = { tag: string | React.ReactNode; diff --git a/client/src/components/Bookmarks/DeleteBookmarkButton.tsx b/client/src/components/Bookmarks/DeleteBookmarkButton.tsx index 911659de7..e63feb762 100644 --- a/client/src/components/Bookmarks/DeleteBookmarkButton.tsx +++ b/client/src/components/Bookmarks/DeleteBookmarkButton.tsx @@ -1,10 +1,17 @@ import { useCallback, useState } from 'react'; +import { + Button, + TrashIcon, + Label, + OGDialog, + OGDialogTrigger, + TooltipAnchor, + OGDialogTemplate, + useToastContext, +} from '@librechat/client'; import type { FC } from 'react'; -import { Button, TrashIcon, Label, OGDialog, OGDialogTrigger, TooltipAnchor } from '~/components'; import { useDeleteConversationTagMutation } from '~/data-provider'; -import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import { NotificationSeverity } from '~/common'; -import { useToastContext } from '~/Providers'; import { useLocalize } from '~/hooks'; const DeleteBookmarkButton: FC<{ diff --git a/client/src/components/Bookmarks/EditBookmarkButton.tsx b/client/src/components/Bookmarks/EditBookmarkButton.tsx index 6e976a53c..7c4355be8 100644 --- a/client/src/components/Bookmarks/EditBookmarkButton.tsx +++ b/client/src/components/Bookmarks/EditBookmarkButton.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; -import type { FC } from 'react'; +import { TooltipAnchor, OGDialogTrigger, EditIcon, Button } from '@librechat/client'; import type { TConversationTag } from 'librechat-data-provider'; -import { TooltipAnchor, OGDialogTrigger, EditIcon, Button } from '~/components'; +import type { FC } from 'react'; import BookmarkEditDialog from './BookmarkEditDialog'; import { useLocalize } from '~/hooks'; diff --git a/client/src/components/Chat/AddMultiConvo.tsx b/client/src/components/Chat/AddMultiConvo.tsx index 79131dfdb..26395c7a6 100644 --- a/client/src/components/Chat/AddMultiConvo.tsx +++ b/client/src/components/Chat/AddMultiConvo.tsx @@ -1,8 +1,8 @@ import { PlusCircle } from 'lucide-react'; +import { TooltipAnchor } from '@librechat/client'; import { isAssistantsEndpoint } from 'librechat-data-provider'; import type { TConversation } from 'librechat-data-provider'; import { useChatContext, useAddedChatContext } from '~/Providers'; -import { TooltipAnchor } from '~/components'; import { mainTextareaId } from '~/common'; import { useLocalize } from '~/hooks'; diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx index a554c5f7d..f488d76b2 100644 --- a/client/src/components/Chat/ChatView.tsx +++ b/client/src/components/Chat/ChatView.tsx @@ -1,6 +1,7 @@ import { memo, useCallback } from 'react'; import { useRecoilValue } from 'recoil'; import { useForm } from 'react-hook-form'; +import { Spinner } from '@librechat/client'; import { useParams } from 'react-router-dom'; import { Constants } from 'librechat-data-provider'; import type { TMessage } from 'librechat-data-provider'; @@ -10,7 +11,6 @@ import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks'; import ConversationStarters from './Input/ConversationStarters'; import { useGetMessagesByConvoId } from '~/data-provider'; import MessagesView from './Messages/MessagesView'; -import { Spinner } from '~/components/svg'; import Presentation from './Presentation'; import { buildTree, cn } from '~/utils'; import ChatForm from './Input/ChatForm'; diff --git a/client/src/components/Chat/ExportAndShareMenu.tsx b/client/src/components/Chat/ExportAndShareMenu.tsx index aa9ee8be9..207efcbc8 100644 --- a/client/src/components/Chat/ExportAndShareMenu.tsx +++ b/client/src/components/Chat/ExportAndShareMenu.tsx @@ -2,11 +2,11 @@ import { useState, useId, useRef } from 'react'; import { useRecoilValue } from 'recoil'; import * as Ariakit from '@ariakit/react'; import { Upload, Share2 } from 'lucide-react'; +import { DropdownPopup, TooltipAnchor, useMediaQuery } from '@librechat/client'; import type * as t from '~/common'; import ExportModal from '~/components/Nav/ExportConversation/ExportModal'; import { ShareButton } from '~/components/Conversations/ConvoOptions'; -import { DropdownPopup, TooltipAnchor } from '~/components/ui'; -import { useMediaQuery, useLocalize } from '~/hooks'; +import { useLocalize } from '~/hooks'; import store from '~/store'; export default function ExportAndShareMenu({ diff --git a/client/src/components/Chat/Header.tsx b/client/src/components/Chat/Header.tsx index 93a265f4a..d252e58df 100644 --- a/client/src/components/Chat/Header.tsx +++ b/client/src/components/Chat/Header.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import { useMediaQuery } from '@librechat/client'; import { useOutletContext } from 'react-router-dom'; import { getConfigDefaults, PermissionTypes, Permissions } from 'librechat-data-provider'; import type { ContextType } from '~/common'; @@ -6,10 +7,10 @@ import ModelSelector from './Menus/Endpoints/ModelSelector'; import { PresetsMenu, HeaderNewChat, OpenSidebar } from './Menus'; import { useGetStartupConfig } from '~/data-provider'; import ExportAndShareMenu from './ExportAndShareMenu'; -import { useMediaQuery, useHasAccess } from '~/hooks'; import BookmarkMenu from './Menus/BookmarkMenu'; import { TemporaryChat } from './TemporaryChat'; import AddMultiConvo from './AddMultiConvo'; +import { useHasAccess } from '~/hooks'; const defaultInterface = getConfigDefaults().interface; diff --git a/client/src/components/Chat/Input/Artifacts.tsx b/client/src/components/Chat/Input/Artifacts.tsx index eb2c49572..493f126e3 100644 --- a/client/src/components/Chat/Input/Artifacts.tsx +++ b/client/src/components/Chat/Input/Artifacts.tsx @@ -1,8 +1,8 @@ import React, { memo, useState, useCallback, useMemo } from 'react'; import * as Ariakit from '@ariakit/react'; +import { CheckboxButton } from '@librechat/client'; import { ArtifactModes } from 'librechat-data-provider'; import { WandSparkles, ChevronDown } from 'lucide-react'; -import CheckboxButton from '~/components/ui/CheckboxButton'; import { useBadgeRowContext } from '~/Providers'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Input/ArtifactsSubMenu.tsx b/client/src/components/Chat/Input/ArtifactsSubMenu.tsx index 944ecb66c..654c129fb 100644 --- a/client/src/components/Chat/Input/ArtifactsSubMenu.tsx +++ b/client/src/components/Chat/Input/ArtifactsSubMenu.tsx @@ -1,8 +1,8 @@ import React from 'react'; import * as Ariakit from '@ariakit/react'; +import { PinIcon } from '@librechat/client'; import { ChevronRight, WandSparkles } from 'lucide-react'; import { ArtifactModes } from 'librechat-data-provider'; -import { PinIcon } from '~/components/svg'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Input/AudioRecorder.tsx b/client/src/components/Chat/Input/AudioRecorder.tsx index eae7c266c..ae4252f4f 100644 --- a/client/src/components/Chat/Input/AudioRecorder.tsx +++ b/client/src/components/Chat/Input/AudioRecorder.tsx @@ -1,8 +1,7 @@ import { useCallback } from 'react'; -import { useChatFormContext, useToastContext } from '~/Providers'; -import { ListeningIcon, Spinner } from '~/components/svg'; +import { useToastContext, TooltipAnchor, ListeningIcon, Spinner } from '@librechat/client'; import { useLocalize, useSpeechToText } from '~/hooks'; -import { TooltipAnchor } from '~/components/ui'; +import { useChatFormContext } from '~/Providers'; import { globalAudioId } from '~/common'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Input/BadgeRow.tsx b/client/src/components/Chat/Input/BadgeRow.tsx index d77fc5426..5036dcd5e 100644 --- a/client/src/components/Chat/Input/BadgeRow.tsx +++ b/client/src/components/Chat/Input/BadgeRow.tsx @@ -8,6 +8,7 @@ import React, { useReducer, useCallback, } from 'react'; +import { Badge } from '@librechat/client'; import { useRecoilValue, useRecoilCallback } from 'recoil'; import type { LucideIcon } from 'lucide-react'; import CodeInterpreter from './CodeInterpreter'; @@ -15,7 +16,6 @@ import { BadgeRowProvider } from '~/Providers'; import ToolsDropdown from './ToolsDropdown'; import type { BadgeItem } from '~/common'; import { useChatBadges } from '~/hooks'; -import { Badge } from '~/components/ui'; import ToolDialogs from './ToolDialogs'; import FileSearch from './FileSearch'; import Artifacts from './Artifacts'; diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 0ca644809..50fa318b3 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -1,5 +1,6 @@ import { memo, useRef, useMemo, useEffect, useState, useCallback } from 'react'; import { useWatch } from 'react-hook-form'; +import { TextareaAutosize } from '@librechat/client'; import { useRecoilState, useRecoilValue } from 'recoil'; import { Constants, isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider'; import { @@ -20,7 +21,6 @@ import { import { mainTextareaId, BadgeItem } from '~/common'; import AttachFileChat from './Files/AttachFileChat'; import FileFormChat from './Files/FileFormChat'; -import { TextareaAutosize } from '~/components'; import { cn, removeFocusRings } from '~/utils'; import TextareaHeader from './TextareaHeader'; import PromptsCommand from './PromptsCommand'; diff --git a/client/src/components/Chat/Input/CircleRender.tsx b/client/src/components/Chat/Input/CircleRender.tsx index 88b794763..5b46f6574 100644 --- a/client/src/components/Chat/Input/CircleRender.tsx +++ b/client/src/components/Chat/Input/CircleRender.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { CircleIcon, CircleDotsIcon } from '~/components/svg'; import { ECallState } from 'librechat-data-provider'; +import { CircleIcon, CircleDotsIcon } from '@librechat/client'; const CircleRender = ({ rmsLevel, isCameraOn, state }) => { const getIconComponent = (state) => { diff --git a/client/src/components/Chat/Input/CodeInterpreter.tsx b/client/src/components/Chat/Input/CodeInterpreter.tsx index f2d9760cc..6805c3300 100644 --- a/client/src/components/Chat/Input/CodeInterpreter.tsx +++ b/client/src/components/Chat/Input/CodeInterpreter.tsx @@ -1,7 +1,7 @@ import React, { memo } from 'react'; import { TerminalSquareIcon } from 'lucide-react'; +import { CheckboxButton } from '@librechat/client'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; -import CheckboxButton from '~/components/ui/CheckboxButton'; import { useLocalize, useHasAccess } from '~/hooks'; import { useBadgeRowContext } from '~/Providers'; diff --git a/client/src/components/Chat/Input/CollapseChat.tsx b/client/src/components/Chat/Input/CollapseChat.tsx index 1bec59217..1ae0caf79 100644 --- a/client/src/components/Chat/Input/CollapseChat.tsx +++ b/client/src/components/Chat/Input/CollapseChat.tsx @@ -1,6 +1,6 @@ import React from 'react'; +import { TooltipAnchor } from '@librechat/client'; import { ChevronDown, ChevronUp } from 'lucide-react'; -import { TooltipAnchor } from '~/components/ui'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Input/EditBadges.tsx b/client/src/components/Chat/Input/EditBadges.tsx index 0c9c0b5b2..821fdd4b3 100644 --- a/client/src/components/Chat/Input/EditBadges.tsx +++ b/client/src/components/Chat/Input/EditBadges.tsx @@ -1,9 +1,9 @@ import React, { useCallback } from 'react'; import { Edit3, Check, X } from 'lucide-react'; +import { Button, Badge } from '@librechat/client'; import type { LucideIcon } from 'lucide-react'; import type { BadgeItem } from '~/common'; import { useChatBadges, useLocalize } from '~/hooks'; -import { Button, Badge } from '~/components/ui'; interface EditBadgesProps { isEditingChatBadges: boolean; diff --git a/client/src/components/Chat/Input/FileSearch.tsx b/client/src/components/Chat/Input/FileSearch.tsx index 4de6c35b6..b5106dd05 100644 --- a/client/src/components/Chat/Input/FileSearch.tsx +++ b/client/src/components/Chat/Input/FileSearch.tsx @@ -1,9 +1,8 @@ import React, { memo } from 'react'; +import { CheckboxButton, VectorIcon } from '@librechat/client'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; -import CheckboxButton from '~/components/ui/CheckboxButton'; import { useLocalize, useHasAccess } from '~/hooks'; import { useBadgeRowContext } from '~/Providers'; -import { VectorIcon } from '~/components/svg'; function FileSearch() { const localize = useLocalize(); diff --git a/client/src/components/Chat/Input/Files/AttachFile.tsx b/client/src/components/Chat/Input/Files/AttachFile.tsx index fe8c03c80..c896109ab 100644 --- a/client/src/components/Chat/Input/Files/AttachFile.tsx +++ b/client/src/components/Chat/Input/Files/AttachFile.tsx @@ -1,5 +1,5 @@ import React, { useRef } from 'react'; -import { FileUpload, TooltipAnchor, AttachmentIcon } from '~/components'; +import { FileUpload, TooltipAnchor, AttachmentIcon } from '@librechat/client'; import { useLocalize, useFileHandling } from '~/hooks'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 9fe298860..5e42dd063 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -1,11 +1,11 @@ -import { useSetRecoilState } from 'recoil'; -import * as Ariakit from '@ariakit/react'; import React, { useRef, useState, useMemo } from 'react'; +import * as Ariakit from '@ariakit/react'; +import { useSetRecoilState } from 'recoil'; import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react'; +import { FileUpload, TooltipAnchor, DropdownPopup, AttachmentIcon } from '@librechat/client'; import { EToolResources, EModelEndpoint, defaultAgentCapabilities } from 'librechat-data-provider'; import type { EndpointFileConfig } from 'librechat-data-provider'; import { useLocalize, useGetAgentsConfig, useFileHandling, useAgentCapabilities } from '~/hooks'; -import { FileUpload, TooltipAnchor, DropdownPopup, AttachmentIcon } from '~/components'; import { ephemeralAgentByConvoId } from '~/store'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Input/Files/DragDropModal.tsx b/client/src/components/Chat/Input/Files/DragDropModal.tsx index f80af70de..c3b337b42 100644 --- a/client/src/components/Chat/Input/Files/DragDropModal.tsx +++ b/client/src/components/Chat/Input/Files/DragDropModal.tsx @@ -1,8 +1,8 @@ import React, { useMemo } from 'react'; +import { OGDialog, OGDialogTemplate } from '@librechat/client'; import { ImageUpIcon, FileSearch, TerminalSquareIcon, FileType2Icon } from 'lucide-react'; import { EToolResources, defaultAgentCapabilities } from 'librechat-data-provider'; import { useLocalize, useGetAgentsConfig, useAgentCapabilities } from '~/hooks'; -import { OGDialog, OGDialogTemplate } from '~/components/ui'; interface DragDropModalProps { onOptionSelect: (option: EToolResources | undefined) => void; diff --git a/client/src/components/Chat/Input/Files/FilePreview.tsx b/client/src/components/Chat/Input/Files/FilePreview.tsx index 400834a5b..600092278 100644 --- a/client/src/components/Chat/Input/Files/FilePreview.tsx +++ b/client/src/components/Chat/Input/Files/FilePreview.tsx @@ -1,7 +1,7 @@ +import { Spinner } from '@librechat/client'; import type { TFile } from 'librechat-data-provider'; import type { ExtendedFile } from '~/common'; -import FileIcon from '~/components/svg/Files/FileIcon'; -import { Spinner } from '~/components'; +import { FileIcon } from '~/components/svg'; import SourceIcon from './SourceIcon'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Input/Files/FileRow.tsx b/client/src/components/Chat/Input/Files/FileRow.tsx index 67d687eef..47da77bbf 100644 --- a/client/src/components/Chat/Input/Files/FileRow.tsx +++ b/client/src/components/Chat/Input/Files/FileRow.tsx @@ -1,11 +1,11 @@ import { useEffect } from 'react'; +import { useToastContext } from '@librechat/client'; import { EToolResources } from 'librechat-data-provider'; import type { ExtendedFile } from '~/common'; import { useDeleteFilesMutation } from '~/data-provider'; -import { useToastContext } from '~/Providers'; -import { useLocalize } from '~/hooks'; import { useFileDeletion } from '~/hooks/Files'; import FileContainer from './FileContainer'; +import { useLocalize } from '~/hooks'; import { logger } from '~/utils'; import Image from './Image'; diff --git a/client/src/components/Chat/Input/Files/FilesView.tsx b/client/src/components/Chat/Input/Files/FilesView.tsx index afffa34c1..13a378b93 100644 --- a/client/src/components/Chat/Input/Files/FilesView.tsx +++ b/client/src/components/Chat/Input/Files/FilesView.tsx @@ -1,6 +1,6 @@ import { FileSources, FileContext } from 'librechat-data-provider'; import type { TFile } from 'librechat-data-provider'; -import { OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle } from '~/components'; +import { OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle } from '@librechat/client'; import { useGetFiles } from '~/data-provider'; import { DataTable, columns } from './Table'; import { useLocalize } from '~/hooks'; diff --git a/client/src/components/Chat/Input/Files/ImagePreview.tsx b/client/src/components/Chat/Input/Files/ImagePreview.tsx index fe761da86..37d4e7900 100644 --- a/client/src/components/Chat/Input/Files/ImagePreview.tsx +++ b/client/src/components/Chat/Input/Files/ImagePreview.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Maximize2 } from 'lucide-react'; -import { OGDialog, OGDialogContent } from '~/components/ui'; import { FileSources } from 'librechat-data-provider'; +import { OGDialog, OGDialogContent } from '@librechat/client'; import ProgressCircle from './ProgressCircle'; import SourceIcon from './SourceIcon'; import { cn } from '~/utils'; @@ -93,9 +93,9 @@ const ImagePreview = ({ const style: styleProps = imageUrl ? { - ...baseStyle, - backgroundImage: `url(${imageUrl})`, - } + ...baseStyle, + backgroundImage: `url(${imageUrl})`, + } : baseStyle; if (typeof style.backgroundImage !== 'string' || style.backgroundImage.length === 0) { diff --git a/client/src/components/Chat/Input/Files/Table/Columns.tsx b/client/src/components/Chat/Input/Files/Table/Columns.tsx index bc60de361..fb0eccee6 100644 --- a/client/src/components/Chat/Input/Files/Table/Columns.tsx +++ b/client/src/components/Chat/Input/Files/Table/Columns.tsx @@ -1,14 +1,19 @@ /* eslint-disable react-hooks/rules-of-hooks */ - import { ArrowUpDown, Database } from 'lucide-react'; import { FileSources, FileContext } from 'librechat-data-provider'; +import { + Button, + Checkbox, + OpenAIMinimalIcon, + AzureMinimalIcon, + useMediaQuery, +} from '@librechat/client'; import type { ColumnDef } from '@tanstack/react-table'; import type { TFile } from 'librechat-data-provider'; -import { Button, Checkbox, OpenAIMinimalIcon, AzureMinimalIcon } from '~/components'; import ImagePreview from '~/components/Chat/Input/Files/ImagePreview'; import FilePreview from '~/components/Chat/Input/Files/FilePreview'; +import { TranslationKeys, useLocalize } from '~/hooks'; import { SortFilterHeader } from './SortFilterHeader'; -import { TranslationKeys, useLocalize, useMediaQuery } from '~/hooks'; import { formatDate, getFileType } from '~/utils'; const contextMap: Record = { diff --git a/client/src/components/Chat/Input/Files/Table/DataTable.tsx b/client/src/components/Chat/Input/Files/Table/DataTable.tsx index c8c3190bc..ffb3e2825 100644 --- a/client/src/components/Chat/Input/Files/Table/DataTable.tsx +++ b/client/src/components/Chat/Input/Files/Table/DataTable.tsx @@ -16,8 +16,6 @@ import type { ColumnFiltersState, } from '@tanstack/react-table'; import { FileContext } from 'librechat-data-provider'; -import type { AugmentedColumnDef } from '~/common'; -import type { TFile } from 'librechat-data-provider'; import { Button, Input, @@ -31,11 +29,14 @@ import { DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, -} from '~/components/ui'; + TrashIcon, + Spinner, + useMediaQuery, +} from '@librechat/client'; +import type { TFile } from 'librechat-data-provider'; +import type { AugmentedColumnDef } from '~/common'; import { useDeleteFilesFromTable } from '~/hooks/Files'; -import { TrashIcon, Spinner } from '~/components/svg'; -import useLocalize from '~/hooks/useLocalize'; -import { useMediaQuery } from '~/hooks'; +import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; import store from '~/store'; diff --git a/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx b/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx index bb9247c15..d275044ee 100644 --- a/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx +++ b/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx @@ -2,13 +2,13 @@ import { Column } from '@tanstack/react-table'; import { ListFilter, FilterX } from 'lucide-react'; import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon } from '@radix-ui/react-icons'; import { + Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, -} from '~/components/ui/DropdownMenu'; -import { Button } from '~/components/ui/Button'; +} from '@librechat/client'; import { useLocalize, TranslationKeys } from '~/hooks'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Input/Files/Table/TemplateTable.tsx b/client/src/components/Chat/Input/Files/Table/TemplateTable.tsx index dc288c73a..9f6b78f91 100644 --- a/client/src/components/Chat/Input/Files/Table/TemplateTable.tsx +++ b/client/src/components/Chat/Input/Files/Table/TemplateTable.tsx @@ -1,4 +1,4 @@ -import { DotsIcon, TrashIcon } from '~/components/svg'; +import { DotsIcon, TrashIcon } from '@librechat/client'; export default function Template() { return ( diff --git a/client/src/components/Chat/Input/HeaderOptions.tsx b/client/src/components/Chat/Input/HeaderOptions.tsx index ac4b03976..5ba263941 100644 --- a/client/src/components/Chat/Input/HeaderOptions.tsx +++ b/client/src/components/Chat/Input/HeaderOptions.tsx @@ -2,12 +2,12 @@ import { useRecoilState } from 'recoil'; import { Settings2 } from 'lucide-react'; import { useState, useEffect, useMemo } from 'react'; import { Root, Anchor } from '@radix-ui/react-popover'; -import { EModelEndpoint, isParamEndpoint, tConvoUpdateSchema } from 'librechat-data-provider'; +import { PluginStoreDialog, TooltipAnchor } from '@librechat/client'; import { useUserKeyQuery } from 'librechat-data-provider/react-query'; +import { EModelEndpoint, isParamEndpoint, tConvoUpdateSchema } from 'librechat-data-provider'; import type { TPreset, TInterfaceConfig } from 'librechat-data-provider'; import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints'; -import { useSetIndexOptions, useMediaQuery, useLocalize } from '~/hooks'; -import { PluginStoreDialog, TooltipAnchor } from '~/components'; +import { useSetIndexOptions, useLocalize } from '~/hooks'; import { useGetEndpointsQuery } from '~/data-provider'; import OptionsPopover from './OptionsPopover'; import PopoverButtons from './PopoverButtons'; diff --git a/client/src/components/Chat/Input/MCPConfigDialog.tsx b/client/src/components/Chat/Input/MCPConfigDialog.tsx new file mode 100644 index 000000000..5cb217086 --- /dev/null +++ b/client/src/components/Chat/Input/MCPConfigDialog.tsx @@ -0,0 +1,121 @@ +import React, { useEffect } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { Button, Input, Label, OGDialog, OGDialogTemplate } from '@librechat/client'; +import { useLocalize } from '~/hooks'; + +export interface ConfigFieldDetail { + title: string; + description: string; +} + +interface MCPConfigDialogProps { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + fieldsSchema: Record; + initialValues: Record; + onSave: (updatedValues: Record) => void; + isSubmitting?: boolean; + onRevoke?: () => void; + serverName: string; +} + +export default function MCPConfigDialog({ + isOpen, + onOpenChange, + fieldsSchema, + initialValues, + onSave, + isSubmitting = false, + onRevoke, + serverName, +}: MCPConfigDialogProps) { + const localize = useLocalize(); + const { + control, + handleSubmit, + reset, + formState: { errors, _ }, + } = useForm>({ + defaultValues: initialValues, + }); + + useEffect(() => { + if (isOpen) { + reset(initialValues); + } + }, [isOpen, initialValues, reset]); + + const onFormSubmit = (data: Record) => { + onSave(data); + }; + + const handleRevoke = () => { + if (onRevoke) { + onRevoke(); + } + }; + + const dialogTitle = localize('com_ui_configure_mcp_variables_for', { 0: serverName }); + const dialogDescription = localize('com_ui_mcp_dialog_desc'); + + return ( + + + {Object.entries(fieldsSchema).map(([key, details]) => ( +

+ + ( + + )} + /> + {details.description && ( +

+ )} + {errors[key] &&

{errors[key]?.message}

} +
+ ))} + + } + selection={{ + selectHandler: handleSubmit(onFormSubmit), + selectClasses: 'bg-green-500 hover:bg-green-600 text-white', + selectText: isSubmitting ? localize('com_ui_saving') : localize('com_ui_save'), + }} + buttons={ + onRevoke && ( + + ) + } + footerClassName="flex justify-end gap-2 px-6 pb-6 pt-2" + showCancelButton={true} + /> + + ); +} diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index 2f9b4071c..8160cdd2f 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -1,9 +1,8 @@ import React, { memo, useCallback } from 'react'; -import MCPConfigDialog from '~/components/ui/MCP/MCPConfigDialog'; -import MCPServerStatusIcon from '~/components/ui/MCP/MCPServerStatusIcon'; -import MultiSelect from '~/components/ui/MultiSelect'; -import { MCPIcon } from '~/components/svg'; +import { MultiSelect, MCPIcon } from '@librechat/client'; +import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon'; import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager'; +import MCPConfigDialog from '~/components/MCP/MCPConfigDialog'; function MCPSelect() { const { @@ -80,7 +79,6 @@ function MCPSelect() { items={configuredServers} selectedValues={mcpValues ?? []} setSelectedValues={batchToggleServers} - defaultSelectedValues={mcpValues ?? []} renderSelectedValues={renderSelectedValues} renderItemContent={renderItemContent} placeholder={placeholderText} diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx index 8f271c0b6..1d704f2a6 100644 --- a/client/src/components/Chat/Input/MCPSubMenu.tsx +++ b/client/src/components/Chat/Input/MCPSubMenu.tsx @@ -1,10 +1,10 @@ import React from 'react'; import * as Ariakit from '@ariakit/react'; import { ChevronRight } from 'lucide-react'; -import { PinIcon, MCPIcon } from '~/components/svg'; -import MCPConfigDialog from '~/components/ui/MCP/MCPConfigDialog'; -import MCPServerStatusIcon from '~/components/ui/MCP/MCPServerStatusIcon'; +import { PinIcon, MCPIcon } from '@librechat/client'; +import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon'; import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager'; +import MCPConfigDialog from '~/components/MCP/MCPConfigDialog'; import { cn } from '~/utils'; interface MCPSubMenuProps { diff --git a/client/src/components/Chat/Input/Mention.tsx b/client/src/components/Chat/Input/Mention.tsx index 067383711..e3472a2aa 100644 --- a/client/src/components/Chat/Input/Mention.tsx +++ b/client/src/components/Chat/Input/Mention.tsx @@ -1,12 +1,13 @@ import { useState, useRef, useEffect } from 'react'; +import { useCombobox } from '@librechat/client'; import { AutoSizer, List } from 'react-virtualized'; import { EModelEndpoint } from 'librechat-data-provider'; -import type { SetterOrUpdater } from 'recoil'; import type { MentionOption, ConvoGenerator } from '~/common'; +import type { SetterOrUpdater } from 'recoil'; import useSelectMention from '~/hooks/Input/useSelectMention'; +import { useLocalize, TranslationKeys } from '~/hooks'; import { useAssistantsMapContext } from '~/Providers'; import useMentions from '~/hooks/Input/useMentions'; -import { useLocalize, useCombobox, TranslationKeys } from '~/hooks'; import { removeCharIfLast } from '~/utils'; import MentionItem from './MentionItem'; diff --git a/client/src/components/Chat/Input/OptionsPopover.tsx b/client/src/components/Chat/Input/OptionsPopover.tsx index a49d31f33..f3102bd27 100644 --- a/client/src/components/Chat/Input/OptionsPopover.tsx +++ b/client/src/components/Chat/Input/OptionsPopover.tsx @@ -1,11 +1,10 @@ import { useRef } from 'react'; import { Save } from 'lucide-react'; import { Portal, Content } from '@radix-ui/react-popover'; +import { Button, CrossIcon, useOnClickOutside } from '@librechat/client'; import type { ReactNode } from 'react'; -import { useLocalize, useOnClickOutside } from '~/hooks'; import { cn, removeFocusOutlines } from '~/utils'; -import { CrossIcon } from '~/components/svg'; -import { Button } from '~/components/ui'; +import { useLocalize } from '~/hooks'; type TOptionsPopoverProps = { children: ReactNode; diff --git a/client/src/components/Chat/Input/PopoverButtons.tsx b/client/src/components/Chat/Input/PopoverButtons.tsx index fa2211ecc..31bb6ee2f 100644 --- a/client/src/components/Chat/Input/PopoverButtons.tsx +++ b/client/src/components/Chat/Input/PopoverButtons.tsx @@ -1,9 +1,8 @@ import { useRecoilState } from 'recoil'; import { EModelEndpoint, SettingsViews } from 'librechat-data-provider'; +import { Button, MessagesSquared, GPTIcon, AssistantIcon, DataIcon } from '@librechat/client'; import type { ReactNode } from 'react'; -import { MessagesSquared, GPTIcon, AssistantIcon, DataIcon } from '~/components/svg'; import { useChatContext } from '~/Providers'; -import { Button } from '~/components/ui'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils/'; import store from '~/store'; diff --git a/client/src/components/Chat/Input/PromptsCommand.tsx b/client/src/components/Chat/Input/PromptsCommand.tsx index 835c7cd31..0756be17e 100644 --- a/client/src/components/Chat/Input/PromptsCommand.tsx +++ b/client/src/components/Chat/Input/PromptsCommand.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react'; import { AutoSizer, List } from 'react-virtualized'; +import { Spinner, useCombobox } from '@librechat/client'; import { useSetRecoilState, useRecoilValue } from 'recoil'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; import type { TPromptGroup } from 'librechat-data-provider'; @@ -7,9 +8,8 @@ import type { PromptOption } from '~/common'; import { removeCharIfLast, mapPromptGroups, detectVariables } from '~/utils'; import VariableDialog from '~/components/Prompts/Groups/VariableDialog'; import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; -import { useLocalize, useCombobox, useHasAccess } from '~/hooks'; +import { useLocalize, useHasAccess } from '~/hooks'; import { useGetAllPromptGroups } from '~/data-provider'; -import { Spinner } from '~/components/svg'; import MentionItem from './MentionItem'; import store from '~/store'; diff --git a/client/src/components/Chat/Input/SendButton.tsx b/client/src/components/Chat/Input/SendButton.tsx index 52a2b4bed..14c21f058 100644 --- a/client/src/components/Chat/Input/SendButton.tsx +++ b/client/src/components/Chat/Input/SendButton.tsx @@ -1,8 +1,7 @@ import React, { forwardRef } from 'react'; import { useWatch } from 'react-hook-form'; import type { Control } from 'react-hook-form'; -import { TooltipAnchor } from '~/components/ui'; -import { SendIcon } from '~/components/svg'; +import { SendIcon, TooltipAnchor } from '@librechat/client'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Input/StopButton.tsx b/client/src/components/Chat/Input/StopButton.tsx index 6c785811c..4a058777f 100644 --- a/client/src/components/Chat/Input/StopButton.tsx +++ b/client/src/components/Chat/Input/StopButton.tsx @@ -1,4 +1,4 @@ -import { TooltipAnchor } from '~/components/ui'; +import { TooltipAnchor } from '@librechat/client'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Input/ToolsDropdown.tsx b/client/src/components/Chat/Input/ToolsDropdown.tsx index 159fe455d..60ad0156f 100644 --- a/client/src/components/Chat/Input/ToolsDropdown.tsx +++ b/client/src/components/Chat/Input/ToolsDropdown.tsx @@ -1,6 +1,7 @@ import React, { useState, useMemo, useCallback } from 'react'; import * as Ariakit from '@ariakit/react'; import { Globe, Settings, Settings2, TerminalSquareIcon } from 'lucide-react'; +import { TooltipAnchor, DropdownPopup, PinIcon, VectorIcon } from '@librechat/client'; import type { MenuItemProps } from '~/common'; import { AuthType, @@ -9,11 +10,9 @@ import { PermissionTypes, defaultAgentCapabilities, } from 'librechat-data-provider'; -import { TooltipAnchor, DropdownPopup } from '~/components'; import { useLocalize, useHasAccess, useAgentCapabilities } from '~/hooks'; import ArtifactsSubMenu from '~/components/Chat/Input/ArtifactsSubMenu'; import MCPSubMenu from '~/components/Chat/Input/MCPSubMenu'; -import { PinIcon, VectorIcon } from '~/components/svg'; import { useBadgeRowContext } from '~/Providers'; import { cn } from '~/utils'; @@ -317,7 +316,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { return ( { const localize = useLocalize(); diff --git a/client/src/components/Chat/Menus/UI/TitleButton.tsx b/client/src/components/Chat/Menus/UI/TitleButton.tsx index c23fdeb3a..d6f7dec09 100644 --- a/client/src/components/Chat/Menus/UI/TitleButton.tsx +++ b/client/src/components/Chat/Menus/UI/TitleButton.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { ChevronDown } from 'lucide-react'; import { Trigger } from '@radix-ui/react-popover'; -import useLocalize from '~/hooks/useLocalize'; +import { useLocalize } from '~/hooks'; export default function TitleButton({ primaryText = '', secondaryText = '' }) { const localize = useLocalize(); diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx index 49f6be255..9c38dd92d 100644 --- a/client/src/components/Chat/Messages/Content/ContentParts.tsx +++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx @@ -11,9 +11,9 @@ import { ThinkingButton } from '~/components/Artifacts/Thinking'; import { MessageContext, SearchContext } from '~/Providers'; import MemoryArtifacts from './MemoryArtifacts'; import Sources from '~/components/Web/Sources'; -import useLocalize from '~/hooks/useLocalize'; import { mapAttachments } from '~/utils/map'; import { EditTextPart } from './Parts'; +import { useLocalize } from '~/hooks'; import store from '~/store'; import Part from './Part'; diff --git a/client/src/components/Chat/Messages/Content/DialogImage.tsx b/client/src/components/Chat/Messages/Content/DialogImage.tsx index 0711757df..9eb5f9e71 100644 --- a/client/src/components/Chat/Messages/Content/DialogImage.tsx +++ b/client/src/components/Chat/Messages/Content/DialogImage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback, useRef } from 'react'; +import { Button, OGDialog, OGDialogContent, TooltipAnchor } from '@librechat/client'; import { X, ArrowDownToLine, PanelLeftOpen, PanelLeftClose, RotateCcw } from 'lucide-react'; -import { Button, OGDialog, OGDialogContent, TooltipAnchor } from '~/components'; import { useLocalize } from '~/hooks'; const getQualityStyles = (quality: string): string => { diff --git a/client/src/components/Chat/Messages/Content/EditMessage.tsx b/client/src/components/Chat/Messages/Content/EditMessage.tsx index e2781aaa5..e87321090 100644 --- a/client/src/components/Chat/Messages/Content/EditMessage.tsx +++ b/client/src/components/Chat/Messages/Content/EditMessage.tsx @@ -1,10 +1,10 @@ import { useRef, useEffect, useCallback } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; import { useForm } from 'react-hook-form'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { TextareaAutosize, TooltipAnchor } from '@librechat/client'; import { useUpdateMessageMutation } from 'librechat-data-provider/react-query'; import type { TEditProps } from '~/common'; import { useChatContext, useAddedChatContext } from '~/Providers'; -import { TextareaAutosize, TooltipAnchor } from '~/components/ui'; import { cn, removeFocusRings } from '~/utils'; import { useLocalize } from '~/hooks'; import Container from './Container'; diff --git a/client/src/components/Chat/Messages/Content/Image.tsx b/client/src/components/Chat/Messages/Content/Image.tsx index ba4f65671..450ed0fc2 100644 --- a/client/src/components/Chat/Messages/Content/Image.tsx +++ b/client/src/components/Chat/Messages/Content/Image.tsx @@ -1,8 +1,8 @@ import React, { useState, useRef, useMemo } from 'react'; +import { Skeleton } from '@librechat/client'; import { LazyLoadImage } from 'react-lazy-load-image-component'; import { cn, scaleImage } from '~/utils'; import DialogImage from './DialogImage'; -import { Skeleton } from '~/components'; const Image = ({ imagePath, diff --git a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx index e0a381ff5..de1e82443 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx @@ -1,12 +1,13 @@ import React, { memo, useMemo, useRef, useEffect } from 'react'; import { useRecoilValue } from 'recoil'; +import { useToastContext } from '@librechat/client'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; -import { useToastContext, useCodeBlockContext } from '~/Providers'; import CodeBlock from '~/components/Messages/Content/CodeBlock'; import useHasAccess from '~/hooks/Roles/useHasAccess'; import { useFileDownload } from '~/data-provider'; -import useLocalize from '~/hooks/useLocalize'; +import { useCodeBlockContext } from '~/Providers'; import { handleDoubleClick } from '~/utils'; +import { useLocalize } from '~/hooks'; import store from '~/store'; type TCodeProps = { diff --git a/client/src/components/Chat/Messages/Content/MessageContent.tsx b/client/src/components/Chat/Messages/Content/MessageContent.tsx index f70a15b77..dd3433d56 100644 --- a/client/src/components/Chat/Messages/Content/MessageContent.tsx +++ b/client/src/components/Chat/Messages/Content/MessageContent.tsx @@ -1,10 +1,10 @@ import { memo, Suspense, useMemo } from 'react'; import { useRecoilValue } from 'recoil'; +import { DelayedRender } from '@librechat/client'; import type { TMessage } from 'librechat-data-provider'; import type { TMessageContentProps, TDisplayProps } from '~/common'; import Error from '~/components/Messages/Content/Error'; import Thinking from '~/components/Artifacts/Thinking'; -import { DelayedRender } from '~/components/ui'; import { useChatContext } from '~/Providers'; import MarkdownLite from './MarkdownLite'; import EditMessage from './EditMessage'; diff --git a/client/src/components/Chat/Messages/Content/Parts/EditTextPart.tsx b/client/src/components/Chat/Messages/Content/Parts/EditTextPart.tsx index ab15355d1..1898920ea 100644 --- a/client/src/components/Chat/Messages/Content/Parts/EditTextPart.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/EditTextPart.tsx @@ -1,5 +1,6 @@ import { useRef, useEffect, useCallback, useMemo } from 'react'; import { useForm } from 'react-hook-form'; +import { TextareaAutosize } from '@librechat/client'; import { ContentTypes } from 'librechat-data-provider'; import { useRecoilState, useRecoilValue } from 'recoil'; import { useUpdateMessageContentMutation } from 'librechat-data-provider/react-query'; @@ -7,7 +8,6 @@ import type { Agents } from 'librechat-data-provider'; import type { TEditProps } from '~/common'; import Container from '~/components/Chat/Messages/Content/Container'; import { useChatContext, useAddedChatContext } from '~/Providers'; -import { TextareaAutosize } from '~/components/ui'; import { cn, removeFocusRings } from '~/utils'; import { useLocalize } from '~/hooks'; import store from '~/store'; diff --git a/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx b/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx index 590b6d7b8..d328f202e 100644 --- a/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/LogLink.tsx @@ -1,6 +1,6 @@ import React from 'react'; +import { useToastContext } from '@librechat/client'; import { useCodeOutputDownload } from '~/data-provider'; -import { useToastContext } from '~/Providers'; interface LogLinkProps { href: string; diff --git a/client/src/components/Chat/Messages/Content/Parts/OpenAIImageGen/OpenAIImageGen.tsx b/client/src/components/Chat/Messages/Content/Parts/OpenAIImageGen/OpenAIImageGen.tsx index ef24c3553..c0f77b14f 100644 --- a/client/src/components/Chat/Messages/Content/Parts/OpenAIImageGen/OpenAIImageGen.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/OpenAIImageGen/OpenAIImageGen.tsx @@ -1,8 +1,8 @@ import { useState, useEffect, useRef, useCallback } from 'react'; +import { PixelCard } from '@librechat/client'; import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider'; import Image from '~/components/Chat/Messages/Content/Image'; import ProgressText from './ProgressText'; -import { PixelCard } from '~/components'; import { scaleImage } from '~/utils'; export default function OpenAIImageGen({ diff --git a/client/src/components/Chat/Messages/Content/ProgressText.tsx b/client/src/components/Chat/Messages/Content/ProgressText.tsx index 1d8030907..e131579f6 100644 --- a/client/src/components/Chat/Messages/Content/ProgressText.tsx +++ b/client/src/components/Chat/Messages/Content/ProgressText.tsx @@ -1,8 +1,8 @@ import * as Popover from '@radix-ui/react-popover'; +import { Spinner } from '@librechat/client'; import { ChevronDown, ChevronUp } from 'lucide-react'; import CancelledIcon from './CancelledIcon'; import FinishedIcon from './FinishedIcon'; -import { Spinner } from '~/components'; import { cn } from '~/utils'; const wrapperClass = diff --git a/client/src/components/Chat/Messages/Content/SearchContent.tsx b/client/src/components/Chat/Messages/Content/SearchContent.tsx index 8b409804b..81752ddf9 100644 --- a/client/src/components/Chat/Messages/Content/SearchContent.tsx +++ b/client/src/components/Chat/Messages/Content/SearchContent.tsx @@ -1,5 +1,6 @@ import { Suspense, useMemo } from 'react'; import { useRecoilValue } from 'recoil'; +import { DelayedRender } from '@librechat/client'; import { ContentTypes } from 'librechat-data-provider'; import type { Agents, @@ -9,7 +10,6 @@ import type { TMessageContentParts, } from 'librechat-data-provider'; import { UnfinishedMessage } from './MessageContent'; -import { DelayedRender } from '~/components/ui'; import Sources from '~/components/Web/Sources'; import { cn, mapAttachments } from '~/utils'; import { SearchContext } from '~/Providers'; diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx index 3408ba5a5..84fc8cdf7 100644 --- a/client/src/components/Chat/Messages/Content/ToolCall.tsx +++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx @@ -1,4 +1,5 @@ import { useMemo, useState, useEffect, useRef, useLayoutEffect } from 'react'; +import { Button } from '@librechat/client'; import { TriangleAlert } from 'lucide-react'; import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider'; import type { TAttachment } from 'librechat-data-provider'; @@ -6,7 +7,6 @@ import { useLocalize, useProgress } from '~/hooks'; import { AttachmentGroup } from './Parts'; import ToolCallInfo from './ToolCallInfo'; import ProgressText from './ProgressText'; -import { Button } from '~/components'; import { logger, cn } from '~/utils'; export default function ToolCall({ diff --git a/client/src/components/Chat/Messages/Feedback.tsx b/client/src/components/Chat/Messages/Feedback.tsx index 4879808d9..a43b9700e 100644 --- a/client/src/components/Chat/Messages/Feedback.tsx +++ b/client/src/components/Chat/Messages/Feedback.tsx @@ -1,6 +1,14 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import * as Ariakit from '@ariakit/react'; import { TFeedback, TFeedbackTag, getTagsForRating } from 'librechat-data-provider'; +import { + Button, + OGDialog, + OGDialogContent, + OGDialogTitle, + ThumbUpIcon, + ThumbDownIcon, +} from '@librechat/client'; import { AlertCircle, PenTool, @@ -11,14 +19,6 @@ import { Lightbulb, Search, } from 'lucide-react'; -import { - Button, - OGDialog, - OGDialogContent, - OGDialogTitle, - ThumbUpIcon, - ThumbDownIcon, -} from '~/components'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; diff --git a/client/src/components/Chat/Messages/Fork.tsx b/client/src/components/Chat/Messages/Fork.tsx index 0a0c0a114..810692c32 100644 --- a/client/src/components/Chat/Messages/Fork.tsx +++ b/client/src/components/Chat/Messages/Fork.tsx @@ -3,11 +3,11 @@ import { useRecoilState } from 'recoil'; import * as Ariakit from '@ariakit/react'; import { VisuallyHidden } from '@ariakit/react'; import { GitFork, InfoIcon } from 'lucide-react'; +import { useToastContext } from '@librechat/client'; import { ForkOptions } from 'librechat-data-provider'; import { GitCommit, GitBranchPlus, ListTree } from 'lucide-react'; import { TranslationKeys, useLocalize, useNavigateToConvo } from '~/hooks'; import { useForkConvoMutation } from '~/data-provider'; -import { useToastContext } from '~/Providers'; import { cn } from '~/utils'; import store from '~/store'; diff --git a/client/src/components/Chat/Messages/HoverButtons.tsx b/client/src/components/Chat/Messages/HoverButtons.tsx index a13266f04..8c9067374 100644 --- a/client/src/components/Chat/Messages/HoverButtons.tsx +++ b/client/src/components/Chat/Messages/HoverButtons.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo, memo } from 'react'; import { useRecoilState } from 'recoil'; import type { TConversation, TMessage, TFeedback } from 'librechat-data-provider'; -import { EditIcon, Clipboard, CheckMark, ContinueIcon, RegenerateIcon } from '~/components'; +import { EditIcon, Clipboard, CheckMark, ContinueIcon, RegenerateIcon } from '@librechat/client'; import { useGenerationsByLatest, useLocalize } from '~/hooks'; import { Fork } from '~/components/Conversations'; import MessageAudio from './MessageAudio'; diff --git a/client/src/components/Chat/Messages/MessageParts.tsx b/client/src/components/Chat/Messages/MessageParts.tsx index 889e75d4e..d53522e64 100644 --- a/client/src/components/Chat/Messages/MessageParts.tsx +++ b/client/src/components/Chat/Messages/MessageParts.tsx @@ -6,7 +6,6 @@ import { useMessageHelpers, useLocalize, useAttachments } from '~/hooks'; import MessageIcon from '~/components/Chat/Messages/MessageIcon'; import ContentParts from './Content/ContentParts'; import SiblingSwitch from './SiblingSwitch'; - import MultiMessage from './MultiMessage'; import HoverButtons from './HoverButtons'; import SubRow from './SubRow'; diff --git a/client/src/components/Chat/Messages/MinimalHoverButtons.tsx b/client/src/components/Chat/Messages/MinimalHoverButtons.tsx index 150cc59f7..3f4e1711a 100644 --- a/client/src/components/Chat/Messages/MinimalHoverButtons.tsx +++ b/client/src/components/Chat/Messages/MinimalHoverButtons.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; +import { Clipboard, CheckMark } from '@librechat/client'; import type { TMessage, TAttachment, SearchResultData } from 'librechat-data-provider'; import { useLocalize, useCopyToClipboard } from '~/hooks'; -import { Clipboard, CheckMark } from '~/components/svg'; type THoverButtons = { message: TMessage; diff --git a/client/src/components/Chat/Messages/MultiMessage.tsx b/client/src/components/Chat/Messages/MultiMessage.tsx index 9050a6149..99733143f 100644 --- a/client/src/components/Chat/Messages/MultiMessage.tsx +++ b/client/src/components/Chat/Messages/MultiMessage.tsx @@ -3,11 +3,8 @@ import { useEffect, useCallback } from 'react'; import { isAssistantsEndpoint } from 'librechat-data-provider'; import type { TMessage } from 'librechat-data-provider'; import type { TMessageProps } from '~/common'; - import MessageContent from '~/components/Messages/MessageContent'; - import MessageParts from './MessageParts'; - import Message from './Message'; import store from '~/store'; diff --git a/client/src/components/Chat/TemporaryChat.tsx b/client/src/components/Chat/TemporaryChat.tsx index 69e39a1a6..a13437949 100644 --- a/client/src/components/Chat/TemporaryChat.tsx +++ b/client/src/components/Chat/TemporaryChat.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { motion } from 'framer-motion'; +import { TooltipAnchor } from '@librechat/client'; import { MessageCircleDashed } from 'lucide-react'; import { useRecoilState, useRecoilCallback } from 'recoil'; -import { TooltipAnchor } from '~/components/ui'; import { useChatContext } from '~/Providers'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; diff --git a/client/src/components/Conversations/Conversations.tsx b/client/src/components/Conversations/Conversations.tsx index 46f217a83..10203c89e 100644 --- a/client/src/components/Conversations/Conversations.tsx +++ b/client/src/components/Conversations/Conversations.tsx @@ -1,11 +1,11 @@ import { useMemo, memo, type FC, useCallback } from 'react'; import throttle from 'lodash/throttle'; import { parseISO, isToday } from 'date-fns'; +import { Spinner, useMediaQuery } from '@librechat/client'; import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized'; -import { useLocalize, TranslationKeys, useMediaQuery } from '~/hooks'; import { TConversation } from 'librechat-data-provider'; +import { useLocalize, TranslationKeys } from '~/hooks'; import { groupConversationsByDate } from '~/utils'; -import { Spinner } from '~/components/svg'; import Convo from './Convo'; interface ConversationsProps { diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index afd14a4cb..3804cbf2b 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -2,14 +2,14 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { useRecoilValue } from 'recoil'; import { useParams } from 'react-router-dom'; import { Constants } from 'librechat-data-provider'; +import { useToastContext, useMediaQuery } from '@librechat/client'; import type { TConversation } from 'librechat-data-provider'; -import { useNavigateToConvo, useMediaQuery, useLocalize } from '~/hooks'; import { useUpdateConversationMutation } from '~/data-provider'; import EndpointIcon from '~/components/Endpoints/EndpointIcon'; +import { useNavigateToConvo, useLocalize } from '~/hooks'; import { useGetEndpointsQuery } from '~/data-provider'; import { NotificationSeverity } from '~/common'; import { ConvoOptions } from './ConvoOptions'; -import { useToastContext } from '~/Providers'; import RenameForm from './RenameForm'; import ConvoLink from './ConvoLink'; import { cn } from '~/utils'; diff --git a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx index 87211c1a6..7affbd8e9 100644 --- a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx +++ b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx @@ -1,6 +1,7 @@ import { useState, useId, useRef, memo, useCallback, useMemo } from 'react'; import * as Menu from '@ariakit/react/menu'; import { useParams, useNavigate } from 'react-router-dom'; +import { DropdownPopup, Spinner, useToastContext } from '@librechat/client'; import { Ellipsis, Share2, Copy, Archive, Pen, Trash } from 'lucide-react'; import type { MouseEvent } from 'react'; import { @@ -9,9 +10,8 @@ import { useArchiveConvoMutation, } from '~/data-provider'; import { useLocalize, useNavigateToConvo, useNewConvo } from '~/hooks'; -import { useToastContext, useChatContext } from '~/Providers'; -import { DropdownPopup, Spinner } from '~/components'; import { NotificationSeverity } from '~/common'; +import { useChatContext } from '~/Providers'; import DeleteButton from './DeleteButton'; import ShareButton from './ShareButton'; import { cn } from '~/utils'; diff --git a/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx b/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx index c40ca5295..3c34cb8c3 100644 --- a/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx +++ b/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react'; import { QueryKeys } from 'librechat-data-provider'; import { useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate } from 'react-router-dom'; -import type { TMessage } from 'librechat-data-provider'; import { Button, Spinner, @@ -10,11 +9,12 @@ import { OGDialogTitle, OGDialogHeader, OGDialogContent, -} from '~/components'; + useToastContext, +} from '@librechat/client'; +import type { TMessage } from 'librechat-data-provider'; import { useDeleteConversationMutation } from '~/data-provider'; import { useLocalize, useNewConvo } from '~/hooks'; import { NotificationSeverity } from '~/common'; -import { useToastContext } from '~/Providers'; type DeleteButtonProps = { conversationId: string; diff --git a/client/src/components/Conversations/ConvoOptions/ShareButton.tsx b/client/src/components/Conversations/ConvoOptions/ShareButton.tsx index c979019f0..177dd5ae5 100644 --- a/client/src/components/Conversations/ConvoOptions/ShareButton.tsx +++ b/client/src/components/Conversations/ConvoOptions/ShareButton.tsx @@ -2,9 +2,8 @@ import React, { useState, useEffect } from 'react'; import { QRCodeSVG } from 'qrcode.react'; import { Copy, CopyCheck } from 'lucide-react'; import { useGetSharedLinkQuery } from 'librechat-data-provider/react-query'; -import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; +import { OGDialogTemplate, Button, Spinner, OGDialog } from '@librechat/client'; import { useLocalize, useCopyToClipboard } from '~/hooks'; -import { Button, Spinner, OGDialog } from '~/components'; import SharedLinkButton from './SharedLinkButton'; import { cn } from '~/utils'; diff --git a/client/src/components/Conversations/ConvoOptions/SharedLinkButton.tsx b/client/src/components/Conversations/ConvoOptions/SharedLinkButton.tsx index 425551574..35609ba5e 100644 --- a/client/src/components/Conversations/ConvoOptions/SharedLinkButton.tsx +++ b/client/src/components/Conversations/ConvoOptions/SharedLinkButton.tsx @@ -1,15 +1,21 @@ import { useState, useCallback } from 'react'; import { QrCode, RotateCw, Trash2 } from 'lucide-react'; +import { + Button, + OGDialog, + Spinner, + TooltipAnchor, + Label, + OGDialogTemplate, + useToastContext, +} from '@librechat/client'; import type { TSharedLinkGetResponse } from 'librechat-data-provider'; import { useCreateSharedLinkMutation, useUpdateSharedLinkMutation, useDeleteSharedLinkMutation, } from '~/data-provider'; -import { Button, OGDialog, Spinner, TooltipAnchor, Label } from '~/components'; -import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import { NotificationSeverity } from '~/common'; -import { useToastContext } from '~/Providers'; import { useLocalize } from '~/hooks'; export default function SharedLinkButton({ diff --git a/client/src/components/Endpoints/Icon.tsx b/client/src/components/Endpoints/Icon.tsx index aa9f4d16c..acc602769 100644 --- a/client/src/components/Endpoints/Icon.tsx +++ b/client/src/components/Endpoints/Icon.tsx @@ -1,11 +1,11 @@ import React, { memo, useState } from 'react'; +import { UserIcon } from '@librechat/client'; import type { TUser } from 'librechat-data-provider'; import type { IconProps } from '~/common'; import MessageEndpointIcon from './MessageEndpointIcon'; import { useAuthContext } from '~/hooks/AuthContext'; import useAvatar from '~/hooks/Messages/useAvatar'; -import useLocalize from '~/hooks/useLocalize'; -import { UserIcon } from '~/components/svg'; +import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; type UserAvatarProps = { @@ -48,15 +48,15 @@ const UserAvatar = memo(({ size, user, avatarSrc, username, className }: UserAva > {(!(user?.avatar ?? '') && (!(user?.username ?? '') || user?.username.trim() === '')) || imageError ? ( - renderDefaultAvatar() - ) : ( - avatar - )} + renderDefaultAvatar() + ) : ( + avatar + )} ); }); diff --git a/client/src/components/Endpoints/MessageEndpointIcon.tsx b/client/src/components/Endpoints/MessageEndpointIcon.tsx index e405c01d3..1eb859c06 100644 --- a/client/src/components/Endpoints/MessageEndpointIcon.tsx +++ b/client/src/components/Endpoints/MessageEndpointIcon.tsx @@ -12,7 +12,7 @@ import { AnthropicIcon, AzureMinimalIcon, CustomMinimalIcon, -} from '~/components/svg'; +} from '@librechat/client'; import UnknownIcon from '~/hooks/Endpoint/UnknownIcon'; import { IconProps } from '~/common'; import { cn } from '~/utils'; diff --git a/client/src/components/Endpoints/MinimalIcon.tsx b/client/src/components/Endpoints/MinimalIcon.tsx index 953859c43..f21cad1e0 100644 --- a/client/src/components/Endpoints/MinimalIcon.tsx +++ b/client/src/components/Endpoints/MinimalIcon.tsx @@ -10,7 +10,7 @@ import { AnthropicIcon, BedrockIcon, Sparkles, -} from '~/components/svg'; +} from '@librechat/client'; import UnknownIcon from '~/hooks/Endpoint/UnknownIcon'; import { IconProps } from '~/common'; import { cn } from '~/utils'; diff --git a/client/src/components/Endpoints/SaveAsPresetDialog.tsx b/client/src/components/Endpoints/SaveAsPresetDialog.tsx index b827a2d8c..6467a4a40 100644 --- a/client/src/components/Endpoints/SaveAsPresetDialog.tsx +++ b/client/src/components/Endpoints/SaveAsPresetDialog.tsx @@ -1,11 +1,9 @@ import React, { useEffect, useState } from 'react'; import { useCreatePresetMutation } from 'librechat-data-provider/react-query'; +import { OGDialogTemplate, OGDialog, Input, Label, useToastContext } from '@librechat/client'; import type { TEditPresetProps } from '~/common'; -import { cn, removeFocusOutlines, cleanupPreset, defaultTextProps } from '~/utils/'; -import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; -import { OGDialog, Input, Label } from '~/components/ui/'; +import { cn, removeFocusOutlines, cleanupPreset, defaultTextProps } from '~/utils'; import { NotificationSeverity } from '~/common'; -import { useToastContext } from '~/Providers'; import { useLocalize } from '~/hooks'; const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => { @@ -23,7 +21,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => }); const toastTitle = - _preset.title ?? '' ? `\`${_preset.title}\`` : localize('com_endpoint_preset_title'); + (_preset.title ?? '') ? `\`${_preset.title}\`` : localize('com_endpoint_preset_title'); createPresetMutation.mutate(_preset, { onSuccess: () => { @@ -76,7 +74,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) => aria-label={localize('com_endpoint_preset_name')} className={cn( defaultTextProps, - 'flex h-10 max-h-10 w-full resize-none border-border-medium px-3 py-2 ', + 'flex h-10 max-h-10 w-full resize-none border-border-medium px-3 py-2', removeFocusOutlines, )} /> diff --git a/client/src/components/Endpoints/Settings/Advanced.tsx b/client/src/components/Endpoints/Settings/Advanced.tsx index 601e9a675..d0beaa902 100644 --- a/client/src/components/Endpoints/Settings/Advanced.tsx +++ b/client/src/components/Endpoints/Settings/Advanced.tsx @@ -1,7 +1,5 @@ import TextareaAutosize from 'react-textarea-autosize'; import { ImageDetail, imageDetailNumeric, imageDetailValue } from 'librechat-data-provider'; -import type { ValueType } from '@rc-component/mini-decimal'; -import type { TModelSelectProps } from '~/common'; import { Input, Label, @@ -10,8 +8,10 @@ import { HoverCard, InputNumber, HoverCardTrigger, -} from '~/components/ui'; -import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/'; +} from '@librechat/client'; +import type { ValueType } from '@rc-component/mini-decimal'; +import type { TModelSelectProps } from '~/common'; +import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils'; import { useLocalize, useDebouncedInput } from '~/hooks'; import OptionHover from './OptionHover'; import { ESide } from '~/common'; @@ -109,7 +109,7 @@ export default function Settings({ placeholder={localize('com_endpoint_openai_prompt_prefix_placeholder')} className={cn( defaultTextProps, - 'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2 ', + 'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2', )} /> diff --git a/client/src/components/Endpoints/Settings/AgentSettings.tsx b/client/src/components/Endpoints/Settings/AgentSettings.tsx index 8ba20f250..f41a8bc19 100644 --- a/client/src/components/Endpoints/Settings/AgentSettings.tsx +++ b/client/src/components/Endpoints/Settings/AgentSettings.tsx @@ -1,4 +1,3 @@ -import type { TModelSelectProps } from '~/common'; import { Switch, Label, @@ -7,7 +6,8 @@ import { InputNumber, SelectDropDown, HoverCardTrigger, -} from '~/components'; +} from '@librechat/client'; +import type { TModelSelectProps } from '~/common'; import { cn, optionText, defaultTextProps, removeFocusRings } from '~/utils'; import OptionHover from './OptionHover'; import { useLocalize } from '~/hooks'; diff --git a/client/src/components/Endpoints/Settings/Assistants.tsx b/client/src/components/Endpoints/Settings/Assistants.tsx index a35f9f7eb..1ae0f8cb4 100644 --- a/client/src/components/Endpoints/Settings/Assistants.tsx +++ b/client/src/components/Endpoints/Settings/Assistants.tsx @@ -1,5 +1,6 @@ import { useState, useMemo, useEffect } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; +import { Label, HoverCard, SelectDropDown, HoverCardTrigger } from '@librechat/client'; import type { Assistant, TPreset } from 'librechat-data-provider'; import type { TModelSelectProps, Option } from '~/common'; import { @@ -9,7 +10,6 @@ import { mapAssistants, createDropdownSetter, } from '~/utils'; -import { Label, HoverCard, SelectDropDown, HoverCardTrigger } from '~/components/ui'; import { useLocalize, useDebouncedInput, useAssistantListMap } from '~/hooks'; import OptionHover from './OptionHover'; import { ESide } from '~/common'; @@ -121,6 +121,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
@@ -178,7 +179,7 @@ export default function Settings({ conversation, setOption, models, readonly }: placeholder={localize('com_endpoint_instructions_assistants_placeholder')} className={cn( defaultTextProps, - 'flex max-h-[240px] min-h-[80px] w-full resize-none px-3 py-2 ', + 'flex max-h-[240px] min-h-[80px] w-full resize-none px-3 py-2', )} /> diff --git a/client/src/components/Endpoints/Settings/Examples.tsx b/client/src/components/Endpoints/Settings/Examples.tsx index 98dac9ea9..10f623685 100644 --- a/client/src/components/Endpoints/Settings/Examples.tsx +++ b/client/src/components/Endpoints/Settings/Examples.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { Plus, Minus } from 'lucide-react'; +import { Button, Label } from '@librechat/client'; import TextareaAutosize from 'react-textarea-autosize'; import type { TExample } from 'librechat-data-provider'; import type { TSetExample } from '~/common'; -import { Button, Label } from '~/components/ui'; import { cn, defaultTextProps } from '~/utils/'; import { useLocalize } from '~/hooks'; @@ -42,7 +42,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }: placeholder="Set example input. Example is ignored if empty." className={cn( defaultTextProps, - 'flex max-h-[138px] min-h-[75px] w-full resize-none px-3 py-2 ', + 'flex max-h-[138px] min-h-[75px] w-full resize-none px-3 py-2', )} /> @@ -67,7 +67,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }: placeholder={'Set example output. Example is ignored if empty.'} className={cn( defaultTextProps, - 'flex max-h-[300px] min-h-[75px] w-full resize-none px-3 py-2 ', + 'flex max-h-[300px] min-h-[75px] w-full resize-none px-3 py-2', )} /> diff --git a/client/src/components/Endpoints/Settings/Google.tsx b/client/src/components/Endpoints/Settings/Google.tsx index 063d5f098..6e513c179 100644 --- a/client/src/components/Endpoints/Settings/Google.tsx +++ b/client/src/components/Endpoints/Settings/Google.tsx @@ -1,6 +1,5 @@ import TextareaAutosize from 'react-textarea-autosize'; import { EModelEndpoint, endpointSettings } from 'librechat-data-provider'; -import type { TModelSelectProps, OnInputNumberChange } from '~/common'; import { Input, Label, @@ -9,7 +8,8 @@ import { InputNumber, SelectDropDown, HoverCardTrigger, -} from '~/components/ui'; +} from '@librechat/client'; +import type { TModelSelectProps, OnInputNumberChange } from '~/common'; import { cn, defaultTextProps, optionText, removeFocusOutlines, removeFocusRings } from '~/utils'; import OptionHoverAlt from '~/components/SidePanel/Parameters/OptionHover'; import { useLocalize, useDebouncedInput } from '~/hooks'; @@ -55,6 +55,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
@@ -221,7 +222,7 @@ export default function Settings({ conversation, setOption, models, readonly }: {localize('com_endpoint_max_output_tokens')}{' '} - ({localize('com_endpoint_default_with_num', { 0: google.maxOutputTokens.default + '' })}) + ( + {localize('com_endpoint_default_with_num', { + 0: google.maxOutputTokens.default + '', + })} + )
diff --git a/client/src/components/Files/ActionButton.tsx b/client/src/components/Files/ActionButton.tsx index 03f58da6c..0d4c59f8d 100644 --- a/client/src/components/Files/ActionButton.tsx +++ b/client/src/components/Files/ActionButton.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Button } from '~/components/ui'; +import { Button } from '@librechat/client'; import { useLocalize } from '~/hooks'; type ActionButtonProps = { diff --git a/client/src/components/Files/DeleteIconButton.tsx b/client/src/components/Files/DeleteIconButton.tsx index 50ac461ba..0173a3d57 100644 --- a/client/src/components/Files/DeleteIconButton.tsx +++ b/client/src/components/Files/DeleteIconButton.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { CrossIcon, TrashIcon } from '~/components/svg'; -import { Button } from '~/components/ui'; +import { Button, TrashIcon } from '@librechat/client'; type DeleteIconButtonProps = { onClick: () => void; diff --git a/client/src/components/Files/FileList/DataTableFile.tsx b/client/src/components/Files/FileList/DataTableFile.tsx index d64699e2e..269001d0a 100644 --- a/client/src/components/Files/FileList/DataTableFile.tsx +++ b/client/src/components/Files/FileList/DataTableFile.tsx @@ -16,27 +16,28 @@ import type { ColumnFiltersState, } from '@tanstack/react-table'; import { FileContext } from 'librechat-data-provider'; -import type { AugmentedColumnDef } from '~/common'; -import type { TFile } from 'librechat-data-provider'; import { Input, Table, Button, + Spinner, TableRow, TableBody, TableCell, TableHead, + TrashIcon, TableHeader, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuCheckboxItem, -} from '~/components/ui'; +} from '@librechat/client'; +import type { TFile } from 'librechat-data-provider'; +import type { AugmentedColumnDef } from '~/common'; import ActionButton from '~/components/Files/ActionButton'; import { useDeleteFilesFromTable } from '~/hooks/Files'; -import { TrashIcon, Spinner } from '~/components/svg'; import UploadFileButton from './UploadFileButton'; -import useLocalize from '~/hooks/useLocalize'; +import { useLocalize } from '~/hooks'; import store from '~/store'; interface DataTableProps { diff --git a/client/src/components/Files/FileList/FileListItem.tsx b/client/src/components/Files/FileList/FileListItem.tsx index 5760b2917..e7ee5d1d2 100644 --- a/client/src/components/Files/FileList/FileListItem.tsx +++ b/client/src/components/Files/FileList/FileListItem.tsx @@ -1,7 +1,6 @@ -import type { TFile } from 'librechat-data-provider'; import React from 'react'; -import { TrashIcon } from '~/components/svg'; -import { Button } from '~/components/ui'; +import { Button, TrashIcon } from '@librechat/client'; +import type { TFile } from 'librechat-data-provider'; type FileListItemProps = { file: TFile; diff --git a/client/src/components/Files/FileList/FileListItem2.tsx b/client/src/components/Files/FileList/FileListItem2.tsx index 2b8ff1d6a..b692a6962 100644 --- a/client/src/components/Files/FileList/FileListItem2.tsx +++ b/client/src/components/Files/FileList/FileListItem2.tsx @@ -1,9 +1,8 @@ -import type { TFile } from 'librechat-data-provider'; -import { FileIcon, PlusIcon } from 'lucide-react'; import React from 'react'; +import { FileIcon, PlusIcon } from 'lucide-react'; +import { Button, DotsIcon, TrashIcon } from '@librechat/client'; +import type { TFile } from 'librechat-data-provider'; import { useNavigate } from 'react-router-dom'; -import { DotsIcon, TrashIcon } from '~/components/svg'; -import { Button } from '~/components/ui'; type FileListItemProps = { file: TFile; diff --git a/client/src/components/Files/FileList/FilePreview.tsx b/client/src/components/Files/FileList/FilePreview.tsx index e0c2624b3..0b416fa70 100644 --- a/client/src/components/Files/FileList/FilePreview.tsx +++ b/client/src/components/Files/FileList/FilePreview.tsx @@ -1,11 +1,10 @@ -import { TFile } from 'librechat-data-provider/dist/types'; import React, { useState } from 'react'; -import { TThread, TVectorStore } from '~/common'; -import { CheckMark, TrashIcon } from '~/components/svg'; -import { Button } from '~/components/ui'; -import DeleteIconButton from '../DeleteIconButton'; +import { TFile } from 'librechat-data-provider/dist/types'; +import { CheckMark, TrashIcon, Button } from '@librechat/client'; import VectorStoreButton from '../VectorStore/VectorStoreButton'; import { CircleIcon, Clock3Icon, InfoIcon } from 'lucide-react'; +import DeleteIconButton from '../DeleteIconButton'; +import { TThread, TVectorStore } from '~/common'; import { useParams } from 'react-router-dom'; const tempFile: TFile = { diff --git a/client/src/components/Files/FileList/FileSidePanel.tsx b/client/src/components/Files/FileList/FileSidePanel.tsx index e1552d2a8..5b5967842 100644 --- a/client/src/components/Files/FileList/FileSidePanel.tsx +++ b/client/src/components/Files/FileList/FileSidePanel.tsx @@ -1,10 +1,9 @@ import React from 'react'; import FileList from './FileList'; +import { Button, Input } from '@librechat/client'; import { TFile } from 'librechat-data-provider/dist/types'; -import FilesSectionSelector from '../FilesSectionSelector'; -import { Button, Input } from '~/components/ui'; -import { ListFilter } from 'lucide-react'; import UploadFileButton from './UploadFileButton'; +import { ListFilter } from 'lucide-react'; import { useLocalize } from '~/hooks'; const fakeFiles = [ diff --git a/client/src/components/Files/FileList/FileTableColumns.tsx b/client/src/components/Files/FileList/FileTableColumns.tsx index 8e670aa80..754cb061d 100644 --- a/client/src/components/Files/FileList/FileTableColumns.tsx +++ b/client/src/components/Files/FileList/FileTableColumns.tsx @@ -1,13 +1,10 @@ /* eslint-disable react-hooks/rules-of-hooks */ -import { FileSources, FileContext } from 'librechat-data-provider'; +import { PlusIcon } from 'lucide-react'; +import { Button, Checkbox, DotsIcon, FileIcon } from '@librechat/client'; import type { ColumnDef } from '@tanstack/react-table'; import type { TFile } from 'librechat-data-provider'; -import { CrossIcon, DotsIcon } from '~/components/svg'; -import { Button, Checkbox } from '~/components/ui'; import { formatDate, getFileType } from '~/utils'; -import useLocalize from '~/hooks/useLocalize'; -import FileIcon from '~/components/svg/Files/FileIcon'; -import { PlusIcon } from 'lucide-react'; +import { useLocalize } from '~/hooks'; export const fileTableColumns: ColumnDef[] = [ { diff --git a/client/src/components/Files/FileList/UploadFileButton.tsx b/client/src/components/Files/FileList/UploadFileButton.tsx index e2b8acdd0..0823f706e 100644 --- a/client/src/components/Files/FileList/UploadFileButton.tsx +++ b/client/src/components/Files/FileList/UploadFileButton.tsx @@ -1,6 +1,6 @@ -import { PlusIcon } from 'lucide-react'; import React from 'react'; -import { Button } from '~/components/ui'; +import { PlusIcon } from 'lucide-react'; +import { Button } from '@librechat/client'; type UploadFileProps = { onClick: () => void; diff --git a/client/src/components/Files/FileList/UploadFileModal.tsx b/client/src/components/Files/FileList/UploadFileModal.tsx index 4e2e31e27..7be59184a 100644 --- a/client/src/components/Files/FileList/UploadFileModal.tsx +++ b/client/src/components/Files/FileList/UploadFileModal.tsx @@ -1,6 +1,5 @@ import React, { useState, ChangeEvent } from 'react'; -import AttachFile from '~/components/Chat/Input/Files/AttachFile'; -import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input } from '~/components/ui'; +import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input } from '@librechat/client'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; diff --git a/client/src/components/Files/VectorStore/VectorStoreButton.tsx b/client/src/components/Files/VectorStore/VectorStoreButton.tsx index dc3e34c4f..6eb0f35a2 100644 --- a/client/src/components/Files/VectorStore/VectorStoreButton.tsx +++ b/client/src/components/Files/VectorStore/VectorStoreButton.tsx @@ -1,6 +1,6 @@ -import { PlusIcon } from 'lucide-react'; import React from 'react'; -import { Button } from '~/components/ui'; +import { PlusIcon } from 'lucide-react'; +import { Button } from '@librechat/client'; type VectorStoreButtonProps = { onClick: () => void; diff --git a/client/src/components/Files/VectorStore/VectorStoreListItem.tsx b/client/src/components/Files/VectorStore/VectorStoreListItem.tsx index 720ebd0af..66a54b159 100644 --- a/client/src/components/Files/VectorStore/VectorStoreListItem.tsx +++ b/client/src/components/Files/VectorStore/VectorStoreListItem.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; +import { Button, DotsIcon, TrashIcon } from '@librechat/client'; import { TVectorStore } from '~/common'; -import { DotsIcon, TrashIcon } from '~/components/svg'; -import { Button } from '~/components/ui'; type VectorStoreListItemProps = { vectorStore: TVectorStore; diff --git a/client/src/components/Files/VectorStore/VectorStorePreview.tsx b/client/src/components/Files/VectorStore/VectorStorePreview.tsx index c1d70107f..d75a82742 100644 --- a/client/src/components/Files/VectorStore/VectorStorePreview.tsx +++ b/client/src/components/Files/VectorStore/VectorStorePreview.tsx @@ -1,7 +1,6 @@ import React, { useState } from 'react'; import DeleteIconButton from '../DeleteIconButton'; -import { Button } from '~/components/ui'; -import { TrashIcon } from '~/components/svg'; +import { TrashIcon, Button } from '@librechat/client'; import { TFile } from 'librechat-data-provider/dist/types'; import UploadFileButton from '../FileList/UploadFileButton'; import UploadFileModal from '../FileList/UploadFileModal'; @@ -99,7 +98,7 @@ export default function VectorStorePreview() { const params = useParams(); return ( -
+
VECTOR STORE diff --git a/client/src/components/Files/VectorStore/VectorStoreSidePanel.tsx b/client/src/components/Files/VectorStore/VectorStoreSidePanel.tsx index b1d0fe166..0616bcc7b 100644 --- a/client/src/components/Files/VectorStore/VectorStoreSidePanel.tsx +++ b/client/src/components/Files/VectorStore/VectorStoreSidePanel.tsx @@ -1,12 +1,9 @@ import React from 'react'; +import { ListFilter } from 'lucide-react'; +import { Button, Input } from '@librechat/client'; +import VectorStoreButton from './VectorStoreButton'; import VectorStoreList from './VectorStoreList'; import { TVectorStore } from '~/common'; -import VectorStoreButton from './VectorStoreButton'; -import { Button, Input } from '~/components/ui'; -import FilesSectionSelector from '../FilesSectionSelector'; -import ActionButton from '../ActionButton'; -import DeleteIconButton from '../DeleteIconButton'; -import { ListFilter } from 'lucide-react'; import { useLocalize } from '~/hooks'; const fakeVectorStores: TVectorStore[] = [ diff --git a/client/src/components/Input/Generations/Regenerate.tsx b/client/src/components/Input/Generations/Regenerate.tsx index 83eabf493..715168cb9 100644 --- a/client/src/components/Input/Generations/Regenerate.tsx +++ b/client/src/components/Input/Generations/Regenerate.tsx @@ -1,7 +1,7 @@ +import { RegenerateIcon } from '@librechat/client'; import type { TGenButtonProps } from '~/common'; -import { RegenerateIcon } from '~/components/svg'; -import Button from './Button'; import { useLocalize } from '~/hooks'; +import Button from './Button'; export default function Regenerate({ onClick }: TGenButtonProps) { const localize = useLocalize(); diff --git a/client/src/components/Input/Generations/Stop.tsx b/client/src/components/Input/Generations/Stop.tsx index 8214152c4..d66baa945 100644 --- a/client/src/components/Input/Generations/Stop.tsx +++ b/client/src/components/Input/Generations/Stop.tsx @@ -1,14 +1,14 @@ +import { StopGeneratingIcon } from '@librechat/client'; import type { TGenButtonProps } from '~/common'; -import { StopGeneratingIcon } from '~/components/svg'; -import Button from './Button'; import { useLocalize } from '~/hooks'; +import Button from './Button'; export default function Stop({ onClick }: TGenButtonProps) { const localize = useLocalize(); return ( ); diff --git a/client/src/components/Input/ModelSelect/Anthropic.tsx b/client/src/components/Input/ModelSelect/Anthropic.tsx index bbe634478..38669871e 100644 --- a/client/src/components/Input/ModelSelect/Anthropic.tsx +++ b/client/src/components/Input/ModelSelect/Anthropic.tsx @@ -1,4 +1,4 @@ -import { SelectDropDown, SelectDropDownPop } from '~/components/ui'; +import { SelectDropDown, SelectDropDownPop } from '@librechat/client'; import type { TModelSelectProps } from '~/common'; import { cn, cardStyle } from '~/utils/'; diff --git a/client/src/components/Input/ModelSelect/ChatGPT.tsx b/client/src/components/Input/ModelSelect/ChatGPT.tsx index 89c884429..1405bcdbd 100644 --- a/client/src/components/Input/ModelSelect/ChatGPT.tsx +++ b/client/src/components/Input/ModelSelect/ChatGPT.tsx @@ -1,4 +1,4 @@ -import { SelectDropDown, SelectDropDownPop } from '~/components/ui'; +import { SelectDropDown, SelectDropDownPop } from '@librechat/client'; import type { TModelSelectProps } from '~/common'; import { cn, cardStyle } from '~/utils/'; diff --git a/client/src/components/Input/ModelSelect/Google.tsx b/client/src/components/Input/ModelSelect/Google.tsx index eab7f9539..eb04adc9e 100644 --- a/client/src/components/Input/ModelSelect/Google.tsx +++ b/client/src/components/Input/ModelSelect/Google.tsx @@ -1,4 +1,4 @@ -import { SelectDropDown, SelectDropDownPop } from '~/components/ui'; +import { SelectDropDown, SelectDropDownPop } from '@librechat/client'; import type { TModelSelectProps } from '~/common'; import { cn, cardStyle } from '~/utils/'; diff --git a/client/src/components/ui/MultiSelectDropDown.tsx b/client/src/components/Input/ModelSelect/MultiSelectDropDown.tsx similarity index 97% rename from client/src/components/ui/MultiSelectDropDown.tsx rename to client/src/components/Input/ModelSelect/MultiSelectDropDown.tsx index 0e2724dcf..6a1deee15 100644 --- a/client/src/components/ui/MultiSelectDropDown.tsx +++ b/client/src/components/Input/ModelSelect/MultiSelectDropDown.tsx @@ -1,4 +1,6 @@ import React, { useState, useRef } from 'react'; +import { Wrench, ArrowRight } from 'lucide-react'; +import { CheckMark, useOnClickOutside, useMultiSearch } from '@librechat/client'; import { Listbox, ListboxButton, @@ -7,12 +9,8 @@ import { ListboxOption, Transition, } from '@headlessui/react'; -import { Wrench, ArrowRight } from 'lucide-react'; -import { CheckMark } from '~/components/svg'; -import useOnClickOutside from '~/hooks/useOnClickOutside'; -import { useMultiSearch } from './MultiSearch'; -import { cn } from '~/utils/'; import type { TPlugin } from 'librechat-data-provider'; +import { cn } from '~/utils/'; export type TMultiSelectDropDownProps = { title?: string; @@ -142,7 +140,7 @@ function MultiSelectDropDown({ viewBox="0 0 24 24" strokeLinecap="round" strokeLinejoin="round" - className="h-4 w-4 text-gray-400" + className="h-4 w-4 text-gray-400" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" diff --git a/client/src/components/ui/MultiSelectPop.tsx b/client/src/components/Input/ModelSelect/MultiSelectPop.tsx similarity index 95% rename from client/src/components/ui/MultiSelectPop.tsx rename to client/src/components/Input/ModelSelect/MultiSelectPop.tsx index 3f4377291..8421c673a 100644 --- a/client/src/components/ui/MultiSelectPop.tsx +++ b/client/src/components/Input/ModelSelect/MultiSelectPop.tsx @@ -1,8 +1,8 @@ import { Wrench } from 'lucide-react'; +import { useMultiSearch } from '@librechat/client'; import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover'; import type { TPlugin } from 'librechat-data-provider'; import MenuItem from '~/components/Chat/Menus/UI/MenuItem'; -import { useMultiSearch } from './MultiSearch'; import { cn } from '~/utils/'; type SelectDropDownProps = { @@ -32,8 +32,6 @@ function MultiSelectPop({ optionValueKey = 'value', searchPlaceholder, }: SelectDropDownProps) { - // const localize = useLocalize(); - const title = _title; const excludeIds = ['select-plugin', 'plugins-label', 'selected-plugins']; @@ -54,14 +52,14 @@ function MultiSelectPop({ - -
- )} -
-
-
- setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} - className={`slider-thumb h-2 w-full appearance-none rounded-lg bg-gradient-to-r from-gray-500 to-gray-500 bg-no-repeat focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${ - disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer' - }`} - tabIndex={0} - style={{ - backgroundSize: '50% 100%', - backgroundPosition: 'left', - }} - aria-valuemin={min} - aria-valuemax={max} - aria-valuenow={value} - aria-valuetext={`${value.toFixed(decimalPlaces).replace('-0.00', '0.00')}`} - disabled={disabled} - /> - {isHovering ? ( -
- {min} - {max} -
- ) : ( -
- )} -
-
- ); -}; - -export default React.memo(ModelParameters); diff --git a/client/src/components/ui/Prompt.tsx b/client/src/components/ui/Prompt.tsx deleted file mode 100644 index 3453ce436..000000000 --- a/client/src/components/ui/Prompt.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useLocalize } from '~/hooks'; - -export default function Prompt({ title, prompt }: { title: string; prompt: string }) { - const localize = useLocalize(); - - return ( -
-

- {title} -

- - {localize('com_ui_use_prompt')} → -
- ); -} diff --git a/client/src/components/ui/Slider.tsx b/client/src/components/ui/Slider.tsx deleted file mode 100644 index 74908b4e6..000000000 --- a/client/src/components/ui/Slider.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import * as SliderPrimitive from '@radix-ui/react-slider'; -import { cn } from '~/utils'; - -const Slider = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { onDoubleClick?: () => void } - >(({ className, onDoubleClick, ...props }, ref) => ( - - - - - - - )); -Slider.displayName = SliderPrimitive.Root.displayName; - -export { Slider }; diff --git a/client/src/components/ui/TermsAndConditionsModal.tsx b/client/src/components/ui/TermsAndConditionsModal.tsx index bdf6e8fb3..0736e422f 100644 --- a/client/src/components/ui/TermsAndConditionsModal.tsx +++ b/client/src/components/ui/TermsAndConditionsModal.tsx @@ -1,10 +1,8 @@ import { useMemo } from 'react'; +import { OGDialog, DialogTemplate, useToastContext } from '@librechat/client'; import type { TTermsOfService } from 'librechat-data-provider'; import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite'; -import DialogTemplate from '~/components/ui/DialogTemplate'; import { useAcceptTermsMutation } from '~/data-provider'; -import { useToastContext } from '~/Providers'; -import { OGDialog } from '~/components/ui'; import { useLocalize } from '~/hooks'; const TermsAndConditionsModal = ({ @@ -73,7 +71,7 @@ const TermsAndConditionsModal = ({ main={
{ const localize = useLocalize(); diff --git a/client/src/hooks/Files/useFileHandling.ts b/client/src/hooks/Files/useFileHandling.ts index 343841776..8c8f33e57 100644 --- a/client/src/hooks/Files/useFileHandling.ts +++ b/client/src/hooks/Files/useFileHandling.ts @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { v4 } from 'uuid'; +import { useToastContext } from '@librechat/client'; import { useQueryClient } from '@tanstack/react-query'; import { QueryKeys, @@ -17,7 +18,6 @@ import { useGetFileConfig, useUploadFileMutation } from '~/data-provider'; import useLocalize, { TranslationKeys } from '~/hooks/useLocalize'; import { useDelayedUploadToast } from './useDelayedUploadToast'; import { processFileForUpload } from '~/utils/heicConverter'; -import { useToastContext } from '~/Providers/ToastContext'; import { useChatContext } from '~/Providers/ChatContext'; import { logger, validateFiles } from '~/utils'; import useClientResize from './useClientResize'; diff --git a/client/src/hooks/Input/index.ts b/client/src/hooks/Input/index.ts index bfc40ebb3..dc959e357 100644 --- a/client/src/hooks/Input/index.ts +++ b/client/src/hooks/Input/index.ts @@ -2,7 +2,6 @@ export * from './useAutoSave'; export { default as useUserKey } from './useUserKey'; export { default as useDebounce } from './useDebounce'; export { default as useTextarea } from './useTextarea'; -export { default as useCombobox } from './useCombobox'; export { default as useQueryParams } from './useQueryParams'; export { default as useHandleKeyUp } from './useHandleKeyUp'; export { default as useRequiresKey } from './useRequiresKey'; diff --git a/client/src/hooks/Input/useSpeechToTextBrowser.ts b/client/src/hooks/Input/useSpeechToTextBrowser.ts index 1d31c3348..d4c301367 100644 --- a/client/src/hooks/Input/useSpeechToTextBrowser.ts +++ b/client/src/hooks/Input/useSpeechToTextBrowser.ts @@ -1,8 +1,8 @@ import { useEffect, useRef, useMemo } from 'react'; import { useRecoilState } from 'recoil'; +import { useToastContext } from '@librechat/client'; import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition'; import useGetAudioSettings from './useGetAudioSettings'; -import { useToastContext } from '~/Providers'; import store from '~/store'; const useSpeechToTextBrowser = ( diff --git a/client/src/hooks/Input/useSpeechToTextExternal.ts b/client/src/hooks/Input/useSpeechToTextExternal.ts index 3160696e9..6571a0911 100644 --- a/client/src/hooks/Input/useSpeechToTextExternal.ts +++ b/client/src/hooks/Input/useSpeechToTextExternal.ts @@ -1,8 +1,8 @@ import { useState, useEffect, useRef } from 'react'; import { useRecoilState } from 'recoil'; +import { useToastContext } from '@librechat/client'; import { useSpeechToTextMutation } from '~/data-provider'; import useGetAudioSettings from './useGetAudioSettings'; -import { useToastContext } from '~/Providers'; import store from '~/store'; const useSpeechToTextExternal = ( @@ -107,7 +107,7 @@ const useSpeechToTextExternal = ( }); setPermission(true); audioStream.current = streamData ?? null; - } catch (err) { + } catch { setPermission(false); } }; @@ -268,6 +268,7 @@ const useSpeechToTextExternal = ( return () => { window.removeEventListener('keydown', handleKeyDown); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isListening]); return { diff --git a/client/src/hooks/Input/useTextToSpeechExternal.ts b/client/src/hooks/Input/useTextToSpeechExternal.ts index cf8edb038..99c1a2344 100644 --- a/client/src/hooks/Input/useTextToSpeechExternal.ts +++ b/client/src/hooks/Input/useTextToSpeechExternal.ts @@ -1,8 +1,8 @@ -import { useRecoilValue } from 'recoil'; import { useState, useMemo, useRef, useCallback, useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; +import { useToastContext } from '@librechat/client'; import { useTextToSpeechMutation, useVoicesQuery } from '~/data-provider'; -import { useToastContext } from '~/Providers/ToastContext'; -import useLocalize from '~/hooks/useLocalize'; +import { useLocalize } from '~/hooks'; import store from '~/store'; const createFormData = (text: string, voice: string) => { diff --git a/client/src/hooks/Input/useTextarea.ts b/client/src/hooks/Input/useTextarea.ts index 7a0711e7a..7d32cbbe0 100644 --- a/client/src/hooks/Input/useTextarea.ts +++ b/client/src/hooks/Input/useTextarea.ts @@ -16,8 +16,8 @@ import useGetSender from '~/hooks/Conversations/useGetSender'; import useFileHandling from '~/hooks/Files/useFileHandling'; import { useInteractionHealthCheck } from '~/data-provider'; import { useChatContext } from '~/Providers/ChatContext'; -import useLocalize from '~/hooks/useLocalize'; import { globalAudioId } from '~/common'; +import { useLocalize } from '~/hooks'; import store from '~/store'; type KeyEvent = KeyboardEvent; diff --git a/client/src/hooks/MCP/useMCPServerInitialization.ts b/client/src/hooks/MCP/useMCPServerInitialization.ts index ec5ab2cc0..8a73c7d49 100644 --- a/client/src/hooks/MCP/useMCPServerInitialization.ts +++ b/client/src/hooks/MCP/useMCPServerInitialization.ts @@ -1,12 +1,12 @@ import { useCallback, useState, useEffect, useMemo } from 'react'; -import { useQueryClient } from '@tanstack/react-query'; +import { useToastContext } from '@librechat/client'; import { QueryKeys } from 'librechat-data-provider'; +import { useQueryClient } from '@tanstack/react-query'; import { useReinitializeMCPServerMutation, useCancelMCPOAuthMutation, } from 'librechat-data-provider/react-query'; import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries'; -import { useToastContext } from '~/Providers'; import { useLocalize } from '~/hooks'; import { logger } from '~/utils'; diff --git a/client/src/hooks/MCP/useMCPServerManager.ts b/client/src/hooks/MCP/useMCPServerManager.ts index 95891b47e..a7d2c7c98 100644 --- a/client/src/hooks/MCP/useMCPServerManager.ts +++ b/client/src/hooks/MCP/useMCPServerManager.ts @@ -1,12 +1,12 @@ +import { useCallback, useState, useMemo, useRef } from 'react'; +import { useToastContext } from '@librechat/client'; import { useQueryClient } from '@tanstack/react-query'; import { Constants, QueryKeys } from 'librechat-data-provider'; -import { useCallback, useState, useMemo, useRef } from 'react'; import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query'; -import { useMCPServerInitialization } from '~/hooks/MCP/useMCPServerInitialization'; -import type { ConfigFieldDetail } from '~/components/ui/MCP/MCPConfigDialog'; import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider'; -import { useToastContext, useBadgeRowContext } from '~/Providers'; -import { useLocalize } from '~/hooks'; +import type { ConfigFieldDetail } from '~/components/MCP/MCPConfigDialog'; +import { useLocalize, useMCPServerInitialization } from '~/hooks'; +import { useBadgeRowContext } from '~/Providers'; export function useMCPServerManager() { const localize = useLocalize(); diff --git a/client/src/hooks/Nav/useSideNavLinks.ts b/client/src/hooks/Nav/useSideNavLinks.ts index 728b85673..734541521 100644 --- a/client/src/hooks/Nav/useSideNavLinks.ts +++ b/client/src/hooks/Nav/useSideNavLinks.ts @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import { Blocks, MCPIcon, AttachmentIcon } from '@librechat/client'; import { MessageSquareQuote, ArrowRightToLine, Settings2, Database, Bookmark } from 'lucide-react'; import { isAssistantsEndpoint, @@ -15,7 +16,6 @@ import BookmarkPanel from '~/components/SidePanel/Bookmarks/BookmarkPanel'; import MemoryViewer from '~/components/SidePanel/Memories/MemoryViewer'; import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch'; import PromptsAccordion from '~/components/Prompts/PromptsAccordion'; -import { Blocks, MCPIcon, AttachmentIcon } from '~/components/svg'; import Parameters from '~/components/SidePanel/Parameters/Panel'; import FilesPanel from '~/components/SidePanel/Files/Panel'; import MCPPanel from '~/components/SidePanel/MCP/MCPPanel'; diff --git a/client/src/hooks/Plugins/useAuthCodeTool.ts b/client/src/hooks/Plugins/useAuthCodeTool.ts index b33e05c16..f523fd01e 100644 --- a/client/src/hooks/Plugins/useAuthCodeTool.ts +++ b/client/src/hooks/Plugins/useAuthCodeTool.ts @@ -2,10 +2,8 @@ import { useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { AuthType, Tools, QueryKeys } from 'librechat-data-provider'; import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query'; -// import { useToastContext } from '~/Providers'; const useAuthCodeTool = (options?: { isEntityTool: boolean }) => { - // const { showToast } = useToastContext(); const queryClient = useQueryClient(); const isEntityTool = options?.isEntityTool ?? true; const updateUserPlugins = useUpdateUserPluginsMutation({ diff --git a/client/src/hooks/Prompts/useCategories.tsx b/client/src/hooks/Prompts/useCategories.tsx index a5083f8ed..b4611e8dc 100644 --- a/client/src/hooks/Prompts/useCategories.tsx +++ b/client/src/hooks/Prompts/useCategories.tsx @@ -1,6 +1,6 @@ import { useGetCategories } from '~/data-provider'; import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; -import useLocalize, { TranslationKeys } from '~/hooks/useLocalize'; +import { useLocalize, TranslationKeys } from '~/hooks'; const loadingCategories: { label: TranslationKeys; value: string }[] = [ { diff --git a/client/src/hooks/ScreenshotContext.tsx b/client/src/hooks/ScreenshotContext.tsx index 2f9069c5f..435327e57 100644 --- a/client/src/hooks/ScreenshotContext.tsx +++ b/client/src/hooks/ScreenshotContext.tsx @@ -1,6 +1,6 @@ import { createContext, useRef, useContext, RefObject } from 'react'; import { toCanvas } from 'html-to-image'; -import { ThemeContext } from '~/hooks/ThemeContext'; +import { ThemeContext } from '@librechat/client'; type ScreenshotContextType = { ref?: RefObject; diff --git a/client/src/hooks/index.ts b/client/src/hooks/index.ts index 83cdb7fbc..92fd39e37 100644 --- a/client/src/hooks/index.ts +++ b/client/src/hooks/index.ts @@ -8,30 +8,26 @@ export * from './Nav'; export * from './Files'; export * from './Generic'; export * from './Input'; +export * from './MCP'; export * from './Messages'; export * from './Plugins'; export * from './Prompts'; export * from './Roles'; export * from './SSE'; export * from './AuthContext'; -export * from './ThemeContext'; export * from './ScreenshotContext'; export * from './ApiErrorBoundaryContext'; export * from './Endpoint'; export type { TranslationKeys } from './useLocalize'; -export { default as useToast } from './useToast'; export { default as useTimeout } from './useTimeout'; export { default as useNewConvo } from './useNewConvo'; export { default as useLocalize } from './useLocalize'; -export { default as useMediaQuery } from './useMediaQuery'; export { default as useChatBadges } from './useChatBadges'; export { default as useScrollToRef } from './useScrollToRef'; export { default as useLocalStorage } from './useLocalStorage'; export { default as useDocumentTitle } from './useDocumentTitle'; -export { default as useDelayedRender } from './useDelayedRender'; -export { default as useOnClickOutside } from './useOnClickOutside'; export { default as useSpeechToText } from './Input/useSpeechToText'; export { default as useTextToSpeech } from './Input/useTextToSpeech'; export { default as useGenerationsByLatest } from './useGenerationsByLatest'; diff --git a/client/src/locales/Translation.spec.ts b/client/src/locales/Translation.spec.ts index 89c7b1d7b..6c6e1b7ac 100644 --- a/client/src/locales/Translation.spec.ts +++ b/client/src/locales/Translation.spec.ts @@ -1,9 +1,8 @@ -// Translation.spec.ts - import i18n from './i18n'; import English from './en/translation.json'; import French from './fr/translation.json'; import Spanish from './es/translation.json'; +import { TranslationKeys } from '~/hooks'; describe('i18next translation tests', () => { // Ensure i18next is initialized before any tests run @@ -36,7 +35,7 @@ describe('i18next translation tests', () => { it('should return the key itself for an invalid key', () => { i18n.changeLanguage('en'); - expect(i18n.t('invalid-key')).toBe('invalid-key'); // Returns the key itself + expect(i18n.t('invalid-key' as TranslationKeys)).toBe('invalid-key'); // Returns the key itself }); it('should correctly format placeholders in the translation', () => { @@ -46,4 +45,4 @@ describe('i18next translation tests', () => { i18n.changeLanguage('fr'); expect(i18n.t('com_endpoint_default_with_num', { 0: 'Marie' })).toBe('par défaut : Marie'); }); -}); \ No newline at end of file +}); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 95aa05fee..444f869dd 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1072,7 +1072,6 @@ "com_ui_use_backup_code": "Use Backup Code Instead", "com_ui_use_memory": "Use memory", "com_ui_use_micrphone": "Use microphone", - "com_ui_use_prompt": "Use prompt", "com_ui_used": "Used", "com_ui_value": "Value", "com_ui_variables": "Variables", @@ -1110,4 +1109,4 @@ "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", "com_user_message": "You" -} \ No newline at end of file +} diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index 77c6d7397..d81cbc075 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -1,4 +1,5 @@ import { useEffect } from 'react'; +import { Spinner } from '@librechat/client'; import { useParams } from 'react-router-dom'; import { Constants, EModelEndpoint } from 'librechat-data-provider'; import { useGetModelsQuery } from 'librechat-data-provider/react-query'; @@ -10,7 +11,6 @@ import { ToolCallsMapProvider } from '~/Providers'; import ChatView from '~/components/Chat/ChatView'; import useAuthRedirect from './useAuthRedirect'; import temporaryStore from '~/store/temporary'; -import { Spinner } from '~/components/svg'; import { useRecoilCallback } from 'recoil'; import store from '~/store'; diff --git a/client/src/routes/Layouts/DashBreadcrumb.tsx b/client/src/routes/Layouts/DashBreadcrumb.tsx index 0138e63ab..c6db3a18b 100644 --- a/client/src/routes/Layouts/DashBreadcrumb.tsx +++ b/client/src/routes/Layouts/DashBreadcrumb.tsx @@ -14,7 +14,7 @@ import { // DropdownMenuItem, // DropdownMenuContent, // DropdownMenuTrigger, -} from '~/components/ui'; +} from '@librechat/client'; import { useLocalize, useCustomLink, useAuthContext } from '~/hooks'; import AdvancedSwitch from '~/components/Prompts/AdvancedSwitch'; // import { RightPanel } from '../../components/Prompts/RightPanel'; diff --git a/client/src/routes/Root.tsx b/client/src/routes/Root.tsx index 42ef88c34..bede0ba54 100644 --- a/client/src/routes/Root.tsx +++ b/client/src/routes/Root.tsx @@ -14,8 +14,8 @@ import { FileMapContext, SetConvoProvider, } from '~/Providers'; -import TermsAndConditionsModal from '~/components/ui/TermsAndConditionsModal'; import { useUserTermsQuery, useGetStartupConfig } from '~/data-provider'; +import { TermsAndConditionsModal } from '~/components/ui'; import { Nav, MobileNav } from '~/components/Nav'; import { useHealthCheck } from '~/data-provider'; import { Banner } from '~/components/Banners'; diff --git a/client/src/routes/RouteErrorBoundary.tsx b/client/src/routes/RouteErrorBoundary.tsx index 6221ab25b..f112e29d7 100644 --- a/client/src/routes/RouteErrorBoundary.tsx +++ b/client/src/routes/RouteErrorBoundary.tsx @@ -1,5 +1,6 @@ +/* eslint-disable i18next/no-literal-string */ +import { Button } from '@librechat/client'; import { useRouteError } from 'react-router-dom'; -import { Button } from '~/components/ui'; import logger from '~/utils/logger'; interface UserAgentData { diff --git a/client/src/routes/Search.tsx b/client/src/routes/Search.tsx index 9508806df..f7992745c 100644 --- a/client/src/routes/Search.tsx +++ b/client/src/routes/Search.tsx @@ -1,11 +1,11 @@ import { useEffect, useMemo } from 'react'; import { useRecoilValue } from 'recoil'; +import { Spinner, useToastContext } from '@librechat/client'; import MinimalMessagesWrapper from '~/components/Chat/Messages/MinimalMessages'; import { useNavScrolling, useLocalize, useAuthContext } from '~/hooks'; import SearchMessage from '~/components/Chat/Messages/SearchMessage'; -import { useToastContext, useFileMapContext } from '~/Providers'; import { useMessagesInfiniteQuery } from '~/data-provider'; -import { Spinner } from '~/components'; +import { useFileMapContext } from '~/Providers'; import { buildTree } from '~/utils'; import store from '~/store'; @@ -23,7 +23,7 @@ export default function Search() { isError, fetchNextPage, isFetchingNextPage, - hasNextPage, + hasNextPage: _hasNextPage, } = useMessagesInfiniteQuery( { search: searchQuery || undefined, diff --git a/client/src/style.css b/client/src/style.css index 4139dfd04..d401bf5a1 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -51,6 +51,7 @@ --amber-800: #92400e; --amber-900: #78350f; --amber-950: #451a03; + --brand-purple: #ab68ff; --gizmo-gray-500: #999; --gizmo-gray-600: #666; --gizmo-gray-950: #0f0f0f; @@ -61,6 +62,7 @@ --font-size-xl: 1.25rem; } html { + --brand-purple: #ab68ff; --presentation: var(--white); --text-primary: var(--gray-800); --text-secondary: var(--gray-600); @@ -121,6 +123,7 @@ html { --switch-unchecked: 0 0% 58%; } .dark { + --brand-purple: #ab68ff; --presentation: var(--gray-800); --text-primary: var(--gray-100); --text-secondary: var(--gray-300); diff --git a/client/src/utils/files.ts b/client/src/utils/files.ts index 3ca07e51d..e98909012 100644 --- a/client/src/utils/files.ts +++ b/client/src/utils/files.ts @@ -1,3 +1,4 @@ +import { SheetPaths, TextPaths, FilePaths, CodePaths } from '~/components/svg'; import { megabyte, QueryKeys, @@ -8,10 +9,6 @@ import { import type { TFile, EndpointFileConfig } from 'librechat-data-provider'; import type { QueryClient } from '@tanstack/react-query'; import type { ExtendedFile } from '~/common'; -import SheetPaths from '~/components/svg/Files/SheetPaths'; -import TextPaths from '~/components/svg/Files/TextPaths'; -import FilePaths from '~/components/svg/Files/FilePaths'; -import CodePaths from '~/components/svg/Files/CodePaths'; export const partialTypes = ['text/x-']; diff --git a/client/src/utils/getThemeFromEnv.js b/client/src/utils/getThemeFromEnv.js new file mode 100644 index 000000000..b294d799a --- /dev/null +++ b/client/src/utils/getThemeFromEnv.js @@ -0,0 +1,58 @@ +/** + * Loads theme configuration from environment variables + * @returns {import('@librechat/client').IThemeRGB | undefined} + */ +export function getThemeFromEnv() { + // Check if any theme environment variables are set + const hasThemeEnvVars = Object.keys(process.env).some((key) => + key.startsWith('REACT_APP_THEME_'), + ); + + if (!hasThemeEnvVars) { + return undefined; // Use default themes + } + + // Build theme object from environment variables + const theme = {}; + + // Helper to get env value with prefix + const getEnv = (key) => process.env[`REACT_APP_THEME_${key}`]; + + // Text colors + if (getEnv('TEXT_PRIMARY')) theme['rgb-text-primary'] = getEnv('TEXT_PRIMARY'); + if (getEnv('TEXT_SECONDARY')) theme['rgb-text-secondary'] = getEnv('TEXT_SECONDARY'); + if (getEnv('TEXT_TERTIARY')) theme['rgb-text-tertiary'] = getEnv('TEXT_TERTIARY'); + if (getEnv('TEXT_WARNING')) theme['rgb-text-warning'] = getEnv('TEXT_WARNING'); + + // Surface colors + if (getEnv('SURFACE_PRIMARY')) theme['rgb-surface-primary'] = getEnv('SURFACE_PRIMARY'); + if (getEnv('SURFACE_SECONDARY')) theme['rgb-surface-secondary'] = getEnv('SURFACE_SECONDARY'); + if (getEnv('SURFACE_TERTIARY')) theme['rgb-surface-tertiary'] = getEnv('SURFACE_TERTIARY'); + if (getEnv('SURFACE_SUBMIT')) theme['rgb-surface-submit'] = getEnv('SURFACE_SUBMIT'); + if (getEnv('SURFACE_SUBMIT_HOVER')) + theme['rgb-surface-submit-hover'] = getEnv('SURFACE_SUBMIT_HOVER'); + if (getEnv('SURFACE_DESTRUCTIVE')) + theme['rgb-surface-destructive'] = getEnv('SURFACE_DESTRUCTIVE'); + if (getEnv('SURFACE_DESTRUCTIVE_HOVER')) + theme['rgb-surface-destructive-hover'] = getEnv('SURFACE_DESTRUCTIVE_HOVER'); + if (getEnv('SURFACE_DIALOG')) theme['rgb-surface-dialog'] = getEnv('SURFACE_DIALOG'); + if (getEnv('SURFACE_CHAT')) theme['rgb-surface-chat'] = getEnv('SURFACE_CHAT'); + + // Border colors + if (getEnv('BORDER_LIGHT')) theme['rgb-border-light'] = getEnv('BORDER_LIGHT'); + if (getEnv('BORDER_MEDIUM')) theme['rgb-border-medium'] = getEnv('BORDER_MEDIUM'); + if (getEnv('BORDER_HEAVY')) theme['rgb-border-heavy'] = getEnv('BORDER_HEAVY'); + if (getEnv('BORDER_XHEAVY')) theme['rgb-border-xheavy'] = getEnv('BORDER_XHEAVY'); + + // Brand colors + if (getEnv('BRAND_PURPLE')) theme['rgb-brand-purple'] = getEnv('BRAND_PURPLE'); + + // Header colors + if (getEnv('HEADER_PRIMARY')) theme['rgb-header-primary'] = getEnv('HEADER_PRIMARY'); + if (getEnv('HEADER_HOVER')) theme['rgb-header-hover'] = getEnv('HEADER_HOVER'); + + // Presentation + if (getEnv('PRESENTATION')) theme['rgb-presentation'] = getEnv('PRESENTATION'); + + return Object.keys(theme).length > 0 ? theme : undefined; +} diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts index 82bf1f8a4..b86676a48 100644 --- a/client/src/utils/index.ts +++ b/client/src/utils/index.ts @@ -4,7 +4,6 @@ export * from './map'; export * from './json'; export * from './files'; export * from './latex'; -export * from './theme'; export * from './forms'; export * from './drafts'; export * from './convos'; @@ -25,6 +24,7 @@ export { default as cleanupPreset } from './cleanupPreset'; export { default as buildDefaultConvo } from './buildDefaultConvo'; export { default as getDefaultEndpoint } from './getDefaultEndpoint'; export { default as createChatSearchParams } from './createChatSearchParams'; +export { getThemeFromEnv } from './getThemeFromEnv'; export const languages = [ 'java', diff --git a/client/tailwind.config.cjs b/client/tailwind.config.cjs index f114a8733..e2f493ad6 100644 --- a/client/tailwind.config.cjs +++ b/client/tailwind.config.cjs @@ -2,7 +2,11 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ['./src/**/*.{js,jsx,ts,tsx}'], + content: [ + './src/**/*.{js,jsx,ts,tsx}', + // Include component library files + '../packages/client/src/**/*.{js,jsx,ts,tsx}', + ], // darkMode: 'class', darkMode: ['class'], theme: { @@ -61,8 +65,8 @@ module.exports = { 800: '#06373e', 900: '#031f29', }, - 'brand-purple': '#ab68ff', - 'presentation': 'var(--presentation)', + 'brand-purple': 'var(--brand-purple)', + presentation: 'var(--presentation)', 'text-primary': 'var(--text-primary)', 'text-secondary': 'var(--text-secondary)', 'text-secondary-alt': 'var(--text-secondary-alt)', @@ -135,7 +139,7 @@ module.exports = { }, plugins: [ require('tailwindcss-animate'), - require('tailwindcss-radix')(), + require('tailwindcss-radix'), // require('@tailwindcss/typography'), ], }; diff --git a/client/test/setupTests.js b/client/test/setupTests.js index 043c7cdf6..c489fc8b5 100644 --- a/client/test/setupTests.js +++ b/client/test/setupTests.js @@ -20,6 +20,21 @@ import 'jest-canvas-mock'; // Mock ResizeObserver import './resizeObserver.mock'; +// Mock window.matchMedia +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + beforeEach(() => { jest.clearAllMocks(); }); diff --git a/client/tsconfig.json b/client/tsconfig.json index ba00b0d24..59ea7f46c 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -35,5 +35,5 @@ "test/setupTests.js", "env.d.ts", "../config/translations/**/*.ts" - ] +, "../packages/client/src/hooks/useDelayedRender.tsx" ] } diff --git a/config/update.js b/config/update.js index e0dba111e..d6ffd5590 100644 --- a/config/update.js +++ b/config/update.js @@ -19,6 +19,7 @@ const directories = [ rootDir, path.resolve(rootDir, 'packages', 'data-provider'), path.resolve(rootDir, 'packages', 'data-schemas'), + path.resolve(rootDir, 'packages', 'client'), path.resolve(rootDir, 'packages', 'api'), path.resolve(rootDir, 'client'), path.resolve(rootDir, 'api'), diff --git a/package-lock.json b/package-lock.json index 97df522f4..9cef1c664 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2744,6 +2744,7 @@ "@dicebear/collection": "^9.2.2", "@dicebear/core": "^9.2.2", "@headlessui/react": "^2.1.2", + "@librechat/client": "*", "@marsidev/react-turnstile": "^1.1.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.2", @@ -2766,8 +2767,8 @@ "@react-spring/web": "^9.7.5", "@tanstack/react-query": "^4.28.0", "@tanstack/react-table": "^8.11.7", - "class-variance-authority": "^0.6.0", - "clsx": "^1.2.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", "cross-env": "^7.0.3", "date-fns": "^3.3.1", @@ -2780,11 +2781,12 @@ "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.3", "input-otp": "^1.4.2", + "jotai": "^2.12.5", "js-cookie": "^3.0.5", "librechat-data-provider": "*", "lodash": "^4.17.21", "lucide-react": "^0.394.0", - "match-sorter": "^6.3.4", + "match-sorter": "^8.1.0", "micromark-extension-llm-math": "^3.1.0", "qrcode.react": "^4.2.0", "rc-input-number": "^7.4.2", @@ -2860,58 +2862,6 @@ "vite-plugin-pwa": "^0.21.2" } }, - "client/node_modules/@ariakit/react": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.15.tgz", - "integrity": "sha512-0V2LkNPFrGRT+SEIiObx/LQjR6v3rR+mKEDUu/3tq7jfCZ+7+6Q6EMR1rFaK+XMkaRY1RWUcj/rRDWAUWnsDww==", - "dependencies": { - "@ariakit/react-core": "0.4.15" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ariakit" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "client/node_modules/@ariakit/react-core": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.17.tgz", - "integrity": "sha512-kFF6n+gC/5CRQIyaMTFoBPio2xUe0k9rZhMNdUobWRmc/twfeLVkODx+8UVYaNyKilTge8G0JFqwvFKku/jKEw==", - "license": "MIT", - "dependencies": { - "@ariakit/core": "0.4.15", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "client/node_modules/@ariakit/react-core/node_modules/@ariakit/core": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.15.tgz", - "integrity": "sha512-vvxmZvkNhiisKM+Y1TbGMUfVVchV/sWu9F0xw0RYADXcimWPK31dd9JnIZs/OQ5pwAryAHmERHwuGQVESkSjwQ==", - "license": "MIT" - }, - "client/node_modules/@ariakit/react/node_modules/@ariakit/react-core": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.15.tgz", - "integrity": "sha512-Up8+U97nAPJdyUh9E8BCEhJYTA+eVztWpHoo1R9zZfHd4cnBWAg5RHxEmMH+MamlvuRxBQA71hFKY/735fDg+A==", - "license": "MIT", - "dependencies": { - "@ariakit/core": "0.4.14", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "client/node_modules/@babel/compat-data": { "version": "7.26.8", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", @@ -4612,6 +4562,22 @@ "@dicebear/core": "^9.0.0" } }, + "client/node_modules/@react-spring/web": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", + "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "client/node_modules/@types/jest": { "version": "29.5.14", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", @@ -4811,6 +4777,33 @@ "node": ">=6" } }, + "client/node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "client/node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -4821,6 +4814,15 @@ "node": ">=4" } }, + "client/node_modules/lucide-react": { + "version": "0.394.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.394.0.tgz", + "integrity": "sha512-PzTbJ0bsyXRhH59k5qe7MpTd5MxlpYZUcM9kGSwvPGAfnn0J6FElDwu2EX6Vuh//F7y60rcVJiFQ7EK9DCMgfw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, "client/node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -4843,16 +4845,6 @@ "node": ">=0.10.0" } }, - "client/node_modules/react-resizable-panels": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.2.tgz", - "integrity": "sha512-j4RNII75fnHkLnbsTb5G5YsDvJsSEZrJK2XSF2z0Tc2jIonYlIVir/Yh/5LvcUFCfs1HqrMAoiBFmIrRjC4XnA==", - "license": "MIT", - "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/ts-jest": { "version": "29.2.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", @@ -5027,9 +5019,42 @@ } }, "node_modules/@ariakit/core": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.14.tgz", - "integrity": "sha512-hpzZvyYzGhP09S9jW1XGsU/FD5K3BKsH1eG/QJ8rfgEeUdPS7BvHPt5lHbOeJ2cMrRzBEvsEzLi1ivfDifHsVA==" + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.15.tgz", + "integrity": "sha512-vvxmZvkNhiisKM+Y1TbGMUfVVchV/sWu9F0xw0RYADXcimWPK31dd9JnIZs/OQ5pwAryAHmERHwuGQVESkSjwQ==", + "license": "MIT" + }, + "node_modules/@ariakit/react": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.17.tgz", + "integrity": "sha512-HQaIboE2axtlncJz1hRTaiQfJ1GGjhdtNcAnPwdjvl2RybfmlHowIB+HTVBp36LzroKPs/M4hPCxk7XTaqRZGg==", + "license": "MIT", + "dependencies": { + "@ariakit/react-core": "0.4.17" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ariakit" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/react-core": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.17.tgz", + "integrity": "sha512-kFF6n+gC/5CRQIyaMTFoBPio2xUe0k9rZhMNdUobWRmc/twfeLVkODx+8UVYaNyKilTge8G0JFqwvFKku/jKEw==", + "license": "MIT", + "dependencies": { + "@ariakit/core": "0.4.15", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } }, "node_modules/@aws-crypto/crc32": { "version": "3.0.0", @@ -17750,6 +17775,25 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", @@ -19028,21 +19072,23 @@ } }, "node_modules/@headlessui/react": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.2.tgz", - "integrity": "sha512-Kb3hgk9gRNRcTZktBrKdHhF3xFhYkca1Rk6e1/im2ENf83dgN54orMW0uSKTXFnUpZOUFZ+wcY05LlipwgZIFQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.4.tgz", + "integrity": "sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==", + "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.16", - "@react-aria/focus": "^3.17.1", - "@react-aria/interactions": "^3.21.3", - "@tanstack/react-virtual": "^3.8.1" + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "react": "^18", - "react-dom": "^18" + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "node_modules/@humanfs/core": { @@ -22143,6 +22189,10 @@ "resolved": "api", "link": true }, + "node_modules/@librechat/client": { + "resolved": "packages/client", + "link": true + }, "node_modules/@librechat/data-schemas": { "resolved": "packages/data-schemas", "link": true @@ -22872,12 +22922,10 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@radix-ui/number": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", - "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" }, "node_modules/@radix-ui/primitive": { "version": "1.0.1", @@ -22888,26 +22936,26 @@ } }, "node_modules/@radix-ui/react-accordion": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz", - "integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz", + "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collapsible": "1.0.3", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collapsible": "1.1.11", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -22918,6 +22966,176 @@ } } }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "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-accordion/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "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-accordion/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "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-accordion/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "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-accordion/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-accordion/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "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-accordion/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "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-accordion/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "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-accordion/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "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-alert-dialog": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz", @@ -23000,25 +23218,25 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", - "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", + "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -23029,6 +23247,159 @@ } } }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "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-collapsible/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "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-collapsible/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-collapsible/node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "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-collapsible/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "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-collapsible/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "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-collapsible/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "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-collection": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", @@ -23441,18 +23812,18 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", - "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -23463,6 +23834,62 @@ } } }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "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-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "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-label/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "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-menu": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.1.tgz", @@ -24183,27 +24610,27 @@ } }, "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", - "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz", + "integrity": "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -24214,6 +24641,279 @@ } } }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "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-radio-group/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "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-radio-group/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "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-radio-group/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "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-radio-group/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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/node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-radio-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "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-radio-group/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.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-radio-group/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "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/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "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-radio-group/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "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/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "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-radio-group/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "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-radio-group/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", @@ -24246,38 +24946,38 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz", - "integrity": "sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -24288,19 +24988,473 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "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-select/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "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-select/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "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-select/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "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-select/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "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-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "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-select/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "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-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "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-select/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-select/node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "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-select/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "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-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "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-select/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "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-select/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "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-select/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "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-select/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "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-select/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "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-select/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "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-select/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-select/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "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-select/node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.3.tgz", - "integrity": "sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -24311,29 +25465,34 @@ } } }, - "node_modules/@radix-ui/react-slider": { + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.2.tgz", - "integrity": "sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "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-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -24344,6 +25503,242 @@ } } }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "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-slider": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz", + "integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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-slider/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "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-slider/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "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-slider/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "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-slider/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "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-slider/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "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-slider/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "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-slider/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "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-slider/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "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-slider/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "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-slider/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -24363,24 +25758,24 @@ } }, "node_modules/@radix-ui/react-switch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", - "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", + "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -24391,6 +25786,150 @@ } } }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "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-switch/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "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-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "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-switch/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "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-switch/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "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-switch/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "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-switch/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "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-switch/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-tabs": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", @@ -24490,6 +26029,39 @@ } } }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "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-use-escape-keydown": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", @@ -24621,49 +26193,43 @@ } }, "node_modules/@react-aria/focus": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.17.1.tgz", - "integrity": "sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ==", + "version": "3.20.5", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.5.tgz", + "integrity": "sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==", "license": "Apache-2.0", "dependencies": { - "@react-aria/interactions": "^3.21.3", - "@react-aria/utils": "^3.24.1", - "@react-types/shared": "^3.23.1", + "@react-aria/interactions": "^3.25.3", + "@react-aria/utils": "^3.29.1", + "@react-types/shared": "^3.30.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" - } - }, - "node_modules/@react-aria/focus/node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-aria/interactions": { - "version": "3.21.3", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.21.3.tgz", - "integrity": "sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA==", + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.3.tgz", + "integrity": "sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==", "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.4", - "@react-aria/utils": "^3.24.1", - "@react-types/shared": "^3.23.1", + "@react-aria/ssr": "^3.9.9", + "@react-aria/utils": "^3.29.1", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.30.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-aria/ssr": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.4.tgz", - "integrity": "sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ==", + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz", + "integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" @@ -24672,32 +26238,25 @@ "node": ">= 12" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-aria/utils": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.24.1.tgz", - "integrity": "sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q==", + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.1.tgz", + "integrity": "sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==", "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.4", - "@react-stately/utils": "^3.10.1", - "@react-types/shared": "^3.23.1", + "@react-aria/ssr": "^3.9.9", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.7", + "@react-types/shared": "^3.30.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" - } - }, - "node_modules/@react-aria/utils/node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-dnd/asap": { @@ -24792,40 +26351,111 @@ "license": "MIT" }, "node_modules/@react-spring/web": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", - "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.0.1.tgz", + "integrity": "sha512-FgQk02OqFrYyJBTTnBTWAU0WPzkHkKXauc6aeexcvATvLapUxwnfGuLlsLYF8BYjEVfkivPT04ziAue6zyRBtQ==", "license": "MIT", + "peer": true, "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/core": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" + "@react-spring/animated": "~10.0.1", + "@react-spring/core": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/web/node_modules/@react-spring/animated": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.1.tgz", + "integrity": "sha512-BGL3hA66Y8Qm3KmRZUlfG/mFbDPYajgil2/jOP0VXf2+o2WPVmcDps/eEgdDqgf5Pv9eBbyj7LschLMuSjlW3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/web/node_modules/@react-spring/core": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.1.tgz", + "integrity": "sha512-KaMMsN1qHuVTsFpg/5ajAVye7OEqhYbCq0g4aKM9bnSZlDBBYpO7Uf+9eixyXN8YEbF+YXaYj9eoWDs+npZ+sA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/web/node_modules/@react-spring/rafz": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.1.tgz", + "integrity": "sha512-UrzG/d6Is+9i0aCAjsjWRqIlFFiC4lFqFHrH63zK935z2YDU95TOFio4VKGISJ5SG0xq4ULy7c1V3KU+XvL+Yg==", + "license": "MIT", + "peer": true + }, + "node_modules/@react-spring/web/node_modules/@react-spring/shared": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.1.tgz", + "integrity": "sha512-KR2tmjDShPruI/GGPfAZOOLvDgkhFseabjvxzZFFggJMPkyICLjO0J6mCIoGtdJSuHywZyc4Mmlgi+C88lS00g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-spring/rafz": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/web/node_modules/@react-spring/types": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.1.tgz", + "integrity": "sha512-Fk1wYVAKL+ZTYK+4YFDpHf3Slsy59pfFFvnnTfRjQQFGlyIo4VejPtDs3CbDiuBjM135YztRyZjIH2VbycB+ZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" } }, "node_modules/@react-stately/utils": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.1.tgz", - "integrity": "sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg==", + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.7.tgz", + "integrity": "sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-types/shared": { - "version": "3.23.1", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.23.1.tgz", - "integrity": "sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.30.0.tgz", + "integrity": "sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==", "license": "Apache-2.0", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@redis/bloom": { @@ -27082,12 +28712,12 @@ } }, "node_modules/@swc/helpers": { - "version": "0.5.11", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", - "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@tanstack/match-sorter-utils": { @@ -27181,20 +28811,20 @@ } }, "node_modules/@tanstack/react-virtual": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.8.2.tgz", - "integrity": "sha512-g78+DA29K0ByAfDkuibfLQqDshf8Aha/zcyEZ+huAX/yS/TWj/CUiEY4IJfDrFacdxIFmsLm0u4VtsLSKTngRw==", + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.8.2" + "@tanstack/virtual-core": "3.13.12" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@tanstack/table-core": { @@ -27210,9 +28840,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.8.2.tgz", - "integrity": "sha512-ffpN6kTaPGwQPoWMcBAHbdv2ZCpj1SugldoYAcY0C4xH+Pej1KCOEUisNeEgbUnXOp8Y/4q6wGPu2tFHthOIQw==", + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", "license": "MIT", "funding": { "type": "github", @@ -27350,6 +28980,16 @@ "node": ">= 10" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -28540,9 +30180,10 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", - "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -29629,6 +31270,19 @@ "node": ">= 6" } }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001706", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001706.tgz", @@ -29857,14 +31511,15 @@ "dev": true }, "node_modules/class-variance-authority": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.6.1.tgz", - "integrity": "sha512-eurOEGc7YVx3majOrOb099PNKgO3KnKSApOprXI4BTq6bcfbqbQXPN2u+rPPmIJ2di23bMwhk0SxCCthBmszEQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", "dependencies": { - "clsx": "1.2.1" + "clsx": "^2.1.1" }, "funding": { - "url": "https://joebell.co.uk" + "url": "https://polar.sh/cva" } }, "node_modules/classnames": { @@ -30020,9 +31675,10 @@ } }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -30177,6 +31833,13 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -30235,6 +31898,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -30306,6 +31979,16 @@ "node": ">= 6" } }, + "node_modules/concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dev": true, + "license": "ISC", + "dependencies": { + "source-map": "^0.6.1" + } + }, "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -30656,6 +32339,19 @@ "postcss": "^8.4" } }, + "node_modules/css-declaration-sorter": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", + "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, "node_modules/css-has-pseudo": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-5.0.2.tgz", @@ -30709,6 +32405,20 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -30760,6 +32470,108 @@ "integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==", "dev": true }, + "node_modules/cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -33425,17 +35237,20 @@ } }, "node_modules/framer-motion": { - "version": "11.5.4", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.5.4.tgz", - "integrity": "sha512-E+tb3/G6SO69POkdJT+3EpdMuhmtCh9EWuK4I1DnIC23L7tFPrl8vxP+LSovwaw6uUr73rUbpb4FgK011wbRJQ==", + "version": "12.23.9", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.9.tgz", + "integrity": "sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ==", "license": "MIT", + "peer": true, "dependencies": { + "motion-dom": "^12.23.9", + "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/is-prop-valid": { @@ -33449,6 +35264,23 @@ } } }, + "node_modules/framer-motion/node_modules/motion-dom": { + "version": "12.23.9", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.9.tgz", + "integrity": "sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A==", + "license": "MIT", + "peer": true, + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/framer-motion/node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT", + "peer": true + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -33591,6 +35423,16 @@ "node": ">=12" } }, + "node_modules/generic-names": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz", + "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^3.2.0" + } + }, "node_modules/generic-pool": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", @@ -33977,7 +35819,8 @@ "node_modules/hamt_plus": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", - "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" + "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==", + "license": "MIT" }, "node_modules/handlebars": { "version": "4.7.8", @@ -34586,9 +36429,9 @@ } }, "node_modules/i18next-browser-languagedetector": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.3.tgz", - "integrity": "sha512-beOOLArattPBc2YZG5IXGJytdYFgUR7cS8Wd6HT4IczIoWKgmTspOQ2yasaGklelVo5seLPmnEKvLHR+E/MdWQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2" @@ -34706,6 +36549,26 @@ "node": ">=0.10.0" } }, + "node_modules/icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==", + "dev": true, + "license": "ISC" + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -34757,6 +36620,19 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-from": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -34773,6 +36649,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-from/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -34860,14 +36759,6 @@ "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.10.0.tgz", "integrity": "sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==" }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/ioredis": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", @@ -36425,6 +38316,27 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jotai": { + "version": "2.12.5", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.12.5.tgz", + "integrity": "sha512-G8m32HW3lSmcz/4mbqx0hgJIQ0ekndKWiYP7kWVKi0p6saLXdSoye+FZiOFyonnd7Q482LCzm8sMDl7Ar1NWDw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/js-base64": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", @@ -37347,6 +39259,16 @@ "node": ">=6.11.5" } }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -37451,6 +39373,13 @@ "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -37785,11 +39714,13 @@ "license": "ISC" }, "node_modules/lucide-react": { - "version": "0.394.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.394.0.tgz", - "integrity": "sha512-PzTbJ0bsyXRhH59k5qe7MpTd5MxlpYZUcM9kGSwvPGAfnn0J6FElDwu2EX6Vuh//F7y60rcVJiFQ7EK9DCMgfw==", + "version": "0.525.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", + "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", + "license": "ISC", + "peer": true, "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/lz-string": { @@ -37805,6 +39736,7 @@ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } @@ -37849,9 +39781,10 @@ } }, "node_modules/match-sorter": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", - "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-8.1.0.tgz", + "integrity": "sha512-0HX3BHPixkbECX+Vt7nS1vJ6P2twPgGTU3PMXjWrl1eyVCL24tFHeyYN1FN5RKLzve0TyzNI9qntqQGbebnfPQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.8", "remove-accents": "0.5.0" @@ -37860,7 +39793,8 @@ "node_modules/match-sorter/node_modules/remove-accents": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", - "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -38486,6 +40420,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -39466,6 +41407,21 @@ "color-name": "^1.1.4" } }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -39890,6 +41846,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -40769,6 +42738,19 @@ "node": ">=0.10" } }, + "node_modules/pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -40949,6 +42931,20 @@ "postcss": "^8.4" } }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, "node_modules/postcss-clamp": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", @@ -41034,6 +43030,42 @@ "postcss": "^8.4" } }, + "node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, "node_modules/postcss-custom-media": { "version": "9.1.5", "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-9.1.5.tgz", @@ -41137,6 +43169,58 @@ "postcss": "^8.4" } }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, "node_modules/postcss-double-position-gradients": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-4.0.4.tgz", @@ -41405,6 +43489,221 @@ "postcss": "^8.4" } }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.3.1.tgz", + "integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "generic-names": "^4.0.0", + "icss-replace-symbols": "^1.1.0", + "lodash.camelcase": "^4.3.0", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "string-hash": "^1.1.1" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/postcss-nested": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", @@ -41449,6 +43748,149 @@ "postcss": "^8.4" } }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dev": true, + "license": "MIT", + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, "node_modules/postcss-opacity-percentage": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-2.0.0.tgz", @@ -41471,6 +43913,23 @@ "postcss": "^8.2" } }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, "node_modules/postcss-overflow-shorthand": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-4.0.1.tgz", @@ -41617,6 +44076,39 @@ "postcss": "^8.4" } }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, "node_modules/postcss-replace-overflow-wrap": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", @@ -41657,6 +44149,39 @@ "node": ">=4" } }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -42225,6 +44750,16 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/promise.series": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz", + "integrity": "sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -42841,19 +45376,20 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", "dependencies": { - "react-style-singleton": "^2.2.1", + "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -42861,6 +45397,16 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.3.tgz", + "integrity": "sha512-7HA8THVBHTzhDK4ON0tvlGXyMAJN1zBeRpuyyremSikgYh2ku6ltD7tsGQOcXx4NKPrZtYCm/5CBr+dkruTGQw==", + "license": "MIT", + "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" + } + }, "node_modules/react-router": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz", @@ -42900,20 +45446,20 @@ } }, "node_modules/react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", - "invariant": "^2.2.4", "tslib": "^2.0.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -42970,6 +45516,15 @@ "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-virtualized/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -43074,6 +45629,7 @@ "version": "0.7.7", "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", + "license": "MIT", "dependencies": { "hamt_plus": "1.0.2" }, @@ -44161,6 +46717,74 @@ "rollup": "*" } }, + "node_modules/rollup-plugin-postcss": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz", + "integrity": "sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "concat-with-sourcemaps": "^1.1.0", + "cssnano": "^5.0.1", + "import-cwd": "^3.0.0", + "p-queue": "^6.6.2", + "pify": "^5.0.0", + "postcss-load-config": "^3.0.0", + "postcss-modules": "^4.0.0", + "promise.series": "^0.2.0", + "resolve": "^1.19.0", + "rollup-pluginutils": "^2.8.2", + "safe-identifier": "^0.4.2", + "style-inject": "^0.3.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "8.x" + } + }, + "node_modules/rollup-plugin-postcss/node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-postcss/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/rollup-plugin-typescript2": { "version": "0.35.0", "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.35.0.tgz", @@ -44226,6 +46850,23 @@ "node": ">= 10.0.0" } }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true, + "license": "MIT" + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -44322,6 +46963,13 @@ } ] }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", + "dev": true, + "license": "ISC" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -44901,6 +47549,14 @@ "integrity": "sha512-I7zYndqOOkNpz9KIdFZ8c8A7zs1YazNewBr8Nsi/tqThfJkVPuP1q7UE2h4B0RwoWZxbBYpd06uoW3NI3SaZXg==", "license": "Apache-2.0" }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", + "dev": true, + "license": "MIT" + }, "node_modules/stable-hash": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", @@ -45082,6 +47738,13 @@ "node": ">=0.6.19" } }, + "node_modules/string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -45397,6 +48060,13 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/style-inject": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", + "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", + "dev": true, + "license": "MIT" + }, "node_modules/style-mod": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", @@ -45410,6 +48080,23 @@ "inline-style-parser": "0.2.3" } }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -45565,6 +48252,101 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/svgo/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -46910,9 +49692,10 @@ } }, "node_modules/use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" @@ -46921,8 +49704,8 @@ "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -46931,11 +49714,12 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/utf8": { @@ -48720,6 +51504,304 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/client": { + "name": "@librechat/client", + "version": "0.1.9", + "devDependencies": { + "@rollup/plugin-alias": "^5.1.0", + "@rollup/plugin-commonjs": "^25.0.2", + "@rollup/plugin-node-resolve": "^15.0.0", + "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-terser": "^0.4.4", + "@tanstack/react-query": "^4.28.0", + "@testing-library/react": "^14.0.0", + "@types/react": "^18.2.11", + "@types/react-dom": "^18.2.4", + "concat-with-sourcemaps": "^1.1.0", + "i18next": "^24.2.3", + "jotai": "^2.12.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^15.4.0", + "rimraf": "^5.0.1", + "rollup": "^4.0.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-postcss": "^4.0.2", + "rollup-plugin-typescript2": "^0.35.0", + "tailwindcss-radix": "^2.8.0", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "@ariakit/react": "^0.4.16", + "@ariakit/react-core": "^0.4.17", + "@headlessui/react": "^2.1.2", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.0.2", + "@radix-ui/react-checkbox": "^1.0.3", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-dialog": "^1.0.2", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.0.5", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.0.0", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.0.3", + "@radix-ui/react-toast": "^1.1.5", + "@react-spring/web": "^10.0.1", + "@tanstack/react-query": "^4.28.0 || ^5.0.0", + "@tanstack/react-table": "^8.11.7", + "@tanstack/react-virtual": "^3.0.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.23.6", + "i18next": "^24.2.2 || ^25.3.2", + "i18next-browser-languagedetector": "^8.2.0", + "input-otp": "^1.4.2", + "jotai": "^2.12.5", + "lucide-react": "^0.525.0", + "match-sorter": "^8.1.0", + "rc-input-number": "^7.4.2", + "react": "^18.2.0 || ^19.1.0", + "react-dom": "^18.2.0 || ^19.1.0", + "react-hook-form": "^7.56.4", + "react-i18next": "^15.4.0 || ^15.6.0", + "react-resizable-panels": "^3.0.2", + "react-textarea-autosize": "^8.4.0", + "tailwind-merge": "^1.9.1" + } + }, + "packages/client/node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "packages/client/node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "packages/client/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "packages/client/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "packages/client/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/client/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/client/node_modules/i18next": { + "version": "24.2.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", + "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.10" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/client/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "packages/client/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/client/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "packages/client/node_modules/react-hook-form": { + "version": "7.60.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz", + "integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "packages/client/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "packages/client/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/data-provider": { "name": "librechat-data-provider", "version": "0.7.902", diff --git a/package.json b/package.json index 1aad42eb6..36327241a 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,9 @@ "build:data-provider": "cd packages/data-provider && npm run build", "build:api": "cd packages/api && npm run build", "build:data-schemas": "cd packages/data-schemas && npm run build", - "frontend": "npm run build:data-provider && npm run build:data-schemas && npm run build:api && cd client && npm run build", - "frontend:ci": "npm run build:data-provider && cd client && npm run build:ci", + "build:client-package": "cd packages/client && npm run build", + "frontend": "npm run build:data-provider && npm run build:data-schemas && npm run build:api && npm run build:client-package && cd client && npm run build", + "frontend:ci": "npm run build:data-provider && npm run build:client-package && cd client && npm run build:ci", "frontend:dev": "cd client && npm run dev", "e2e": "playwright test --config=e2e/playwright.config.local.ts", "e2e:headed": "playwright test --config=e2e/playwright.config.local.ts --headed", diff --git a/packages/client/package.json b/packages/client/package.json new file mode 100644 index 000000000..5edd6505c --- /dev/null +++ b/packages/client/package.json @@ -0,0 +1,92 @@ +{ + "name": "@librechat/client", + "version": "0.1.9", + "description": "React components for LibreChat", + "main": "dist/index.js", + "module": "dist/index.es.js", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.es.js", + "require": "./dist/index.js", + "types": "./dist/types/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && rollup -c --bundleConfigAsCjs", + "build:watch": "rollup -c -w --bundleConfigAsCjs", + "dev": "rollup -c -w --bundleConfigAsCjs" + }, + "peerDependencies": { + "@tanstack/react-query": "^4.28.0 || ^5.0.0", + "i18next": "^24.2.2 || ^25.3.2", + "jotai": "^2.12.5", + "react": "^18.2.0 || ^19.1.0", + "react-dom": "^18.2.0 || ^19.1.0", + "react-i18next": "^15.4.0 || ^15.6.0", + "@tanstack/react-table": "^8.11.7", + "@tanstack/react-virtual": "^3.0.0", + "@ariakit/react": "^0.4.16", + "@ariakit/react-core": "^0.4.17", + "@headlessui/react": "^2.1.2", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.0.2", + "@radix-ui/react-checkbox": "^1.0.3", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-dialog": "^1.0.2", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.0.5", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.0.0", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.0.3", + "@radix-ui/react-toast": "^1.1.5", + "@react-spring/web": "^10.0.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.23.6", + "i18next-browser-languagedetector": "^8.2.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.525.0", + "match-sorter": "^8.1.0", + "rc-input-number": "^7.4.2", + "react-hook-form": "^7.56.4", + "react-resizable-panels": "^3.0.2", + "react-textarea-autosize": "^8.4.0", + "tailwind-merge": "^1.9.1" + }, + "devDependencies": { + "@rollup/plugin-alias": "^5.1.0", + "@rollup/plugin-commonjs": "^25.0.2", + "@rollup/plugin-node-resolve": "^15.0.0", + "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-terser": "^0.4.4", + "@tanstack/react-query": "^4.28.0", + "@testing-library/react": "^14.0.0", + "@types/react": "^18.2.11", + "@types/react-dom": "^18.2.4", + "concat-with-sourcemaps": "^1.1.0", + "i18next": "^24.2.3", + "jotai": "^2.12.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^15.4.0", + "rimraf": "^5.0.1", + "rollup": "^4.0.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-postcss": "^4.0.2", + "rollup-plugin-typescript2": "^0.35.0", + "tailwindcss-radix": "^2.8.0", + "typescript": "^5.0.0" + } +} diff --git a/packages/client/rollup.config.js b/packages/client/rollup.config.js new file mode 100644 index 000000000..1523ca9f7 --- /dev/null +++ b/packages/client/rollup.config.js @@ -0,0 +1,84 @@ +// ESM bundler config for React components +import { fileURLToPath } from 'url'; +import alias from '@rollup/plugin-alias'; +import terser from '@rollup/plugin-terser'; +import postcss from 'rollup-plugin-postcss'; +import replace from '@rollup/plugin-replace'; +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import typescript from 'rollup-plugin-typescript2'; +import { dirname, resolve as pathResolve } from 'path'; +import peerDepsExternal from 'rollup-plugin-peer-deps-external'; +import pkg from './package.json'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const plugins = [ + peerDepsExternal(), + alias({ + entries: [{ find: '~', replacement: pathResolve(__dirname, 'src') }], + }), + resolve({ + extensions: ['.js', '.jsx', '.ts', '.tsx'], + browser: true, + preferBuiltins: false, + }), + replace({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), + preventAssignment: true, + }), + commonjs(), + postcss({ + extract: false, + inject: true, + minimize: process.env.NODE_ENV === 'production', + modules: false, + config: { + path: './postcss.config.js', + }, + }), + typescript({ + tsconfig: './tsconfig.json', + useTsconfigDeclarationDir: true, + clean: true, + check: false, + }), + terser({ + compress: { + directives: false, + }, + }), +]; + +export default { + input: 'src/index.ts', + output: [ + { + file: pkg.main, + format: 'cjs', + sourcemap: true, + exports: 'named', + }, + { + file: pkg.module, + format: 'esm', + sourcemap: true, + exports: 'named', + }, + ], + external: [ + ...Object.keys(pkg.peerDependencies || {}), + 'react/jsx-runtime', + 'react/jsx-dev-runtime', + ], + preserveSymlinks: true, + plugins, + onwarn(warning, warn) { + // Ignore "use client" directive warnings + if (warning.code === 'MODULE_LEVEL_DIRECTIVE') { + return; + } + warn(warning); + }, +}; diff --git a/client/src/Providers/ToastContext.tsx b/packages/client/src/Providers/ToastContext.tsx similarity index 77% rename from client/src/Providers/ToastContext.tsx rename to packages/client/src/Providers/ToastContext.tsx index 2f0e5efcf..afe24995d 100644 --- a/client/src/Providers/ToastContext.tsx +++ b/packages/client/src/Providers/ToastContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext } from 'react'; +import { createContext, useContext, ReactNode } from 'react'; import type { TShowToast } from '~/common'; import useToast from '~/hooks/useToast'; @@ -14,7 +14,7 @@ export function useToastContext() { return useContext(ToastContext); } -export default function ToastProvider({ children }) { +export default function ToastProvider({ children }: { children: ReactNode }) { const { showToast } = useToast(); return {children}; diff --git a/packages/client/src/Providers/index.ts b/packages/client/src/Providers/index.ts new file mode 100644 index 000000000..41cb62ae9 --- /dev/null +++ b/packages/client/src/Providers/index.ts @@ -0,0 +1,2 @@ +export { default as ToastProvider } from './ToastContext'; +export * from './ToastContext'; diff --git a/packages/client/src/common/index.ts b/packages/client/src/common/index.ts new file mode 100644 index 000000000..7a855f3c1 --- /dev/null +++ b/packages/client/src/common/index.ts @@ -0,0 +1,11 @@ +export type { + TShowToast, + Option, + OptionWithIcon, + DropdownValueSetter, + MentionOption, +} from './types'; + +export { NotificationSeverity } from './types'; + +export type { MenuItemProps } from './menus'; diff --git a/packages/client/src/common/menus.ts b/packages/client/src/common/menus.ts new file mode 100644 index 000000000..c46ad3f8b --- /dev/null +++ b/packages/client/src/common/menus.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export type RenderProp< + P = React.HTMLAttributes & { + ref?: React.Ref; + }, +> = (props: P) => React.ReactNode; + +export interface MenuItemProps { + id?: string; + label?: string; + onClick?: (e: React.MouseEvent) => void; + icon?: React.ReactNode; + kbd?: string; + show?: boolean; + disabled?: boolean; + separate?: boolean; + hideOnClick?: boolean; + dialog?: React.ReactElement; + ref?: React.Ref; + render?: + | RenderProp & { ref?: React.Ref | undefined }> + | React.ReactElement> + | undefined; +} diff --git a/packages/client/src/common/types.ts b/packages/client/src/common/types.ts new file mode 100644 index 000000000..aaeb5f524 --- /dev/null +++ b/packages/client/src/common/types.ts @@ -0,0 +1,33 @@ +export enum NotificationSeverity { + INFO = 'info', + SUCCESS = 'success', + WARNING = 'warning', + ERROR = 'error', +} + +export type TShowToast = { + message: string; + severity?: NotificationSeverity; + showIcon?: boolean; + duration?: number; + status?: 'error' | 'success' | 'warning' | 'info'; +}; + +export type Option = Record & { + label?: string; + value: string | number | null; +}; + +export type OptionWithIcon = Option & { icon?: React.ReactNode }; +export type DropdownValueSetter = (value: string | Option | OptionWithIcon) => void; +export type MentionOption = OptionWithIcon & { + type: string; + value: string; + description?: string; +}; + +export interface SelectedValues { + endpoint: string | null; + model: string | null; + modelSpec: string | null; +} diff --git a/client/src/components/ui/Accordion.tsx b/packages/client/src/components/Accordion.tsx similarity index 94% rename from client/src/components/ui/Accordion.tsx rename to packages/client/src/components/Accordion.tsx index 5c8ed39a7..a969531e8 100644 --- a/client/src/components/ui/Accordion.tsx +++ b/packages/client/src/components/Accordion.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import * as AccordionPrimitive from '@radix-ui/react-accordion'; import { ChevronDownIcon } from '@radix-ui/react-icons'; - import { cn } from '~/utils'; const Accordion = AccordionPrimitive.Root; @@ -28,7 +27,7 @@ const AccordionTrigger = React.forwardRef< {...props} > {children} - + )); diff --git a/client/src/components/ui/AlertDialog.tsx b/packages/client/src/components/AlertDialog.tsx similarity index 99% rename from client/src/components/ui/AlertDialog.tsx rename to packages/client/src/components/AlertDialog.tsx index 6a5a2dbfc..4e78fda21 100644 --- a/client/src/components/ui/AlertDialog.tsx +++ b/packages/client/src/components/AlertDialog.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; - -import { cn } from '../../utils'; +import { cn } from '~/utils'; const AlertDialog = AlertDialogPrimitive.Root; diff --git a/packages/client/src/components/AnimatedSearchInput.tsx b/packages/client/src/components/AnimatedSearchInput.tsx new file mode 100644 index 000000000..66af0c102 --- /dev/null +++ b/packages/client/src/components/AnimatedSearchInput.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { Search } from 'lucide-react'; +import { cn } from '~/utils'; + +const AnimatedSearchInput = ({ + value, + onChange, + isSearching: searching, + placeholder, +}: { + value?: string; + onChange: (e: React.ChangeEvent) => void; + isSearching?: boolean; + placeholder: string; +}) => { + const isSearching = searching === true; + const hasValue = value != null && value.length > 0; + + return ( +
+
+
+ {/* Icon on the left */} +
+ +
+ + {/* Input field */} + + + {/* Gradient overlay */} +
+ + {/* Animated loading indicator */} +
+
+
+
+
+
+
+
+ + {/* Outer glow effect */} +
+
+
+
+
+
+
+
+ ); +}; + +export default AnimatedSearchInput; diff --git a/client/src/components/ui/AnimatedTabs.css b/packages/client/src/components/AnimatedTabs.css similarity index 96% rename from client/src/components/ui/AnimatedTabs.css rename to packages/client/src/components/AnimatedTabs.css index 808f5c4ac..4f16a965c 100644 --- a/client/src/components/ui/AnimatedTabs.css +++ b/packages/client/src/components/AnimatedTabs.css @@ -26,7 +26,7 @@ position: relative; } -.animated-tab[data-state="active"] { +.animated-tab[data-state='active'] { border-bottom-color: transparent !important; } diff --git a/client/src/components/ui/AnimatedTabs.tsx b/packages/client/src/components/AnimatedTabs.tsx similarity index 93% rename from client/src/components/ui/AnimatedTabs.tsx rename to packages/client/src/components/AnimatedTabs.tsx index 809699f30..131d871aa 100644 --- a/client/src/components/ui/AnimatedTabs.tsx +++ b/packages/client/src/components/AnimatedTabs.tsx @@ -1,13 +1,13 @@ import * as Ariakit from '@ariakit/react'; -import { ReactNode, forwardRef, useEffect, useRef } from 'react'; +import { forwardRef, useEffect, useRef } from 'react'; import type { ElementRef } from 'react'; import { cn } from '~/utils'; import './AnimatedTabs.css'; export interface TabItem { id?: string; - label: ReactNode; - content: ReactNode; + label: React.ReactNode; + content: React.ReactNode; disabled?: boolean; } @@ -23,7 +23,7 @@ export interface AnimatedTabsProps { } function usePrevious(value: T) { - const ref = useRef(); + const ref = useRef(undefined); useEffect(() => { ref.current = value; }, [value]); @@ -132,6 +132,7 @@ export function AnimatedTabs({ className={tabClassName} data-state={tabIds[index] === firstTabId ? 'active' : 'inactive'} > + {/* TypeScript workaround for React i18next children type compatibility */} {tab.label} ))} @@ -150,6 +151,7 @@ export function AnimatedTabs({ tabId={tabIds[index]} className={tabPanelClassName} > + {/* TypeScript workaround for React i18next children type compatibility */} {tab.content} ))} diff --git a/client/src/components/ui/Badge.tsx b/packages/client/src/components/Badge.tsx similarity index 92% rename from client/src/components/ui/Badge.tsx rename to packages/client/src/components/Badge.tsx index d99017c73..2469eccb2 100644 --- a/client/src/components/ui/Badge.tsx +++ b/packages/client/src/components/Badge.tsx @@ -1,12 +1,15 @@ import type React from 'react'; - import { X, Plus } from 'lucide-react'; import { motion } from 'framer-motion'; import type { ButtonHTMLAttributes } from 'react'; import type { LucideIcon } from 'lucide-react'; import { cn } from '~/utils'; -interface BadgeProps extends ButtonHTMLAttributes { +interface BadgeProps + extends Omit< + ButtonHTMLAttributes, + 'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag' + > { icon?: LucideIcon; label: string; id?: string; @@ -71,7 +74,7 @@ export default function Badge({ }} whileTap={{ scale: isDragging ? 1.1 : isDisabled ? 1 : 0.97 }} transition={{ type: 'tween', duration: 0.1, ease: 'easeOut' }} - {...props} + {...(props as React.ComponentProps)} > {Icon && } {label} diff --git a/client/src/components/ui/Breadcrumb.tsx b/packages/client/src/components/Breadcrumb.tsx similarity index 91% rename from client/src/components/ui/Breadcrumb.tsx rename to packages/client/src/components/Breadcrumb.tsx index 08082e51b..f1026a830 100644 --- a/client/src/components/ui/Breadcrumb.tsx +++ b/packages/client/src/components/Breadcrumb.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; import { ChevronRight, MoreHorizontal } from 'lucide-react'; - import { cn } from '~/utils'; const Breadcrumb = React.forwardRef< @@ -17,7 +16,7 @@ const BreadcrumbList = React.forwardRef ); @@ -58,7 +57,7 @@ const BreadcrumbPage = React.forwardRef ), diff --git a/client/src/components/ui/Button.tsx b/packages/client/src/components/Button.tsx similarity index 100% rename from client/src/components/ui/Button.tsx rename to packages/client/src/components/Button.tsx diff --git a/client/src/components/ui/Checkbox.tsx b/packages/client/src/components/Checkbox.tsx similarity index 96% rename from client/src/components/ui/Checkbox.tsx rename to packages/client/src/components/Checkbox.tsx index a28af18c7..36a81ac73 100644 --- a/client/src/components/ui/Checkbox.tsx +++ b/packages/client/src/components/Checkbox.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; import { Check } from 'lucide-react'; -import { cn } from '../../utils'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { cn } from '~/utils'; const Checkbox = React.forwardRef< React.ElementRef, diff --git a/client/src/components/ui/CheckboxButton.tsx b/packages/client/src/components/CheckboxButton.tsx similarity index 98% rename from client/src/components/ui/CheckboxButton.tsx rename to packages/client/src/components/CheckboxButton.tsx index d71ee71b4..1858b7b68 100644 --- a/client/src/components/ui/CheckboxButton.tsx +++ b/packages/client/src/components/CheckboxButton.tsx @@ -1,7 +1,7 @@ +import * as React from 'react'; import { useEffect } from 'react'; import { Checkbox, useStoreState, useCheckboxStore } from '@ariakit/react'; import { cn } from '~/utils'; -import * as React from 'react'; const CheckboxButton = React.forwardRef< HTMLInputElement, @@ -63,7 +63,7 @@ const CheckboxButton = React.forwardRef< render={ @@ -211,12 +200,11 @@ export default function DataTable({ onFilterChange, filterValue, isLoading, + enableSearch = true, }: DataTableProps) { - const localize = useLocalize(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); const tableContainerRef = useRef(null); - const search = useRecoilValue(store.search); const [isDeleting, setIsDeleting] = useState(false); const [rowSelection, setRowSelection] = useState>({}); const [sorting, setSorting] = useState(defaultSort); @@ -371,16 +359,15 @@ export default function DataTable({ isDeleting={isDeleting} disabled={!table.getFilteredSelectedRowModel().rows.length || isDeleting} isSmallScreen={isSmallScreen} - localize={localize} /> )} - {filterColumn !== undefined && table.getColumn(filterColumn) && search.enabled && ( + {filterColumn !== undefined && table.getColumn(filterColumn) && enableSearch && (
setSearchTerm(e.target.value)} isSearching={isSearching} - placeholder={`${localize('com_ui_search')}...`} + placeholder="Search..." />
)} @@ -448,7 +435,7 @@ export default function DataTable({ {!virtualRows.length && ( - {localize('com_ui_no_data')} + No data available )} diff --git a/client/src/components/ui/DataTableColumnHeader.tsx b/packages/client/src/components/DataTableColumnHeader.tsx similarity index 83% rename from client/src/components/ui/DataTableColumnHeader.tsx rename to packages/client/src/components/DataTableColumnHeader.tsx index 01d335a0e..606f10b12 100644 --- a/client/src/components/ui/DataTableColumnHeader.tsx +++ b/packages/client/src/components/DataTableColumnHeader.tsx @@ -1,8 +1,5 @@ -import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons'; import { Column } from '@tanstack/react-table'; - -import { cn } from '~/utils'; -import { Button } from './Button'; +import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons'; import { DropdownMenu, DropdownMenuContent, @@ -10,6 +7,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from './DropdownMenu'; +import { Button } from './Button'; +import { cn } from '~/utils'; interface DataTableColumnHeaderProps extends React.HTMLAttributes { column: Column; @@ -29,7 +28,7 @@ export function DataTableColumnHeader({
- diff --git a/client/src/components/ui/MultiSelect.tsx b/packages/client/src/components/MultiSelect.tsx similarity index 92% rename from client/src/components/ui/MultiSelect.tsx rename to packages/client/src/components/MultiSelect.tsx index 20e251279..d7798e810 100644 --- a/client/src/components/ui/MultiSelect.tsx +++ b/packages/client/src/components/MultiSelect.tsx @@ -14,7 +14,6 @@ interface MultiSelectProps { items: T[]; label?: string; placeholder?: string; - defaultSelectedValues?: T[]; onSelectedValuesChange?: (values: T[]) => void; renderSelectedValues?: (values: T[], placeholder?: string) => React.ReactNode; className?: string; @@ -47,7 +46,6 @@ export default function MultiSelect({ items, label, placeholder = 'Select...', - defaultSelectedValues = [], onSelectedValuesChange, renderSelectedValues = defaultRender, className, @@ -89,7 +87,7 @@ export default function MultiSelect({ )} onChange={(e) => e.stopPropagation()} > - {selectIcon && selectIcon} + {selectIcon && {selectIcon as React.JSX.Element}} {renderSelectedValues(selectedValues, placeholder)} @@ -132,8 +130,12 @@ export default function MultiSelect({ )} > {renderItemContent - ? renderItemContent(value, defaultContent, isCurrentItemSelected) - : defaultContent} + ? (renderItemContent( + value, + defaultContent, + isCurrentItemSelected, + ) as React.JSX.Element) + : (defaultContent as React.JSX.Element)} ); })} diff --git a/client/src/components/ui/OGDialogTemplate.tsx b/packages/client/src/components/OGDialogTemplate.tsx similarity index 93% rename from client/src/components/ui/OGDialogTemplate.tsx rename to packages/client/src/components/OGDialogTemplate.tsx index 8595c13ac..fe2e919a4 100644 --- a/client/src/components/ui/OGDialogTemplate.tsx +++ b/packages/client/src/components/OGDialogTemplate.tsx @@ -9,7 +9,7 @@ import { } from './OriginalDialog'; import { useLocalize } from '~/hooks'; import { Button } from './Button'; -import { Spinner } from '../svg'; +import { Spinner } from '~/svgs'; import { cn } from '~/utils/'; type SelectionProps = { @@ -97,7 +97,11 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref - {isLoading === true ? : selectText} + {isLoading === true ? ( + + ) : ( + (selectText as React.JSX.Element) + )} ) : null}
diff --git a/client/src/components/ui/OriginalDialog.tsx b/packages/client/src/components/OriginalDialog.tsx similarity index 100% rename from client/src/components/ui/OriginalDialog.tsx rename to packages/client/src/components/OriginalDialog.tsx diff --git a/client/src/components/ui/Pagination.tsx b/packages/client/src/components/Pagination.tsx similarity index 100% rename from client/src/components/ui/Pagination.tsx rename to packages/client/src/components/Pagination.tsx diff --git a/client/src/components/ui/PixelCard.tsx b/packages/client/src/components/PixelCard.tsx similarity index 96% rename from client/src/components/ui/PixelCard.tsx rename to packages/client/src/components/PixelCard.tsx index 141a3ee55..c1f3d8971 100644 --- a/client/src/components/ui/PixelCard.tsx +++ b/packages/client/src/components/PixelCard.tsx @@ -173,7 +173,7 @@ export default function PixelCard({ const containerRef = useRef(null); const canvasRef = useRef(null); const pixelsRef = useRef([]); - const animationRef = useRef(); + const animationRef = useRef(undefined); const timePrevRef = useRef(performance.now()); const progressRef = useRef(progress); const reducedMotion = useRef( @@ -221,9 +221,11 @@ export default function PixelCard({ let idle = true; for (const p of pixelsRef.current) { if (method === 'appearWithProgress') { - progressRef.current !== undefined - ? p.appearWithProgress(progressRef.current) - : (p.isIdle = true); + if (progressRef.current !== undefined) { + p.appearWithProgress(progressRef.current); + } else { + p.isIdle = true; + } } else { // @ts-ignore dynamic dispatch p[method](); @@ -312,7 +314,9 @@ export default function PixelCard({ useEffect(() => { initPixels(); const obs = new ResizeObserver(initPixels); - containerRef.current && obs.observe(containerRef.current); + if (containerRef.current) { + obs.observe(containerRef.current); + } return () => { obs.disconnect(); cancelAnimationFrame(animationRef.current!); diff --git a/client/src/components/ui/Progress.tsx b/packages/client/src/components/Progress.tsx similarity index 100% rename from client/src/components/ui/Progress.tsx rename to packages/client/src/components/Progress.tsx diff --git a/client/src/components/ui/QuestionMark.tsx b/packages/client/src/components/QuestionMark.tsx similarity index 99% rename from client/src/components/ui/QuestionMark.tsx rename to packages/client/src/components/QuestionMark.tsx index 89785837a..f018226f1 100644 --- a/client/src/components/ui/QuestionMark.tsx +++ b/packages/client/src/components/QuestionMark.tsx @@ -1,4 +1,5 @@ import { cn } from '~/utils'; + export const QuestionMark = ({ className = '' }) => { return ( diff --git a/client/src/components/ui/Resizable.tsx b/packages/client/src/components/Resizable.tsx similarity index 75% rename from client/src/components/ui/Resizable.tsx rename to packages/client/src/components/Resizable.tsx index fc9a32df8..19d3c50e3 100644 --- a/client/src/components/ui/Resizable.tsx +++ b/packages/client/src/components/Resizable.tsx @@ -24,13 +24,13 @@ const ResizableHandle = ({ }) => ( div]:rotate-90', + 'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90', className, )} {...props} > {withHandle && ( -
+
)} @@ -46,13 +46,13 @@ const ResizableHandleAlt = ({ }) => ( div]:rotate-90', + 'group relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90', className, )} {...props} > {withHandle && ( -
+
)} diff --git a/client/src/components/ui/Select.tsx b/packages/client/src/components/Select.tsx similarity index 97% rename from client/src/components/ui/Select.tsx rename to packages/client/src/components/Select.tsx index 671bae443..3301e3577 100644 --- a/client/src/components/ui/Select.tsx +++ b/packages/client/src/components/Select.tsx @@ -4,12 +4,15 @@ import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix import { cn } from '~/utils'; +// @ts-ignore - Radix UI type conflicts with React types const Select = SelectPrimitive.Root; +// @ts-ignore - Radix UI type conflicts with React types const SelectGroup = SelectPrimitive.Group; const SelectValue = SelectPrimitive.Value; +// @ts-ignore - Radix UI type conflicts with React types const SelectTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/client/src/components/ui/SelectDropDown.tsx b/packages/client/src/components/SelectDropDown.tsx similarity index 96% rename from client/src/components/ui/SelectDropDown.tsx rename to packages/client/src/components/SelectDropDown.tsx index 454799192..e9a6027bc 100644 --- a/client/src/components/ui/SelectDropDown.tsx +++ b/packages/client/src/components/SelectDropDown.tsx @@ -8,10 +8,9 @@ import { ListboxOptions, } from '@headlessui/react'; import type { Option, OptionWithIcon, DropdownValueSetter } from '~/common'; -import CheckMark from '~/components/svg/CheckMark'; import { useMultiSearch } from './MultiSearch'; -import { useLocalize } from '~/hooks'; -import { cn } from '~/utils/'; +import { CheckMark } from '~/svgs'; +import { cn } from '~/utils'; type SelectDropDownProps = { id?: string; @@ -75,7 +74,6 @@ function SelectDropDown({ searchPlaceholder, showOptionIcon = false, }: SelectDropDownProps) { - const localize = useLocalize(); const transitionProps = { className: 'top-full mt-3' }; if (showAbove) { transitionProps.className = 'bottom-full mb-3'; @@ -84,9 +82,8 @@ function SelectDropDown({ let title = _title; if (emptyTitle) { title = ''; - } else if (!(title ?? '')) { - title = localize('com_ui_model'); } + const values = availableValues ?? []; // Enable searchable select if enough items are provided. @@ -186,7 +183,7 @@ function SelectDropDown({ - {renderOption()} + {renderOption() as React.JSX.Element} )} - {searchRender} + {searchRender as React.JSX.Element} {options.map((option: string | Option, i: number) => { if (!option) { return null; diff --git a/client/src/components/ui/Separator.tsx b/packages/client/src/components/Separator.tsx similarity index 67% rename from client/src/components/ui/Separator.tsx rename to packages/client/src/components/Separator.tsx index f95d48a79..7ba6d3809 100644 --- a/client/src/components/ui/Separator.tsx +++ b/packages/client/src/components/Separator.tsx @@ -5,18 +5,22 @@ import { cn } from '~/utils'; const Separator = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef + React.ComponentPropsWithoutRef & { + className?: string; + } >(({ className = '', orientation = 'horizontal', decorative = true, ...props }, ref) => ( )); Separator.displayName = SeparatorPrimitive.Root.displayName; diff --git a/client/src/components/ui/Skeleton.tsx b/packages/client/src/components/Skeleton.tsx similarity index 100% rename from client/src/components/ui/Skeleton.tsx rename to packages/client/src/components/Skeleton.tsx diff --git a/packages/client/src/components/Slider.tsx b/packages/client/src/components/Slider.tsx new file mode 100644 index 000000000..4be0f2003 --- /dev/null +++ b/packages/client/src/components/Slider.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; +import { cn } from '~/utils'; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + className?: string; + onDoubleClick?: () => void; + } +>(({ className, onDoubleClick, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/client/src/components/ui/SplitText.spec.tsx b/packages/client/src/components/SplitText.spec.tsx similarity index 100% rename from client/src/components/ui/SplitText.spec.tsx rename to packages/client/src/components/SplitText.spec.tsx diff --git a/client/src/components/ui/SplitText.tsx b/packages/client/src/components/SplitText.tsx similarity index 77% rename from client/src/components/ui/SplitText.tsx rename to packages/client/src/components/SplitText.tsx index cde04eb8a..46dd75736 100644 --- a/client/src/components/ui/SplitText.tsx +++ b/packages/client/src/components/SplitText.tsx @@ -1,6 +1,36 @@ import { useSprings, animated, SpringConfig } from '@react-spring/web'; import { useEffect, useRef, useState } from 'react'; +interface SegmenterOptions { + granularity?: 'grapheme' | 'word' | 'sentence'; + localeMatcher?: 'lookup' | 'best fit'; +} + +interface SegmentData { + segment: string; + index: number; + input: string; + isWordLike?: boolean; +} + +interface Segments { + [Symbol.iterator](): IterableIterator; +} + +interface IntlSegmenter { + segment(input: string): Segments; +} + +interface IntlSegmenterConstructor { + new (locales?: string | string[], options?: SegmenterOptions): IntlSegmenter; +} + +declare global { + interface Intl { + Segmenter: IntlSegmenterConstructor; + } +} + interface SplitTextProps { text?: string; className?: string; @@ -16,12 +46,14 @@ interface SplitTextProps { } const splitGraphemes = (text: string): string[] => { - if (typeof Intl !== 'undefined' && Intl.Segmenter) { - const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); + if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) { + const segmenter = new (Intl as typeof Intl & { Segmenter: IntlSegmenterConstructor }).Segmenter( + 'en', + { granularity: 'grapheme' }, + ); const segments = segmenter.segment(text); - return Array.from(segments).map((s) => s.segment); + return Array.from(segments).map((s: SegmentData) => s.segment); } else { - // Fallback for browsers without Intl.Segmenter return [...text]; } }; @@ -45,12 +77,12 @@ const SplitText: React.FC = ({ const ref = useRef(null); const animatedCount = useRef(0); - const springs = useSprings( + const [springs] = useSprings( letters.length, - letters.map((_, i) => ({ + (i) => ({ from: animationFrom, to: inView - ? async (next: (props: any) => Promise) => { + ? async (next) => { await next(animationTo); animatedCount.current += 1; if (animatedCount.current === letters.length && onLetterAnimationComplete) { @@ -60,7 +92,8 @@ const SplitText: React.FC = ({ : animationFrom, delay: i * delay, config: { easing }, - })), + }), + [inView, text, delay, animationFrom, animationTo, easing, onLetterAnimationComplete], ); useEffect(() => { @@ -118,7 +151,7 @@ const SplitText: React.FC = ({ return ( {letter} diff --git a/client/src/components/ui/Switch.tsx b/packages/client/src/components/Switch.tsx similarity index 100% rename from client/src/components/ui/Switch.tsx rename to packages/client/src/components/Switch.tsx diff --git a/client/src/components/ui/Table.tsx b/packages/client/src/components/Table.tsx similarity index 100% rename from client/src/components/ui/Table.tsx rename to packages/client/src/components/Table.tsx diff --git a/client/src/components/ui/Tabs.tsx b/packages/client/src/components/Tabs.tsx similarity index 100% rename from client/src/components/ui/Tabs.tsx rename to packages/client/src/components/Tabs.tsx diff --git a/client/src/components/ui/Tag.tsx b/packages/client/src/components/Tag.tsx similarity index 76% rename from client/src/components/ui/Tag.tsx rename to packages/client/src/components/Tag.tsx index 38660d11c..4d17617d6 100644 --- a/client/src/components/ui/Tag.tsx +++ b/packages/client/src/components/Tag.tsx @@ -30,17 +30,17 @@ const TagPrimitiveRoot = React.forwardRef( {CancelButton ? CancelButton : onRemove && ( - - )} + + )}
), ); diff --git a/client/src/components/ui/Textarea.tsx b/packages/client/src/components/Textarea.tsx similarity index 96% rename from client/src/components/ui/Textarea.tsx rename to packages/client/src/components/Textarea.tsx index a738bc8b7..f9167e91c 100644 --- a/client/src/components/ui/Textarea.tsx +++ b/packages/client/src/components/Textarea.tsx @@ -1,8 +1,7 @@ /* eslint-disable */ import * as React from 'react'; import TextareaAutosize from 'react-textarea-autosize'; - -import { cn } from '../../utils'; +import { cn } from '~/utils'; export interface TextareaProps extends React.TextareaHTMLAttributes {} diff --git a/client/src/components/ui/TextareaAutosize.tsx b/packages/client/src/components/TextareaAutosize.tsx similarity index 75% rename from client/src/components/ui/TextareaAutosize.tsx rename to packages/client/src/components/TextareaAutosize.tsx index eeaa94d4a..78083ab7a 100644 --- a/client/src/components/ui/TextareaAutosize.tsx +++ b/packages/client/src/components/TextareaAutosize.tsx @@ -1,13 +1,13 @@ -import { useRecoilValue } from 'recoil'; +import { useAtomValue } from 'jotai'; import { forwardRef, useLayoutEffect, useState } from 'react'; import ReactTextareaAutosize from 'react-textarea-autosize'; import type { TextareaAutosizeProps } from 'react-textarea-autosize'; -import store from '~/store'; +import { chatDirectionAtom } from '~/store'; export const TextareaAutosize = forwardRef( (props, ref) => { const [, setIsRerendered] = useState(false); - const chatDirection = useRecoilValue(store.chatDirection).toLowerCase(); + const chatDirection = useAtomValue(chatDirectionAtom).toLowerCase(); useLayoutEffect(() => setIsRerendered(true), []); return ; }, diff --git a/client/src/components/ui/ThemeSelector.tsx b/packages/client/src/components/ThemeSelector.tsx similarity index 91% rename from client/src/components/ui/ThemeSelector.tsx rename to packages/client/src/components/ThemeSelector.tsx index e643f05d8..5954b96e7 100644 --- a/client/src/components/ui/ThemeSelector.tsx +++ b/packages/client/src/components/ThemeSelector.tsx @@ -1,6 +1,6 @@ -import React, { useContext, useCallback, useEffect, useState } from 'react'; +import { useContext, useCallback, useEffect, useState } from 'react'; import { Sun, Moon, Monitor } from 'lucide-react'; -import { ThemeContext } from '~/hooks'; +import { ThemeContext } from '../theme'; declare global { interface Window { @@ -8,8 +8,10 @@ declare global { } } +type ThemeType = 'system' | 'dark' | 'light'; + const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) => void }) => { - const themeIcons = { + const themeIcons: Record = { system: , dark: , light: , @@ -45,7 +47,7 @@ const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) = } }} > - {themeIcons[theme]} + {themeIcons[theme as ThemeType]} ); }; diff --git a/client/src/components/ui/Toast.tsx b/packages/client/src/components/Toast.tsx similarity index 98% rename from client/src/components/ui/Toast.tsx rename to packages/client/src/components/Toast.tsx index 54dcc4ed0..34ca1a053 100644 --- a/client/src/components/ui/Toast.tsx +++ b/packages/client/src/components/Toast.tsx @@ -2,7 +2,7 @@ import * as RadixToast from '@radix-ui/react-toast'; import { NotificationSeverity } from '~/common/types'; import { useToast } from '~/hooks'; -export default function Toast() { +export function Toast() { const { toast, onOpenChange } = useToast(); const severityClassName = { [NotificationSeverity.INFO]: 'border-gray-500 bg-gray-500', diff --git a/client/src/components/ui/Tooltip.tsx b/packages/client/src/components/Tooltip.tsx similarity index 100% rename from client/src/components/ui/Tooltip.tsx rename to packages/client/src/components/Tooltip.tsx diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts new file mode 100644 index 000000000..edaf240dc --- /dev/null +++ b/packages/client/src/components/index.ts @@ -0,0 +1,51 @@ +export * from './Accordion'; +export * from './AnimatedTabs'; +export * from './AlertDialog'; +export * from './Breadcrumb'; +export * from './Button'; +export * from './Checkbox'; +export * from './DataTableColumnHeader'; +export * from './Dialog'; +export * from './DropdownMenu'; +export * from './HoverCard'; +export * from './Input'; +export * from './InputNumber'; +export * from './Label'; +export * from './OriginalDialog'; +export * from './QuestionMark'; +export * from './Slider'; +export * from './Separator'; +export * from './InputCombobox'; +export * from './Skeleton'; +export * from './Switch'; +export * from './Table'; +export * from './Tabs'; +export * from './Tag'; +export * from './Textarea'; +export * from './TextareaAutosize'; +export * from './Toast'; +export * from './Tooltip'; +export * from './Pagination'; +export * from './Progress'; +export * from './InputOTP'; +export * from './MultiSearch'; +export * from './Resizable'; +export { default as Badge } from './Badge'; +export { default as Combobox } from './Combobox'; +export { default as Dropdown } from './Dropdown'; +export { default as SplitText } from './SplitText'; +export { default as DataTable } from './DataTable'; +export { default as FormInput } from './FormInput'; +export { default as PixelCard } from './PixelCard'; +export { default as FileUpload } from './FileUpload'; +export { default as MultiSelect } from './MultiSelect'; +export { default as DropdownPopup } from './DropdownPopup'; +export { default as DelayedRender } from './DelayedRender'; +export { default as ThemeSelector } from './ThemeSelector'; +export { default as CheckboxButton } from './CheckboxButton'; +export { default as DialogTemplate } from './DialogTemplate'; +export { default as SelectDropDown } from './SelectDropDown'; +export { default as ControlCombobox } from './ControlCombobox'; +export { default as OGDialogTemplate } from './OGDialogTemplate'; +export { default as InputWithDropdown } from './InputWithDropDown'; +export { default as AnimatedSearchInput } from './AnimatedSearchInput'; diff --git a/client/src/hooks/ThemeContext.tsx b/packages/client/src/hooks/ThemeContext.old.tsx similarity index 89% rename from client/src/hooks/ThemeContext.tsx rename to packages/client/src/hooks/ThemeContext.old.tsx index 76b272598..74516fcab 100644 --- a/client/src/hooks/ThemeContext.tsx +++ b/packages/client/src/hooks/ThemeContext.old.tsx @@ -1,9 +1,9 @@ //ThemeContext.js // source: https://plainenglish.io/blog/light-and-dark-mode-in-react-web-application-with-tailwind-css-89674496b942 -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import React, { createContext, useState, useEffect } from 'react'; import { getInitialTheme, applyFontSize } from '~/utils'; -import store from '~/store'; +import { fontSizeAtom } from '~/store'; type ProviderValue = { theme: string; @@ -26,9 +26,15 @@ export const isDark = (theme: string): boolean => { export const ThemeContext = createContext(defaultContextValue); -export const ThemeProvider = ({ initialTheme, children }) => { +export const ThemeProvider = ({ + initialTheme, + children, +}: { + initialTheme?: string; + children: React.ReactNode; +}) => { const [theme, setTheme] = useState(getInitialTheme); - const setFontSize = useSetRecoilState(store.fontSize); + const setFontSize = useSetAtom(fontSizeAtom); const rawSetTheme = (rawTheme: string) => { const root = window.document.documentElement; diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts new file mode 100644 index 000000000..019dae0ee --- /dev/null +++ b/packages/client/src/hooks/index.ts @@ -0,0 +1,10 @@ +// Theme exports are now handled by the theme module in the main index.ts + +export type { TranslationKeys } from './useLocalize'; + +export { default as useToast } from './useToast'; +export { default as useCombobox } from './useCombobox'; +export { default as useLocalize } from './useLocalize'; +export { default as useMediaQuery } from './useMediaQuery'; +export { default as useDelayedRender } from './useDelayedRender'; +export { default as useOnClickOutside } from './useOnClickOutside'; diff --git a/client/src/hooks/Input/useCombobox.ts b/packages/client/src/hooks/useCombobox.ts similarity index 100% rename from client/src/hooks/Input/useCombobox.ts rename to packages/client/src/hooks/useCombobox.ts diff --git a/client/src/hooks/useDelayedRender.tsx b/packages/client/src/hooks/useDelayedRender.tsx similarity index 100% rename from client/src/hooks/useDelayedRender.tsx rename to packages/client/src/hooks/useDelayedRender.tsx diff --git a/packages/client/src/hooks/useLocalize.ts b/packages/client/src/hooks/useLocalize.ts new file mode 100644 index 000000000..83366cff1 --- /dev/null +++ b/packages/client/src/hooks/useLocalize.ts @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; +import { TOptions } from 'i18next'; +import { useAtomValue } from 'jotai'; +import { useTranslation } from 'react-i18next'; +import { resources } from '~/locales/i18n'; +import { langAtom } from '~/store'; + +export type TranslationKeys = keyof typeof resources.en.translation; + +export default function useLocalize() { + const lang = useAtomValue(langAtom); + const { t, i18n } = useTranslation(); + + useEffect(() => { + if (i18n.language !== lang) { + i18n.changeLanguage(lang); + } + }, [lang, i18n]); + + return (phraseKey: TranslationKeys, options?: TOptions) => t(phraseKey, options); +} diff --git a/client/src/hooks/useMediaQuery.tsx b/packages/client/src/hooks/useMediaQuery.tsx similarity index 100% rename from client/src/hooks/useMediaQuery.tsx rename to packages/client/src/hooks/useMediaQuery.tsx diff --git a/client/src/hooks/useOnClickOutside.ts b/packages/client/src/hooks/useOnClickOutside.ts similarity index 100% rename from client/src/hooks/useOnClickOutside.ts rename to packages/client/src/hooks/useOnClickOutside.ts diff --git a/client/src/hooks/useToast.ts b/packages/client/src/hooks/useToast.ts similarity index 87% rename from client/src/hooks/useToast.ts rename to packages/client/src/hooks/useToast.ts index 92f7bbfe1..9937c640d 100644 --- a/client/src/hooks/useToast.ts +++ b/packages/client/src/hooks/useToast.ts @@ -1,11 +1,11 @@ -import { useRecoilState } from 'recoil'; +import { useAtom } from 'jotai'; import { useRef, useEffect } from 'react'; import type { TShowToast } from '~/common'; import { NotificationSeverity } from '~/common'; -import store from '~/store'; +import { toastState, type ToastState } from '~/store'; export default function useToast(showDelay = 100) { - const [toast, setToast] = useRecoilState(store.toastState); + const [toast, setToast] = useAtom(toastState); const showTimerRef = useRef(null); const hideTimerRef = useRef(null); @@ -45,7 +45,7 @@ export default function useToast(showDelay = 100) { }); // Hides the toast after the specified duration hideTimerRef.current = window.setTimeout(() => { - setToast((prevToast) => ({ ...prevToast, open: false })); + setToast((prevToast: ToastState) => ({ ...prevToast, open: false })); }, duration); }, showDelay); }; diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts new file mode 100644 index 000000000..709697c99 --- /dev/null +++ b/packages/client/src/index.ts @@ -0,0 +1,24 @@ +// Components +export * from './components'; + +// Hooks +export * from './hooks'; + +// Common +export * from './common'; +export * from './common/types'; + +// Store +export * from './store'; + +// SVGs +export * from './svgs'; + +// Utils +export * from './utils'; + +// Providers +export * from './Providers'; + +// Theme +export * from './theme'; diff --git a/packages/client/src/locales/Translation.spec.ts b/packages/client/src/locales/Translation.spec.ts new file mode 100644 index 000000000..fc3de6e0e --- /dev/null +++ b/packages/client/src/locales/Translation.spec.ts @@ -0,0 +1,47 @@ +import i18n from './i18n'; +import English from './en/translation.json'; +import French from './fr/translation.json'; +import Spanish from './es/translation.json'; + +describe('i18next translation tests', () => { + // Ensure i18next is initialized before any tests run + beforeAll(async () => { + if (!i18n.isInitialized) { + await i18n.init(); + } + }); + + it('should return the correct translation for a valid key in English', () => { + i18n.changeLanguage('en'); + expect(i18n.t('com_ui_examples')).toBe(English.com_ui_examples); + }); + + it('should return the correct translation for a valid key in French', () => { + i18n.changeLanguage('fr'); + expect(i18n.t('com_ui_examples')).toBe(French.com_ui_examples); + }); + + it('should return the correct translation for a valid key in Spanish', () => { + i18n.changeLanguage('es'); + expect(i18n.t('com_ui_examples')).toBe(Spanish.com_ui_examples); + }); + + it('should fallback to English for an invalid language code', () => { + // When an invalid language is provided, i18next should fallback to English + i18n.changeLanguage('invalid-code'); + expect(i18n.t('com_ui_examples')).toBe(English.com_ui_examples); + }); + + it('should return the key itself for an invalid key', () => { + i18n.changeLanguage('en'); + expect(i18n.t('invalid-key')).toBe('invalid-key'); // Returns the key itself + }); + + it('should correctly format placeholders in the translation', () => { + i18n.changeLanguage('en'); + expect(i18n.t('com_endpoint_default_with_num', { 0: 'John' })).toBe('default: John'); + + i18n.changeLanguage('fr'); + expect(i18n.t('com_endpoint_default_with_num', { 0: 'Marie' })).toBe('par défaut : Marie'); + }); +}); diff --git a/packages/client/src/locales/ar/translation.json b/packages/client/src/locales/ar/translation.json new file mode 100644 index 000000000..aa0372363 --- /dev/null +++ b/packages/client/src/locales/ar/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "إلغاء" +} diff --git a/packages/client/src/locales/ca/translation.json b/packages/client/src/locales/ca/translation.json new file mode 100644 index 000000000..8c42ae78f --- /dev/null +++ b/packages/client/src/locales/ca/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Cancel·la" +} diff --git a/packages/client/src/locales/cs/translation.json b/packages/client/src/locales/cs/translation.json new file mode 100644 index 000000000..4d34a884c --- /dev/null +++ b/packages/client/src/locales/cs/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Zrušit" +} diff --git a/packages/client/src/locales/da/translation.json b/packages/client/src/locales/da/translation.json new file mode 100644 index 000000000..796263989 --- /dev/null +++ b/packages/client/src/locales/da/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Annuller" +} diff --git a/packages/client/src/locales/de/translation.json b/packages/client/src/locales/de/translation.json new file mode 100644 index 000000000..b1d864561 --- /dev/null +++ b/packages/client/src/locales/de/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Abbrechen" +} diff --git a/packages/client/src/locales/en/translation.json b/packages/client/src/locales/en/translation.json new file mode 100644 index 000000000..de39a3005 --- /dev/null +++ b/packages/client/src/locales/en/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Cancel" +} diff --git a/packages/client/src/locales/es/translation.json b/packages/client/src/locales/es/translation.json new file mode 100644 index 000000000..c831d5737 --- /dev/null +++ b/packages/client/src/locales/es/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Cancelar" +} diff --git a/packages/client/src/locales/et/translation.json b/packages/client/src/locales/et/translation.json new file mode 100644 index 000000000..fffcde416 --- /dev/null +++ b/packages/client/src/locales/et/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Tühista" +} diff --git a/packages/client/src/locales/fa/translation.json b/packages/client/src/locales/fa/translation.json new file mode 100644 index 000000000..73e57d028 --- /dev/null +++ b/packages/client/src/locales/fa/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "لغو کنید" +} diff --git a/packages/client/src/locales/fi/translation.json b/packages/client/src/locales/fi/translation.json new file mode 100644 index 000000000..00797137b --- /dev/null +++ b/packages/client/src/locales/fi/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Peruuta" +} diff --git a/packages/client/src/locales/fr/translation.json b/packages/client/src/locales/fr/translation.json new file mode 100644 index 000000000..1d6265d6c --- /dev/null +++ b/packages/client/src/locales/fr/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Annuler" +} diff --git a/packages/client/src/locales/he/translation.json b/packages/client/src/locales/he/translation.json new file mode 100644 index 000000000..5bf5a86d9 --- /dev/null +++ b/packages/client/src/locales/he/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "בטל" +} diff --git a/packages/client/src/locales/hu/translation.json b/packages/client/src/locales/hu/translation.json new file mode 100644 index 000000000..8616ee927 --- /dev/null +++ b/packages/client/src/locales/hu/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Mégse" +} diff --git a/packages/client/src/locales/i18n.ts b/packages/client/src/locales/i18n.ts new file mode 100644 index 000000000..f463723a4 --- /dev/null +++ b/packages/client/src/locales/i18n.ts @@ -0,0 +1,87 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import translationEn from './en/translation.json'; +import translationAr from './ar/translation.json'; +import translationCa from './ca/translation.json'; +import translationCs from './cs/translation.json'; +import translationDa from './da/translation.json'; +import translationDe from './de/translation.json'; +import translationEs from './es/translation.json'; +import translationEt from './et/translation.json'; +import translationFa from './fa/translation.json'; +import translationFr from './fr/translation.json'; +import translationIt from './it/translation.json'; +import translationPl from './pl/translation.json'; +import translationPt_BR from './pt-BR/translation.json'; +import translationPt_PT from './pt-PT/translation.json'; +import translationRu from './ru/translation.json'; +import translationJa from './ja/translation.json'; +import translationKa from './ka/translation.json'; +import translationSv from './sv/translation.json'; +import translationKo from './ko/translation.json'; +import translationTh from './th/translation.json'; +import translationTr from './tr/translation.json'; +import translationVi from './vi/translation.json'; +import translationNl from './nl/translation.json'; +import translationId from './id/translation.json'; +import translationHe from './he/translation.json'; +import translationHu from './hu/translation.json'; +import translationFi from './fi/translation.json'; +import translationZh_Hans from './zh-Hans/translation.json'; +import translationZh_Hant from './zh-Hant/translation.json'; + +export const defaultNS = 'translation'; + +export const resources = { + en: { translation: translationEn }, + ar: { translation: translationAr }, + ca: { translation: translationCa }, + cs: { translation: translationCs }, + 'zh-Hans': { translation: translationZh_Hans }, + 'zh-Hant': { translation: translationZh_Hant }, + da: { translation: translationDa }, + de: { translation: translationDe }, + es: { translation: translationEs }, + et: { translation: translationEt }, + fa: { translation: translationFa }, + fr: { translation: translationFr }, + it: { translation: translationIt }, + pl: { translation: translationPl }, + 'pt-BR': { translation: translationPt_BR }, + 'pt-PT': { translation: translationPt_PT }, + ru: { translation: translationRu }, + ja: { translation: translationJa }, + ka: { translation: translationKa }, + sv: { translation: translationSv }, + ko: { translation: translationKo }, + th: { translation: translationTh }, + tr: { translation: translationTr }, + vi: { translation: translationVi }, + nl: { translation: translationNl }, + id: { translation: translationId }, + he: { translation: translationHe }, + hu: { translation: translationHu }, + fi: { translation: translationFi }, +} as const; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: { + 'zh-TW': ['zh-Hant', 'en'], + 'zh-HK': ['zh-Hant', 'en'], + zh: ['zh-Hans', 'en'], + default: ['en'], + }, + fallbackNS: 'translation', + ns: ['translation'], + debug: false, + defaultNS, + resources, + interpolation: { escapeValue: false }, + }); + +export default i18n; diff --git a/packages/client/src/locales/id/translation.json b/packages/client/src/locales/id/translation.json new file mode 100644 index 000000000..24d3467bd --- /dev/null +++ b/packages/client/src/locales/id/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Batal" +} diff --git a/packages/client/src/locales/it/translation.json b/packages/client/src/locales/it/translation.json new file mode 100644 index 000000000..cc082b4a2 --- /dev/null +++ b/packages/client/src/locales/it/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Annulla" +} diff --git a/packages/client/src/locales/ja/translation.json b/packages/client/src/locales/ja/translation.json new file mode 100644 index 000000000..4400b0282 --- /dev/null +++ b/packages/client/src/locales/ja/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "キャンセル" +} diff --git a/packages/client/src/locales/ka/translation.json b/packages/client/src/locales/ka/translation.json new file mode 100644 index 000000000..0c90d6bf7 --- /dev/null +++ b/packages/client/src/locales/ka/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "გაუქმება" +} diff --git a/packages/client/src/locales/ko/translation.json b/packages/client/src/locales/ko/translation.json new file mode 100644 index 000000000..96650ed6a --- /dev/null +++ b/packages/client/src/locales/ko/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "취소" +} diff --git a/packages/client/src/locales/nl/translation.json b/packages/client/src/locales/nl/translation.json new file mode 100644 index 000000000..2ec1fb8c6 --- /dev/null +++ b/packages/client/src/locales/nl/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Annuleren" +} diff --git a/packages/client/src/locales/pl/translation.json b/packages/client/src/locales/pl/translation.json new file mode 100644 index 000000000..9c05e4cd7 --- /dev/null +++ b/packages/client/src/locales/pl/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Anuluj" +} diff --git a/packages/client/src/locales/pt-BR/translation.json b/packages/client/src/locales/pt-BR/translation.json new file mode 100644 index 000000000..c831d5737 --- /dev/null +++ b/packages/client/src/locales/pt-BR/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Cancelar" +} diff --git a/packages/client/src/locales/pt-PT/translation.json b/packages/client/src/locales/pt-PT/translation.json new file mode 100644 index 000000000..c831d5737 --- /dev/null +++ b/packages/client/src/locales/pt-PT/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Cancelar" +} diff --git a/packages/client/src/locales/ru/translation.json b/packages/client/src/locales/ru/translation.json new file mode 100644 index 000000000..e3f16ae66 --- /dev/null +++ b/packages/client/src/locales/ru/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Отмена" +} diff --git a/packages/client/src/locales/sv/translation.json b/packages/client/src/locales/sv/translation.json new file mode 100644 index 000000000..c606ec621 --- /dev/null +++ b/packages/client/src/locales/sv/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Avbryt" +} diff --git a/packages/client/src/locales/th/translation.json b/packages/client/src/locales/th/translation.json new file mode 100644 index 000000000..b78cd2cc2 --- /dev/null +++ b/packages/client/src/locales/th/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "ยกเลิก" +} diff --git a/packages/client/src/locales/tr/translation.json b/packages/client/src/locales/tr/translation.json new file mode 100644 index 000000000..10468437f --- /dev/null +++ b/packages/client/src/locales/tr/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "İptal" +} diff --git a/packages/client/src/locales/vi/translation.json b/packages/client/src/locales/vi/translation.json new file mode 100644 index 000000000..64634b0df --- /dev/null +++ b/packages/client/src/locales/vi/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "Hủy" +} diff --git a/packages/client/src/locales/zh-Hans/translation.json b/packages/client/src/locales/zh-Hans/translation.json new file mode 100644 index 000000000..d763c638c --- /dev/null +++ b/packages/client/src/locales/zh-Hans/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "取消" +} diff --git a/packages/client/src/locales/zh-Hant/translation.json b/packages/client/src/locales/zh-Hant/translation.json new file mode 100644 index 000000000..d763c638c --- /dev/null +++ b/packages/client/src/locales/zh-Hant/translation.json @@ -0,0 +1,3 @@ +{ + "com_ui_cancel": "取消" +} diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts new file mode 100644 index 000000000..6e23be6e0 --- /dev/null +++ b/packages/client/src/store.ts @@ -0,0 +1,20 @@ +import { atom } from 'jotai'; +import { NotificationSeverity } from '~/common'; + +export const langAtom = atom('en'); +export const chatDirectionAtom = atom('ltr'); +export const fontSizeAtom = atom('text-base'); + +export type ToastState = { + open: boolean; + message: string; + severity: NotificationSeverity; + showIcon: boolean; +}; + +export const toastState = atom({ + open: false, + message: '', + severity: NotificationSeverity.SUCCESS, + showIcon: true, +}); diff --git a/client/src/components/svg/AnthropicIcon.tsx b/packages/client/src/svgs/AnthropicIcon.tsx similarity index 100% rename from client/src/components/svg/AnthropicIcon.tsx rename to packages/client/src/svgs/AnthropicIcon.tsx diff --git a/client/src/components/svg/AnthropicMinimalIcon.tsx b/packages/client/src/svgs/AnthropicMinimalIcon.tsx similarity index 100% rename from client/src/components/svg/AnthropicMinimalIcon.tsx rename to packages/client/src/svgs/AnthropicMinimalIcon.tsx diff --git a/client/src/components/svg/AppleIcon.tsx b/packages/client/src/svgs/AppleIcon.tsx similarity index 99% rename from client/src/components/svg/AppleIcon.tsx rename to packages/client/src/svgs/AppleIcon.tsx index 4f1ff404d..e84b70386 100644 --- a/client/src/components/svg/AppleIcon.tsx +++ b/packages/client/src/svgs/AppleIcon.tsx @@ -15,4 +15,4 @@ export default function AppleIcon() { /> ); -} \ No newline at end of file +} diff --git a/client/src/components/svg/ArchiveIcon.tsx b/packages/client/src/svgs/ArchiveIcon.tsx similarity index 100% rename from client/src/components/svg/ArchiveIcon.tsx rename to packages/client/src/svgs/ArchiveIcon.tsx diff --git a/client/src/components/svg/AssistantIcon.tsx b/packages/client/src/svgs/AssistantIcon.tsx similarity index 100% rename from client/src/components/svg/AssistantIcon.tsx rename to packages/client/src/svgs/AssistantIcon.tsx diff --git a/client/src/components/svg/AttachmentIcon.tsx b/packages/client/src/svgs/AttachmentIcon.tsx similarity index 100% rename from client/src/components/svg/AttachmentIcon.tsx rename to packages/client/src/svgs/AttachmentIcon.tsx diff --git a/client/src/components/svg/AzureMinimalIcon.tsx b/packages/client/src/svgs/AzureMinimalIcon.tsx similarity index 99% rename from client/src/components/svg/AzureMinimalIcon.tsx rename to packages/client/src/svgs/AzureMinimalIcon.tsx index 171a0c1eb..bd73c5d13 100644 --- a/client/src/components/svg/AzureMinimalIcon.tsx +++ b/packages/client/src/svgs/AzureMinimalIcon.tsx @@ -1,4 +1,3 @@ - import { cn } from '~/utils/'; export default function AzureMinimalIcon({ diff --git a/client/src/components/svg/BedrockIcon.tsx b/packages/client/src/svgs/BedrockIcon.tsx similarity index 100% rename from client/src/components/svg/BedrockIcon.tsx rename to packages/client/src/svgs/BedrockIcon.tsx diff --git a/client/src/components/svg/BirthdayIcon.tsx b/packages/client/src/svgs/BirthdayIcon.tsx similarity index 100% rename from client/src/components/svg/BirthdayIcon.tsx rename to packages/client/src/svgs/BirthdayIcon.tsx diff --git a/client/src/components/svg/Blocks.tsx b/packages/client/src/svgs/Blocks.tsx similarity index 100% rename from client/src/components/svg/Blocks.tsx rename to packages/client/src/svgs/Blocks.tsx diff --git a/client/src/components/svg/CautionIcon.tsx b/packages/client/src/svgs/CautionIcon.tsx similarity index 100% rename from client/src/components/svg/CautionIcon.tsx rename to packages/client/src/svgs/CautionIcon.tsx diff --git a/client/src/components/svg/ChatGPTMinimalIcon.tsx b/packages/client/src/svgs/ChatGPTMinimalIcon.tsx similarity index 100% rename from client/src/components/svg/ChatGPTMinimalIcon.tsx rename to packages/client/src/svgs/ChatGPTMinimalIcon.tsx diff --git a/client/src/components/svg/ChatIcon.tsx b/packages/client/src/svgs/ChatIcon.tsx similarity index 100% rename from client/src/components/svg/ChatIcon.tsx rename to packages/client/src/svgs/ChatIcon.tsx diff --git a/client/src/components/svg/CheckMark.tsx b/packages/client/src/svgs/CheckMark.tsx similarity index 100% rename from client/src/components/svg/CheckMark.tsx rename to packages/client/src/svgs/CheckMark.tsx diff --git a/client/src/components/svg/CircleHelpIcon.tsx b/packages/client/src/svgs/CircleHelpIcon.tsx similarity index 100% rename from client/src/components/svg/CircleHelpIcon.tsx rename to packages/client/src/svgs/CircleHelpIcon.tsx diff --git a/client/src/components/svg/Clipboard.tsx b/packages/client/src/svgs/Clipboard.tsx similarity index 100% rename from client/src/components/svg/Clipboard.tsx rename to packages/client/src/svgs/Clipboard.tsx diff --git a/client/src/components/svg/CodeyIcon.tsx b/packages/client/src/svgs/CodeyIcon.tsx similarity index 100% rename from client/src/components/svg/CodeyIcon.tsx rename to packages/client/src/svgs/CodeyIcon.tsx diff --git a/client/src/components/svg/ContinueIcon.tsx b/packages/client/src/svgs/ContinueIcon.tsx similarity index 100% rename from client/src/components/svg/ContinueIcon.tsx rename to packages/client/src/svgs/ContinueIcon.tsx diff --git a/client/src/components/svg/ConvoIcon.tsx b/packages/client/src/svgs/ConvoIcon.tsx similarity index 100% rename from client/src/components/svg/ConvoIcon.tsx rename to packages/client/src/svgs/ConvoIcon.tsx diff --git a/client/src/components/svg/CrossIcon.tsx b/packages/client/src/svgs/CrossIcon.tsx similarity index 100% rename from client/src/components/svg/CrossIcon.tsx rename to packages/client/src/svgs/CrossIcon.tsx diff --git a/client/src/components/svg/CustomMinimalIcon.tsx b/packages/client/src/svgs/CustomMinimalIcon.tsx similarity index 100% rename from client/src/components/svg/CustomMinimalIcon.tsx rename to packages/client/src/svgs/CustomMinimalIcon.tsx diff --git a/client/src/components/svg/DarkModeIcon.tsx b/packages/client/src/svgs/DarkModeIcon.tsx similarity index 100% rename from client/src/components/svg/DarkModeIcon.tsx rename to packages/client/src/svgs/DarkModeIcon.tsx diff --git a/client/src/components/svg/DataIcon.tsx b/packages/client/src/svgs/DataIcon.tsx similarity index 100% rename from client/src/components/svg/DataIcon.tsx rename to packages/client/src/svgs/DataIcon.tsx diff --git a/client/src/components/svg/DiscordIcon.tsx b/packages/client/src/svgs/DiscordIcon.tsx similarity index 100% rename from client/src/components/svg/DiscordIcon.tsx rename to packages/client/src/svgs/DiscordIcon.tsx diff --git a/client/src/components/svg/DislikeIcon.tsx b/packages/client/src/svgs/DislikeIcon.tsx similarity index 100% rename from client/src/components/svg/DislikeIcon.tsx rename to packages/client/src/svgs/DislikeIcon.tsx diff --git a/client/src/components/svg/DotsIcon.tsx b/packages/client/src/svgs/DotsIcon.tsx similarity index 100% rename from client/src/components/svg/DotsIcon.tsx rename to packages/client/src/svgs/DotsIcon.tsx diff --git a/client/src/components/svg/EditIcon.tsx b/packages/client/src/svgs/EditIcon.tsx similarity index 100% rename from client/src/components/svg/EditIcon.tsx rename to packages/client/src/svgs/EditIcon.tsx diff --git a/client/src/components/svg/ExperimentIcon.tsx b/packages/client/src/svgs/ExperimentIcon.tsx similarity index 100% rename from client/src/components/svg/ExperimentIcon.tsx rename to packages/client/src/svgs/ExperimentIcon.tsx diff --git a/client/src/components/svg/FacebookIcon.tsx b/packages/client/src/svgs/FacebookIcon.tsx similarity index 100% rename from client/src/components/svg/FacebookIcon.tsx rename to packages/client/src/svgs/FacebookIcon.tsx diff --git a/client/src/components/svg/GPTIcon.tsx b/packages/client/src/svgs/GPTIcon.tsx similarity index 100% rename from client/src/components/svg/GPTIcon.tsx rename to packages/client/src/svgs/GPTIcon.tsx diff --git a/client/src/components/svg/GearIcon.tsx b/packages/client/src/svgs/GearIcon.tsx similarity index 100% rename from client/src/components/svg/GearIcon.tsx rename to packages/client/src/svgs/GearIcon.tsx diff --git a/client/src/components/svg/GeminiIcon.tsx b/packages/client/src/svgs/GeminiIcon.tsx similarity index 100% rename from client/src/components/svg/GeminiIcon.tsx rename to packages/client/src/svgs/GeminiIcon.tsx diff --git a/client/src/components/svg/GithubIcon.tsx b/packages/client/src/svgs/GithubIcon.tsx similarity index 100% rename from client/src/components/svg/GithubIcon.tsx rename to packages/client/src/svgs/GithubIcon.tsx diff --git a/client/src/components/svg/GoogleIcon.tsx b/packages/client/src/svgs/GoogleIcon.tsx similarity index 100% rename from client/src/components/svg/GoogleIcon.tsx rename to packages/client/src/svgs/GoogleIcon.tsx diff --git a/client/src/components/svg/GoogleIconChat.tsx b/packages/client/src/svgs/GoogleIconChat.tsx similarity index 100% rename from client/src/components/svg/GoogleIconChat.tsx rename to packages/client/src/svgs/GoogleIconChat.tsx diff --git a/client/src/components/svg/GoogleMinimalIcon.tsx b/packages/client/src/svgs/GoogleMinimalIcon.tsx similarity index 100% rename from client/src/components/svg/GoogleMinimalIcon.tsx rename to packages/client/src/svgs/GoogleMinimalIcon.tsx diff --git a/client/src/components/svg/LightModeIcon.tsx b/packages/client/src/svgs/LightModeIcon.tsx similarity index 100% rename from client/src/components/svg/LightModeIcon.tsx rename to packages/client/src/svgs/LightModeIcon.tsx diff --git a/client/src/components/svg/LightningIcon.tsx b/packages/client/src/svgs/LightningIcon.tsx similarity index 100% rename from client/src/components/svg/LightningIcon.tsx rename to packages/client/src/svgs/LightningIcon.tsx diff --git a/client/src/components/svg/LikeIcon.tsx b/packages/client/src/svgs/LikeIcon.tsx similarity index 100% rename from client/src/components/svg/LikeIcon.tsx rename to packages/client/src/svgs/LikeIcon.tsx diff --git a/client/src/components/svg/LinkIcon.tsx b/packages/client/src/svgs/LinkIcon.tsx similarity index 100% rename from client/src/components/svg/LinkIcon.tsx rename to packages/client/src/svgs/LinkIcon.tsx diff --git a/client/src/components/svg/ListeningIcon.tsx b/packages/client/src/svgs/ListeningIcon.tsx similarity index 78% rename from client/src/components/svg/ListeningIcon.tsx rename to packages/client/src/svgs/ListeningIcon.tsx index e81c7e37e..ef8de6a4e 100644 --- a/client/src/components/svg/ListeningIcon.tsx +++ b/packages/client/src/svgs/ListeningIcon.tsx @@ -1,6 +1,10 @@ import { cn } from '~/utils/'; -export default function ListeningIcon({ className }) { +type ListeningIconProps = { + className?: string; +}; + +export default function ListeningIcon({ className }: ListeningIconProps) { return ( + + + ); +} +``` + +### 3. Set Up Your Base CSS + +Ensure your app has CSS variables defined as fallbacks: + +```css +/* style.css */ +:root { + --white: #fff; + --gray-800: #212121; + --gray-100: #ececec; + /* ... other color definitions */ +} + +html { + --text-primary: var(--gray-800); + --surface-primary: var(--white); + /* ... other theme variables */ +} + +.dark { + --text-primary: var(--gray-100); + --surface-primary: var(--gray-900); + /* ... other dark theme variables */ +} +``` + +### 4. Configure Tailwind + +Update your `tailwind.config.js`: + +```js +module.exports = { + content: [ + './src/**/*.{js,jsx,ts,tsx}', + // Include component library files + './node_modules/@librechat/client/dist/**/*.js', + ], + darkMode: ['class'], + theme: { + extend: { + colors: { + // Map CSS variables to Tailwind colors + 'text-primary': 'var(--text-primary)', + 'surface-primary': 'var(--surface-primary)', + 'brand-purple': 'var(--brand-purple)', + // ... other colors + }, + }, + }, +}; +``` + +### 5. Use Theme Colors in Components + +```tsx +function MyComponent() { + return ( +
+

Hello World

+ +
+ ); +} +``` + +## Available Theme Colors + +### Text Colors +- `text-text-primary` - Primary text color +- `text-text-secondary` - Secondary text color +- `text-text-secondary-alt` - Alternative secondary text +- `text-text-tertiary` - Tertiary text color +- `text-text-warning` - Warning text color + +### Surface Colors +- `bg-surface-primary` - Primary background +- `bg-surface-secondary` - Secondary background +- `bg-surface-tertiary` - Tertiary background +- `bg-surface-submit` - Submit button background +- `bg-surface-destructive` - Destructive action background +- `bg-surface-dialog` - Dialog/modal background +- `bg-surface-chat` - Chat interface background + +### Border Colors +- `border-border-light` - Light border +- `border-border-medium` - Medium border +- `border-border-heavy` - Heavy border +- `border-border-xheavy` - Extra heavy border + +### Other Colors +- `bg-brand-purple` - Brand purple color +- `bg-presentation` - Presentation background +- `ring-ring-primary` - Focus ring color + +## Creating Custom Themes + +### 1. Define Your Theme + +```tsx +import { IThemeRGB } from '@librechat/client'; + +export const customTheme: IThemeRGB = { + 'rgb-text-primary': '0 0 0', // Black + 'rgb-text-secondary': '100 100 100', // Gray + 'rgb-surface-primary': '255 255 255', // White + 'rgb-surface-submit': '0 128 0', // Green + 'rgb-brand-purple': '138 43 226', // Blue Violet + // ... define other colors +}; +``` + +### 2. Use Your Custom Theme + +```tsx +import { ThemeProvider } from '@librechat/client'; +import { customTheme } from './themes/custom'; + +function App() { + return ( + + + + ); +} +``` + +## Environment Variable Themes + +Load theme colors from environment variables: + +### 1. Create Environment Variables + +```env +# .env.local +REACT_APP_THEME_BRAND_PURPLE=171 104 255 +REACT_APP_THEME_TEXT_PRIMARY=33 33 33 +REACT_APP_THEME_TEXT_SECONDARY=66 66 66 +REACT_APP_THEME_SURFACE_PRIMARY=255 255 255 +REACT_APP_THEME_SURFACE_SUBMIT=4 120 87 +``` + +### 2. Create a Theme Loader + +```tsx +function getThemeFromEnv(): IThemeRGB | undefined { + // Check if any theme environment variables are set + const hasThemeEnvVars = Object.keys(process.env).some(key => + key.startsWith('REACT_APP_THEME_') + ); + + if (!hasThemeEnvVars) { + return undefined; // Use default themes + } + + return { + 'rgb-text-primary': process.env.REACT_APP_THEME_TEXT_PRIMARY || '33 33 33', + 'rgb-brand-purple': process.env.REACT_APP_THEME_BRAND_PURPLE || '171 104 255', + // ... other colors + }; +} +``` + +### 3. Apply Environment Theme + +```tsx + + + +``` + +## Dark/Light Mode + +The ThemeProvider handles dark/light mode automatically: + +### Using the Theme Hook + +```tsx +import { useTheme } from '@librechat/client'; + +function ThemeToggle() { + const { theme, setTheme } = useTheme(); + + return ( + + ); +} +``` + +### Theme Options +- `'light'` - Force light mode +- `'dark'` - Force dark mode +- `'system'` - Follow system preference + +## Migration Guide + +If you're migrating from an older theme system: + +### 1. Update Imports + +**Before:** +```tsx +import { ThemeContext, ThemeProvider } from '~/hooks/ThemeContext'; +``` + +**After:** +```tsx +import { ThemeContext, ThemeProvider } from '@librechat/client'; +``` + +### 2. Update ThemeProvider Usage + +The new ThemeProvider is backward compatible but adds new capabilities: + +```tsx + + + +``` + +### 3. Existing Components + +Components using ThemeContext continue to work without changes: + +```tsx +// This still works! +const { theme, setTheme } = useContext(ThemeContext); +``` + +## Implementation Details + +### File Structure +``` +packages/client/src/theme/ +├── context/ +│ └── ThemeProvider.tsx # Main theme provider +├── types/ +│ └── index.ts # TypeScript interfaces +├── themes/ +│ ├── default.ts # Light theme colors +│ ├── dark.ts # Dark theme colors +│ └── index.ts # Theme exports +├── utils/ +│ ├── applyTheme.ts # Apply CSS variables +│ ├── tailwindConfig.ts # Tailwind helpers +│ └── createTailwindColors.js +├── README.md # This documentation +└── index.ts # Main exports +``` + +### CSS Variable Format + +The theme system uses RGB values in CSS variables: +- CSS Variable: `--text-primary: rgb(33 33 33)` +- Theme Definition: `'rgb-text-primary': '33 33 33'` +- Tailwind Usage: `text-text-primary` + +### RGB Format Requirements + +All color values must be in space-separated RGB format: +- ✅ Correct: `'255 255 255'` +- ❌ Incorrect: `'#ffffff'` or `'rgb(255, 255, 255)'` + +This format allows Tailwind to apply opacity modifiers like `bg-surface-primary/50`. + +## Troubleshooting + +### Common Issues + +#### 1. Colors Not Applying +- **Issue**: Custom theme colors aren't showing +- **Solution**: Ensure you're passing the `themeRGB` prop to ThemeProvider +- **Check**: CSS variables in DevTools should show `rgb(R G B)` format + +#### 2. Circular Reference Errors +- **Issue**: `--brand-purple: var(--brand-purple)` creates infinite loop +- **Solution**: Use direct color values: `--brand-purple: #ab68ff` + +#### 3. Dark Mode Not Working +- **Issue**: Dark mode doesn't switch +- **Solution**: Ensure `darkMode: ['class']` is in your Tailwind config +- **Check**: The `` element should have `class="dark"` in dark mode + +#### 4. TypeScript Errors +- **Issue**: Type errors when defining themes +- **Solution**: Import and use the `IThemeRGB` interface: +```tsx +import { IThemeRGB } from '@librechat/client'; +``` + +### Debugging Tips + +1. **Check CSS Variables**: Use browser DevTools to inspect computed CSS variables +2. **Verify Theme Application**: Look for inline styles on the root element +3. **Console Errors**: Check for validation errors in the console +4. **Test Isolation**: Try a minimal theme to isolate issues + +## Examples + +### Dynamic Theme Switching + +```tsx +import { ThemeProvider, defaultTheme, darkTheme } from '@librechat/client'; +import { useState } from 'react'; + +function App() { + const [isDark, setIsDark] = useState(false); + + return ( + + + + + ); +} +``` + +### Multi-Theme Selector + +```tsx +const themes = { + default: undefined, // Use CSS defaults + ocean: { + 'rgb-brand-purple': '0 119 190', + 'rgb-surface-primary': '240 248 255', + // ... ocean theme colors + }, + forest: { + 'rgb-brand-purple': '34 139 34', + 'rgb-surface-primary': '245 255 250', + // ... forest theme colors + }, +}; + +function App() { + const [selectedTheme, setSelectedTheme] = useState('default'); + + return ( + + + + + ); +} +``` + +### Using with the Main Application + +When using the ThemeProvider in your main application with localStorage persistence: + +```tsx +import { ThemeProvider } from '@librechat/client'; +import { getThemeFromEnv } from './utils'; + +function App() { + const envTheme = getThemeFromEnv(); + + return ( + + {/* Your app content */} + + ); +} +``` + +**Important**: Props passed to ThemeProvider will override stored values on initial mount. Only pass props when you explicitly want to override the user's saved preferences. + +## Contributing + +When adding new theme colors: + +1. Add the type definition in `types/index.ts` +2. Add the color to default and dark themes +3. Update the applyTheme mapping +4. Add to Tailwind configuration +5. Document in this README + +## License + +This theme system is part of the @librechat/client package. \ No newline at end of file diff --git a/packages/client/src/theme/atoms/themeAtoms.ts b/packages/client/src/theme/atoms/themeAtoms.ts new file mode 100644 index 000000000..316d092fb --- /dev/null +++ b/packages/client/src/theme/atoms/themeAtoms.ts @@ -0,0 +1,36 @@ +import { atomWithStorage } from 'jotai/utils'; +import { IThemeRGB } from '../types'; + +/** + * Atom for storing the theme mode (light/dark/system) in localStorage + * Key: 'color-theme' + */ +export const themeModeAtom = atomWithStorage('color-theme', 'system', undefined, { + getOnInit: true, +}); + +/** + * Atom for storing custom theme colors in localStorage + * Key: 'theme-colors' + */ +export const themeColorsAtom = atomWithStorage( + 'theme-colors', + undefined, + undefined, + { + getOnInit: true, + }, +); + +/** + * Atom for storing the theme name in localStorage + * Key: 'theme-name' + */ +export const themeNameAtom = atomWithStorage( + 'theme-name', + undefined, + undefined, + { + getOnInit: true, + }, +); diff --git a/packages/client/src/theme/context/ThemeProvider.tsx b/packages/client/src/theme/context/ThemeProvider.tsx new file mode 100644 index 000000000..c80379616 --- /dev/null +++ b/packages/client/src/theme/context/ThemeProvider.tsx @@ -0,0 +1,165 @@ +import React, { createContext, useContext, useEffect, useMemo, useCallback, useRef } from 'react'; +import { useAtom } from 'jotai'; +import { IThemeRGB } from '../types'; +import applyTheme from '../utils/applyTheme'; +import { themeModeAtom, themeColorsAtom, themeNameAtom } from '../atoms/themeAtoms'; + +type ThemeContextType = { + theme: string; // 'light' | 'dark' | 'system' + setTheme: (theme: string) => void; + themeRGB?: IThemeRGB; + setThemeRGB: (colors?: IThemeRGB) => void; + themeName?: string; + setThemeName: (name?: string) => void; + resetTheme: () => void; +}; + +// Export ThemeContext so it can be imported from hooks +export const ThemeContext = createContext({ + theme: 'system', + setTheme: () => undefined, + setThemeRGB: () => undefined, + setThemeName: () => undefined, + resetTheme: () => undefined, +}); + +export interface ThemeProviderProps { + children: React.ReactNode; + themeRGB?: IThemeRGB; + themeName?: string; + initialTheme?: string; +} + +/** + * Check if theme is dark + */ +export const isDark = (theme: string): boolean => { + if (theme === 'system') { + return window.matchMedia('(prefers-color-scheme: dark)').matches; + } + return theme === 'dark'; +}; + +/** + * ThemeProvider component that handles both dark/light mode switching + * and dynamic color themes via CSS variables with localStorage persistence + */ +export function ThemeProvider({ + children, + themeRGB: propThemeRGB, + themeName: propThemeName, + initialTheme, +}: ThemeProviderProps) { + // Use jotai atoms for persistent state + const [theme, setTheme] = useAtom(themeModeAtom); + const [storedThemeRGB, setStoredThemeRGB] = useAtom(themeColorsAtom); + const [storedThemeName, setStoredThemeName] = useAtom(themeNameAtom); + + // Track if props have been initialized + const propsInitialized = useRef(false); + + // Initialize from props only once on mount + useEffect(() => { + if (!propsInitialized.current) { + propsInitialized.current = true; + + // Set initial theme if provided + if (initialTheme) { + setTheme(initialTheme); + } + + // Set initial theme colors if provided + if (propThemeRGB) { + setStoredThemeRGB(propThemeRGB); + } + + // Set initial theme name if provided + if (propThemeName) { + setStoredThemeName(propThemeName); + } + } + }, [initialTheme, propThemeRGB, propThemeName, setTheme, setStoredThemeRGB, setStoredThemeName]); + + // Apply class-based dark mode + const applyThemeMode = useCallback((rawTheme: string) => { + const root = window.document.documentElement; + const darkMode = isDark(rawTheme); + + root.classList.remove(darkMode ? 'light' : 'dark'); + root.classList.add(darkMode ? 'dark' : 'light'); + }, []); + + // Handle system theme changes + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const changeThemeOnSystemChange = () => { + if (theme === 'system') { + applyThemeMode('system'); + } + }; + + mediaQuery.addEventListener('change', changeThemeOnSystemChange); + return () => { + mediaQuery.removeEventListener('change', changeThemeOnSystemChange); + }; + }, [theme, applyThemeMode]); + + // Apply dark/light mode class + useEffect(() => { + applyThemeMode(theme); + }, [theme, applyThemeMode]); + + // Apply dynamic color theme + useEffect(() => { + if (storedThemeRGB) { + applyTheme(storedThemeRGB); + } + }, [storedThemeRGB]); + + // Reset theme function + const resetTheme = useCallback(() => { + setTheme('system'); + setStoredThemeRGB(undefined); + setStoredThemeName(undefined); + // Remove any custom CSS variables + const root = document.documentElement; + const customProps = Array.from(root.style).filter((prop) => prop.startsWith('--')); + customProps.forEach((prop) => root.style.removeProperty(prop)); + }, [setTheme, setStoredThemeRGB, setStoredThemeName]); + + const value = useMemo( + () => ({ + theme, + setTheme, + themeRGB: storedThemeRGB, + setThemeRGB: setStoredThemeRGB, + themeName: storedThemeName, + setThemeName: setStoredThemeName, + resetTheme, + }), + [ + theme, + setTheme, + storedThemeRGB, + setStoredThemeRGB, + storedThemeName, + setStoredThemeName, + resetTheme, + ], + ); + + return {children}; +} + +/** + * Hook to access the current theme context + */ +export function useTheme() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} + +export default ThemeProvider; diff --git a/packages/client/src/theme/index.ts b/packages/client/src/theme/index.ts new file mode 100644 index 000000000..18d02ed1e --- /dev/null +++ b/packages/client/src/theme/index.ts @@ -0,0 +1,14 @@ +// Export types +export * from './types'; + +// Export ThemeProvider, ThemeContext, useTheme hook, and isDark +export { ThemeProvider, ThemeContext, useTheme, isDark } from './context/ThemeProvider'; + +// Export utility functions +export { default as applyTheme } from './utils/applyTheme'; + +// Export theme atoms for persistence +export { themeModeAtom, themeColorsAtom, themeNameAtom } from './atoms/themeAtoms'; + +// Export predefined themes +export * from './themes'; diff --git a/packages/client/src/theme/themes/dark.ts b/packages/client/src/theme/themes/dark.ts new file mode 100644 index 000000000..150fee69c --- /dev/null +++ b/packages/client/src/theme/themes/dark.ts @@ -0,0 +1,72 @@ +import { IThemeRGB } from '../types'; + +/** + * Dark theme + * RGB values extracted from the existing dark mode CSS variables + */ +export const darkTheme: IThemeRGB = { + // Text colors + 'rgb-text-primary': '236 236 236', // #ececec (gray-100) + 'rgb-text-secondary': '205 205 205', // #cdcdcd (gray-300) + 'rgb-text-secondary-alt': '153 150 150', // #999696 (gray-400) + 'rgb-text-tertiary': '89 89 89', // #595959 (gray-500) + 'rgb-text-warning': '245 158 11', // #f59e0b (amber-500) + + // Ring colors (not defined in dark mode, using default) + 'rgb-ring-primary': '89 89 89', // #595959 (gray-500) + + // Header colors + 'rgb-header-primary': '47 47 47', // #2f2f2f (gray-700) + 'rgb-header-hover': '66 66 66', // #424242 (gray-600) + 'rgb-header-button-hover': '47 47 47', // #2f2f2f (gray-700) + + // Surface colors + 'rgb-surface-active': '89 89 89', // #595959 (gray-500) + 'rgb-surface-active-alt': '47 47 47', // #2f2f2f (gray-700) + 'rgb-surface-hover': '66 66 66', // #424242 (gray-600) + 'rgb-surface-hover-alt': '66 66 66', // #424242 (gray-600) + 'rgb-surface-primary': '13 13 13', // #0d0d0d (gray-900) + 'rgb-surface-primary-alt': '23 23 23', // #171717 (gray-850) + 'rgb-surface-primary-contrast': '23 23 23', // #171717 (gray-850) + 'rgb-surface-secondary': '33 33 33', // #212121 (gray-800) + 'rgb-surface-secondary-alt': '33 33 33', // #212121 (gray-800) + 'rgb-surface-tertiary': '47 47 47', // #2f2f2f (gray-700) + 'rgb-surface-tertiary-alt': '47 47 47', // #2f2f2f (gray-700) + 'rgb-surface-dialog': '23 23 23', // #171717 (gray-850) + 'rgb-surface-submit': '4 120 87', // #047857 (green-700) + 'rgb-surface-submit-hover': '6 95 70', // #065f46 (green-800) + 'rgb-surface-destructive': '153 27 27', // #991b1b (red-800) + 'rgb-surface-destructive-hover': '127 29 29', // #7f1d1d (red-900) + 'rgb-surface-chat': '47 47 47', // #2f2f2f (gray-700) + + // Border colors + 'rgb-border-light': '47 47 47', // #2f2f2f (gray-700) + 'rgb-border-medium': '66 66 66', // #424242 (gray-600) + 'rgb-border-medium-alt': '66 66 66', // #424242 (gray-600) + 'rgb-border-heavy': '89 89 89', // #595959 (gray-500) + 'rgb-border-xheavy': '153 150 150', // #999696 (gray-400) + + // Brand colors + 'rgb-brand-purple': '171 104 255', // #ab68ff + + // Presentation + 'rgb-presentation': '33 33 33', // #212121 (gray-800) + + // Utility colors (mapped to existing colors for backwards compatibility) + 'rgb-background': '33 33 33', // Same as surface-primary + 'rgb-foreground': '255 255 255', // Same as text-primary + 'rgb-primary': '66 66 66', // Same as surface-active + 'rgb-primary-foreground': '255 255 255', // Same as surface-primary-contrast + 'rgb-secondary': '42 42 42', // Same as surface-secondary + 'rgb-secondary-foreground': '193 193 193', // Same as text-secondary + 'rgb-muted': '56 56 56', // Same as surface-tertiary + 'rgb-muted-foreground': '140 140 140', // Same as text-tertiary + 'rgb-accent': '82 82 82', // Same as surface-active-alt + 'rgb-accent-foreground': '255 255 255', // Same as text-primary + 'rgb-destructive-foreground': '255 255 255', // Same as text-primary + 'rgb-border': '82 82 82', // Same as border-medium + 'rgb-input': '66 66 66', // Same as border-light + 'rgb-ring': '255 255 255', // Same as ring-primary + 'rgb-card': '42 42 42', // Same as surface-secondary + 'rgb-card-foreground': '255 255 255', // Same as text-primary +}; diff --git a/packages/client/src/theme/themes/default.ts b/packages/client/src/theme/themes/default.ts new file mode 100644 index 000000000..d636cc0c2 --- /dev/null +++ b/packages/client/src/theme/themes/default.ts @@ -0,0 +1,72 @@ +import { IThemeRGB } from '../types'; + +/** + * Default light theme + * RGB values extracted from the existing CSS variables + */ +export const defaultTheme: IThemeRGB = { + // Text colors + 'rgb-text-primary': '33 33 33', // #212121 (gray-800) + 'rgb-text-secondary': '66 66 66', // #424242 (gray-600) + 'rgb-text-secondary-alt': '89 89 89', // #595959 (gray-500) + 'rgb-text-tertiary': '89 89 89', // #595959 (gray-500) + 'rgb-text-warning': '245 158 11', // #f59e0b (amber-500) + + // Ring colors + 'rgb-ring-primary': '89 89 89', // #595959 (gray-500) + + // Header colors + 'rgb-header-primary': '255 255 255', // #fff (white) + 'rgb-header-hover': '247 247 248', // #f7f7f8 (gray-50) + 'rgb-header-button-hover': '247 247 248', // #f7f7f8 (gray-50) + + // Surface colors + 'rgb-surface-active': '236 236 236', // #ececec (gray-100) + 'rgb-surface-active-alt': '227 227 227', // #e3e3e3 (gray-200) + 'rgb-surface-hover': '227 227 227', // #e3e3e3 (gray-200) + 'rgb-surface-hover-alt': '205 205 205', // #cdcdcd (gray-300) + 'rgb-surface-primary': '255 255 255', // #fff (white) + 'rgb-surface-primary-alt': '247 247 248', // #f7f7f8 (gray-50) + 'rgb-surface-primary-contrast': '236 236 236', // #ececec (gray-100) + 'rgb-surface-secondary': '247 247 248', // #f7f7f8 (gray-50) + 'rgb-surface-secondary-alt': '227 227 227', // #e3e3e3 (gray-200) + 'rgb-surface-tertiary': '236 236 236', // #ececec (gray-100) + 'rgb-surface-tertiary-alt': '255 255 255', // #fff (white) + 'rgb-surface-dialog': '255 255 255', // #fff (white) + 'rgb-surface-submit': '4 120 87', // #047857 (green-700) + 'rgb-surface-submit-hover': '6 95 70', // #065f46 (green-800) + 'rgb-surface-destructive': '185 28 28', // #b91c1c (red-700) + 'rgb-surface-destructive-hover': '153 27 27', // #991b1b (red-800) + 'rgb-surface-chat': '255 255 255', // #fff (white) + + // Border colors + 'rgb-border-light': '227 227 227', // #e3e3e3 (gray-200) + 'rgb-border-medium': '205 205 205', // #cdcdcd (gray-300) + 'rgb-border-medium-alt': '205 205 205', // #cdcdcd (gray-300) + 'rgb-border-heavy': '153 150 150', // #999696 (gray-400) + 'rgb-border-xheavy': '89 89 89', // #595959 (gray-500) + + // Brand colors + 'rgb-brand-purple': '171 104 255', // #ab68ff + + // Presentation + 'rgb-presentation': '255 255 255', // #fff (white) + + // Utility colors (mapped to existing colors for backwards compatibility) + 'rgb-background': '255 255 255', // Same as surface-primary + 'rgb-foreground': '17 17 17', // Same as text-primary + 'rgb-primary': '235 235 235', // Same as surface-active + 'rgb-primary-foreground': '0 0 0', // Same as surface-primary-contrast + 'rgb-secondary': '247 247 248', // Same as surface-secondary + 'rgb-secondary-foreground': '66 66 66', // Same as text-secondary + 'rgb-muted': '250 250 250', // Same as surface-tertiary + 'rgb-muted-foreground': '120 120 120', // Same as text-tertiary + 'rgb-accent': '245 245 245', // Same as surface-active-alt + 'rgb-accent-foreground': '17 17 17', // Same as text-primary + 'rgb-destructive-foreground': '17 17 17', // Same as text-primary + 'rgb-border': '215 215 215', // Same as border-medium + 'rgb-input': '230 230 230', // Same as border-light + 'rgb-ring': '180 180 180', // Same as ring-primary + 'rgb-card': '247 247 248', // Same as surface-secondary + 'rgb-card-foreground': '17 17 17', // Same as text-primary +}; diff --git a/packages/client/src/theme/themes/index.ts b/packages/client/src/theme/themes/index.ts new file mode 100644 index 000000000..89b1b0e25 --- /dev/null +++ b/packages/client/src/theme/themes/index.ts @@ -0,0 +1,2 @@ +export { defaultTheme } from './default'; +export { darkTheme } from './dark'; diff --git a/packages/client/src/theme/types/index.ts b/packages/client/src/theme/types/index.ts new file mode 100644 index 000000000..47ab88d6b --- /dev/null +++ b/packages/client/src/theme/types/index.ts @@ -0,0 +1,189 @@ +/** + * Defines the color channels. Passed to the context from each app. + * RGB values should be in format "255 255 255" (space-separated) + */ +export interface IThemeRGB { + // Text colors + 'rgb-text-primary'?: string; + 'rgb-text-secondary'?: string; + 'rgb-text-secondary-alt'?: string; + 'rgb-text-tertiary'?: string; + 'rgb-text-warning'?: string; + + // Ring colors + 'rgb-ring-primary'?: string; + + // Header colors + 'rgb-header-primary'?: string; + 'rgb-header-hover'?: string; + 'rgb-header-button-hover'?: string; + + // Surface colors + 'rgb-surface-active'?: string; + 'rgb-surface-active-alt'?: string; + 'rgb-surface-hover'?: string; + 'rgb-surface-hover-alt'?: string; + 'rgb-surface-primary'?: string; + 'rgb-surface-primary-alt'?: string; + 'rgb-surface-primary-contrast'?: string; + 'rgb-surface-secondary'?: string; + 'rgb-surface-secondary-alt'?: string; + 'rgb-surface-tertiary'?: string; + 'rgb-surface-tertiary-alt'?: string; + 'rgb-surface-dialog'?: string; + 'rgb-surface-submit'?: string; + 'rgb-surface-submit-hover'?: string; + 'rgb-surface-destructive'?: string; + 'rgb-surface-destructive-hover'?: string; + 'rgb-surface-chat'?: string; + + // Border colors + 'rgb-border-light'?: string; + 'rgb-border-medium'?: string; + 'rgb-border-medium-alt'?: string; + 'rgb-border-heavy'?: string; + 'rgb-border-xheavy'?: string; + + // Brand colors + 'rgb-brand-purple'?: string; + + // Presentation + 'rgb-presentation'?: string; + + // Utility colors + 'rgb-background'?: string; + 'rgb-foreground'?: string; + 'rgb-primary'?: string; + 'rgb-primary-foreground'?: string; + 'rgb-secondary'?: string; + 'rgb-secondary-foreground'?: string; + 'rgb-muted'?: string; + 'rgb-muted-foreground'?: string; + 'rgb-accent'?: string; + 'rgb-accent-foreground'?: string; + 'rgb-destructive-foreground'?: string; + 'rgb-border'?: string; + 'rgb-input'?: string; + 'rgb-ring'?: string; + 'rgb-card'?: string; + 'rgb-card-foreground'?: string; +} + +/** + * Name of the CSS variables used in tailwind.config + */ +export interface IThemeVariables { + '--text-primary': string; + '--text-secondary': string; + '--text-secondary-alt': string; + '--text-tertiary': string; + '--text-warning': string; + '--ring-primary': string; + '--header-primary': string; + '--header-hover': string; + '--header-button-hover': string; + '--surface-active': string; + '--surface-active-alt': string; + '--surface-hover': string; + '--surface-hover-alt': string; + '--surface-primary': string; + '--surface-primary-alt': string; + '--surface-primary-contrast': string; + '--surface-secondary': string; + '--surface-secondary-alt': string; + '--surface-tertiary': string; + '--surface-tertiary-alt': string; + '--surface-dialog': string; + '--surface-submit': string; + '--surface-submit-hover': string; + '--surface-destructive': string; + '--surface-destructive-hover': string; + '--surface-chat': string; + '--border-light': string; + '--border-medium': string; + '--border-medium-alt': string; + '--border-heavy': string; + '--border-xheavy': string; + '--brand-purple': string; + '--presentation': string; + + // Utility variables + '--background': string; + '--foreground': string; + '--primary': string; + '--primary-foreground': string; + '--secondary': string; + '--secondary-foreground': string; + '--muted': string; + '--muted-foreground': string; + '--accent': string; + '--accent-foreground': string; + '--destructive-foreground': string; + '--border': string; + '--input': string; + '--ring': string; + '--card': string; + '--card-foreground': string; +} + +/** + * Name of the defined colors in the Tailwind theme + */ +export interface IThemeColors { + 'text-primary'?: string; + 'text-secondary'?: string; + 'text-secondary-alt'?: string; + 'text-tertiary'?: string; + 'text-warning'?: string; + 'ring-primary'?: string; + 'header-primary'?: string; + 'header-hover'?: string; + 'header-button-hover'?: string; + 'surface-active'?: string; + 'surface-active-alt'?: string; + 'surface-hover'?: string; + 'surface-hover-alt'?: string; + 'surface-primary'?: string; + 'surface-primary-alt'?: string; + 'surface-primary-contrast'?: string; + 'surface-secondary'?: string; + 'surface-secondary-alt'?: string; + 'surface-tertiary'?: string; + 'surface-tertiary-alt'?: string; + 'surface-dialog'?: string; + 'surface-submit'?: string; + 'surface-submit-hover'?: string; + 'surface-destructive'?: string; + 'surface-destructive-hover'?: string; + 'surface-chat'?: string; + 'border-light'?: string; + 'border-medium'?: string; + 'border-medium-alt'?: string; + 'border-heavy'?: string; + 'border-xheavy'?: string; + 'brand-purple'?: string; + presentation?: string; + + // Utility colors + background?: string; + foreground?: string; + primary?: string; + 'primary-foreground'?: string; + secondary?: string; + 'secondary-foreground'?: string; + muted?: string; + 'muted-foreground'?: string; + accent?: string; + 'accent-foreground'?: string; + 'destructive-foreground'?: string; + border?: string; + input?: string; + ring?: string; + card?: string; + 'card-foreground'?: string; +} + +export interface Theme { + name: string; + colors: IThemeRGB; +} diff --git a/packages/client/src/theme/utils/applyTheme.ts b/packages/client/src/theme/utils/applyTheme.ts new file mode 100644 index 000000000..f98db54dd --- /dev/null +++ b/packages/client/src/theme/utils/applyTheme.ts @@ -0,0 +1,115 @@ +import { IThemeRGB, IThemeVariables } from '../types'; + +/** + * Validates RGB string format (e.g., "255 255 255") + */ +function validateRGB(rgb: string): boolean { + if (!rgb) return true; + const rgbRegex = /^(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})$/; + const match = rgb.match(rgbRegex); + + if (!match) return false; + + // Check that each value is between 0-255 + const [, r, g, b] = match; + return [r, g, b].every((val) => { + const num = parseInt(val, 10); + return num >= 0 && num <= 255; + }); +} + +/** + * Maps theme RGB values to CSS variables + */ +function mapTheme(rgb: IThemeRGB): Partial { + const variables: Partial = {}; + + // Map each RGB value to its corresponding CSS variable + const mappings: Record = { + 'rgb-text-primary': '--text-primary', + 'rgb-text-secondary': '--text-secondary', + 'rgb-text-secondary-alt': '--text-secondary-alt', + 'rgb-text-tertiary': '--text-tertiary', + 'rgb-text-warning': '--text-warning', + 'rgb-ring-primary': '--ring-primary', + 'rgb-header-primary': '--header-primary', + 'rgb-header-hover': '--header-hover', + 'rgb-header-button-hover': '--header-button-hover', + 'rgb-surface-active': '--surface-active', + 'rgb-surface-active-alt': '--surface-active-alt', + 'rgb-surface-hover': '--surface-hover', + 'rgb-surface-hover-alt': '--surface-hover-alt', + 'rgb-surface-primary': '--surface-primary', + 'rgb-surface-primary-alt': '--surface-primary-alt', + 'rgb-surface-primary-contrast': '--surface-primary-contrast', + 'rgb-surface-secondary': '--surface-secondary', + 'rgb-surface-secondary-alt': '--surface-secondary-alt', + 'rgb-surface-tertiary': '--surface-tertiary', + 'rgb-surface-tertiary-alt': '--surface-tertiary-alt', + 'rgb-surface-dialog': '--surface-dialog', + 'rgb-surface-submit': '--surface-submit', + 'rgb-surface-submit-hover': '--surface-submit-hover', + 'rgb-surface-destructive': '--surface-destructive', + 'rgb-surface-destructive-hover': '--surface-destructive-hover', + 'rgb-surface-chat': '--surface-chat', + 'rgb-border-light': '--border-light', + 'rgb-border-medium': '--border-medium', + 'rgb-border-medium-alt': '--border-medium-alt', + 'rgb-border-heavy': '--border-heavy', + 'rgb-border-xheavy': '--border-xheavy', + 'rgb-brand-purple': '--brand-purple', + 'rgb-presentation': '--presentation', + + // Utility colors + 'rgb-background': '--background', + 'rgb-foreground': '--foreground', + 'rgb-primary': '--primary', + 'rgb-primary-foreground': '--primary-foreground', + 'rgb-secondary': '--secondary', + 'rgb-secondary-foreground': '--secondary-foreground', + 'rgb-muted': '--muted', + 'rgb-muted-foreground': '--muted-foreground', + 'rgb-accent': '--accent', + 'rgb-accent-foreground': '--accent-foreground', + 'rgb-destructive-foreground': '--destructive-foreground', + 'rgb-border': '--border', + 'rgb-input': '--input', + 'rgb-ring': '--ring', + 'rgb-card': '--card', + 'rgb-card-foreground': '--card-foreground', + }; + + Object.entries(mappings).forEach(([rgbKey, cssVar]) => { + const value = rgb[rgbKey as keyof IThemeRGB]; + if (value) { + variables[cssVar] = value; + } + }); + + return variables; +} + +/** + * Applies theme to the document root + * Sets CSS variables as rgb() values for compatibility with existing CSS + */ +export default function applyTheme(themeRGB?: IThemeRGB) { + if (!themeRGB) return; + + const themeObject = mapTheme(themeRGB); + const root = document.documentElement; + + Object.entries(themeObject).forEach(([cssVar, value]) => { + if (!value) return; + + const validation = validateRGB(value); + if (!validation) { + console.error(`Invalid RGB value for ${cssVar}: ${value}`); + return; + } + + // Set the CSS variable as rgb() value for compatibility + // This ensures existing CSS that expects color values (not space-separated RGB) continues to work + root.style.setProperty(cssVar, `rgb(${value})`); + }); +} diff --git a/packages/client/src/theme/utils/createTailwindColors.js b/packages/client/src/theme/utils/createTailwindColors.js new file mode 100644 index 000000000..de110ac18 --- /dev/null +++ b/packages/client/src/theme/utils/createTailwindColors.js @@ -0,0 +1,86 @@ +/** + * Helper function to create a color value that uses CSS variables with alpha support + * This is a CommonJS version for use in tailwind.config.js + */ +function withOpacity(variableName) { + return ({ opacityValue }) => { + if (opacityValue !== undefined) { + // The CSS variable already contains rgb() so we need to extract the values + return `rgba(var(${variableName}), ${opacityValue})`.replace('rgb(', '').replace(')', ''); + } + return `var(${variableName})`; + }; +} + +/** + * Creates Tailwind color configuration that uses CSS variables + * This allows dynamic theme switching by changing CSS variable values + */ +function createTailwindColors() { + return { + 'text-primary': withOpacity('--text-primary'), + 'text-secondary': withOpacity('--text-secondary'), + 'text-secondary-alt': withOpacity('--text-secondary-alt'), + 'text-tertiary': withOpacity('--text-tertiary'), + 'text-warning': withOpacity('--text-warning'), + 'ring-primary': withOpacity('--ring-primary'), + 'header-primary': withOpacity('--header-primary'), + 'header-hover': withOpacity('--header-hover'), + 'header-button-hover': withOpacity('--header-button-hover'), + 'surface-active': withOpacity('--surface-active'), + 'surface-active-alt': withOpacity('--surface-active-alt'), + 'surface-hover': withOpacity('--surface-hover'), + 'surface-hover-alt': withOpacity('--surface-hover-alt'), + 'surface-primary': withOpacity('--surface-primary'), + 'surface-primary-alt': withOpacity('--surface-primary-alt'), + 'surface-primary-contrast': withOpacity('--surface-primary-contrast'), + 'surface-secondary': withOpacity('--surface-secondary'), + 'surface-secondary-alt': withOpacity('--surface-secondary-alt'), + 'surface-tertiary': withOpacity('--surface-tertiary'), + 'surface-tertiary-alt': withOpacity('--surface-tertiary-alt'), + 'surface-dialog': withOpacity('--surface-dialog'), + 'surface-submit': withOpacity('--surface-submit'), + 'surface-submit-hover': withOpacity('--surface-submit-hover'), + 'surface-destructive': withOpacity('--surface-destructive'), + 'surface-destructive-hover': withOpacity('--surface-destructive-hover'), + 'surface-chat': withOpacity('--surface-chat'), + 'border-light': withOpacity('--border-light'), + 'border-medium': withOpacity('--border-medium'), + 'border-medium-alt': withOpacity('--border-medium-alt'), + 'border-heavy': withOpacity('--border-heavy'), + 'border-xheavy': withOpacity('--border-xheavy'), + 'brand-purple': withOpacity('--brand-purple'), + presentation: withOpacity('--presentation'), + background: withOpacity('--background'), + foreground: withOpacity('--foreground'), + primary: { + DEFAULT: withOpacity('--primary'), + foreground: withOpacity('--primary-foreground'), + }, + secondary: { + DEFAULT: withOpacity('--secondary'), + foreground: withOpacity('--secondary-foreground'), + }, + muted: { + DEFAULT: withOpacity('--muted'), + foreground: withOpacity('--muted-foreground'), + }, + accent: { + DEFAULT: withOpacity('--accent'), + foreground: withOpacity('--accent-foreground'), + }, + destructive: { + DEFAULT: withOpacity('--surface-destructive'), + foreground: withOpacity('--destructive-foreground'), + }, + border: withOpacity('--border'), + input: withOpacity('--input'), + ring: withOpacity('--ring'), + card: { + DEFAULT: withOpacity('--card'), + foreground: withOpacity('--card-foreground'), + }, + }; +} + +module.exports = { createTailwindColors }; diff --git a/packages/client/src/utils/index.ts b/packages/client/src/utils/index.ts new file mode 100644 index 000000000..94696af63 --- /dev/null +++ b/packages/client/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './utils'; +export * from './theme'; diff --git a/client/src/utils/theme.ts b/packages/client/src/utils/theme.ts similarity index 95% rename from client/src/utils/theme.ts rename to packages/client/src/utils/theme.ts index d389dea57..a5bc645a0 100644 --- a/client/src/utils/theme.ts +++ b/packages/client/src/utils/theme.ts @@ -34,5 +34,5 @@ export const getInitialTheme = () => { } } - return 'light'; // light theme as the default; + return 'light'; }; diff --git a/packages/client/src/utils/utils.ts b/packages/client/src/utils/utils.ts new file mode 100644 index 000000000..fad4c32a0 --- /dev/null +++ b/packages/client/src/utils/utils.ts @@ -0,0 +1,7 @@ +// ESM utility functions +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export const cn = (...inputs: ClassValue[]): string => { + return twMerge(clsx(inputs)); +}; diff --git a/packages/client/tailwind.config.js b/packages/client/tailwind.config.js new file mode 100644 index 000000000..df7ee31c6 --- /dev/null +++ b/packages/client/tailwind.config.js @@ -0,0 +1,13 @@ +const { createTailwindColors } = require('./src/theme/utils/createTailwindColors.js'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{js,jsx,ts,tsx}'], + darkMode: ['class'], + theme: { + extend: { + colors: createTailwindColors(), + }, + }, + plugins: [], +}; diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json new file mode 100644 index 000000000..447cb1e66 --- /dev/null +++ b/packages/client/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "jsx": "react-jsx", + "jsxImportSource": "react", + "declaration": true, + "declarationMap": true, + "declarationDir": "./dist/types", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": "./src", + "paths": { + "~/*": ["./*"] + }, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": [ + "dist", + "node_modules", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.spec.ts", + "**/*.spec.tsx" + ] +} From 32f7dbd11f5a3fa22427b792b9cba99ad156b329 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 27 Jul 2025 13:29:43 -0400 Subject: [PATCH 018/224] =?UTF-8?q?=F0=9F=90=B3=20ci:=20Build=20`client`?= =?UTF-8?q?=20package=20for=20`Dockerfile.multi`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.multi | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dockerfile.multi b/Dockerfile.multi index 57f9813f4..d04587d1c 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -16,6 +16,7 @@ COPY package*.json ./ COPY packages/data-provider/package*.json ./packages/data-provider/ COPY packages/api/package*.json ./packages/api/ COPY packages/data-schemas/package*.json ./packages/data-schemas/ +COPY packages/client/package*.json ./packages/client/ COPY client/package*.json ./client/ COPY api/package*.json ./api/ @@ -45,11 +46,18 @@ COPY --from=data-provider-build /app/packages/data-provider/dist /app/packages/d COPY --from=data-schemas-build /app/packages/data-schemas/dist /app/packages/data-schemas/dist RUN npm run build +# Build `client` package +FROM base AS client-package-build +WORKDIR /app/packages/client +COPY packages/client ./ +RUN npm run build + # Client build FROM base AS client-build WORKDIR /app/client COPY client ./ COPY --from=data-provider-build /app/packages/data-provider/dist /app/packages/data-provider/dist +COPY --from=client-package-build /app/packages/client/dist /app/packages/client/dist ENV NODE_OPTIONS="--max-old-space-size=2048" RUN npm run build From 9fddb0ff6ab5bf54fb6e5cc18e08cb89a5896adc Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 27 Jul 2025 15:11:42 -0400 Subject: [PATCH 019/224] =?UTF-8?q?=F0=9F=90=8B=20ci:=20Include=20`package?= =?UTF-8?q?s/client`=20source=20files=20in=20`Dockerfile.multi`=20Client?= =?UTF-8?q?=20Build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.multi | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.multi b/Dockerfile.multi index d04587d1c..296f8ec35 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -58,6 +58,7 @@ WORKDIR /app/client COPY client ./ COPY --from=data-provider-build /app/packages/data-provider/dist /app/packages/data-provider/dist COPY --from=client-package-build /app/packages/client/dist /app/packages/client/dist +COPY --from=client-package-build /app/packages/client/src /app/packages/client/src ENV NODE_OPTIONS="--max-old-space-size=2048" RUN npm run build From 2ce6ac74f41589923695dde53bba8c533a18a4a4 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:18:59 +0200 Subject: [PATCH 020/224] =?UTF-8?q?=F0=9F=93=BB=20feat:=20radio=20componen?= =?UTF-8?q?t=20(#8692)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📦 feat: Add Radio component * 📦 feat: Integrate localization for 'No options available' message in Radio component * 📦 feat: Bump version to 0.2.0 in package.json * 📦 feat: Update client package version to 0.2.0 in package-lock.json --- package-lock.json | 2 +- packages/client/package.json | 2 +- packages/client/src/components/Radio.tsx | 99 +++++++++++++++++++ packages/client/src/components/index.ts | 1 + .../client/src/locales/en/translation.json | 3 +- 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 packages/client/src/components/Radio.tsx diff --git a/package-lock.json b/package-lock.json index 9cef1c664..e89438099 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51506,7 +51506,7 @@ }, "packages/client": { "name": "@librechat/client", - "version": "0.1.9", + "version": "0.2.0", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^25.0.2", diff --git a/packages/client/package.json b/packages/client/package.json index 5edd6505c..f1775ed13 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/client", - "version": "0.1.9", + "version": "0.2.0", "description": "React components for LibreChat", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/client/src/components/Radio.tsx b/packages/client/src/components/Radio.tsx new file mode 100644 index 000000000..b419f78e6 --- /dev/null +++ b/packages/client/src/components/Radio.tsx @@ -0,0 +1,99 @@ +import React, { useState, useRef, useLayoutEffect, useCallback, memo } from 'react'; +import { useLocalize } from '~/hooks'; + +interface Option { + value: string; + label: string; +} + +interface RadioProps { + options: Option[]; + value?: string; + onChange?: (value: string) => void; + disabled?: boolean; +} + +const Radio = memo(function Radio({ options, value, onChange, disabled = false }: RadioProps) { + const localize = useLocalize(); + const [currentValue, setCurrentValue] = useState(value ?? ''); + const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); + const [backgroundStyle, setBackgroundStyle] = useState({}); + + const handleChange = (newValue: string) => { + setCurrentValue(newValue); + onChange?.(newValue); + }; + + const updateBackgroundStyle = useCallback(() => { + const selectedIndex = options.findIndex((opt) => opt.value === currentValue); + if (selectedIndex >= 0 && buttonRefs.current[selectedIndex]) { + const selectedButton = buttonRefs.current[selectedIndex]; + const container = selectedButton?.parentElement; + if (selectedButton && container) { + const containerRect = container.getBoundingClientRect(); + const buttonRect = selectedButton.getBoundingClientRect(); + const offsetLeft = buttonRect.left - containerRect.left - 4; + setBackgroundStyle({ + width: `${buttonRect.width}px`, + transform: `translateX(${offsetLeft}px)`, + }); + } + } + }, [currentValue, options]); + + useLayoutEffect(() => { + updateBackgroundStyle(); + }, [updateBackgroundStyle]); + + useLayoutEffect(() => { + if (value !== undefined) { + setCurrentValue(value); + } + }, [value]); + + if (options.length === 0) { + return ( +
+ + {localize('com_ui_no_options')} + +
+ ); + } + + const selectedIndex = options.findIndex((opt) => opt.value === currentValue); + + return ( +
+ {selectedIndex >= 0 && ( +
+ )} + {options.map((option, index) => ( + + ))} +
+ ); +}); + +export default Radio; diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts index edaf240dc..0269a0455 100644 --- a/packages/client/src/components/index.ts +++ b/packages/client/src/components/index.ts @@ -30,6 +30,7 @@ export * from './Progress'; export * from './InputOTP'; export * from './MultiSearch'; export * from './Resizable'; +export { default as Radio } from './Radio'; export { default as Badge } from './Badge'; export { default as Combobox } from './Combobox'; export { default as Dropdown } from './Dropdown'; diff --git a/packages/client/src/locales/en/translation.json b/packages/client/src/locales/en/translation.json index de39a3005..9913398ff 100644 --- a/packages/client/src/locales/en/translation.json +++ b/packages/client/src/locales/en/translation.json @@ -1,3 +1,4 @@ { - "com_ui_cancel": "Cancel" + "com_ui_cancel": "Cancel", + "com_ui_no_options": "No options available" } From 37aba18a9621956fff12e17ba1b44dfff79e914b Mon Sep 17 00:00:00 2001 From: ryanh-ai <3118399+ryanh-ai@users.noreply.github.com> Date: Mon, 28 Jul 2025 09:24:08 -0700 Subject: [PATCH 021/224] =?UTF-8?q?=F0=9F=AA=9F=20feat:=20Context=20Window?= =?UTF-8?q?=20for=20`amazon.nova-premier`=20(#8689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/utils/tokens.js | 1 + 1 file changed, 1 insertion(+) diff --git a/api/utils/tokens.js b/api/utils/tokens.js index a35f18a51..8f2173cbf 100644 --- a/api/utils/tokens.js +++ b/api/utils/tokens.js @@ -196,6 +196,7 @@ const amazonModels = { 'amazon.nova-micro-v1:0': 127000, // -1000 from max, 'amazon.nova-lite-v1:0': 295000, // -5000 from max, 'amazon.nova-pro-v1:0': 295000, // -5000 from max, + 'amazon.nova-premier-v1:0': 995000, // -5000 from max, }; const bedrockModels = { From 0ef3fefaec57b064f08502dd1a137112479b6bfd Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Mon, 28 Jul 2025 09:25:34 -0700 Subject: [PATCH 022/224] =?UTF-8?q?=F0=9F=8F=B9=20feat:=20Concurrent=20MCP?= =?UTF-8?q?=20Initialization=20Support=20(#8677)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: Enhance MCP Connection Status Management - Introduced new functions to retrieve and manage connection status for multiple MCP servers, including OAuth flow checks and server-specific status retrieval. - Refactored the MCP connection status endpoints to support both all servers and individual server queries. - Replaced the old server initialization hook with a new `useMCPServerManager` hook for improved state management and handling of multiple OAuth flows. - Updated the MCPPanel component to utilize the new context provider for better state handling and UI updates. - Fixed a number of UI bugs when initializing servers * 🗣️ i18n: Remove unused strings from translation.json * refactor: move helper functions out of the route module into mcp service file * ci: add tests for newly added functions in mcp service file * fix: memoize setMCPValues to avoid render loop --- api/server/routes/mcp.js | 144 +++-- api/server/services/MCP.js | 119 +++- api/server/services/MCP.spec.js | 510 ++++++++++++++++++ .../MCP/ServerInitializationSection.tsx | 48 +- .../src/components/SidePanel/MCP/MCPPanel.tsx | 11 +- client/src/hooks/MCP/index.ts | 2 +- .../hooks/MCP/useMCPServerInitialization.ts | 317 ----------- client/src/hooks/MCP/useMCPServerManager.ts | 436 +++++++++++---- client/src/hooks/Plugins/useMCPSelect.ts | 10 +- client/src/locales/en/translation.json | 2 - packages/data-provider/src/api-endpoints.ts | 2 + packages/data-provider/src/data-service.ts | 6 + .../src/react-query/react-query-service.ts | 19 + packages/data-provider/src/types/index.ts | 1 + packages/data-provider/src/types/queries.ts | 7 + 15 files changed, 1092 insertions(+), 542 deletions(-) create mode 100644 api/server/services/MCP.spec.js delete mode 100644 client/src/hooks/MCP/useMCPServerInitialization.ts create mode 100644 packages/data-provider/src/types/index.ts diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index c49ba4cc3..35bba77ae 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -4,6 +4,7 @@ const { MCPOAuthHandler } = require('@librechat/api'); const { CacheKeys, Constants } = require('librechat-data-provider'); const { findToken, updateToken, createToken, deleteTokens } = require('~/models'); const { setCachedTools, getCachedTools, loadCustomConfig } = require('~/server/services/Config'); +const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP'); const { getUserPluginAuthValue } = require('~/server/services/PluginService'); const { getMCPManager, getFlowStateManager } = require('~/config'); const { requireJwtAuth } = require('~/server/middleware'); @@ -468,7 +469,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { /** * Get connection status for all MCP servers - * This endpoint returns the actual connection status from MCPManager without disconnecting idle connections + * This endpoint returns all app level and user-scoped connection statuses from MCPManager without disconnecting idle connections */ router.get('/connection/status', requireJwtAuth, async (req, res) => { try { @@ -478,84 +479,19 @@ router.get('/connection/status', requireJwtAuth, async (req, res) => { return res.status(401).json({ error: 'User not authenticated' }); } - const mcpManager = getMCPManager(user.id); + const { mcpConfig, appConnections, userConnections, oauthServers } = await getMCPSetupData( + user.id, + ); const connectionStatus = {}; - const printConfig = false; - const config = await loadCustomConfig(printConfig); - const mcpConfig = config?.mcpServers; - - const appConnections = mcpManager.getAllConnections() || new Map(); - const userConnections = mcpManager.getUserConnections(user.id) || new Map(); - const oauthServers = mcpManager.getOAuthServers() || new Set(); - - if (!mcpConfig) { - return res.status(404).json({ error: 'MCP config not found' }); - } - - // Get flow manager to check for active/timed-out OAuth flows - const flowsCache = getLogStores(CacheKeys.FLOWS); - const flowManager = getFlowStateManager(flowsCache); - for (const [serverName] of Object.entries(mcpConfig)) { - const getConnectionState = (serverName) => - appConnections.get(serverName)?.connectionState ?? - userConnections.get(serverName)?.connectionState ?? - 'disconnected'; - - const baseConnectionState = getConnectionState(serverName); - - let hasActiveOAuthFlow = false; - let hasFailedOAuthFlow = false; - - if (baseConnectionState === 'disconnected' && oauthServers.has(serverName)) { - try { - // Check for user-specific OAuth flows - const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName); - const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth'); - if (flowState) { - // Check if flow failed or timed out - const flowAge = Date.now() - flowState.createdAt; - const flowTTL = flowState.ttl || 180000; // Default 3 minutes - - if (flowState.status === 'FAILED' || flowAge > flowTTL) { - hasFailedOAuthFlow = true; - logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, { - flowId, - status: flowState.status, - flowAge, - flowTTL, - timedOut: flowAge > flowTTL, - }); - } else if (flowState.status === 'PENDING') { - hasActiveOAuthFlow = true; - logger.debug(`[MCP Connection Status] Found active OAuth flow for ${serverName}`, { - flowId, - flowAge, - flowTTL, - }); - } - } - } catch (error) { - logger.error( - `[MCP Connection Status] Error checking OAuth flows for ${serverName}:`, - error, - ); - } - } - - // Determine the final connection state - let finalConnectionState = baseConnectionState; - if (hasFailedOAuthFlow) { - finalConnectionState = 'error'; // Report as error if OAuth failed - } else if (hasActiveOAuthFlow && baseConnectionState === 'disconnected') { - finalConnectionState = 'connecting'; // Still waiting for OAuth - } - - connectionStatus[serverName] = { - requiresOAuth: oauthServers.has(serverName), - connectionState: finalConnectionState, - }; + connectionStatus[serverName] = await getServerConnectionStatus( + user.id, + serverName, + appConnections, + userConnections, + oauthServers, + ); } res.json({ @@ -563,11 +499,67 @@ router.get('/connection/status', requireJwtAuth, async (req, res) => { connectionStatus, }); } catch (error) { + if (error.message === 'MCP config not found') { + return res.status(404).json({ error: error.message }); + } logger.error('[MCP Connection Status] Failed to get connection status', error); res.status(500).json({ error: 'Failed to get connection status' }); } }); +/** + * Get connection status for a single MCP server + * This endpoint returns the connection status for a specific server for a given user + */ +router.get('/connection/status/:serverName', requireJwtAuth, async (req, res) => { + try { + const user = req.user; + const { serverName } = req.params; + + if (!user?.id) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + if (!serverName) { + return res.status(400).json({ error: 'Server name is required' }); + } + + const { mcpConfig, appConnections, userConnections, oauthServers } = await getMCPSetupData( + user.id, + ); + + if (!mcpConfig[serverName]) { + return res + .status(404) + .json({ error: `MCP server '${serverName}' not found in configuration` }); + } + + const serverStatus = await getServerConnectionStatus( + user.id, + serverName, + appConnections, + userConnections, + oauthServers, + ); + + res.json({ + success: true, + serverName, + connectionStatus: serverStatus.connectionState, + requiresOAuth: serverStatus.requiresOAuth, + }); + } catch (error) { + if (error.message === 'MCP config not found') { + return res.status(404).json({ error: error.message }); + } + logger.error( + `[MCP Per-Server Status] Failed to get connection status for ${req.params.serverName}`, + error, + ); + res.status(500).json({ error: 'Failed to get connection status' }); + } +}); + /** * Check which authentication values exist for a specific MCP server * This endpoint returns only boolean flags indicating if values are set, not the actual values diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 997098182..f8ec2d04d 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -12,7 +12,7 @@ const { } = require('@librechat/api'); const { findToken, createToken, updateToken } = require('~/models'); const { getMCPManager, getFlowStateManager } = require('~/config'); -const { getCachedTools } = require('./Config'); +const { getCachedTools, loadCustomConfig } = require('./Config'); const { getLogStores } = require('~/cache'); /** @@ -239,6 +239,123 @@ async function createMCPTool({ req, res, toolKey, provider: _provider }) { return toolInstance; } +/** + * Get MCP setup data including config, connections, and OAuth servers + * @param {string} userId - The user ID + * @returns {Object} Object containing mcpConfig, appConnections, userConnections, and oauthServers + */ +async function getMCPSetupData(userId) { + const printConfig = false; + const config = await loadCustomConfig(printConfig); + const mcpConfig = config?.mcpServers; + + if (!mcpConfig) { + throw new Error('MCP config not found'); + } + + const mcpManager = getMCPManager(userId); + const appConnections = mcpManager.getAllConnections() || new Map(); + const userConnections = mcpManager.getUserConnections(userId) || new Map(); + const oauthServers = mcpManager.getOAuthServers() || new Set(); + + return { + mcpConfig, + appConnections, + userConnections, + oauthServers, + }; +} + +/** + * Check OAuth flow status for a user and server + * @param {string} userId - The user ID + * @param {string} serverName - The server name + * @returns {Object} Object containing hasActiveFlow and hasFailedFlow flags + */ +async function checkOAuthFlowStatus(userId, serverName) { + const flowsCache = getLogStores(CacheKeys.FLOWS); + const flowManager = getFlowStateManager(flowsCache); + const flowId = MCPOAuthHandler.generateFlowId(userId, serverName); + + try { + const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth'); + if (!flowState) { + return { hasActiveFlow: false, hasFailedFlow: false }; + } + + const flowAge = Date.now() - flowState.createdAt; + const flowTTL = flowState.ttl || 180000; // Default 3 minutes + + if (flowState.status === 'FAILED' || flowAge > flowTTL) { + logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, { + flowId, + status: flowState.status, + flowAge, + flowTTL, + timedOut: flowAge > flowTTL, + }); + return { hasActiveFlow: false, hasFailedFlow: true }; + } + + if (flowState.status === 'PENDING') { + logger.debug(`[MCP Connection Status] Found active OAuth flow for ${serverName}`, { + flowId, + flowAge, + flowTTL, + }); + return { hasActiveFlow: true, hasFailedFlow: false }; + } + + return { hasActiveFlow: false, hasFailedFlow: false }; + } catch (error) { + logger.error(`[MCP Connection Status] Error checking OAuth flows for ${serverName}:`, error); + return { hasActiveFlow: false, hasFailedFlow: false }; + } +} + +/** + * Get connection status for a specific MCP server + * @param {string} userId - The user ID + * @param {string} serverName - The server name + * @param {Map} appConnections - App-level connections + * @param {Map} userConnections - User-level connections + * @param {Set} oauthServers - Set of OAuth servers + * @returns {Object} Object containing requiresOAuth and connectionState + */ +async function getServerConnectionStatus( + userId, + serverName, + appConnections, + userConnections, + oauthServers, +) { + const getConnectionState = () => + appConnections.get(serverName)?.connectionState ?? + userConnections.get(serverName)?.connectionState ?? + 'disconnected'; + + const baseConnectionState = getConnectionState(); + let finalConnectionState = baseConnectionState; + + if (baseConnectionState === 'disconnected' && oauthServers.has(serverName)) { + const { hasActiveFlow, hasFailedFlow } = await checkOAuthFlowStatus(userId, serverName); + + if (hasFailedFlow) { + finalConnectionState = 'error'; + } else if (hasActiveFlow) { + finalConnectionState = 'connecting'; + } + } + + return { + requiresOAuth: oauthServers.has(serverName), + connectionState: finalConnectionState, + }; +} + module.exports = { createMCPTool, + getMCPSetupData, + checkOAuthFlowStatus, + getServerConnectionStatus, }; diff --git a/api/server/services/MCP.spec.js b/api/server/services/MCP.spec.js new file mode 100644 index 000000000..8c81abd68 --- /dev/null +++ b/api/server/services/MCP.spec.js @@ -0,0 +1,510 @@ +const { logger } = require('@librechat/data-schemas'); +const { MCPOAuthHandler } = require('@librechat/api'); +const { CacheKeys } = require('librechat-data-provider'); +const { getMCPSetupData, checkOAuthFlowStatus, getServerConnectionStatus } = require('./MCP'); + +// Mock all dependencies +jest.mock('@librechat/data-schemas', () => ({ + logger: { + debug: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('@librechat/api', () => ({ + MCPOAuthHandler: { + generateFlowId: jest.fn(), + }, +})); + +jest.mock('librechat-data-provider', () => ({ + CacheKeys: { + FLOWS: 'flows', + }, +})); + +jest.mock('./Config', () => ({ + loadCustomConfig: jest.fn(), +})); + +jest.mock('~/config', () => ({ + getMCPManager: jest.fn(), + getFlowStateManager: jest.fn(), +})); + +jest.mock('~/cache', () => ({ + getLogStores: jest.fn(), +})); + +jest.mock('~/models', () => ({ + findToken: jest.fn(), + createToken: jest.fn(), + updateToken: jest.fn(), +})); + +describe('tests for the new helper functions used by the MCP connection status endpoints', () => { + let mockLoadCustomConfig; + let mockGetMCPManager; + let mockGetFlowStateManager; + let mockGetLogStores; + + beforeEach(() => { + jest.clearAllMocks(); + + mockLoadCustomConfig = require('./Config').loadCustomConfig; + mockGetMCPManager = require('~/config').getMCPManager; + mockGetFlowStateManager = require('~/config').getFlowStateManager; + mockGetLogStores = require('~/cache').getLogStores; + }); + + describe('getMCPSetupData', () => { + const mockUserId = 'user-123'; + const mockConfig = { + mcpServers: { + server1: { type: 'stdio' }, + server2: { type: 'http' }, + }, + }; + + beforeEach(() => { + mockGetMCPManager.mockReturnValue({ + getAllConnections: jest.fn(() => new Map()), + getUserConnections: jest.fn(() => new Map()), + getOAuthServers: jest.fn(() => new Set()), + }); + }); + + it('should successfully return MCP setup data', async () => { + mockLoadCustomConfig.mockResolvedValue(mockConfig); + + const mockAppConnections = new Map([['server1', { status: 'connected' }]]); + const mockUserConnections = new Map([['server2', { status: 'disconnected' }]]); + const mockOAuthServers = new Set(['server2']); + + const mockMCPManager = { + getAllConnections: jest.fn(() => mockAppConnections), + getUserConnections: jest.fn(() => mockUserConnections), + getOAuthServers: jest.fn(() => mockOAuthServers), + }; + mockGetMCPManager.mockReturnValue(mockMCPManager); + + const result = await getMCPSetupData(mockUserId); + + expect(mockLoadCustomConfig).toHaveBeenCalledWith(false); + expect(mockGetMCPManager).toHaveBeenCalledWith(mockUserId); + expect(mockMCPManager.getAllConnections).toHaveBeenCalled(); + expect(mockMCPManager.getUserConnections).toHaveBeenCalledWith(mockUserId); + expect(mockMCPManager.getOAuthServers).toHaveBeenCalled(); + + expect(result).toEqual({ + mcpConfig: mockConfig.mcpServers, + appConnections: mockAppConnections, + userConnections: mockUserConnections, + oauthServers: mockOAuthServers, + }); + }); + + it('should throw error when MCP config not found', async () => { + mockLoadCustomConfig.mockResolvedValue({}); + await expect(getMCPSetupData(mockUserId)).rejects.toThrow('MCP config not found'); + }); + + it('should handle null values from MCP manager gracefully', async () => { + mockLoadCustomConfig.mockResolvedValue(mockConfig); + + const mockMCPManager = { + getAllConnections: jest.fn(() => null), + getUserConnections: jest.fn(() => null), + getOAuthServers: jest.fn(() => null), + }; + mockGetMCPManager.mockReturnValue(mockMCPManager); + + const result = await getMCPSetupData(mockUserId); + + expect(result).toEqual({ + mcpConfig: mockConfig.mcpServers, + appConnections: new Map(), + userConnections: new Map(), + oauthServers: new Set(), + }); + }); + }); + + describe('checkOAuthFlowStatus', () => { + const mockUserId = 'user-123'; + const mockServerName = 'test-server'; + const mockFlowId = 'flow-123'; + + beforeEach(() => { + const mockFlowsCache = {}; + const mockFlowManager = { + getFlowState: jest.fn(), + }; + + mockGetLogStores.mockReturnValue(mockFlowsCache); + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + MCPOAuthHandler.generateFlowId.mockReturnValue(mockFlowId); + }); + + it('should return false flags when no flow state exists', async () => { + const mockFlowManager = { getFlowState: jest.fn(() => null) }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + + const result = await checkOAuthFlowStatus(mockUserId, mockServerName); + + expect(mockGetLogStores).toHaveBeenCalledWith(CacheKeys.FLOWS); + expect(MCPOAuthHandler.generateFlowId).toHaveBeenCalledWith(mockUserId, mockServerName); + expect(mockFlowManager.getFlowState).toHaveBeenCalledWith(mockFlowId, 'mcp_oauth'); + expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: false }); + }); + + it('should detect failed flow when status is FAILED', async () => { + const mockFlowState = { + status: 'FAILED', + createdAt: Date.now() - 60000, // 1 minute ago + ttl: 180000, + }; + const mockFlowManager = { getFlowState: jest.fn(() => mockFlowState) }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + + const result = await checkOAuthFlowStatus(mockUserId, mockServerName); + + expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: true }); + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining('Found failed OAuth flow'), + expect.objectContaining({ + flowId: mockFlowId, + status: 'FAILED', + }), + ); + }); + + it('should detect failed flow when flow has timed out', async () => { + const mockFlowState = { + status: 'PENDING', + createdAt: Date.now() - 200000, // 200 seconds ago (> 180s TTL) + ttl: 180000, + }; + const mockFlowManager = { getFlowState: jest.fn(() => mockFlowState) }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + + const result = await checkOAuthFlowStatus(mockUserId, mockServerName); + + expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: true }); + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining('Found failed OAuth flow'), + expect.objectContaining({ + timedOut: true, + }), + ); + }); + + it('should detect failed flow when TTL not specified and flow exceeds default TTL', async () => { + const mockFlowState = { + status: 'PENDING', + createdAt: Date.now() - 200000, // 200 seconds ago (> 180s default TTL) + // ttl not specified, should use 180000 default + }; + const mockFlowManager = { getFlowState: jest.fn(() => mockFlowState) }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + + const result = await checkOAuthFlowStatus(mockUserId, mockServerName); + + expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: true }); + }); + + it('should detect active flow when status is PENDING and within TTL', async () => { + const mockFlowState = { + status: 'PENDING', + createdAt: Date.now() - 60000, // 1 minute ago (< 180s TTL) + ttl: 180000, + }; + const mockFlowManager = { getFlowState: jest.fn(() => mockFlowState) }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + + const result = await checkOAuthFlowStatus(mockUserId, mockServerName); + + expect(result).toEqual({ hasActiveFlow: true, hasFailedFlow: false }); + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining('Found active OAuth flow'), + expect.objectContaining({ + flowId: mockFlowId, + }), + ); + }); + + it('should return false flags for other statuses', async () => { + const mockFlowState = { + status: 'COMPLETED', + createdAt: Date.now() - 60000, + ttl: 180000, + }; + const mockFlowManager = { getFlowState: jest.fn(() => mockFlowState) }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + + const result = await checkOAuthFlowStatus(mockUserId, mockServerName); + + expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: false }); + }); + + it('should handle errors gracefully', async () => { + const mockError = new Error('Flow state error'); + const mockFlowManager = { + getFlowState: jest.fn(() => { + throw mockError; + }), + }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + + const result = await checkOAuthFlowStatus(mockUserId, mockServerName); + + expect(result).toEqual({ hasActiveFlow: false, hasFailedFlow: false }); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Error checking OAuth flows'), + mockError, + ); + }); + }); + + describe('getServerConnectionStatus', () => { + const mockUserId = 'user-123'; + const mockServerName = 'test-server'; + + it('should return app connection state when available', async () => { + const appConnections = new Map([[mockServerName, { connectionState: 'connected' }]]); + const userConnections = new Map(); + const oauthServers = new Set(); + + const result = await getServerConnectionStatus( + mockUserId, + mockServerName, + appConnections, + userConnections, + oauthServers, + ); + + expect(result).toEqual({ + requiresOAuth: false, + connectionState: 'connected', + }); + }); + + it('should fallback to user connection state when app connection not available', async () => { + const appConnections = new Map(); + const userConnections = new Map([[mockServerName, { connectionState: 'connecting' }]]); + const oauthServers = new Set(); + + const result = await getServerConnectionStatus( + mockUserId, + mockServerName, + appConnections, + userConnections, + oauthServers, + ); + + expect(result).toEqual({ + requiresOAuth: false, + connectionState: 'connecting', + }); + }); + + it('should default to disconnected when no connections exist', async () => { + const appConnections = new Map(); + const userConnections = new Map(); + const oauthServers = new Set(); + + const result = await getServerConnectionStatus( + mockUserId, + mockServerName, + appConnections, + userConnections, + oauthServers, + ); + + expect(result).toEqual({ + requiresOAuth: false, + connectionState: 'disconnected', + }); + }); + + it('should prioritize app connection over user connection', async () => { + const appConnections = new Map([[mockServerName, { connectionState: 'connected' }]]); + const userConnections = new Map([[mockServerName, { connectionState: 'disconnected' }]]); + const oauthServers = new Set(); + + const result = await getServerConnectionStatus( + mockUserId, + mockServerName, + appConnections, + userConnections, + oauthServers, + ); + + expect(result).toEqual({ + requiresOAuth: false, + connectionState: 'connected', + }); + }); + + it('should indicate OAuth requirement when server is in OAuth servers set', async () => { + const appConnections = new Map(); + const userConnections = new Map(); + const oauthServers = new Set([mockServerName]); + + const result = await getServerConnectionStatus( + mockUserId, + mockServerName, + appConnections, + userConnections, + oauthServers, + ); + + expect(result.requiresOAuth).toBe(true); + }); + + it('should handle OAuth flow status when disconnected and requires OAuth with failed flow', async () => { + const appConnections = new Map(); + const userConnections = new Map(); + const oauthServers = new Set([mockServerName]); + + // Mock flow state to return failed flow + const mockFlowManager = { + getFlowState: jest.fn(() => ({ + status: 'FAILED', + createdAt: Date.now() - 60000, + ttl: 180000, + })), + }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + mockGetLogStores.mockReturnValue({}); + MCPOAuthHandler.generateFlowId.mockReturnValue('test-flow-id'); + + const result = await getServerConnectionStatus( + mockUserId, + mockServerName, + appConnections, + userConnections, + oauthServers, + ); + + expect(result).toEqual({ + requiresOAuth: true, + connectionState: 'error', + }); + }); + + it('should handle OAuth flow status when disconnected and requires OAuth with active flow', async () => { + const appConnections = new Map(); + const userConnections = new Map(); + const oauthServers = new Set([mockServerName]); + + // Mock flow state to return active flow + const mockFlowManager = { + getFlowState: jest.fn(() => ({ + status: 'PENDING', + createdAt: Date.now() - 60000, // 1 minute ago + ttl: 180000, // 3 minutes TTL + })), + }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + mockGetLogStores.mockReturnValue({}); + MCPOAuthHandler.generateFlowId.mockReturnValue('test-flow-id'); + + const result = await getServerConnectionStatus( + mockUserId, + mockServerName, + appConnections, + userConnections, + oauthServers, + ); + + expect(result).toEqual({ + requiresOAuth: true, + connectionState: 'connecting', + }); + }); + + it('should handle OAuth flow status when disconnected and requires OAuth with no flow', async () => { + const appConnections = new Map(); + const userConnections = new Map(); + const oauthServers = new Set([mockServerName]); + + // Mock flow state to return no flow + const mockFlowManager = { + getFlowState: jest.fn(() => null), + }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + mockGetLogStores.mockReturnValue({}); + MCPOAuthHandler.generateFlowId.mockReturnValue('test-flow-id'); + + const result = await getServerConnectionStatus( + mockUserId, + mockServerName, + appConnections, + userConnections, + oauthServers, + ); + + expect(result).toEqual({ + requiresOAuth: true, + connectionState: 'disconnected', + }); + }); + + it('should not check OAuth flow status when server is connected', async () => { + const mockFlowManager = { + getFlowState: jest.fn(), + }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + mockGetLogStores.mockReturnValue({}); + + const appConnections = new Map([[mockServerName, { connectionState: 'connected' }]]); + const userConnections = new Map(); + const oauthServers = new Set([mockServerName]); + + const result = await getServerConnectionStatus( + mockUserId, + mockServerName, + appConnections, + userConnections, + oauthServers, + ); + + expect(result).toEqual({ + requiresOAuth: true, + connectionState: 'connected', + }); + + // Should not call flow manager since server is connected + expect(mockFlowManager.getFlowState).not.toHaveBeenCalled(); + }); + + it('should not check OAuth flow status when server does not require OAuth', async () => { + const mockFlowManager = { + getFlowState: jest.fn(), + }; + mockGetFlowStateManager.mockReturnValue(mockFlowManager); + mockGetLogStores.mockReturnValue({}); + + const appConnections = new Map(); + const userConnections = new Map(); + const oauthServers = new Set(); // Server not in OAuth servers + + const result = await getServerConnectionStatus( + mockUserId, + mockServerName, + appConnections, + userConnections, + oauthServers, + ); + + expect(result).toEqual({ + requiresOAuth: false, + connectionState: 'disconnected', + }); + + // Should not call flow manager since server doesn't require OAuth + expect(mockFlowManager.getFlowState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/client/src/components/MCP/ServerInitializationSection.tsx b/client/src/components/MCP/ServerInitializationSection.tsx index ea30f7135..36c9ca6b1 100644 --- a/client/src/components/MCP/ServerInitializationSection.tsx +++ b/client/src/components/MCP/ServerInitializationSection.tsx @@ -1,7 +1,8 @@ -import React, { useState, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { Button } from '@librechat/client'; import { RefreshCw, Link } from 'lucide-react'; -import { useLocalize, useMCPServerInitialization } from '~/hooks'; +import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager'; +import { useLocalize } from '~/hooks'; interface ServerInitializationSectionProps { serverName: string; @@ -14,32 +15,27 @@ export default function ServerInitializationSection({ }: ServerInitializationSectionProps) { const localize = useLocalize(); - const [oauthUrl, setOauthUrl] = useState(null); - - // Use the shared initialization hook - const { initializeServer, isLoading, connectionStatus, cancelOAuthFlow, isCancellable } = - useMCPServerInitialization({ - onOAuthStarted: (name, url) => { - // Store the OAuth URL locally for display - setOauthUrl(url); - }, - onSuccess: () => { - // Clear OAuth URL on success - setOauthUrl(null); - }, - }); + // Use the centralized server manager instead of the old initialization hook so we can handle multiple oauth flows at once + const { + initializeServer, + connectionStatus, + cancelOAuthFlow, + isInitializing, + isCancellable, + getOAuthUrl, + } = useMCPServerManager(); const serverStatus = connectionStatus[serverName]; const isConnected = serverStatus?.connectionState === 'connected'; const canCancel = isCancellable(serverName); + const isServerInitializing = isInitializing(serverName); + const serverOAuthUrl = getOAuthUrl(serverName); const handleInitializeClick = useCallback(() => { - setOauthUrl(null); initializeServer(serverName); }, [initializeServer, serverName]); const handleCancelClick = useCallback(() => { - setOauthUrl(null); cancelOAuthFlow(serverName); }, [cancelOAuthFlow, serverName]); @@ -49,11 +45,11 @@ export default function ServerInitializationSection({
); @@ -70,13 +66,13 @@ export default function ServerInitializationSection({
{/* Only show authenticate button when OAuth URL is not present */} - {!oauthUrl && ( + {!serverOAuthUrl && (
{/* OAuth URL display */} - {oauthUrl && ( + {serverOAuthUrl && (
@@ -106,7 +102,7 @@ export default function ServerInitializationSection({
- - - {isEnabled && ( - -
-
- {localize('com_ui_artifacts_options')} + return ( +
+ + ) => { + e.stopPropagation(); + handleArtifactsToggle(); + }} + onMouseEnter={() => { + if (isEnabled) { + menuStore.show(); + } + }} + className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover" + /> + } + > +
+ + {localize('com_ui_artifacts')} + {isEnabled && }
- - {/* Include shadcn/ui Option */} - { - event.preventDefault(); - event.stopPropagation(); - handleShadcnToggle(); - }} - disabled={isCustomEnabled} - className={cn( - 'mb-1 flex items-center justify-between rounded-lg px-2 py-2', - 'cursor-pointer text-text-primary outline-none transition-colors', - 'hover:bg-black/[0.075] dark:hover:bg-white/10', - 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', - isCustomEnabled && 'cursor-not-allowed opacity-50', - )} - > -
- - {localize('com_ui_include_shadcnui' as any)} -
-
- - {/* Custom Prompt Mode Option */} - { - event.preventDefault(); - event.stopPropagation(); - handleCustomToggle(); + + + + {isEnabled && ( + -
- - {localize('com_ui_custom_prompt_mode' as any)} +
+
+ {localize('com_ui_artifacts_options')} +
+ + {/* Include shadcn/ui Option */} + { + event.preventDefault(); + event.stopPropagation(); + handleShadcnToggle(); + }} + disabled={isCustomEnabled} + className={cn( + 'mb-1 flex items-center justify-between rounded-lg px-2 py-2', + 'cursor-pointer text-text-primary outline-none transition-colors', + 'hover:bg-black/[0.075] dark:hover:bg-white/10', + 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', + isCustomEnabled && 'cursor-not-allowed opacity-50', + )} + > +
+ + {localize('com_ui_include_shadcnui' as any)} +
+
+ + {/* Custom Prompt Mode Option */} + { + event.preventDefault(); + event.stopPropagation(); + handleCustomToggle(); + }} + className={cn( + 'flex items-center justify-between rounded-lg px-2 py-2', + 'cursor-pointer text-text-primary outline-none transition-colors', + 'hover:bg-black/[0.075] dark:hover:bg-white/10', + 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', + )} + > +
+ + {localize('com_ui_custom_prompt_mode' as any)} +
+
- -
-
- )} -
- ); -}; + + )} + +
+ ); + }, +); + +ArtifactsSubMenu.displayName = 'ArtifactsSubMenu'; export default React.memo(ArtifactsSubMenu); diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx index 1d704f2a6..3628b732a 100644 --- a/client/src/components/Chat/Input/MCPSubMenu.tsx +++ b/client/src/components/Chat/Input/MCPSubMenu.tsx @@ -11,115 +11,115 @@ interface MCPSubMenuProps { placeholder?: string; } -const MCPSubMenu = ({ placeholder, ...props }: MCPSubMenuProps) => { - const { - configuredServers, - mcpValues, - isPinned, - setIsPinned, - placeholderText, - toggleServerSelection, - getServerStatusIconProps, - getConfigDialogProps, - } = useMCPServerManager(); +const MCPSubMenu = React.forwardRef( + ({ placeholder, ...props }, ref) => { + const { + configuredServers, + mcpValues, + isPinned, + setIsPinned, + placeholderText, + toggleServerSelection, + getServerStatusIconProps, + getConfigDialogProps, + } = useMCPServerManager(); - const menuStore = Ariakit.useMenuStore({ - focusLoop: true, - showTimeout: 100, - placement: 'right', - }); + const menuStore = Ariakit.useMenuStore({ + focusLoop: true, + showTimeout: 100, + placement: 'right', + }); - // Don't render if no MCP servers are configured - if (!configuredServers || configuredServers.length === 0) { - return null; - } + // Don't render if no MCP servers are configured + if (!configuredServers || configuredServers.length === 0) { + return null; + } - const configDialogProps = getConfigDialogProps(); + const configDialogProps = getConfigDialogProps(); - return ( - <> - - ) => { - e.stopPropagation(); - menuStore.toggle(); - }} - className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover" - /> - } - > -
- - {placeholder || placeholderText} - -
- -
- - {configuredServers.map((serverName) => { - const statusIconProps = getServerStatusIconProps(serverName); - const isSelected = mcpValues?.includes(serverName) ?? false; - - const statusIcon = statusIconProps && ; - - return ( - { - event.preventDefault(); - toggleServerSelection(serverName); + return ( +
+ + ) => { + e.stopPropagation(); + menuStore.toggle(); }} - className={cn( - 'flex items-center gap-2 rounded-lg px-2 py-1.5 text-text-primary hover:cursor-pointer', - 'scroll-m-1 outline-none transition-colors', - 'hover:bg-black/[0.075] dark:hover:bg-white/10', - 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', - 'w-full min-w-0 justify-between text-sm', - )} - > - + + + {configuredServers.map((serverName) => { + const statusIconProps = getServerStatusIconProps(serverName); + const isSelected = mcpValues?.includes(serverName) ?? false; + + const statusIcon = statusIconProps && ; + + return ( + { + event.preventDefault(); + toggleServerSelection(serverName); + }} + className={cn( + 'flex items-center gap-2 rounded-lg px-2 py-1.5 text-text-primary hover:cursor-pointer', + 'scroll-m-1 outline-none transition-colors', + 'hover:bg-black/[0.075] dark:hover:bg-white/10', + 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', + 'w-full min-w-0 justify-between text-sm', + )} > - - {serverName} - - {statusIcon &&
{statusIcon}
} -
- ); - })} -
-
- {configDialogProps && } - - ); -}; +
+ + {serverName} +
+ {statusIcon &&
{statusIcon}
} + + ); + })} + + + {configDialogProps && } +
+ ); + }, +); + +MCPSubMenu.displayName = 'MCPSubMenu'; export default React.memo(MCPSubMenu); From ec3cbca6e367870840b7bda8e7aff63ffd1310ed Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 28 Jul 2025 14:21:39 -0400 Subject: [PATCH 024/224] =?UTF-8?q?=E2=9C=A8=20feat:=20Enhance=20Redis=20C?= =?UTF-8?q?onfig=20and=20Error=20Handling=20(#8709)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: Enhance Redis Config and Error Handling - Added new Redis configuration options: `REDIS_RETRY_MAX_DELAY`, `REDIS_RETRY_MAX_ATTEMPTS`, `REDIS_CONNECT_TIMEOUT`, and `REDIS_ENABLE_OFFLINE_QUEUE` to improve connection resilience. - Implemented error handling for Redis cache creation and session store initialization in `cacheFactory.js`. - Enhanced logging for Redis client events and errors in `redisClients.js`. - Updated `README.md` to document new Redis configuration options. * chore: Add JSDoc comments to Redis configuration options in cacheConfig.js for improved clarity and documentation * ci: update cacheFactory tests * refactor: remove fallback * fix: Improve error handling in Redis cache creation, re-throw errors when expected --- api/cache/cacheConfig.js | 8 ++ api/cache/cacheFactory.js | 61 +++++++++++--- api/cache/cacheFactory.spec.js | 142 ++++++++++++++++++++++++++++++++- api/cache/redisClients.js | 116 ++++++++++++++++++++++++++- redis-config/README.md | 9 +++ 5 files changed, 318 insertions(+), 18 deletions(-) diff --git a/api/cache/cacheConfig.js b/api/cache/cacheConfig.js index 87c403bae..1ca3e902e 100644 --- a/api/cache/cacheConfig.js +++ b/api/cache/cacheConfig.js @@ -44,6 +44,14 @@ const cacheConfig = { REDIS_KEY_PREFIX: process.env[REDIS_KEY_PREFIX_VAR] || REDIS_KEY_PREFIX || '', REDIS_MAX_LISTENERS: math(process.env.REDIS_MAX_LISTENERS, 40), REDIS_PING_INTERVAL: math(process.env.REDIS_PING_INTERVAL, 0), + /** Max delay between reconnection attempts in ms */ + REDIS_RETRY_MAX_DELAY: math(process.env.REDIS_RETRY_MAX_DELAY, 3000), + /** Max number of reconnection attempts (0 = infinite) */ + REDIS_RETRY_MAX_ATTEMPTS: math(process.env.REDIS_RETRY_MAX_ATTEMPTS, 10), + /** Connection timeout in ms */ + REDIS_CONNECT_TIMEOUT: math(process.env.REDIS_CONNECT_TIMEOUT, 10000), + /** Queue commands when disconnected */ + REDIS_ENABLE_OFFLINE_QUEUE: isEnabled(process.env.REDIS_ENABLE_OFFLINE_QUEUE ?? 'true'), CI: isEnabled(process.env.CI), DEBUG_MEMORY_CACHE: isEnabled(process.env.DEBUG_MEMORY_CACHE), diff --git a/api/cache/cacheFactory.js b/api/cache/cacheFactory.js index b9739f4d3..bc361d661 100644 --- a/api/cache/cacheFactory.js +++ b/api/cache/cacheFactory.js @@ -1,12 +1,13 @@ const KeyvRedis = require('@keyv/redis').default; const { Keyv } = require('keyv'); -const { cacheConfig } = require('./cacheConfig'); -const { keyvRedisClient, ioredisClient, GLOBAL_PREFIX_SEPARATOR } = require('./redisClients'); +const { RedisStore } = require('rate-limit-redis'); const { Time } = require('librechat-data-provider'); +const { logger } = require('@librechat/data-schemas'); const { RedisStore: ConnectRedis } = require('connect-redis'); const MemoryStore = require('memorystore')(require('express-session')); +const { keyvRedisClient, ioredisClient, GLOBAL_PREFIX_SEPARATOR } = require('./redisClients'); +const { cacheConfig } = require('./cacheConfig'); const { violationFile } = require('./keyvFiles'); -const { RedisStore } = require('rate-limit-redis'); /** * Creates a cache instance using Redis or a fallback store. Suitable for general caching needs. @@ -20,11 +21,21 @@ const standardCache = (namespace, ttl = undefined, fallbackStore = undefined) => cacheConfig.USE_REDIS && !cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES?.includes(namespace) ) { - const keyvRedis = new KeyvRedis(keyvRedisClient); - const cache = new Keyv(keyvRedis, { namespace, ttl }); - keyvRedis.namespace = cacheConfig.REDIS_KEY_PREFIX; - keyvRedis.keyPrefixSeparator = GLOBAL_PREFIX_SEPARATOR; - return cache; + try { + const keyvRedis = new KeyvRedis(keyvRedisClient); + const cache = new Keyv(keyvRedis, { namespace, ttl }); + keyvRedis.namespace = cacheConfig.REDIS_KEY_PREFIX; + keyvRedis.keyPrefixSeparator = GLOBAL_PREFIX_SEPARATOR; + + cache.on('error', (err) => { + logger.error(`Cache error in namespace ${namespace}:`, err); + }); + + return cache; + } catch (err) { + logger.error(`Failed to create Redis cache for namespace ${namespace}:`, err); + throw err; + } } if (fallbackStore) return new Keyv({ store: fallbackStore, namespace, ttl }); return new Keyv({ namespace, ttl }); @@ -50,7 +61,13 @@ const violationCache = (namespace, ttl = undefined) => { const sessionCache = (namespace, ttl = undefined) => { namespace = namespace.endsWith(':') ? namespace : `${namespace}:`; if (!cacheConfig.USE_REDIS) return new MemoryStore({ ttl, checkPeriod: Time.ONE_DAY }); - return new ConnectRedis({ client: ioredisClient, ttl, prefix: namespace }); + const store = new ConnectRedis({ client: ioredisClient, ttl, prefix: namespace }); + if (ioredisClient) { + ioredisClient.on('error', (err) => { + logger.error(`Session store Redis error for namespace ${namespace}:`, err); + }); + } + return store; }; /** @@ -62,8 +79,30 @@ const limiterCache = (prefix) => { if (!prefix) throw new Error('prefix is required'); if (!cacheConfig.USE_REDIS) return undefined; prefix = prefix.endsWith(':') ? prefix : `${prefix}:`; - return new RedisStore({ sendCommand, prefix }); + + try { + if (!ioredisClient) { + logger.warn(`Redis client not available for rate limiter with prefix ${prefix}`); + return undefined; + } + + return new RedisStore({ sendCommand, prefix }); + } catch (err) { + logger.error(`Failed to create Redis rate limiter for prefix ${prefix}:`, err); + return undefined; + } +}; + +const sendCommand = (...args) => { + if (!ioredisClient) { + logger.warn('Redis client not available for command execution'); + return Promise.reject(new Error('Redis client not available')); + } + + return ioredisClient.call(...args).catch((err) => { + logger.error('Redis command execution failed:', err); + throw err; + }); }; -const sendCommand = (...args) => ioredisClient?.call(...args); module.exports = { standardCache, sessionCache, violationCache, limiterCache }; diff --git a/api/cache/cacheFactory.spec.js b/api/cache/cacheFactory.spec.js index 76d01a915..ce364a4a3 100644 --- a/api/cache/cacheFactory.spec.js +++ b/api/cache/cacheFactory.spec.js @@ -6,13 +6,17 @@ const mockKeyvRedis = { keyPrefixSeparator: '', }; -const mockKeyv = jest.fn().mockReturnValue({ mock: 'keyv' }); +const mockKeyv = jest.fn().mockReturnValue({ + mock: 'keyv', + on: jest.fn(), +}); const mockConnectRedis = jest.fn().mockReturnValue({ mock: 'connectRedis' }); const mockMemoryStore = jest.fn().mockReturnValue({ mock: 'memoryStore' }); const mockRedisStore = jest.fn().mockReturnValue({ mock: 'redisStore' }); const mockIoredisClient = { call: jest.fn(), + on: jest.fn(), }; const mockKeyvRedisClient = {}; @@ -53,6 +57,14 @@ jest.mock('rate-limit-redis', () => ({ RedisStore: mockRedisStore, })); +jest.mock('@librechat/data-schemas', () => ({ + logger: { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + }, +})); + // Import after mocking const { standardCache, sessionCache, violationCache, limiterCache } = require('./cacheFactory'); const { cacheConfig } = require('./cacheConfig'); @@ -142,6 +154,28 @@ describe('cacheFactory', () => { expect(require('@keyv/redis').default).toHaveBeenCalledWith(mockKeyvRedisClient); expect(mockKeyv).toHaveBeenCalledWith(mockKeyvRedis, { namespace, ttl }); }); + + it('should throw error when Redis cache creation fails', () => { + cacheConfig.USE_REDIS = true; + const namespace = 'test-namespace'; + const ttl = 3600; + const testError = new Error('Redis connection failed'); + + const KeyvRedis = require('@keyv/redis').default; + KeyvRedis.mockImplementationOnce(() => { + throw testError; + }); + + expect(() => standardCache(namespace, ttl)).toThrow('Redis connection failed'); + + const { logger } = require('@librechat/data-schemas'); + expect(logger.error).toHaveBeenCalledWith( + `Failed to create Redis cache for namespace ${namespace}:`, + testError, + ); + + expect(mockKeyv).not.toHaveBeenCalled(); + }); }); describe('violationCache', () => { @@ -233,6 +267,86 @@ describe('cacheFactory', () => { checkPeriod: Time.ONE_DAY, }); }); + + it('should throw error when ConnectRedis constructor fails', () => { + cacheConfig.USE_REDIS = true; + const namespace = 'sessions'; + const ttl = 86400; + + // Mock ConnectRedis to throw an error during construction + const redisError = new Error('Redis connection failed'); + mockConnectRedis.mockImplementationOnce(() => { + throw redisError; + }); + + // The error should propagate up, not be caught + expect(() => sessionCache(namespace, ttl)).toThrow('Redis connection failed'); + + // Verify that MemoryStore was NOT used as fallback + expect(mockMemoryStore).not.toHaveBeenCalled(); + }); + + it('should register error handler but let errors propagate to Express', () => { + cacheConfig.USE_REDIS = true; + const namespace = 'sessions'; + + // Create a mock session store with middleware methods + const mockSessionStore = { + get: jest.fn(), + set: jest.fn(), + destroy: jest.fn(), + }; + mockConnectRedis.mockReturnValue(mockSessionStore); + + const store = sessionCache(namespace); + + // Verify error handler was registered + expect(mockIoredisClient.on).toHaveBeenCalledWith('error', expect.any(Function)); + + // Get the error handler + const errorHandler = mockIoredisClient.on.mock.calls.find((call) => call[0] === 'error')[1]; + + // Simulate an error from Redis during a session operation + const redisError = new Error('Socket closed unexpectedly'); + + // The error handler should log but not swallow the error + const { logger } = require('@librechat/data-schemas'); + errorHandler(redisError); + + expect(logger.error).toHaveBeenCalledWith( + `Session store Redis error for namespace ${namespace}::`, + redisError, + ); + + // Now simulate what happens when session middleware tries to use the store + const callback = jest.fn(); + mockSessionStore.get.mockImplementation((sid, cb) => { + cb(new Error('Redis connection lost')); + }); + + // Call the store's get method (as Express session would) + store.get('test-session-id', callback); + + // The error should be passed to the callback, not swallowed + expect(callback).toHaveBeenCalledWith(new Error('Redis connection lost')); + }); + + it('should handle null ioredisClient gracefully', () => { + cacheConfig.USE_REDIS = true; + const namespace = 'sessions'; + + // Temporarily set ioredisClient to null (simulating connection not established) + const originalClient = require('./redisClients').ioredisClient; + require('./redisClients').ioredisClient = null; + + // ConnectRedis might accept null client but would fail on first use + // The important thing is it doesn't throw uncaught exceptions during construction + const store = sessionCache(namespace); + expect(store).toBeDefined(); + + // Restore original client + require('./redisClients').ioredisClient = originalClient; + }); }); describe('limiterCache', () => { @@ -274,8 +388,10 @@ describe('cacheFactory', () => { }); }); - it('should pass sendCommand function that calls ioredisClient.call', () => { + it('should pass sendCommand function that calls ioredisClient.call', async () => { cacheConfig.USE_REDIS = true; + mockIoredisClient.call.mockResolvedValue('test-value'); + limiterCache('rate-limit'); const sendCommandCall = mockRedisStore.mock.calls[0][0]; @@ -283,9 +399,29 @@ describe('cacheFactory', () => { // Test that sendCommand properly delegates to ioredisClient.call const args = ['GET', 'test-key']; - sendCommand(...args); + const result = await sendCommand(...args); expect(mockIoredisClient.call).toHaveBeenCalledWith(...args); + expect(result).toBe('test-value'); + }); + + it('should handle sendCommand errors properly', async () => { + cacheConfig.USE_REDIS = true; + + // Mock the call method to reject with an error + const testError = new Error('Redis error'); + mockIoredisClient.call.mockRejectedValue(testError); + + limiterCache('rate-limit'); + + const sendCommandCall = mockRedisStore.mock.calls[0][0]; + const sendCommand = sendCommandCall.sendCommand; + + // Test that sendCommand properly handles errors + const args = ['GET', 'test-key']; + + await expect(sendCommand(...args)).rejects.toThrow('Redis error'); + expect(mockIoredisClient.call).toHaveBeenCalledWith(...args); }); it('should handle undefined prefix', () => { diff --git a/api/cache/redisClients.js b/api/cache/redisClients.js index 46c2813e9..17889d24f 100644 --- a/api/cache/redisClients.js +++ b/api/cache/redisClients.js @@ -13,23 +13,82 @@ const ca = cacheConfig.REDIS_CA; /** @type {import('ioredis').Redis | import('ioredis').Cluster | null} */ let ioredisClient = null; if (cacheConfig.USE_REDIS) { + /** @type {import('ioredis').RedisOptions | import('ioredis').ClusterOptions} */ const redisOptions = { username: username, password: password, tls: ca ? { ca } : undefined, keyPrefix: `${cacheConfig.REDIS_KEY_PREFIX}${GLOBAL_PREFIX_SEPARATOR}`, maxListeners: cacheConfig.REDIS_MAX_LISTENERS, + retryStrategy: (times) => { + if ( + cacheConfig.REDIS_RETRY_MAX_ATTEMPTS > 0 && + times > cacheConfig.REDIS_RETRY_MAX_ATTEMPTS + ) { + logger.error( + `ioredis giving up after ${cacheConfig.REDIS_RETRY_MAX_ATTEMPTS} reconnection attempts`, + ); + return null; + } + const delay = Math.min(times * 50, cacheConfig.REDIS_RETRY_MAX_DELAY); + logger.info(`ioredis reconnecting... attempt ${times}, delay ${delay}ms`); + return delay; + }, + reconnectOnError: (err) => { + const targetError = 'READONLY'; + if (err.message.includes(targetError)) { + logger.warn('ioredis reconnecting due to READONLY error'); + return true; + } + return false; + }, + enableOfflineQueue: cacheConfig.REDIS_ENABLE_OFFLINE_QUEUE, + connectTimeout: cacheConfig.REDIS_CONNECT_TIMEOUT, + maxRetriesPerRequest: 3, }; ioredisClient = urls.length === 1 ? new IoRedis(cacheConfig.REDIS_URI, redisOptions) - : new IoRedis.Cluster(cacheConfig.REDIS_URI, { redisOptions }); + : new IoRedis.Cluster(cacheConfig.REDIS_URI, { + redisOptions, + clusterRetryStrategy: (times) => { + if ( + cacheConfig.REDIS_RETRY_MAX_ATTEMPTS > 0 && + times > cacheConfig.REDIS_RETRY_MAX_ATTEMPTS + ) { + logger.error( + `ioredis cluster giving up after ${cacheConfig.REDIS_RETRY_MAX_ATTEMPTS} reconnection attempts`, + ); + return null; + } + const delay = Math.min(times * 100, cacheConfig.REDIS_RETRY_MAX_DELAY); + logger.info(`ioredis cluster reconnecting... attempt ${times}, delay ${delay}ms`); + return delay; + }, + enableOfflineQueue: cacheConfig.REDIS_ENABLE_OFFLINE_QUEUE, + }); ioredisClient.on('error', (err) => { logger.error('ioredis client error:', err); }); + ioredisClient.on('connect', () => { + logger.info('ioredis client connected'); + }); + + ioredisClient.on('ready', () => { + logger.info('ioredis client ready'); + }); + + ioredisClient.on('reconnecting', (delay) => { + logger.info(`ioredis client reconnecting in ${delay}ms`); + }); + + ioredisClient.on('close', () => { + logger.warn('ioredis client connection closed'); + }); + /** Ping Interval to keep the Redis server connection alive (if enabled) */ let pingInterval = null; const clearPingInterval = () => { @@ -42,7 +101,9 @@ if (cacheConfig.USE_REDIS) { if (cacheConfig.REDIS_PING_INTERVAL > 0) { pingInterval = setInterval(() => { if (ioredisClient && ioredisClient.status === 'ready') { - ioredisClient.ping(); + ioredisClient.ping().catch((err) => { + logger.error('ioredis ping failed:', err); + }); } }, cacheConfig.REDIS_PING_INTERVAL * 1000); ioredisClient.on('close', clearPingInterval); @@ -56,8 +117,32 @@ if (cacheConfig.USE_REDIS) { /** * ** WARNING ** Keyv Redis client does not support Prefix like ioredis above. * The prefix feature will be handled by the Keyv-Redis store in cacheFactory.js + * @type {import('@keyv/redis').RedisClientOptions | import('@keyv/redis').RedisClusterOptions} */ - const redisOptions = { username, password, socket: { tls: ca != null, ca } }; + const redisOptions = { + username, + password, + socket: { + tls: ca != null, + ca, + connectTimeout: cacheConfig.REDIS_CONNECT_TIMEOUT, + reconnectStrategy: (retries) => { + if ( + cacheConfig.REDIS_RETRY_MAX_ATTEMPTS > 0 && + retries > cacheConfig.REDIS_RETRY_MAX_ATTEMPTS + ) { + logger.error( + `@keyv/redis client giving up after ${cacheConfig.REDIS_RETRY_MAX_ATTEMPTS} reconnection attempts`, + ); + return new Error('Max reconnection attempts reached'); + } + const delay = Math.min(retries * 100, cacheConfig.REDIS_RETRY_MAX_DELAY); + logger.info(`@keyv/redis reconnecting... attempt ${retries}, delay ${delay}ms`); + return delay; + }, + }, + disableOfflineQueue: !cacheConfig.REDIS_ENABLE_OFFLINE_QUEUE, + }; keyvRedisClient = urls.length === 1 @@ -73,6 +158,27 @@ if (cacheConfig.USE_REDIS) { logger.error('@keyv/redis client error:', err); }); + keyvRedisClient.on('connect', () => { + logger.info('@keyv/redis client connected'); + }); + + keyvRedisClient.on('ready', () => { + logger.info('@keyv/redis client ready'); + }); + + keyvRedisClient.on('reconnecting', () => { + logger.info('@keyv/redis client reconnecting...'); + }); + + keyvRedisClient.on('disconnect', () => { + logger.warn('@keyv/redis client disconnected'); + }); + + keyvRedisClient.connect().catch((err) => { + logger.error('@keyv/redis initial connection failed:', err); + throw err; + }); + /** Ping Interval to keep the Redis server connection alive (if enabled) */ let pingInterval = null; const clearPingInterval = () => { @@ -85,7 +191,9 @@ if (cacheConfig.USE_REDIS) { if (cacheConfig.REDIS_PING_INTERVAL > 0) { pingInterval = setInterval(() => { if (keyvRedisClient && keyvRedisClient.isReady) { - keyvRedisClient.ping(); + keyvRedisClient.ping().catch((err) => { + logger.error('@keyv/redis ping failed:', err); + }); } }, cacheConfig.REDIS_PING_INTERVAL * 1000); keyvRedisClient.on('disconnect', clearPingInterval); diff --git a/redis-config/README.md b/redis-config/README.md index 024d0b168..607075569 100644 --- a/redis-config/README.md +++ b/redis-config/README.md @@ -174,6 +174,15 @@ REDIS_KEY_PREFIX=librechat # Connection limits REDIS_MAX_LISTENERS=40 + +# Ping interval to keep connection alive (seconds, 0 to disable) +REDIS_PING_INTERVAL=0 + +# Reconnection configuration +REDIS_RETRY_MAX_DELAY=3000 # Max delay between reconnection attempts (ms) +REDIS_RETRY_MAX_ATTEMPTS=10 # Max reconnection attempts (0 = infinite) +REDIS_CONNECT_TIMEOUT=10000 # Connection timeout (ms) +REDIS_ENABLE_OFFLINE_QUEUE=true # Queue commands when disconnected ``` ## TLS/SSL Redis Setup From 8e6eef04abb5727c7d8708c7363a0ce3f51a2a5b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 28 Jul 2025 15:12:29 -0400 Subject: [PATCH 025/224] =?UTF-8?q?=F0=9F=94=A7=20fix:=20Update=20Proxy=20?= =?UTF-8?q?Config=20for=20OpenAI=20Image=20Tools=20(#8712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replaced HttpsProxyAgent with ProxyAgent from undici for improved proxy handling in DALLE3.js and OpenAIImageTools.js. - Updated fetchOptions to use dispatcher for proxy configuration. - Added new test suite for DALLE3 to verify proxy configuration behavior based on environment variables. --- api/app/clients/tools/structured/DALLE3.js | 10 +- .../tools/structured/OpenAIImageTools.js | 25 ++++- .../structured/specs/DALLE3-proxy.spec.js | 94 +++++++++++++++++++ 3 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js diff --git a/api/app/clients/tools/structured/DALLE3.js b/api/app/clients/tools/structured/DALLE3.js index 7c2a56fe7..5f6e335a1 100644 --- a/api/app/clients/tools/structured/DALLE3.js +++ b/api/app/clients/tools/structured/DALLE3.js @@ -3,8 +3,8 @@ const path = require('path'); const OpenAI = require('openai'); const fetch = require('node-fetch'); const { v4: uuidv4 } = require('uuid'); +const { ProxyAgent } = require('undici'); const { Tool } = require('@langchain/core/tools'); -const { HttpsProxyAgent } = require('https-proxy-agent'); const { FileContext, ContentTypes } = require('librechat-data-provider'); const { getImageBasename } = require('~/server/services/Files/images'); const extractBaseURL = require('~/utils/extractBaseURL'); @@ -46,7 +46,10 @@ class DALLE3 extends Tool { } if (process.env.PROXY) { - config.httpAgent = new HttpsProxyAgent(process.env.PROXY); + const proxyAgent = new ProxyAgent(process.env.PROXY); + config.fetchOptions = { + dispatcher: proxyAgent, + }; } /** @type {OpenAI} */ @@ -163,7 +166,8 @@ Error Message: ${error.message}`); if (this.isAgent) { let fetchOptions = {}; if (process.env.PROXY) { - fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY); + const proxyAgent = new ProxyAgent(process.env.PROXY); + fetchOptions.dispatcher = proxyAgent; } const imageResponse = await fetch(theImageUrl, fetchOptions); const arrayBuffer = await imageResponse.arrayBuffer(); diff --git a/api/app/clients/tools/structured/OpenAIImageTools.js b/api/app/clients/tools/structured/OpenAIImageTools.js index 411db1edf..920555da3 100644 --- a/api/app/clients/tools/structured/OpenAIImageTools.js +++ b/api/app/clients/tools/structured/OpenAIImageTools.js @@ -3,10 +3,10 @@ const axios = require('axios'); const { v4 } = require('uuid'); const OpenAI = require('openai'); const FormData = require('form-data'); +const { ProxyAgent } = require('undici'); const { tool } = require('@langchain/core/tools'); const { logAxiosError } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); -const { HttpsProxyAgent } = require('https-proxy-agent'); const { ContentTypes, EImageOutputType } = require('librechat-data-provider'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { extractBaseURL } = require('~/utils'); @@ -189,7 +189,10 @@ function createOpenAIImageTools(fields = {}) { } const clientConfig = { ...closureConfig }; if (process.env.PROXY) { - clientConfig.httpAgent = new HttpsProxyAgent(process.env.PROXY); + const proxyAgent = new ProxyAgent(process.env.PROXY); + clientConfig.fetchOptions = { + dispatcher: proxyAgent, + }; } /** @type {OpenAI} */ @@ -335,7 +338,10 @@ Error Message: ${error.message}`); const clientConfig = { ...closureConfig }; if (process.env.PROXY) { - clientConfig.httpAgent = new HttpsProxyAgent(process.env.PROXY); + const proxyAgent = new ProxyAgent(process.env.PROXY); + clientConfig.fetchOptions = { + dispatcher: proxyAgent, + }; } const formData = new FormData(); @@ -447,6 +453,19 @@ Error Message: ${error.message}`); baseURL, }; + if (process.env.PROXY) { + try { + const url = new URL(process.env.PROXY); + axiosConfig.proxy = { + host: url.hostname.replace(/^\[|\]$/g, ''), + port: url.port ? parseInt(url.port, 10) : undefined, + protocol: url.protocol.replace(':', ''), + }; + } catch (error) { + logger.error('Error parsing proxy URL:', error); + } + } + if (process.env.IMAGE_GEN_OAI_AZURE_API_VERSION && process.env.IMAGE_GEN_OAI_BASEURL) { axiosConfig.params = { 'api-version': process.env.IMAGE_GEN_OAI_AZURE_API_VERSION, diff --git a/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js b/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js new file mode 100644 index 000000000..768d81e88 --- /dev/null +++ b/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js @@ -0,0 +1,94 @@ +const DALLE3 = require('../DALLE3'); +const { ProxyAgent } = require('undici'); + +const processFileURL = jest.fn(); + +jest.mock('~/server/services/Files/images', () => ({ + getImageBasename: jest.fn().mockImplementation((url) => { + const parts = url.split('/'); + const lastPart = parts.pop(); + const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i; + if (imageExtensionRegex.test(lastPart)) { + return lastPart; + } + return ''; + }), +})); + +jest.mock('fs', () => { + return { + existsSync: jest.fn(), + mkdirSync: jest.fn(), + promises: { + writeFile: jest.fn(), + readFile: jest.fn(), + unlink: jest.fn(), + }, + }; +}); + +jest.mock('path', () => { + return { + resolve: jest.fn(), + join: jest.fn(), + relative: jest.fn(), + extname: jest.fn().mockImplementation((filename) => { + return filename.slice(filename.lastIndexOf('.')); + }), + }; +}); + +describe('DALLE3 Proxy Configuration', () => { + let originalEnv; + + beforeAll(() => { + originalEnv = { ...process.env }; + }); + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should configure ProxyAgent in fetchOptions.dispatcher when PROXY env is set', () => { + // Set proxy environment variable + process.env.PROXY = 'http://proxy.example.com:8080'; + process.env.DALLE_API_KEY = 'test-api-key'; + + // Create instance + const dalleWithProxy = new DALLE3({ processFileURL }); + + // Check that the openai client exists + expect(dalleWithProxy.openai).toBeDefined(); + + // Check that _options exists and has fetchOptions with a dispatcher + expect(dalleWithProxy.openai._options).toBeDefined(); + expect(dalleWithProxy.openai._options.fetchOptions).toBeDefined(); + expect(dalleWithProxy.openai._options.fetchOptions.dispatcher).toBeDefined(); + expect(dalleWithProxy.openai._options.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent); + }); + + it('should not configure ProxyAgent when PROXY env is not set', () => { + // Ensure PROXY is not set + delete process.env.PROXY; + process.env.DALLE_API_KEY = 'test-api-key'; + + // Create instance + const dalleWithoutProxy = new DALLE3({ processFileURL }); + + // Check that the openai client exists + expect(dalleWithoutProxy.openai).toBeDefined(); + + // Check that _options exists but fetchOptions either doesn't exist or doesn't have a dispatcher + expect(dalleWithoutProxy.openai._options).toBeDefined(); + + // fetchOptions should either not exist or not have a dispatcher + if (dalleWithoutProxy.openai._options.fetchOptions) { + expect(dalleWithoutProxy.openai._options.fetchOptions.dispatcher).toBeUndefined(); + } + }); +}); From a4ca4b7d9db5ba1ed76c2ed9c6e8008ccd1a646d Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 28 Jul 2025 15:14:37 -0400 Subject: [PATCH 026/224] =?UTF-8?q?=F0=9F=94=80=20fix:=20Rerender=20Edge?= =?UTF-8?q?=20Cases=20After=20Migration=20to=20Shared=20Package=20(#8713)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: render issues in PromptForm by decoupling nested dependencies as a result of @librechat/client components * fix: MemoryViewer flicker by moving EditMemoryButton and DeleteMemoryButton outside of rendering * fix: CategorySelector to use DropdownPopup for improved mobile compatibility * chore: imports --- .../src/components/Prompts/DeleteVersion.tsx | 44 ++- .../Prompts/Groups/CategorySelector.tsx | 110 ++++--- client/src/components/Prompts/PromptForm.tsx | 299 +++++++++++------- .../SidePanel/Memories/MemoryViewer.tsx | 216 ++++++------- 4 files changed, 401 insertions(+), 268 deletions(-) diff --git a/client/src/components/Prompts/DeleteVersion.tsx b/client/src/components/Prompts/DeleteVersion.tsx index 541549145..8a7f6109b 100644 --- a/client/src/components/Prompts/DeleteVersion.tsx +++ b/client/src/components/Prompts/DeleteVersion.tsx @@ -1,8 +1,10 @@ +import React, { useCallback } from 'react'; import { Trash2 } from 'lucide-react'; +import { useDeletePrompt } from '~/data-provider'; import { Button, OGDialog, OGDialogTrigger, Label, OGDialogTemplate } from '@librechat/client'; import { useLocalize } from '~/hooks'; -const DeleteVersion = ({ +const DeleteConfirmDialog = ({ name, disabled, selectHandler, @@ -58,4 +60,42 @@ const DeleteVersion = ({ ); }; -export default DeleteVersion; +interface DeletePromptProps { + promptId?: string; + groupId: string; + promptName: string; + disabled: boolean; +} + +const DeletePrompt = React.memo( + ({ promptId, groupId, promptName, disabled }: DeletePromptProps) => { + const deletePromptMutation = useDeletePrompt(); + + const handleDelete = useCallback(() => { + if (!promptId) { + console.warn('No prompt ID provided for deletion'); + return; + } + deletePromptMutation.mutate({ + _id: promptId, + groupId, + }); + }, [promptId, groupId, deletePromptMutation]); + + if (!promptId) { + return null; + } + + return ( + + ); + }, +); + +DeletePrompt.displayName = 'DeletePrompt'; + +export default DeletePrompt; diff --git a/client/src/components/Prompts/Groups/CategorySelector.tsx b/client/src/components/Prompts/Groups/CategorySelector.tsx index 3ceb7242b..e8661b8b6 100644 --- a/client/src/components/Prompts/Groups/CategorySelector.tsx +++ b/client/src/components/Prompts/Groups/CategorySelector.tsx @@ -1,10 +1,13 @@ -import React, { useMemo } from 'react'; -import { Dropdown } from '@librechat/client'; +import React, { useMemo, useState } from 'react'; +import * as Ariakit from '@ariakit/react'; import { useTranslation } from 'react-i18next'; -import { useFormContext, Controller } from 'react-hook-form'; +import { DropdownPopup } from '@librechat/client'; import { LocalStorageKeys } from 'librechat-data-provider'; +import { useFormContext, Controller } from 'react-hook-form'; +import type { MenuItemProps } from '@librechat/client'; import type { ReactNode } from 'react'; import { useCategories } from '~/hooks'; +import { cn } from '~/utils'; interface CategorySelectorProps { currentCategory?: string; @@ -20,10 +23,11 @@ const CategorySelector: React.FC = ({ const { t } = useTranslation(); const formContext = useFormContext(); const { categories, emptyCategory } = useCategories(); + const [isOpen, setIsOpen] = useState(false); - const control = formContext.control; - const watch = formContext.watch; - const setValue = formContext.setValue; + const control = formContext?.control; + const watch = formContext?.watch; + const setValue = formContext?.setValue; const watchedCategory = watch ? watch('category') : currentCategory; @@ -46,53 +50,71 @@ const CategorySelector: React.FC = ({ return categoryOption; }, [categoryOption, t]); + const menuItems: MenuItemProps[] = useMemo(() => { + if (!categories) return []; + + return categories.map((category) => ({ + id: category.value, + label: category.label, + icon: 'icon' in category ? category.icon : undefined, + onClick: () => { + const value = category.value || ''; + if (formContext && setValue) { + setValue('category', value, { shouldDirty: false }); + } + localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value); + onValueChange?.(value); + setIsOpen(false); + }, + })); + }, [categories, formContext, setValue, onValueChange]); + + const trigger = ( + setIsOpen(!isOpen)} + aria-label="Prompt's category selector" + aria-labelledby="category-selector-label" + > +
+ {'icon' in displayCategory && displayCategory.icon != null && ( + {displayCategory.icon as ReactNode} + )} + {displayCategory.value ? displayCategory.label : t('com_ui_category')} +
+ +
+ ); + return formContext ? ( ( - { - setValue('category', value, { shouldDirty: false }); - localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value); - onValueChange?.(value); - }} - aria-labelledby="category-selector-label" - ariaLabel="Prompt's category selector" - className={className} - options={categories || []} - renderValue={() => ( -
- {'icon' in displayCategory && displayCategory.icon != null && ( - {displayCategory.icon as ReactNode} - )} - {displayCategory.label} -
- )} + )} /> ) : ( - { - localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value); - onValueChange?.(value); - }} - aria-labelledby="category-selector-label" - ariaLabel="Prompt's category selector" - className={className} - options={categories || []} - renderValue={() => ( -
- {'icon' in displayCategory && displayCategory.icon != null && ( - {displayCategory.icon as ReactNode} - )} - {displayCategory.label} -
- )} + ); }; diff --git a/client/src/components/Prompts/PromptForm.tsx b/client/src/components/Prompts/PromptForm.tsx index 32277d5b6..1be920a05 100644 --- a/client/src/components/Prompts/PromptForm.tsx +++ b/client/src/components/Prompts/PromptForm.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, useMemo, useCallback, useRef } from 'react'; +import React from 'react'; import debounce from 'lodash/debounce'; import { useRecoilValue } from 'recoil'; import { Menu, Rocket } from 'lucide-react'; @@ -6,14 +7,13 @@ import { useForm, FormProvider } from 'react-hook-form'; import { useParams, useOutletContext } from 'react-router-dom'; import { Button, Skeleton, useToastContext } from '@librechat/client'; import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider'; -import type { TCreatePrompt } from 'librechat-data-provider'; +import type { TCreatePrompt, TPrompt, TPromptGroup } from 'librechat-data-provider'; import { - useCreatePrompt, useGetPrompts, + useCreatePrompt, useGetPromptGroup, useUpdatePromptGroup, useMakePromptProduction, - useDeletePrompt, } from '~/data-provider'; import { useAuthContext, usePromptGroupsNav, useHasAccess, useLocalize } from '~/hooks'; import CategorySelector from './Groups/CategorySelector'; @@ -22,7 +22,7 @@ import PromptVariables from './PromptVariables'; import { cn, findPromptGroup } from '~/utils'; import PromptVersions from './PromptVersions'; import { PromptsEditorMode } from '~/common'; -import DeleteConfirm from './DeleteVersion'; +import DeleteVersion from './DeleteVersion'; import PromptDetails from './PromptDetails'; import PromptEditor from './PromptEditor'; import SkeletonForm from './SkeletonForm'; @@ -32,16 +32,136 @@ import PromptName from './PromptName'; import Command from './Command'; import store from '~/store'; +interface RightPanelProps { + group: TPromptGroup; + prompts: TPrompt[]; + selectedPrompt: any; + selectionIndex: number; + selectedPromptId?: string; + isLoadingPrompts: boolean; + setSelectionIndex: React.Dispatch>; +} + +const RightPanel = React.memo( + ({ + group, + prompts, + selectedPrompt, + selectedPromptId, + isLoadingPrompts, + selectionIndex, + setSelectionIndex, + }: RightPanelProps) => { + const localize = useLocalize(); + const { showToast } = useToastContext(); + const editorMode = useRecoilValue(store.promptsEditorMode); + const hasShareAccess = useHasAccess({ + permissionType: PermissionTypes.PROMPTS, + permission: Permissions.SHARED_GLOBAL, + }); + + const updateGroupMutation = useUpdatePromptGroup({ + onError: () => { + showToast({ + status: 'error', + message: localize('com_ui_prompt_update_error'), + }); + }, + }); + + const makeProductionMutation = useMakePromptProduction(); + + const groupId = group?._id || ''; + const groupName = group?.name || ''; + const groupCategory = group?.category || ''; + const isLoadingGroup = !group; + + return ( +
+
+ + updateGroupMutation.mutate({ + id: groupId, + payload: { name: groupName, category: value }, + }) + } + /> +
+ {hasShareAccess && } + {editorMode === PromptsEditorMode.ADVANCED && ( + + )} + +
+
+ {editorMode === PromptsEditorMode.ADVANCED && + (isLoadingPrompts + ? Array.from({ length: 6 }).map((_, index: number) => ( +
+ +
+ )) + : prompts.length > 0 && ( + + ))} +
+ ); + }, +); + +RightPanel.displayName = 'RightPanel'; + const PromptForm = () => { const params = useParams(); const localize = useLocalize(); const { user } = useAuthContext(); - const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd); const { showToast } = useToastContext(); + const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd); const promptId = params.promptId || ''; - const [selectionIndex, setSelectionIndex] = useState(0); const editorMode = useRecoilValue(store.promptsEditorMode); + const [selectionIndex, setSelectionIndex] = useState(0); + const prevIsEditingRef = useRef(false); const [isEditing, setIsEditing] = useState(false); const [initialLoad, setInitialLoad] = useState(true); @@ -72,11 +192,9 @@ const PromptForm = () => { [prompts, selectionIndex], ); + const selectedPromptId = useMemo(() => selectedPrompt?._id, [selectedPrompt?._id]); + const { groupsQuery } = useOutletContext>(); - const hasShareAccess = useHasAccess({ - permissionType: PermissionTypes.PROMPTS, - permission: Permissions.SHARED_GLOBAL, - }); const updateGroupMutation = useUpdatePromptGroup({ onError: () => { @@ -88,7 +206,6 @@ const PromptForm = () => { }); const makeProductionMutation = useMakePromptProduction(); - const deletePromptMutation = useDeletePrompt(); const createPromptMutation = useCreatePrompt({ onMutate: (variables) => { @@ -177,24 +294,40 @@ const PromptForm = () => { return () => window.removeEventListener('resize', handleResize); }, []); - const debouncedUpdateOneliner = useCallback( - debounce((oneliner: string) => { - if (!group || !group._id) { - return console.warn('Group not found'); - } - updateGroupMutation.mutate({ id: group._id, payload: { oneliner } }); - }, 950), - [updateGroupMutation, group], + const debouncedUpdateOneliner = useMemo( + () => + debounce((groupId: string, oneliner: string, mutate: any) => { + mutate({ id: groupId, payload: { oneliner } }); + }, 950), + [], ); - const debouncedUpdateCommand = useCallback( - debounce((command: string) => { + const debouncedUpdateCommand = useMemo( + () => + debounce((groupId: string, command: string, mutate: any) => { + mutate({ id: groupId, payload: { command } }); + }, 950), + [], + ); + + const handleUpdateOneliner = useCallback( + (oneliner: string) => { if (!group || !group._id) { return console.warn('Group not found'); } - updateGroupMutation.mutate({ id: group._id, payload: { command } }); - }, 950), - [updateGroupMutation, group], + debouncedUpdateOneliner(group._id, oneliner, updateGroupMutation.mutate); + }, + [group, updateGroupMutation.mutate, debouncedUpdateOneliner], + ); + + const handleUpdateCommand = useCallback( + (command: string) => { + if (!group || !group._id) { + return console.warn('Group not found'); + } + debouncedUpdateCommand(group._id, command, updateGroupMutation.mutate); + }, + [group, updateGroupMutation.mutate, debouncedUpdateCommand], ); if (initialLoad) { @@ -217,89 +350,7 @@ const PromptForm = () => { return null; } - const groupId = group._id; - const groupName = group.name; - const groupCategory = group.category; - - const RightPanel = () => ( -
-
- - updateGroupMutation.mutate({ - id: groupId, - payload: { name: groupName, category: value }, - }) - } - /> -
- {hasShareAccess && } - {editorMode === PromptsEditorMode.ADVANCED && ( - - )} - { - if (!selectedPrompt || !selectedPrompt._id) { - console.warn('No prompt is selected or prompt _id is missing'); - return; - } - deletePromptMutation.mutate({ - _id: selectedPrompt._id, - groupId, - }); - }} - /> -
-
- {editorMode === PromptsEditorMode.ADVANCED && - (isLoadingPrompts - ? Array.from({ length: 6 }).map((_, index: number) => ( -
- -
- )) - : prompts.length > 0 && ( - - ))} -
- ); return ( @@ -339,7 +390,17 @@ const PromptForm = () => {
- {editorMode === PromptsEditorMode.SIMPLE && } + {editorMode === PromptsEditorMode.SIMPLE && ( + + )}
)} @@ -352,11 +413,11 @@ const PromptForm = () => {
)} @@ -364,7 +425,15 @@ const PromptForm = () => { {editorMode === PromptsEditorMode.ADVANCED && (
- +
)}
@@ -395,7 +464,15 @@ const PromptForm = () => { >
- +
diff --git a/client/src/components/SidePanel/Memories/MemoryViewer.tsx b/client/src/components/SidePanel/Memories/MemoryViewer.tsx index 89459d66e..473b1e06b 100644 --- a/client/src/components/SidePanel/Memories/MemoryViewer.tsx +++ b/client/src/components/SidePanel/Memories/MemoryViewer.tsx @@ -4,18 +4,18 @@ import { Plus } from 'lucide-react'; import { matchSorter } from 'match-sorter'; import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider'; import { - Spinner, - EditIcon, - TrashIcon, Table, Input, Label, Button, Switch, + Spinner, TableRow, OGDialog, + EditIcon, TableHead, TableBody, + TrashIcon, TableCell, TableHeader, TooltipAnchor, @@ -25,10 +25,10 @@ import { } from '@librechat/client'; import type { TUserMemory } from 'librechat-data-provider'; import { - useGetUserQuery, - useMemoriesQuery, - useDeleteMemoryMutation, useUpdateMemoryPreferencesMutation, + useDeleteMemoryMutation, + useMemoriesQuery, + useGetUserQuery, } from '~/data-provider'; import { useLocalize, useAuthContext, useHasAccess } from '~/hooks'; import MemoryCreateDialog from './MemoryCreateDialog'; @@ -36,18 +36,114 @@ import MemoryEditDialog from './MemoryEditDialog'; import AdminSettings from './AdminSettings'; import { cn } from '~/utils'; +const EditMemoryButton = ({ memory }: { memory: TUserMemory }) => { + const localize = useLocalize(); + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + + return ( + } + > + + setOpen(!open)} + className="h-8 w-8 p-0" + > + + + } + /> + + + ); +}; + +const DeleteMemoryButton = ({ memory }: { memory: TUserMemory }) => { + const localize = useLocalize(); + const { showToast } = useToastContext(); + const [open, setOpen] = useState(false); + const { mutate: deleteMemory } = useDeleteMemoryMutation(); + const [deletingKey, setDeletingKey] = useState(null); + + const confirmDelete = async () => { + setDeletingKey(memory.key); + deleteMemory(memory.key, { + onSuccess: () => { + showToast({ + message: localize('com_ui_deleted'), + status: 'success', + }); + setOpen(false); + }, + onError: () => + showToast({ + message: localize('com_ui_error'), + status: 'error', + }), + onSettled: () => setDeletingKey(null), + }); + }; + + return ( + + + setOpen(!open)} + className="h-8 w-8 p-0" + > + {deletingKey === memory.key ? ( + + ) : ( + + )} + + } + /> + + + {localize('com_ui_delete_confirm')} "{memory.key}"? + + } + selection={{ + selectHandler: confirmDelete, + selectClasses: + 'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white', + selectText: localize('com_ui_delete'), + }} + /> + + ); +}; + +const pageSize = 10; export default function MemoryViewer() { const localize = useLocalize(); const { user } = useAuthContext(); const { data: userData } = useGetUserQuery(); const { data: memData, isLoading } = useMemoriesQuery(); - const { mutate: deleteMemory } = useDeleteMemoryMutation(); const { showToast } = useToastContext(); const [pageIndex, setPageIndex] = useState(0); const [searchQuery, setSearchQuery] = useState(''); - const pageSize = 10; const [createDialogOpen, setCreateDialogOpen] = useState(false); - const [deletingKey, setDeletingKey] = useState(null); const [referenceSavedMemories, setReferenceSavedMemories] = useState(true); const updateMemoryPreferencesMutation = useUpdateMemoryPreferencesMutation({ @@ -119,108 +215,6 @@ export default function MemoryViewer() { return 'stroke-green-500'; }; - const EditMemoryButton = ({ memory }: { memory: TUserMemory }) => { - const [open, setOpen] = useState(false); - const triggerRef = useRef(null); - - // Only show edit button if user has UPDATE permission - if (!hasUpdateAccess) { - return null; - } - - return ( - } - > - - setOpen(!open)} - className="h-8 w-8 p-0" - > - - - } - /> - - - ); - }; - - const DeleteMemoryButton = ({ memory }: { memory: TUserMemory }) => { - const [open, setOpen] = useState(false); - - if (!hasUpdateAccess) { - return null; - } - - const confirmDelete = async () => { - setDeletingKey(memory.key); - deleteMemory(memory.key, { - onSuccess: () => { - showToast({ - message: localize('com_ui_deleted'), - status: 'success', - }); - setOpen(false); - }, - onError: () => - showToast({ - message: localize('com_ui_error'), - status: 'error', - }), - onSettled: () => setDeletingKey(null), - }); - }; - - return ( - - - setOpen(!open)} - className="h-8 w-8 p-0" - > - {deletingKey === memory.key ? ( - - ) : ( - - )} - - } - /> - - - {localize('com_ui_delete_confirm')} "{memory.key}"? - - } - selection={{ - selectHandler: confirmDelete, - selectClasses: - 'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white', - selectText: localize('com_ui_delete'), - }} - /> - - ); - }; - if (isLoading) { return (
From ef9d9b12763568c8fbed999fa1cda29d183bf08d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:15:06 -0400 Subject: [PATCH 027/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#8676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/en/translation.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index edef833a4..964abab23 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -851,6 +851,7 @@ "com_ui_mcp_authenticated_success": "MCP server '{{0}}' authenticated successfully", "com_ui_mcp_dialog_desc": "Please enter the necessary information below.", "com_ui_mcp_enter_var": "Enter value for {{0}}", + "com_ui_mcp_init_cancelled": "MCP server '{{0}}' initialization was cancelled due to simultaneous request", "com_ui_mcp_init_failed": "Failed to initialize MCP server", "com_ui_mcp_initialize": "Initialize", "com_ui_mcp_initialized_success": "MCP server '{{0}}' initialized successfully", From c4677ab3fb547ff3689a0d8a8771dca6cfee8bb9 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Tue, 29 Jul 2025 06:08:46 -0700 Subject: [PATCH 028/224] =?UTF-8?q?=F0=9F=94=91=20refactor:=20MCP=20Settin?= =?UTF-8?q?gs=20Rendering=20Logic=20for=20OAuth=20Servers=20(#8718)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add OAuth servers to conditional rendering logic for MCPPanel in SideNav * feat: add startup flag check to conditional rendering logic * fix: correct improper handling of failure state in reinitialize endpoint * fix: change MCP config components to better handle servers without customUserVars - removes the subtle reinitialize button from config components of servers without customUserVars or OAuth - adds a placeholder message for components where servers have no customUserVars configured * style: swap CustomUserVarsSection and ServerInitializationSection positions * style: fix coloring for light mode and align more with existing design patterns * chore: remove extraneous comments * chore: reorder imports and `isEnabled` from api package --------- Co-authored-by: Danny Avila --- api/server/routes/config.js | 8 ++++- api/server/routes/mcp.js | 16 ++++++--- .../components/MCP/CustomUserVarsSection.tsx | 8 +++-- client/src/components/MCP/MCPConfigDialog.tsx | 1 + .../MCP/ServerInitializationSection.tsx | 17 +++++---- .../src/components/SidePanel/MCP/MCPPanel.tsx | 36 ++++++++++--------- client/src/hooks/MCP/useMCPServerManager.ts | 6 ++++ client/src/hooks/Nav/useSideNavLinks.ts | 5 ++- client/src/locales/en/translation.json | 2 +- packages/data-provider/src/config.ts | 2 ++ 10 files changed, 68 insertions(+), 33 deletions(-) diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 55d4cc306..bd1b0b12c 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -1,10 +1,11 @@ const express = require('express'); +const { isEnabled } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { CacheKeys, defaultSocialLogins, Constants } = require('librechat-data-provider'); const { getCustomConfig } = require('~/server/services/Config/getCustomConfig'); const { getLdapConfig } = require('~/server/services/Config/ldap'); const { getProjectByName } = require('~/models/Project'); -const { isEnabled } = require('~/server/utils'); +const { getMCPManager } = require('~/config'); const { getLogStores } = require('~/cache'); const router = express.Router(); @@ -102,11 +103,16 @@ router.get('/', async function (req, res) { payload.mcpServers = {}; const config = await getCustomConfig(); if (config?.mcpServers != null) { + const mcpManager = getMCPManager(); + const oauthServers = mcpManager.getOAuthServers(); + for (const serverName in config.mcpServers) { const serverConfig = config.mcpServers[serverName]; payload.mcpServers[serverName] = { customUserVars: serverConfig?.customUserVars || {}, chatMenu: serverConfig?.chatMenu, + isOAuth: oauthServers.has(serverName), + startup: serverConfig?.startup, }; } } diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index 35bba77ae..f66d671a8 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -452,11 +452,19 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { `[MCP Reinitialize] Sending response for ${serverName} - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`, ); + const getResponseMessage = () => { + if (oauthRequired) { + return `MCP server '${serverName}' ready for OAuth authentication`; + } + if (userConnection) { + return `MCP server '${serverName}' reinitialized successfully`; + } + return `Failed to reinitialize MCP server '${serverName}'`; + }; + res.json({ - success: true, - message: oauthRequired - ? `MCP server '${serverName}' ready for OAuth authentication` - : `MCP server '${serverName}' reinitialized successfully`, + success: userConnection && !oauthRequired, + message: getResponseMessage(), serverName, oauthRequired, oauthUrl, diff --git a/client/src/components/MCP/CustomUserVarsSection.tsx b/client/src/components/MCP/CustomUserVarsSection.tsx index 1d6060c91..c178fae24 100644 --- a/client/src/components/MCP/CustomUserVarsSection.tsx +++ b/client/src/components/MCP/CustomUserVarsSection.tsx @@ -110,13 +110,15 @@ export default function CustomUserVarsSection({ const handleRevokeClick = () => { onRevoke(); - // Reset form after revoke reset(); }; - // Don't render if no fields to configure if (!fields || Object.keys(fields).length === 0) { - return null; + return ( +
+ {localize('com_sidepanel_mcp_no_custom_vars', { '0': serverName })} +
+ ); } return ( diff --git a/client/src/components/MCP/MCPConfigDialog.tsx b/client/src/components/MCP/MCPConfigDialog.tsx index 1ad0ea7b6..5bcd590ec 100644 --- a/client/src/components/MCP/MCPConfigDialog.tsx +++ b/client/src/components/MCP/MCPConfigDialog.tsx @@ -132,6 +132,7 @@ export default function MCPConfigDialog({ 0} /> diff --git a/client/src/components/MCP/ServerInitializationSection.tsx b/client/src/components/MCP/ServerInitializationSection.tsx index 36c9ca6b1..2113a9f84 100644 --- a/client/src/components/MCP/ServerInitializationSection.tsx +++ b/client/src/components/MCP/ServerInitializationSection.tsx @@ -7,11 +7,13 @@ import { useLocalize } from '~/hooks'; interface ServerInitializationSectionProps { serverName: string; requiresOAuth: boolean; + hasCustomUserVars?: boolean; } export default function ServerInitializationSection({ serverName, requiresOAuth, + hasCustomUserVars = false, }: ServerInitializationSectionProps) { const localize = useLocalize(); @@ -39,8 +41,7 @@ export default function ServerInitializationSection({ cancelOAuthFlow(serverName); }, [cancelOAuthFlow, serverName]); - // Show subtle reinitialize option if connected - if (isConnected) { + if (isConnected && (requiresOAuth || hasCustomUserVars)) { return (
diff --git a/client/src/components/SidePanel/MCP/MCPPanel.tsx b/client/src/components/SidePanel/MCP/MCPPanel.tsx index a948f5f65..c67517278 100644 --- a/client/src/components/SidePanel/MCP/MCPPanel.tsx +++ b/client/src/components/SidePanel/MCP/MCPPanel.tsx @@ -141,29 +141,31 @@ function MCPPanelContent() { {localize('com_sidepanel_mcp_variables_for', { '0': serverBeingEdited.serverName })} - {/* Server Initialization Section */}
- { + if (selectedServerNameForEditing) { + handleConfigSave(selectedServerNameForEditing, authData); + } + }} + onRevoke={() => { + if (selectedServerNameForEditing) { + handleConfigRevoke(selectedServerNameForEditing); + } + }} + isSubmitting={updateUserPluginsMutation.isLoading} />
- {/* Custom User Variables Section */} - { - if (selectedServerNameForEditing) { - handleConfigSave(selectedServerNameForEditing, authData); - } - }} - onRevoke={() => { - if (selectedServerNameForEditing) { - handleConfigRevoke(selectedServerNameForEditing); - } - }} - isSubmitting={updateUserPluginsMutation.isLoading} + requiresOAuth={serverStatus?.requiresOAuth || false} + hasCustomUserVars={ + serverBeingEdited.config.customUserVars && + Object.keys(serverBeingEdited.config.customUserVars).length > 0 + } />
); diff --git a/client/src/hooks/MCP/useMCPServerManager.ts b/client/src/hooks/MCP/useMCPServerManager.ts index b68feade3..71ffa471b 100644 --- a/client/src/hooks/MCP/useMCPServerManager.ts +++ b/client/src/hooks/MCP/useMCPServerManager.ts @@ -234,6 +234,12 @@ export function useMCPServerManager() { cleanupServerState(serverName); } + } else { + showToast({ + message: localize('com_ui_mcp_init_failed', { 0: serverName }), + status: 'error', + }); + cleanupServerState(serverName); } } catch (error) { console.error(`[MCP Manager] Failed to initialize ${serverName}:`, error); diff --git a/client/src/hooks/Nav/useSideNavLinks.ts b/client/src/hooks/Nav/useSideNavLinks.ts index 734541521..93f96de06 100644 --- a/client/src/hooks/Nav/useSideNavLinks.ts +++ b/client/src/hooks/Nav/useSideNavLinks.ts @@ -155,7 +155,10 @@ export default function useSideNavLinks({ if ( startupConfig?.mcpServers && Object.values(startupConfig.mcpServers).some( - (server) => server.customUserVars && Object.keys(server.customUserVars).length > 0, + (server: any) => + (server.customUserVars && Object.keys(server.customUserVars).length > 0) || + server.isOAuth || + server.startup === false, ) ) { links.push({ diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 964abab23..23ea0e26c 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -506,6 +506,7 @@ "com_sidepanel_hide_panel": "Hide Panel", "com_sidepanel_manage_files": "Manage Files", "com_sidepanel_mcp_no_servers_with_vars": "No MCP servers with configurable variables.", + "com_sidepanel_mcp_no_custom_vars": "No custom user variables set for {{0}}", "com_sidepanel_mcp_variables_for": "MCP Variables for {{0}}", "com_sidepanel_parameters": "Parameters", "com_sources_image_alt": "Search result image", @@ -851,7 +852,6 @@ "com_ui_mcp_authenticated_success": "MCP server '{{0}}' authenticated successfully", "com_ui_mcp_dialog_desc": "Please enter the necessary information below.", "com_ui_mcp_enter_var": "Enter value for {{0}}", - "com_ui_mcp_init_cancelled": "MCP server '{{0}}' initialization was cancelled due to simultaneous request", "com_ui_mcp_init_failed": "Failed to initialize MCP server", "com_ui_mcp_initialize": "Initialize", "com_ui_mcp_initialized_success": "MCP server '{{0}}' initialized successfully", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 89a3aa138..4d53fba80 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -613,6 +613,8 @@ export type TStartupConfig = { } >; chatMenu?: boolean; + isOAuth?: boolean; + startup?: boolean; } >; mcpPlaceholder?: string; From 6671fcb714ef156ed7c0dc92902cc9dad89234d6 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 29 Jul 2025 15:09:52 +0200 Subject: [PATCH 029/224] =?UTF-8?q?=F0=9F=9B=82=20refactor:=20Use=20`disco?= =?UTF-8?q?verAuthorizationServerMetadata`=20for=20MCP=20OAuth=20(#8723)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use discoverAuthorizationServerMetadata instead of discoverMetadata Uses the discoverAuthorizationServerMetadata function from the upstream TS SDK. This has the advantage of falling back to OIDC discovery metadata if the OAuth discovery metadata doesn't exist which is the case with e.g. keycloak. * chore: import order --------- Co-authored-by: Danny Avila --- packages/api/src/mcp/oauth/handler.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 99ad4971a..1c776739f 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -1,10 +1,10 @@ import { randomBytes } from 'crypto'; import { logger } from '@librechat/data-schemas'; import { - discoverOAuthMetadata, registerClient, startAuthorization, exchangeAuthorization, + discoverAuthorizationServerMetadata, discoverOAuthProtectedResourceMetadata, } from '@modelcontextprotocol/sdk/client/auth.js'; import { OAuthMetadataSchema } from '@modelcontextprotocol/sdk/shared/auth.js'; @@ -61,7 +61,7 @@ export class MCPOAuthHandler { // Discover OAuth metadata logger.debug(`[MCPOAuth] Discovering OAuth metadata from ${authServerUrl}`); - const rawMetadata = await discoverOAuthMetadata(authServerUrl); + const rawMetadata = await discoverAuthorizationServerMetadata(authServerUrl); if (!rawMetadata) { logger.error(`[MCPOAuth] Failed to discover OAuth metadata from ${authServerUrl}`); @@ -466,7 +466,10 @@ export class MCPOAuthHandler { throw new Error('No token URL available for refresh'); } else { /** Auto-discover OAuth configuration for refresh */ - const { metadata: oauthMetadata } = await this.discoverMetadata(metadata.serverUrl); + const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl); + if (!oauthMetadata) { + throw new Error('Failed to discover OAuth metadata for token refresh'); + } if (!oauthMetadata.token_endpoint) { throw new Error('No token endpoint found in OAuth metadata'); } @@ -584,7 +587,7 @@ export class MCPOAuthHandler { } /** Auto-discover OAuth configuration for refresh */ - const { metadata: oauthMetadata } = await this.discoverMetadata(metadata.serverUrl); + const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl); if (!oauthMetadata.token_endpoint) { throw new Error('No token endpoint found in OAuth metadata'); From 6fd3b569accac0986afe9c18c390da081e116169 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:54:07 -0700 Subject: [PATCH 030/224] =?UTF-8?q?=E2=9A=92=EF=B8=8F=20fix:=20MCP=20Initi?= =?UTF-8?q?alization=20Flows=20(#8734)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add OAuth flow back in to success state * feat: disable server clicks during initialization to prevent spam * fix: correct new tab behavior for OAuth between one-click and normal initialization flows * fix: stop polling on error during oauth (was infinite popping toasts because we didn't clear interval) * fix: cleanupServerState should be called after successful cancelOauth, not before * fix: change from completeFlow to failFlow to avoid stale client IDs on OAuth after cancellation * fix: add logic to differentiate between cancelled and failed flows when checking status for indicators (so error triangle indicator doesn't show up on cancellaiton) --- api/server/routes/mcp.js | 4 +- api/server/services/MCP.js | 28 +++++++---- .../src/components/Chat/Input/MCPSelect.tsx | 9 +++- .../src/components/Chat/Input/MCPSubMenu.tsx | 5 ++ .../MCP/ServerInitializationSection.tsx | 2 +- client/src/hooks/MCP/useMCPServerManager.ts | 48 +++++++++++++++---- 6 files changed, 73 insertions(+), 23 deletions(-) diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index f66d671a8..270c8525a 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -303,7 +303,7 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => { } // Cancel the flow by marking it as failed - await flowManager.completeFlow(flowId, 'mcp_oauth', null, 'User cancelled OAuth flow'); + await flowManager.failFlow(flowId, 'mcp_oauth', 'User cancelled OAuth flow'); logger.info(`[MCP OAuth Cancel] Successfully cancelled OAuth flow for ${serverName}`); @@ -463,7 +463,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { }; res.json({ - success: userConnection && !oauthRequired, + success: (userConnection && !oauthRequired) || (oauthRequired && oauthUrl), message: getResponseMessage(), serverName, oauthRequired, diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index f8ec2d04d..147def1bb 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -287,14 +287,26 @@ async function checkOAuthFlowStatus(userId, serverName) { const flowTTL = flowState.ttl || 180000; // Default 3 minutes if (flowState.status === 'FAILED' || flowAge > flowTTL) { - logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, { - flowId, - status: flowState.status, - flowAge, - flowTTL, - timedOut: flowAge > flowTTL, - }); - return { hasActiveFlow: false, hasFailedFlow: true }; + const wasCancelled = flowState.error && flowState.error.includes('cancelled'); + + if (wasCancelled) { + logger.debug(`[MCP Connection Status] Found cancelled OAuth flow for ${serverName}`, { + flowId, + status: flowState.status, + error: flowState.error, + }); + return { hasActiveFlow: false, hasFailedFlow: false }; + } else { + logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, { + flowId, + status: flowState.status, + flowAge, + flowTTL, + timedOut: flowAge > flowTTL, + error: flowState.error, + }); + return { hasActiveFlow: false, hasFailedFlow: true }; + } } if (flowState.status === 'PENDING') { diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index 8160cdd2f..01c4d72a4 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -13,6 +13,7 @@ function MCPSelect() { batchToggleServers, getServerStatusIconProps, getConfigDialogProps, + isInitializing, localize, } = useMCPServerManager(); @@ -32,14 +33,18 @@ function MCPSelect() { const renderItemContent = useCallback( (serverName: string, defaultContent: React.ReactNode) => { const statusIconProps = getServerStatusIconProps(serverName); + const isServerInitializing = isInitializing(serverName); // Common wrapper for the main content (check mark + text) // Ensures Check & Text are adjacent and the group takes available space. const mainContentWrapper = ( @@ -58,7 +63,7 @@ function MCPSelect() { return mainContentWrapper; }, - [getServerStatusIconProps], + [getServerStatusIconProps, isInitializing], ); // Don't render if no servers are selected and not pinned diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx index 3628b732a..fc690089d 100644 --- a/client/src/components/Chat/Input/MCPSubMenu.tsx +++ b/client/src/components/Chat/Input/MCPSubMenu.tsx @@ -22,6 +22,7 @@ const MCPSubMenu = React.forwardRef( toggleServerSelection, getServerStatusIconProps, getConfigDialogProps, + isInitializing, } = useMCPServerManager(); const menuStore = Ariakit.useMenuStore({ @@ -86,6 +87,7 @@ const MCPSubMenu = React.forwardRef( {configuredServers.map((serverName) => { const statusIconProps = getServerStatusIconProps(serverName); const isSelected = mcpValues?.includes(serverName) ?? false; + const isServerInitializing = isInitializing(serverName); const statusIcon = statusIconProps && ; @@ -96,12 +98,15 @@ const MCPSubMenu = React.forwardRef( event.preventDefault(); toggleServerSelection(serverName); }} + disabled={isServerInitializing} className={cn( 'flex items-center gap-2 rounded-lg px-2 py-1.5 text-text-primary hover:cursor-pointer', 'scroll-m-1 outline-none transition-colors', 'hover:bg-black/[0.075] dark:hover:bg-white/10', 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', 'w-full min-w-0 justify-between text-sm', + isServerInitializing && + 'opacity-50 hover:bg-transparent dark:hover:bg-transparent', )} >
diff --git a/client/src/components/MCP/ServerInitializationSection.tsx b/client/src/components/MCP/ServerInitializationSection.tsx index 2113a9f84..0623ba1a2 100644 --- a/client/src/components/MCP/ServerInitializationSection.tsx +++ b/client/src/components/MCP/ServerInitializationSection.tsx @@ -34,7 +34,7 @@ export default function ServerInitializationSection({ const serverOAuthUrl = getOAuthUrl(serverName); const handleInitializeClick = useCallback(() => { - initializeServer(serverName); + initializeServer(serverName, false); }, [initializeServer, serverName]); const handleCancelClick = useCallback(() => { diff --git a/client/src/hooks/MCP/useMCPServerManager.ts b/client/src/hooks/MCP/useMCPServerManager.ts index 71ffa471b..b42854545 100644 --- a/client/src/hooks/MCP/useMCPServerManager.ts +++ b/client/src/hooks/MCP/useMCPServerManager.ts @@ -171,6 +171,7 @@ export function useMCPServerManager() { message: localize('com_ui_mcp_oauth_timeout', { 0: serverName }), status: 'error', }); + clearInterval(pollInterval); cleanupServerState(serverName); return; } @@ -180,10 +181,15 @@ export function useMCPServerManager() { message: localize('com_ui_mcp_init_failed'), status: 'error', }); + clearInterval(pollInterval); cleanupServerState(serverName); + return; } } catch (error) { console.error(`[MCP Manager] Error polling server ${serverName}:`, error); + clearInterval(pollInterval); + cleanupServerState(serverName); + return; } }, 3500); @@ -201,7 +207,7 @@ export function useMCPServerManager() { ); const initializeServer = useCallback( - async (serverName: string) => { + async (serverName: string, autoOpenOAuth: boolean = true) => { updateServerState(serverName, { isInitializing: true }); try { @@ -216,7 +222,9 @@ export function useMCPServerManager() { isInitializing: true, }); - window.open(response.oauthUrl, '_blank', 'noopener,noreferrer'); + if (autoOpenOAuth) { + window.open(response.oauthUrl, '_blank', 'noopener,noreferrer'); + } startServerPolling(serverName); } else { @@ -265,13 +273,25 @@ export function useMCPServerManager() { const cancelOAuthFlow = useCallback( (serverName: string) => { - queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]); - cleanupServerState(serverName); - cancelOAuthMutation.mutate(serverName); + // Call backend cancellation first, then clean up frontend state on success + cancelOAuthMutation.mutate(serverName, { + onSuccess: () => { + // Only clean up frontend state after backend confirms cancellation + cleanupServerState(serverName); + queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]); - showToast({ - message: localize('com_ui_mcp_oauth_cancelled', { 0: serverName }), - status: 'warning', + showToast({ + message: localize('com_ui_mcp_oauth_cancelled', { 0: serverName }), + status: 'warning', + }); + }, + onError: (error) => { + console.error(`[MCP Manager] Failed to cancel OAuth for ${serverName}:`, error); + showToast({ + message: localize('com_ui_mcp_init_failed', { 0: serverName }), + status: 'error', + }); + }, }); }, [queryClient, cleanupServerState, showToast, localize, cancelOAuthMutation], @@ -309,6 +329,10 @@ export function useMCPServerManager() { const disconnectedServers: string[] = []; serverNames.forEach((serverName) => { + if (isInitializing(serverName)) { + return; + } + const serverStatus = connectionStatus[serverName]; if (serverStatus?.connectionState === 'connected') { connectedServers.push(serverName); @@ -323,11 +347,15 @@ export function useMCPServerManager() { initializeServer(serverName); }); }, - [connectionStatus, setMCPValues, initializeServer], + [connectionStatus, setMCPValues, initializeServer, isInitializing], ); const toggleServerSelection = useCallback( (serverName: string) => { + if (isInitializing(serverName)) { + return; + } + const currentValues = mcpValues ?? []; const isCurrentlySelected = currentValues.includes(serverName); @@ -343,7 +371,7 @@ export function useMCPServerManager() { } } }, - [mcpValues, setMCPValues, connectionStatus, initializeServer], + [mcpValues, setMCPValues, connectionStatus, initializeServer, isInitializing], ); const handleConfigSave = useCallback( From 32081245dad803561dd61dbe607d98fc276d9032 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 29 Jul 2025 14:59:58 -0400 Subject: [PATCH 031/224] =?UTF-8?q?=F0=9F=AA=B5=20chore:=20Remove=20Unnece?= =?UTF-8?q?ssary=20Comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server/routes/mcp.js | 29 +------------------ .../src/components/Chat/Input/MCPSelect.tsx | 8 ++--- client/src/hooks/MCP/useMCPServerManager.ts | 8 ++--- 3 files changed, 8 insertions(+), 37 deletions(-) diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index 270c8525a..4e35052cd 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -93,7 +93,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => { return res.redirect('/oauth/error?error=missing_state'); } - // Extract flow ID from state const flowId = state; logger.debug('[MCP OAuth] Using flow ID from state', { flowId }); @@ -116,22 +115,17 @@ router.get('/:serverName/oauth/callback', async (req, res) => { hasCodeVerifier: !!flowState.codeVerifier, }); - // Complete the OAuth flow logger.debug('[MCP OAuth] Completing OAuth flow'); const tokens = await MCPOAuthHandler.completeOAuthFlow(flowId, code, flowManager); logger.info('[MCP OAuth] OAuth flow completed, tokens received in callback route'); - // Try to establish the MCP connection with the new tokens try { const mcpManager = getMCPManager(flowState.userId); logger.debug(`[MCP OAuth] Attempting to reconnect ${serverName} with new OAuth tokens`); - // For user-level OAuth, try to establish the connection if (flowState.userId !== 'system') { - // We need to get the user object - in this case we'll need to reconstruct it const user = { id: flowState.userId }; - // Try to establish connection with the new tokens const userConnection = await mcpManager.getUserConnection({ user, serverName, @@ -148,10 +142,8 @@ router.get('/:serverName/oauth/callback', async (req, res) => { `[MCP OAuth] Successfully reconnected ${serverName} for user ${flowState.userId}`, ); - // Fetch and cache tools now that we have a successful connection const userTools = (await getCachedTools({ userId: flowState.userId })) || {}; - // Remove any old tools from this server in the user's cache const mcpDelimiter = Constants.mcp_delimiter; for (const key of Object.keys(userTools)) { if (key.endsWith(`${mcpDelimiter}${serverName}`)) { @@ -159,7 +151,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => { } } - // Add the new tools from this server const tools = await userConnection.fetchTools(); for (const tool of tools) { const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`; @@ -173,7 +164,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => { }; } - // Save the updated user tool cache await setCachedTools(userTools, { userId: flowState.userId }); logger.debug( @@ -183,7 +173,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => { logger.debug(`[MCP OAuth] System-level OAuth completed for ${serverName}`); } } catch (error) { - // Don't fail the OAuth callback if reconnection fails - the tokens are still saved logger.warn( `[MCP OAuth] Failed to reconnect ${serverName} after OAuth, but tokens are saved:`, error, @@ -219,7 +208,6 @@ router.get('/oauth/tokens/:flowId', requireJwtAuth, async (req, res) => { return res.status(401).json({ error: 'User not authenticated' }); } - // Allow system flows or user-owned flows if (!flowId.startsWith(`${user.id}:`) && !flowId.startsWith('system:')) { return res.status(403).json({ error: 'Access denied' }); } @@ -287,11 +275,7 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => { const flowsCache = getLogStores(CacheKeys.FLOWS); const flowManager = getFlowStateManager(flowsCache); - - // Generate the flow ID for this user/server combination const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName); - - // Check if flow exists const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth'); if (!flowState) { @@ -302,7 +286,6 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => { }); } - // Cancel the flow by marking it as failed await flowManager.failFlow(flowId, 'mcp_oauth', 'User cancelled OAuth flow'); logger.info(`[MCP OAuth Cancel] Successfully cancelled OAuth flow for ${serverName}`); @@ -379,8 +362,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { createToken, deleteTokens, }, - returnOnOAuth: true, // Return immediately when OAuth is initiated - // Add OAuth handlers to capture the OAuth URL when needed + returnOnOAuth: true, oauthStart: async (authURL) => { logger.info(`[MCP Reinitialize] OAuth URL received: ${authURL}`); oauthUrl = authURL; @@ -395,7 +377,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { `[MCP Reinitialize] OAuth state - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`, ); - // Check if this is an OAuth error - if so, the flow state should be set up now const isOAuthError = err.message?.includes('OAuth') || err.message?.includes('authentication') || @@ -408,7 +389,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { `[MCP Reinitialize] OAuth required for ${serverName} (isOAuthError: ${isOAuthError}, oauthRequired: ${oauthRequired}, isOAuthFlowInitiated: ${isOAuthFlowInitiated})`, ); oauthRequired = true; - // Don't return error - continue so frontend can handle OAuth } else { logger.error( `[MCP Reinitialize] Error initializing MCP server ${serverName} for user:`, @@ -418,11 +398,9 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { } } - // Only fetch and cache tools if we successfully connected (no OAuth required) if (userConnection && !oauthRequired) { const userTools = (await getCachedTools({ userId: user.id })) || {}; - // Remove any old tools from this server in the user's cache const mcpDelimiter = Constants.mcp_delimiter; for (const key of Object.keys(userTools)) { if (key.endsWith(`${mcpDelimiter}${serverName}`)) { @@ -430,7 +408,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { } } - // Add the new tools from this server const tools = await userConnection.fetchTools(); for (const tool of tools) { const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`; @@ -444,7 +421,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { }; } - // Save the updated user tool cache await setCachedTools(userTools, { userId: user.id }); } @@ -593,19 +569,16 @@ router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => { const pluginKey = `${Constants.mcp_prefix}${serverName}`; const authValueFlags = {}; - // Check existence of saved values for each custom user variable (don't fetch actual values) if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') { for (const varName of Object.keys(serverConfig.customUserVars)) { try { const value = await getUserPluginAuthValue(user.id, varName, false, pluginKey); - // Only store boolean flag indicating if value exists authValueFlags[varName] = !!(value && value.length > 0); } catch (err) { logger.error( `[MCP Auth Value Flags] Error checking ${varName} for user ${user.id}:`, err, ); - // Default to false if we can't check authValueFlags[varName] = false; } } diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index 01c4d72a4..e3d1878ff 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -35,8 +35,10 @@ function MCPSelect() { const statusIconProps = getServerStatusIconProps(serverName); const isServerInitializing = isInitializing(serverName); - // Common wrapper for the main content (check mark + text) - // Ensures Check & Text are adjacent and the group takes available space. + /** + Common wrapper for the main content (check mark + text). + Ensures Check & Text are adjacent and the group takes available space. + */ const mainContentWrapper = ( -
diff --git a/client/src/components/MCP/MCPConfigDialog.tsx b/client/src/components/MCP/MCPConfigDialog.tsx index 5bcd590ec..7c4c86fce 100644 --- a/client/src/components/MCP/MCPConfigDialog.tsx +++ b/client/src/components/MCP/MCPConfigDialog.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { Loader2, KeyRound, PlugZap, AlertTriangle } from 'lucide-react'; +import { KeyRound, PlugZap, AlertTriangle } from 'lucide-react'; import { + Spinner, OGDialog, OGDialogTitle, OGDialogHeader, OGDialogContent, - OGDialogDescription, } from '@librechat/client'; import type { MCPServerStatus } from 'librechat-data-provider'; import ServerInitializationSection from './ServerInitializationSection'; @@ -45,9 +45,6 @@ export default function MCPConfigDialog({ const dialogTitle = hasFields ? localize('com_ui_configure_mcp_variables_for', { 0: serverName }) : `${serverName} MCP Server`; - const dialogDescription = hasFields - ? localize('com_ui_mcp_dialog_desc') - : `Manage connection and settings for the ${serverName} MCP server.`; // Helper function to render status badge based on connection state const renderStatusBadge = () => { @@ -60,7 +57,7 @@ export default function MCPConfigDialog({ if (connectionState === 'connecting') { return (
- + {localize('com_ui_connecting')}
); @@ -107,26 +104,24 @@ export default function MCPConfigDialog({ return ( - +
- {dialogTitle} + + {dialogTitle.charAt(0).toUpperCase() + dialogTitle.slice(1)} + {renderStatusBadge()}
- {dialogDescription}
- {/* Content */} -
- {/* Custom User Variables Section */} - {})} - isSubmitting={isSubmitting} - /> -
+ {/* Custom User Variables Section */} + {})} + isSubmitting={isSubmitting} + /> {/* Server Initialization Section */} -
- +
+
@@ -110,8 +111,8 @@ function InitializingStatusIcon({ serverName, onCancel, canCancel }: Initializin return (
-
@@ -121,8 +122,8 @@ function InitializingStatusIcon({ serverName, onCancel, canCancel }: Initializin function ConnectingStatusIcon({ serverName }: StatusIconProps) { return (
-
diff --git a/client/src/components/MCP/ServerInitializationSection.tsx b/client/src/components/MCP/ServerInitializationSection.tsx index 0623ba1a2..5d793921e 100644 --- a/client/src/components/MCP/ServerInitializationSection.tsx +++ b/client/src/components/MCP/ServerInitializationSection.tsx @@ -1,23 +1,24 @@ -import React, { useCallback } from 'react'; -import { Button } from '@librechat/client'; -import { RefreshCw, Link } from 'lucide-react'; +import React from 'react'; +import { RefreshCw } from 'lucide-react'; +import { Button, Spinner } from '@librechat/client'; import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager'; import { useLocalize } from '~/hooks'; interface ServerInitializationSectionProps { + sidePanel?: boolean; serverName: string; requiresOAuth: boolean; hasCustomUserVars?: boolean; } export default function ServerInitializationSection({ + sidePanel = false, serverName, requiresOAuth, hasCustomUserVars = false, }: ServerInitializationSectionProps) { const localize = useLocalize(); - // Use the centralized server manager instead of the old initialization hook so we can handle multiple oauth flows at once const { initializeServer, connectionStatus, @@ -33,99 +34,66 @@ export default function ServerInitializationSection({ const isServerInitializing = isInitializing(serverName); const serverOAuthUrl = getOAuthUrl(serverName); - const handleInitializeClick = useCallback(() => { - initializeServer(serverName, false); - }, [initializeServer, serverName]); + const shouldShowReinit = isConnected && (requiresOAuth || hasCustomUserVars); + const shouldShowInit = !isConnected && !serverOAuthUrl; - const handleCancelClick = useCallback(() => { - cancelOAuthFlow(serverName); - }, [cancelOAuthFlow, serverName]); - - if (isConnected && (requiresOAuth || hasCustomUserVars)) { - return ( -
- -
- ); - } - - if (isConnected) { + if (!shouldShowReinit && !shouldShowInit && !serverOAuthUrl) { return null; } - return ( -
-
+ if (serverOAuthUrl) { + return ( + <>
- - {requiresOAuth - ? localize('com_ui_mcp_not_authenticated', { 0: serverName }) - : localize('com_ui_mcp_not_initialized', { 0: serverName })} - -
- {/* Only show authenticate button when OAuth URL is not present */} - {!serverOAuthUrl && ( + - )} -
- - {/* OAuth URL display */} - {serverOAuthUrl && ( -
-
-
- -
- - {localize('com_ui_auth_url')} - -
-
- - -
-

- {localize('com_ui_oauth_flow_desc')} -

- )} + + ); + } + + // Unified button rendering + const isReinit = shouldShowReinit; + const outerClass = isReinit ? 'flex justify-start' : 'flex justify-end'; + const buttonVariant = isReinit ? undefined : 'default'; + const buttonText = isServerInitializing + ? localize('com_ui_loading') + : isReinit + ? localize('com_ui_reinitialize') + : requiresOAuth + ? localize('com_ui_authenticate') + : localize('com_ui_mcp_initialize'); + const icon = isServerInitializing ? ( + + ) : ( + + ); + + return ( +
+
); } diff --git a/client/src/components/SidePanel/MCP/MCPPanel.tsx b/client/src/components/SidePanel/MCP/MCPPanel.tsx index c67517278..b90efa197 100644 --- a/client/src/components/SidePanel/MCP/MCPPanel.tsx +++ b/client/src/components/SidePanel/MCP/MCPPanel.tsx @@ -6,8 +6,8 @@ import { Constants, QueryKeys } from 'librechat-data-provider'; import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query'; import type { TUpdateUserPlugins } from 'librechat-data-provider'; import ServerInitializationSection from '~/components/MCP/ServerInitializationSection'; -import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection'; import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries'; +import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection'; import BadgeRowProvider from '~/Providers/BadgeRowContext'; import { useGetStartupConfig } from '~/data-provider'; import MCPPanelSkeleton from './MCPPanelSkeleton'; @@ -127,20 +127,12 @@ function MCPPanelContent() { const serverStatus = connectionStatus[selectedServerNameForEditing]; return ( -
- -

- {localize('com_sidepanel_mcp_variables_for', { '0': serverBeingEdited.serverName })} -

-
+
{mcpServerDefinitions.map((server) => { const serverStatus = connectionStatus[server.serverName]; @@ -189,7 +182,7 @@ function MCPPanelContent() { {server.serverName} {serverStatus && ( (({ className, ...props }, ref) => ( )); From 25c993d93edb204f03d8493b7e71bfdd11672a24 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 30 Jul 2025 15:18:17 -0400 Subject: [PATCH 036/224] =?UTF-8?q?=F0=9F=93=A6=20chore:=20bump=20@librech?= =?UTF-8?q?at/agents=20to=20v2.4.69=20(#8769)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/package.json | 2 +- package-lock.json | 30 +++++++++++++++--------------- packages/api/package.json | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/api/package.json b/api/package.json index 229f9527c..990f37815 100644 --- a/api/package.json +++ b/api/package.json @@ -49,7 +49,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.68", + "@librechat/agents": "^2.4.69", "@librechat/api": "*", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.0", diff --git a/package-lock.json b/package-lock.json index 976742199..ee490258c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.68", + "@librechat/agents": "^2.4.69", "@librechat/api": "*", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.0", @@ -21276,13 +21276,13 @@ } }, "node_modules/@langchain/langgraph": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.3.11.tgz", - "integrity": "sha512-Lh8oga4ismQyw1NGZKoHPdeGke1g5HMF7V0nBlc5R7GnV8tfC6pdsXjiEH6sYsHsRDInfy8uQeob/BwEmMSSbQ==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.3.12.tgz", + "integrity": "sha512-4jKvfmxxgQyKnCvXdFbcKt6MdfaJoQ2WWqBR16o2E6D2RxqHvnLMMClZh4FSd6WYw39z5LGWvzRapFbRMqxu1A==", "license": "MIT", "dependencies": { "@langchain/langgraph-checkpoint": "~0.0.18", - "@langchain/langgraph-sdk": "~0.0.100", + "@langchain/langgraph-sdk": "~0.0.102", "uuid": "^10.0.0", "zod": "^3.25.32" }, @@ -21328,9 +21328,9 @@ } }, "node_modules/@langchain/langgraph-sdk": { - "version": "0.0.100", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.100.tgz", - "integrity": "sha512-mQuj0KgjD31Me+/W658OtdlOACOjgipWp/hF80OY4w4LqWCNIQWJBWMZ3f1/E8jpog/XBCROR37auFc7Fj+4Dw==", + "version": "0.0.104", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.104.tgz", + "integrity": "sha512-wUO6GMy65Y7DsWtjTJ3dA59enrZy2wN4o48AMYN7dF7u/PMXXYyBjBCKSzgVWqO6uWH2yNpyGDrcMwKuk5kQLA==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.15", @@ -21573,9 +21573,9 @@ } }, "node_modules/@librechat/agents": { - "version": "2.4.68", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.68.tgz", - "integrity": "sha512-05UhnUJJ6/I8KVkhJ9NrQcm3UKhA/cXG8yT2VU+QQRJoDf7qnt47DRBP87ZEWRGMLh2civq1OWQPW2BHf2eL4A==", + "version": "2.4.69", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.69.tgz", + "integrity": "sha512-Yt0rttqOaZQeZPIB68I8RdnU6SHeh0OJV5yEg8mx9EHTA7SnV/lOlDhn424aXdpMvYZYuxAt/Fev3jTC7qKiTg==", "license": "MIT", "dependencies": { "@langchain/anthropic": "^0.3.24", @@ -22148,9 +22148,9 @@ } }, "node_modules/@librechat/agents/node_modules/openai": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-5.10.1.tgz", - "integrity": "sha512-fq6xVfv1/gpLbsj8fArEt3b6B9jBxdhAK+VJ+bDvbUvNd+KTLlA3bnDeYZaBsGH9LUhJ1M1yXfp9sEyBLMx6eA==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.11.0.tgz", + "integrity": "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg==", "license": "Apache-2.0", "bin": { "openai": "bin/cli" @@ -51414,7 +51414,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.68", + "@librechat/agents": "^2.4.69", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.0", "axios": "^1.8.2", diff --git a/packages/api/package.json b/packages/api/package.json index 62f16900d..12bbd1824 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -70,7 +70,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.68", + "@librechat/agents": "^2.4.69", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.0", "axios": "^1.8.2", From 03a924eacad942ca4c419ff6df0d19474316bb24 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:23:12 -0400 Subject: [PATCH 037/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#8739)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌍 i18n: Update translation.json with latest translations * chore: Remove unused translation keys for MCP custom variables --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Danny Avila --- client/src/locales/ar/translation.json | 1 - client/src/locales/ca/translation.json | 1 - client/src/locales/cs/translation.json | 1 - client/src/locales/da/translation.json | 1 - client/src/locales/de/translation.json | 1 - client/src/locales/es/translation.json | 1 - client/src/locales/et/translation.json | 1 - client/src/locales/fa/translation.json | 1 - client/src/locales/fi/translation.json | 1 - client/src/locales/fr/translation.json | 1 - client/src/locales/he/translation.json | 1 - client/src/locales/hu/translation.json | 1 - client/src/locales/id/translation.json | 1 - client/src/locales/it/translation.json | 1 - client/src/locales/ja/translation.json | 1 - client/src/locales/ko/translation.json | 1 - client/src/locales/lv/translation.json | 2 -- client/src/locales/nl/translation.json | 3 +-- client/src/locales/pl/translation.json | 1 - client/src/locales/pt-BR/translation.json | 1 - client/src/locales/pt-PT/translation.json | 1 - client/src/locales/ru/translation.json | 1 - client/src/locales/sv/translation.json | 3 +-- client/src/locales/th/translation.json | 1 - client/src/locales/tr/translation.json | 1 - client/src/locales/vi/translation.json | 1 - client/src/locales/zh-Hans/translation.json | 2 -- client/src/locales/zh-Hant/translation.json | 1 - 28 files changed, 2 insertions(+), 32 deletions(-) diff --git a/client/src/locales/ar/translation.json b/client/src/locales/ar/translation.json index d75c1a231..49fde151c 100644 --- a/client/src/locales/ar/translation.json +++ b/client/src/locales/ar/translation.json @@ -722,7 +722,6 @@ "com_ui_upload_success": "تم تحميل الملف بنجاح", "com_ui_upload_type": "اختر نوع التحميل", "com_ui_use_micrphone": "استخدام الميكروفون", - "com_ui_use_prompt": "استخدم الأمر", "com_ui_variables": "متغيرات", "com_ui_variables_info": "استخدم أقواس مزدوجة في نصك لإنشاء متغيرات، مثل `{{متغير كمثال}}`، لملئها لاحقاً عند استخدام النص البرمجي.", "com_ui_version_var": "الإصدار {{0}}", diff --git a/client/src/locales/ca/translation.json b/client/src/locales/ca/translation.json index 10f652e6b..627760ed2 100644 --- a/client/src/locales/ca/translation.json +++ b/client/src/locales/ca/translation.json @@ -852,7 +852,6 @@ "com_ui_use_2fa_code": "Utilitza codi 2FA", "com_ui_use_backup_code": "Utilitza codi de recuperació", "com_ui_use_micrphone": "Utilitza el micròfon", - "com_ui_use_prompt": "Utilitza prompt", "com_ui_used": "Utilitzat", "com_ui_variables": "Variables", "com_ui_variables_info": "Utilitza claus dobles per crear variables, per ex. `{{exemple variable}}`, per omplir-les quan utilitzis el prompt.", diff --git a/client/src/locales/cs/translation.json b/client/src/locales/cs/translation.json index 7d0dffeef..716a1aaac 100644 --- a/client/src/locales/cs/translation.json +++ b/client/src/locales/cs/translation.json @@ -721,7 +721,6 @@ "com_ui_use_2fa_code": "Použít kód 2FA", "com_ui_use_backup_code": "Použít záložní kód", "com_ui_use_micrphone": "Použít mikrofon", - "com_ui_use_prompt": "Použít výzvu", "com_ui_used": "Použito", "com_ui_variables": "Proměnné", "com_ui_variables_info": "Použijte dvojité složené závorky k vytvoření proměnných, např. `{{příklad proměnné}}`, které lze vyplnit při použití výzvy.", diff --git a/client/src/locales/da/translation.json b/client/src/locales/da/translation.json index 708433398..e75622d55 100644 --- a/client/src/locales/da/translation.json +++ b/client/src/locales/da/translation.json @@ -908,7 +908,6 @@ "com_ui_use_2fa_code": "Brug 2FA-kode i stedet", "com_ui_use_backup_code": "Brug backup-koden i stedet", "com_ui_use_micrphone": "Brug mikrofon", - "com_ui_use_prompt": "Brug prompt", "com_ui_used": "Brugt", "com_ui_variables": "Variabler", "com_ui_variables_info": "Brug dobbelte parenteser i din tekst til at oprette variabler, f.eks.{{example variable}}`, som senere skal udfyldes ved brug af prompten.", diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index 4ed9dd4af..e8e6b266d 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -1048,7 +1048,6 @@ "com_ui_use_backup_code": "Stattdessen Backup-Code verwenden", "com_ui_use_memory": "Erinnerung nutzen", "com_ui_use_micrphone": "Mikrofon verwenden", - "com_ui_use_prompt": "Prompt verwenden", "com_ui_used": "Verwendet", "com_ui_value": "Wert", "com_ui_variables": "Variablen", diff --git a/client/src/locales/es/translation.json b/client/src/locales/es/translation.json index c9af2c50d..14dc81d06 100644 --- a/client/src/locales/es/translation.json +++ b/client/src/locales/es/translation.json @@ -743,7 +743,6 @@ "com_ui_upload_success": "Archivo subido con éxito", "com_ui_upload_type": "Seleccionar tipo de carga", "com_ui_use_micrphone": "Usar micrófono", - "com_ui_use_prompt": "Usar prompt", "com_ui_variables": "Variables", "com_ui_variables_info": "Utilice llaves dobles en su texto para crear variables, por ejemplo `{{variable de ejemplo}}`, para completarlas posteriormente al usar el prompt.", "com_ui_verify": "Verificar", diff --git a/client/src/locales/et/translation.json b/client/src/locales/et/translation.json index 95d222f82..72f82cb75 100644 --- a/client/src/locales/et/translation.json +++ b/client/src/locales/et/translation.json @@ -930,7 +930,6 @@ "com_ui_use_2fa_code": "Kasuta hoopis 2FA koodi", "com_ui_use_backup_code": "Kasuta hoopis varukoodi", "com_ui_use_micrphone": "Kasuta mikrofoni", - "com_ui_use_prompt": "Kasuta sisendit", "com_ui_used": "Kasutatud", "com_ui_variables": "Muutujad", "com_ui_variables_info": "Kasuta oma tekstis topelt sulgusid, et luua muutujaid, nt `{{näidismuutuja}}`, et hiljem sisendi kasutamisel täita.", diff --git a/client/src/locales/fa/translation.json b/client/src/locales/fa/translation.json index 7b6102bab..1ed06aaf2 100644 --- a/client/src/locales/fa/translation.json +++ b/client/src/locales/fa/translation.json @@ -832,7 +832,6 @@ "com_ui_use_2fa_code": "به جای آن از کد 2FA استفاده کنید", "com_ui_use_backup_code": "به جای آن از کد پشتیبان استفاده کنید", "com_ui_use_micrphone": "از میکروفون استفاده کنید", - "com_ui_use_prompt": "از prompt استفاده کنید", "com_ui_used": "استفاده می شود", "com_ui_variables": "متغیرها", "com_ui_variables_info": "از پرانتزهای دوتایی در متن خود برای ایجاد متغیرها استفاده کنید، به عنوان مثال. `{{example variable}}`، تا بعداً هنگام استفاده از درخواست پر شود.", diff --git a/client/src/locales/fi/translation.json b/client/src/locales/fi/translation.json index cb57d635d..e401562ef 100644 --- a/client/src/locales/fi/translation.json +++ b/client/src/locales/fi/translation.json @@ -596,7 +596,6 @@ "com_ui_upload_invalid_var": "Virheellinen ladattava tiedosto. Tiedoston täytyy olla enintään {{0}} MB kokoinen kuvatiedosto", "com_ui_upload_success": "Tiedoston lataus onnistui", "com_ui_use_micrphone": "Käytä mikrofonia", - "com_ui_use_prompt": "Käytä syötettä", "com_ui_variables": "Muuttujat", "com_ui_variables_info": "Käytä kaksoisaaltosulkeita tekstissäsi muuttujien luomiseen, esim. {{esimerkkimuuttuja}}. Muuttujia voi täyttää myöhemmin syötettä käyttäessä.", "com_ui_version_var": "Versio {{0}}", diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index 29ed6f1d1..e065af573 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -1040,7 +1040,6 @@ "com_ui_use_backup_code": "Utiliser un code de sauvegarde à la place", "com_ui_use_memory": "Utiliser le Souvenir", "com_ui_use_micrphone": "Utiliser le microphone", - "com_ui_use_prompt": "Utiliser le prompt", "com_ui_used": "Déjà utilisé", "com_ui_value": "Valeur", "com_ui_variables": "Variables", diff --git a/client/src/locales/he/translation.json b/client/src/locales/he/translation.json index eedf6e9f7..cc782d68e 100644 --- a/client/src/locales/he/translation.json +++ b/client/src/locales/he/translation.json @@ -1039,7 +1039,6 @@ "com_ui_use_backup_code": "השתמש בקוד גיבוי במקום", "com_ui_use_memory": "השתמש בזיכרון", "com_ui_use_micrphone": "שימוש במיקורפון", - "com_ui_use_prompt": "השתמש בהנחיה (פרומפט)", "com_ui_used": "נוצל", "com_ui_value": "ערך", "com_ui_variables": "משתנים", diff --git a/client/src/locales/hu/translation.json b/client/src/locales/hu/translation.json index 71c60533f..4a7cbab5e 100644 --- a/client/src/locales/hu/translation.json +++ b/client/src/locales/hu/translation.json @@ -832,7 +832,6 @@ "com_ui_use_2fa_code": "2FA kód használata helyette", "com_ui_use_backup_code": "Biztonsági mentési kód használata helyette", "com_ui_use_micrphone": "Mikrofon használata", - "com_ui_use_prompt": "Prompt használata", "com_ui_used": "Használt", "com_ui_variables": "Változók", "com_ui_variables_info": "Használjon dupla kapcsos zárójeleket a szövegben változók létrehozásához, pl. `{{példa változó}}`, amelyeket később a prompt használatakor kitölthet.", diff --git a/client/src/locales/id/translation.json b/client/src/locales/id/translation.json index f780f80ca..05e94c28c 100644 --- a/client/src/locales/id/translation.json +++ b/client/src/locales/id/translation.json @@ -287,6 +287,5 @@ "com_ui_upload": "Unggah", "com_ui_upload_error": "Ada kesalahan saat mengunggah file Anda", "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 a8a477ec9..4d7f96da1 100644 --- a/client/src/locales/it/translation.json +++ b/client/src/locales/it/translation.json @@ -815,7 +815,6 @@ "com_ui_use_2fa_code": "Usa invece il codice 2FA", "com_ui_use_backup_code": "Usa invece il codice di backup", "com_ui_use_micrphone": "Usa microfono", - "com_ui_use_prompt": "Usa prompt", "com_ui_used": "Usato", "com_ui_variables": "Variabili", "com_ui_variables_info": "Usa le doppie parentesi graffe nel testo per creare variabili, ad esempio `{{variabile esempio}}`, da compilare successivamente quando utilizzi il prompt.", diff --git a/client/src/locales/ja/translation.json b/client/src/locales/ja/translation.json index 649b1f2f6..b7eade0f8 100644 --- a/client/src/locales/ja/translation.json +++ b/client/src/locales/ja/translation.json @@ -1047,7 +1047,6 @@ "com_ui_use_backup_code": "代わりにバックアップコードを使用する", "com_ui_use_memory": "メモリを使用する", "com_ui_use_micrphone": "マイクを使用する", - "com_ui_use_prompt": "プロンプトの利用", "com_ui_used": "使用済み", "com_ui_value": "値", "com_ui_variables": "変数", diff --git a/client/src/locales/ko/translation.json b/client/src/locales/ko/translation.json index 43cb599f4..ce5e794b3 100644 --- a/client/src/locales/ko/translation.json +++ b/client/src/locales/ko/translation.json @@ -959,7 +959,6 @@ "com_ui_use_backup_code": "백업 코드 사용", "com_ui_use_memory": "메모리 사용", "com_ui_use_micrphone": "마이크 사용", - "com_ui_use_prompt": "프롬프트 사용", "com_ui_used": "사용됨", "com_ui_value": "값", "com_ui_variables": "변수", diff --git a/client/src/locales/lv/translation.json b/client/src/locales/lv/translation.json index 4144546e5..6fa3f9dfc 100644 --- a/client/src/locales/lv/translation.json +++ b/client/src/locales/lv/translation.json @@ -859,7 +859,6 @@ "com_ui_mcp_not_authenticated": "{{0}} nav autentificēts (nepieciešams OAuth).", "com_ui_mcp_not_initialized": "{{0}} nav inicializēts", "com_ui_mcp_oauth_cancelled": "OAuth pieteikšanās atcelta {{0}}", - "com_ui_mcp_oauth_no_url": "Nepieciešama OAuth autentifikācija, bet URL nav padots", "com_ui_mcp_oauth_timeout": "OAuth pieteikšanās beidzās priekš {{0}}", "com_ui_mcp_server_not_found": "Serveris nav atrasts.", "com_ui_mcp_servers": "MCP serveri", @@ -1073,7 +1072,6 @@ "com_ui_use_backup_code": "Izmantojiet rezerves kodu", "com_ui_use_memory": "Izmantot atmiņu", "com_ui_use_micrphone": "Izmantot mikrofonu", - "com_ui_use_prompt": "Izmantojiet uzvedni", "com_ui_used": "Lietots", "com_ui_value": "Vērtība", "com_ui_variables": "Mainīgie", diff --git a/client/src/locales/nl/translation.json b/client/src/locales/nl/translation.json index 11f411ca4..d357709d1 100644 --- a/client/src/locales/nl/translation.json +++ b/client/src/locales/nl/translation.json @@ -351,6 +351,5 @@ "com_ui_terms_and_conditions": "Gebruiksvoorwaarden", "com_ui_unarchive": "Uit archiveren", "com_ui_unarchive_error": "Kan conversatie niet uit archiveren", - "com_ui_upload_success": "Bestand succesvol geüpload", - "com_ui_use_prompt": "Gebruik prompt" + "com_ui_upload_success": "Bestand succesvol geüpload" } \ No newline at end of file diff --git a/client/src/locales/pl/translation.json b/client/src/locales/pl/translation.json index 3dd44a04b..e4d9b7487 100644 --- a/client/src/locales/pl/translation.json +++ b/client/src/locales/pl/translation.json @@ -708,7 +708,6 @@ "com_ui_upload_success": "Pomyślnie przesłano plik", "com_ui_upload_type": "Wybierz typ przesyłania", "com_ui_use_micrphone": "Użyj mikrofonu", - "com_ui_use_prompt": "Użyj podpowiedzi", "com_ui_variables": "Zmienne", "com_ui_variables_info": "Użyj podwójnych nawiasów klamrowych w tekście, aby utworzyć zmienne, np. `{{przykładowa zmienna}}`, które później można wypełnić podczas używania promptu.", "com_ui_version_var": "Wersja {{0}}", diff --git a/client/src/locales/pt-BR/translation.json b/client/src/locales/pt-BR/translation.json index e8276ae54..647cd87cd 100644 --- a/client/src/locales/pt-BR/translation.json +++ b/client/src/locales/pt-BR/translation.json @@ -817,7 +817,6 @@ "com_ui_use_2fa_code": "Use o código 2FA em vez disso", "com_ui_use_backup_code": "Use o código de backup", "com_ui_use_micrphone": "Usar microfone", - "com_ui_use_prompt": "Usar prompt", "com_ui_used": "Usado", "com_ui_variables": "Variáveis", "com_ui_variables_info": "Use chaves duplas no seu texto para criar variáveis, por exemplo, `{{exemplo de variável}}`, para preencher posteriormente ao usar o prompt.", diff --git a/client/src/locales/pt-PT/translation.json b/client/src/locales/pt-PT/translation.json index 08f0b9577..a8f02bbc8 100644 --- a/client/src/locales/pt-PT/translation.json +++ b/client/src/locales/pt-PT/translation.json @@ -856,7 +856,6 @@ "com_ui_use_2fa_code": "Usar Código 2FA", "com_ui_use_backup_code": "Usar Código da cópia de segurança", "com_ui_use_micrphone": "Usar microfone", - "com_ui_use_prompt": "Usar prompt", "com_ui_used": "Usado", "com_ui_variables": "Variáveis", "com_ui_variables_info": "Use chaves duplas no seu texto para criar variáveis, por exemplo, `{{exemplo de variável}}`, para preencher posteriormente ao usar o prompt.", diff --git a/client/src/locales/ru/translation.json b/client/src/locales/ru/translation.json index cd774c542..4a82cc162 100644 --- a/client/src/locales/ru/translation.json +++ b/client/src/locales/ru/translation.json @@ -849,7 +849,6 @@ "com_ui_use_2fa_code": "Использовать код 2FA вместо этого", "com_ui_use_backup_code": "Использовать резервный код вместо этого", "com_ui_use_micrphone": "Использовать микрофон", - "com_ui_use_prompt": "Использовать промпт", "com_ui_used": "Использован", "com_ui_variables": "Переменные", "com_ui_variables_info": "Используйте двойные фигурные скобки в тексте для создания переменных, например `{{пример переменной}}`, чтобы заполнить их позже при использовании промта.", diff --git a/client/src/locales/sv/translation.json b/client/src/locales/sv/translation.json index 71d92626a..8bf9ebddd 100644 --- a/client/src/locales/sv/translation.json +++ b/client/src/locales/sv/translation.json @@ -420,6 +420,5 @@ "com_ui_terms_and_conditions": "Villkor för användning", "com_ui_unarchive": "Avarkivera", "com_ui_unarchive_error": "Kunde inte avarkivera chatt", - "com_ui_upload_success": "Uppladdningen av filen lyckades", - "com_ui_use_prompt": "Använd prompt" + "com_ui_upload_success": "Uppladdningen av filen lyckades" } \ No newline at end of file diff --git a/client/src/locales/th/translation.json b/client/src/locales/th/translation.json index 17910a2b1..733e75eab 100644 --- a/client/src/locales/th/translation.json +++ b/client/src/locales/th/translation.json @@ -789,7 +789,6 @@ "com_ui_use_2fa_code": "ใช้รหัส 2FA แทน", "com_ui_use_backup_code": "ใช้รหัสสำรองแทน", "com_ui_use_micrphone": "ใช้ไมโครโฟน", - "com_ui_use_prompt": "ใช้พรอมต์", "com_ui_used": "ใช้แล้ว", "com_ui_variables": "ตัวแปร", "com_ui_variables_info": "ใช้วงเล็บคู่ในข้อความของคุณเพื่อสร้างตัวแปร เช่น {{example variable}} เพื่อเติมภายหลังเมื่อใช้พรอมต์", diff --git a/client/src/locales/tr/translation.json b/client/src/locales/tr/translation.json index dd928e027..64ce86322 100644 --- a/client/src/locales/tr/translation.json +++ b/client/src/locales/tr/translation.json @@ -716,7 +716,6 @@ "com_ui_upload_success": "Dosya başarıyla yüklendi", "com_ui_upload_type": "Yükleme Türünü Seç", "com_ui_use_micrphone": "Mikrofon kullan", - "com_ui_use_prompt": "İstemi kullan", "com_ui_variables": "Değişkenler", "com_ui_variables_info": "İstemi kullanırken daha sonra doldurmak üzere metninizde çift süslü parantez kullanın, örn. `{{example variable}}`.", "com_ui_version_var": "Sürüm {{0}}", diff --git a/client/src/locales/vi/translation.json b/client/src/locales/vi/translation.json index bdd8e612b..8e4188f1f 100644 --- a/client/src/locales/vi/translation.json +++ b/client/src/locales/vi/translation.json @@ -375,7 +375,6 @@ "com_ui_unarchive_error": "Không thể bỏ lưu trữ cuộc trò chuyện", "com_ui_upload": "Tải lên", "com_ui_upload_success": "Tải tệp thành công", - "com_ui_use_prompt": "Sử dụng gợi ý", "com_ui_versions": "Phiên bản", "com_ui_web_searching_again": "Tìm kiếm lại trên web", "com_ui_write": "Ghi", diff --git a/client/src/locales/zh-Hans/translation.json b/client/src/locales/zh-Hans/translation.json index 956d16f03..9362e5182 100644 --- a/client/src/locales/zh-Hans/translation.json +++ b/client/src/locales/zh-Hans/translation.json @@ -854,7 +854,6 @@ "com_ui_mcp_not_authenticated": "{{0}} 未认证(需要 OAuth)", "com_ui_mcp_not_initialized": "{{0}} 未初始化", "com_ui_mcp_oauth_cancelled": "{{0}} OAuth 登录已取消", - "com_ui_mcp_oauth_no_url": "需要 OAuth 认证,但未提供 URL", "com_ui_mcp_oauth_timeout": "{{0}} OAuth 登录超时", "com_ui_mcp_server_not_found": "未找到服务器。", "com_ui_mcp_servers": "MCP 服务器", @@ -1067,7 +1066,6 @@ "com_ui_use_backup_code": "使用备份代码代替", "com_ui_use_memory": "使用记忆", "com_ui_use_micrphone": "使用麦克风", - "com_ui_use_prompt": "使用提示词", "com_ui_used": "已使用", "com_ui_value": "值", "com_ui_variables": "变量", diff --git a/client/src/locales/zh-Hant/translation.json b/client/src/locales/zh-Hant/translation.json index a2e3fd0b8..e7040ea9a 100644 --- a/client/src/locales/zh-Hant/translation.json +++ b/client/src/locales/zh-Hant/translation.json @@ -737,7 +737,6 @@ "com_ui_usage": "使用率", "com_ui_use_memory": "使用記憶", "com_ui_use_micrphone": "使用麥克風", - "com_ui_use_prompt": "使用提示", "com_ui_variables": "變數", "com_ui_variables_info": "在文字中使用雙大括號來建立變數,例如 `{{example variable}}`,以便在使用提示時填入。", "com_ui_version_var": "版本 {{0}}", From 6fc9abd4ad6800531e226627f31b66a3138ddfae Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 30 Jul 2025 15:54:33 -0400 Subject: [PATCH 038/224] =?UTF-8?q?=E2=9C=82=EF=B8=8F=20fix:=20Remove=20Im?= =?UTF-8?q?age=20Payloads=20from=20Memory=20Processing=20(#8770)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server/controllers/agents/client.js | 36 ++- api/server/controllers/agents/client.test.js | 227 +++++++++++++++++++ 2 files changed, 262 insertions(+), 1 deletion(-) diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index f9dacbe5a..d8fda5159 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -512,6 +512,39 @@ class AgentClient extends BaseClient { return withoutKeys; } + /** + * Filters out image URLs from message content + * @param {BaseMessage} message - The message to filter + * @returns {BaseMessage} - A new message with image URLs removed + */ + filterImageUrls(message) { + if (!message.content || typeof message.content === 'string') { + return message; + } + + if (Array.isArray(message.content)) { + const filteredContent = message.content.filter( + (part) => part.type !== ContentTypes.IMAGE_URL, + ); + + if (filteredContent.length === 1 && filteredContent[0].type === ContentTypes.TEXT) { + const MessageClass = message.constructor; + return new MessageClass({ + content: filteredContent[0].text, + additional_kwargs: message.additional_kwargs, + }); + } + + const MessageClass = message.constructor; + return new MessageClass({ + content: filteredContent, + additional_kwargs: message.additional_kwargs, + }); + } + + return message; + } + /** * @param {BaseMessage[]} messages * @returns {Promise} @@ -540,7 +573,8 @@ class AgentClient extends BaseClient { } } - const bufferString = getBufferString(messagesToProcess); + const filteredMessages = messagesToProcess.map((msg) => this.filterImageUrls(msg)); + const bufferString = getBufferString(filteredMessages); const bufferMessage = new HumanMessage(`# Current Chat:\n\n${bufferString}`); return await this.processMemory([bufferMessage]); } catch (error) { diff --git a/api/server/controllers/agents/client.test.js b/api/server/controllers/agents/client.test.js index aab972fec..92b97ad00 100644 --- a/api/server/controllers/agents/client.test.js +++ b/api/server/controllers/agents/client.test.js @@ -727,4 +727,231 @@ describe('AgentClient - titleConvo', () => { }); }); }); + + describe('runMemory method', () => { + let client; + let mockReq; + let mockRes; + let mockAgent; + let mockOptions; + let mockProcessMemory; + + beforeEach(() => { + jest.clearAllMocks(); + + mockAgent = { + id: 'agent-123', + endpoint: EModelEndpoint.openAI, + provider: EModelEndpoint.openAI, + model_parameters: { + model: 'gpt-4', + }, + }; + + mockReq = { + app: { + locals: { + memory: { + messageWindowSize: 3, + }, + }, + }, + user: { + id: 'user-123', + personalization: { + memories: true, + }, + }, + }; + + mockRes = {}; + + mockOptions = { + req: mockReq, + res: mockRes, + agent: mockAgent, + }; + + mockProcessMemory = jest.fn().mockResolvedValue([]); + + client = new AgentClient(mockOptions); + client.processMemory = mockProcessMemory; + client.conversationId = 'convo-123'; + client.responseMessageId = 'response-123'; + }); + + it('should filter out image URLs from message content', async () => { + const { HumanMessage, AIMessage } = require('@langchain/core/messages'); + const messages = [ + new HumanMessage({ + content: [ + { + type: 'text', + text: 'What is in this image?', + }, + { + type: 'image_url', + image_url: { + url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', + detail: 'auto', + }, + }, + ], + }), + new AIMessage('I can see a small red pixel in the image.'), + new HumanMessage({ + content: [ + { + type: 'text', + text: 'What about this one?', + }, + { + type: 'image_url', + image_url: { + url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/', + detail: 'high', + }, + }, + ], + }), + ]; + + await client.runMemory(messages); + + expect(mockProcessMemory).toHaveBeenCalledTimes(1); + const processedMessage = mockProcessMemory.mock.calls[0][0][0]; + + // Verify the buffer message was created + expect(processedMessage.constructor.name).toBe('HumanMessage'); + expect(processedMessage.content).toContain('# Current Chat:'); + + // Verify that image URLs are not in the buffer string + expect(processedMessage.content).not.toContain('image_url'); + expect(processedMessage.content).not.toContain('data:image'); + expect(processedMessage.content).not.toContain('base64'); + + // Verify text content is preserved + expect(processedMessage.content).toContain('What is in this image?'); + expect(processedMessage.content).toContain('I can see a small red pixel in the image.'); + expect(processedMessage.content).toContain('What about this one?'); + }); + + it('should handle messages with only text content', async () => { + const { HumanMessage, AIMessage } = require('@langchain/core/messages'); + const messages = [ + new HumanMessage('Hello, how are you?'), + new AIMessage('I am doing well, thank you!'), + new HumanMessage('That is great to hear.'), + ]; + + await client.runMemory(messages); + + expect(mockProcessMemory).toHaveBeenCalledTimes(1); + const processedMessage = mockProcessMemory.mock.calls[0][0][0]; + + expect(processedMessage.content).toContain('Hello, how are you?'); + expect(processedMessage.content).toContain('I am doing well, thank you!'); + expect(processedMessage.content).toContain('That is great to hear.'); + }); + + it('should handle mixed content types correctly', async () => { + const { HumanMessage } = require('@langchain/core/messages'); + const { ContentTypes } = require('librechat-data-provider'); + + const messages = [ + new HumanMessage({ + content: [ + { + type: 'text', + text: 'Here is some text', + }, + { + type: ContentTypes.IMAGE_URL, + image_url: { + url: 'https://example.com/image.png', + }, + }, + { + type: 'text', + text: ' and more text', + }, + ], + }), + ]; + + await client.runMemory(messages); + + expect(mockProcessMemory).toHaveBeenCalledTimes(1); + const processedMessage = mockProcessMemory.mock.calls[0][0][0]; + + // Should contain text parts but not image URLs + expect(processedMessage.content).toContain('Here is some text'); + expect(processedMessage.content).toContain('and more text'); + expect(processedMessage.content).not.toContain('example.com/image.png'); + expect(processedMessage.content).not.toContain('IMAGE_URL'); + }); + + it('should preserve original messages without mutation', async () => { + const { HumanMessage } = require('@langchain/core/messages'); + const originalContent = [ + { + type: 'text', + text: 'Original text', + }, + { + type: 'image_url', + image_url: { + url: 'data:image/png;base64,ABC123', + }, + }, + ]; + + const messages = [ + new HumanMessage({ + content: [...originalContent], + }), + ]; + + await client.runMemory(messages); + + // Verify original message wasn't mutated + expect(messages[0].content).toHaveLength(2); + expect(messages[0].content[1].type).toBe('image_url'); + expect(messages[0].content[1].image_url.url).toBe('data:image/png;base64,ABC123'); + }); + + it('should handle message window size correctly', async () => { + const { HumanMessage, AIMessage } = require('@langchain/core/messages'); + const messages = [ + new HumanMessage('Message 1'), + new AIMessage('Response 1'), + new HumanMessage('Message 2'), + new AIMessage('Response 2'), + new HumanMessage('Message 3'), + new AIMessage('Response 3'), + ]; + + // Window size is set to 3 in mockReq + await client.runMemory(messages); + + expect(mockProcessMemory).toHaveBeenCalledTimes(1); + const processedMessage = mockProcessMemory.mock.calls[0][0][0]; + + // Should only include last 3 messages due to window size + expect(processedMessage.content).toContain('Message 3'); + expect(processedMessage.content).toContain('Response 3'); + expect(processedMessage.content).not.toContain('Message 1'); + expect(processedMessage.content).not.toContain('Response 1'); + }); + + it('should return early if processMemory is not set', async () => { + const { HumanMessage } = require('@langchain/core/messages'); + client.processMemory = null; + + const result = await client.runMemory([new HumanMessage('Test')]); + + expect(result).toBeUndefined(); + expect(mockProcessMemory).not.toHaveBeenCalled(); + }); + }); }); From 5eed5009e9e53031cc1cb43c6843d6b787298b69 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:59:27 -0400 Subject: [PATCH 039/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#8771)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/ca/translation.json | 24 ++++ client/src/locales/de/translation.json | 2 - client/src/locales/en/translation.json | 2 +- client/src/locales/fr/translation.json | 16 ++- client/src/locales/he/translation.json | 2 - client/src/locales/ja/translation.json | 2 - client/src/locales/lv/translation.json | 7 +- client/src/locales/pt-BR/translation.json | 122 ++++++++++++++++++++ client/src/locales/pt-PT/translation.json | 39 +++++++ client/src/locales/zh-Hans/translation.json | 13 +-- client/src/locales/zh-Hant/translation.json | 37 +++++- 11 files changed, 235 insertions(+), 31 deletions(-) diff --git a/client/src/locales/ca/translation.json b/client/src/locales/ca/translation.json index 627760ed2..eacd855c1 100644 --- a/client/src/locales/ca/translation.json +++ b/client/src/locales/ca/translation.json @@ -61,6 +61,7 @@ "com_assistants_non_retrieval_model": "La cerca de fitxers no està habilitada en aquest model. Selecciona un altre model.", "com_assistants_retrieval": "Recuperació", "com_assistants_running_action": "Executant acció", + "com_assistants_running_var": "Executant {{0}}", "com_assistants_search_name": "Cerca assistents per nom", "com_assistants_update_actions_error": "S'ha produït un error en crear o actualitzar l'acció.", "com_assistants_update_actions_success": "Acció creada o actualitzada amb èxit", @@ -122,6 +123,7 @@ "com_auth_reset_password_if_email_exists": "Si existeix un compte amb aquest correu, s'ha enviat un correu amb instruccions per restablir la contrasenya. Comprova la carpeta de correu brossa.", "com_auth_reset_password_link_sent": "Correu enviat", "com_auth_reset_password_success": "Contrasenya restablerta amb èxit", + "com_auth_saml_login": "Continuar amb SAML", "com_auth_sign_in": "Inicia sessió", "com_auth_sign_up": "Registra't", "com_auth_submit_registration": "Envia el registre", @@ -133,6 +135,8 @@ "com_auth_username_min_length": "El nom d'usuari ha de tenir almenys 2 caràcters", "com_auth_verify_your_identity": "Verifica la teva identitat", "com_auth_welcome_back": "Benvingut/da de nou", + "com_citation_more_details": "Més detalls sobre {{label}}", + "com_citation_source": "Font", "com_click_to_download": "(fes clic aquí per descarregar)", "com_download_expired": "(descàrrega caducada)", "com_download_expires": "(fes clic aquí per descarregar - caduca en {{0}})", @@ -299,6 +303,18 @@ "com_nav_auto_transcribe_audio": "Transcriu àudio automàticament", "com_nav_automatic_playback": "Reprodueix automàticament el darrer missatge", "com_nav_balance": "Balanç", + "com_nav_balance_day": "dia", + "com_nav_balance_days": "dies", + "com_nav_balance_hour": "hora", + "com_nav_balance_hours": "hores", + "com_nav_balance_minute": "minut", + "com_nav_balance_minutes": "minuts", + "com_nav_balance_month": "mes", + "com_nav_balance_months": "mesos", + "com_nav_balance_second": "segon", + "com_nav_balance_seconds": "segons", + "com_nav_balance_week": "setmana", + "com_nav_balance_weeks": "setmanes", "com_nav_browser": "Navegador", "com_nav_center_chat_input": "Centra la entrada del xat a la pantalla de benvinguda", "com_nav_change_picture": "Canvia la imatge", @@ -560,8 +576,10 @@ "com_ui_confirm_action": "Confirma l'acció", "com_ui_confirm_admin_use_change": "Canviar aquesta opció bloquejarà l'accés als administradors, inclòs tu mateix. Segur que vols continuar?", "com_ui_confirm_change": "Confirma el canvi", + "com_ui_connecting": "Connectant", "com_ui_context": "Context", "com_ui_continue": "Continua", + "com_ui_continue_oauth": "Continuar amb OAuth", "com_ui_controls": "Controls", "com_ui_convo_delete_error": "No s'ha pogut eliminar la conversa", "com_ui_copied": "Copiat!", @@ -625,6 +643,7 @@ "com_ui_duplication_processing": "Duplicant conversa...", "com_ui_duplication_success": "Conversa duplicada amb èxit", "com_ui_edit": "Edita", + "com_ui_edit_memory": "Editar memòria", "com_ui_empty_category": "-", "com_ui_endpoint": "Extrem", "com_ui_endpoint_menu": "Menú d'extrem LLM", @@ -701,6 +720,8 @@ "com_ui_manage": "Gestiona", "com_ui_max_tags": "El màxim permès és {{0}}, s'utilitzen els últims valors.", "com_ui_mcp_servers": "Servidors MCP", + "com_ui_memories": "Memòries", + "com_ui_memory": "Memòria", "com_ui_mention": "Menciona un endpoint, assistent o predefinit per canviar-hi ràpidament", "com_ui_min_tags": "No es poden eliminar més valors, el mínim requerit és {{0}}.", "com_ui_misc": "Miscel·lània", @@ -729,6 +750,7 @@ "com_ui_off": "Desactivat", "com_ui_on": "Activat", "com_ui_openai": "OpenAI", + "com_ui_optional": "(opcional)", "com_ui_page": "Pàgina", "com_ui_prev": "Anterior", "com_ui_preview": "Previsualitza", @@ -778,6 +800,7 @@ "com_ui_schema": "Esquema", "com_ui_scope": "Abast", "com_ui_search": "Cerca", + "com_ui_seconds": "segons", "com_ui_secret_key": "Clau secreta", "com_ui_select": "Selecciona", "com_ui_select_file": "Selecciona un fitxer", @@ -859,6 +882,7 @@ "com_ui_version_var": "Versió {{0}}", "com_ui_versions": "Versions", "com_ui_view_source": "Mostra el xat original", + "com_ui_web_search_processing": "Processant resultats", "com_ui_weekend_morning": "Bon cap de setmana", "com_ui_write": "Escriptura", "com_ui_x_selected": "{{0}} seleccionats", diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index e8e6b266d..196509eca 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -503,7 +503,6 @@ "com_sidepanel_hide_panel": "Seitenleiste ausblenden", "com_sidepanel_manage_files": "Dateien verwalten", "com_sidepanel_mcp_no_servers_with_vars": "Keine MCP-Server mit konfigurierbaren Variablen.", - "com_sidepanel_mcp_variables_for": "MCP Variablen für {{0}}", "com_sidepanel_parameters": "KI-Einstellungen", "com_sources_image_alt": "Suchergebnis Bild\n", "com_sources_more_sources": "+{{count}} Quellen\n", @@ -840,7 +839,6 @@ "com_ui_low": "Niedrig", "com_ui_manage": "Verwalten", "com_ui_max_tags": "Die maximale Anzahl ist {{0}}, es werden die neuesten Werte verwendet.", - "com_ui_mcp_dialog_desc": "Bitte geben Sie unten die erforderlichen Informationen ein.", "com_ui_mcp_enter_var": "Geben Sie einen Wert für {{0}} ein", "com_ui_mcp_server_not_found": "Server nicht gefunden", "com_ui_mcp_servers": "MCP Server", diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index ae61f049a..51ae742aa 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1102,4 +1102,4 @@ "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", "com_user_message": "You" -} +} \ No newline at end of file diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index e065af573..71b8eb268 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -502,7 +502,6 @@ "com_sidepanel_hide_panel": "Masquer le panneau", "com_sidepanel_manage_files": "Gérer les fichiers", "com_sidepanel_mcp_no_servers_with_vars": "Aucun serveur MCP dont les variables sont configurables.", - "com_sidepanel_mcp_variables_for": "Variables MCP de {{0}}", "com_sidepanel_parameters": "Paramètres", "com_sources_image_alt": "Image de résultat de recherche", "com_sources_more_sources": "+{{count}} sources", @@ -837,7 +836,6 @@ "com_ui_low": "Faible", "com_ui_manage": "Gérer", "com_ui_max_tags": "Le nombre maximum autorisé est {{0}}, en utilisant les dernières valeurs.", - "com_ui_mcp_dialog_desc": "Veuillez saisir les informations importantes ci-dessous.", "com_ui_mcp_enter_var": "Saisissez la valeur de {{0}}", "com_ui_mcp_server_not_found": "Le serveur n'a pas été trouvé.", "com_ui_mcp_servers": "Serveurs MCP", @@ -1021,18 +1019,18 @@ "com_ui_update": "Mettre à jour", "com_ui_update_mcp_error": "Une erreur est survenue lors de la création ou l'actualisation du MCP.", "com_ui_update_mcp_success": "Création ou actualisation du MCP réussie", - "com_ui_upload": "Téléverser", - "com_ui_upload_code_files": "Téléverser pour l'Interpréteur de Code", + "com_ui_upload": "Télécharger", + "com_ui_upload_code_files": "Télécharger pour l'Interpréteur de Code", "com_ui_upload_delay": "Le téléversement de \"{{0}}\" prend plus de temps que prévu. Veuillez patienter pendant que le fichier termine son indexation pour la récupération.", "com_ui_upload_error": "Une erreur s'est produite lors du téléversement de votre fichier", - "com_ui_upload_file_context": "Téléverser le contexte du fichier", - "com_ui_upload_file_search": "Téléverser pour la recherche de fichiers", - "com_ui_upload_files": "Téléverser des fichiers", - "com_ui_upload_image": "Téléverser une image", + "com_ui_upload_file_context": "Télécharger le contexte du fichier", + "com_ui_upload_file_search": "Télécharger pour la recherche dans un fichier", + "com_ui_upload_files": "Télécharger des fichiers", + "com_ui_upload_image": "Télécharger une image", "com_ui_upload_image_input": "Téléverser une image", "com_ui_upload_invalid": "Fichier non valide pour le téléchargement. L'image ne doit pas dépasser la limite", "com_ui_upload_invalid_var": "Fichier non valide pour le téléchargement. L'image ne doit pas dépasser {{0}} Mo", - "com_ui_upload_ocr_text": "Téléverser en tant que texte", + "com_ui_upload_ocr_text": "Téléchager en tant que texte", "com_ui_upload_success": "Fichier téléversé avec succès", "com_ui_upload_type": "Sélectionner le type de téléversement", "com_ui_usage": "Utilisation", diff --git a/client/src/locales/he/translation.json b/client/src/locales/he/translation.json index cc782d68e..d2b189f15 100644 --- a/client/src/locales/he/translation.json +++ b/client/src/locales/he/translation.json @@ -495,7 +495,6 @@ "com_sidepanel_hide_panel": "הסתר פאנל", "com_sidepanel_manage_files": "נהל קבצים", "com_sidepanel_mcp_no_servers_with_vars": "אין שרתי MCP עם משתנים הניתנים להגדרה.", - "com_sidepanel_mcp_variables_for": "משתני MCP עבור {{0}}", "com_sidepanel_parameters": "פרמטרים", "com_sources_image_alt": "תמונת תוצאות החיפוש", "com_sources_more_sources": "+{{count}}} מקורות", @@ -832,7 +831,6 @@ "com_ui_low": "נמוך", "com_ui_manage": "נהל", "com_ui_max_tags": "המספר המקסימלי המותר על פי הערכים העדכניים הוא {{0}}.", - "com_ui_mcp_dialog_desc": "אנא הזן למטה את המידע הדרוש", "com_ui_mcp_enter_var": "הזן ערך עבור {{0}}", "com_ui_mcp_server_not_found": "נשרת לא נמצא", "com_ui_mcp_servers": "שרתי MCP", diff --git a/client/src/locales/ja/translation.json b/client/src/locales/ja/translation.json index b7eade0f8..96b0b6594 100644 --- a/client/src/locales/ja/translation.json +++ b/client/src/locales/ja/translation.json @@ -501,7 +501,6 @@ "com_sidepanel_hide_panel": "パネルを隠す", "com_sidepanel_manage_files": "ファイルを管理", "com_sidepanel_mcp_no_servers_with_vars": "設定可能な変数を持つMCPサーバーはありません。", - "com_sidepanel_mcp_variables_for": "{{0}}のMCP変数", "com_sidepanel_parameters": "パラメータ", "com_sources_image_alt": "検索結果画像", "com_sources_more_sources": "+{{count}} ソース", @@ -839,7 +838,6 @@ "com_ui_low": "低い", "com_ui_manage": "管理", "com_ui_max_tags": "最新の値を使用した場合、許可される最大数は {{0}} です。", - "com_ui_mcp_dialog_desc": "以下に必要事項を入力してください。", "com_ui_mcp_enter_var": "{{0}}の値を入力する。", "com_ui_mcp_server_not_found": "サーバーが見つかりません。", "com_ui_mcp_servers": "MCP サーバー", diff --git a/client/src/locales/lv/translation.json b/client/src/locales/lv/translation.json index 6fa3f9dfc..bc852c349 100644 --- a/client/src/locales/lv/translation.json +++ b/client/src/locales/lv/translation.json @@ -506,7 +506,6 @@ "com_sidepanel_hide_panel": "Slēpt paneli", "com_sidepanel_manage_files": "Pārvaldīt failus", "com_sidepanel_mcp_no_servers_with_vars": "Nav MCP serveru ar konfigurējamiem mainīgajiem.", - "com_sidepanel_mcp_variables_for": "MCP parametri {{0}}", "com_sidepanel_parameters": "Parametri", "com_sources_image_alt": "Meklēšanas rezultāta attēls", "com_sources_more_sources": "+{{count}} avoti", @@ -838,7 +837,7 @@ "com_ui_instructions": "Instrukcijas", "com_ui_key": "Atslēga", "com_ui_late_night": "Priecīgu vēlu nakti", - "com_ui_latest_footer": "Katrs mākslīgais intelekts ikvienam.", + "com_ui_latest_footer": "Mākslīgais intelekts ikvienam.", "com_ui_latest_production_version": "Jaunākā produkcijas versija", "com_ui_latest_version": "Jaunākā versija", "com_ui_librechat_code_api_key": "Iegūstiet savu LibreChat koda interpretatora API atslēgu", @@ -851,13 +850,10 @@ "com_ui_manage": "Pārvaldīt", "com_ui_max_tags": "Maksimālais atļautais skaits ir {{0}}, izmantojot jaunākās vērtības.", "com_ui_mcp_authenticated_success": "MCP serveris '{{0}}' veiksmīgi autentificēts", - "com_ui_mcp_dialog_desc": "Lūdzu, ievadiet nepieciešamo informāciju zemāk.", "com_ui_mcp_enter_var": "Ievadiet vērtību {{0}}", "com_ui_mcp_init_failed": "Neizdevās inicializēt MCP serveri", "com_ui_mcp_initialize": "Inicializēt", "com_ui_mcp_initialized_success": "MCP serveris '{{0}}' veiksmīgi inicializēts", - "com_ui_mcp_not_authenticated": "{{0}} nav autentificēts (nepieciešams OAuth).", - "com_ui_mcp_not_initialized": "{{0}} nav inicializēts", "com_ui_mcp_oauth_cancelled": "OAuth pieteikšanās atcelta {{0}}", "com_ui_mcp_oauth_timeout": "OAuth pieteikšanās beidzās priekš {{0}}", "com_ui_mcp_server_not_found": "Serveris nav atrasts.", @@ -917,7 +913,6 @@ "com_ui_oauth_error_missing_code": "Trūkst autorizācijas koda. Lūdzu, mēģiniet vēlreiz.", "com_ui_oauth_error_missing_state": "Trūkst stāvokļa parametrs. Lūdzu, mēģiniet vēlreiz.", "com_ui_oauth_error_title": "Autentifikācija neizdevās", - "com_ui_oauth_flow_desc": "Pabeidziet OAuth plūsmu jaunajā logā un pēc tam atgriezieties šeit.", "com_ui_oauth_success_description": "Jūsu autentifikācija bija veiksmīga. Šis logs aizvērsies pēc", "com_ui_oauth_success_title": "Autentifikācija veiksmīga", "com_ui_of": "no", diff --git a/client/src/locales/pt-BR/translation.json b/client/src/locales/pt-BR/translation.json index 647cd87cd..f60ac45d6 100644 --- a/client/src/locales/pt-BR/translation.json +++ b/client/src/locales/pt-BR/translation.json @@ -28,6 +28,7 @@ "com_agents_no_access": "Não tens permissões para editar este agente.", "com_agents_no_agent_id_error": "Nenhum ID de agente encontrado. Certifique-se de que o agente seja criado primeiro.", "com_agents_not_available": "Agente não disponível.", + "com_agents_search_info": "Quando ativado, permite seu agente buscar informações atualizadas na web. Requer uma chave de API válida.", "com_agents_search_name": "Pesquisar agentes por nome", "com_agents_update_error": "Houve um erro ao atualizar seu agente.", "com_assistants_action_attempt": "Assistente quer falar com {{0}}", @@ -130,6 +131,7 @@ "com_auth_reset_password_if_email_exists": "Se uma conta com esse e-mail existir, um e-mail com instruções para redefinir a senha foi enviado. Certifique-se de verificar sua pasta de spam.", "com_auth_reset_password_link_sent": "E-mail enviado", "com_auth_reset_password_success": "Senha redefinida com sucesso", + "com_auth_saml_login": "Continue com SAML", "com_auth_sign_in": "Entrar", "com_auth_sign_up": "Inscrever-se", "com_auth_submit_registration": "Enviar registro", @@ -158,6 +160,7 @@ "com_endpoint_anthropic_thinking_budget": "Determina o número máximo de tokens que o Claude pode utilizar para o seu processo de raciocínio interno. Orçamentos maiores podem melhorar a qualidade da resposta, permitindo uma análise mais completa para problemas complexos, embora o Claude possa não usar todo o orçamento alocado, especialmente em intervalos acima de 32K. Essa configuração deve ser menor que \"Máximo de tokens de saída\".", "com_endpoint_anthropic_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_anthropic_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_anthropic_use_web_search": "Habilita a funcionalidade de pesquisa na web usando o recurso integrado da Anthropic. Isso permite que o modelo pesquise informações atualizadas na web e forneça respostas mais precisas e atuais.", "com_endpoint_assistant": "Assistente", "com_endpoint_assistant_model": "Modelo de Assistente", "com_endpoint_assistant_placeholder": "Por favor, selecione um Assistente no Painel Lateral Direito", @@ -203,6 +206,7 @@ "com_endpoint_google_custom_name_placeholder": "Defina um nome personalizado para o Google", "com_endpoint_google_maxoutputtokens": "Número máximo de tokens que podem ser gerados na resposta. Especifique um valor mais baixo para respostas mais curtas e um valor mais alto para respostas mais longas. Nota: os modelos podem parar antes de atingir esse máximo.", "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_thinking": "Habilita ou desabilita o pensamento. Essa opção é suportada apenas por certos modelos (série 2.5). Para modelos antigos, esta opção pode não ter efeito.", "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_instructions_assistants": "Substituir Instruções", @@ -227,6 +231,7 @@ "com_endpoint_openai_stop": "Até 4 sequências onde a API parará de gerar mais tokens.", "com_endpoint_openai_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_openai_topp": "Uma alternativa à amostragem com temperatura, chamada amostragem de núcleo, onde o modelo considera os resultados dos tokens com massa de probabilidade top_p. Então, 0.1 significa que apenas os tokens que compreendem os 10% principais da massa de probabilidade são considerados. Recomendamos alterar isso ou a temperatura, mas não ambos.", + "com_endpoint_openai_use_responses_api": "Usa a API de Respostas ao invés de Conclusões de Chat, que inclui funcionalidades extendidas da OpenAI. Requerida para o1-pro, o3-pro, e para habilitar resumos de raciocínio.", "com_endpoint_output": "Saída", "com_endpoint_plug_image_detail": "Detalhe da Imagem", "com_endpoint_plug_resend_files": "Reenviar Arquivos", @@ -259,6 +264,7 @@ "com_endpoint_reasoning_effort": "Esforço de raciocínio", "com_endpoint_save_as_preset": "Salvar Como Preset", "com_endpoint_search": "Procurar endpoint por nome", + "com_endpoint_search_models": "Buscar modelos...", "com_endpoint_set_custom_name": "Defina um nome personalizado, caso você possa encontrar este preset", "com_endpoint_skip_hover": "Habilitar pular a etapa de conclusão, que revisa a resposta final e os passos gerados", "com_endpoint_stop": "Sequências de Parada", @@ -305,6 +311,16 @@ "com_nav_auto_transcribe_audio": "Transcrever áudio automaticamente", "com_nav_automatic_playback": "Reprodução Automática da Última Mensagem", "com_nav_balance": "Crédito", + "com_nav_balance_day": "dia", + "com_nav_balance_days": "dias", + "com_nav_balance_hour": "hora", + "com_nav_balance_hours": "horas", + "com_nav_balance_minute": "minuto", + "com_nav_balance_minutes": "minutos", + "com_nav_balance_month": "mês", + "com_nav_balance_months": "meses", + "com_nav_balance_second": "segundo", + "com_nav_balance_seconds": "segundos", "com_nav_browser": "Navegador", "com_nav_change_picture": "Mudar foto", "com_nav_chat_commands": "Comandos do chat", @@ -362,9 +378,11 @@ "com_nav_info_show_thinking": "Quando ativado, o chat apresentará os menus pendentes de raciocínio abertos por predefinição, permitindo-lhe ver o raciocínio da IA em tempo real. Quando desativado, os menus suspensos de raciocínio permanecerão fechados por predefinição para uma interface mais limpa e simplificada", "com_nav_info_user_name_display": "Quando habilitado, o nome de usuário do remetente será mostrado acima de cada mensagem que você enviar. Quando desabilitado, você verá apenas \"Você\" acima de suas mensagens.", "com_nav_lang_arabic": "العربية", + "com_nav_lang_armenian": "Armênio", "com_nav_lang_auto": "Detecção automática", "com_nav_lang_brazilian_portuguese": "Português Brasileiro", "com_nav_lang_chinese": "中文", + "com_nav_lang_danish": "Dinamarquês", "com_nav_lang_dutch": "Nederlands", "com_nav_lang_english": "English", "com_nav_lang_estonian": "Eesti keel", @@ -445,6 +463,7 @@ "com_sidepanel_hide_panel": "Ocultar Painel", "com_sidepanel_manage_files": "Gerenciar Arquivos", "com_sidepanel_parameters": "Parâmetros", + "com_sources_tab_images": "Imagens", "com_ui_2fa_account_security": "A autenticação de dois fatores acrescenta uma camada extra de segurança à sua conta", "com_ui_2fa_disable": "Desabilitar 2FA", "com_ui_2fa_disable_error": "Ocorreu um erro ao desativar a autenticação de dois fatores", @@ -456,13 +475,17 @@ "com_ui_2fa_setup": "Configurar 2FA", "com_ui_2fa_verified": "Autenticação de dois fatores verificada com sucesso", "com_ui_accept": "Eu aceito", + "com_ui_active": "Ativo", "com_ui_add": "Adicionar", + "com_ui_add_mcp": "Adicionar MCP", + "com_ui_add_mcp_server": "Adicionar Servidor MCP", "com_ui_add_model_preset": "Adicionar um modelo ou predefinição para uma resposta adicional", "com_ui_add_multi_conversation": "Adicionar multi-conversação", "com_ui_admin": "Admin", "com_ui_admin_access_warning": "Desabilitar o acesso de Admin a esse recurso pode causar problemas inesperados na IU que exigem atualização. Se salvo, a única maneira de reverter é por meio da configuração de interface na configuração librechat.yaml que afeta todas as funções.", "com_ui_admin_settings": "Configurações de Admin", "com_ui_advanced": "Avançado", + "com_ui_advanced_settings": "Opções Avançadas", "com_ui_agent": "Agente", "com_ui_agent_delete_error": "Houve um erro ao excluir o agente", "com_ui_agent_deleted": "Agente excluído com sucesso", @@ -470,6 +493,12 @@ "com_ui_agent_duplicated": "Agente duplicado com sucesso", "com_ui_agent_editing_allowed": "Outros usuários já podem editar este agente", "com_ui_agent_shared_to_all": "algo precisa ir aqui. esta vazio", + "com_ui_agent_version": "Versão", + "com_ui_agent_version_active": "Versão Ativa", + "com_ui_agent_version_empty": "Não há versões disponíveis", + "com_ui_agent_version_history": "Histórico de Versões", + "com_ui_agent_version_no_date": "Data não disponível", + "com_ui_agent_version_unknown_date": "Data desconhecida", "com_ui_agents": "Agentes", "com_ui_agents_allow_create": "Permitir a criação de agentes", "com_ui_agents_allow_share_global": "Permitir compartilhamento de agentes para todos os usuários", @@ -495,12 +524,15 @@ "com_ui_attach_error_openai": "Não é possível anexar arquivos de Assistente a outros endpoints", "com_ui_attach_error_size": "Limite de tamanho de arquivo excedido para o endpoint:", "com_ui_attach_error_type": "Tipo de arquivo não suportado para o endpoint:", + "com_ui_attach_remove": "Remover arquivo", "com_ui_attach_warn_endpoint": "Arquivos não compatíveis podem ser ignorados sem uma ferramenta compatível", "com_ui_attachment": "Anexo", "com_ui_auth_type": "Tipo de autenticação", "com_ui_auth_url": "URL de autorização", + "com_ui_authenticate": "Autenticar", "com_ui_authentication": "Autenticação", "com_ui_authentication_type": "Tipo de Autenticação", + "com_ui_available_tools": "Ferramentas Disponíveis", "com_ui_avatar": "Avatar", "com_ui_azure": "Azure", "com_ui_back_to_chat": "Voltar ao Chat", @@ -532,6 +564,8 @@ "com_ui_bulk_delete_error": "Falha ao excluir links compartilhados", "com_ui_callback_url": "URL de retorno de chamada", "com_ui_cancel": "Cancelar", + "com_ui_cancelled": "Cancelado", + "com_ui_category": "Categoria", "com_ui_chat": "Chat", "com_ui_chat_history": "Histórico de Chat", "com_ui_clear": "Limpar", @@ -540,6 +574,7 @@ "com_ui_client_secret": "Segredo do cliente", "com_ui_close": "Fechar", "com_ui_close_menu": "Fechar Menu", + "com_ui_close_window": "Fechar Janela", "com_ui_code": "Código", "com_ui_collapse_chat": "Recolher bate-papo", "com_ui_command_placeholder": "Opcional: Insira um comando para o prompt ou o nome será usado.", @@ -548,9 +583,12 @@ "com_ui_confirm_action": "Confirmar Ação", "com_ui_confirm_admin_use_change": "Alterar esta configuração bloqueará o acesso para administradores, incluindo você. Tem certeza de que deseja prosseguir?", "com_ui_confirm_change": "Confirmar alteração", + "com_ui_connecting": "Conectando", "com_ui_context": "Contexto", "com_ui_continue": "Continuar", + "com_ui_continue_oauth": "Continuar com OAuth", "com_ui_controls": "Controles", + "com_ui_convo_delete_error": "Falha ao excluir conversa", "com_ui_copied": "Copiado!", "com_ui_copied_to_clipboard": "Copiado para a área de transferência", "com_ui_copy_code": "Copiar código", @@ -558,7 +596,9 @@ "com_ui_copy_to_clipboard": "Copiar para a área de transferência", "com_ui_create": "Criar", "com_ui_create_link": "Criar link", + "com_ui_create_memory": "Criar Memória", "com_ui_create_prompt": "Criar Prompt", + "com_ui_creating_image": "Criando a imagem. Pode levar algum tempo", "com_ui_currently_production": "Atualmente em produção", "com_ui_custom": "Personalizado", "com_ui_custom_header_name": "Nome do cabeçalho personalizado", @@ -591,13 +631,22 @@ "com_ui_delete_confirm": "Isso excluirá", "com_ui_delete_confirm_prompt_version_var": "Isso excluirá a versão selecionada para \"{{0}}\". Se não houver outras versões, o prompt será excluído.", "com_ui_delete_conversation": "Excluir chat?", + "com_ui_delete_mcp": "Remover MCP", + "com_ui_delete_mcp_confirm": "Você tem certeza que quer remover este servidor MCP?", + "com_ui_delete_mcp_error": "Falha ao remover servidor MCP", + "com_ui_delete_mcp_success": "Servidor MCP removido com sucesso", + "com_ui_delete_memory": "Remover Memória", "com_ui_delete_prompt": "Excluir Prompt?", "com_ui_delete_shared_link": "Excluir link compartilhado?", + "com_ui_delete_success": "Removido com sucesso", "com_ui_delete_tool": "Excluir Ferramenta", "com_ui_delete_tool_confirm": "Tem certeza de que deseja excluir esta ferramenta?", + "com_ui_deleted": "Removido", + "com_ui_deleting_file": "Removendo arquivo...", "com_ui_descending": "Desc", "com_ui_description": "Descrição", "com_ui_description_placeholder": "Opcional: Insira uma descrição para exibir para o prompt", + "com_ui_detailed": "Detalhado", "com_ui_disabling": "Desativando...", "com_ui_download": "Download", "com_ui_download_artifact": "Download artefato", @@ -612,18 +661,35 @@ "com_ui_duplication_processing": "Duplicando conversa...", "com_ui_duplication_success": "Conversa duplicada com sucesso", "com_ui_edit": "Editar", + "com_ui_edit_mcp_server": "Editar Servidor MCP", + "com_ui_edit_memory": "Editar Memória", "com_ui_empty_category": "-", "com_ui_endpoint": "Endpoint", "com_ui_endpoint_menu": "Menu endpoint LLM", "com_ui_enter": "Entrar", "com_ui_enter_api_key": "Insira a chave da API", + "com_ui_enter_key": "Inserir chave", "com_ui_enter_openapi_schema": "Insira seu esquema OpenAPI aqui", + "com_ui_enter_value": "Inserir valor", "com_ui_error": "Erro", "com_ui_error_connection": "Erro ao conectar ao servidor, tente atualizar a página.", "com_ui_error_save_admin_settings": "Houve um erro ao salvar suas configurações de admin.", + "com_ui_error_updating_preferences": "Erro ao atualizar preferências", "com_ui_examples": "Exemplos", + "com_ui_expand_chat": "Expandir Chat", "com_ui_export_convo_modal": "Exportar Modal de Conversação", + "com_ui_feedback_more": "Mais...", + "com_ui_feedback_more_information": "Fornecer feedback adicional", + "com_ui_feedback_negative": "Precisa de melhorias", + "com_ui_feedback_placeholder": "Por favor, forneça qualquer feedback adicional aqui", + "com_ui_feedback_positive": "Amei isso", + "com_ui_feedback_tag_accurate_reliable": "Preciso e confiável", + "com_ui_feedback_tag_creative_solution": "Solução Criativa", + "com_ui_feedback_tag_not_helpful": "Faltou informação útil", + "com_ui_feedback_tag_unjustified_refusal": "Recusado com razão", "com_ui_field_required": "Este campo é obrigatório", + "com_ui_file_size": "Tamanho do Arquivo", + "com_ui_files": "Arquivos", "com_ui_filter_prompts": "Filtrar prompts", "com_ui_filter_prompts_name": "Filtrar prompts por nome", "com_ui_finance": "Financiar", @@ -652,13 +718,23 @@ "com_ui_generate_backup": "Gerar códigos de backup", "com_ui_generate_qrcode": "Gerar QR Code", "com_ui_generating": "Gerando...", + "com_ui_generation_settings": "Configurações de Geração", "com_ui_global_group": "algo precisa ir aqui. estava vazio", "com_ui_go_back": "Volte", "com_ui_go_to_conversation": "Ir para a conversa", + "com_ui_good_afternoon": "Boa tarde", + "com_ui_good_evening": "Boa noite", + "com_ui_good_morning": "Bom dia", "com_ui_happy_birthday": "É meu 1º aniversário!", + "com_ui_hide_image_details": "Esconder Detalhes de Imagem", + "com_ui_hide_password": "Esconder senha", "com_ui_hide_qr": "Ocultar QR Code", "com_ui_host": "Host", + "com_ui_icon": "Ícone", "com_ui_idea": "Ideias", + "com_ui_image_created": "Imagem criada", + "com_ui_image_details": "Detalhes da Imagem", + "com_ui_image_edited": "Imagem editada", "com_ui_image_gen": "Geração de Imagem", "com_ui_import": "Importar", "com_ui_import_conversation_error": "Houve um erro ao importar suas conversas", @@ -668,6 +744,7 @@ "com_ui_include_shadcnui": "Incluir instruções de componentes shadcn/ui", "com_ui_input": "Entrada", "com_ui_instructions": "Instruções", + "com_ui_key": "Chave", "com_ui_latest_footer": "Toda IA para Todos.", "com_ui_latest_production_version": "Última versão de produção", "com_ui_latest_version": "Ultima versão", @@ -679,6 +756,28 @@ "com_ui_logo": "{{0}} Logo", "com_ui_manage": "Gerenciar", "com_ui_max_tags": "O número máximo permitido é {{0}}, usando os valores mais recentes.", + "com_ui_mcp_authenticated_success": "Servidor MCP '{{0}}' autenticado com sucesso", + "com_ui_mcp_enter_var": "Insira um valor para {{0}}", + "com_ui_mcp_initialize": "Inicializar", + "com_ui_mcp_initialized_success": "Servidor MCP '{{0}}' inicializou com sucesso", + "com_ui_mcp_server_not_found": "Servidor não encontrado.", + "com_ui_mcp_servers": "Servidores MCP", + "com_ui_mcp_update_var": "Atualizar {{0}}", + "com_ui_mcp_url": "URL do Servidor MCP", + "com_ui_medium": "Médio", + "com_ui_memories": "Memórias", + "com_ui_memories_allow_create": "Permitir criação de Memórias", + "com_ui_memories_allow_update": "Permite a atualização de Memórias", + "com_ui_memories_allow_use": "Permite a utilização de Memórias", + "com_ui_memories_filter": "Filtrar memórias...", + "com_ui_memory": "Memória", + "com_ui_memory_created": "Memória criada com sucesso", + "com_ui_memory_deleted": "Memória removida", + "com_ui_memory_deleted_items": "Memórias removidas", + "com_ui_memory_storage_full": "Armazenamento de Memória Cheio", + "com_ui_memory_updated": "Memória salva atualizada", + "com_ui_memory_updated_items": "Memórias Atualizadas", + "com_ui_memory_would_exceed": "Impossível salvar - excederia o limite por {{tokens} tokens. Remova memórias existentes para liberar espaço.", "com_ui_mention": "Mencione um endpoint, assistente ou predefinição para alternar rapidamente para ele", "com_ui_min_tags": "Não é possível remover mais valores, um mínimo de {{0}} é necessário.", "com_ui_misc": "Diversos", @@ -824,7 +923,30 @@ "com_ui_version_var": "Versão {{0}}", "com_ui_versions": "Versões", "com_ui_view_source": "Ver chat de origem", + "com_ui_web_search": "Busca na web", + "com_ui_web_search_cohere_key": "Insira a chave de API Cohere", + "com_ui_web_search_firecrawl_url": "URL da API Firecrawl (opcional)", + "com_ui_web_search_jina_key": "Insira a chave de API Jina", + "com_ui_web_search_processing": "Resultados de processamento", + "com_ui_web_search_provider": "Provedor de Buscas", + "com_ui_web_search_provider_searxng": "SearXNG", + "com_ui_web_search_provider_serper": "Serper API", + "com_ui_web_search_provider_serper_key": "Obtenha sua chave de API Serper", + "com_ui_web_search_reading": "Resultados da leitura", + "com_ui_web_search_reranker": "Reranker", + "com_ui_web_search_reranker_cohere": "Cohere", + "com_ui_web_search_reranker_cohere_key": "Obtenha sua chave de API Cohere", + "com_ui_web_search_reranker_jina": "Jina AI", + "com_ui_web_search_reranker_jina_key": "Obtenha sua chave de API Jina", + "com_ui_web_search_scraper": "Scraper", + "com_ui_web_search_scraper_firecrawl": "Firecrawl API", + "com_ui_web_search_scraper_firecrawl_key": "Obtenha sua chave de API Firecrawl", + "com_ui_web_search_searxng_api_key": "Insira sua Chave de API SearXNG (opcional)", + "com_ui_web_searching": "Procurando na web", + "com_ui_web_searching_again": "Procurando na web novamente", + "com_ui_weekend_morning": "Boa semana", "com_ui_write": "Escrevendo", + "com_ui_x_selected": "{{0}} selecionado", "com_ui_yes": "Sim", "com_ui_zoom": "Zoom", "com_user_message": "Você" diff --git a/client/src/locales/pt-PT/translation.json b/client/src/locales/pt-PT/translation.json index a8f02bbc8..ad9116bb5 100644 --- a/client/src/locales/pt-PT/translation.json +++ b/client/src/locales/pt-PT/translation.json @@ -14,9 +14,14 @@ "com_agents_file_search_disabled": "O Agente deve ser criado antes carregar ficheiros para Pesquisar.", "com_agents_file_search_info": "Quando ativo, os agentes serão informados dos nomes de ficheiros listados abaixo, permitindo aos mesmos a extração de contexto relevante.", "com_agents_instructions_placeholder": "As instruções do sistema que o agente usa", + "com_agents_mcp_description_placeholder": "Em poucas palavras explica o que faz", + "com_agents_mcp_icon_size": "Tamanho mínimo é 128 x 128 px", + "com_agents_mcp_name_placeholder": "Ferramenta Costumizada", + "com_agents_mcps_disabled": "Precisas de criar um agente antes de adicionar MCPs.", "com_agents_missing_provider_model": "Por favor, escolhe um provedor e modelo antes de criar um agente.", "com_agents_name_placeholder": "Opcional: O nome do agente", "com_agents_no_access": "Não tens permissões para editar este agente.", + "com_agents_no_agent_id_error": "Nenhum ID de Agente Encontrado. Por favor, garante que tens um agente criado.", "com_agents_not_available": "Agente não disponível.", "com_agents_search_name": "Pesquisar agentes por nome", "com_agents_update_error": "Houve um erro ao atualizar seu agente.", @@ -181,6 +186,7 @@ "com_endpoint_default_empty": "padrão: vazio", "com_endpoint_default_with_num": "padrão: {{0}}", "com_endpoint_deprecated": "Deprecado", + "com_endpoint_disable_streaming_label": "Desligar Streaming", "com_endpoint_examples": "Presets", "com_endpoint_export": "Exportar", "com_endpoint_export_share": "Exportar/Compartilhar", @@ -258,6 +264,7 @@ "com_endpoint_top_k": "Top K", "com_endpoint_top_p": "Top P", "com_endpoint_use_active_assistant": "Usar Assistente Ativo", + "com_endpoint_use_responses_api": "Usar Respostas da API", "com_error_expired_user_key": "A chave fornecida para {{0}} expirou em {{1}}. Por favor, forneça uma nova chave e tente novamente.", "com_error_files_dupe": "Ficheiro duplicado detectado", "com_error_files_empty": "Ficheiros vazios não são permitidos.", @@ -276,6 +283,7 @@ "com_files_number_selected": "{{0}} de {{1}} arquivo(s) selecionado(s)", "com_generated_files": "Ficheiros gerados:", "com_hide_examples": "Ocultar Exemplos", + "com_info_heic_converting": "Converter imagem HEIC para JPEG...", "com_nav_2fa": "Autenticação de dois fatores (2FA)", "com_nav_account_settings": "Configurações da Conta", "com_nav_always_make_prod": "Sempre tornar novas versões produção", @@ -293,16 +301,21 @@ "com_nav_auto_transcribe_audio": "Transcrever áudio automaticamente", "com_nav_automatic_playback": "Reprodução Automática da Última Mensagem", "com_nav_balance": "Equilíbrio", + "com_nav_balance_auto_refill_disabled": "Carregamento automático está desligado.", + "com_nav_balance_auto_refill_error": "Erro nas configurações de carregamento automático.", + "com_nav_balance_auto_refill_settings": "Configurações de Carregamento Automático", "com_nav_balance_day": "dia", "com_nav_balance_days": "dias", "com_nav_balance_every": "Todos", "com_nav_balance_hour": "horas", "com_nav_balance_hours": "horas", "com_nav_balance_interval": "Intervalo:", + "com_nav_balance_last_refill": "Último Carregamento:", "com_nav_balance_minute": "minuto", "com_nav_balance_minutes": "minutos", "com_nav_balance_month": "mês", "com_nav_balance_months": "meses", + "com_nav_balance_next_refill": "Próximo Carregamento:", "com_nav_balance_second": "segundo", "com_nav_balance_seconds": "segundos", "com_nav_balance_week": "semana", @@ -364,9 +377,13 @@ "com_nav_info_save_draft": "Quando habilitado, o texto e os anexos que você inserir no formulário de chat serão salvos automaticamente localmente como rascunhos. Esses rascunhos estarão disponíveis mesmo se você recarregar a página ou mudar para uma conversa diferente. Os rascunhos são armazenados localmente no seu dispositivo e são excluídos uma vez que a mensagem é enviada.", "com_nav_info_user_name_display": "Quando habilitado, o nome de usuário do remetente será mostrado acima de cada mensagem que você enviar. Quando desabilitado, você verá apenas \"Você\" acima de suas mensagens.", "com_nav_lang_arabic": "العربية", + "com_nav_lang_armenian": "Armênio", "com_nav_lang_auto": "Detecção automática", "com_nav_lang_brazilian_portuguese": "Português Brasileiro", + "com_nav_lang_catalan": "Catalão", "com_nav_lang_chinese": "中文", + "com_nav_lang_czech": "Checo", + "com_nav_lang_danish": "Dinamarquês", "com_nav_lang_dutch": "Nederlands", "com_nav_lang_english": "English", "com_nav_lang_estonian": "Eesti keel", @@ -375,10 +392,12 @@ "com_nav_lang_georgian": "ქართული", "com_nav_lang_german": "Deutsch", "com_nav_lang_hebrew": "עברית", + "com_nav_lang_hungarian": "Húngaro", "com_nav_lang_indonesia": "Indonesia", "com_nav_lang_italian": "Italiano", "com_nav_lang_japanese": "日本語", "com_nav_lang_korean": "한국어", + "com_nav_lang_persian": "Persa", "com_nav_lang_polish": "Polski", "com_nav_lang_portuguese": "Português", "com_nav_lang_russian": "Русский", @@ -393,6 +412,8 @@ "com_nav_log_out": "Sair", "com_nav_long_audio_warning": "Textos mais longos levarão mais tempo para processar.", "com_nav_maximize_chat_space": "Maximizar espaço de conversa", + "com_nav_mcp_configure_server": "Configurar {{0}}", + "com_nav_mcp_status_connecting": "{{0}} - A ligar", "com_nav_modular_chat": "Habilitar troca de Endpoints no meio da conversa", "com_nav_my_files": "Meus Arquivos", "com_nav_not_supported": "Não Suportado", @@ -411,9 +432,12 @@ "com_nav_search_placeholder": "Buscar mensagens", "com_nav_send_message": "Enviar mensagem", "com_nav_setting_account": "Conta", + "com_nav_setting_balance": "Saldo", "com_nav_setting_chat": "Chat", "com_nav_setting_data": "Controles de dados", "com_nav_setting_general": "Geral", + "com_nav_setting_mcp": "Configurações MCP", + "com_nav_setting_personalization": "Personalização", "com_nav_setting_speech": "Fala", "com_nav_settings": "Configurações", "com_nav_shared_links": "Links compartilhados", @@ -447,6 +471,8 @@ "com_sidepanel_hide_panel": "Ocultar Painel", "com_sidepanel_manage_files": "Gerenciar Arquivos", "com_sidepanel_parameters": "Parâmetros", + "com_sources_image_alt": "Resultado da pesquisa de imagem", + "com_sources_more_sources": "+{{count}} fontes", "com_sources_tab_all": "Todos", "com_sources_tab_images": "Imagens", "com_sources_tab_news": "Notícias", @@ -462,7 +488,10 @@ "com_ui_2fa_verified": "Autenticação de dois fatores verificado com sucesso", "com_ui_accept": "Eu aceito", "com_ui_action_button": "Botão de Acção", + "com_ui_active": "Ativo", "com_ui_add": "Adicionar", + "com_ui_add_mcp": "Adicionar MCP", + "com_ui_add_mcp_server": "Adicionar Servidor MCP", "com_ui_add_model_preset": "Adicionar um modelo ou predefinição para uma resposta adicional", "com_ui_add_multi_conversation": "Adicionar conversação múltiplca", "com_ui_adding_details": "A adicionar detalhes", @@ -483,9 +512,12 @@ "com_ui_agent_var": "{{0}} agente", "com_ui_agent_version": "Versão", "com_ui_agent_version_active": "Versão ativa", + "com_ui_agent_version_empty": "Sem versões disponíveis", "com_ui_agent_version_history": "Histórico de versões", + "com_ui_agent_version_no_date": "Data não disponível", "com_ui_agent_version_restore": "Restaurar", "com_ui_agent_version_title": "Versão {{versionNumber}}", + "com_ui_agent_version_unknown_date": "Data desconhecida", "com_ui_agents": "Agentes", "com_ui_agents_allow_create": "Permitir a criação de Agentes", "com_ui_agents_allow_share_global": "Permitir a partilha de Agentes com todos os utilizadores", @@ -516,8 +548,10 @@ "com_ui_attachment": "Anexo", "com_ui_auth_type": "Tipo de Autenticação", "com_ui_auth_url": "Endereço de Autorização", + "com_ui_authenticate": "Autenticar", "com_ui_authentication": "Autenticação", "com_ui_authentication_type": "Tipo de Autenticação", + "com_ui_auto": "Automático", "com_ui_avatar": "Avatar", "com_ui_azure": "Azure", "com_ui_back_to_chat": "Voltar ao Chat", @@ -559,6 +593,7 @@ "com_ui_client_secret": "Client Secret", "com_ui_close": "Fechar", "com_ui_close_menu": "Fechar Menu", + "com_ui_close_window": "Fechar Janela", "com_ui_code": "Código", "com_ui_collapse_chat": "Colapsar Conversa", "com_ui_command_placeholder": "Opcional: Insira um comando para o prompt ou o nome será usado.", @@ -567,8 +602,10 @@ "com_ui_confirm_action": "Confirmar Ação", "com_ui_confirm_admin_use_change": "Mudar esta configuração irá bloquear acessos para administradores, você inclusivé. Tem a certeza que pretende avançar?", "com_ui_confirm_change": "Confirmar alteração", + "com_ui_connecting": "A ligar", "com_ui_context": "Contexto", "com_ui_continue": "Continuar", + "com_ui_continue_oauth": "Continuar com OAuth", "com_ui_controls": "Controles", "com_ui_copied": "Copiado!", "com_ui_copied_to_clipboard": "Copiado para a área de transferência", @@ -577,6 +614,7 @@ "com_ui_copy_to_clipboard": "Copiar para a área de transferência", "com_ui_create": "Criar", "com_ui_create_link": "Criar link", + "com_ui_create_memory": "Criar memória", "com_ui_create_prompt": "Criar Prompt", "com_ui_currently_production": "Atualmente em produção", "com_ui_custom": "Costumizar", @@ -610,6 +648,7 @@ "com_ui_delete_confirm": "Isso excluirá", "com_ui_delete_confirm_prompt_version_var": "Isso excluirá a versão selecionada para \"{{0}}\". Se não houver outras versões, o prompt será excluído.", "com_ui_delete_conversation": "Excluir chat?", + "com_ui_delete_mcp": "Apagar MCP", "com_ui_delete_prompt": "Excluir Prompt?", "com_ui_delete_shared_link": "Apagar endereço partilhado?", "com_ui_delete_tool": "Excluir Ferramenta", diff --git a/client/src/locales/zh-Hans/translation.json b/client/src/locales/zh-Hans/translation.json index 9362e5182..0b5832f7a 100644 --- a/client/src/locales/zh-Hans/translation.json +++ b/client/src/locales/zh-Hans/translation.json @@ -13,7 +13,7 @@ "com_agents_enable_file_search": "启用文件搜索", "com_agents_file_context": "文件上下文(OCR)", "com_agents_file_context_disabled": "必须先创建智能体,才能上传文件用于文件上下文。", - "com_agents_file_context_info": "作为”上下文“上传的文件会通过 OCR 处理以提取文本,然后将其添加到代理的指令中。这非常适合文档、带有文本的图像或 PDF 文件等需要文件完整文本内容的场景。", + "com_agents_file_context_info": "作为 ”上下文“ 上传的文件会通过 OCR 处理以提取文本,然后将其添加到智能体的指令中。这非常适合文档、带有文本的图片或 PDF 文件等需要文件完整文本内容的场景。", "com_agents_file_search_disabled": "必须先创建智能体,才能上传文件用于文件搜索。", "com_agents_file_search_info": "启用后,系统会告知代理以下列出的具体文件名,使其能够从这些文件中检索相关内容。", "com_agents_instructions_placeholder": "智能体使用的系统指令", @@ -143,6 +143,7 @@ "com_auth_username_min_length": "用户名至少 2 个字符", "com_auth_verify_your_identity": "验证您的身份", "com_auth_welcome_back": "欢迎", + "com_citation_more_details": "有关 {{label}} 的更多详情", "com_citation_source": "来源", "com_click_to_download": "(点击此处下载)", "com_download_expired": "下载已过期", @@ -208,6 +209,7 @@ "com_endpoint_google_maxoutputtokens": "响应中可以生成的最大词元数。指定较低的值以获得较短的响应,指定较高的值以获得较长的响应。注意:模型可能会在达到此最大值之前停止。", "com_endpoint_google_temp": "值越高表示输出越随机,值越低表示输出越确定。建议不要同时改变此值和 Top-p。", "com_endpoint_google_thinking": "启用或禁用推理。此设置仅支持某些模型(2.5 系列)。对于更老的模型,此设置可能没有效果。", + "com_endpoint_google_thinking_budget": "指导模型使用的思考词元数量。实际数量可能会超过或低于该值,具体取决于提示词。\\n\\n此设置仅支持某些模型(2.5 系列)。Gemini 2.5 Pro 支持 128-32768 个词元。Gemini 2.5 Flash 支持 0-24576 个词元。Gemini 2.5 Flash Lite 支持 512-24576 个词元。\\n\\n留空或设置为 “-1” 以让模型自动决定何时以及思考多少。默认情况下,Gemini 2.5 Flash Lite 不进行思考。", "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_google_use_search_grounding": "使用 Google 的基础搜索特性,通过实时网络搜索结果优化响应。这使得模型能够访问当前信息,提供更准确、更及时的答案。", @@ -393,7 +395,7 @@ "com_nav_hide_panel": "隐藏最右侧面板", "com_nav_info_balance": "余额显示您剩余的词元额度。词元额度对应一定的货币价值(例如:1000 额度 = 0.001 美元)。", "com_nav_info_code_artifacts": "启用在对话旁显示实验性代码工件", - "com_nav_info_code_artifacts_agent": "使该代理能够使用代码附件。默认情况下,除非启用“自定义提示模式”,否则会添加与附件使用相关的额外说明。", + "com_nav_info_code_artifacts_agent": "使该智能体能够使用代码工件。默认情况下,除非启用“自定义提示词模式”,否则会添加与 Artifacts 使用相关的额外说明。", "com_nav_info_custom_prompt_mode": "启用后,默认的 Artifacts 系统提示词将不会包含在内。在此模式下,必须手动提供所有生成工件的指令。", "com_nav_info_enter_to_send": "启用后,按下 `ENTER` 将发送您的消息。禁用后,按下 `ENTER` 将添加新行,您需要按下 `CTRL + ENTER` / `⌘ + ENTER` 来发送消息。", "com_nav_info_fork_change_default": "`仅可见消息` 仅包含到所选消息的直接路径,`包含相关分支` 添加路径上的分支,`包含所有目标` 包括所有连接的消息和分支。", @@ -504,7 +506,6 @@ "com_sidepanel_hide_panel": "隐藏侧边栏", "com_sidepanel_manage_files": "管理文件", "com_sidepanel_mcp_no_servers_with_vars": "没有支持可配置变量的 MCP 服务器。", - "com_sidepanel_mcp_variables_for": "MCP 变量:{{0}}", "com_sidepanel_parameters": "参数", "com_sources_image_alt": "搜索结果图片", "com_sources_more_sources": "+{{count}} 个来源", @@ -530,6 +531,7 @@ "com_ui_add_mcp_server": "添加 MCP 服务器", "com_ui_add_model_preset": "添加一个模型或预设以获得额外的回复", "com_ui_add_multi_conversation": "添加多个对话", + "com_ui_adding_details": "添加细节", "com_ui_admin": "管理", "com_ui_admin_access_warning": "禁用管理员对此特性的访问可能会导致界面出现异常,需要刷新页面。如果保存此设置,唯一的恢复方式是通过 librechat.yaml 配置文件中的界面设置进行修改,这将影响所有角色。", "com_ui_admin_settings": "管理员设置", @@ -846,13 +848,10 @@ "com_ui_manage": "管理", "com_ui_max_tags": "最多允许 {{0}} 个,用最新值。", "com_ui_mcp_authenticated_success": "MCP 服务器 “{{0}}” 认证成功", - "com_ui_mcp_dialog_desc": "请在下方输入必要的信息。", "com_ui_mcp_enter_var": "输入值:{{0}}", "com_ui_mcp_init_failed": "初始化 MCP 服务器失败", "com_ui_mcp_initialize": "初始化", "com_ui_mcp_initialized_success": "MCP 服务器 “{{0}}” 初始化成功", - "com_ui_mcp_not_authenticated": "{{0}} 未认证(需要 OAuth)", - "com_ui_mcp_not_initialized": "{{0}} 未初始化", "com_ui_mcp_oauth_cancelled": "{{0}} OAuth 登录已取消", "com_ui_mcp_oauth_timeout": "{{0}} OAuth 登录超时", "com_ui_mcp_server_not_found": "未找到服务器。", @@ -912,7 +911,6 @@ "com_ui_oauth_error_missing_code": "缺少身份验证代码。请重试。", "com_ui_oauth_error_missing_state": "缺少状态参数。请重试。", "com_ui_oauth_error_title": "认证失败", - "com_ui_oauth_flow_desc": "在新窗口中完成 OAuth 流程,然后返回此处。", "com_ui_oauth_success_description": "您的身份验证成功。此窗口将在以下时间后关闭:", "com_ui_oauth_success_title": "认证成功", "com_ui_of": "/", @@ -1039,6 +1037,7 @@ "com_ui_tool_more_info": "有关此工具的更多信息", "com_ui_tools": "工具", "com_ui_travel": "旅行", + "com_ui_trust_app": "我信任此应用", "com_ui_unarchive": "取消归档", "com_ui_unarchive_error": "取消归档对话失败", "com_ui_unknown": "未知", diff --git a/client/src/locales/zh-Hant/translation.json b/client/src/locales/zh-Hant/translation.json index e7040ea9a..52cbeb644 100644 --- a/client/src/locales/zh-Hant/translation.json +++ b/client/src/locales/zh-Hant/translation.json @@ -9,9 +9,13 @@ "com_agents_create_error": "建立您的代理時發生錯誤。", "com_agents_description_placeholder": "選填:在此描述您的代理程式", "com_agents_enable_file_search": "啟用檔案搜尋", + "com_agents_file_context": "文件內容 (OCR)", "com_agents_file_search_disabled": "必須先建立代理才能上傳檔案進行檔案搜尋。", "com_agents_file_search_info": "啟用後,代理將會被告知以下列出的確切檔案名稱,使其能夠從這些檔案中擷取相關內容。", "com_agents_instructions_placeholder": "代理程式使用的系統指令", + "com_agents_mcp_description_placeholder": "簡要解釋它的作用", + "com_agents_mcp_icon_size": "最小尺寸 128 x 128 px", + "com_agents_mcp_name_placeholder": "自定義工具", "com_agents_missing_provider_model": "請在建立代理前選擇供應商和模型。", "com_agents_name_placeholder": "選填:代理人的名稱", "com_agents_no_access": "您沒有權限編輯此助理", @@ -59,6 +63,7 @@ "com_assistants_update_error": "更新您的助理時發生錯誤。", "com_assistants_update_success": "更新成功", "com_auth_already_have_account": "已經有帳號了?", + "com_auth_apple_login": "Apple登入", "com_auth_back_to_login": "返回登入", "com_auth_click": "點選", "com_auth_click_here": "點選這裡", @@ -121,6 +126,7 @@ "com_auth_username_max_length": "使用者名稱長度必須少於 20 個字元", "com_auth_username_min_length": "使用者名稱長度必須至少有 2 個字元", "com_auth_welcome_back": "歡迎回來", + "com_citation_source": "來源", "com_click_to_download": "(點選此處下載)", "com_download_expired": "下載已過期", "com_download_expires": "(點擊此處下載 - {{0}} 後過期)", @@ -134,6 +140,7 @@ "com_endpoint_anthropic_temp": "範圍從 0 到 1。對於分析/多選題,使用接近 0 的溫度,對於創意和生成式任務,使用接近 1 的溫度。我們建議修改這個或 Top P,但不建議兩者都修改。", "com_endpoint_anthropic_topk": "Top-k 改變模型選擇輸出 token 的方式。Top-k 為 1 表示所選 token 在模型詞彙表中所有 token 中最可能(也稱為貪婪解碼),而 Top-k 為 3 表示下一個 token 從最可能的 3 個 token 中選擇(使用溫度)。", "com_endpoint_anthropic_topp": "Top-p 改變模型選擇輸出 token 的方式。從最可能的 K(見 topK 參數)開始選擇 token,直到它們的機率之和達到 top-p 值。", + "com_endpoint_anthropic_use_web_search": "啟用 Anthropic 內建的網路搜尋功能,使模型能夠在網路上搜尋最新資訊,從而提供更準確、即時的回應。", "com_endpoint_assistant": "助理", "com_endpoint_assistant_model": "AI 模型", "com_endpoint_assistant_placeholder": "請從右側面板選擇一位助理", @@ -178,6 +185,7 @@ "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_google_use_search_grounding": "使用 Google 的基礎搜尋功能,以即時的網路搜尋結果增強回應。這使模型能夠取得最新資訊,並提供更準確、最新的答案。", "com_endpoint_instructions_assistants": "覆寫提示指令", "com_endpoint_instructions_assistants_placeholder": "覆寫助理的提示指令。這對於在每次執行時修改行為很有用。", "com_endpoint_max_output_tokens": "最大輸出 token 數", @@ -199,6 +207,7 @@ "com_endpoint_openai_stop": "最多 4 個序列,API 將在生成更多 token 時停止。", "com_endpoint_openai_temp": "較高的值表示更隨機,而較低的值表示更集中和確定。我們建議修改這個或 Top P,但不建議兩者都修改。", "com_endpoint_openai_topp": "與溫度取樣的替代方法,稱為核心取樣,其中模型考慮 top_p 機率質量的 token 結果。所以 0.1 表示只考慮佔 top 10% 機率質量的 token。我們建議修改這個或溫度,但不建議兩者都修改。", + "com_endpoint_openai_use_web_search": "啟用 OpenAI 內建的網路搜尋功能,使模型能夠在網路上搜尋最新資訊,從而提供更準確、即時的回應。", "com_endpoint_output": "輸出", "com_endpoint_plug_image_detail": "影像詳細資訊", "com_endpoint_plug_resend_files": "重新傳送檔案", @@ -245,7 +254,7 @@ "com_error_files_upload": "上傳檔案時發生錯誤", "com_error_files_upload_canceled": "檔案上傳請求已取消。注意:檔案上傳可能仍在處理中,需要手動刪除。", "com_error_files_validation": "驗證檔案時發生錯誤。", - "com_error_input_length": "最新訊息的字元數過長,已超過字元限制({{0}})。請縮短您的訊息內容、在對話參數中調整最大上下文大小,或是建立分支對話以繼續。", + "com_error_input_length": "最新訊息的 Token 數量過多,已超出 Token 限制,或是您設定的 Token 限制參數有誤,影響了上下文視窗。更多資訊:{{0}}。請縮短您的訊息、調整對話參數中的最大上下文大小,或分叉 (fork) 此對話以繼續。", "com_error_invalid_user_key": "提供的金鑰無效。請提供有效的金鑰並重試。", "com_error_moderation": "您所提交的內容似乎被我們的內容審查系統標記為不符合社群準則。我們無法就此特定主題繼續進行討論。如果您有任何其他問題或想要探討的主題,請編輯您的訊息或開啟新的對話。", "com_error_no_base_url": "找不到基礎 URL。請提供一個基礎 URL 後再試一次。", @@ -612,7 +621,6 @@ "com_ui_logo": "{{0}} 標誌", "com_ui_manage": "管理", "com_ui_max_tags": "允許的最大數量為 {{0}},已使用最新值。", - "com_ui_mcp_dialog_desc": "請在下方輸入必要資訊。", "com_ui_mcp_enter_var": "請輸入 {{0}} 的值", "com_ui_mcp_server_not_found": "找不到伺服器。", "com_ui_mcp_url": "MCP 伺服器", @@ -623,12 +631,18 @@ "com_ui_memories_allow_update": "允許更新記憶", "com_ui_memories_allow_use": "允許使用記憶", "com_ui_memories_filter": "篩選記憶...", + "com_ui_memory": "記憶", + "com_ui_memory_already_exceeded": "記憶體儲存已滿——已超過 {{tokens}} 個 token。在新增記憶前請先刪除現有記憶。", "com_ui_memory_created": "記憶建立成功", "com_ui_memory_deleted": "記憶已刪除", "com_ui_memory_deleted_items": "已刪除的記憶", + "com_ui_memory_error": "記憶錯誤", "com_ui_memory_key_exists": "已存在具有此鍵值的記憶。請使用不同的鍵值。", + "com_ui_memory_key_validation": "記憶鍵只能包含小寫字母與底線。", + "com_ui_memory_storage_full": "記憶儲存空間已滿", "com_ui_memory_updated": "已更新儲存的記憶", "com_ui_memory_updated_items": "已更新的記憶", + "com_ui_memory_would_exceed": "無法儲存——會超出限制 {{tokens}} 個 token。請刪除現有記憶以釋放空間。", "com_ui_mention": "提及端點、助理或預設設定以快速切換", "com_ui_min_tags": "無法再移除更多值,至少需要 {{0}} 個。", "com_ui_model": "模型", @@ -741,6 +755,25 @@ "com_ui_variables_info": "在文字中使用雙大括號來建立變數,例如 `{{example variable}}`,以便在使用提示時填入。", "com_ui_version_var": "版本 {{0}}", "com_ui_versions": "版本", + "com_ui_web_search": "網路搜尋", + "com_ui_web_search_cohere_key": "輸入 Cohere API Key", + "com_ui_web_search_firecrawl_url": "Firecrawl API URL(可選)", + "com_ui_web_search_jina_key": "輸入 Jina API Key", + "com_ui_web_search_processing": "正在處理結果", + "com_ui_web_search_provider_searxng": "SearXNG", + "com_ui_web_search_provider_serper": "Serper API", + "com_ui_web_search_provider_serper_key": "取得您的 Serper API key", + "com_ui_web_search_reading": "正在讀取結果", + "com_ui_web_search_reranker_cohere": "Cohere", + "com_ui_web_search_reranker_cohere_key": "取得您的 Cohere API key", + "com_ui_web_search_reranker_jina": "Jina AI", + "com_ui_web_search_reranker_jina_key": "取得您的 Jina API key", + "com_ui_web_search_scraper_firecrawl": "Firecrawl API", + "com_ui_web_search_scraper_firecrawl_key": "取得您的 Firecrawl API key", + "com_ui_web_search_searxng_api_key": "輸入 SearXNG API Key (可選)", + "com_ui_web_search_searxng_instance_url": "SearXNG Instance URL", + "com_ui_web_searching": "正在搜尋網路", + "com_ui_web_searching_again": "正在重新搜尋網路", "com_ui_yes": "是", "com_ui_zoom": "縮放", "com_user_message": "您" From 056172f007565d1ec6539f1fa717fc2cd1b243ad Mon Sep 17 00:00:00 2001 From: wartek69 Date: Thu, 31 Jul 2025 13:24:49 +0200 Subject: [PATCH 040/224] =?UTF-8?q?=F0=9F=94=92=20feat:=20MCP=20OAuth=20Co?= =?UTF-8?q?nfig=20for=20Metadata=20Parameters=20(#8691)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(mcp): add default metadata for pre-configured oauth * removed lingering comment * added configurable options & jest unit tests * Update handler.test.ts * Update handler.ts --------- Co-authored-by: Alex Co-authored-by: Danny Avila --- packages/api/src/mcp/oauth/handler.test.ts | 190 +++++++++++++++++++++ packages/api/src/mcp/oauth/handler.ts | 17 +- packages/data-provider/src/mcp.ts | 8 + 3 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/mcp/oauth/handler.test.ts diff --git a/packages/api/src/mcp/oauth/handler.test.ts b/packages/api/src/mcp/oauth/handler.test.ts new file mode 100644 index 000000000..c65ffc151 --- /dev/null +++ b/packages/api/src/mcp/oauth/handler.test.ts @@ -0,0 +1,190 @@ +import { MCPOAuthHandler } from './handler'; +import type { MCPOptions } from 'librechat-data-provider'; + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('@modelcontextprotocol/sdk/client/auth.js', () => ({ + startAuthorization: jest.fn(), +})); + +import { startAuthorization } from '@modelcontextprotocol/sdk/client/auth.js'; + +const mockStartAuthorization = startAuthorization as jest.MockedFunction; + +describe('MCPOAuthHandler - Configurable OAuth Metadata', () => { + const mockServerName = 'test-server'; + const mockServerUrl = 'https://example.com/mcp'; + const mockUserId = 'user-123'; + + beforeEach(() => { + jest.clearAllMocks(); + process.env.DOMAIN_SERVER = 'http://localhost:3080'; + + // Mock startAuthorization to return a successful response + mockStartAuthorization.mockResolvedValue({ + authorizationUrl: new URL('https://auth.example.com/oauth/authorize?client_id=test'), + codeVerifier: 'test-code-verifier', + }); + }); + + afterEach(() => { + delete process.env.DOMAIN_SERVER; + }); + + describe('Pre-configured OAuth Metadata Fields', () => { + const baseConfig: MCPOptions['oauth'] = { + authorization_url: 'https://auth.example.com/oauth/authorize', + token_url: 'https://auth.example.com/oauth/token', + client_id: 'test-client-id', + client_secret: 'test-client-secret', + }; + + it('should use default values when OAuth metadata fields are not configured', async () => { + await MCPOAuthHandler.initiateOAuthFlow( + mockServerName, + mockServerUrl, + mockUserId, + baseConfig, + ); + + expect(mockStartAuthorization).toHaveBeenCalledWith( + mockServerUrl, + expect.objectContaining({ + metadata: expect.objectContaining({ + grant_types_supported: ['authorization_code', 'refresh_token'], + token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'], + response_types_supported: ['code'], + code_challenge_methods_supported: ['S256', 'plain'], + }), + }), + ); + }); + + it('should use custom grant_types_supported when provided', async () => { + const config = { + ...baseConfig, + grant_types_supported: ['authorization_code'], + }; + + await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config); + + expect(mockStartAuthorization).toHaveBeenCalledWith( + mockServerUrl, + expect.objectContaining({ + metadata: expect.objectContaining({ + grant_types_supported: ['authorization_code'], + }), + }), + ); + }); + + it('should use custom token_endpoint_auth_methods_supported when provided', async () => { + const config = { + ...baseConfig, + token_endpoint_auth_methods_supported: ['client_secret_post'], + }; + + await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config); + + expect(mockStartAuthorization).toHaveBeenCalledWith( + mockServerUrl, + expect.objectContaining({ + metadata: expect.objectContaining({ + token_endpoint_auth_methods_supported: ['client_secret_post'], + }), + }), + ); + }); + + it('should use custom response_types_supported when provided', async () => { + const config = { + ...baseConfig, + response_types_supported: ['code', 'token'], + }; + + await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config); + + expect(mockStartAuthorization).toHaveBeenCalledWith( + mockServerUrl, + expect.objectContaining({ + metadata: expect.objectContaining({ + response_types_supported: ['code', 'token'], + }), + }), + ); + }); + + it('should use custom code_challenge_methods_supported when provided', async () => { + const config = { + ...baseConfig, + code_challenge_methods_supported: ['S256'], + }; + + await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config); + + expect(mockStartAuthorization).toHaveBeenCalledWith( + mockServerUrl, + expect.objectContaining({ + metadata: expect.objectContaining({ + code_challenge_methods_supported: ['S256'], + }), + }), + ); + }); + + it('should use all custom OAuth metadata fields when provided together', async () => { + const config = { + ...baseConfig, + grant_types_supported: ['authorization_code', 'client_credentials'], + token_endpoint_auth_methods_supported: ['none'], + response_types_supported: ['code', 'token', 'id_token'], + code_challenge_methods_supported: ['S256'], + }; + + await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config); + + expect(mockStartAuthorization).toHaveBeenCalledWith( + mockServerUrl, + expect.objectContaining({ + metadata: expect.objectContaining({ + grant_types_supported: ['authorization_code', 'client_credentials'], + token_endpoint_auth_methods_supported: ['none'], + response_types_supported: ['code', 'token', 'id_token'], + code_challenge_methods_supported: ['S256'], + }), + }), + ); + }); + + it('should handle empty arrays as valid custom values', async () => { + const config = { + ...baseConfig, + grant_types_supported: [], + token_endpoint_auth_methods_supported: [], + response_types_supported: [], + code_challenge_methods_supported: [], + }; + + await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config); + + expect(mockStartAuthorization).toHaveBeenCalledWith( + mockServerUrl, + expect.objectContaining({ + metadata: expect.objectContaining({ + grant_types_supported: [], + token_endpoint_auth_methods_supported: [], + response_types_supported: [], + code_challenge_methods_supported: [], + }), + }), + ); + }); + }); +}); diff --git a/packages/api/src/mcp/oauth/handler.ts b/packages/api/src/mcp/oauth/handler.ts index 1c776739f..739a5c651 100644 --- a/packages/api/src/mcp/oauth/handler.ts +++ b/packages/api/src/mcp/oauth/handler.ts @@ -181,9 +181,22 @@ export class MCPOAuthHandler { authorization_endpoint: config.authorization_url, token_endpoint: config.token_url, issuer: serverUrl, - scopes_supported: config.scope?.split(' '), + scopes_supported: config.scope?.split(' ') ?? [], + grant_types_supported: config?.grant_types_supported ?? [ + 'authorization_code', + 'refresh_token', + ], + token_endpoint_auth_methods_supported: config?.token_endpoint_auth_methods_supported ?? [ + 'client_secret_basic', + 'client_secret_post', + ], + response_types_supported: config?.response_types_supported ?? ['code'], + code_challenge_methods_supported: config?.code_challenge_methods_supported ?? [ + 'S256', + 'plain', + ], }; - + logger.debug(`[MCPOAuth] metadata for "${serverName}": ${JSON.stringify(metadata)}`); const clientInfo: OAuthClientInformation = { client_id: config.client_id, client_secret: config.client_secret, diff --git a/packages/data-provider/src/mcp.ts b/packages/data-provider/src/mcp.ts index 2b85449a0..b244f7fe5 100644 --- a/packages/data-provider/src/mcp.ts +++ b/packages/data-provider/src/mcp.ts @@ -36,6 +36,14 @@ const BaseOptionsSchema = z.object({ redirect_uri: z.string().url().optional(), /** Token exchange method */ token_exchange_method: z.nativeEnum(TokenExchangeMethodEnum).optional(), + /** Supported grant types (defaults to ['authorization_code', 'refresh_token']) */ + grant_types_supported: z.array(z.string()).optional(), + /** Supported token endpoint authentication methods (defaults to ['client_secret_basic', 'client_secret_post']) */ + token_endpoint_auth_methods_supported: z.array(z.string()).optional(), + /** Supported response types (defaults to ['code']) */ + response_types_supported: z.array(z.string()).optional(), + /** Supported code challenge methods (defaults to ['S256', 'plain']) */ + code_challenge_methods_supported: z.array(z.string()).optional(), }) .optional(), customUserVars: z From e192c99c7d448e29d61c4727d1bb7c71e61a7a45 Mon Sep 17 00:00:00 2001 From: William Kim Date: Thu, 31 Jul 2025 20:28:33 +0900 Subject: [PATCH 041/224] =?UTF-8?q?=F0=9F=94=A7=20fix:=20Apply=20Convo=20E?= =?UTF-8?q?xport=20filename=20sanitization=20at=20export,=20not=20input=20?= =?UTF-8?q?(#8779)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Woosub Kim --- client/src/components/Nav/ExportConversation/ExportModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/Nav/ExportConversation/ExportModal.tsx b/client/src/components/Nav/ExportConversation/ExportModal.tsx index 85a1f9be3..642b5bbc8 100644 --- a/client/src/components/Nav/ExportConversation/ExportModal.tsx +++ b/client/src/components/Nav/ExportConversation/ExportModal.tsx @@ -72,7 +72,7 @@ export default function ExportModal({ const { exportConversation } = useExportConversation({ conversation, - filename, + filename: filenamify(filename), type, includeOptions, exportBranches, @@ -95,7 +95,7 @@ export default function ExportModal({ setFileName(filenamify(e.target.value || ''))} + onChange={(e) => setFileName(e.target.value || '')} placeholder={localize('com_nav_export_filename_placeholder')} />
From f1c6e4d55ee54c82b65bf239bef280c603cf3e9a Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Fri, 1 Aug 2025 11:47:00 -0700 Subject: [PATCH 042/224] =?UTF-8?q?=F0=9F=A7=AA=20ci:=20Unit=20Tests=20for?= =?UTF-8?q?=20MCP=20Routes=20(#8803)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: remove unreachable if checks from mcp routes * test: add tests for mcp routes --- api/server/routes/__tests__/mcp.spec.js | 1259 +++++++++++++++++++++++ api/server/routes/mcp.js | 8 +- 2 files changed, 1260 insertions(+), 7 deletions(-) create mode 100644 api/server/routes/__tests__/mcp.spec.js diff --git a/api/server/routes/__tests__/mcp.spec.js b/api/server/routes/__tests__/mcp.spec.js new file mode 100644 index 000000000..272b9f723 --- /dev/null +++ b/api/server/routes/__tests__/mcp.spec.js @@ -0,0 +1,1259 @@ +const express = require('express'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); + +jest.mock('@librechat/api', () => ({ + MCPOAuthHandler: { + initiateOAuthFlow: jest.fn(), + getFlowState: jest.fn(), + completeOAuthFlow: jest.fn(), + generateFlowId: jest.fn(), + }, +})); + +jest.mock('@librechat/data-schemas', () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + createModels: jest.fn(() => ({ + User: { + findOne: jest.fn(), + findById: jest.fn(), + }, + Conversation: { + findOne: jest.fn(), + findById: jest.fn(), + }, + })), +})); + +jest.mock('~/models', () => ({ + findToken: jest.fn(), + updateToken: jest.fn(), + createToken: jest.fn(), + deleteTokens: jest.fn(), +})); + +jest.mock('~/server/services/Config', () => ({ + setCachedTools: jest.fn(), + getCachedTools: jest.fn(), + loadCustomConfig: jest.fn(), +})); + +jest.mock('~/server/services/MCP', () => ({ + getMCPSetupData: jest.fn(), + getServerConnectionStatus: jest.fn(), +})); + +jest.mock('~/server/services/PluginService', () => ({ + getUserPluginAuthValue: jest.fn(), +})); + +jest.mock('~/config', () => ({ + getMCPManager: jest.fn(), + getFlowStateManager: jest.fn(), +})); + +jest.mock('~/cache', () => ({ + getLogStores: jest.fn(), +})); + +jest.mock('~/server/middleware', () => ({ + requireJwtAuth: (req, res, next) => next(), +})); + +describe('MCP Routes', () => { + let app; + let mongoServer; + let mcpRouter; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); + + require('~/db/models'); + + mcpRouter = require('../mcp'); + + app = express(); + app.use(express.json()); + + app.use((req, res, next) => { + req.user = { id: 'test-user-id' }; + next(); + }); + + app.use('/api/mcp', mcpRouter); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('GET /:serverName/oauth/initiate', () => { + const { MCPOAuthHandler } = require('@librechat/api'); + const { getLogStores } = require('~/cache'); + + it('should initiate OAuth flow successfully', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + metadata: { + serverUrl: 'https://test-server.com', + oauth: { clientId: 'test-client-id' }, + }, + }), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + MCPOAuthHandler.initiateOAuthFlow.mockResolvedValue({ + authorizationUrl: 'https://oauth.example.com/auth', + flowId: 'test-flow-id', + }); + + const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ + userId: 'test-user-id', + flowId: 'test-flow-id', + }); + + expect(response.status).toBe(302); + expect(response.headers.location).toBe('https://oauth.example.com/auth'); + expect(MCPOAuthHandler.initiateOAuthFlow).toHaveBeenCalledWith( + 'test-server', + 'https://test-server.com', + 'test-user-id', + { clientId: 'test-client-id' }, + ); + }); + + it('should return 403 when userId does not match authenticated user', async () => { + const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ + userId: 'different-user-id', + flowId: 'test-flow-id', + }); + + expect(response.status).toBe(403); + expect(response.body).toEqual({ error: 'User mismatch' }); + }); + + it('should return 404 when flow state is not found', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue(null), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ + userId: 'test-user-id', + flowId: 'non-existent-flow-id', + }); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ error: 'Flow not found' }); + }); + + it('should return 400 when flow state has missing OAuth config', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + metadata: { + serverUrl: 'https://test-server.com', + }, + }), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ + userId: 'test-user-id', + flowId: 'test-flow-id', + }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ error: 'Invalid flow state' }); + }); + + it('should return 500 when OAuth initiation throws unexpected error', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockRejectedValue(new Error('Database error')), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ + userId: 'test-user-id', + flowId: 'test-flow-id', + }); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Failed to initiate OAuth' }); + }); + + it('should return 400 when flow state metadata is null', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + id: 'test-flow-id', + metadata: null, + }), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({ + userId: 'test-user-id', + flowId: 'test-flow-id', + }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ error: 'Invalid flow state' }); + }); + }); + + describe('GET /:serverName/oauth/callback', () => { + const { MCPOAuthHandler } = require('@librechat/api'); + const { getLogStores } = require('~/cache'); + + it('should redirect to error page when OAuth error is received', async () => { + const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ + error: 'access_denied', + state: 'test-flow-id', + }); + + expect(response.status).toBe(302); + expect(response.headers.location).toBe('/oauth/error?error=access_denied'); + }); + + it('should redirect to error page when code is missing', async () => { + const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ + state: 'test-flow-id', + }); + + expect(response.status).toBe(302); + expect(response.headers.location).toBe('/oauth/error?error=missing_code'); + }); + + it('should redirect to error page when state is missing', async () => { + const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ + code: 'test-auth-code', + }); + + expect(response.status).toBe(302); + expect(response.headers.location).toBe('/oauth/error?error=missing_state'); + }); + + it('should redirect to error page when flow state is not found', async () => { + MCPOAuthHandler.getFlowState.mockResolvedValue(null); + + const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ + code: 'test-auth-code', + state: 'invalid-flow-id', + }); + + expect(response.status).toBe(302); + expect(response.headers.location).toBe('/oauth/error?error=invalid_state'); + }); + + it('should handle OAuth callback successfully', async () => { + const mockFlowManager = { + completeFlow: jest.fn().mockResolvedValue(), + }; + const mockFlowState = { + serverName: 'test-server', + userId: 'test-user-id', + metadata: { toolFlowId: 'tool-flow-123' }, + clientInfo: {}, + codeVerifier: 'test-verifier', + }; + const mockTokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + }; + + MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState); + MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens); + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const mockUserConnection = { + fetchTools: jest.fn().mockResolvedValue([ + { + name: 'test-tool', + description: 'A test tool', + inputSchema: { type: 'object' }, + }, + ]), + }; + const mockMcpManager = { + getUserConnection: jest.fn().mockResolvedValue(mockUserConnection), + }; + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + + const { getCachedTools, setCachedTools } = require('~/server/services/Config'); + const { Constants } = require('librechat-data-provider'); + getCachedTools.mockResolvedValue({ + [`existing-tool${Constants.mcp_delimiter}test-server`]: { type: 'function' }, + [`other-tool${Constants.mcp_delimiter}other-server`]: { type: 'function' }, + }); + setCachedTools.mockResolvedValue(); + + const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ + code: 'test-auth-code', + state: 'test-flow-id', + }); + + expect(response.status).toBe(302); + expect(response.headers.location).toBe('/oauth/success?serverName=test-server'); + expect(MCPOAuthHandler.completeOAuthFlow).toHaveBeenCalledWith( + 'test-flow-id', + 'test-auth-code', + mockFlowManager, + ); + expect(mockFlowManager.completeFlow).toHaveBeenCalledWith( + 'tool-flow-123', + 'mcp_oauth', + mockTokens, + ); + }); + + it('should redirect to error page when callback processing fails', async () => { + MCPOAuthHandler.getFlowState.mockRejectedValue(new Error('Callback error')); + + const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ + code: 'test-auth-code', + state: 'test-flow-id', + }); + + expect(response.status).toBe(302); + expect(response.headers.location).toBe('/oauth/error?error=callback_failed'); + }); + + it('should handle system-level OAuth completion', async () => { + const mockFlowManager = { + completeFlow: jest.fn().mockResolvedValue(), + }; + const mockFlowState = { + serverName: 'test-server', + userId: 'system', + metadata: { toolFlowId: 'tool-flow-123' }, + clientInfo: {}, + codeVerifier: 'test-verifier', + }; + const mockTokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + }; + + MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState); + MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens); + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ + code: 'test-auth-code', + state: 'test-flow-id', + }); + + expect(response.status).toBe(302); + expect(response.headers.location).toBe('/oauth/success?serverName=test-server'); + }); + + it('should handle reconnection failure after OAuth', async () => { + const mockFlowManager = { + completeFlow: jest.fn().mockResolvedValue(), + }; + const mockFlowState = { + serverName: 'test-server', + userId: 'test-user-id', + metadata: { toolFlowId: 'tool-flow-123' }, + clientInfo: {}, + codeVerifier: 'test-verifier', + }; + const mockTokens = { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + }; + + MCPOAuthHandler.getFlowState.mockResolvedValue(mockFlowState); + MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens); + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const mockMcpManager = { + getUserConnection: jest.fn().mockRejectedValue(new Error('Reconnection failed')), + }; + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + + const { getCachedTools, setCachedTools } = require('~/server/services/Config'); + getCachedTools.mockResolvedValue({}); + setCachedTools.mockResolvedValue(); + + const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({ + code: 'test-auth-code', + state: 'test-flow-id', + }); + + expect(response.status).toBe(302); + expect(response.headers.location).toBe('/oauth/success?serverName=test-server'); + }); + }); + + describe('GET /oauth/tokens/:flowId', () => { + const { getLogStores } = require('~/cache'); + + it('should return tokens for completed flow', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'COMPLETED', + result: { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + }, + }), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/oauth/tokens/test-user-id:flow-123'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + tokens: { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + }, + }); + }); + + it('should return 401 when user is not authenticated', async () => { + const unauthApp = express(); + unauthApp.use(express.json()); + unauthApp.use((req, res, next) => { + req.user = null; + next(); + }); + unauthApp.use('/api/mcp', mcpRouter); + + const response = await request(unauthApp).get('/api/mcp/oauth/tokens/test-flow-id'); + + expect(response.status).toBe(401); + expect(response.body).toEqual({ error: 'User not authenticated' }); + }); + + it('should return 403 when user tries to access flow they do not own', async () => { + const response = await request(app).get('/api/mcp/oauth/tokens/other-user-id:flow-123'); + + expect(response.status).toBe(403); + expect(response.body).toEqual({ error: 'Access denied' }); + }); + + it('should return 404 when flow is not found', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue(null), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get( + '/api/mcp/oauth/tokens/test-user-id:non-existent-flow', + ); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ error: 'Flow not found' }); + }); + + it('should return 400 when flow is not completed', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'PENDING', + result: null, + }), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/oauth/tokens/test-user-id:pending-flow'); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ error: 'Flow not completed' }); + }); + + it('should return 500 when token retrieval throws an unexpected error', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockRejectedValue(new Error('Database connection failed')), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/oauth/tokens/test-user-id:error-flow'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Failed to get tokens' }); + }); + }); + + describe('GET /oauth/status/:flowId', () => { + const { getLogStores } = require('~/cache'); + + it('should return flow status when flow exists', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'PENDING', + error: null, + }), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/oauth/status/test-flow-id'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + status: 'PENDING', + completed: false, + failed: false, + error: null, + }); + }); + + it('should return 404 when flow is not found', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue(null), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/oauth/status/non-existent-flow'); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ error: 'Flow not found' }); + }); + + it('should return 500 when status check fails', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockRejectedValue(new Error('Database error')), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const response = await request(app).get('/api/mcp/oauth/status/error-flow-id'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Failed to get flow status' }); + }); + }); + + describe('POST /oauth/cancel/:serverName', () => { + const { getLogStores } = require('~/cache'); + const { MCPOAuthHandler } = require('@librechat/api'); + + it('should cancel OAuth flow successfully', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + status: 'PENDING', + }), + failFlow: jest.fn().mockResolvedValue(), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + MCPOAuthHandler.generateFlowId.mockReturnValue('test-user-id:test-server'); + + const response = await request(app).post('/api/mcp/oauth/cancel/test-server'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + message: 'OAuth flow for test-server cancelled successfully', + }); + + expect(mockFlowManager.failFlow).toHaveBeenCalledWith( + 'test-user-id:test-server', + 'mcp_oauth', + 'User cancelled OAuth flow', + ); + }); + + it('should return success message when no active flow exists', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue(null), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + MCPOAuthHandler.generateFlowId.mockReturnValue('test-user-id:test-server'); + + const response = await request(app).post('/api/mcp/oauth/cancel/test-server'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + message: 'No active OAuth flow to cancel', + }); + }); + + it('should return 500 when cancellation fails', async () => { + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ status: 'PENDING' }), + failFlow: jest.fn().mockRejectedValue(new Error('Database error')), + }; + + getLogStores.mockReturnValue({}); + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + MCPOAuthHandler.generateFlowId.mockReturnValue('test-user-id:test-server'); + + const response = await request(app).post('/api/mcp/oauth/cancel/test-server'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Failed to cancel OAuth flow' }); + }); + + it('should return 401 when user is not authenticated', async () => { + const unauthApp = express(); + unauthApp.use(express.json()); + unauthApp.use((req, res, next) => { + req.user = null; + next(); + }); + unauthApp.use('/api/mcp', mcpRouter); + + const response = await request(unauthApp).post('/api/mcp/oauth/cancel/test-server'); + + expect(response.status).toBe(401); + expect(response.body).toEqual({ error: 'User not authenticated' }); + }); + }); + + describe('POST /:serverName/reinitialize', () => { + const { loadCustomConfig } = require('~/server/services/Config'); + const { getUserPluginAuthValue } = require('~/server/services/PluginService'); + + it('should return 404 when server is not found in configuration', async () => { + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'other-server': {}, + }, + }); + + const response = await request(app).post('/api/mcp/non-existent-server/reinitialize'); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ + error: "MCP server 'non-existent-server' not found in configuration", + }); + }); + + it('should handle OAuth requirement during reinitialize', async () => { + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'oauth-server': { + customUserVars: {}, + }, + }, + }); + + const mockMcpManager = { + disconnectServer: jest.fn().mockResolvedValue(), + mcpConfigs: {}, + getUserConnection: jest.fn().mockImplementation(async ({ oauthStart }) => { + if (oauthStart) { + await oauthStart('https://oauth.example.com/auth'); + } + throw new Error('OAuth flow initiated - return early'); + }), + }; + + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + require('~/config').getFlowStateManager.mockReturnValue({}); + require('~/cache').getLogStores.mockReturnValue({}); + + const response = await request(app).post('/api/mcp/oauth-server/reinitialize'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: 'https://oauth.example.com/auth', + message: "MCP server 'oauth-server' ready for OAuth authentication", + serverName: 'oauth-server', + oauthRequired: true, + oauthUrl: 'https://oauth.example.com/auth', + }); + }); + + it('should return 500 when reinitialize fails with non-OAuth error', async () => { + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'error-server': {}, + }, + }); + + const mockMcpManager = { + disconnectServer: jest.fn().mockResolvedValue(), + mcpConfigs: {}, + getUserConnection: jest.fn().mockRejectedValue(new Error('Connection failed')), + }; + + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + require('~/config').getFlowStateManager.mockReturnValue({}); + require('~/cache').getLogStores.mockReturnValue({}); + + const response = await request(app).post('/api/mcp/error-server/reinitialize'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + error: 'Failed to reinitialize MCP server for user', + }); + }); + + it('should return 500 when unexpected error occurs', async () => { + loadCustomConfig.mockRejectedValue(new Error('Config loading failed')); + + const response = await request(app).post('/api/mcp/test-server/reinitialize'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Internal server error' }); + }); + + it('should return 401 when user is not authenticated', async () => { + const unauthApp = express(); + unauthApp.use(express.json()); + unauthApp.use((req, res, next) => { + req.user = null; + next(); + }); + unauthApp.use('/api/mcp', mcpRouter); + + const response = await request(unauthApp).post('/api/mcp/test-server/reinitialize'); + + expect(response.status).toBe(401); + expect(response.body).toEqual({ error: 'User not authenticated' }); + }); + + it('should handle errors when fetching custom user variables', async () => { + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'test-server': { + customUserVars: { + API_KEY: 'test-key-var', + SECRET_TOKEN: 'test-secret-var', + }, + }, + }, + }); + + getUserPluginAuthValue + .mockResolvedValueOnce('test-api-key-value') + .mockRejectedValueOnce(new Error('Database error')); + + const mockUserConnection = { + fetchTools: jest.fn().mockResolvedValue([]), + }; + + const mockMcpManager = { + disconnectServer: jest.fn().mockResolvedValue(), + mcpConfigs: {}, + getUserConnection: jest.fn().mockResolvedValue(mockUserConnection), + }; + + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + require('~/config').getFlowStateManager.mockReturnValue({}); + require('~/cache').getLogStores.mockReturnValue({}); + + const { getCachedTools, setCachedTools } = require('~/server/services/Config'); + getCachedTools.mockResolvedValue({}); + setCachedTools.mockResolvedValue(); + + const response = await request(app).post('/api/mcp/test-server/reinitialize'); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + }); + + it('should return failure message when reinitialize completely fails', async () => { + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'test-server': {}, + }, + }); + + const mockMcpManager = { + disconnectServer: jest.fn().mockResolvedValue(), + mcpConfigs: {}, + getUserConnection: jest.fn().mockResolvedValue(null), + }; + + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + require('~/config').getFlowStateManager.mockReturnValue({}); + require('~/cache').getLogStores.mockReturnValue({}); + + const { getCachedTools, setCachedTools } = require('~/server/services/Config'); + const { Constants } = require('librechat-data-provider'); + getCachedTools.mockResolvedValue({ + [`existing-tool${Constants.mcp_delimiter}test-server`]: { type: 'function' }, + }); + setCachedTools.mockResolvedValue(); + + const response = await request(app).post('/api/mcp/test-server/reinitialize'); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(false); + expect(response.body.message).toBe("Failed to reinitialize MCP server 'test-server'"); + }); + }); + + describe('GET /connection/status', () => { + const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP'); + + it('should return connection status for all servers', async () => { + const mockMcpConfig = { + server1: { endpoint: 'http://server1.com' }, + server2: { endpoint: 'http://server2.com' }, + }; + + getMCPSetupData.mockResolvedValue({ + mcpConfig: mockMcpConfig, + appConnections: {}, + userConnections: {}, + oauthServers: [], + }); + + getServerConnectionStatus + .mockResolvedValueOnce({ + connectionState: 'connected', + requiresOAuth: false, + }) + .mockResolvedValueOnce({ + connectionState: 'disconnected', + requiresOAuth: true, + }); + + const response = await request(app).get('/api/mcp/connection/status'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + connectionStatus: { + server1: { + connectionState: 'connected', + requiresOAuth: false, + }, + server2: { + connectionState: 'disconnected', + requiresOAuth: true, + }, + }, + }); + + expect(getMCPSetupData).toHaveBeenCalledWith('test-user-id'); + expect(getServerConnectionStatus).toHaveBeenCalledTimes(2); + }); + + it('should return 404 when MCP config is not found', async () => { + getMCPSetupData.mockRejectedValue(new Error('MCP config not found')); + + const response = await request(app).get('/api/mcp/connection/status'); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ error: 'MCP config not found' }); + }); + + it('should return 500 when connection status check fails', async () => { + getMCPSetupData.mockRejectedValue(new Error('Database error')); + + const response = await request(app).get('/api/mcp/connection/status'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Failed to get connection status' }); + }); + + it('should return 401 when user is not authenticated', async () => { + const unauthApp = express(); + unauthApp.use(express.json()); + unauthApp.use((req, res, next) => { + req.user = null; + next(); + }); + unauthApp.use('/api/mcp', mcpRouter); + + const response = await request(unauthApp).get('/api/mcp/connection/status'); + + expect(response.status).toBe(401); + expect(response.body).toEqual({ error: 'User not authenticated' }); + }); + }); + + describe('GET /connection/status/:serverName', () => { + const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP'); + + it('should return connection status for OAuth-required server', async () => { + const mockMcpConfig = { + 'oauth-server': { endpoint: 'http://oauth-server.com' }, + }; + + getMCPSetupData.mockResolvedValue({ + mcpConfig: mockMcpConfig, + appConnections: {}, + userConnections: {}, + oauthServers: [], + }); + + getServerConnectionStatus.mockResolvedValue({ + connectionState: 'requires_auth', + requiresOAuth: true, + }); + + const response = await request(app).get('/api/mcp/connection/status/oauth-server'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + serverName: 'oauth-server', + connectionStatus: 'requires_auth', + requiresOAuth: true, + }); + }); + + it('should return 404 when server is not found in configuration', async () => { + getMCPSetupData.mockResolvedValue({ + mcpConfig: { + 'other-server': { endpoint: 'http://other-server.com' }, + }, + appConnections: {}, + userConnections: {}, + oauthServers: [], + }); + + const response = await request(app).get('/api/mcp/connection/status/non-existent-server'); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ + error: "MCP server 'non-existent-server' not found in configuration", + }); + }); + + it('should return 404 when MCP config is not found', async () => { + getMCPSetupData.mockRejectedValue(new Error('MCP config not found')); + + const response = await request(app).get('/api/mcp/connection/status/test-server'); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ error: 'MCP config not found' }); + }); + + it('should return 500 when connection status check fails', async () => { + getMCPSetupData.mockRejectedValue(new Error('Database connection failed')); + + const response = await request(app).get('/api/mcp/connection/status/test-server'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Failed to get connection status' }); + }); + + it('should return 401 when user is not authenticated', async () => { + const unauthApp = express(); + unauthApp.use(express.json()); + unauthApp.use((req, res, next) => { + req.user = null; + next(); + }); + unauthApp.use('/api/mcp', mcpRouter); + + const response = await request(unauthApp).get('/api/mcp/connection/status/test-server'); + + expect(response.status).toBe(401); + expect(response.body).toEqual({ error: 'User not authenticated' }); + }); + }); + + describe('GET /:serverName/auth-values', () => { + const { loadCustomConfig } = require('~/server/services/Config'); + const { getUserPluginAuthValue } = require('~/server/services/PluginService'); + + it('should return auth value flags for server', async () => { + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'test-server': { + customUserVars: { + API_KEY: 'some-env-var', + SECRET_TOKEN: 'another-env-var', + }, + }, + }, + }); + + getUserPluginAuthValue.mockResolvedValueOnce('some-api-key-value').mockResolvedValueOnce(''); + + const response = await request(app).get('/api/mcp/test-server/auth-values'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + serverName: 'test-server', + authValueFlags: { + API_KEY: true, + SECRET_TOKEN: false, + }, + }); + + expect(getUserPluginAuthValue).toHaveBeenCalledTimes(2); + }); + + it('should return 404 when server is not found in configuration', async () => { + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'other-server': {}, + }, + }); + + const response = await request(app).get('/api/mcp/non-existent-server/auth-values'); + + expect(response.status).toBe(404); + expect(response.body).toEqual({ + error: "MCP server 'non-existent-server' not found in configuration", + }); + }); + + it('should handle errors when checking auth values', async () => { + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'test-server': { + customUserVars: { + API_KEY: 'some-env-var', + }, + }, + }, + }); + + getUserPluginAuthValue.mockRejectedValue(new Error('Database error')); + + const response = await request(app).get('/api/mcp/test-server/auth-values'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + serverName: 'test-server', + authValueFlags: { + API_KEY: false, + }, + }); + }); + + it('should return 500 when auth values check throws unexpected error', async () => { + loadCustomConfig.mockRejectedValue(new Error('Config loading failed')); + + const response = await request(app).get('/api/mcp/test-server/auth-values'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Failed to check auth value flags' }); + }); + + it('should handle customUserVars that is not an object', async () => { + const { loadCustomConfig } = require('~/server/services/Config'); + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'test-server': { + customUserVars: 'not-an-object', + }, + }, + }); + + const response = await request(app).get('/api/mcp/test-server/auth-values'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + success: true, + serverName: 'test-server', + authValueFlags: {}, + }); + }); + + it('should return 401 when user is not authenticated in auth-values endpoint', async () => { + const appWithoutAuth = express(); + appWithoutAuth.use(express.json()); + appWithoutAuth.use('/api/mcp', mcpRouter); + + const response = await request(appWithoutAuth).get('/api/mcp/test-server/auth-values'); + + expect(response.status).toBe(401); + expect(response.body).toEqual({ error: 'User not authenticated' }); + }); + }); + + describe('POST /:serverName/reinitialize - Tool Deletion Coverage', () => { + it('should handle null cached tools during reinitialize (triggers || {} fallback)', async () => { + const { loadCustomConfig, getCachedTools } = require('~/server/services/Config'); + + const mockUserConnection = { + fetchTools: jest.fn().mockResolvedValue([{ name: 'new-tool', description: 'A new tool' }]), + }; + + const mockMcpManager = { + getUserConnection: jest.fn().mockResolvedValue(mockUserConnection), + disconnectServer: jest.fn(), + initializeServer: jest.fn(), + mcpConfigs: {}, + }; + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'test-server': { env: { API_KEY: 'test-key' } }, + }, + }); + + getCachedTools.mockResolvedValue(null); + + const response = await request(app).post('/api/mcp/test-server/reinitialize').expect(200); + + expect(response.body).toEqual({ + message: "MCP server 'test-server' reinitialized successfully", + success: true, + oauthRequired: false, + oauthUrl: null, + serverName: 'test-server', + }); + }); + + it('should delete existing cached tools during successful reinitialize', async () => { + const { + loadCustomConfig, + getCachedTools, + setCachedTools, + } = require('~/server/services/Config'); + + const mockUserConnection = { + fetchTools: jest.fn().mockResolvedValue([{ name: 'new-tool', description: 'A new tool' }]), + }; + + const mockMcpManager = { + getUserConnection: jest.fn().mockResolvedValue(mockUserConnection), + disconnectServer: jest.fn(), + initializeServer: jest.fn(), + mcpConfigs: {}, + }; + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + + loadCustomConfig.mockResolvedValue({ + mcpServers: { + 'test-server': { env: { API_KEY: 'test-key' } }, + }, + }); + + const existingTools = { + 'old-tool_mcp_test-server': { type: 'function' }, + 'other-tool_mcp_other-server': { type: 'function' }, + }; + getCachedTools.mockResolvedValue(existingTools); + + const response = await request(app).post('/api/mcp/test-server/reinitialize').expect(200); + + expect(response.body).toEqual({ + message: "MCP server 'test-server' reinitialized successfully", + success: true, + oauthRequired: false, + oauthUrl: null, + serverName: 'test-server', + }); + + expect(setCachedTools).toHaveBeenCalledWith( + expect.objectContaining({ + 'new-tool_mcp_test-server': expect.any(Object), + 'other-tool_mcp_other-server': { type: 'function' }, + }), + { userId: 'test-user-id' }, + ); + expect(setCachedTools).toHaveBeenCalledWith( + expect.not.objectContaining({ + 'old-tool_mcp_test-server': expect.anything(), + }), + { userId: 'test-user-id' }, + ); + }); + }); + + describe('GET /:serverName/oauth/callback - Edge Cases', () => { + it('should handle OAuth callback without toolFlowId (falsy toolFlowId)', async () => { + const { MCPOAuthHandler } = require('@librechat/api'); + MCPOAuthHandler.getFlowState = jest.fn().mockResolvedValue({ + id: 'test-flow-id', + userId: 'test-user-id', + metadata: { + serverUrl: 'https://example.com', + oauth: {}, + // No toolFlowId property + }, + clientInfo: {}, + codeVerifier: 'test-verifier', + }); + + const mockFlowManager = { + completeFlow: jest.fn(), + }; + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const mockMcpManager = { + getUserConnection: jest.fn().mockResolvedValue({ + fetchTools: jest.fn().mockResolvedValue([]), + }), + }; + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback?code=test-code&state=test-flow-id') + .expect(302); + + expect(mockFlowManager.completeFlow).not.toHaveBeenCalled(); + expect(response.headers.location).toContain('/oauth/success'); + }); + + it('should handle null cached tools in OAuth callback (triggers || {} fallback)', async () => { + const { getCachedTools } = require('~/server/services/Config'); + getCachedTools.mockResolvedValue(null); + + const mockFlowManager = { + getFlowState: jest.fn().mockResolvedValue({ + id: 'test-flow-id', + userId: 'test-user-id', + metadata: { serverUrl: 'https://example.com', oauth: {} }, + clientInfo: {}, + codeVerifier: 'test-verifier', + }), + completeFlow: jest.fn(), + }; + require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager); + + const mockMcpManager = { + getUserConnection: jest.fn().mockResolvedValue({ + fetchTools: jest + .fn() + .mockResolvedValue([{ name: 'test-tool', description: 'Test tool' }]), + }), + }; + require('~/config').getMCPManager.mockReturnValue(mockMcpManager); + + const response = await request(app) + .get('/api/mcp/test-server/oauth/callback?code=test-code&state=test-flow-id') + .expect(302); + + expect(response.headers.location).toContain('/oauth/success'); + }); + }); +}); diff --git a/api/server/routes/mcp.js b/api/server/routes/mcp.js index 4e35052cd..a725cf666 100644 --- a/api/server/routes/mcp.js +++ b/api/server/routes/mcp.js @@ -337,9 +337,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => { for (const varName of Object.keys(serverConfig.customUserVars)) { try { const value = await getUserPluginAuthValue(user.id, varName, false); - if (value) { - customUserVars[varName] = value; - } + customUserVars[varName] = value; } catch (err) { logger.error(`[MCP Reinitialize] Error fetching ${varName} for user ${user.id}:`, err); } @@ -504,10 +502,6 @@ router.get('/connection/status/:serverName', requireJwtAuth, async (req, res) => return res.status(401).json({ error: 'User not authenticated' }); } - if (!serverName) { - return res.status(400).json({ error: 'Server name is required' }); - } - const { mcpConfig, appConnections, userConnections, oauthServers } = await getMCPSetupData( user.id, ); From c6fb4686ef4ce5e8a84f5c2b0c0e267ee58ff2b5 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 1 Aug 2025 15:52:00 -0400 Subject: [PATCH 043/224] =?UTF-8?q?=F0=9F=8C=90=20ci:=20Bump=20Locize=20i1?= =?UTF-8?q?8n=20Action=20Version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/locize-i18n-sync.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/locize-i18n-sync.yml b/.github/workflows/locize-i18n-sync.yml index 082d3a46a..f34648dfd 100644 --- a/.github/workflows/locize-i18n-sync.yml +++ b/.github/workflows/locize-i18n-sync.yml @@ -48,7 +48,7 @@ jobs: # 2. Download translation files from locize. - name: Download Translations from locize - uses: locize/download@v1 + uses: locize/download@v2 with: project-id: ${{ secrets.LOCIZE_PROJECT_ID }} path: "client/src/locales" From 077248a8a72dbfd3395969d095133e7ae5c34396 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:55:42 -0400 Subject: [PATCH 044/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#8808)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/ar/translation.json | 2 +- client/src/locales/ca/translation.json | 2 +- client/src/locales/cs/translation.json | 2 +- client/src/locales/da/translation.json | 2 +- client/src/locales/de/translation.json | 2 +- client/src/locales/en/translation.json | 2 +- client/src/locales/es/translation.json | 2 +- client/src/locales/et/translation.json | 2 +- client/src/locales/fa/translation.json | 2 +- client/src/locales/fi/translation.json | 2 +- client/src/locales/fr/translation.json | 2 +- client/src/locales/he/translation.json | 2 +- client/src/locales/hu/translation.json | 2 +- client/src/locales/hy/translation.json | 2 +- client/src/locales/id/translation.json | 2 +- client/src/locales/it/translation.json | 2 +- client/src/locales/ja/translation.json | 2 +- client/src/locales/ka/translation.json | 2 +- client/src/locales/ko/translation.json | 107 +++++++++++++++++++- client/src/locales/lv/translation.json | 2 +- client/src/locales/nl/translation.json | 2 +- client/src/locales/pl/translation.json | 2 +- client/src/locales/pt-BR/translation.json | 2 +- client/src/locales/pt-PT/translation.json | 2 +- client/src/locales/ru/translation.json | 2 +- client/src/locales/sv/translation.json | 2 +- client/src/locales/th/translation.json | 2 +- client/src/locales/tr/translation.json | 2 +- client/src/locales/vi/translation.json | 2 +- client/src/locales/zh-Hans/translation.json | 2 +- client/src/locales/zh-Hant/translation.json | 2 +- 31 files changed, 136 insertions(+), 31 deletions(-) diff --git a/client/src/locales/ar/translation.json b/client/src/locales/ar/translation.json index 49fde151c..496b5c5b7 100644 --- a/client/src/locales/ar/translation.json +++ b/client/src/locales/ar/translation.json @@ -729,4 +729,4 @@ "com_ui_yes": "نعم", "com_ui_zoom": "تكبير", "com_user_message": "أنت" -} \ No newline at end of file +} diff --git a/client/src/locales/ca/translation.json b/client/src/locales/ca/translation.json index eacd855c1..e456ac4a6 100644 --- a/client/src/locales/ca/translation.json +++ b/client/src/locales/ca/translation.json @@ -889,4 +889,4 @@ "com_ui_yes": "Sí", "com_ui_zoom": "Zoom", "com_user_message": "Tu" -} \ No newline at end of file +} diff --git a/client/src/locales/cs/translation.json b/client/src/locales/cs/translation.json index 716a1aaac..e4a65e7ea 100644 --- a/client/src/locales/cs/translation.json +++ b/client/src/locales/cs/translation.json @@ -732,4 +732,4 @@ "com_ui_yes": "Ano", "com_ui_zoom": "Přiblížit", "com_user_message": "Vy" -} \ No newline at end of file +} diff --git a/client/src/locales/da/translation.json b/client/src/locales/da/translation.json index e75622d55..a51f10659 100644 --- a/client/src/locales/da/translation.json +++ b/client/src/locales/da/translation.json @@ -940,4 +940,4 @@ "com_ui_yes": "Ja", "com_ui_zoom": "Zoom", "com_user_message": "Du" -} \ No newline at end of file +} diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index 196509eca..5876eebd2 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -1083,4 +1083,4 @@ "com_ui_yes": "Ja", "com_ui_zoom": "Zoom", "com_user_message": "Du" -} \ No newline at end of file +} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 51ae742aa..ae61f049a 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1102,4 +1102,4 @@ "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", "com_user_message": "You" -} \ No newline at end of file +} diff --git a/client/src/locales/es/translation.json b/client/src/locales/es/translation.json index 14dc81d06..fb3f33e19 100644 --- a/client/src/locales/es/translation.json +++ b/client/src/locales/es/translation.json @@ -754,4 +754,4 @@ "com_ui_yes": "Sí", "com_ui_zoom": "Zoom", "com_user_message": "Usted" -} \ No newline at end of file +} diff --git a/client/src/locales/et/translation.json b/client/src/locales/et/translation.json index 72f82cb75..dc5a7b946 100644 --- a/client/src/locales/et/translation.json +++ b/client/src/locales/et/translation.json @@ -962,4 +962,4 @@ "com_ui_yes": "Jah", "com_ui_zoom": "Suumi", "com_user_message": "Sina" -} \ No newline at end of file +} diff --git a/client/src/locales/fa/translation.json b/client/src/locales/fa/translation.json index 1ed06aaf2..5ad32be7b 100644 --- a/client/src/locales/fa/translation.json +++ b/client/src/locales/fa/translation.json @@ -844,4 +844,4 @@ "com_ui_yes": "بله", "com_ui_zoom": "بزرگنمایی ضربه بزنید؛", "com_user_message": "شما" -} \ No newline at end of file +} diff --git a/client/src/locales/fi/translation.json b/client/src/locales/fi/translation.json index e401562ef..2a0ad4c60 100644 --- a/client/src/locales/fi/translation.json +++ b/client/src/locales/fi/translation.json @@ -602,4 +602,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 71b8eb268..9e8360423 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -1074,4 +1074,4 @@ "com_ui_yes": "Oui", "com_ui_zoom": "Zoom", "com_user_message": "Vous" -} \ No newline at end of file +} diff --git a/client/src/locales/he/translation.json b/client/src/locales/he/translation.json index d2b189f15..5edb6861b 100644 --- a/client/src/locales/he/translation.json +++ b/client/src/locales/he/translation.json @@ -1066,4 +1066,4 @@ "com_ui_yes": "כן", "com_ui_zoom": "זום", "com_user_message": "אתה" -} \ No newline at end of file +} diff --git a/client/src/locales/hu/translation.json b/client/src/locales/hu/translation.json index 4a7cbab5e..a7d12b29a 100644 --- a/client/src/locales/hu/translation.json +++ b/client/src/locales/hu/translation.json @@ -844,4 +844,4 @@ "com_ui_yes": "Igen", "com_ui_zoom": "Zoom", "com_user_message": "Ön" -} \ No newline at end of file +} diff --git a/client/src/locales/hy/translation.json b/client/src/locales/hy/translation.json index 0553bd565..73ff8ef94 100644 --- a/client/src/locales/hy/translation.json +++ b/client/src/locales/hy/translation.json @@ -198,4 +198,4 @@ "com_ui_write": "Գրում է", "com_ui_yes": "Այո", "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 05e94c28c..58a214188 100644 --- a/client/src/locales/id/translation.json +++ b/client/src/locales/id/translation.json @@ -288,4 +288,4 @@ "com_ui_upload_error": "Ada kesalahan saat mengunggah file Anda", "com_ui_upload_success": "Berhasil mengunggah file", "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 4d7f96da1..cbaf7c45b 100644 --- a/client/src/locales/it/translation.json +++ b/client/src/locales/it/translation.json @@ -827,4 +827,4 @@ "com_ui_yes": "Sì", "com_ui_zoom": "Zoom", "com_user_message": "Mostra nome utente nei messaggi" -} \ No newline at end of file +} diff --git a/client/src/locales/ja/translation.json b/client/src/locales/ja/translation.json index 96b0b6594..9ba078b5d 100644 --- a/client/src/locales/ja/translation.json +++ b/client/src/locales/ja/translation.json @@ -1082,4 +1082,4 @@ "com_ui_yes": "はい", "com_ui_zoom": "ズーム", "com_user_message": "あなた" -} \ No newline at end of file +} diff --git a/client/src/locales/ka/translation.json b/client/src/locales/ka/translation.json index 1504a8bde..538554898 100644 --- a/client/src/locales/ka/translation.json +++ b/client/src/locales/ka/translation.json @@ -66,4 +66,4 @@ "com_nav_lang_turkish": "თურქული", "com_nav_lang_vietnamese": "ვიეტნამური", "com_nav_language": "ენა" -} \ No newline at end of file +} diff --git a/client/src/locales/ko/translation.json b/client/src/locales/ko/translation.json index ce5e794b3..f7bc47da4 100644 --- a/client/src/locales/ko/translation.json +++ b/client/src/locales/ko/translation.json @@ -24,6 +24,7 @@ "com_agents_missing_provider_model": "에이전트를 생성하기 전에 제공업체와 모델을 선택해 주세요", "com_agents_name_placeholder": "선택 사항: 에이전트의 이름", "com_agents_no_access": "이 에이전트를 수정할 권한이 없습니다", + "com_agents_no_agent_id_error": "에이전트 ID를 찾을 수 없습니다. 먼저 에이전트가 생성되었는지 확인하세요.", "com_agents_not_available": "에이전트를 사용할 수 없음", "com_agents_search_info": "활성화하면 에이전트가 최신 정보를 검색할 수 있도록 허용합니다. 유효한 API 키가 필요합니다.", "com_agents_search_name": "이름으로 에이전트 검색", @@ -128,6 +129,7 @@ "com_auth_reset_password_if_email_exists": "해당 이메일 주소로 등록된 계정이 있다면, 비밀번호 재설정 안내 메일을 발송했습니다. 스팸 폴더도 확인해 주세요.", "com_auth_reset_password_link_sent": "이메일 전송", "com_auth_reset_password_success": "비밀번호 재설정 성공", + "com_auth_saml_login": "SAML로 계속하기", "com_auth_sign_in": "로그인", "com_auth_sign_up": "가입하기", "com_auth_submit_registration": "등록하기", @@ -156,6 +158,7 @@ "com_endpoint_anthropic_thinking_budget": "Claude의 내부 추론에 사용할 수 있는 최대 토큰 수를 결정합니다. 큰 예산은 복잡한 문제에 대해 더 철저한 분석을 가능하게 하여 응답 품질을 개선할 수 있지만, 32K 이상 범위에서는 Claude가 할당된 전체 예산을 모두 사용하지 않을 수도 있습니다. 이 설정은 \"최대 출력 토큰\"보다 낮아야 합니다.", "com_endpoint_anthropic_topk": "Top-k는 모델이 출력에 사용할 토큰을 선택하는 방식을 변경합니다. top-k가 1인 경우 모델의 어휘 중 가장 확률이 높은 토큰이 선택됩니다(greedy decoding). top-k가 3인 경우 다음 토큰은 가장 확률이 높은 3개의 토큰 중에서 선택됩니다(temperature 사용).", "com_endpoint_anthropic_topp": "Top-p는 모델이 출력에 사용할 토큰을 선택하는 방식을 변경합니다. 토큰은 가장 높은 확률부터 가장 낮은 확률까지 선택됩니다. 선택된 토큰의 확률의 합이 top-p 값과 같아질 때까지 선택됩니다.", + "com_endpoint_anthropic_use_web_search": "Anthropic의 내장 검색 기능을 사용하여 웹 검색 기능을 활성화합니다. 모델이 최신 정보를 검색하여 더 정확하고 현재의 응답을 제공할 수 있게 합니다.", "com_endpoint_assistant": "어시스턴트", "com_endpoint_assistant_model": "에이전트 모델", "com_endpoint_assistant_placeholder": "오른쪽 사이드 패널에서 에이전트를 선택하세요", @@ -193,6 +196,8 @@ "com_endpoint_deprecated": "단축됨", "com_endpoint_deprecated_info": "이 엔드포인트는 단축되었으며 향후 버전에서 제거될 수 있습니다. 대신 에이전트 엔드포인트를 사용하세요.", "com_endpoint_deprecated_info_a11y": "이 플러그인 엔드포인트는 단축되었으며 향후 버전에서 제거될 수 있습니다. 대신 에이전트 엔드포인트를 사용하세요.", + "com_endpoint_disable_streaming": "스트리밍 응답을 비활성화하고 완전한 응답을 한 번에 받습니다. o3와 같이 스트리밍을 위해 조직 확인이 필요한 모델에 유용합니다", + "com_endpoint_disable_streaming_label": "스트리밍 비활성화", "com_endpoint_examples": " 프리셋", "com_endpoint_export": "내보내기", "com_endpoint_export_share": "내보내기/공유", @@ -201,8 +206,11 @@ "com_endpoint_google_custom_name_placeholder": "Google에 대한 사용자 정의 이름 설정", "com_endpoint_google_maxoutputtokens": "응답에서 생성할 수 있는 최대 토큰 수입니다. 짧은 응답에는 낮은 값을, 긴 응답에는 높은 값을 지정하세요.", "com_endpoint_google_temp": "높은 값 = 더 무작위, 낮은 값 = 더 집중적이고 결정적입니다. 이 값을 변경하거나 Top P 중 하나만 변경하는 것을 권장합니다.", + "com_endpoint_google_thinking": "추론을 활성화하거나 비활성화합니다. 이 설정은 특정 모델(2.5 시리즈)에서만 지원됩니다. 이전 모델의 경우 이 설정이 영향을 미치지 않을 수 있습니다.", + "com_endpoint_google_thinking_budget": "모델이 사용하는 추론 토큰 수를 안내합니다. 실제 양은 프롬프트에 따라 이 값을 초과하거나 미달될 수 있습니다.\n\n이 설정은 특정 모델(2.5 시리즈)에서만 지원됩니다. Gemini 2.5 Pro는 128-32,768 토큰을 지원합니다. Gemini 2.5 Flash는 0-24,576 토큰을 지원합니다. Gemini 2.5 Flash Lite는 512-24,576 토큰을 지원합니다.\n\n비워두거나 \"-1\"로 설정하면 모델이 언제 얼마나 생각할지 자동으로 결정합니다. 기본적으로 Gemini 2.5 Flash Lite는 생각하지 않습니다.", "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_google_use_search_grounding": "Google의 검색 그라운딩 기능을 사용하여 실시간 웹 검색 결과로 응답을 향상시킵니다. 모델이 현재 정보에 접근하여 더 정확하고 최신의 답변을 제공할 수 있게 합니다.", "com_endpoint_instructions_assistants": "에이전트 지침 재정의", "com_endpoint_instructions_assistants_placeholder": "어시스턴트의 지침을 재정의합니다. 이를 통해 실행마다 동작을 수정할 수 있습니다.", "com_endpoint_max_output_tokens": "최대 출력 토큰 수", @@ -220,11 +228,14 @@ "com_endpoint_openai_pres": "텍스트에서 토큰이 나타나는지 여부에 따라 새로운 토큰에 패널티를 부여합니다. 이전에 나온 텍스트에 나타나는 토큰에 대한 패널티를 증가시켜 새로운 주제에 대해 이야기할 가능성을 높입니다.", "com_endpoint_openai_prompt_prefix_placeholder": "시스템 메시지에 포함할 사용자 정의 지시사항을 설정하세요. 기본값: 없음", "com_endpoint_openai_reasoning_effort": "o1 및 o3 모델 전용: 추론 모델의 추론 노력(reasoning effort)을 제한합니다. 추론 노력을 줄이면 응답 속도가 빨라지고, 응답에서 사용되는 추론 관련 토큰 수가 줄어들 수 있습니다.", + "com_endpoint_openai_reasoning_summary": "Responses API 전용: 모델이 수행한 추론의 요약입니다. 디버깅과 모델의 추론 과정을 이해하는 데 유용할 수 있습니다. none, auto, concise 또는 detailed로 설정하세요.", "com_endpoint_openai_resend": "이전에 첨부한 모든 이미지를 다시 전송합니다. 참고: 이렇게 하면 토큰 비용이 크게 증가할 수 있으며, 많은 이미지를 첨부하면 오류가 발생할 수 있습니다.", "com_endpoint_openai_resend_files": "이전에 첨부한 모든 파일을 다시 보내세요. 참고: 이렇게 하면 토큰 비용이 증가하고 많은 첨부 파일로 인해 오류가 발생할 수 있습니다.", "com_endpoint_openai_stop": "API가 추가 토큰 생성을 중지할 최대 4개의 시퀀스입니다.", "com_endpoint_openai_temp": "높은 값 = 더 무작위, 낮은 값 = 더 집중적이고 결정적입니다. 이 값을 변경하거나 Top P 중 하나만 변경하는 것을 권장합니다.", "com_endpoint_openai_topp": "온도를 사용한 샘플링 대신, top_p 확률 질량을 고려하는 nucleus 샘플링입니다. 따라서 0.1은 상위 10% 확률 질량을 구성하는 토큰만 고려합니다. 이 값을 변경하거나 온도를 변경하는 것을 권장하지만, 둘 다 변경하지는 마세요.", + "com_endpoint_openai_use_responses_api": "OpenAI의 확장 기능이 포함된 Chat Completions 대신 Responses API를 사용합니다. o1-pro, o3-pro에 필수이며 추론 요약을 활성화하는 데 필요합니다.", + "com_endpoint_openai_use_web_search": "OpenAI의 내장 검색 기능을 사용하여 웹 검색 기능을 활성화합니다. 모델이 최신 정보를 검색하여 더 정확하고 현재의 응답을 제공할 수 있게 합니다.", "com_endpoint_output": "출력", "com_endpoint_plug_image_detail": "이미지 상세 정보", "com_endpoint_plug_resend_files": "파일 재전송", @@ -255,6 +266,7 @@ "com_endpoint_prompt_prefix_assistants_placeholder": "추가 지시사항 또는 컨텍스트를 Assistant의 기본 지시사항에 추가합니다. 비어 있으면 무시됩니다.", "com_endpoint_prompt_prefix_placeholder": "사용자 정의 지시사항 또는 컨텍스트를 설정하세요. 비어 있으면 무시됩니다.", "com_endpoint_reasoning_effort": "추론 노력", + "com_endpoint_reasoning_summary": "추론 요약", "com_endpoint_save_as_preset": "프리셋으로 저장", "com_endpoint_search": "이름으로 엔드포인트 검색", "com_endpoint_search_endpoint_models": "{{0}} 모델 검색중...", @@ -270,6 +282,8 @@ "com_endpoint_top_k": "Top K", "com_endpoint_top_p": "Top P", "com_endpoint_use_active_assistant": "활성 에이전트 사용", + "com_endpoint_use_responses_api": "Responses API 사용", + "com_endpoint_use_search_grounding": "Google 검색으로 그라운딩", "com_error_expired_user_key": "{{0}}에 대한 키가 {{1}}에 만료되었습니다. 새 키를 제공하고 다시 시도해주세요.", "com_error_files_dupe": "중복된 파일이 감지되었습니다", "com_error_files_empty": "빈 파일은 허용되지 않습니다", @@ -278,6 +292,8 @@ "com_error_files_upload": "파일 업로드 중 오류가 발생했습니다", "com_error_files_upload_canceled": "파일 업로드가 취소되었습니다. 참고: 업로드 처리가 아직 진행 중일 수 있으며 수동으로 삭제해야 할 수 있습니다.", "com_error_files_validation": "파일 유효성 검사 중 오류가 발생했습니다", + "com_error_google_tool_conflict": "내장 Google 도구는 외부 도구와 함께 사용할 수 없습니다. 내장 도구 또는 외부 도구 중 하나를 비활성화하세요.", + "com_error_heic_conversion": "HEIC 이미지를 JPEG로 변환하는 데 실패했습니다. 수동으로 이미지를 변환하거나 다른 형식을 사용해 보세요.", "com_error_input_length": "최신 메시지의 토큰 수가 너무 많아 토큰 제한을 초과했거나, 토큰 제한 관련 파라미터가 잘못 설정되어 있어 컨텍스트 창에 부정적인 영향을 미치고 있습니다. 자세한 정보: {{0}}. 메시지를 줄이거나, 대화 파라미터에서 최대 컨텍스트 크기를 조정하거나, 대화를 포크(fork)하여 계속 진행해 주세요.", "com_error_invalid_agent_provider": "\"{{0}}\" 제공자는 에이전트와 함께 사용할 수 없습니다. 에이전트 설정으로 이동하여 현재 사용 가능한 제공자를 선택하세요.", "com_error_invalid_user_key": "제공된 키가 유효하지 않습니다. 키를 제공하고 다시 시도해주세요.", @@ -290,6 +306,7 @@ "com_files_table": "내용이 비어 있었습니다.", "com_generated_files": "생성된 파일:", "com_hide_examples": "예시 숨기기", + "com_info_heic_converting": "HEIC 이미지를 JPEG로 변환 중...", "com_nav_2fa": "이단계 인증 (2FA)", "com_nav_account_settings": "계정 설정", "com_nav_always_make_prod": "항상 새 버전을 프로덕션으로 설정", @@ -307,6 +324,26 @@ "com_nav_auto_transcribe_audio": "오디오 자동 변환", "com_nav_automatic_playback": "최신 메시지 자동 재생", "com_nav_balance": "잔고", + "com_nav_balance_auto_refill_disabled": "자동 충전이 비활성화되었습니다.", + "com_nav_balance_auto_refill_error": "자동 충전 설정을 불러오는 중 오류가 발생했습니다.", + "com_nav_balance_auto_refill_settings": "자동 충전 설정", + "com_nav_balance_day": "일", + "com_nav_balance_days": "일", + "com_nav_balance_every": "매", + "com_nav_balance_hour": "시간", + "com_nav_balance_hours": "시간", + "com_nav_balance_interval": "간격:", + "com_nav_balance_last_refill": "마지막 충전:", + "com_nav_balance_minute": "분", + "com_nav_balance_minutes": "분", + "com_nav_balance_month": "월", + "com_nav_balance_next_refill": "다음 충전:", + "com_nav_balance_next_refill_info": "다음 충전은 두 조건이 모두 충족될 때만 자동으로 발생합니다: 마지막 충전 이후 지정된 시간 간격이 지났고, 프롬프트를 보내면 잔액이 0 아래로 떨어질 때입니다.", + "com_nav_balance_refill_amount": "충전 금액:", + "com_nav_balance_second": "초", + "com_nav_balance_seconds": "초", + "com_nav_balance_week": "주", + "com_nav_balance_weeks": "주", "com_nav_browser": "브라우저", "com_nav_center_chat_input": "환영 화면에서 채팅 입력 중앙 정렬", "com_nav_change_picture": "프로필 사진 변경", @@ -367,6 +404,7 @@ "com_nav_info_show_thinking": "이 기능을 활성화하면, 채팅에서 추론 드롭다운이 기본적으로 열려 있어 AI의 사고 과정을 실시간으로 볼 수 있습니다. 비활성화하면 더 깔끔하고 간결한 인터페이스를 위해 드롭다운이 기본적으로 닫힙니다.", "com_nav_info_user_name_display": "활성화하면 보내는 각 메시지 위에 사용자 이름이 표시됩니다. 비활성화하면 내 메시지 위에 \"나\"라고만 표시됩니다.", "com_nav_lang_arabic": "العربية", + "com_nav_lang_armenian": "아르메니아어", "com_nav_lang_auto": "자동 감지", "com_nav_lang_brazilian_portuguese": "Português Brasileiro", "com_nav_lang_catalan": "카탈로니아어", @@ -386,6 +424,7 @@ "com_nav_lang_italian": "Italiano", "com_nav_lang_japanese": "日本語", "com_nav_lang_korean": "한국어", + "com_nav_lang_latvian": "라트비아어", "com_nav_lang_persian": "페르시아어", "com_nav_lang_polish": "Polski", "com_nav_lang_portuguese": "Português", @@ -395,12 +434,17 @@ "com_nav_lang_thai": "ไทย", "com_nav_lang_traditional_chinese": "繁體中文", "com_nav_lang_turkish": "Türkçe", + "com_nav_lang_uyghur": "위구르어", "com_nav_lang_vietnamese": "Tiếng Việt", "com_nav_language": "언어", "com_nav_latex_parsing": "메시지에서 LaTeX 구문 분석(성능에 영향을 줄 수 있음)", "com_nav_log_out": "로그아웃", "com_nav_long_audio_warning": "긴 텍스트일수록 처리 시간이 더 오래 걸립니다.", "com_nav_maximize_chat_space": "채팅창 최대화", + "com_nav_mcp_configure_server": "{{0}} 설정", + "com_nav_mcp_status_connecting": "{{0}} - 연결 중", + "com_nav_mcp_vars_update_error": "MCP 사용자 정의 변수 업데이트 오류: {{0}}", + "com_nav_mcp_vars_updated": "MCP 사용자 정의 변수가 성공적으로 업데이트되었습니다.", "com_nav_modular_chat": "대화 중간에 엔드포인트 전환 허용", "com_nav_my_files": "내 파일", "com_nav_not_supported": "지원되지 않음", @@ -424,6 +468,8 @@ "com_nav_setting_chat": "채팅", "com_nav_setting_data": "데이터 제어", "com_nav_setting_general": "일반", + "com_nav_setting_mcp": "MCP 설정", + "com_nav_setting_personalization": "개인화", "com_nav_setting_speech": "음성", "com_nav_settings": "설정", "com_nav_shared_links": "공유 링크", @@ -456,6 +502,7 @@ "com_sidepanel_conversation_tags": "북마크", "com_sidepanel_hide_panel": "패널 숨기기", "com_sidepanel_manage_files": "파일 관리", + "com_sidepanel_mcp_no_servers_with_vars": "설정 가능한 변수가 있는 MCP 서버가 없습니다.", "com_sidepanel_parameters": "매개변수", "com_sources_image_alt": "검색 결과 이미지", "com_sources_more_sources": "+{{count}}개 소스", @@ -475,6 +522,7 @@ "com_ui_2fa_verified": "이단계 인증이 성공적으로 인증되었습니다", "com_ui_accept": "동의합니다", "com_ui_action_button": "액션 버튼", + "com_ui_active": "활성", "com_ui_add": "추가", "com_ui_add_mcp": "MCP 추가", "com_ui_add_mcp_server": "MCP 서버 추가", @@ -527,6 +575,7 @@ "com_ui_archive_error": "대화 아카이브 실패", "com_ui_artifact_click": "클릭하여 열기", "com_ui_artifacts": "아티팩트", + "com_ui_artifacts_options": "아티팩트 옵션", "com_ui_artifacts_toggle": "아티팩트 UI 표시/숨기기", "com_ui_artifacts_toggle_agent": "아티팩트 활성화", "com_ui_ascending": "오름차순", @@ -544,11 +593,14 @@ "com_ui_attachment": "첨부 파일", "com_ui_auth_type": "인증 유형", "com_ui_auth_url": "인증 URL", + "com_ui_authenticate": "인증", "com_ui_authentication": "인증", "com_ui_authentication_type": "인증 방식", + "com_ui_auto": "자동", "com_ui_available_tools": "사용 가능 툴", "com_ui_avatar": "프로필 사진", "com_ui_azure": "Azure", + "com_ui_back": "뒤로", "com_ui_back_to_chat": "채팅으로 돌아가기", "com_ui_back_to_prompts": "프롬프트로 돌아가기", "com_ui_backup_codes": "백업 코드", @@ -588,16 +640,21 @@ "com_ui_client_secret": "클라이언트 비밀", "com_ui_close": "닫기", "com_ui_close_menu": "메뉴 닫기", + "com_ui_close_window": "창 닫기", "com_ui_code": "코드", "com_ui_collapse_chat": "채팅 접기", "com_ui_command_placeholder": "선택 사항: 프롬프트에 대한 명령어를 입력하세요. 입력하지 않으면 이름이 사용됩니다.", "com_ui_command_usage_placeholder": "명령어나 이름으로 프롬프트 선택", "com_ui_complete_setup": "설정 완료", + "com_ui_concise": "간결", + "com_ui_configure_mcp_variables_for": "{{0}}의 변수 설정", "com_ui_confirm_action": "작업 확인", "com_ui_confirm_admin_use_change": "이 설정을 변경하면 관리자 포함 모든 사용자의 접근이 차단됩니다. 계속하시겠습니까?", "com_ui_confirm_change": "변경 확인", + "com_ui_connecting": "연결 중", "com_ui_context": "맥락", "com_ui_continue": "계속", + "com_ui_continue_oauth": "OAuth로 계속하기", "com_ui_controls": "컨트롤", "com_ui_convo_delete_error": "대화 삭제 실패", "com_ui_copied": "복사됨", @@ -610,6 +667,7 @@ "com_ui_create_memory": "메모리 생성", "com_ui_create_prompt": "프롬프트 만들기", "com_ui_creating_image": "이미지 생성 중입니다. 잠시 기다려 주세요.", + "com_ui_current": "현재", "com_ui_currently_production": "현재 프로덕션 중", "com_ui_custom": "사용자 지정", "com_ui_custom_header_name": "사용자 지정 헤더 이름", @@ -647,15 +705,19 @@ "com_ui_delete_mcp_error": "MCP 서버 삭제 실패", "com_ui_delete_mcp_success": "MCP 서버 삭제 완료", "com_ui_delete_memory": "메모리 삭제", + "com_ui_delete_not_allowed": "삭제 작업이 허용되지 않습니다", "com_ui_delete_prompt": "프롬프트를 삭제하시겠습니까?", "com_ui_delete_shared_link": "공유 링크를 삭제하시겠습니까?", + "com_ui_delete_success": "성공적으로 삭제됨", "com_ui_delete_tool": "도구 삭제", "com_ui_delete_tool_confirm": "이 도구를 삭제하시겠습니까?", "com_ui_deleted": "삭제 완료", + "com_ui_deleting_file": "파일 삭제 중...", "com_ui_descending": "내림차순", "com_ui_description": "설명", "com_ui_description_placeholder": "선택 사항: 프롬프트에 표시할 설명을 입력하세요", "com_ui_deselect_all": "모두 선택 해제", + "com_ui_detailed": "상세", "com_ui_disabling": "비활성화 중...", "com_ui_download": "다운로드", "com_ui_download_artifact": "아티팩트 다운로드", @@ -697,6 +759,7 @@ "com_ui_feedback_tag_attention_to_detail": "디테일 함", "com_ui_feedback_tag_bad_style": "표현이나 말투가 어색함", "com_ui_feedback_tag_clear_well_written": "글이 분명하고 매끄럽게 작성됨", + "com_ui_feedback_tag_creative_solution": "창의적인 해결책", "com_ui_feedback_tag_inaccurate": "정확하지 않거나 잘못된 응답", "com_ui_feedback_tag_missing_image": "이미지가 포함될 줄 알았음", "com_ui_feedback_tag_not_helpful": "유용한 정보가 부족함", @@ -716,6 +779,7 @@ "com_ui_fork_change_default": "기본 포크 옵션", "com_ui_fork_default": "기본 포크 옵션 사용", "com_ui_fork_error": "대화 분기 중 오류가 발생했습니다", + "com_ui_fork_error_rate_limit": "포크 요청이 너무 많습니다. 나중에 다시 시도하세요", "com_ui_fork_from_message": "포크 옵션 선택", "com_ui_fork_info_1": "이 설정을 사용하면 원하는 동작으로 메시지를 분기할 수 있습니다.", "com_ui_fork_info_2": "\"포킹(Forking)\"은 현재 대화에서 특정 메시지를 시작/종료 지점으로 하여 새로운 대화를 생성하고, 선택한 옵션에 따라 복사본을 만드는 것을 의미합니다.", @@ -748,7 +812,9 @@ "com_ui_good_morning": "좋은 아침입니다", "com_ui_happy_birthday": "내 첫 생일이야!", "com_ui_hide_image_details": "이미지 세부정보 숨기기", + "com_ui_hide_password": "비밀번호 숨기기", "com_ui_hide_qr": "QR 코드 숨기기", + "com_ui_high": "높음", "com_ui_host": "호스트", "com_ui_icon": "아이콘", "com_ui_idea": "아이디어", @@ -775,10 +841,21 @@ "com_ui_loading": "로딩 중...", "com_ui_locked": "잠김", "com_ui_logo": "{{0}} 로고", + "com_ui_low": "낮음", "com_ui_manage": "관리", "com_ui_max_tags": "최대 {{0}}개까지만 허용됩니다. 최신 값을 사용 중입니다.", + "com_ui_mcp_authenticated_success": "MCP 서버 '{{0}}'가 성공적으로 인증되었습니다", + "com_ui_mcp_enter_var": "{{0}}의 값 입력", + "com_ui_mcp_init_failed": "MCP 서버 초기화 실패", + "com_ui_mcp_initialize": "초기화", + "com_ui_mcp_initialized_success": "MCP 서버 '{{0}}'가 성공적으로 초기화되었습니다", + "com_ui_mcp_oauth_cancelled": "{{0}}의 OAuth 로그인이 취소되었습니다", + "com_ui_mcp_oauth_timeout": "{{0}}의 OAuth 로그인 시간이 초과되었습니다", + "com_ui_mcp_server_not_found": "서버를 찾을 수 없습니다.", "com_ui_mcp_servers": "MCP 서버", + "com_ui_mcp_update_var": "{{0}} 업데이트", "com_ui_mcp_url": "MCP 서버 URL", + "com_ui_medium": "중간", "com_ui_memories": "메모리", "com_ui_memories_allow_create": "메모리 생성 허용", "com_ui_memories_allow_opt_out": "사용자가 메모리 기능을 비활성화할 수 있도록 허용", @@ -787,12 +864,17 @@ "com_ui_memories_allow_use": "메모리 사용 허용", "com_ui_memories_filter": "메모리 필터링...", "com_ui_memory": "메모리", + "com_ui_memory_already_exceeded": "메모리 저장소가 이미 가득 참 - {{tokens}} 토큰 초과. 새로운 메모리를 추가하기 전에 기존 메모리를 삭제하세요.", "com_ui_memory_created": "메모리 생성 완료", "com_ui_memory_deleted": "메모리 삭제 완료", "com_ui_memory_deleted_items": "삭제된 메모리", + "com_ui_memory_error": "메모리 오류", "com_ui_memory_key_exists": "이 키를 가진 메모리가 이미 존재합니다. 다른 키를 사용해주세요.", + "com_ui_memory_key_validation": "메모리 키는 소문자와 밑줄만 포함해야 합니다.", + "com_ui_memory_storage_full": "메모리 저장소가 가득 참", "com_ui_memory_updated": "저장된 메모리 업데이트 완료", "com_ui_memory_updated_items": "저장된 메모리", + "com_ui_memory_would_exceed": "저장할 수 없음 - {{tokens}} 토큰 제한 초과. 공간을 확보하기 위해 기존 메모리를 삭제하세요.", "com_ui_mention": "엔드포인트, 어시스턴트 또는 프리셋을 언급하여 빠르게 전환하세요", "com_ui_min_tags": "최소 {{0}}개는 필수로 입력해야 합니다. 더 이상 값을 제거할 수 없습니다.", "com_ui_misc": "기타", @@ -819,8 +901,17 @@ "com_ui_not_used": "미사용", "com_ui_nothing_found": "찾을 수 없습니다", "com_ui_oauth": "OAuth", + "com_ui_oauth_connected_to": "연결됨:", + "com_ui_oauth_error_callback_failed": "인증 콜백이 실패했습니다. 다시 시도하세요.", + "com_ui_oauth_error_generic": "인증이 실패했습니다. 다시 시도하세요.", + "com_ui_oauth_error_missing_code": "인증 코드가 누락되었습니다. 다시 시도하세요.", + "com_ui_oauth_error_missing_state": "상태 파라미터가 누락되었습니다. 다시 시도하세요.", + "com_ui_oauth_error_title": "인증 실패", + "com_ui_oauth_success_description": "인증에 성공했습니다. 이 창은 닫힙니다.", + "com_ui_oauth_success_title": "인증 성공", "com_ui_of": "/", "com_ui_off": "꺼짐", + "com_ui_offline": "오프라인", "com_ui_on": "켜기", "com_ui_openai": "OpenAI", "com_ui_optional": "(선택사항)", @@ -843,6 +934,7 @@ "com_ui_prompts_allow_share_global": "모든 사용자와 프롬프트 공유 허용", "com_ui_prompts_allow_use": "프롬프트 사용 허용", "com_ui_provider": "제공자", + "com_ui_quality": "품질", "com_ui_read_aloud": "소리내어 읽기", "com_ui_redirecting_to_provider": "{{0}}로 이동하는 중입니다. 잠시 기다리세요...", "com_ui_reference_saved_memories": "저장된 메모리 참고", @@ -852,12 +944,14 @@ "com_ui_regenerate_backup": "백업 코드 재생성", "com_ui_regenerating": "재생성 중...", "com_ui_region": "지역", + "com_ui_reinitialize": "다시 초기화", "com_ui_rename": "이름 바꾸기", "com_ui_rename_conversation": "대화 이름 변경", "com_ui_rename_failed": "대화 이름 변경 실패", "com_ui_rename_prompt": "프롬프트 이름 변경", "com_ui_requires_auth": "인증이 필요합니다", "com_ui_reset_var": "{{0}} 초기화", + "com_ui_reset_zoom": "초기화", "com_ui_result": "결과", "com_ui_revoke": "취소", "com_ui_revoke_info": "사용자가 제공한 자격 증명을 모두 취소합니다.", @@ -873,9 +967,11 @@ "com_ui_save_badge_changes": "배지 변경 사항 저장하시겠습니까?", "com_ui_save_submit": "저장 및 제출", "com_ui_saved": "저장되었습니다!", + "com_ui_saving": "저장 중...", "com_ui_schema": "스키마", "com_ui_scope": "범위", "com_ui_search": "검색", + "com_ui_seconds": "초", "com_ui_secret_key": "비밀 키", "com_ui_select": "선택", "com_ui_select_all": "모두 선택", @@ -888,6 +984,7 @@ "com_ui_select_search_plugin": "이름으로 플러그인 검색", "com_ui_select_search_provider": "이름으로 공급자 검색", "com_ui_select_search_region": "이름으로 지역 검색", + "com_ui_set": "설정", "com_ui_share": "공유하기", "com_ui_share_create_message": "이름과 공유 후에 추가하는 메시지는 비공개로 유지됩니다.", "com_ui_share_delete_error": "공유 링크를 삭제하는 중에 오류가 발생했습니다.", @@ -905,6 +1002,7 @@ "com_ui_show": "보기", "com_ui_show_all": "전체 보기", "com_ui_show_image_details": "이미지 세부사항 보기", + "com_ui_show_password": "비밀번호 표시", "com_ui_show_qr": "QR 코드 보기", "com_ui_sign_in_to_domain": "{{0}}에 로그인", "com_ui_simple": "간단", @@ -930,12 +1028,16 @@ "com_ui_token_exchange_method": "토큰 교환 방식", "com_ui_token_url": "토큰 URL", "com_ui_tokens": "토큰", + "com_ui_tool_collection_prefix": "제공하는 도구 모음", + "com_ui_tool_info": "도구 정보", + "com_ui_tool_more_info": "이 도구에 대한 추가 정보", "com_ui_tools": "도구", "com_ui_travel": "여행", "com_ui_trust_app": "신뢰할 수 있는 어플리케이션", "com_ui_unarchive": "아카이브 해제", "com_ui_unarchive_error": "대화 아카이브 해제 실패", "com_ui_unknown": "알 수 없음", + "com_ui_unset": "설정 해제", "com_ui_untitled": "제목 없음", "com_ui_update": "업데이트", "com_ui_update_mcp_error": " MCP 생성 혹은 업데이트 중 오류가 발생했습니다.", @@ -974,6 +1076,7 @@ "com_ui_web_search_jina_key": "Jina API 키 입력", "com_ui_web_search_processing": "결과 처리 중", "com_ui_web_search_provider": "검색 제공자", + "com_ui_web_search_provider_searxng": "SearXNG", "com_ui_web_search_provider_serper": "Serper API", "com_ui_web_search_provider_serper_key": "Serper API 키 발급받기", "com_ui_web_search_reading": "결과 읽기 중", @@ -985,6 +1088,8 @@ "com_ui_web_search_scraper": "스크래퍼", "com_ui_web_search_scraper_firecrawl": "Firecrawl API", "com_ui_web_search_scraper_firecrawl_key": "Firecrawl API 키 발급받기", + "com_ui_web_search_searxng_api_key": "SearXNG API 키 입력 (선택사항)", + "com_ui_web_search_searxng_instance_url": "SearXNG 인스턴스 URL", "com_ui_web_searching": "웹 검색 진행 중", "com_ui_web_searching_again": "웹 검색 다시 진행", "com_ui_weekend_morning": "행복한 주말 되세요", @@ -993,4 +1098,4 @@ "com_ui_yes": "네", "com_ui_zoom": "확대/축소", "com_user_message": "당신" -} \ No newline at end of file +} diff --git a/client/src/locales/lv/translation.json b/client/src/locales/lv/translation.json index bc852c349..e95abce66 100644 --- a/client/src/locales/lv/translation.json +++ b/client/src/locales/lv/translation.json @@ -1104,4 +1104,4 @@ "com_ui_yes": "Jā", "com_ui_zoom": "Tālummaiņa", "com_user_message": "Tu" -} \ No newline at end of file +} diff --git a/client/src/locales/nl/translation.json b/client/src/locales/nl/translation.json index d357709d1..30eaee38c 100644 --- a/client/src/locales/nl/translation.json +++ b/client/src/locales/nl/translation.json @@ -352,4 +352,4 @@ "com_ui_unarchive": "Uit archiveren", "com_ui_unarchive_error": "Kan conversatie niet uit archiveren", "com_ui_upload_success": "Bestand succesvol geüpload" -} \ No newline at end of file +} diff --git a/client/src/locales/pl/translation.json b/client/src/locales/pl/translation.json index e4d9b7487..869443501 100644 --- a/client/src/locales/pl/translation.json +++ b/client/src/locales/pl/translation.json @@ -717,4 +717,4 @@ "com_ui_yes": "Tak", "com_ui_zoom": "Powiększ", "com_user_message": "Ty" -} \ 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 f60ac45d6..186021436 100644 --- a/client/src/locales/pt-BR/translation.json +++ b/client/src/locales/pt-BR/translation.json @@ -950,4 +950,4 @@ "com_ui_yes": "Sim", "com_ui_zoom": "Zoom", "com_user_message": "Você" -} \ No newline at end of file +} diff --git a/client/src/locales/pt-PT/translation.json b/client/src/locales/pt-PT/translation.json index ad9116bb5..c55155a94 100644 --- a/client/src/locales/pt-PT/translation.json +++ b/client/src/locales/pt-PT/translation.json @@ -917,4 +917,4 @@ "com_ui_yes": "Sim", "com_ui_zoom": "Ampliar", "com_user_message": "Você" -} \ No newline at end of file +} diff --git a/client/src/locales/ru/translation.json b/client/src/locales/ru/translation.json index 4a82cc162..b3b14bbee 100644 --- a/client/src/locales/ru/translation.json +++ b/client/src/locales/ru/translation.json @@ -862,4 +862,4 @@ "com_ui_yes": "Да", "com_ui_zoom": "Масштаб", "com_user_message": "Вы" -} \ No newline at end of file +} diff --git a/client/src/locales/sv/translation.json b/client/src/locales/sv/translation.json index 8bf9ebddd..3ff377417 100644 --- a/client/src/locales/sv/translation.json +++ b/client/src/locales/sv/translation.json @@ -421,4 +421,4 @@ "com_ui_unarchive": "Avarkivera", "com_ui_unarchive_error": "Kunde inte avarkivera chatt", "com_ui_upload_success": "Uppladdningen av filen lyckades" -} \ No newline at end of file +} diff --git a/client/src/locales/th/translation.json b/client/src/locales/th/translation.json index 733e75eab..716db5e7c 100644 --- a/client/src/locales/th/translation.json +++ b/client/src/locales/th/translation.json @@ -800,4 +800,4 @@ "com_ui_yes": "ใช่", "com_ui_zoom": "ขยาย", "com_user_message": "คุณ" -} \ No newline at end of file +} diff --git a/client/src/locales/tr/translation.json b/client/src/locales/tr/translation.json index 64ce86322..549cd8f44 100644 --- a/client/src/locales/tr/translation.json +++ b/client/src/locales/tr/translation.json @@ -724,4 +724,4 @@ "com_ui_yes": "Evet", "com_ui_zoom": "Yakınlaştır", "com_user_message": "Sen" -} \ No newline at end of file +} diff --git a/client/src/locales/vi/translation.json b/client/src/locales/vi/translation.json index 8e4188f1f..866ba4946 100644 --- a/client/src/locales/vi/translation.json +++ b/client/src/locales/vi/translation.json @@ -382,4 +382,4 @@ "com_ui_yes": "Đồng ý", "com_ui_zoom": "Phóng", "com_warning_resubmit_unsupported": "Điểm cuối này không hỗ trợ việc gửi lại tin nhắn AI." -} \ 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 0b5832f7a..1e59d765a 100644 --- a/client/src/locales/zh-Hans/translation.json +++ b/client/src/locales/zh-Hans/translation.json @@ -1103,4 +1103,4 @@ "com_ui_yes": "是的", "com_ui_zoom": "缩放", "com_user_message": "您" -} \ 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 52cbeb644..a47bfe8f0 100644 --- a/client/src/locales/zh-Hant/translation.json +++ b/client/src/locales/zh-Hant/translation.json @@ -777,4 +777,4 @@ "com_ui_yes": "是", "com_ui_zoom": "縮放", "com_user_message": "您" -} \ No newline at end of file +} From 33c8b87edd0ee58c454a76bc167bc137af572739 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 1 Aug 2025 16:10:12 -0400 Subject: [PATCH 045/224] =?UTF-8?q?=F0=9F=93=A6=20chore:=20Bump=20`@modelc?= =?UTF-8?q?ontextprotocol/sdk`=20to=20v1.17.1=20(#8809)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/package.json | 2 +- package-lock.json | 10 +++++----- packages/api/package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/package.json b/api/package.json index 990f37815..c0a983248 100644 --- a/api/package.json +++ b/api/package.json @@ -52,7 +52,7 @@ "@librechat/agents": "^2.4.69", "@librechat/api": "*", "@librechat/data-schemas": "*", - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.1", "@node-saml/passport-saml": "^5.1.0", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", diff --git a/package-lock.json b/package-lock.json index ee490258c..dff97b559 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "@librechat/agents": "^2.4.69", "@librechat/api": "*", "@librechat/data-schemas": "*", - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.1", "@node-saml/passport-saml": "^5.1.0", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", @@ -22442,9 +22442,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.0.tgz", - "integrity": "sha512-qFfbWFA7r1Sd8D697L7GkTd36yqDuTkvz0KfOGkgXR8EUhQn3/EDNIR/qUdQNMT8IjmasBvHWuXeisxtXTQT2g==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.1.tgz", + "integrity": "sha512-CPle1OQehbWqd25La9Ack5B07StKIxh4+Bf19qnpZKJC1oI22Y0czZHbifjw1UoczIfKBwBDAp/dFxvHG13B5A==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -51416,7 +51416,7 @@ "@langchain/core": "^0.3.62", "@librechat/agents": "^2.4.69", "@librechat/data-schemas": "*", - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.8.2", "diff": "^7.0.0", "eventsource": "^3.0.2", diff --git a/packages/api/package.json b/packages/api/package.json index 12bbd1824..5cebb81f2 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -72,7 +72,7 @@ "@langchain/core": "^0.3.62", "@librechat/agents": "^2.4.69", "@librechat/data-schemas": "*", - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.8.2", "diff": "^7.0.0", "eventsource": "^3.0.2", From 863401bcdf133c1dfbc6c0095613b018669580a0 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 2 Aug 2025 12:19:58 -0400 Subject: [PATCH 046/224] =?UTF-8?q?=F0=9F=94=A7=20fix:=20Assistants=20API?= =?UTF-8?q?=20SDK=20calls=20to=20match=20Updated=20Arguments=20(#8818)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: remove comments in agents endpoint error handler * chore: improve openai sdk typing * chore: improve typing for azure asst init * 🔧 fix: Assistants API SDK calls to match Updated Arguments --- api/server/controllers/agents/errors.js | 12 ------------ api/server/controllers/assistants/chatV1.js | 6 +++--- api/server/controllers/assistants/chatV2.js | 2 +- api/server/controllers/assistants/errors.js | 4 ++-- api/server/controllers/assistants/helpers.js | 10 ++++++++++ api/server/controllers/assistants/v1.js | 4 ++-- api/server/middleware/abortRun.js | 4 ++-- api/server/routes/convos.js | 2 +- api/server/services/AssistantService.js | 10 ++++++---- .../services/Endpoints/assistants/initalize.js | 4 ++-- .../services/Endpoints/azureAssistants/initialize.js | 6 +++--- api/server/services/Runs/RunManager.js | 5 ++--- api/server/services/Runs/StreamRunManager.js | 2 +- api/server/services/Runs/handle.js | 2 +- api/server/services/Threads/manage.js | 8 +++++--- 15 files changed, 41 insertions(+), 40 deletions(-) diff --git a/api/server/controllers/agents/errors.js b/api/server/controllers/agents/errors.js index b3bb1cea6..8c8418248 100644 --- a/api/server/controllers/agents/errors.js +++ b/api/server/controllers/agents/errors.js @@ -105,8 +105,6 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch return res.end(); } await cache.delete(cacheKey); - // const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id); - // logger.debug(`[${originPath}] Cancelled run:`, cancelledRun); } catch (error) { logger.error(`[${originPath}] Error cancelling run`, error); } @@ -115,7 +113,6 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch let run; try { - // run = await openai.beta.threads.runs.retrieve(thread_id, run_id); await recordUsage({ ...run.usage, model: run.model, @@ -128,18 +125,9 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch let finalEvent; try { - // const errorContentPart = { - // text: { - // value: - // error?.message ?? 'There was an error processing your request. Please try again later.', - // }, - // type: ContentTypes.ERROR, - // }; - finalEvent = { final: true, conversation: await getConvo(req.user.id, conversationId), - // runMessages, }; } catch (error) { logger.error(`[${originPath}] Error finalizing error process`, error); diff --git a/api/server/controllers/assistants/chatV1.js b/api/server/controllers/assistants/chatV1.js index b4fe0d901..09770b56d 100644 --- a/api/server/controllers/assistants/chatV1.js +++ b/api/server/controllers/assistants/chatV1.js @@ -152,7 +152,7 @@ const chatV1 = async (req, res) => { return res.end(); } await cache.delete(cacheKey); - const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id); + const cancelledRun = await openai.beta.threads.runs.cancel(run_id, { thread_id }); logger.debug('[/assistants/chat/] Cancelled run:', cancelledRun); } catch (error) { logger.error('[/assistants/chat/] Error cancelling run', error); @@ -162,7 +162,7 @@ const chatV1 = async (req, res) => { let run; try { - run = await openai.beta.threads.runs.retrieve(thread_id, run_id); + run = await openai.beta.threads.runs.retrieve(run_id, { thread_id }); await recordUsage({ ...run.usage, model: run.model, @@ -623,7 +623,7 @@ const chatV1 = async (req, res) => { if (!response.run.usage) { await sleep(3000); - completedRun = await openai.beta.threads.runs.retrieve(thread_id, response.run.id); + completedRun = await openai.beta.threads.runs.retrieve(response.run.id, { thread_id }); if (completedRun.usage) { await recordUsage({ ...completedRun.usage, diff --git a/api/server/controllers/assistants/chatV2.js b/api/server/controllers/assistants/chatV2.js index e1ba93bc2..c569dc837 100644 --- a/api/server/controllers/assistants/chatV2.js +++ b/api/server/controllers/assistants/chatV2.js @@ -467,7 +467,7 @@ const chatV2 = async (req, res) => { if (!response.run.usage) { await sleep(3000); - completedRun = await openai.beta.threads.runs.retrieve(thread_id, response.run.id); + completedRun = await openai.beta.threads.runs.retrieve(response.run.id, { thread_id }); if (completedRun.usage) { await recordUsage({ ...completedRun.usage, diff --git a/api/server/controllers/assistants/errors.js b/api/server/controllers/assistants/errors.js index 182b230fb..1c76b138c 100644 --- a/api/server/controllers/assistants/errors.js +++ b/api/server/controllers/assistants/errors.js @@ -108,7 +108,7 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch return res.end(); } await cache.delete(cacheKey); - const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id); + const cancelledRun = await openai.beta.threads.runs.cancel(run_id, { thread_id }); logger.debug(`[${originPath}] Cancelled run:`, cancelledRun); } catch (error) { logger.error(`[${originPath}] Error cancelling run`, error); @@ -118,7 +118,7 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch let run; try { - run = await openai.beta.threads.runs.retrieve(thread_id, run_id); + run = await openai.beta.threads.runs.retrieve(run_id, { thread_id }); await recordUsage({ ...run.usage, model: run.model, diff --git a/api/server/controllers/assistants/helpers.js b/api/server/controllers/assistants/helpers.js index f5735f0b8..1bbc0915b 100644 --- a/api/server/controllers/assistants/helpers.js +++ b/api/server/controllers/assistants/helpers.js @@ -173,6 +173,16 @@ const listAssistantsForAzure = async ({ req, res, version, azureConfig = {}, que }; }; +/** + * Initializes the OpenAI client. + * @param {object} params - The parameters object. + * @param {ServerRequest} params.req - The request object. + * @param {ServerResponse} params.res - The response object. + * @param {TEndpointOption} params.endpointOption - The endpoint options. + * @param {boolean} params.initAppClient - Whether to initialize the app client. + * @param {string} params.overrideEndpoint - The endpoint to override. + * @returns {Promise<{ openai: OpenAIClient, openAIApiKey: string; client: import('~/app/clients/OpenAIClient') }>} - The initialized OpenAI client. + */ async function getOpenAIClient({ req, res, endpointOption, initAppClient, overrideEndpoint }) { let endpoint = overrideEndpoint ?? req.body.endpoint ?? req.query.endpoint; const version = await getCurrentVersion(req, endpoint); diff --git a/api/server/controllers/assistants/v1.js b/api/server/controllers/assistants/v1.js index e723cda4f..10c59d913 100644 --- a/api/server/controllers/assistants/v1.js +++ b/api/server/controllers/assistants/v1.js @@ -197,7 +197,7 @@ const deleteAssistant = async (req, res) => { await validateAuthor({ req, openai }); const assistant_id = req.params.id; - const deletionStatus = await openai.beta.assistants.del(assistant_id); + const deletionStatus = await openai.beta.assistants.delete(assistant_id); if (deletionStatus?.deleted) { await deleteAssistantActions({ req, assistant_id }); } @@ -365,7 +365,7 @@ const uploadAssistantAvatar = async (req, res) => { try { await fs.unlink(req.file.path); logger.debug('[/:agent_id/avatar] Temp. image upload file deleted'); - } catch (error) { + } catch { logger.debug('[/:agent_id/avatar] Temp. image upload file already deleted'); } } diff --git a/api/server/middleware/abortRun.js b/api/server/middleware/abortRun.js index 2846c6eef..dd50d7bf9 100644 --- a/api/server/middleware/abortRun.js +++ b/api/server/middleware/abortRun.js @@ -47,7 +47,7 @@ async function abortRun(req, res) { try { await cache.set(cacheKey, 'cancelled', three_minutes); - const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id); + const cancelledRun = await openai.beta.threads.runs.cancel(run_id, { thread_id }); logger.debug('[abortRun] Cancelled run:', cancelledRun); } catch (error) { logger.error('[abortRun] Error cancelling run', error); @@ -60,7 +60,7 @@ async function abortRun(req, res) { } try { - const run = await openai.beta.threads.runs.retrieve(thread_id, run_id); + const run = await openai.beta.threads.runs.retrieve(run_id, { thread_id }); await recordUsage({ ...run.usage, model: run.model, diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 18dbf8db0..7113f2d63 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -111,7 +111,7 @@ router.delete('/', async (req, res) => { /** @type {{ openai: OpenAI }} */ const { openai } = await assistantClients[endpoint].initializeClient({ req, res }); try { - const response = await openai.beta.threads.del(thread_id); + const response = await openai.beta.threads.delete(thread_id); logger.debug('Deleted OpenAI thread:', response); } catch (error) { logger.error('Error deleting OpenAI thread:', error); diff --git a/api/server/services/AssistantService.js b/api/server/services/AssistantService.js index 5354b2e33..a9ac26e47 100644 --- a/api/server/services/AssistantService.js +++ b/api/server/services/AssistantService.js @@ -281,7 +281,7 @@ function createInProgressHandler(openai, thread_id, messages) { openai.seenCompletedMessages.add(message_id); - const message = await openai.beta.threads.messages.retrieve(thread_id, message_id); + const message = await openai.beta.threads.messages.retrieve(message_id, { thread_id }); if (!message?.content?.length) { return; } @@ -435,9 +435,11 @@ async function runAssistant({ }; }); - const outputs = await processRequiredActions(openai, actions); - - const toolRun = await openai.beta.threads.runs.submitToolOutputs(run.thread_id, run.id, outputs); + const tool_outputs = await processRequiredActions(openai, actions); + const toolRun = await openai.beta.threads.runs.submitToolOutputs(run.id, { + thread_id: run.thread_id, + tool_outputs, + }); // Recursive call with accumulated steps and messages return await runAssistant({ diff --git a/api/server/services/Endpoints/assistants/initalize.js b/api/server/services/Endpoints/assistants/initalize.js index fe37e3410..9ef5228e3 100644 --- a/api/server/services/Endpoints/assistants/initalize.js +++ b/api/server/services/Endpoints/assistants/initalize.js @@ -6,7 +6,7 @@ const { getUserKeyExpiry, checkUserKeyExpiry, } = require('~/server/services/UserService'); -const OpenAIClient = require('~/app/clients/OpenAIClient'); +const OAIClient = require('~/app/clients/OpenAIClient'); const { isUserProvided } = require('~/server/utils'); const initializeClient = async ({ req, res, endpointOption, version, initAppClient = false }) => { @@ -79,7 +79,7 @@ const initializeClient = async ({ req, res, endpointOption, version, initAppClie openai.res = res; if (endpointOption && initAppClient) { - const client = new OpenAIClient(apiKey, clientOptions); + const client = new OAIClient(apiKey, clientOptions); return { client, openai, diff --git a/api/server/services/Endpoints/azureAssistants/initialize.js b/api/server/services/Endpoints/azureAssistants/initialize.js index e002da566..51d52b8ac 100644 --- a/api/server/services/Endpoints/azureAssistants/initialize.js +++ b/api/server/services/Endpoints/azureAssistants/initialize.js @@ -3,11 +3,11 @@ const { ProxyAgent } = require('undici'); const { constructAzureURL, isUserProvided, resolveHeaders } = require('@librechat/api'); const { ErrorTypes, EModelEndpoint, mapModelToAzureConfig } = require('librechat-data-provider'); const { + checkUserKeyExpiry, getUserKeyValues, getUserKeyExpiry, - checkUserKeyExpiry, } = require('~/server/services/UserService'); -const OpenAIClient = require('~/app/clients/OpenAIClient'); +const OAIClient = require('~/app/clients/OpenAIClient'); class Files { constructor(client) { @@ -184,7 +184,7 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie } if (endpointOption && initAppClient) { - const client = new OpenAIClient(apiKey, clientOptions); + const client = new OAIClient(apiKey, clientOptions); return { client, openai, diff --git a/api/server/services/Runs/RunManager.js b/api/server/services/Runs/RunManager.js index c8deeb926..39e577fb5 100644 --- a/api/server/services/Runs/RunManager.js +++ b/api/server/services/Runs/RunManager.js @@ -91,11 +91,10 @@ class RunManager { * @param {boolean} [params.final] - The end of the run polling loop, due to `requires_action`, `cancelling`, `cancelled`, `failed`, `completed`, or `expired` statuses. */ async fetchRunSteps({ openai, thread_id, run_id, runStatus, final = false }) { - // const { data: steps, first_id, last_id, has_more } = await openai.beta.threads.runs.steps.list(thread_id, run_id); + // const { data: steps, first_id, last_id, has_more } = await openai.beta.threads.runs.steps.list(run_id, { thread_id }); const { data: _steps } = await openai.beta.threads.runs.steps.list( - thread_id, run_id, - {}, + { thread_id }, { timeout: 3000, maxRetries: 5, diff --git a/api/server/services/Runs/StreamRunManager.js b/api/server/services/Runs/StreamRunManager.js index 4f6994e0c..b8cc57e8c 100644 --- a/api/server/services/Runs/StreamRunManager.js +++ b/api/server/services/Runs/StreamRunManager.js @@ -573,9 +573,9 @@ class StreamRunManager { let toolRun; try { toolRun = this.openai.beta.threads.runs.submitToolOutputsStream( - run.thread_id, run.id, { + thread_id: run.thread_id, tool_outputs, stream: true, }, diff --git a/api/server/services/Runs/handle.js b/api/server/services/Runs/handle.js index dd048219b..e49f46a69 100644 --- a/api/server/services/Runs/handle.js +++ b/api/server/services/Runs/handle.js @@ -179,7 +179,7 @@ async function waitForRun({ * @return {Promise} A promise that resolves to an array of RunStep objects. */ async function _retrieveRunSteps({ openai, thread_id, run_id }) { - const runSteps = await openai.beta.threads.runs.steps.list(thread_id, run_id); + const runSteps = await openai.beta.threads.runs.steps.list(run_id, { thread_id }); return runSteps; } diff --git a/api/server/services/Threads/manage.js b/api/server/services/Threads/manage.js index 5eace214c..4cc1e107e 100644 --- a/api/server/services/Threads/manage.js +++ b/api/server/services/Threads/manage.js @@ -192,7 +192,8 @@ async function addThreadMetadata({ openai, thread_id, messageId, messages }) { const promises = []; for (const message of messages) { promises.push( - openai.beta.threads.messages.update(thread_id, message.id, { + openai.beta.threads.messages.update(message.id, { + thread_id, metadata: { messageId, }, @@ -263,7 +264,8 @@ async function syncMessages({ } modifyPromises.push( - openai.beta.threads.messages.update(thread_id, apiMessage.id, { + openai.beta.threads.messages.update(apiMessage.id, { + thread_id, metadata: { messageId: dbMessage.messageId, }, @@ -413,7 +415,7 @@ async function checkMessageGaps({ }) { const promises = []; promises.push(openai.beta.threads.messages.list(thread_id, defaultOrderQuery)); - promises.push(openai.beta.threads.runs.steps.list(thread_id, run_id)); + promises.push(openai.beta.threads.runs.steps.list(run_id, { thread_id })); /** @type {[{ data: ThreadMessage[] }, { data: RunStep[] }]} */ const [response, stepsResponse] = await Promise.all(promises); From bc43423f581daf70221c9043a20f817689d2a781 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 2 Aug 2025 12:37:18 -0400 Subject: [PATCH 047/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Add=20Tibetan=20?= =?UTF-8?q?and=20Ukrainian=20languages=20to=20localization=20(#8819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌍 i18n: Add Tibetan and Ukrainian languages to localization * feat: Update language selector to include Tibetan and Ukrainian options * feat: Add translation files for Tibetan and Ukrainian languages * chore: Update English translation.json with new language keys * docs: Create localization guide for adding new languages * Update README.md --- .../Nav/SettingsTabs/General/General.tsx | 2 + client/src/locales/README.md | 135 ++++++++++++++++++ client/src/locales/bo/translation.json | 1 + client/src/locales/en/translation.json | 2 + client/src/locales/i18n.ts | 4 + client/src/locales/uk/translation.json | 1 + 6 files changed, 145 insertions(+) create mode 100644 client/src/locales/README.md create mode 100644 client/src/locales/bo/translation.json create mode 100644 client/src/locales/uk/translation.json diff --git a/client/src/components/Nav/SettingsTabs/General/General.tsx b/client/src/components/Nav/SettingsTabs/General/General.tsx index 886867980..a746c4cf7 100644 --- a/client/src/components/Nav/SettingsTabs/General/General.tsx +++ b/client/src/components/Nav/SettingsTabs/General/General.tsx @@ -105,6 +105,8 @@ export const LangSelector = ({ { value: 'nl-NL', label: localize('com_nav_lang_dutch') }, { value: 'id-ID', label: localize('com_nav_lang_indonesia') }, { value: 'fi-FI', label: localize('com_nav_lang_finnish') }, + { value: 'bo', label: localize('com_nav_lang_tibetan') }, + { value: 'uk-UA', label: localize('com_nav_lang_ukrainian') }, ]; return ( diff --git a/client/src/locales/README.md b/client/src/locales/README.md new file mode 100644 index 000000000..aa40db0f4 --- /dev/null +++ b/client/src/locales/README.md @@ -0,0 +1,135 @@ +# LibreChat Localization Guide + +This guide explains how to add new languages to LibreChat's localization system. + +## Adding a New Language + +To add a new language to LibreChat, follow these steps: + +### 1. Add the Language to Locize Project + +- Navigate to the [LibreChat locize project](https://www.locize.app/cat/62uyy7c9), +- Click the "ADD LANGUAGE" button, typically found within the "..." menu of the "Start to translate" card on the project overview page. + +### 2. Update the Language Selector Component + +Edit `client/src/components/Nav/SettingsTabs/General/General.tsx` and add your new language option to the `languageOptions` array: + +```typescript +{ value: 'language-code', label: localize('com_nav_lang_language_name') }, +``` + +Example: +```typescript +{ value: 'bo', label: localize('com_nav_lang_tibetan') }, +{ value: 'uk-UA', label: localize('com_nav_lang_ukrainian') }, +``` + +**Note:** Use the appropriate language code format: +- Use simple codes (e.g., `bo`) for languages without regional variants +- Use region-specific codes (e.g., `uk-UA`) when needed + +### 3. Add Localization Keys + +In `client/src/locales/en/translation.json`, add the corresponding localization key for your language label: + +```json +"com_nav_lang_language_name": "Native Language Name", +``` + +Example: +```json +"com_nav_lang_tibetan": "བོད་སྐད་", +"com_nav_lang_ukrainian": "Українська", +``` + +**Best Practice:** Use the native language name as the value. + +### 4. Create the Translation File + +Create a new directory and translation file: + +```bash +mkdir -p client/src/locales/[language-code] +``` + +Create `client/src/locales/[language-code]/translation.json` with an empty JSON object: + +```json +{ +} +``` + +Example: +- `client/src/locales/bo/translation.json` +- `client/src/locales/uk/translation.json` + +### 5. Configure i18n + +Update `client/src/locales/i18n.ts`: + +1. Import the new translation file: +```typescript +import translationLanguageCode from './language-code/translation.json'; +``` + +2. Add it to the `resources` object: +```typescript +export const resources = { + // ... existing languages + 'language-code': { translation: translationLanguageCode }, +} as const; +``` + +Example: +```typescript +import translationBo from './bo/translation.json'; +import translationUk from './uk/translation.json'; + +export const resources = { + // ... existing languages + bo: { translation: translationBo }, + uk: { translation: translationUk }, +} as const; +``` + +### 6. Handle Fallback Languages (Optional) + +If your language should fall back to a specific language when translations are missing, update the `fallbackLng` configuration in `i18n.ts`: + +```typescript +fallbackLng: { + 'language-variant': ['fallback-language', 'en'], + // ... existing fallbacks +}, +``` + +## Translation Process + +After adding a new language: + +1. The empty translation file will be populated through LibreChat's automated translation platform +2. Only the English (`en`) translation file should be manually updated +3. Other language translations are managed externally + +## Language Code Standards + +- Use ISO 639-1 codes for most languages (e.g., `en`, `fr`, `de`) +- Use ISO 639-1 with region codes when needed (e.g., `pt-BR`, `zh-Hans`) +- Tibetan uses `bo` (Bodic) +- Ukrainian uses `uk` or `uk-UA` with region + +## Testing + +After adding a new language: + +1. Restart the development server +2. Navigate to Settings > General +3. Verify your language appears in the dropdown +4. Select it to ensure it changes the UI language code + +## Notes + +- Keep language options alphabetically sorted in the dropdown for better UX +- Always use native script for language names in the dropdown +- The system will use English as fallback for any missing translations diff --git a/client/src/locales/bo/translation.json b/client/src/locales/bo/translation.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/client/src/locales/bo/translation.json @@ -0,0 +1 @@ +{} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index ae61f049a..67fa367b1 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -435,8 +435,10 @@ "com_nav_lang_spanish": "Español", "com_nav_lang_swedish": "Svenska", "com_nav_lang_thai": "ไทย", + "com_nav_lang_tibetan": "བོད་སྐད་", "com_nav_lang_traditional_chinese": "繁體中文", "com_nav_lang_turkish": "Türkçe", + "com_nav_lang_ukrainian": "Українська", "com_nav_lang_uyghur": "Uyƣur tili", "com_nav_lang_vietnamese": "Tiếng Việt", "com_nav_language": "Language", diff --git a/client/src/locales/i18n.ts b/client/src/locales/i18n.ts index 223872fe6..9af02cef3 100644 --- a/client/src/locales/i18n.ts +++ b/client/src/locales/i18n.ts @@ -35,6 +35,8 @@ import translationHy from './hy/translation.json'; import translationFi from './fi/translation.json'; import translationZh_Hans from './zh-Hans/translation.json'; import translationZh_Hant from './zh-Hant/translation.json'; +import translationBo from './bo/translation.json'; +import translationUk from './uk/translation.json'; export const defaultNS = 'translation'; @@ -71,6 +73,8 @@ export const resources = { hu: { translation: translationHu }, hy: { translation: translationHy }, fi: { translation: translationFi }, + bo: { translation: translationBo }, + uk: { translation: translationUk }, } as const; i18n diff --git a/client/src/locales/uk/translation.json b/client/src/locales/uk/translation.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/client/src/locales/uk/translation.json @@ -0,0 +1 @@ +{} From 7ef2c626e29af3c42e91cb1de7d057d306456dd1 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 2 Aug 2025 18:04:04 -0400 Subject: [PATCH 048/224] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20feat:=20Add=20R?= =?UTF-8?q?eset-Meili-Sync=20Script=20for=20MongoDB=20Flags=20(#8823)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/reset-meili-sync.js | 92 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 93 insertions(+) create mode 100644 config/reset-meili-sync.js diff --git a/config/reset-meili-sync.js b/config/reset-meili-sync.js new file mode 100644 index 000000000..e29324d6a --- /dev/null +++ b/config/reset-meili-sync.js @@ -0,0 +1,92 @@ +const path = require('path'); +const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose')); +require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); +const { askQuestion, silentExit } = require('./helpers'); +const connect = require('./connect'); + +(async () => { + await connect(); + + console.purple('---------------------------------------'); + console.purple('Reset MeiliSearch Synchronization Flags'); + console.purple('---------------------------------------'); + console.yellow('\nThis script will reset the MeiliSearch indexing flags in MongoDB.'); + console.yellow('Use this when MeiliSearch data has been deleted or corrupted,'); + console.yellow('and you need to trigger a full re-synchronization.\n'); + + const confirm = await askQuestion( + 'Are you sure you want to reset all MeiliSearch sync flags? (y/N): ', + ); + + if (confirm.toLowerCase() !== 'y') { + console.orange('Operation cancelled.'); + silentExit(0); + } + + try { + // Reset _meiliIndex flags for messages + console.cyan('\nResetting message sync flags...'); + const messageResult = await mongoose.connection.db + .collection('messages') + .updateMany({ _meiliIndex: true }, { $set: { _meiliIndex: false } }); + + console.green(`✓ Reset ${messageResult.modifiedCount} message sync flags`); + + // Reset _meiliIndex flags for conversations + console.cyan('\nResetting conversation sync flags...'); + const conversationResult = await mongoose.connection.db + .collection('conversations') + .updateMany({ _meiliIndex: true }, { $set: { _meiliIndex: false } }); + + console.green(`✓ Reset ${conversationResult.modifiedCount} conversation sync flags`); + + // Get current counts + const totalMessages = await mongoose.connection.db.collection('messages').countDocuments(); + const totalConversations = await mongoose.connection.db + .collection('conversations') + .countDocuments(); + + console.purple('\n---------------------------------------'); + console.green('MeiliSearch sync flags have been reset successfully!'); + console.cyan(`\nTotal messages to sync: ${totalMessages}`); + console.cyan(`Total conversations to sync: ${totalConversations}`); + console.yellow('\nThe next time LibreChat starts or performs a sync check,'); + console.yellow('all data will be re-indexed into MeiliSearch.'); + console.purple('---------------------------------------\n'); + + // Ask if user wants to see advanced options + const showAdvanced = await askQuestion('Show advanced options? (y/N): '); + + if (showAdvanced.toLowerCase() === 'y') { + console.cyan('\nAdvanced Options:'); + console.yellow('1. To trigger immediate sync, restart LibreChat'); + console.yellow('2. To disable sync, set MEILI_NO_SYNC=true in .env'); + console.yellow( + '3. To adjust sync batch size, set MEILI_SYNC_BATCH_SIZE in .env (default: 100)', + ); + console.yellow('4. To adjust sync delay, set MEILI_SYNC_DELAY_MS in .env (default: 100ms)'); + console.yellow( + '5. To change sync threshold, set MEILI_SYNC_THRESHOLD in .env (default: 1000)\n', + ); + } + + silentExit(0); + } catch (error) { + console.red('\nError resetting MeiliSearch sync flags:'); + console.error(error); + silentExit(1); + } +})(); + +process.on('uncaughtException', (err) => { + if (!err.message.includes('fetch failed')) { + console.error('There was an uncaught error:'); + console.error(err); + } + + if (err.message.includes('fetch failed')) { + return; + } else { + process.exit(1); + } +}); diff --git a/package.json b/package.json index 36327241a..c39c5444d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "reset-password": "node config/reset-password.js", "ban-user": "node config/ban-user.js", "delete-user": "node config/delete-user.js", + "reset-meili-sync": "node config/reset-meili-sync.js", "update-banner": "node config/update-banner.js", "delete-banner": "node config/delete-banner.js", "backend": "cross-env NODE_ENV=production node api/server/index.js", From 33834cd484d70ab095987b4defd08ce84a0f3bc2 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 3 Aug 2025 17:11:14 -0400 Subject: [PATCH 049/224] =?UTF-8?q?=F0=9F=A7=B9=20feat:=20Automatic=20File?= =?UTF-8?q?=20Cleanup=20for=20Mistral=20OCR=20Uploads=20(#8827)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Handle optional token_endpoint in OAuth metadata discovery * chore: Simplify permission typing logic in checkAccess function * feat: Implement `deleteMistralFile` function and integrate file cleanup in `uploadMistralOCR` --- packages/api/src/files/mistral/crud.spec.ts | 387 +++++++++++++++++++- packages/api/src/files/mistral/crud.ts | 52 ++- packages/api/src/mcp/oauth/handler.ts | 2 +- packages/api/src/middleware/access.ts | 9 +- 4 files changed, 438 insertions(+), 12 deletions(-) diff --git a/packages/api/src/files/mistral/crud.spec.ts b/packages/api/src/files/mistral/crud.spec.ts index 309e5565b..a7db60180 100644 --- a/packages/api/src/files/mistral/crud.spec.ts +++ b/packages/api/src/files/mistral/crud.spec.ts @@ -50,8 +50,9 @@ import type { MistralFileUploadResponse, MistralSignedUrlResponse, OCRResult } f import { logger as mockLogger } from '@librechat/data-schemas'; import { uploadDocumentToMistral, - uploadMistralOCR, uploadAzureMistralOCR, + deleteMistralFile, + uploadMistralOCR, getSignedUrl, performOCR, } from './crud'; @@ -216,6 +217,56 @@ describe('MistralOCR Service', () => { }); }); + describe('deleteMistralFile', () => { + it('should delete a file from Mistral API', async () => { + mockAxios.delete!.mockResolvedValueOnce({ data: {} }); + + await deleteMistralFile({ + fileId: 'file-123', + apiKey: 'test-api-key', + baseURL: 'https://api.mistral.ai/v1', + }); + + expect(mockAxios.delete).toHaveBeenCalledWith('https://api.mistral.ai/v1/files/file-123', { + headers: { + Authorization: 'Bearer test-api-key', + }, + }); + }); + + it('should use default baseURL when not provided', async () => { + mockAxios.delete!.mockResolvedValueOnce({ data: {} }); + + await deleteMistralFile({ + fileId: 'file-456', + apiKey: 'test-api-key', + }); + + expect(mockAxios.delete).toHaveBeenCalledWith('https://api.mistral.ai/v1/files/file-456', { + headers: { + Authorization: 'Bearer test-api-key', + }, + }); + }); + + it('should not throw when deletion fails', async () => { + mockAxios.delete!.mockRejectedValueOnce(new Error('Delete failed')); + + // Should not throw + await expect( + deleteMistralFile({ + fileId: 'file-789', + apiKey: 'test-api-key', + }), + ).resolves.not.toThrow(); + + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error deleting Mistral file file-789:', + expect.any(Error), + ); + }); + }); + describe('performOCR', () => { it('should perform OCR using Mistral API (document_url)', async () => { const mockResponse: { data: OCRResult } = { @@ -1345,6 +1396,340 @@ describe('MistralOCR Service', () => { expect(authHeader).toBe('Bearer hardcoded-api-key-12345'); }); }); + + describe('File cleanup', () => { + beforeEach(() => { + const mockReadStream: MockReadStream = { + on: jest.fn().mockImplementation(function ( + this: MockReadStream, + event: string, + handler: () => void, + ) { + if (event === 'end') { + handler(); + } + return this; + }), + pipe: jest.fn().mockImplementation(function (this: MockReadStream) { + return this; + }), + pause: jest.fn(), + resume: jest.fn(), + emit: jest.fn(), + once: jest.fn(), + destroy: jest.fn(), + path: '/tmp/upload/file.pdf', + fd: 1, + flags: 'r', + mode: 0o666, + autoClose: true, + bytesRead: 0, + closed: false, + pending: false, + }; + + (jest.mocked(fs).createReadStream as jest.Mock).mockReturnValue(mockReadStream); + // Clear all mocks before each test + mockAxios.delete!.mockClear(); + }); + + it('should delete the uploaded file after successful OCR processing', async () => { + mockLoadAuthValues.mockResolvedValue({ + OCR_API_KEY: 'test-api-key', + OCR_BASEURL: 'https://api.mistral.ai/v1', + }); + + // Mock file upload response + mockAxios.post!.mockResolvedValueOnce({ + data: { + id: 'file-cleanup-123', + object: 'file', + bytes: 1024, + created_at: Date.now(), + filename: 'document.pdf', + purpose: 'ocr', + } as MistralFileUploadResponse, + }); + + // Mock signed URL response + mockAxios.get!.mockResolvedValueOnce({ + data: { + url: 'https://signed-url.com', + expires_at: Date.now() + 86400000, + } as MistralSignedUrlResponse, + }); + + // Mock OCR response + mockAxios.post!.mockResolvedValueOnce({ + data: { + model: 'mistral-ocr-latest', + pages: [ + { + index: 0, + markdown: 'OCR content', + images: [], + dimensions: { dpi: 300, height: 1100, width: 850 }, + }, + ], + document_annotation: '', + usage_info: { + pages_processed: 1, + doc_size_bytes: 1024, + }, + }, + }); + + // Mock delete file response + mockAxios.delete!.mockResolvedValueOnce({ data: {} }); + + const req = { + user: { id: 'user123' }, + app: { + locals: { + ocr: { + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + }, + }, + } as unknown as ExpressRequest; + + const file = { + path: '/tmp/upload/file.pdf', + originalname: 'document.pdf', + mimetype: 'application/pdf', + } as Express.Multer.File; + + await uploadMistralOCR({ + req, + file, + loadAuthValues: mockLoadAuthValues, + }); + + // Verify delete was called with correct parameters + expect(mockAxios.delete).toHaveBeenCalledWith( + 'https://api.mistral.ai/v1/files/file-cleanup-123', + { + headers: { + Authorization: 'Bearer test-api-key', + }, + }, + ); + expect(mockAxios.delete).toHaveBeenCalledTimes(1); + }); + + it('should delete the uploaded file even when OCR processing fails', async () => { + mockLoadAuthValues.mockResolvedValue({ + OCR_API_KEY: 'test-api-key', + OCR_BASEURL: 'https://api.mistral.ai/v1', + }); + + // Mock file upload response + mockAxios.post!.mockResolvedValueOnce({ + data: { + id: 'file-cleanup-456', + object: 'file', + bytes: 1024, + created_at: Date.now(), + filename: 'document.pdf', + purpose: 'ocr', + } as MistralFileUploadResponse, + }); + + // Mock signed URL response + mockAxios.get!.mockResolvedValueOnce({ + data: { + url: 'https://signed-url.com', + expires_at: Date.now() + 86400000, + } as MistralSignedUrlResponse, + }); + + // Mock OCR to fail + mockAxios.post!.mockRejectedValueOnce(new Error('OCR processing failed')); + + // Mock delete file response + mockAxios.delete!.mockResolvedValueOnce({ data: {} }); + + const req = { + user: { id: 'user123' }, + app: { + locals: { + ocr: { + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + }, + }, + } as unknown as ExpressRequest; + + const file = { + path: '/tmp/upload/file.pdf', + originalname: 'document.pdf', + mimetype: 'application/pdf', + } as Express.Multer.File; + + await expect( + uploadMistralOCR({ + req, + file, + loadAuthValues: mockLoadAuthValues, + }), + ).rejects.toThrow('Error uploading document to Mistral OCR API'); + + // Verify delete was still called despite the error + expect(mockAxios.delete).toHaveBeenCalledWith( + 'https://api.mistral.ai/v1/files/file-cleanup-456', + { + headers: { + Authorization: 'Bearer test-api-key', + }, + }, + ); + expect(mockAxios.delete).toHaveBeenCalledTimes(1); + }); + + it('should handle deletion errors gracefully without throwing', async () => { + mockLoadAuthValues.mockResolvedValue({ + OCR_API_KEY: 'test-api-key', + OCR_BASEURL: 'https://api.mistral.ai/v1', + }); + + // Mock file upload response + mockAxios.post!.mockResolvedValueOnce({ + data: { + id: 'file-cleanup-789', + object: 'file', + bytes: 1024, + created_at: Date.now(), + filename: 'document.pdf', + purpose: 'ocr', + } as MistralFileUploadResponse, + }); + + // Mock signed URL response + mockAxios.get!.mockResolvedValueOnce({ + data: { + url: 'https://signed-url.com', + expires_at: Date.now() + 86400000, + } as MistralSignedUrlResponse, + }); + + // Mock OCR response + mockAxios.post!.mockResolvedValueOnce({ + data: { + model: 'mistral-ocr-latest', + pages: [ + { + index: 0, + markdown: 'OCR content', + images: [], + dimensions: { dpi: 300, height: 1100, width: 850 }, + }, + ], + document_annotation: '', + usage_info: { + pages_processed: 1, + doc_size_bytes: 1024, + }, + }, + }); + + // Mock delete to fail + mockAxios.delete!.mockRejectedValueOnce(new Error('Delete failed')); + + const req = { + user: { id: 'user123' }, + app: { + locals: { + ocr: { + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + }, + }, + } as unknown as ExpressRequest; + + const file = { + path: '/tmp/upload/file.pdf', + originalname: 'document.pdf', + mimetype: 'application/pdf', + } as Express.Multer.File; + + // Should not throw even if delete fails + const result = await uploadMistralOCR({ + req, + file, + loadAuthValues: mockLoadAuthValues, + }); + + expect(result).toEqual({ + filename: 'document.pdf', + bytes: expect.any(Number), + filepath: 'mistral_ocr', + text: 'OCR content\n\n', + images: [], + }); + + // Verify delete was attempted + expect(mockAxios.delete).toHaveBeenCalledWith( + 'https://api.mistral.ai/v1/files/file-cleanup-789', + { + headers: { + Authorization: 'Bearer test-api-key', + }, + }, + ); + + // Verify error was logged + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error deleting Mistral file file-cleanup-789:', + expect.any(Error), + ); + }); + + it('should not attempt cleanup if file upload fails', async () => { + mockLoadAuthValues.mockResolvedValue({ + OCR_API_KEY: 'test-api-key', + OCR_BASEURL: 'https://api.mistral.ai/v1', + }); + + // Mock file upload to fail + mockAxios.post!.mockRejectedValueOnce(new Error('Upload failed')); + + const req = { + user: { id: 'user123' }, + app: { + locals: { + ocr: { + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + }, + }, + } as unknown as ExpressRequest; + + const file = { + path: '/tmp/upload/file.pdf', + originalname: 'document.pdf', + mimetype: 'application/pdf', + } as Express.Multer.File; + + await expect( + uploadMistralOCR({ + req, + file, + loadAuthValues: mockLoadAuthValues, + }), + ).rejects.toThrow('Error uploading document to Mistral OCR API'); + + // Verify delete was NOT called since upload failed + expect(mockAxios.delete).not.toHaveBeenCalled(); + }); + }); }); describe('uploadAzureMistralOCR', () => { diff --git a/packages/api/src/files/mistral/crud.ts b/packages/api/src/files/mistral/crud.ts index eac843310..077351a7e 100644 --- a/packages/api/src/files/mistral/crud.ts +++ b/packages/api/src/files/mistral/crud.ts @@ -172,6 +172,35 @@ export async function performOCR({ }); } +/** + * Deletes a file from Mistral API + * @param params Delete parameters + * @param params.fileId The file ID to delete + * @param params.apiKey Mistral API key + * @param params.baseURL Mistral API base URL + * @returns Promise that resolves when the file is deleted + */ +export async function deleteMistralFile({ + fileId, + apiKey, + baseURL = DEFAULT_MISTRAL_BASE_URL, +}: { + fileId: string; + apiKey: string; + baseURL?: string; +}): Promise { + try { + const result = await axios.delete(`${baseURL}/files/${fileId}`, { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + logger.debug(`Mistral file ${fileId} deleted successfully:`, result.data); + } catch (error) { + logger.error(`Error deleting Mistral file ${fileId}:`, error); + } +} + /** * Determines if a value needs to be loaded from environment */ @@ -335,8 +364,14 @@ function createOCRError(error: unknown, baseMessage: string): Error { * along with the `filename` and `bytes` properties. */ export const uploadMistralOCR = async (context: OCRContext): Promise => { + let mistralFileId: string | undefined; + let apiKey: string | undefined; + let baseURL: string | undefined; + try { - const { apiKey, baseURL } = await loadAuthConfig(context); + const authConfig = await loadAuthConfig(context); + apiKey = authConfig.apiKey; + baseURL = authConfig.baseURL; const model = getModelConfig(context.req.app.locals?.ocr); const mistralFile = await uploadDocumentToMistral({ @@ -346,6 +381,8 @@ export const uploadMistralOCR = async (context: OCRContext): Promise { - if ( - role.permissions?.[permissionType as keyof typeof role.permissions]?.[ - permission as keyof (typeof role.permissions)[typeof permissionType] - ] - ) { + if (permissionValue[permission as keyof typeof permissionValue]) { return true; } From dfdafdbd093aafc9b762f373df2d95d9a1ced529 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Mon, 4 Aug 2025 20:44:00 +0200 Subject: [PATCH 050/224] =?UTF-8?q?=F0=9F=96=8C=EF=B8=8F=20feat:=20add=20a?= =?UTF-8?q?nimation=20styles=20for=20popovers=20and=20tooltips=20(#8831)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Update client version to 0.2.2 and add animation styles for popovers and tooltips * refactor: Remove focus outline styles from Dropdown component * feat: Update client version to 0.2.3 and add Select component export --------- Co-authored-by: Danny Avila --- package-lock.json | 2 +- packages/client/package.json | 2 +- .../client/src/components/AnimatePopover.css | 27 +++++++ .../client/src/components/ControlCombobox.tsx | 1 + packages/client/src/components/Dropdown.css | 78 +++++++++++++++++++ packages/client/src/components/Dropdown.tsx | 1 + .../client/src/components/DropdownPopup.tsx | 1 + .../client/src/components/MultiSelect.tsx | 1 + packages/client/src/components/Select.tsx | 1 - packages/client/src/components/Tooltip.css | 20 +++++ packages/client/src/components/Tooltip.tsx | 1 + packages/client/src/components/index.ts | 1 + 12 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 packages/client/src/components/AnimatePopover.css create mode 100644 packages/client/src/components/Dropdown.css create mode 100644 packages/client/src/components/Tooltip.css diff --git a/package-lock.json b/package-lock.json index dff97b559..426a4a7db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51507,7 +51507,7 @@ }, "packages/client": { "name": "@librechat/client", - "version": "0.2.1", + "version": "0.2.3", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^25.0.2", diff --git a/packages/client/package.json b/packages/client/package.json index ee22cadfa..0e6a19aeb 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/client", - "version": "0.2.1", + "version": "0.2.3", "description": "React components for LibreChat", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/client/src/components/AnimatePopover.css b/packages/client/src/components/AnimatePopover.css new file mode 100644 index 000000000..5a83bfa64 --- /dev/null +++ b/packages/client/src/components/AnimatePopover.css @@ -0,0 +1,27 @@ +.animate-popover { + transform-origin: top; + opacity: 0; + transition: + opacity 150ms cubic-bezier(0.4, 0, 0.2, 1), + transform 150ms cubic-bezier(0.4, 0, 0.2, 1); + transform: scale(0.95) translateY(-0.5rem); +} + +.animate-popover[data-enter] { + opacity: 1; + transform: scale(1) translateY(0); +} + +.animate-popover-left { + transform-origin: left; + opacity: 0; + transition: + opacity 150ms cubic-bezier(0.4, 0, 0.2, 1), + transform 150ms cubic-bezier(0.4, 0, 0.2, 1); + transform: scale(0.95) translateX(-0.5rem); +} + +.animate-popover-left[data-enter] { + opacity: 1; + transform: scale(1) translateX(0); +} diff --git a/packages/client/src/components/ControlCombobox.tsx b/packages/client/src/components/ControlCombobox.tsx index c3d00b0d2..e512b11a5 100644 --- a/packages/client/src/components/ControlCombobox.tsx +++ b/packages/client/src/components/ControlCombobox.tsx @@ -4,6 +4,7 @@ import { Search, ChevronDown } from 'lucide-react'; import { useMemo, useState, useRef, memo, useEffect } from 'react'; import { SelectRenderer } from '@ariakit/react-core/select/select-renderer'; import type { OptionWithIcon } from '~/common'; +import './AnimatePopover.css'; import { cn } from '~/utils'; interface ControlComboboxProps { diff --git a/packages/client/src/components/Dropdown.css b/packages/client/src/components/Dropdown.css new file mode 100644 index 000000000..e529af953 --- /dev/null +++ b/packages/client/src/components/Dropdown.css @@ -0,0 +1,78 @@ +.popover-ui { + display: flex; + max-height: min(var(--popover-available-height, 1700px), 1700px); + flex-direction: column; + overflow: auto; + overscroll-behavior: contain; + border-radius: 1rem; + border-width: 1px; + border-style: solid; + border-color: var(--border-light); + background-color: var(--surface-primary); + padding: 0.5rem; + color: var(--text-primary); + box-shadow: + 0 10px 15px -3px rgb(0 0 0 / 0.1), + 0 4px 6px -4px rgb(0 0 0 / 0.1); + transform-origin: top; + opacity: 0; + transition-property: opacity, scale, translate; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + scale: 0.95; + translate: 0 -0.5rem; + margin-top: 4px; + margin-right: -2px; +} + +.popover-animate { + opacity: 0; + transform: scale(0.95) translateY(-0.5rem); + transition: + opacity 150ms cubic-bezier(0.4, 0, 0.2, 1), + transform 150ms cubic-bezier(0.4, 0, 0.2, 1); +} +.popover-animate[data-enter] { + opacity: 1; + transform: scale(1) translateY(0); +} + +.popover-ui:focus-visible, +.popover-ui[data-focus-visible] { + outline: var(--bg-surface-hover); + outline-offset: -1px; +} + +.popover-ui:where(.dark, .dark *) { + background-color: var(--surface-secondary); + color: var(--text-secondary); + box-shadow: + 0 10px 15px -3px rgb(0 0 0 / 0.25), + 0 4px 6px -4px rgb(0 0 0 / 0.1); +} + +.select-item { + display: flex; + cursor: pointer; + scroll-margin: 0.5rem; + align-items: center; + gap: 0.5rem; + border-radius: 0.5rem; + padding: 0.5rem; + outline: none !important; +} + +.select-item[aria-disabled='true'] { + opacity: 0.5; +} + +.select-item[data-active-item] { + background-color: var(--surface-hover); + color: var(--text-primary); +} + +.popover-ui[data-enter] { + opacity: 1; + scale: 1; + translate: 0; +} diff --git a/packages/client/src/components/Dropdown.tsx b/packages/client/src/components/Dropdown.tsx index 2cb94f2b0..b191878c8 100644 --- a/packages/client/src/components/Dropdown.tsx +++ b/packages/client/src/components/Dropdown.tsx @@ -2,6 +2,7 @@ import React from 'react'; import * as Select from '@ariakit/react/select'; import type { Option } from '~/common'; import { cn } from '~/utils/'; +import './Dropdown.css'; interface DropdownProps { value?: string; diff --git a/packages/client/src/components/DropdownPopup.tsx b/packages/client/src/components/DropdownPopup.tsx index 0953811f4..9d4c7f153 100644 --- a/packages/client/src/components/DropdownPopup.tsx +++ b/packages/client/src/components/DropdownPopup.tsx @@ -2,6 +2,7 @@ import React from 'react'; import * as Ariakit from '@ariakit/react'; import type * as t from '~/common'; import { cn } from '~/utils'; +import './Dropdown.css'; interface DropdownProps { keyPrefix?: string; diff --git a/packages/client/src/components/MultiSelect.tsx b/packages/client/src/components/MultiSelect.tsx index d7798e810..a793e1c55 100644 --- a/packages/client/src/components/MultiSelect.tsx +++ b/packages/client/src/components/MultiSelect.tsx @@ -8,6 +8,7 @@ import { SelectPopover, SelectProvider, } from '@ariakit/react'; +import './AnimatePopover.css'; import { cn } from '~/utils'; interface MultiSelectProps { diff --git a/packages/client/src/components/Select.tsx b/packages/client/src/components/Select.tsx index 3301e3577..39f32f4f3 100644 --- a/packages/client/src/components/Select.tsx +++ b/packages/client/src/components/Select.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import * as SelectPrimitive from '@radix-ui/react-select'; import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; - import { cn } from '~/utils'; // @ts-ignore - Radix UI type conflicts with React types diff --git a/packages/client/src/components/Tooltip.css b/packages/client/src/components/Tooltip.css new file mode 100644 index 000000000..92e52037c --- /dev/null +++ b/packages/client/src/components/Tooltip.css @@ -0,0 +1,20 @@ +.tooltip { + z-index: 50; + cursor: pointer; + border-radius: 0.275rem; + background-color: var(--surface-primary); + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + font-size: 1rem; + line-height: 1.5rem; + color: black; + box-shadow: 0 2px 4px 0 rgb(0 0 0 / 0.25); +} + +.tooltip:where(.dark, .dark *) { + background-color: var(--surface-primary); + color: white; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.35); +} diff --git a/packages/client/src/components/Tooltip.tsx b/packages/client/src/components/Tooltip.tsx index 2bf89e24d..b83452d65 100644 --- a/packages/client/src/components/Tooltip.tsx +++ b/packages/client/src/components/Tooltip.tsx @@ -2,6 +2,7 @@ import * as Ariakit from '@ariakit/react'; import { AnimatePresence, motion } from 'framer-motion'; import { forwardRef, useMemo } from 'react'; import { cn } from '~/utils'; +import './Tooltip.css'; interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps { description: string; diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts index 0269a0455..f315a2ca1 100644 --- a/packages/client/src/components/index.ts +++ b/packages/client/src/components/index.ts @@ -30,6 +30,7 @@ export * from './Progress'; export * from './InputOTP'; export * from './MultiSearch'; export * from './Resizable'; +export * from './Select'; export { default as Radio } from './Radio'; export { default as Badge } from './Badge'; export { default as Combobox } from './Combobox'; From fcefc6eedfec405af86028910a261fbd2eac6e7a Mon Sep 17 00:00:00 2001 From: SollalF <64600280+SollalF@users.noreply.github.com> Date: Tue, 5 Aug 2025 02:49:36 +0800 Subject: [PATCH 051/224] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20OpenID=20Audi?= =?UTF-8?q?ence=20Parameter=20(#8837)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: Add OpenID audience parameter support in authorization requests * Updated .env.example to include OPENID_AUDIENCE variable for configuration. * Enhanced openidStrategy to set the audience parameter in authorization requests if specified, improving OpenID integration. * Update .env.example * Update openidStrategy.js --------- Co-authored-by: Danny Avila --- .env.example | 2 ++ api/strategies/openidStrategy.js | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/.env.example b/.env.example index 23777fe26..d0435c746 100644 --- a/.env.example +++ b/.env.example @@ -442,6 +442,8 @@ OPENID_REQUIRED_ROLE_PARAMETER_PATH= OPENID_USERNAME_CLAIM= # Set to determine which user info property returned from OpenID Provider to store as the User's name OPENID_NAME_CLAIM= +# Optional audience parameter for OpenID authorization requests +OPENID_AUDIENCE= OPENID_BUTTON_LABEL= OPENID_IMAGE_URL= diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index 605f0b054..f2151f3df 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -104,6 +104,14 @@ class CustomOpenIDStrategy extends OpenIDStrategy { if (options?.state && !params.has('state')) { params.set('state', options.state); } + + if (process.env.OPENID_AUDIENCE) { + params.set('audience', process.env.OPENID_AUDIENCE); + logger.debug( + `[openidStrategy] Adding audience to authorization request: ${process.env.OPENID_AUDIENCE}`, + ); + } + return params; } } From a6c99a326781c034ff3ef5401baff2905268451e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:54:07 -0400 Subject: [PATCH 052/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#8828)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/zh-Hans/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/locales/zh-Hans/translation.json b/client/src/locales/zh-Hans/translation.json index 1e59d765a..1c8b7aeda 100644 --- a/client/src/locales/zh-Hans/translation.json +++ b/client/src/locales/zh-Hans/translation.json @@ -435,8 +435,10 @@ "com_nav_lang_spanish": "Español", "com_nav_lang_swedish": "Svenska", "com_nav_lang_thai": "ไทย", + "com_nav_lang_tibetan": "བོད་སྐད་", "com_nav_lang_traditional_chinese": "繁體中文", "com_nav_lang_turkish": "Türkçe", + "com_nav_lang_ukrainian": "Українська", "com_nav_lang_uyghur": "Uyƣur tili", "com_nav_lang_vietnamese": "Tiếng Việt", "com_nav_language": "语言", @@ -1075,7 +1077,6 @@ "com_ui_view_memory": "查看记忆", "com_ui_view_source": "查看来源对话", "com_ui_web_search": "网络搜索", - "com_ui_web_search_api_subtitle": "搜索网络以获取最新信息", "com_ui_web_search_cohere_key": "输入 Cohere API Key", "com_ui_web_search_firecrawl_url": "Firecrawl API URL(可选)", "com_ui_web_search_jina_key": "输入 Jina API Key", From 02dc71f4b75fdb12b4bcda68928b15cbeff1a447 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:24:24 -0400 Subject: [PATCH 053/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#8845)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/lv/translation.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/locales/lv/translation.json b/client/src/locales/lv/translation.json index e95abce66..cb59d448f 100644 --- a/client/src/locales/lv/translation.json +++ b/client/src/locales/lv/translation.json @@ -435,8 +435,10 @@ "com_nav_lang_spanish": "Spāņu", "com_nav_lang_swedish": "Zviedru", "com_nav_lang_thai": "ไทย", + "com_nav_lang_tibetan": "Tibetiešu", "com_nav_lang_traditional_chinese": "繁體中文", "com_nav_lang_turkish": "Türkçe", + "com_nav_lang_ukrainian": "Ukraiņu", "com_nav_lang_uyghur": "Uyƣur tili", "com_nav_lang_vietnamese": "Tiếng Việt", "com_nav_language": "Valoda", From 4175a3ea193e547507d9d76d7dd80929c12030d3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 4 Aug 2025 15:25:07 -0400 Subject: [PATCH 054/224] =?UTF-8?q?=E2=9C=A8=20v0.8.0-rc1=20(#8846)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- Dockerfile.multi | 2 +- api/package.json | 2 +- client/package.json | 2 +- e2e/jestSetup.js | 2 +- helm/librechat/Chart.yaml | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- packages/data-provider/src/config.ts | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8ec2c2fcd..84d33d1bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# v0.7.9 +# v0.8.0-rc1 # Base node image FROM node:20-alpine AS node diff --git a/Dockerfile.multi b/Dockerfile.multi index 296f8ec35..4cc03d460 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -1,5 +1,5 @@ # Dockerfile.multi -# v0.7.9 +# v0.8.0-rc1 # Base for all builds FROM node:20-alpine AS base-min diff --git a/api/package.json b/api/package.json index c0a983248..4ea1335c3 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/backend", - "version": "v0.7.9", + "version": "v0.8.0-rc1", "description": "", "scripts": { "start": "echo 'please run this from the root directory'", diff --git a/client/package.json b/client/package.json index f1d68d27e..0438b4bd8 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/frontend", - "version": "v0.7.9", + "version": "v0.8.0-rc1", "description": "", "type": "module", "scripts": { diff --git a/e2e/jestSetup.js b/e2e/jestSetup.js index 44787250b..a86b738eb 100644 --- a/e2e/jestSetup.js +++ b/e2e/jestSetup.js @@ -1,3 +1,3 @@ -// v0.7.9 +// v0.8.0-rc1 // See .env.test.example for an example of the '.env.test' file. require('dotenv').config({ path: './e2e/.env.test' }); diff --git a/helm/librechat/Chart.yaml b/helm/librechat/Chart.yaml index 55a665140..9aee8a149 100755 --- a/helm/librechat/Chart.yaml +++ b/helm/librechat/Chart.yaml @@ -22,7 +22,7 @@ version: 1.8.9 # It is recommended to use it with quotes. # renovate: image=ghcr.io/danny-avila/librechat -appVersion: "v0.7.9" +appVersion: "v0.8.0-rc1" home: https://www.librechat.ai diff --git a/package-lock.json b/package-lock.json index 426a4a7db..1b344c091 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "LibreChat", - "version": "v0.7.9", + "version": "v0.8.0-rc1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "LibreChat", - "version": "v0.7.9", + "version": "v0.8.0-rc1", "license": "ISC", "workspaces": [ "api", @@ -47,7 +47,7 @@ }, "api": { "name": "@librechat/backend", - "version": "v0.7.9", + "version": "v0.8.0-rc1", "license": "ISC", "dependencies": { "@anthropic-ai/sdk": "^0.52.0", @@ -2735,7 +2735,7 @@ }, "client": { "name": "@librechat/frontend", - "version": "v0.7.9", + "version": "v0.8.0-rc1", "license": "ISC", "dependencies": { "@ariakit/react": "^0.4.15", diff --git a/package.json b/package.json index c39c5444d..3a86469ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LibreChat", - "version": "v0.7.9", + "version": "v0.8.0-rc1", "description": "", "workspaces": [ "api", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 4d53fba80..969590e3a 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1464,7 +1464,7 @@ export enum TTSProviders { /** Enum for app-wide constants */ export enum Constants { /** Key for the app's version. */ - VERSION = 'v0.7.9', + VERSION = 'v0.8.0-rc1', /** Key for the Custom Config's version (librechat.yaml). */ CONFIG_VERSION = '1.2.8', /** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */ From 36c8947029cffb205d7f312a118a90634d5bc866 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 6 Aug 2025 19:26:40 -0400 Subject: [PATCH 055/224] =?UTF-8?q?=F0=9F=94=84=20refactor:=20Select=20Ope?= =?UTF-8?q?nRouter=20LLM=20Class=20Dynamically=20by=20`baseURL`=20(#8898)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/src/endpoints/openai/llm.ts | 7 ++++++- packages/api/src/types/openai.ts | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/api/src/endpoints/openai/llm.ts b/packages/api/src/endpoints/openai/llm.ts index 552470dea..5f3b5b827 100644 --- a/packages/api/src/endpoints/openai/llm.ts +++ b/packages/api/src/endpoints/openai/llm.ts @@ -1,4 +1,5 @@ import { ProxyAgent } from 'undici'; +import { Providers } from '@librechat/agents'; import { KnownEndpoints, removeNullishValues } from 'librechat-data-provider'; import type { BindToolsInput } from '@langchain/core/language_models/chat_models'; import type { AzureOpenAIInput } from '@langchain/openai'; @@ -222,9 +223,13 @@ export function getOpenAIConfig( }); } - return { + const result: t.LLMConfigResult = { llmConfig, configOptions, tools, }; + if (useOpenRouter) { + result.provider = Providers.OPENROUTER; + } + return result; } diff --git a/packages/api/src/types/openai.ts b/packages/api/src/types/openai.ts index b9886fa51..df9d4b5cb 100644 --- a/packages/api/src/types/openai.ts +++ b/packages/api/src/types/openai.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { openAISchema, EModelEndpoint } from 'librechat-data-provider'; import type { TEndpointOption, TAzureConfig, TEndpoint } from 'librechat-data-provider'; import type { BindToolsInput } from '@langchain/core/language_models/chat_models'; -import type { OpenAIClientOptions } from '@librechat/agents'; +import type { OpenAIClientOptions, Providers } from '@librechat/agents'; import type { AzureOptions } from './azure'; export type OpenAIParameters = z.infer; @@ -35,6 +35,7 @@ export interface LLMConfigResult { llmConfig: ClientOptions; configOptions: OpenAIConfiguration; tools?: BindToolsInput[]; + provider?: Providers; } /** From 1092392ed8071e832aa1e7da206836a5dcf052e0 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 6 Aug 2025 19:45:57 -0400 Subject: [PATCH 056/224] =?UTF-8?q?=F0=9F=93=82=20fix:=20File=20Cleanup=20?= =?UTF-8?q?for=20Uploaded=20"Agent"=20Files=20(#8900)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server/routes/files/files.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index e0b354ef6..8cca0f709 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -413,13 +413,15 @@ router.post('/', async (req, res) => { logger.error('[/files] Error deleting file:', error); } res.status(500).json({ message }); - } - - if (cleanup) { - try { - await fs.unlink(req.file.path); - } catch (error) { - logger.error('[/files] Error deleting file after file processing:', error); + } finally { + if (cleanup) { + try { + await fs.unlink(req.file.path); + } catch (error) { + logger.error('[/files] Error deleting file after file processing:', error); + } + } else { + logger.debug('[/files] File processing completed without cleanup'); } } }); From 0b071c06f68828ec542ddf8439947ccfb96dc233 Mon Sep 17 00:00:00 2001 From: Sebastien Bruel <93573440+sbruel@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:12:05 +0900 Subject: [PATCH 057/224] =?UTF-8?q?=F0=9F=A5=9E=20refactor:=20Duplicate=20?= =?UTF-8?q?Agent=20Versions=20as=20Informational=20Instead=20of=20Errors?= =?UTF-8?q?=20(#8881)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix error when updating an agent with no changes * Add tests * Revert translation file changes --- api/models/Agent.js | 15 +- api/models/Agent.spec.js | 93 ++--- api/server/controllers/agents/v1.js | 3 + api/server/controllers/agents/v1.spec.js | 22 + .../SidePanel/Agents/AgentPanel.test.tsx | 380 ++++++++++++++++++ .../SidePanel/Agents/AgentPanel.tsx | 64 +-- client/src/data-provider/Agents/mutations.ts | 9 +- client/src/locales/en/translation.json | 1 - packages/data-provider/src/types/mutations.ts | 7 +- 9 files changed, 474 insertions(+), 120 deletions(-) create mode 100644 client/src/components/SidePanel/Agents/AgentPanel.test.tsx diff --git a/api/models/Agent.js b/api/models/Agent.js index 7cb32e6cb..a85357671 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -316,17 +316,10 @@ const updateAgent = async (searchParameter, updateData, options = {}) => { if (shouldCreateVersion) { const duplicateVersion = isDuplicateVersion(updateData, versionData, versions, actionsHash); if (duplicateVersion && !forceVersion) { - const error = new Error( - 'Duplicate version: This would create a version identical to an existing one', - ); - error.statusCode = 409; - error.details = { - duplicateVersion, - versionIndex: versions.findIndex( - (v) => JSON.stringify(duplicateVersion) === JSON.stringify(v), - ), - }; - throw error; + // No changes detected, return the current agent without creating a new version + const agentObj = currentAgent.toObject(); + agentObj.version = versions.length; + return agentObj; } } diff --git a/api/models/Agent.spec.js b/api/models/Agent.spec.js index 8953ae048..dcd46bc6d 100644 --- a/api/models/Agent.spec.js +++ b/api/models/Agent.spec.js @@ -879,45 +879,31 @@ describe('models/Agent', () => { expect(emptyParamsAgent.model_parameters).toEqual({}); }); - test('should detect duplicate versions and reject updates', async () => { - const originalConsoleError = console.error; - console.error = jest.fn(); + test('should not create new version for duplicate updates', async () => { + const authorId = new mongoose.Types.ObjectId(); + const testCases = generateVersionTestCases(); - try { - const authorId = new mongoose.Types.ObjectId(); - const testCases = generateVersionTestCases(); + for (const testCase of testCases) { + const testAgentId = `agent_${uuidv4()}`; - for (const testCase of testCases) { - const testAgentId = `agent_${uuidv4()}`; + await createAgent({ + id: testAgentId, + provider: 'test', + model: 'test-model', + author: authorId, + ...testCase.initial, + }); - await createAgent({ - id: testAgentId, - provider: 'test', - model: 'test-model', - author: authorId, - ...testCase.initial, - }); + const updatedAgent = await updateAgent({ id: testAgentId }, testCase.update); + expect(updatedAgent.versions).toHaveLength(2); // No new version created - await updateAgent({ id: testAgentId }, testCase.update); + // Update with duplicate data should succeed but not create a new version + const duplicateUpdate = await updateAgent({ id: testAgentId }, testCase.duplicate); - let error; - try { - await updateAgent({ id: testAgentId }, testCase.duplicate); - } catch (e) { - error = e; - } + expect(duplicateUpdate.versions).toHaveLength(2); // No new version created - expect(error).toBeDefined(); - expect(error.message).toContain('Duplicate version'); - expect(error.statusCode).toBe(409); - expect(error.details).toBeDefined(); - expect(error.details.duplicateVersion).toBeDefined(); - - const agent = await getAgent({ id: testAgentId }); - expect(agent.versions).toHaveLength(2); - } - } finally { - console.error = originalConsoleError; + const agent = await getAgent({ id: testAgentId }); + expect(agent.versions).toHaveLength(2); } }); @@ -1093,20 +1079,13 @@ describe('models/Agent', () => { expect(secondUpdate.versions).toHaveLength(3); // Update without forceVersion and no changes should not create a version - let error; - try { - await updateAgent( - { id: agentId }, - { tools: ['listEvents_action_test.com', 'createEvent_action_test.com'] }, - { updatingUserId: authorId.toString(), forceVersion: false }, - ); - } catch (e) { - error = e; - } + const duplicateUpdate = await updateAgent( + { id: agentId }, + { tools: ['listEvents_action_test.com', 'createEvent_action_test.com'] }, + { updatingUserId: authorId.toString(), forceVersion: false }, + ); - expect(error).toBeDefined(); - expect(error.message).toContain('Duplicate version'); - expect(error.statusCode).toBe(409); + expect(duplicateUpdate.versions).toHaveLength(3); // No new version created }); test('should handle isDuplicateVersion with arrays containing null/undefined values', async () => { @@ -2400,11 +2379,18 @@ describe('models/Agent', () => { agent_ids: ['agent1', 'agent2'], }); - await updateAgent({ id: agentId }, { agent_ids: ['agent1', 'agent2', 'agent3'] }); + const updatedAgent = await updateAgent( + { id: agentId }, + { agent_ids: ['agent1', 'agent2', 'agent3'] }, + ); + expect(updatedAgent.versions).toHaveLength(2); - await expect( - updateAgent({ id: agentId }, { agent_ids: ['agent1', 'agent2', 'agent3'] }), - ).rejects.toThrow('Duplicate version'); + // Update with same agent_ids should succeed but not create a new version + const duplicateUpdate = await updateAgent( + { id: agentId }, + { agent_ids: ['agent1', 'agent2', 'agent3'] }, + ); + expect(duplicateUpdate.versions).toHaveLength(2); // No new version created }); test('should handle agent_ids field alongside other fields', async () => { @@ -2543,9 +2529,10 @@ describe('models/Agent', () => { expect(updated.versions).toHaveLength(2); expect(updated.agent_ids).toEqual([]); - await expect(updateAgent({ id: agentId }, { agent_ids: [] })).rejects.toThrow( - 'Duplicate version', - ); + // Update with same empty agent_ids should succeed but not create a new version + const duplicateUpdate = await updateAgent({ id: agentId }, { agent_ids: [] }); + expect(duplicateUpdate.versions).toHaveLength(2); // No new version created + expect(duplicateUpdate.agent_ids).toEqual([]); }); test('should handle agent without agent_ids field', async () => { diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index c3c616760..32d682d6d 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -194,6 +194,9 @@ const updateAgentHandler = async (req, res) => { }); } + // Add version count to the response + updatedAgent.version = updatedAgent.versions ? updatedAgent.versions.length : 0; + if (updatedAgent.author) { updatedAgent.author = updatedAgent.author.toString(); } diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index 5ac2645c0..0574dde4e 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -498,6 +498,28 @@ describe('Agent Controllers - Mass Assignment Protection', () => { expect(mockRes.json).toHaveBeenCalledWith({ error: 'Agent not found' }); }); + test('should include version field in update response', async () => { + mockReq.user.id = existingAgentAuthorId.toString(); + mockReq.params.id = existingAgentId; + mockReq.body = { + name: 'Updated with Version Check', + }; + + await updateAgentHandler(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalled(); + const updatedAgent = mockRes.json.mock.calls[0][0]; + + // Verify version field is included and is a number + expect(updatedAgent).toHaveProperty('version'); + expect(typeof updatedAgent.version).toBe('number'); + expect(updatedAgent.version).toBeGreaterThanOrEqual(1); + + // Verify in database + const agentInDb = await Agent.findOne({ id: existingAgentId }); + expect(updatedAgent.version).toBe(agentInDb.versions.length); + }); + test('should handle validation errors properly', async () => { mockReq.user.id = existingAgentAuthorId.toString(); mockReq.params.id = existingAgentId; diff --git a/client/src/components/SidePanel/Agents/AgentPanel.test.tsx b/client/src/components/SidePanel/Agents/AgentPanel.test.tsx new file mode 100644 index 000000000..c8129abe0 --- /dev/null +++ b/client/src/components/SidePanel/Agents/AgentPanel.test.tsx @@ -0,0 +1,380 @@ +/** + * @jest-environment jsdom + */ +import * as React from 'react'; +import { describe, it, expect, beforeEach, jest } from '@jest/globals'; +import { render, waitFor, fireEvent } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import type { Agent } from 'librechat-data-provider'; + +// Mock toast context - define this after all mocks +let mockShowToast: jest.Mock; + +// Mock notification severity enum before other imports +jest.mock('~/common/types', () => ({ + NotificationSeverity: { + SUCCESS: 'success', + ERROR: 'error', + INFO: 'info', + WARNING: 'warning', + }, +})); + +// Mock store to prevent import errors +jest.mock('~/store/toast', () => ({ + default: () => ({ + showToast: jest.fn(), + }), +})); + +jest.mock('~/store', () => {}); + +// Mock the data service to control network responses +jest.mock('librechat-data-provider', () => { + const actualModule = jest.requireActual('librechat-data-provider') as any; + return { + ...actualModule, + dataService: { + updateAgent: jest.fn(), + }, + Tools: { + execute_code: 'execute_code', + file_search: 'file_search', + web_search: 'web_search', + }, + Constants: { + EPHEMERAL_AGENT_ID: 'ephemeral', + }, + SystemRoles: { + ADMIN: 'ADMIN', + }, + EModelEndpoint: { + agents: 'agents', + chatGPTBrowser: 'chatGPTBrowser', + gptPlugins: 'gptPlugins', + }, + isAssistantsEndpoint: jest.fn(() => false), + }; +}); + +jest.mock('@librechat/client', () => ({ + Button: ({ children, onClick, ...props }: any) => ( + + ), + useToastContext: () => ({ + get showToast() { + return mockShowToast || jest.fn(); + }, + }), +})); + +// Mock other dependencies +jest.mock('librechat-data-provider/react-query', () => ({ + useGetModelsQuery: () => ({ data: {} }), +})); + +jest.mock('~/utils', () => ({ + createProviderOption: jest.fn((provider: string) => ({ value: provider, label: provider })), + getDefaultAgentFormValues: jest.fn(() => ({ + id: '', + name: '', + description: '', + model: '', + provider: '', + })), +})); + +jest.mock('~/hooks', () => ({ + useSelectAgent: () => ({ onSelect: jest.fn() }), + useLocalize: () => (key: string) => key, + useAuthContext: () => ({ user: { id: 'user-123', role: 'USER' } }), +})); + +jest.mock('~/Providers/AgentPanelContext', () => ({ + useAgentPanelContext: () => ({ + activePanel: 'builder', + agentsConfig: { allowedProviders: [] }, + setActivePanel: jest.fn(), + endpointsConfig: {}, + setCurrentAgentId: jest.fn(), + agent_id: 'agent-123', + }), +})); + +jest.mock('~/common', () => ({ + Panel: { + model: 'model', + builder: 'builder', + advanced: 'advanced', + }, +})); + +// Mock child components to simplify testing +jest.mock('./AgentPanelSkeleton', () => ({ + __esModule: true, + default: () =>
{`Loading...`}
, +})); + +jest.mock('./Advanced/AdvancedPanel', () => ({ + __esModule: true, + default: () =>
{`Advanced Panel`}
, +})); + +jest.mock('./AgentConfig', () => ({ + __esModule: true, + default: () =>
{`Agent Config`}
, +})); + +jest.mock('./AgentSelect', () => ({ + __esModule: true, + default: () =>
{`Agent Select`}
, +})); + +jest.mock('./ModelPanel', () => ({ + __esModule: true, + default: () =>
{`Model Panel`}
, +})); + +// Mock AgentFooter to provide a save button +jest.mock('./AgentFooter', () => ({ + __esModule: true, + default: () => ( + + ), +})); + +// Mock react-hook-form to capture form submission +let mockFormSubmitHandler: (() => void) | null = null; + +jest.mock('react-hook-form', () => { + const actual = jest.requireActual('react-hook-form') as any; + return { + ...actual, + useForm: () => { + const methods = actual.useForm({ + defaultValues: { + id: 'agent-123', + name: 'Test Agent', + description: 'Test description', + model: 'gpt-4', + provider: 'openai', + tools: [], + execute_code: false, + file_search: false, + web_search: false, + }, + }); + + return { + ...methods, + handleSubmit: (onSubmit: any) => (e?: any) => { + e?.preventDefault?.(); + mockFormSubmitHandler = () => onSubmit(methods.getValues()); + return mockFormSubmitHandler; + }, + }; + }, + FormProvider: ({ children }: any) => children, + useWatch: () => 'agent-123', + }; +}); + +// Import after mocks +import { dataService } from 'librechat-data-provider'; +import { useGetAgentByIdQuery } from '~/data-provider'; +import AgentPanel from './AgentPanel'; + +// Mock useGetAgentByIdQuery +jest.mock('~/data-provider', () => { + const actual = jest.requireActual('~/data-provider') as any; + return { + ...actual, + useGetAgentByIdQuery: jest.fn(), + useUpdateAgentMutation: actual.useUpdateAgentMutation, + }; +}); + +// Test wrapper with QueryClient +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + return ({ children }: { children: React.ReactNode }) => ( + {children} + ); +}; + +// Test helpers +const setupMocks = () => { + const mockUseGetAgentByIdQuery = useGetAgentByIdQuery as jest.MockedFunction< + typeof useGetAgentByIdQuery + >; + const mockUpdateAgent = dataService.updateAgent as jest.MockedFunction< + typeof dataService.updateAgent + >; + + return { mockUseGetAgentByIdQuery, mockUpdateAgent }; +}; + +const mockAgentQuery = ( + mockUseGetAgentByIdQuery: jest.MockedFunction, + agent: Partial, +) => { + mockUseGetAgentByIdQuery.mockReturnValue({ + data: { + id: 'agent-123', + author: 'user-123', + isCollaborative: false, + ...agent, + } as Agent, + isInitialLoading: false, + } as any); +}; + +const createMockAgent = (overrides: Partial = {}): Agent => + ({ + id: 'agent-123', + provider: 'openai', + model: 'gpt-4', + ...overrides, + }) as Agent; + +const renderAndSubmitForm = async () => { + const Wrapper = createWrapper(); + const { container, rerender } = render(, { wrapper: Wrapper }); + + const form = container.querySelector('form'); + expect(form).toBeTruthy(); + + fireEvent.submit(form!); + + if (mockFormSubmitHandler) { + mockFormSubmitHandler(); + } + + return { container, rerender, form }; +}; + +describe('AgentPanel - Update Agent Toast Messages', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockShowToast = jest.fn(); + mockFormSubmitHandler = null; + }); + + describe('AgentPanel', () => { + it('should show "no changes" toast when version does not change', async () => { + const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks(); + + // Mock the agent query with version 2 + mockAgentQuery(mockUseGetAgentByIdQuery, { + name: 'Test Agent', + version: 2, + }); + + // Mock network response - same version + mockUpdateAgent.mockResolvedValue(createMockAgent({ name: 'Test Agent', version: 2 })); + + await renderAndSubmitForm(); + + // Wait for the toast to be shown + await waitFor(() => { + expect(mockShowToast).toHaveBeenCalledWith({ + message: 'com_ui_no_changes', + status: 'info', + }); + }); + }); + + it('should show "update success" toast when version changes', async () => { + const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks(); + + // Mock the agent query with version 2 + mockAgentQuery(mockUseGetAgentByIdQuery, { + name: 'Test Agent', + version: 2, + }); + + // Mock network response - different version + mockUpdateAgent.mockResolvedValue(createMockAgent({ name: 'Test Agent', version: 3 })); + + await renderAndSubmitForm(); + + await waitFor(() => { + expect(mockShowToast).toHaveBeenCalledWith({ + message: 'com_assistants_update_success Test Agent', + }); + }); + }); + + it('should show "update success" with default name when agent has no name', async () => { + const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks(); + + // Mock the agent query without name + mockAgentQuery(mockUseGetAgentByIdQuery, { + version: 1, + }); + + // Mock network response - no name + mockUpdateAgent.mockResolvedValue(createMockAgent({ version: 2 })); + + await renderAndSubmitForm(); + + await waitFor(() => { + expect(mockShowToast).toHaveBeenCalledWith({ + message: 'com_assistants_update_success com_ui_agent', + }); + }); + }); + + it('should show "update success" when agent query has no version (undefined)', async () => { + const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks(); + + // Mock the agent query with no version data + mockAgentQuery(mockUseGetAgentByIdQuery, { + name: 'Test Agent', + // No version property + }); + + mockUpdateAgent.mockResolvedValue(createMockAgent({ name: 'Test Agent', version: 1 })); + + await renderAndSubmitForm(); + + await waitFor(() => { + expect(mockShowToast).toHaveBeenCalledWith({ + message: 'com_assistants_update_success Test Agent', + }); + }); + }); + + it('should show error toast on update failure', async () => { + const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks(); + + // Mock the agent query + mockAgentQuery(mockUseGetAgentByIdQuery, { + name: 'Test Agent', + version: 1, + }); + + // Mock network error + mockUpdateAgent.mockRejectedValue(new Error('Update failed')); + + await renderAndSubmitForm(); + + await waitFor(() => { + expect(mockShowToast).toHaveBeenCalledWith({ + message: 'com_agents_update_error com_ui_error: Update failed', + status: 'error', + }); + }); + }); + }); +}); diff --git a/client/src/components/SidePanel/Agents/AgentPanel.tsx b/client/src/components/SidePanel/Agents/AgentPanel.tsx index 37a7d33d5..1f97ae09c 100644 --- a/client/src/components/SidePanel/Agents/AgentPanel.tsx +++ b/client/src/components/SidePanel/Agents/AgentPanel.tsx @@ -1,5 +1,5 @@ import { Plus } from 'lucide-react'; -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useRef } from 'react'; import { Button, useToastContext } from '@librechat/client'; import { useWatch, useForm, FormProvider } from 'react-hook-form'; import { useGetModelsQuery } from 'librechat-data-provider/react-query'; @@ -54,6 +54,7 @@ export default function AgentPanel() { const { control, handleSubmit, reset } = methods; const agent_id = useWatch({ control, name: 'id' }); + const previousVersionRef = useRef(); const allowedProviders = useMemo( () => new Set(agentsConfig?.allowedProviders), @@ -77,50 +78,29 @@ export default function AgentPanel() { /* Mutations */ const update = useUpdateAgentMutation({ + onMutate: () => { + // Store the current version before mutation + previousVersionRef.current = agentQuery.data?.version; + }, onSuccess: (data) => { - showToast({ - message: `${localize('com_assistants_update_success')} ${ - data.name ?? localize('com_ui_agent') - }`, - }); + // Check if agent version is the same (no changes were made) + if (previousVersionRef.current !== undefined && data.version === previousVersionRef.current) { + showToast({ + message: localize('com_ui_no_changes'), + status: 'info', + }); + } else { + showToast({ + message: `${localize('com_assistants_update_success')} ${ + data.name ?? localize('com_ui_agent') + }`, + }); + } + // Clear the ref after use + previousVersionRef.current = undefined; }, onError: (err) => { - const error = err as Error & { - statusCode?: number; - details?: { duplicateVersion?: any; versionIndex?: number }; - response?: { status?: number; data?: any }; - }; - - const isDuplicateVersionError = - (error.statusCode === 409 && error.details?.duplicateVersion) || - (error.response?.status === 409 && error.response?.data?.details?.duplicateVersion); - - if (isDuplicateVersionError) { - let versionIndex: number | undefined = undefined; - - if (error.details?.versionIndex !== undefined) { - versionIndex = error.details.versionIndex; - } else if (error.response?.data?.details?.versionIndex !== undefined) { - versionIndex = error.response.data.details.versionIndex; - } - - if (versionIndex === undefined || versionIndex < 0) { - showToast({ - message: localize('com_agents_update_error'), - status: 'error', - duration: 5000, - }); - } else { - showToast({ - message: localize('com_ui_agent_version_duplicate', { versionIndex: versionIndex + 1 }), - status: 'error', - duration: 10000, - }); - } - - return; - } - + const error = err as Error; showToast({ message: `${localize('com_agents_update_error')}${ error.message ? ` ${localize('com_ui_error')}: ${error.message}` : '' diff --git a/client/src/data-provider/Agents/mutations.ts b/client/src/data-provider/Agents/mutations.ts index 8fd3a842a..6e2935708 100644 --- a/client/src/data-provider/Agents/mutations.ts +++ b/client/src/data-provider/Agents/mutations.ts @@ -43,11 +43,7 @@ export const useCreateAgentMutation = ( */ export const useUpdateAgentMutation = ( options?: t.UpdateAgentMutationOptions, -): UseMutationResult< - t.Agent, - t.DuplicateVersionError, - { agent_id: string; data: t.AgentUpdateParams } -> => { +): UseMutationResult => { const queryClient = useQueryClient(); return useMutation( ({ agent_id, data }: { agent_id: string; data: t.AgentUpdateParams }) => { @@ -59,8 +55,7 @@ export const useUpdateAgentMutation = ( { onMutate: (variables) => options?.onMutate?.(variables), onError: (error, variables, context) => { - const typedError = error as t.DuplicateVersionError; - return options?.onError?.(typedError, variables, context); + return options?.onError?.(error, variables, context); }, onSuccess: (updatedAgent, variables, context) => { const listRes = queryClient.getQueryData([ diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 67fa367b1..0cefd5a13 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -554,7 +554,6 @@ "com_ui_agent_var": "{{0}} agent", "com_ui_agent_version": "Version", "com_ui_agent_version_active": "Active Version", - "com_ui_agent_version_duplicate": "Duplicate version detected. This would create a version identical to Version {{versionIndex}}.", "com_ui_agent_version_empty": "No versions available", "com_ui_agent_version_error": "Error fetching versions", "com_ui_agent_version_history": "Version History", diff --git a/packages/data-provider/src/types/mutations.ts b/packages/data-provider/src/types/mutations.ts index 48e34b5d8..7b1b65ab7 100644 --- a/packages/data-provider/src/types/mutations.ts +++ b/packages/data-provider/src/types/mutations.ts @@ -136,12 +136,7 @@ export type DuplicateVersionError = Error & { }; }; -export type UpdateAgentMutationOptions = MutationOptions< - Agent, - UpdateAgentVariables, - unknown, - DuplicateVersionError ->; +export type UpdateAgentMutationOptions = MutationOptions; export type DuplicateAgentBody = { agent_id: string; From 8530594f37120492574aad507ea77a6437fa4e38 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Wed, 6 Aug 2025 23:19:06 -0700 Subject: [PATCH 058/224] =?UTF-8?q?=F0=9F=9F=A2=20fix:=20Incorrect=20`cust?= =?UTF-8?q?omUserVars`=20Set=20States=20(#8905)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/data-schemas/src/methods/pluginAuth.ts | 9 +++++++-- packages/data-schemas/src/types/pluginAuth.ts | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/data-schemas/src/methods/pluginAuth.ts b/packages/data-schemas/src/methods/pluginAuth.ts index 5355fec50..9f79552a8 100644 --- a/packages/data-schemas/src/methods/pluginAuth.ts +++ b/packages/data-schemas/src/methods/pluginAuth.ts @@ -10,15 +10,20 @@ import type { // Factory function that takes mongoose instance and returns the methods export function createPluginAuthMethods(mongoose: typeof import('mongoose')) { /** - * Finds a single plugin auth entry by userId and authField + * Finds a single plugin auth entry by userId and authField (and optionally pluginKey) */ async function findOnePluginAuth({ userId, authField, + pluginKey, }: FindPluginAuthParams): Promise { try { const PluginAuth: Model = mongoose.models.PluginAuth; - return await PluginAuth.findOne({ userId, authField }).lean(); + return await PluginAuth.findOne({ + userId, + authField, + ...(pluginKey && { pluginKey }), + }).lean(); } catch (error) { throw new Error( `Failed to find plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`, diff --git a/packages/data-schemas/src/types/pluginAuth.ts b/packages/data-schemas/src/types/pluginAuth.ts index 421769eaa..c38bc790a 100644 --- a/packages/data-schemas/src/types/pluginAuth.ts +++ b/packages/data-schemas/src/types/pluginAuth.ts @@ -18,6 +18,7 @@ export interface PluginAuthQuery { export interface FindPluginAuthParams { userId: string; authField: string; + pluginKey?: string; } export interface FindPluginAuthsByKeysParams { From 47caafa8f84abd67edc61f5744e2cb83064b9b73 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Wed, 6 Aug 2025 23:31:05 -0700 Subject: [PATCH 059/224] =?UTF-8?q?=F0=9F=94=A7=20fix:=20MCP=20Queries=20a?= =?UTF-8?q?nd=20Connections=20(#8870)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add refetchQueries on connection success so ToolSelectDialog doesn't require hard refresh * fix: change hook so we only query connection status when mcpServers are configured * fix: change refetchQueries to invalidateQueries for tools after server connection update --------- Co-authored-by: Danny Avila --- client/src/hooks/MCP/useMCPServerManager.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/hooks/MCP/useMCPServerManager.ts b/client/src/hooks/MCP/useMCPServerManager.ts index 74e9dd7c7..b6b782cdc 100644 --- a/client/src/hooks/MCP/useMCPServerManager.ts +++ b/client/src/hooks/MCP/useMCPServerManager.ts @@ -81,7 +81,9 @@ export function useMCPServerManager() { return initialStates; }); - const { data: connectionStatusData } = useMCPConnectionStatusQuery(); + const { data: connectionStatusData } = useMCPConnectionStatusQuery({ + enabled: !!startupConfig?.mcpServers && Object.keys(startupConfig.mcpServers).length > 0, + }); const connectionStatus = useMemo( () => connectionStatusData?.connectionStatus || {}, [connectionStatusData?.connectionStatus], @@ -158,6 +160,8 @@ export function useMCPServerManager() { setMCPValues([...currentValues, serverName]); } + await queryClient.invalidateQueries([QueryKeys.tools]); + // This delay is to ensure UI has updated with new connection status before cleanup // Otherwise servers will show as disconnected for a second after OAuth flow completes setTimeout(() => { From 429bb6653a8078a401288bba318cd5680a1ef3c0 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 7 Aug 2025 14:36:10 -0400 Subject: [PATCH 060/224] =?UTF-8?q?=F0=9F=93=A6=20chore:=20bump=20`@librec?= =?UTF-8?q?hat/agents`=20to=20v2.4.70=20(#8923)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/package.json | 2 +- package-lock.json | 18 +++++++++--------- packages/api/package.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/api/package.json b/api/package.json index 4ea1335c3..bab4ef5e7 100644 --- a/api/package.json +++ b/api/package.json @@ -49,7 +49,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.69", + "@librechat/agents": "^2.4.70", "@librechat/api": "*", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", diff --git a/package-lock.json b/package-lock.json index 1b344c091..4e3ad26eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.69", + "@librechat/agents": "^2.4.70", "@librechat/api": "*", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", @@ -20124,9 +20124,9 @@ } }, "node_modules/@langchain/anthropic": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.3.24.tgz", - "integrity": "sha512-Gi1TwXu5vkCxUMToiXaiwTTWq9v3WMyU3ldB/VEWjzbkr3nKF5kcp+HLqhvV7WWOFVTTNgG+pzfq8JALecq5MA==", + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.3.26.tgz", + "integrity": "sha512-IRCjkxsMx6MZUZmv/aYX5A9RdIduzdR0eeOc4rX8waBcYP7qmtA/CUTNmTtMSoXfOfJY4s3414bkVNBkmS0+5g==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.56.0", @@ -21573,12 +21573,12 @@ } }, "node_modules/@librechat/agents": { - "version": "2.4.69", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.69.tgz", - "integrity": "sha512-Yt0rttqOaZQeZPIB68I8RdnU6SHeh0OJV5yEg8mx9EHTA7SnV/lOlDhn424aXdpMvYZYuxAt/Fev3jTC7qKiTg==", + "version": "2.4.70", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.70.tgz", + "integrity": "sha512-3FzKUd+VwAuPZBDYKPe707BNtnIsDVKGUpYgqjmnpSjDd5O5h1TUkcTjK36C68sNezsT6FJcDz1vozIEhN2aiA==", "license": "MIT", "dependencies": { - "@langchain/anthropic": "^0.3.24", + "@langchain/anthropic": "^0.3.26", "@langchain/aws": "^0.1.12", "@langchain/community": "^0.3.47", "@langchain/core": "^0.3.62", @@ -51414,7 +51414,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.69", + "@librechat/agents": "^2.4.70", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.8.2", diff --git a/packages/api/package.json b/packages/api/package.json index 5cebb81f2..1397a4bd0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -70,7 +70,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.69", + "@librechat/agents": "^2.4.70", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.8.2", From b9f72f4869e06217333dbf2038a936c0affc005f Mon Sep 17 00:00:00 2001 From: Joseph Licata <54822374+usnavy13@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:38:08 -0400 Subject: [PATCH 061/224] =?UTF-8?q?=F0=9F=8E=9A=EF=B8=8F=20refactor:=20Upd?= =?UTF-8?q?ate=20Min.=20Values=20for=20OpenAI=20Parameters=20(#8922)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/data-provider/src/schemas.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index bc488d404..8ad8fc6d8 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -208,13 +208,13 @@ export const openAISettings = { default: 1 as const, }, presence_penalty: { - min: 0 as const, + min: -2 as const, max: 2 as const, step: 0.01 as const, default: 0 as const, }, frequency_penalty: { - min: 0 as const, + min: -2 as const, max: 2 as const, step: 0.01 as const, default: 0 as const, @@ -374,13 +374,13 @@ export const agentsSettings = { default: 1 as const, }, presence_penalty: { - min: 0 as const, + min: -2 as const, max: 2 as const, step: 0.01 as const, default: 0 as const, }, frequency_penalty: { - min: 0 as const, + min: -2 as const, max: 2 as const, step: 0.01 as const, default: 0 as const, From d95d8032cc23c98ea48383e02100c1c6528b059c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 7 Aug 2025 15:03:19 -0400 Subject: [PATCH 062/224] =?UTF-8?q?=E2=9C=A8=20feat:=20GPT-OSS=20models=20?= =?UTF-8?q?Token=20Limits=20&=20Rates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/models/tx.js | 5 ++++- api/models/tx.spec.js | 12 ++++++++++++ api/utils/tokens.js | 10 ++++++++-- api/utils/tokens.spec.js | 28 +++++++++++++++++++++++++++- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/api/models/tx.js b/api/models/tx.js index b6d627620..ba7a04218 100644 --- a/api/models/tx.js +++ b/api/models/tx.js @@ -1,4 +1,4 @@ -const { matchModelName } = require('../utils'); +const { matchModelName } = require('../utils/tokens'); const defaultRate = 6; /** @@ -147,6 +147,9 @@ const tokenValues = Object.assign( codestral: { prompt: 0.3, completion: 0.9 }, 'ministral-8b': { prompt: 0.1, completion: 0.1 }, 'ministral-3b': { prompt: 0.04, completion: 0.04 }, + // GPT-OSS models + 'gpt-oss-20b': { prompt: 0.05, completion: 0.2 }, + 'gpt-oss-120b': { prompt: 0.15, completion: 0.6 }, }, bedrockValues, ); diff --git a/api/models/tx.spec.js b/api/models/tx.spec.js index 114b7b892..393a29482 100644 --- a/api/models/tx.spec.js +++ b/api/models/tx.spec.js @@ -311,6 +311,18 @@ describe('getMultiplier', () => { defaultRate, ); }); + + it('should return correct multipliers for GPT-OSS models', () => { + const models = ['gpt-oss-20b', 'gpt-oss-120b']; + models.forEach((key) => { + const expectedPrompt = tokenValues[key].prompt; + const expectedCompletion = tokenValues[key].completion; + expect(getMultiplier({ valueKey: key, tokenType: 'prompt' })).toBe(expectedPrompt); + expect(getMultiplier({ valueKey: key, tokenType: 'completion' })).toBe(expectedCompletion); + expect(getMultiplier({ model: key, tokenType: 'prompt' })).toBe(expectedPrompt); + expect(getMultiplier({ model: key, tokenType: 'completion' })).toBe(expectedCompletion); + }); + }); }); describe('AWS Bedrock Model Tests', () => { diff --git a/api/utils/tokens.js b/api/utils/tokens.js index 8f2173cbf..f33a82526 100644 --- a/api/utils/tokens.js +++ b/api/utils/tokens.js @@ -234,6 +234,9 @@ const aggregateModels = { ...xAIModels, // misc. kimi: 131000, + // GPT-OSS + 'gpt-oss-20b': 131000, + 'gpt-oss-120b': 131000, }; const maxTokensMap = { @@ -250,6 +253,8 @@ const modelMaxOutputs = { o1: 32268, // -500 from max: 32,768 'o1-mini': 65136, // -500 from max: 65,536 'o1-preview': 32268, // -500 from max: 32,768 + 'gpt-oss-20b': 131000, + 'gpt-oss-120b': 131000, system_default: 1024, }; @@ -468,10 +473,11 @@ const tiktokenModels = new Set([ ]); module.exports = { - tiktokenModels, - maxTokensMap, inputSchema, modelSchema, + maxTokensMap, + tiktokenModels, + maxOutputTokensMap, matchModelName, processModelData, getModelMaxTokens, diff --git a/api/utils/tokens.spec.js b/api/utils/tokens.spec.js index 2d4f05158..246fee80b 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -1,5 +1,11 @@ const { EModelEndpoint } = require('librechat-data-provider'); -const { getModelMaxTokens, processModelData, matchModelName, maxTokensMap } = require('./tokens'); +const { + maxOutputTokensMap, + getModelMaxTokens, + processModelData, + matchModelName, + maxTokensMap, +} = require('./tokens'); describe('getModelMaxTokens', () => { test('should return correct tokens for exact match', () => { @@ -349,6 +355,26 @@ describe('getModelMaxTokens', () => { expect(getModelMaxTokens('o3')).toBe(o3Tokens); expect(getModelMaxTokens('openai/o3')).toBe(o3Tokens); }); + + test('should return correct tokens for GPT-OSS models', () => { + const expected = maxTokensMap[EModelEndpoint.openAI]['gpt-oss-20b']; + ['gpt-oss-20b', 'gpt-oss-120b', 'openai/gpt-oss-20b', 'openai/gpt-oss-120b'].forEach((name) => { + expect(getModelMaxTokens(name)).toBe(expected); + }); + }); + + test('should return correct max output tokens for GPT-OSS models', () => { + const { getModelMaxOutputTokens } = require('./tokens'); + ['gpt-oss-20b', 'gpt-oss-120b'].forEach((model) => { + expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]); + expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe( + maxOutputTokensMap[EModelEndpoint.openAI][model], + ); + expect(getModelMaxOutputTokens(model, EModelEndpoint.azureOpenAI)).toBe( + maxOutputTokensMap[EModelEndpoint.azureOpenAI][model], + ); + }); + }); }); describe('matchModelName', () => { From c787515894b5cf38adec2cb6a07eaf0c0684fd99 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 7 Aug 2025 15:59:06 -0400 Subject: [PATCH 063/224] =?UTF-8?q?=F0=9F=A7=A0=20feat:=20Add=20`minimal`?= =?UTF-8?q?=20Reasoning=20Effort=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/locales/en/translation.json | 3 ++- package-lock.json | 2 +- packages/data-provider/package.json | 2 +- packages/data-provider/src/parameterSettings.ts | 2 ++ packages/data-provider/src/schemas.ts | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 0cefd5a13..740edaa42 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -229,7 +229,7 @@ "com_endpoint_openai_max_tokens": "Optional 'max_tokens' field, representing the maximum number of tokens that can be generated in the chat completion. The total length of input tokens and generated tokens is limited by the models context length. You may experience errors if this number exceeds the max context tokens.", "com_endpoint_openai_pres": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.", "com_endpoint_openai_prompt_prefix_placeholder": "Set custom instructions to include in System Message. Default: none", - "com_endpoint_openai_reasoning_effort": "o1 and o3 models only: constrains effort on reasoning for reasoning models. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.", + "com_endpoint_openai_reasoning_effort": "Reasoning models only: constrains effort on reasoning. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. 'Minimal' produces very few reasoning tokens for fastest time-to-first-token, especially well-suited for coding and instruction following.", "com_endpoint_openai_reasoning_summary": "Responses API only: A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. Set to none,auto, concise, or detailed.", "com_endpoint_openai_resend": "Resend all previously attached images. Note: this can significantly increase token cost and you may experience errors with many image attachments.", "com_endpoint_openai_resend_files": "Resend all previously attached files. Note: this will increase token cost and you may experience errors with many attachments.", @@ -860,6 +860,7 @@ "com_ui_mcp_update_var": "Update {{0}}", "com_ui_mcp_url": "MCP Server URL", "com_ui_medium": "Medium", + "com_ui_minimal": "Minimal", "com_ui_memories": "Memories", "com_ui_memories_allow_create": "Allow creating Memories", "com_ui_memories_allow_opt_out": "Allow users to opt out of Memories", diff --git a/package-lock.json b/package-lock.json index 4e3ad26eb..970c367fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51805,7 +51805,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.903", + "version": "0.8.001", "license": "ISC", "dependencies": { "axios": "^1.8.2", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index b54440a58..32d86c779 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.7.903", + "version": "0.8.001", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/parameterSettings.ts b/packages/data-provider/src/parameterSettings.ts index dee4e8b77..bebfa8377 100644 --- a/packages/data-provider/src/parameterSettings.ts +++ b/packages/data-provider/src/parameterSettings.ts @@ -221,12 +221,14 @@ const openAIParams: Record = { component: 'slider', options: [ ReasoningEffort.none, + ReasoningEffort.minimal, ReasoningEffort.low, ReasoningEffort.medium, ReasoningEffort.high, ], enumMappings: { [ReasoningEffort.none]: 'com_ui_none', + [ReasoningEffort.minimal]: 'com_ui_minimal', [ReasoningEffort.low]: 'com_ui_low', [ReasoningEffort.medium]: 'com_ui_medium', [ReasoningEffort.high]: 'com_ui_high', diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 8ad8fc6d8..1b18ea24b 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -113,6 +113,7 @@ export enum ImageDetail { export enum ReasoningEffort { none = '', + minimal = 'minimal', low = 'low', medium = 'medium', high = 'high', From 8a5047c456be2e300102037cf21cd08172a7ebcb Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 7 Aug 2025 16:00:49 -0400 Subject: [PATCH 064/224] =?UTF-8?q?=F0=9F=93=A6=20chore:=20bump=20`@librec?= =?UTF-8?q?hat/agents`=20to=20v2.4.71?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/package.json | 2 +- package-lock.json | 10 +++++----- packages/api/package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/package.json b/api/package.json index bab4ef5e7..7f57d8e8a 100644 --- a/api/package.json +++ b/api/package.json @@ -49,7 +49,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.70", + "@librechat/agents": "^2.4.71", "@librechat/api": "*", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", diff --git a/package-lock.json b/package-lock.json index 970c367fb..60aaa9737 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.70", + "@librechat/agents": "^2.4.71", "@librechat/api": "*", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", @@ -21573,9 +21573,9 @@ } }, "node_modules/@librechat/agents": { - "version": "2.4.70", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.70.tgz", - "integrity": "sha512-3FzKUd+VwAuPZBDYKPe707BNtnIsDVKGUpYgqjmnpSjDd5O5h1TUkcTjK36C68sNezsT6FJcDz1vozIEhN2aiA==", + "version": "2.4.71", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.71.tgz", + "integrity": "sha512-D2txcNt2pi6npwPPIwYUKIovBFGHZ56Z2aivVsqYPL9/JoHJW/kbuCiccEmjTUG0XULmQYksIEDOTvQz+6Js+Q==", "license": "MIT", "dependencies": { "@langchain/anthropic": "^0.3.26", @@ -51414,7 +51414,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.70", + "@librechat/agents": "^2.4.71", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.8.2", diff --git a/packages/api/package.json b/packages/api/package.json index 1397a4bd0..9f8541618 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -70,7 +70,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.70", + "@librechat/agents": "^2.4.71", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.8.2", From 430557676deaa4672925b7c9544a3781bc52c67b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 7 Aug 2025 16:01:29 -0400 Subject: [PATCH 065/224] =?UTF-8?q?=E2=9C=A8=20feat:=20GPT-5=20Token=20Lim?= =?UTF-8?q?its,=20Rates,=20Icon,=20Reasoning=20Support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/models/tx.js | 9 +++ api/models/tx.spec.js | 77 ++++++++++++++++++- api/utils/tokens.js | 6 ++ api/utils/tokens.spec.js | 61 +++++++++++++++ .../Endpoints/MessageEndpointIcon.tsx | 2 +- 5 files changed, 151 insertions(+), 4 deletions(-) diff --git a/api/models/tx.js b/api/models/tx.js index ba7a04218..ca69660f2 100644 --- a/api/models/tx.js +++ b/api/models/tx.js @@ -87,6 +87,9 @@ const tokenValues = Object.assign( 'gpt-4.1': { prompt: 2, completion: 8 }, 'gpt-4.5': { prompt: 75, completion: 150 }, 'gpt-4o-mini': { prompt: 0.15, completion: 0.6 }, + 'gpt-5': { prompt: 1.25, completion: 10 }, + 'gpt-5-mini': { prompt: 0.25, completion: 2 }, + 'gpt-5-nano': { prompt: 0.05, completion: 0.4 }, 'gpt-4o': { prompt: 2.5, completion: 10 }, 'gpt-4o-2024-05-13': { prompt: 5, completion: 15 }, 'gpt-4-1106': { prompt: 10, completion: 30 }, @@ -217,6 +220,12 @@ const getValueKey = (model, endpoint) => { return 'gpt-4.1'; } else if (modelName.includes('gpt-4o-2024-05-13')) { return 'gpt-4o-2024-05-13'; + } else if (modelName.includes('gpt-5-nano')) { + return 'gpt-5-nano'; + } else if (modelName.includes('gpt-5-mini')) { + return 'gpt-5-mini'; + } else if (modelName.includes('gpt-5')) { + return 'gpt-5'; } else if (modelName.includes('gpt-4o-mini')) { return 'gpt-4o-mini'; } else if (modelName.includes('gpt-4o')) { diff --git a/api/models/tx.spec.js b/api/models/tx.spec.js index 393a29482..d315d5862 100644 --- a/api/models/tx.spec.js +++ b/api/models/tx.spec.js @@ -25,8 +25,14 @@ describe('getValueKey', () => { expect(getValueKey('gpt-4-some-other-info')).toBe('8k'); }); - it('should return undefined for model names that do not match any known patterns', () => { - expect(getValueKey('gpt-5-some-other-info')).toBeUndefined(); + it('should return "gpt-5" for model name containing "gpt-5"', () => { + expect(getValueKey('gpt-5-some-other-info')).toBe('gpt-5'); + expect(getValueKey('gpt-5-2025-01-30')).toBe('gpt-5'); + expect(getValueKey('gpt-5-2025-01-30-0130')).toBe('gpt-5'); + expect(getValueKey('openai/gpt-5')).toBe('gpt-5'); + expect(getValueKey('openai/gpt-5-2025-01-30')).toBe('gpt-5'); + expect(getValueKey('gpt-5-turbo')).toBe('gpt-5'); + expect(getValueKey('gpt-5-0130')).toBe('gpt-5'); }); it('should return "gpt-3.5-turbo-1106" for model name containing "gpt-3.5-turbo-1106"', () => { @@ -84,6 +90,29 @@ describe('getValueKey', () => { expect(getValueKey('gpt-4.1-nano-0125')).toBe('gpt-4.1-nano'); }); + it('should return "gpt-5" for model type of "gpt-5"', () => { + expect(getValueKey('gpt-5-2025-01-30')).toBe('gpt-5'); + expect(getValueKey('gpt-5-2025-01-30-0130')).toBe('gpt-5'); + expect(getValueKey('openai/gpt-5')).toBe('gpt-5'); + expect(getValueKey('openai/gpt-5-2025-01-30')).toBe('gpt-5'); + expect(getValueKey('gpt-5-turbo')).toBe('gpt-5'); + expect(getValueKey('gpt-5-0130')).toBe('gpt-5'); + }); + + it('should return "gpt-5-mini" for model type of "gpt-5-mini"', () => { + expect(getValueKey('gpt-5-mini-2025-01-30')).toBe('gpt-5-mini'); + expect(getValueKey('openai/gpt-5-mini')).toBe('gpt-5-mini'); + expect(getValueKey('gpt-5-mini-0130')).toBe('gpt-5-mini'); + expect(getValueKey('gpt-5-mini-2025-01-30-0130')).toBe('gpt-5-mini'); + }); + + it('should return "gpt-5-nano" for model type of "gpt-5-nano"', () => { + expect(getValueKey('gpt-5-nano-2025-01-30')).toBe('gpt-5-nano'); + expect(getValueKey('openai/gpt-5-nano')).toBe('gpt-5-nano'); + expect(getValueKey('gpt-5-nano-0130')).toBe('gpt-5-nano'); + expect(getValueKey('gpt-5-nano-2025-01-30-0130')).toBe('gpt-5-nano'); + }); + it('should return "gpt-4o" for model type of "gpt-4o"', () => { expect(getValueKey('gpt-4o-2024-08-06')).toBe('gpt-4o'); expect(getValueKey('gpt-4o-2024-08-06-0718')).toBe('gpt-4o'); @@ -207,6 +236,48 @@ describe('getMultiplier', () => { ); }); + it('should return the correct multiplier for gpt-5', () => { + const valueKey = getValueKey('gpt-5-2025-01-30'); + expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5'].prompt); + expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe( + tokenValues['gpt-5'].completion, + ); + expect(getMultiplier({ model: 'gpt-5-preview', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5'].prompt, + ); + expect(getMultiplier({ model: 'openai/gpt-5', tokenType: 'completion' })).toBe( + tokenValues['gpt-5'].completion, + ); + }); + + it('should return the correct multiplier for gpt-5-mini', () => { + const valueKey = getValueKey('gpt-5-mini-2025-01-30'); + expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5-mini'].prompt); + expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe( + tokenValues['gpt-5-mini'].completion, + ); + expect(getMultiplier({ model: 'gpt-5-mini-preview', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5-mini'].prompt, + ); + expect(getMultiplier({ model: 'openai/gpt-5-mini', tokenType: 'completion' })).toBe( + tokenValues['gpt-5-mini'].completion, + ); + }); + + it('should return the correct multiplier for gpt-5-nano', () => { + const valueKey = getValueKey('gpt-5-nano-2025-01-30'); + expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5-nano'].prompt); + expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe( + tokenValues['gpt-5-nano'].completion, + ); + expect(getMultiplier({ model: 'gpt-5-nano-preview', tokenType: 'prompt' })).toBe( + tokenValues['gpt-5-nano'].prompt, + ); + expect(getMultiplier({ model: 'openai/gpt-5-nano', tokenType: 'completion' })).toBe( + tokenValues['gpt-5-nano'].completion, + ); + }); + it('should return the correct multiplier for gpt-4o', () => { const valueKey = getValueKey('gpt-4o-2024-08-06'); expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-4o'].prompt); @@ -307,7 +378,7 @@ describe('getMultiplier', () => { }); it('should return defaultRate if derived valueKey does not match any known patterns', () => { - expect(getMultiplier({ tokenType: 'prompt', model: 'gpt-5-some-other-info' })).toBe( + expect(getMultiplier({ tokenType: 'prompt', model: 'gpt-10-some-other-info' })).toBe( defaultRate, ); }); diff --git a/api/utils/tokens.js b/api/utils/tokens.js index f33a82526..0785dda01 100644 --- a/api/utils/tokens.js +++ b/api/utils/tokens.js @@ -19,6 +19,9 @@ const openAIModels = { 'gpt-4.1': 1047576, 'gpt-4.1-mini': 1047576, 'gpt-4.1-nano': 1047576, + 'gpt-5': 400000, + 'gpt-5-mini': 400000, + 'gpt-5-nano': 400000, 'gpt-4o': 127500, // -500 from max 'gpt-4o-mini': 127500, // -500 from max 'gpt-4o-2024-05-13': 127500, // -500 from max @@ -253,6 +256,9 @@ const modelMaxOutputs = { o1: 32268, // -500 from max: 32,768 'o1-mini': 65136, // -500 from max: 65,536 'o1-preview': 32268, // -500 from max: 32,768 + 'gpt-5': 128000, + 'gpt-5-mini': 128000, + 'gpt-5-nano': 128000, 'gpt-oss-20b': 131000, 'gpt-oss-120b': 131000, system_default: 1024, diff --git a/api/utils/tokens.spec.js b/api/utils/tokens.spec.js index 246fee80b..cc09bab31 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -156,6 +156,35 @@ describe('getModelMaxTokens', () => { ); }); + test('should return correct tokens for gpt-5 matches', () => { + expect(getModelMaxTokens('gpt-5')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5']); + expect(getModelMaxTokens('gpt-5-preview')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5']); + expect(getModelMaxTokens('openai/gpt-5')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5']); + expect(getModelMaxTokens('gpt-5-2025-01-30')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5'], + ); + }); + + test('should return correct tokens for gpt-5-mini matches', () => { + expect(getModelMaxTokens('gpt-5-mini')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5-mini']); + expect(getModelMaxTokens('gpt-5-mini-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5-mini'], + ); + expect(getModelMaxTokens('openai/gpt-5-mini')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5-mini'], + ); + }); + + test('should return correct tokens for gpt-5-nano matches', () => { + expect(getModelMaxTokens('gpt-5-nano')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5-nano']); + expect(getModelMaxTokens('gpt-5-nano-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5-nano'], + ); + expect(getModelMaxTokens('openai/gpt-5-nano')).toBe( + maxTokensMap[EModelEndpoint.openAI]['gpt-5-nano'], + ); + }); + test('should return correct tokens for Anthropic models', () => { const models = [ 'claude-2.1', @@ -363,6 +392,19 @@ describe('getModelMaxTokens', () => { }); }); + test('should return correct max output tokens for GPT-5 models', () => { + const { getModelMaxOutputTokens } = require('./tokens'); + ['gpt-5', 'gpt-5-mini', 'gpt-5-nano'].forEach((model) => { + expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]); + expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe( + maxOutputTokensMap[EModelEndpoint.openAI][model], + ); + expect(getModelMaxOutputTokens(model, EModelEndpoint.azureOpenAI)).toBe( + maxOutputTokensMap[EModelEndpoint.azureOpenAI][model], + ); + }); + }); + test('should return correct max output tokens for GPT-OSS models', () => { const { getModelMaxOutputTokens } = require('./tokens'); ['gpt-oss-20b', 'gpt-oss-120b'].forEach((model) => { @@ -446,6 +488,25 @@ describe('matchModelName', () => { expect(matchModelName('gpt-4.1-nano-2024-08-06')).toBe('gpt-4.1-nano'); }); + it('should return the closest matching key for gpt-5 matches', () => { + expect(matchModelName('openai/gpt-5')).toBe('gpt-5'); + expect(matchModelName('gpt-5-preview')).toBe('gpt-5'); + expect(matchModelName('gpt-5-2025-01-30')).toBe('gpt-5'); + expect(matchModelName('gpt-5-2025-01-30-0130')).toBe('gpt-5'); + }); + + it('should return the closest matching key for gpt-5-mini matches', () => { + expect(matchModelName('openai/gpt-5-mini')).toBe('gpt-5-mini'); + expect(matchModelName('gpt-5-mini-preview')).toBe('gpt-5-mini'); + expect(matchModelName('gpt-5-mini-2025-01-30')).toBe('gpt-5-mini'); + }); + + it('should return the closest matching key for gpt-5-nano matches', () => { + expect(matchModelName('openai/gpt-5-nano')).toBe('gpt-5-nano'); + expect(matchModelName('gpt-5-nano-preview')).toBe('gpt-5-nano'); + expect(matchModelName('gpt-5-nano-2025-01-30')).toBe('gpt-5-nano'); + }); + // Tests for Google models it('should return the exact model name if it exists in maxTokensMap - Google models', () => { expect(matchModelName('text-bison-32k', EModelEndpoint.google)).toBe('text-bison-32k'); diff --git a/client/src/components/Endpoints/MessageEndpointIcon.tsx b/client/src/components/Endpoints/MessageEndpointIcon.tsx index 1eb859c06..0a9782ce9 100644 --- a/client/src/components/Endpoints/MessageEndpointIcon.tsx +++ b/client/src/components/Endpoints/MessageEndpointIcon.tsx @@ -25,7 +25,7 @@ type EndpointIcon = { function getOpenAIColor(_model: string | null | undefined) { const model = _model?.toLowerCase() ?? ''; - if (model && /\b(o\d)\b/i.test(model)) { + if (model && (/\b(o\d)\b/i.test(model) || /\bgpt-[5-9]\b/i.test(model))) { return '#000000'; } return model.includes('gpt-4') ? '#AB68FF' : '#19C37D'; From 8238fb49e0f07f81a9916a3db1936d813fb726b9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 7 Aug 2025 16:22:10 -0400 Subject: [PATCH 066/224] =?UTF-8?q?=F0=9F=93=A6=20chore:=20bump=20`@librec?= =?UTF-8?q?hat/agents`=20to=20v2.4.72?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/package.json | 2 +- package-lock.json | 10 +++++----- packages/api/package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/package.json b/api/package.json index 7f57d8e8a..8b1e6459e 100644 --- a/api/package.json +++ b/api/package.json @@ -49,7 +49,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.71", + "@librechat/agents": "^2.4.72", "@librechat/api": "*", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", diff --git a/package-lock.json b/package-lock.json index 60aaa9737..102f98448 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.71", + "@librechat/agents": "^2.4.72", "@librechat/api": "*", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", @@ -21573,9 +21573,9 @@ } }, "node_modules/@librechat/agents": { - "version": "2.4.71", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.71.tgz", - "integrity": "sha512-D2txcNt2pi6npwPPIwYUKIovBFGHZ56Z2aivVsqYPL9/JoHJW/kbuCiccEmjTUG0XULmQYksIEDOTvQz+6Js+Q==", + "version": "2.4.72", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.72.tgz", + "integrity": "sha512-4AaTN3JcPa0hdIsMlGDOm6nT1YkFByo/P9i2V9ji1nlrgcamL2PTMrUPcLB3QFmnNupHFxb62/3t3GaviZ94Bw==", "license": "MIT", "dependencies": { "@langchain/anthropic": "^0.3.26", @@ -51414,7 +51414,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.71", + "@librechat/agents": "^2.4.72", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.8.2", diff --git a/packages/api/package.json b/packages/api/package.json index 9f8541618..f33854d46 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -70,7 +70,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.71", + "@librechat/agents": "^2.4.72", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.8.2", From e6fa01d514c1f26e44563d2d3f728691b8c2aaec Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Thu, 7 Aug 2025 22:24:42 +0200 Subject: [PATCH 067/224] =?UTF-8?q?=F0=9F=92=AC=20style:=20Enhance=20Toolt?= =?UTF-8?q?ip=20with=20HTML=20support=20and=20Improve=20Styling=20(#8915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: Enhance Tooltip component with HTML support and styling improvements * ✨ feat: Integrate DOMPurify for HTML sanitization in Tooltip component --- .../components/MCP/CustomUserVarsSection.tsx | 3 +- package-lock.json | 61 ++++--------------- packages/client/package.json | 3 +- packages/client/src/components/Tooltip.css | 10 +++ packages/client/src/components/Tooltip.tsx | 48 +++++++++++++-- 5 files changed, 71 insertions(+), 54 deletions(-) diff --git a/client/src/components/MCP/CustomUserVarsSection.tsx b/client/src/components/MCP/CustomUserVarsSection.tsx index 051634249..98e392554 100644 --- a/client/src/components/MCP/CustomUserVarsSection.tsx +++ b/client/src/components/MCP/CustomUserVarsSection.tsx @@ -32,13 +32,14 @@ function AuthField({ name, config, hasValue, control, errors }: AuthFieldProps)
- +
} /> diff --git a/package-lock.json b/package-lock.json index 102f98448..dede8c7c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2236,20 +2236,6 @@ "node": ">= 0.8.0" } }, - "api/node_modules/express-rate-limit": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", - "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "4 || 5 || ^5.0.0-beta.1" - } - }, "api/node_modules/express-session": { "version": "1.18.2", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", @@ -2510,38 +2496,6 @@ } } }, - "api/node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" - } - }, - "api/node_modules/mongodb-connection-string-url/node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "api/node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=16" - } - }, "api/node_modules/mongoose": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", @@ -29450,7 +29404,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true + "devOptional": true }, "node_modules/@types/unist": { "version": "2.0.10", @@ -33161,6 +33115,16 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -51507,7 +51471,7 @@ }, "packages/client": { "name": "@librechat/client", - "version": "0.2.3", + "version": "0.2.4", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^25.0.2", @@ -51560,6 +51524,7 @@ "@tanstack/react-virtual": "^3.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dompurify": "^3.2.6", "framer-motion": "^12.23.6", "i18next": "^24.2.2 || ^25.3.2", "i18next-browser-languagedetector": "^8.2.0", diff --git a/packages/client/package.json b/packages/client/package.json index 0e6a19aeb..189363f52 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/client", - "version": "0.2.3", + "version": "0.2.4", "description": "React components for LibreChat", "main": "dist/index.js", "module": "dist/index.es.js", @@ -54,6 +54,7 @@ "@react-spring/web": "^10.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dompurify": "^3.2.6", "framer-motion": "^12.23.6", "i18next-browser-languagedetector": "^8.2.0", "input-otp": "^1.4.2", diff --git a/packages/client/src/components/Tooltip.css b/packages/client/src/components/Tooltip.css index 92e52037c..46ecb37cb 100644 --- a/packages/client/src/components/Tooltip.css +++ b/packages/client/src/components/Tooltip.css @@ -11,6 +11,16 @@ line-height: 1.5rem; color: black; box-shadow: 0 2px 4px 0 rgb(0 0 0 / 0.25); + /* Enhanced layout for longer descriptions */ + max-width: 320px; + word-wrap: break-word; + text-align: left; +} + +@media (max-width: 640px) { + .tooltip { + max-width: 200px; + } } .tooltip:where(.dark, .dark *) { diff --git a/packages/client/src/components/Tooltip.tsx b/packages/client/src/components/Tooltip.tsx index b83452d65..2f7675d10 100644 --- a/packages/client/src/components/Tooltip.tsx +++ b/packages/client/src/components/Tooltip.tsx @@ -1,6 +1,7 @@ +import DOMPurify from 'dompurify'; import * as Ariakit from '@ariakit/react'; +import { forwardRef, useId, useMemo } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; -import { forwardRef, useMemo } from 'react'; import { cn } from '~/utils'; import './Tooltip.css'; @@ -8,18 +9,47 @@ interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps { description: string; side?: 'top' | 'bottom' | 'left' | 'right'; className?: string; - focusable?: boolean; role?: string; + enableHTML?: boolean; } export const TooltipAnchor = forwardRef(function TooltipAnchor( - { description, side = 'top', className, role, ...props }, + { description, side = 'top', className, role, enableHTML = false, ...props }, ref, ) { const tooltip = Ariakit.useTooltipStore({ placement: side }); const mounted = Ariakit.useStoreState(tooltip, (state) => state.mounted); const placement = Ariakit.useStoreState(tooltip, (state) => state.placement); + const id = useId(); + const sanitizer = useMemo(() => { + const instance = DOMPurify(); + instance.addHook('afterSanitizeAttributes', (node) => { + if (node.tagName && node.tagName === 'A') { + node.setAttribute('target', '_blank'); + node.setAttribute('rel', 'noopener noreferrer'); + } + }); + return instance; + }, []); + + const sanitizedHTML = useMemo(() => { + if (!enableHTML) { + return ''; + } + try { + return sanitizer.sanitize(description, { + ALLOWED_TAGS: ['a', 'strong', 'b', 'em', 'i', 'br', 'code'], + ALLOWED_ATTR: ['href', 'class', 'target', 'rel'], + ALLOW_DATA_ATTR: false, + ALLOW_ARIA_ATTR: false, + }); + } catch (error) { + console.error('Sanitization failed', error); + return description; + } + }, [enableHTML, description, sanitizer]); + const { x, y } = useMemo(() => { const dir = placement.split('-')[0]; switch (dir) { @@ -49,6 +79,7 @@ export const TooltipAnchor = forwardRef(func {...props} ref={ref} role={role} + aria-describedby={id} onKeyDown={handleKeyDown} className={cn('cursor-pointer', className)} /> @@ -58,6 +89,7 @@ export const TooltipAnchor = forwardRef(func gutter={4} alwaysVisible className="tooltip" + id={id} render={ (func } > - {description} + {enableHTML ? ( +
+ ) : ( + description + )} )} From 922f43f52056e16774d7cd7f87cf831a806e74a5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:25:24 -0400 Subject: [PATCH 068/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#8907)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/da/translation.json | 1 - client/src/locales/de/translation.json | 1 - client/src/locales/et/translation.json | 1 - client/src/locales/fr/translation.json | 1 - client/src/locales/he/translation.json | 1 - client/src/locales/ja/translation.json | 1 - client/src/locales/ko/translation.json | 1 - client/src/locales/lv/translation.json | 65 ++++++++++----------- client/src/locales/zh-Hans/translation.json | 1 - 9 files changed, 32 insertions(+), 41 deletions(-) diff --git a/client/src/locales/da/translation.json b/client/src/locales/da/translation.json index a51f10659..9face0571 100644 --- a/client/src/locales/da/translation.json +++ b/client/src/locales/da/translation.json @@ -516,7 +516,6 @@ "com_ui_agent_var": "{{0}} agent", "com_ui_agent_version": "Version", "com_ui_agent_version_active": "Aktiv version", - "com_ui_agent_version_duplicate": "Duplikatversion fundet. Dette vil skabe en version, der er identisk med Version {{versionIndex}}.", "com_ui_agent_version_empty": "Ingen tilgængelige versioner", "com_ui_agent_version_error": "Fejl ved hentning af versioner", "com_ui_agent_version_history": "Versionshistorik", diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index 5876eebd2..2ef81ea29 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -548,7 +548,6 @@ "com_ui_agent_var": "{{0}} Agent", "com_ui_agent_version": "Version", "com_ui_agent_version_active": "Aktive Version\n", - "com_ui_agent_version_duplicate": "Doppelte Version entdeckt. Dies würde eine Version erzeugen, die identisch mit der Version {{versionIndex}} ist.", "com_ui_agent_version_empty": "Keine Versionen verfügbar\n", "com_ui_agent_version_error": "Fehler beim Abrufen der Versionen", "com_ui_agent_version_history": "Versionsgeschichte\n", diff --git a/client/src/locales/et/translation.json b/client/src/locales/et/translation.json index dc5a7b946..e27f46914 100644 --- a/client/src/locales/et/translation.json +++ b/client/src/locales/et/translation.json @@ -517,7 +517,6 @@ "com_ui_agent_var": "{{0}} agent", "com_ui_agent_version": "Versioon", "com_ui_agent_version_active": "Aktiivne versioon", - "com_ui_agent_version_duplicate": "Tuvastati duplikaatversioon. See looks versiooni, mis on identne versiooniga {{versionIndex}}.", "com_ui_agent_version_empty": "Versioone pole saadaval", "com_ui_agent_version_error": "Viga versioonide laadimisel", "com_ui_agent_version_history": "Versioonide ajalugu", diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index 9e8360423..455f5dca6 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -547,7 +547,6 @@ "com_ui_agent_var": "agent {{0}}", "com_ui_agent_version": "Version", "com_ui_agent_version_active": "Version active", - "com_ui_agent_version_duplicate": "Duplicata de version détecté. Cela créerait une version identique à la version {{versionIndex}}", "com_ui_agent_version_empty": "Aucune version disponible", "com_ui_agent_version_error": "Erreur lors de la collecte des versions", "com_ui_agent_version_history": "Historique des versions", diff --git a/client/src/locales/he/translation.json b/client/src/locales/he/translation.json index 5edb6861b..e4a853b7c 100644 --- a/client/src/locales/he/translation.json +++ b/client/src/locales/he/translation.json @@ -540,7 +540,6 @@ "com_ui_agent_var": "{{0}} סוכנים", "com_ui_agent_version": "גרסה", "com_ui_agent_version_active": "גרסת הפעלה", - "com_ui_agent_version_duplicate": "זוהתה גרסה כפולה, פעולה זו תיצור גרסה זהה לגרסה {{versionIndex}}.", "com_ui_agent_version_empty": "אין גרסאות זמינות", "com_ui_agent_version_error": "שגיאה באחזור גרסאות", "com_ui_agent_version_history": "היסטוריית גרסאות", diff --git a/client/src/locales/ja/translation.json b/client/src/locales/ja/translation.json index 9ba078b5d..27b9fe407 100644 --- a/client/src/locales/ja/translation.json +++ b/client/src/locales/ja/translation.json @@ -546,7 +546,6 @@ "com_ui_agent_var": "{{0}}エージェント", "com_ui_agent_version": "バージョン", "com_ui_agent_version_active": "アクティブバージョン", - "com_ui_agent_version_duplicate": "重複バージョンが検出されました。これにより、バージョン{{versionIndex}}と同一のバージョンが作成されます。", "com_ui_agent_version_empty": "利用可能なバージョンはありません", "com_ui_agent_version_error": "バージョン取得エラー", "com_ui_agent_version_history": "バージョン履歴", diff --git a/client/src/locales/ko/translation.json b/client/src/locales/ko/translation.json index f7bc47da4..6a3ba3cb7 100644 --- a/client/src/locales/ko/translation.json +++ b/client/src/locales/ko/translation.json @@ -549,7 +549,6 @@ "com_ui_agent_var": "{{0}} 에이전트", "com_ui_agent_version": "버전", "com_ui_agent_version_active": "활성 버전", - "com_ui_agent_version_duplicate": "중복 버전이 감지되었습니다. 이는 버전 {{versionIndex}}와 동일한 버전을 생성합니다.", "com_ui_agent_version_empty": "사용 가능한 버전이 없습니다", "com_ui_agent_version_error": "버전 정보 가져오기 오류", "com_ui_agent_version_history": "버전 기록", diff --git a/client/src/locales/lv/translation.json b/client/src/locales/lv/translation.json index cb59d448f..890e9714a 100644 --- a/client/src/locales/lv/translation.json +++ b/client/src/locales/lv/translation.json @@ -96,7 +96,7 @@ "com_auth_email_verification_failed_token_missing": "Verifikācija neizdevās, trūkst tokena", "com_auth_email_verification_in_progress": "Jūsu e-pasta verifikācija, lūdzu, uzgaidiet", "com_auth_email_verification_invalid": "Nederīga e-pasta verifikācija", - "com_auth_email_verification_redirecting": "Pārvirzīšana {{0}} sekundēs...", + "com_auth_email_verification_redirecting": "Pārvirzu {{0}} sekundēs...", "com_auth_email_verification_resend_prompt": "Nesaņēmāt e-pastu?", "com_auth_email_verification_success": "E-pasts veiksmīgi pārbaudīts", "com_auth_email_verifying_ellipsis": "Pārbauda...", @@ -155,7 +155,7 @@ "com_endpoint_ai": "Mākslīgais intelekts", "com_endpoint_anthropic_maxoutputtokens": "Maksimālais atbildē ģenerējamo tokenu skaits. Norādiet zemāku vērtību īsākām atbildēm un augstāku vērtību garākām atbildēm. Piezīme: modeļi var apstāties pirms šī maksimālā skaita sasniegšanas.", "com_endpoint_anthropic_prompt_cache": "Uzvednes kešatmiņa ļauj atkārtoti izmantot lielu kontekstu vai instrukcijas API izsaukumos, samazinot izmaksas un ābildes ātrumu.", - "com_endpoint_anthropic_temp": "Diapazons no 0 līdz 1. Analītiskiem/atbilžu variantiem izmantojiet temp vērtību tuvāk 0, bet radošiem un ģeneratīviem uzdevumiem — tuvāk 1. Iesakām mainīt šo vai Top P, bet ne abus.", + "com_endpoint_anthropic_temp": "Diapazons no 0 līdz 1. Analītiskiem/atbilžu variantiem izmantot temp vērtību tuvāk 0, bet radošiem un ģeneratīviem uzdevumiem — tuvāk 1. Iesakām mainīt šo vai Top P, bet ne abus.", "com_endpoint_anthropic_thinking": "Iespējo iekšējo domāšanu atbalstītajiem Claude modeļiem (3.7 Sonnet). Piezīme: nepieciešams iestatīt \"Domāšanas budžetu\", kam arī jābūt zemākam par \"Max Output Tokens\".", "com_endpoint_anthropic_thinking_budget": "Nosaka maksimālo žetonu skaitu, ko Claude drīkst izmantot savā iekšējā domāšanas procesā. Lielāki budžeti var uzlabot atbilžu kvalitāti, nodrošinot rūpīgāku analīzi sarežģītām problēmām, lai gan Claude var neizmantot visu piešķirto budžetu, īpaši diapazonos virs 32 000. Šim iestatījumam jābūt zemākam par \"Maksimālie izvades tokeni\".", "com_endpoint_anthropic_topk": "Top-k maina to, kā modelis atlasa marķierus izvadei. Ja top-k ir 1, tas nozīmē, ka atlasītais marķieris ir visticamākais starp visiem modeļa vārdu krājumā esošajiem marķieriem (to sauc arī par alkatīgo dekodēšanu), savukārt, ja top-k ir 3, tas nozīmē, ka nākamais marķieris tiek izvēlēts no 3 visticamākajiem marķieriem (izmantojot temperatūru).", @@ -188,7 +188,7 @@ "com_endpoint_config_placeholder": "Iestatiet savu atslēgu galvenes izvēlnē, lai izveidotu sarunu.", "com_endpoint_config_value": "Ievadiet vērtību", "com_endpoint_context": "Konteksts", - "com_endpoint_context_info": "Maksimālais tokenu skaits, ko var izmantot kontekstam. Izmantojiet to, lai kontrolētu, cik tokenu tiek nosūtīti katrā pieprasījumā. Ja tas nav norādīts, tiks izmantoti sistēmas noklusējuma iestatījumi, pamatojoties uz zināmo modeļu konteksta lielumu. Augstāku vērtību iestatīšana var izraisīt kļūdas un/vai augstākas tokenu izmaksas.", + "com_endpoint_context_info": "Maksimālais tokenu skaits, ko var izmantot kontekstam. Izmanto to, lai kontrolētu, cik tokenu tiek nosūtīti katrā pieprasījumā. Ja tas nav norādīts, tiks izmantoti sistēmas noklusējuma iestatījumi, pamatojoties uz zināmo modeļu konteksta lielumu. Augstāku vērtību iestatīšana var izraisīt kļūdas un/vai augstākas tokenu izmaksas.", "com_endpoint_context_tokens": "Maksimālais konteksta tokenu skaits", "com_endpoint_custom_name": "Pielāgots nosaukums", "com_endpoint_default": "noklusējuma", @@ -212,7 +212,7 @@ "com_endpoint_google_thinking_budget": "Norāda modeļa izmantoto domāšanas tokenu skaitu. Faktiskais skaits var pārsniegt vai būt mazāks par šo vērtību atkarībā no uzvednes.\n\nŠo iestatījumu atbalsta tikai noteikti modeļi (2.5 sērija). Gemini 2.5 Pro atbalsta 128–32 768 žetonus. Gemini 2.5 Flash atbalsta 0–24 576 žetonus. Gemini 2.5 Flash Lite atbalsta 512–24 576 žetonus.\n\nAtstājiet tukšu vai iestatiet uz \"-1\", lai modelis automātiski izlemtu, kad un cik daudz domāt. Pēc noklusējuma Gemini 2.5 Flash Lite nedomā.", "com_endpoint_google_topk": "Top-k maina to, kā modelis atlasa marķierus izvadei. Ja top-k ir 1, tas nozīmē, ka atlasītais marķieris ir visticamākais starp visiem modeļa vārdu krājumā esošajiem marķieriem (to sauc arī par alkatīgo dekodēšanu), savukārt, ja top-k ir 3, tas nozīmē, ka nākamais marķieris tiek izvēlēts no 3 visticamākajiem marķieriem (izmantojot temperatūru).", "com_endpoint_google_topp": "`Top-p` maina to, kā modelis atlasa tokenus izvadei. Marķieri tiek atlasīti no K (skatīt parametru topK) ticamākās līdz vismazāk ticamajai, līdz to varbūtību summa ir vienāda ar `top-p` vērtību.", - "com_endpoint_google_use_search_grounding": "Izmantojiet Google meklēšanas pamatošanas funkciju, lai uzlabotu atbildes ar reāllaika tīmekļa meklēšanas rezultātiem. Tas ļauj modeļiem piekļūt aktuālajai informācijai un sniegt precīzākas, aktuālākas atbildes.", + "com_endpoint_google_use_search_grounding": "Izmantot Google meklēšanas pamatošanas funkciju, lai uzlabotu atbildes ar reāllaika tīmekļa meklēšanas rezultātiem. Tas ļauj modeļiem piekļūt aktuālajai informācijai un sniegt precīzākas, aktuālākas atbildes.", "com_endpoint_instructions_assistants": "Pārrakstīt instrukcijas", "com_endpoint_instructions_assistants_placeholder": "Pārraksta asistenta norādījumus. Tas ir noderīgi, lai mainītu darbību katrā palaišanas reizē.", "com_endpoint_max_output_tokens": "Maksimālais izvades tokenu skaits", @@ -236,14 +236,14 @@ "com_endpoint_openai_stop": "Līdz 4 secībām, kurās API pārtrauks turpmāku tokenu ģenerēšanu.", "com_endpoint_openai_temp": "Augstākas vērtības = nejaušāks, savukārt zemākas vērtības = fokusētāks un deterministiskāks. Iesakām mainīt šo vai Top P, bet ne abus.", "com_endpoint_openai_topp": "Alternatīva izlasei ar temperatūru, ko sauc par kodola izlasi, kur modelis ņem vērā tokenu rezultātus ar varbūtības masu top_p. Tātad 0,1 nozīmē, ka tiek ņemti vērā tikai tie tokeni, kas veido augšējo 10% varbūtības masu. Mēs iesakām mainīt šo vai temperatūru, bet ne abus.", - "com_endpoint_openai_use_responses_api": "Izmantojiet Response API sarunas pabeigšanas vietā, kas ietver paplašinātas OpenAI funkcijas. Nepieciešams o1-pro, o3-pro un spriešanas kopsavilkumu iespējošanai.", + "com_endpoint_openai_use_responses_api": "Izmantot Response API sarunas pabeigšanas vietā, kas ietver paplašinātas OpenAI funkcijas. Nepieciešams o1-pro, o3-pro un spriešanas kopsavilkumu iespējošanai.", "com_endpoint_openai_use_web_search": "Iespējojiet tīmekļa meklēšanas funkcionalitāti, izmantojot OpenAI iebūvētās meklēšanas iespējas. Tas ļauj modelim meklēt tīmeklī aktuālu informāciju un sniegt precīzākas, aktuālākas atbildes.", "com_endpoint_output": "Izvade", "com_endpoint_plug_image_detail": "Attēla detaļas", "com_endpoint_plug_resend_files": "Atkārtoti nosūtīt failus", "com_endpoint_plug_set_custom_instructions_for_gpt_placeholder": "Iestatiet pielāgotas instrukcijas, kas jāiekļauj sistēmas ziņā. Noklusējuma vērtība: nav", "com_endpoint_plug_skip_completion": "Izlaist pabeigšanu", - "com_endpoint_plug_use_functions": "Izmant funkcijas", + "com_endpoint_plug_use_functions": "Izmantot funkcijas", "com_endpoint_presence_penalty": "Klātbūtnes sods", "com_endpoint_preset": "iepriekš iestatīts", "com_endpoint_preset_custom_name_placeholder": "kaut kam šeit ir jānotiek. bija tukšs", @@ -308,15 +308,15 @@ "com_files_table": "kaut kam šeit ir jānotiek. bija tukšs", "com_generated_files": "Ģenerētie faili:", "com_hide_examples": "Slēpt piemērus", - "com_info_heic_converting": "HEIC attēla konvertēšana uz JPEG...", + "com_info_heic_converting": "Konvertēju HEIC attēlu uz JPEG...", "com_nav_2fa": "Divfaktoru autentifikācija (2FA)", "com_nav_account_settings": "Konta iestatījumi", "com_nav_always_make_prod": "Vienmēr uzlieciet jaunas versijas produkcijā", "com_nav_archive_created_at": "Arhivēšanas datums", "com_nav_archive_name": "Vārds", - "com_nav_archived_chats": "Arhivētas sarunas", + "com_nav_archived_chats": "Arhivētās sarunas", "com_nav_at_command": "@-Komanda", - "com_nav_at_command_description": "Pārslēgšanas komanda \"@\" galapunktu, modeļu, sākotnējo iestatījumu u. c. pārslēgšanai.", + "com_nav_at_command_description": "Pārslēgšanas komanda \"@\" galapunktu, modeļu, sākotnējo iestatījumu u.c. pārslēgšanai.", "com_nav_audio_play_error": "Kļūda, atskaņojot audio: {{0}}", "com_nav_audio_process_error": "Kļūda, apstrādājot audio: {{0}}", "com_nav_auto_scroll": "Automātiski iet uz jaunāko ziņu, atverot sarunu", @@ -334,14 +334,14 @@ "com_nav_balance_every": "Katras", "com_nav_balance_hour": "stunda", "com_nav_balance_hours": "stundas", - "com_nav_balance_interval": "Intervāls:", + "com_nav_balance_interval": "Atjaunošanas biežums:", "com_nav_balance_last_refill": "Pēdējā bilances papildišana:", "com_nav_balance_minute": "minūte", "com_nav_balance_minutes": "minūtes", "com_nav_balance_month": "mēnesis", "com_nav_balance_months": "mēneši", "com_nav_balance_next_refill": "Nākamā bilances papildināšana:", - "com_nav_balance_next_refill_info": "Nākamā bilances papildināšana notiks automātiski tikai tad, ja būs izpildīti abi nosacījumi: kopš pēdējās bilances papildināšanas ir pagājis norādītais laika intervāls un uzaicinājuma nosūtīšana izraisītu jūsu atlikuma samazināšanos zem nulles.", + "com_nav_balance_next_refill_info": "Nākamā bilances papildināšana notiks automātiski tikai tad, ja būs izpildīti abi nosacījumi: kopš pēdējās bilances papildināšanas ir pagājis norādītais laika atjaunošanas biežums un uzaicinājuma nosūtīšana izraisītu jūsu atlikuma samazināšanos zem nulles.", "com_nav_balance_refill_amount": "Bilances papildināšanas apjoms:", "com_nav_balance_second": "otrais", "com_nav_balance_seconds": "sekundes", @@ -353,10 +353,10 @@ "com_nav_chat_commands": "Sarunu komandas", "com_nav_chat_commands_info": "Šīs komandas tiek aktivizētas, ierakstot noteiktas rakstzīmes ziņas sākumā. Katru komandu aktivizē tai norādītais prefikss. Varat tās atspējot, ja bieži izmantojat šīs rakstzīmes ziņojumu sākumā.", "com_nav_chat_direction": "Sarunas virziens", - "com_nav_clear_all_chats": "Notīrīt visas sarunas", + "com_nav_clear_all_chats": "Dzēst visas saglabātās sarunas", "com_nav_clear_cache_confirm_message": "Vai tiešām vēlaties notīrīt kešatmiņu?", "com_nav_clear_conversation": "Skaidras sarunas", - "com_nav_clear_conversation_confirm_message": "Vai tiešām vēlaties notīrīt visas sarunas? Šī darbība ir neatgriezeniska.", + "com_nav_clear_conversation_confirm_message": "Vai tiešām vēlaties dzēst visas saglabātās sarunas? Šī darbība ir neatgriezeniska.", "com_nav_close_sidebar": "Aizvērt sānu joslu", "com_nav_commands": "Komandas", "com_nav_confirm_clear": "Apstiprināt dzēšanu", @@ -371,7 +371,7 @@ "com_nav_delete_data_info": "Visi jūsu dati tiks dzēsti.", "com_nav_delete_warning": "BRĪDINĀJUMS: Tas neatgriezeniski izdzēsīs jūsu kontu.", "com_nav_enable_cache_tts": "Iespējot kešatmiņu TTS", - "com_nav_enable_cloud_browser_voice": "Izmantojiet cloud-based balsis", + "com_nav_enable_cloud_browser_voice": "Izmantot mākonī bāzētas balsis", "com_nav_enabled": "Iespējots", "com_nav_engine": "Dzinējs", "com_nav_enter_to_send": "Nospiediet taustiņu Enter, lai nosūtītu ziņas", @@ -392,7 +392,7 @@ "com_nav_font_size_xl": "Īpaši liels", "com_nav_font_size_xs": "Īpaši mazs", "com_nav_help_faq": "Palīdzība un bieži uzdotie jautājumi", - "com_nav_hide_panel": "Slēpt labās malējās sānu paneli", + "com_nav_hide_panel": "Slēpt labo sāna paneli", "com_nav_info_balance": "Bilance parāda, cik daudz tokenu kredītu jums ir atlicis izmantot. Tokenu kredīti tiek pārvērsti naudas vērtībā (piemēram, 1000 kredīti = 0,001 USD).", "com_nav_info_code_artifacts": "Iespējo eksperimentāla koda artefaktu rādīšanu blakus sarunai", "com_nav_info_code_artifacts_agent": "Iespējo koda artefaktu izmantošanu šim aģentam. Pēc noklusējuma tiek pievienotas papildu instrukcijas, kas attiecas uz artefaktu izmantošanu, ja vien nav iespējots \"Pielāgots uzvednes režīms\".", @@ -445,7 +445,7 @@ "com_nav_latex_parsing": "LaTeX parsēšana ziņās (var ietekmēt veiktspēju)", "com_nav_log_out": "Izrakstīties", "com_nav_long_audio_warning": "Garāku tekstu apstrāde prasīs ilgāku laiku.", - "com_nav_maximize_chat_space": "Maksimāli izmantojiet sarunas telpu", + "com_nav_maximize_chat_space": "Maksimāli izmantot sarunas telpas izmērus", "com_nav_mcp_configure_server": "Konfigurēt {{0}}", "com_nav_mcp_status_connecting": "{{0}} - Savienojas", "com_nav_mcp_vars_update_error": "Kļūda, atjauninot MCP pielāgotos lietotāja parametrus: {{0}}", @@ -465,7 +465,7 @@ "com_nav_profile_picture": "Profila attēls", "com_nav_save_badges_state": "Saglabāt nozīmīšu stāvokli", "com_nav_save_drafts": "Saglabāt melnrakstus lokāli", - "com_nav_scroll_button": "Ritiniet līdz beigu pogai", + "com_nav_scroll_button": "Pāriet uz pēdējo ierakstu poga", "com_nav_search_placeholder": "Meklēt ziņas", "com_nav_send_message": "Sūtīt ziņu", "com_nav_setting_account": "Konts", @@ -481,10 +481,10 @@ "com_nav_show_code": "Vienmēr rādīt kodu, izmantojot koda interpretētāju", "com_nav_show_thinking": "Pēc noklusējuma atvērt domāšanas nolaižamos sarakstus", "com_nav_slash_command": "/-Komanda", - "com_nav_slash_command_description": "Pārslēgt komandu \"/\", lai atlasītu uzvedni, izmantojot tastatūru", - "com_nav_speech_to_text": "Runas pārvēršana tekstā", + "com_nav_slash_command_description": "Ieslēgt komandu \"/\", lai atlasītu uzvedni izmantojot tastatūru", + "com_nav_speech_to_text": "Balss pārvēršana tekstā", "com_nav_stop_generating": "Pārtraukt ģenerēšanu", - "com_nav_text_to_speech": "Teksts runā", + "com_nav_text_to_speech": "Teksta pārvēršana balsī", "com_nav_theme": "Tēma", "com_nav_theme_dark": "Tumšs", "com_nav_theme_light": "Gaišs", @@ -554,7 +554,6 @@ "com_ui_agent_var": "{{0}} aģents", "com_ui_agent_version": "Versija", "com_ui_agent_version_active": "Aktīvā versija", - "com_ui_agent_version_duplicate": "Atrasta dublikāta versija. Šī darbība izveidotu versiju, kas ir identiska citai, jau esošai versijai. {{versionIndex}}.", "com_ui_agent_version_empty": "Nav pieejamu versiju", "com_ui_agent_version_error": "Kļūda, ielādējot versijas", "com_ui_agent_version_history": "Versiju vēsture", @@ -717,13 +716,13 @@ "com_ui_delete_tool": "Dzēst rīku", "com_ui_delete_tool_confirm": "Vai tiešām vēlaties dzēst šo rīku?", "com_ui_deleted": "Dzēsts", - "com_ui_deleting_file": "Tiek dzēsts fails...", + "com_ui_deleting_file": "Dzēšu failu...", "com_ui_descending": "Dilstošs", "com_ui_description": "Apraksts", "com_ui_description_placeholder": "Pēc izvēles: ievadiet aprakstu, kas jāparāda uzvednē", "com_ui_deselect_all": "Noņemt atlasi visam", "com_ui_detailed": "Detalizēta", - "com_ui_disabling": "Atspējošana...", + "com_ui_disabling": "Atspējo...", "com_ui_download": "Lejupielādēt", "com_ui_download_artifact": "Lejupielādēt artefaktu", "com_ui_download_backup": "Lejupielādēt rezerves kodus", @@ -734,7 +733,7 @@ "com_ui_dropdown_variables_info": "Izveidojiet pielāgotas nolaižamās izvēlnes savām uzvednēm:{{variable_name:option1|option2|option3}}`", "com_ui_duplicate": "Dublikāts", "com_ui_duplication_error": "Sarunas dublēšanas laikā radās kļūda.", - "com_ui_duplication_processing": "Sarunas dublēšana...", + "com_ui_duplication_processing": "Dublēju sarunu...", "com_ui_duplication_success": "Saruna veiksmīgi dublēta", "com_ui_edit": "Rediģēt", "com_ui_edit_editing_image": "Attēla rediģēšana", @@ -799,7 +798,7 @@ "com_ui_fork_info_visible": "Šī opcija atzaro tikai redzamās ziņas; citiem vārdiem sakot, tiešo ceļu uz mērķa ziņām bez atzariem.", "com_ui_fork_more_details_about": "Skatiet papildu informāciju un detaļas par \"{{0}}\" atzarojuma variantu", "com_ui_fork_more_info_options": "Skatiet detalizētu visu atzarojuma opciju un to darbības skaidrojumu", - "com_ui_fork_processing": "Sarunas atzarošana...", + "com_ui_fork_processing": "Atzaroju sarunu...", "com_ui_fork_remember": "Atcerēties", "com_ui_fork_remember_checked": "Jūsu izvēle tiks atcerēta pēc lietošanas. To var jebkurā laikā mainīt iestatījumos.", "com_ui_fork_split_target": "Sāciet atzarošanu šeit", @@ -808,7 +807,7 @@ "com_ui_fork_visible": "Tikai redzamās ziņas", "com_ui_generate_backup": "Ģenerēt rezerves kodus", "com_ui_generate_qrcode": "Ģenerēt QR kodu", - "com_ui_generating": "Notiek ģenerēšana...", + "com_ui_generating": "Ģenerē...", "com_ui_generation_settings": "Ģenerēšanas iestatījumi", "com_ui_getting_started": "Darba sākšana", "com_ui_global_group": "kaut kam šeit ir jānotiek. bija tukšs", @@ -944,13 +943,13 @@ "com_ui_provider": "Pakalpojumu sniedzējs", "com_ui_quality": "Kvalitāte", "com_ui_read_aloud": "Lasīt skaļi", - "com_ui_redirecting_to_provider": "Pāradresācija uz {{0}}, lūdzu, uzgaidiet...", - "com_ui_reference_saved_memories": "Atsauces uz saglabātajām atmiņām", - "com_ui_reference_saved_memories_description": "Ļaujiet asistentam atsaukties uz jūsu saglabātajām atmiņām un izmantot tās, atbildot", + "com_ui_redirecting_to_provider": "Pārvirzu uz {{0}}, lūdzu, uzgaidiet...", + "com_ui_reference_saved_memories": "References uz saglabātajām atmiņām", + "com_ui_reference_saved_memories_description": "Ļaut asistentam atsaukties uz jūsu saglabātajām atmiņām un izmantot tās atbildot", "com_ui_refresh_link": "Atsvaidzināt saiti", "com_ui_regenerate": "Atjaunot", "com_ui_regenerate_backup": "Atjaunot rezerves kodus", - "com_ui_regenerating": "Atjaunošanās...", + "com_ui_regenerating": "Atjaunojas...", "com_ui_region": "Reģions", "com_ui_reinitialize": "Reinicializēt", "com_ui_rename": "Pārdēvēt", @@ -962,7 +961,7 @@ "com_ui_reset_zoom": "Atiestatīt tālummaiņu", "com_ui_result": "Rezultāts", "com_ui_revoke": "Atsaukt", - "com_ui_revoke_info": "Atsaukt visus lietotāja sniegtos kredenciāļu datus", + "com_ui_revoke_info": "Atcelt visus lietotāja sniegtos lietotāja datus", "com_ui_revoke_key_confirm": "Vai tiešām vēlaties atsaukt šo atslēgu?", "com_ui_revoke_key_endpoint": "Atsaukt atslēgu priekš {{0}}", "com_ui_revoke_keys": "Atsaukt atslēgas", @@ -1030,7 +1029,7 @@ "com_ui_temporary": "Pagaidu saruna", "com_ui_terms_and_conditions": "Noteikumi un nosacījumi", "com_ui_terms_of_service": "Pakalpojumu sniegšanas noteikumi", - "com_ui_thinking": "Domājot...", + "com_ui_thinking": "Domā...", "com_ui_thoughts": "Domas", "com_ui_token": "tokens", "com_ui_token_exchange_method": "Tokenu apmaiņas metode", @@ -1072,7 +1071,7 @@ "com_ui_used": "Lietots", "com_ui_value": "Vērtība", "com_ui_variables": "Mainīgie", - "com_ui_variables_info": "Mainīgo veidošanai tekstā izmantojiet dubultās iekavas, piemēram, `{{example variable}}`, lai vēlāk aizpildītu, izmantojot uzvedni.", + "com_ui_variables_info": "Mainīgo veidošanai tekstā izmantot dubultās iekavas, piemēram, `{{example variable}}`, lai vēlāk aizpildītu, izmantojot uzvedni.", "com_ui_verify": "Pārbaudīt", "com_ui_version_var": "Versija {{0}}", "com_ui_versions": "Versijas", diff --git a/client/src/locales/zh-Hans/translation.json b/client/src/locales/zh-Hans/translation.json index 1c8b7aeda..1cd0a8f21 100644 --- a/client/src/locales/zh-Hans/translation.json +++ b/client/src/locales/zh-Hans/translation.json @@ -554,7 +554,6 @@ "com_ui_agent_var": "{{0}} 智能体", "com_ui_agent_version": "版本", "com_ui_agent_version_active": "活动版本", - "com_ui_agent_version_duplicate": "检测到重复版本。这将创建与版本 {{versionIndex}} 完全相同的版本。", "com_ui_agent_version_empty": "无可用版本", "com_ui_agent_version_error": "获取版本时发生错误", "com_ui_agent_version_history": "版本历史", From 486fe34a2b2d277de79abc18aa465bd21349349f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:42:44 -0400 Subject: [PATCH 069/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#8924)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌍 i18n: Update translation.json with latest translations * Update translation.json --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Danny Avila --- client/src/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 740edaa42..c44af0f4d 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -860,7 +860,6 @@ "com_ui_mcp_update_var": "Update {{0}}", "com_ui_mcp_url": "MCP Server URL", "com_ui_medium": "Medium", - "com_ui_minimal": "Minimal", "com_ui_memories": "Memories", "com_ui_memories_allow_create": "Allow creating Memories", "com_ui_memories_allow_opt_out": "Allow users to opt out of Memories", @@ -882,6 +881,7 @@ "com_ui_memory_would_exceed": "Cannot save - would exceed limit by {{tokens}} tokens. Delete existing memories to make space.", "com_ui_mention": "Mention an endpoint, assistant, or preset to quickly switch to it", "com_ui_min_tags": "Cannot remove more values, a minimum of {{0}} are required.", + "com_ui_minimal": "Minimal", "com_ui_misc": "Misc.", "com_ui_model": "Model", "com_ui_model_parameters": "Model Parameters", From 7147bce3c3d001a2af4f2e5b53c131e6985b5331 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 7 Aug 2025 20:49:40 -0400 Subject: [PATCH 070/224] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20OpenAI=20Verb?= =?UTF-8?q?osity=20Parameter=20(#8929)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP: Verbosity OpenAI Parameter * 🔧 chore: remove unused import of extractEnvVariable from parsers.ts * ✨ feat: add comprehensive tests for getOpenAIConfig and enhance verbosity handling * fix: Handling for maxTokens in GPT-5+ models and add corresponding tests * feat: Implement GPT-5+ model handling in processMemory function --- api/server/controllers/agents/client.js | 6 + api/server/controllers/agents/client.test.js | 172 +++++++ client/src/locales/en/translation.json | 2 + .../api/src/agents/__tests__/memory.test.ts | 240 +++++++++- packages/api/src/agents/memory.ts | 18 +- packages/api/src/endpoints/openai/llm.spec.ts | 424 ++++++++++++++++++ packages/api/src/endpoints/openai/llm.ts | 90 +++- .../data-provider/src/parameterSettings.ts | 22 + packages/data-provider/src/parsers.ts | 1 - packages/data-provider/src/schemas.ts | 13 + packages/data-provider/src/types.ts | 1 + packages/data-schemas/src/schema/defaults.ts | 4 + packages/data-schemas/src/schema/preset.ts | 1 + packages/data-schemas/src/types/convo.ts | 1 + 14 files changed, 989 insertions(+), 6 deletions(-) create mode 100644 packages/api/src/endpoints/openai/llm.spec.ts diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index d8fda5159..7a6d0cce1 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -1129,6 +1129,12 @@ class AgentClient extends BaseClient { delete clientOptions.maxTokens; } + if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { + clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; + clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; + delete clientOptions.maxTokens; + } + clientOptions = Object.assign( Object.fromEntries( Object.entries(clientOptions).filter(([key]) => !omitTitleOptions.has(key)), diff --git a/api/server/controllers/agents/client.test.js b/api/server/controllers/agents/client.test.js index 92b97ad00..5079bcebd 100644 --- a/api/server/controllers/agents/client.test.js +++ b/api/server/controllers/agents/client.test.js @@ -728,6 +728,178 @@ describe('AgentClient - titleConvo', () => { }); }); + describe('getOptions method - GPT-5+ model handling', () => { + let mockReq; + let mockRes; + let mockAgent; + let mockOptions; + + beforeEach(() => { + jest.clearAllMocks(); + + mockAgent = { + id: 'agent-123', + endpoint: EModelEndpoint.openAI, + provider: EModelEndpoint.openAI, + model_parameters: { + model: 'gpt-5', + }, + }; + + mockReq = { + app: { + locals: {}, + }, + user: { + id: 'user-123', + }, + }; + + mockRes = {}; + + mockOptions = { + req: mockReq, + res: mockRes, + agent: mockAgent, + }; + + client = new AgentClient(mockOptions); + }); + + it('should move maxTokens to modelKwargs.max_completion_tokens for GPT-5 models', () => { + const clientOptions = { + model: 'gpt-5', + maxTokens: 2048, + temperature: 0.7, + }; + + // Simulate the getOptions logic that handles GPT-5+ models + if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { + clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; + clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; + delete clientOptions.maxTokens; + } + + expect(clientOptions.maxTokens).toBeUndefined(); + expect(clientOptions.modelKwargs).toBeDefined(); + expect(clientOptions.modelKwargs.max_completion_tokens).toBe(2048); + expect(clientOptions.temperature).toBe(0.7); // Other options should remain + }); + + it('should handle GPT-5+ models with existing modelKwargs', () => { + const clientOptions = { + model: 'gpt-6', + maxTokens: 1500, + temperature: 0.8, + modelKwargs: { + customParam: 'value', + }, + }; + + // Simulate the getOptions logic + if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { + clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; + clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; + delete clientOptions.maxTokens; + } + + expect(clientOptions.maxTokens).toBeUndefined(); + expect(clientOptions.modelKwargs).toEqual({ + customParam: 'value', + max_completion_tokens: 1500, + }); + }); + + it('should not modify maxTokens for non-GPT-5+ models', () => { + const clientOptions = { + model: 'gpt-4', + maxTokens: 2048, + temperature: 0.7, + }; + + // Simulate the getOptions logic + if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { + clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; + clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; + delete clientOptions.maxTokens; + } + + // Should not be modified since it's GPT-4 + expect(clientOptions.maxTokens).toBe(2048); + expect(clientOptions.modelKwargs).toBeUndefined(); + }); + + it('should handle various GPT-5+ model formats', () => { + const testCases = [ + { model: 'gpt-5', shouldTransform: true }, + { model: 'gpt-5-turbo', shouldTransform: true }, + { model: 'gpt-6', shouldTransform: true }, + { model: 'gpt-7-preview', shouldTransform: true }, + { model: 'gpt-8', shouldTransform: true }, + { model: 'gpt-9-mini', shouldTransform: true }, + { model: 'gpt-4', shouldTransform: false }, + { model: 'gpt-4o', shouldTransform: false }, + { model: 'gpt-3.5-turbo', shouldTransform: false }, + { model: 'claude-3', shouldTransform: false }, + ]; + + testCases.forEach(({ model, shouldTransform }) => { + const clientOptions = { + model, + maxTokens: 1000, + }; + + // Simulate the getOptions logic + if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { + clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; + clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; + delete clientOptions.maxTokens; + } + + if (shouldTransform) { + expect(clientOptions.maxTokens).toBeUndefined(); + expect(clientOptions.modelKwargs?.max_completion_tokens).toBe(1000); + } else { + expect(clientOptions.maxTokens).toBe(1000); + expect(clientOptions.modelKwargs).toBeUndefined(); + } + }); + }); + + it('should not transform if maxTokens is null or undefined', () => { + const testCases = [ + { model: 'gpt-5', maxTokens: null }, + { model: 'gpt-5', maxTokens: undefined }, + { model: 'gpt-6', maxTokens: 0 }, // Should transform even if 0 + ]; + + testCases.forEach(({ model, maxTokens }, index) => { + const clientOptions = { + model, + maxTokens, + temperature: 0.7, + }; + + // Simulate the getOptions logic + if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { + clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; + clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; + delete clientOptions.maxTokens; + } + + if (index < 2) { + // null or undefined cases + expect(clientOptions.maxTokens).toBe(maxTokens); + expect(clientOptions.modelKwargs).toBeUndefined(); + } else { + // 0 case - should transform + expect(clientOptions.maxTokens).toBeUndefined(); + expect(clientOptions.modelKwargs?.max_completion_tokens).toBe(0); + } + }); + }); + }); + describe('runMemory method', () => { let client; let mockReq; diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index c44af0f4d..023e703c9 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -231,6 +231,7 @@ "com_endpoint_openai_prompt_prefix_placeholder": "Set custom instructions to include in System Message. Default: none", "com_endpoint_openai_reasoning_effort": "Reasoning models only: constrains effort on reasoning. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. 'Minimal' produces very few reasoning tokens for fastest time-to-first-token, especially well-suited for coding and instruction following.", "com_endpoint_openai_reasoning_summary": "Responses API only: A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. Set to none,auto, concise, or detailed.", + "com_endpoint_openai_verbosity": "Constrains the verbosity of the model's response. Lower values will result in more concise responses, while higher values will result in more verbose responses. Currently supported values are low, medium, and high.", "com_endpoint_openai_resend": "Resend all previously attached images. Note: this can significantly increase token cost and you may experience errors with many image attachments.", "com_endpoint_openai_resend_files": "Resend all previously attached files. Note: this will increase token cost and you may experience errors with many attachments.", "com_endpoint_openai_stop": "Up to 4 sequences where the API will stop generating further tokens.", @@ -269,6 +270,7 @@ "com_endpoint_prompt_prefix_placeholder": "Set custom instructions or context. Ignored if empty.", "com_endpoint_reasoning_effort": "Reasoning Effort", "com_endpoint_reasoning_summary": "Reasoning Summary", + "com_endpoint_verbosity": "Verbosity", "com_endpoint_save_as_preset": "Save As Preset", "com_endpoint_search": "Search endpoint by name", "com_endpoint_search_endpoint_models": "Search {{0}} models...", diff --git a/packages/api/src/agents/__tests__/memory.test.ts b/packages/api/src/agents/__tests__/memory.test.ts index 21dda8b0e..7b6999279 100644 --- a/packages/api/src/agents/__tests__/memory.test.ts +++ b/packages/api/src/agents/__tests__/memory.test.ts @@ -1,5 +1,8 @@ -import { Tools, type MemoryArtifact } from 'librechat-data-provider'; -import { createMemoryTool } from '../memory'; +import { Response } from 'express'; +import { Providers } from '@librechat/agents'; +import { Tools } from 'librechat-data-provider'; +import type { MemoryArtifact } from 'librechat-data-provider'; +import { createMemoryTool, processMemory } from '../memory'; // Mock the logger jest.mock('winston', () => ({ @@ -25,6 +28,22 @@ jest.mock('~/utils', () => ({ }, })); +// Mock the Run module +jest.mock('@librechat/agents', () => ({ + ...jest.requireActual('@librechat/agents'), + Run: { + create: jest.fn(), + }, + Providers: { + OPENAI: 'openai', + ANTHROPIC: 'anthropic', + AZURE: 'azure', + }, + GraphEvents: { + TOOL_END: 'tool_end', + }, +})); + describe('createMemoryTool', () => { let mockSetMemory: jest.Mock; @@ -163,3 +182,220 @@ describe('createMemoryTool', () => { }); }); }); + +describe('processMemory - GPT-5+ handling', () => { + let mockSetMemory: jest.Mock; + let mockDeleteMemory: jest.Mock; + let mockRes: Partial; + + beforeEach(() => { + jest.clearAllMocks(); + mockSetMemory = jest.fn().mockResolvedValue({ ok: true }); + mockDeleteMemory = jest.fn().mockResolvedValue({ ok: true }); + mockRes = { + headersSent: false, + write: jest.fn(), + }; + + // Setup the Run.create mock + const { Run } = jest.requireMock('@librechat/agents'); + (Run.create as jest.Mock).mockResolvedValue({ + processStream: jest.fn().mockResolvedValue('Memory processed'), + }); + }); + + it('should remove temperature for GPT-5 models', async () => { + await processMemory({ + res: mockRes as Response, + userId: 'test-user', + setMemory: mockSetMemory, + deleteMemory: mockDeleteMemory, + messages: [], + memory: 'Test memory', + messageId: 'msg-123', + conversationId: 'conv-123', + instructions: 'Test instructions', + llmConfig: { + provider: Providers.OPENAI, + model: 'gpt-5', + temperature: 0.7, // This should be removed + maxTokens: 1000, // This should be moved to modelKwargs + }, + }); + + const { Run } = jest.requireMock('@librechat/agents'); + expect(Run.create).toHaveBeenCalledWith( + expect.objectContaining({ + graphConfig: expect.objectContaining({ + llmConfig: expect.objectContaining({ + model: 'gpt-5', + modelKwargs: { + max_completion_tokens: 1000, + }, + }), + }), + }), + ); + + // Verify temperature was removed + const callArgs = (Run.create as jest.Mock).mock.calls[0][0]; + expect(callArgs.graphConfig.llmConfig.temperature).toBeUndefined(); + expect(callArgs.graphConfig.llmConfig.maxTokens).toBeUndefined(); + }); + + it('should handle GPT-5+ models with existing modelKwargs', async () => { + await processMemory({ + res: mockRes as Response, + userId: 'test-user', + setMemory: mockSetMemory, + deleteMemory: mockDeleteMemory, + messages: [], + memory: 'Test memory', + messageId: 'msg-123', + conversationId: 'conv-123', + instructions: 'Test instructions', + llmConfig: { + provider: Providers.OPENAI, + model: 'gpt-6', + temperature: 0.8, + maxTokens: 2000, + modelKwargs: { + customParam: 'value', + }, + }, + }); + + const { Run } = jest.requireMock('@librechat/agents'); + expect(Run.create).toHaveBeenCalledWith( + expect.objectContaining({ + graphConfig: expect.objectContaining({ + llmConfig: expect.objectContaining({ + model: 'gpt-6', + modelKwargs: { + customParam: 'value', + max_completion_tokens: 2000, + }, + }), + }), + }), + ); + + const callArgs = (Run.create as jest.Mock).mock.calls[0][0]; + expect(callArgs.graphConfig.llmConfig.temperature).toBeUndefined(); + expect(callArgs.graphConfig.llmConfig.maxTokens).toBeUndefined(); + }); + + it('should not modify non-GPT-5+ models', async () => { + await processMemory({ + res: mockRes as Response, + userId: 'test-user', + setMemory: mockSetMemory, + deleteMemory: mockDeleteMemory, + messages: [], + memory: 'Test memory', + messageId: 'msg-123', + conversationId: 'conv-123', + instructions: 'Test instructions', + llmConfig: { + provider: Providers.OPENAI, + model: 'gpt-4', + temperature: 0.7, + maxTokens: 1000, + }, + }); + + const { Run } = jest.requireMock('@librechat/agents'); + expect(Run.create).toHaveBeenCalledWith( + expect.objectContaining({ + graphConfig: expect.objectContaining({ + llmConfig: expect.objectContaining({ + model: 'gpt-4', + temperature: 0.7, + maxTokens: 1000, + }), + }), + }), + ); + + // Verify nothing was moved to modelKwargs for GPT-4 + const callArgs = (Run.create as jest.Mock).mock.calls[0][0]; + expect(callArgs.graphConfig.llmConfig.modelKwargs).toBeUndefined(); + }); + + it('should handle various GPT-5+ model formats', async () => { + const testCases = [ + { model: 'gpt-5', shouldTransform: true }, + { model: 'gpt-5-turbo', shouldTransform: true }, + { model: 'gpt-7-preview', shouldTransform: true }, + { model: 'gpt-9', shouldTransform: true }, + { model: 'gpt-4o', shouldTransform: false }, + { model: 'gpt-3.5-turbo', shouldTransform: false }, + ]; + + for (const { model, shouldTransform } of testCases) { + jest.clearAllMocks(); + const { Run } = jest.requireMock('@librechat/agents'); + (Run.create as jest.Mock).mockResolvedValue({ + processStream: jest.fn().mockResolvedValue('Memory processed'), + }); + + await processMemory({ + res: mockRes as Response, + userId: 'test-user', + setMemory: mockSetMemory, + deleteMemory: mockDeleteMemory, + messages: [], + memory: 'Test memory', + messageId: 'msg-123', + conversationId: 'conv-123', + instructions: 'Test instructions', + llmConfig: { + provider: Providers.OPENAI, + model, + temperature: 0.5, + maxTokens: 1500, + }, + }); + + const callArgs = (Run.create as jest.Mock).mock.calls[0][0]; + const llmConfig = callArgs.graphConfig.llmConfig; + + if (shouldTransform) { + expect(llmConfig.temperature).toBeUndefined(); + expect(llmConfig.maxTokens).toBeUndefined(); + expect(llmConfig.modelKwargs?.max_completion_tokens).toBe(1500); + } else { + expect(llmConfig.temperature).toBe(0.5); + expect(llmConfig.maxTokens).toBe(1500); + expect(llmConfig.modelKwargs).toBeUndefined(); + } + } + }); + + it('should use default model (gpt-4.1-mini) without temperature removal when no llmConfig provided', async () => { + await processMemory({ + res: mockRes as Response, + userId: 'test-user', + setMemory: mockSetMemory, + deleteMemory: mockDeleteMemory, + messages: [], + memory: 'Test memory', + messageId: 'msg-123', + conversationId: 'conv-123', + instructions: 'Test instructions', + // No llmConfig provided + }); + + const { Run } = jest.requireMock('@librechat/agents'); + expect(Run.create).toHaveBeenCalledWith( + expect.objectContaining({ + graphConfig: expect.objectContaining({ + llmConfig: expect.objectContaining({ + model: 'gpt-4.1-mini', + temperature: 0.4, // Default temperature should remain + }), + }), + }), + ); + }); +}); diff --git a/packages/api/src/agents/memory.ts b/packages/api/src/agents/memory.ts index e0c217cd8..298ebc04f 100644 --- a/packages/api/src/agents/memory.ts +++ b/packages/api/src/agents/memory.ts @@ -5,8 +5,10 @@ import { Tools } from 'librechat-data-provider'; import { logger } from '@librechat/data-schemas'; import { Run, Providers, GraphEvents } from '@librechat/agents'; import type { + OpenAIClientOptions, StreamEventData, ToolEndCallback, + ClientOptions, EventHandler, ToolEndData, LLMConfig, @@ -332,7 +334,7 @@ ${memory ?? 'No existing memories'}`; disableStreaming: true, }; - const finalLLMConfig = { + const finalLLMConfig: ClientOptions = { ...defaultLLMConfig, ...llmConfig, /** @@ -342,6 +344,20 @@ ${memory ?? 'No existing memories'}`; disableStreaming: true, }; + // Handle GPT-5+ models + if ('model' in finalLLMConfig && /\bgpt-[5-9]\b/i.test(finalLLMConfig.model ?? '')) { + // Remove temperature for GPT-5+ models + delete finalLLMConfig.temperature; + + // Move maxTokens to modelKwargs for GPT-5+ models + if ('maxTokens' in finalLLMConfig && finalLLMConfig.maxTokens != null) { + const modelKwargs = (finalLLMConfig as OpenAIClientOptions).modelKwargs ?? {}; + modelKwargs.max_completion_tokens = finalLLMConfig.maxTokens; + delete finalLLMConfig.maxTokens; + (finalLLMConfig as OpenAIClientOptions).modelKwargs = modelKwargs; + } + } + const artifactPromises: Promise[] = []; const memoryCallback = createMemoryCallback({ res, artifactPromises }); const customHandlers = { diff --git a/packages/api/src/endpoints/openai/llm.spec.ts b/packages/api/src/endpoints/openai/llm.spec.ts new file mode 100644 index 000000000..79d9e0562 --- /dev/null +++ b/packages/api/src/endpoints/openai/llm.spec.ts @@ -0,0 +1,424 @@ +import { ReasoningEffort, ReasoningSummary, Verbosity } from 'librechat-data-provider'; +import type { RequestInit } from 'undici'; +import { getOpenAIConfig } from './llm'; + +describe('getOpenAIConfig', () => { + const mockApiKey = 'test-api-key'; + + it('should create basic config with default values', () => { + const result = getOpenAIConfig(mockApiKey); + + expect(result.llmConfig).toMatchObject({ + streaming: true, + model: '', + apiKey: mockApiKey, + }); + expect(result.configOptions).toEqual({}); + expect(result.tools).toEqual([]); + }); + + it('should apply model options', () => { + const modelOptions = { + model: 'gpt-5', + temperature: 0.7, + max_tokens: 1000, + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions }); + + expect(result.llmConfig).toMatchObject({ + model: 'gpt-5', + temperature: 0.7, + modelKwargs: { + max_completion_tokens: 1000, + }, + }); + expect((result.llmConfig as Record).max_tokens).toBeUndefined(); + expect((result.llmConfig as Record).maxTokens).toBeUndefined(); + }); + + it('should separate known and unknown params from addParams', () => { + const addParams = { + temperature: 0.5, // known param + topP: 0.9, // known param + customParam1: 'value1', // unknown param + customParam2: { nested: true }, // unknown param + maxTokens: 500, // known param + }; + + const result = getOpenAIConfig(mockApiKey, { addParams }); + + expect(result.llmConfig.temperature).toBe(0.5); + expect(result.llmConfig.topP).toBe(0.9); + expect(result.llmConfig.maxTokens).toBe(500); + expect(result.llmConfig.modelKwargs).toEqual({ + customParam1: 'value1', + customParam2: { nested: true }, + }); + }); + + it('should not add modelKwargs if all params are known', () => { + const addParams = { + temperature: 0.5, + topP: 0.9, + maxTokens: 500, + }; + + const result = getOpenAIConfig(mockApiKey, { addParams }); + + expect(result.llmConfig.modelKwargs).toBeUndefined(); + }); + + it('should handle empty addParams', () => { + const result = getOpenAIConfig(mockApiKey, { addParams: {} }); + + expect(result.llmConfig.modelKwargs).toBeUndefined(); + }); + + it('should handle reasoning params for useResponsesApi', () => { + const modelOptions = { + reasoning_effort: ReasoningEffort.high, + reasoning_summary: ReasoningSummary.detailed, + }; + + const result = getOpenAIConfig(mockApiKey, { + modelOptions: { ...modelOptions, useResponsesApi: true }, + }); + + expect(result.llmConfig.reasoning).toEqual({ + effort: ReasoningEffort.high, + summary: ReasoningSummary.detailed, + }); + expect((result.llmConfig as Record).reasoning_effort).toBeUndefined(); + expect((result.llmConfig as Record).reasoning_summary).toBeUndefined(); + }); + + it('should handle reasoning params without useResponsesApi', () => { + const modelOptions = { + reasoning_effort: ReasoningEffort.high, + reasoning_summary: ReasoningSummary.detailed, + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions }); + + expect((result.llmConfig as Record).reasoning_effort).toBe( + ReasoningEffort.high, + ); + expect(result.llmConfig.reasoning).toBeUndefined(); + }); + + it('should handle OpenRouter configuration', () => { + const reverseProxyUrl = 'https://openrouter.ai/api/v1'; + + const result = getOpenAIConfig(mockApiKey, { reverseProxyUrl }); + + expect(result.configOptions?.baseURL).toBe(reverseProxyUrl); + expect(result.configOptions?.defaultHeaders).toMatchObject({ + 'HTTP-Referer': 'https://librechat.ai', + 'X-Title': 'LibreChat', + }); + expect(result.llmConfig.include_reasoning).toBe(true); + expect(result.provider).toBe('openrouter'); + }); + + it('should handle Azure configuration', () => { + const azure = { + azureOpenAIApiInstanceName: 'test-instance', + azureOpenAIApiDeploymentName: 'test-deployment', + azureOpenAIApiVersion: '2023-05-15', + azureOpenAIApiKey: 'azure-key', + }; + + const result = getOpenAIConfig(mockApiKey, { azure }); + + expect(result.llmConfig).toMatchObject({ + ...azure, + model: 'test-deployment', + }); + }); + + it('should handle web search model option', () => { + const modelOptions = { + model: 'gpt-5', + web_search: true, + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions }); + + expect(result.llmConfig.useResponsesApi).toBe(true); + expect(result.tools).toEqual([{ type: 'web_search_preview' }]); + }); + + it('should drop params for search models', () => { + const modelOptions = { + model: 'gpt-4o-search', + temperature: 0.7, + frequency_penalty: 0.5, + max_tokens: 1000, + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions }); + + expect(result.llmConfig.temperature).toBeUndefined(); + expect((result.llmConfig as Record).frequency_penalty).toBeUndefined(); + expect(result.llmConfig.maxTokens).toBe(1000); // max_tokens is allowed + }); + + it('should handle custom dropParams', () => { + const modelOptions = { + temperature: 0.7, + topP: 0.9, + customParam: 'value', + }; + + const result = getOpenAIConfig(mockApiKey, { + modelOptions, + dropParams: ['temperature', 'customParam'], + }); + + expect(result.llmConfig.temperature).toBeUndefined(); + expect(result.llmConfig.topP).toBe(0.9); + expect((result.llmConfig as Record).customParam).toBeUndefined(); + }); + + it('should handle proxy configuration', () => { + const proxy = 'http://proxy.example.com:8080'; + + const result = getOpenAIConfig(mockApiKey, { proxy }); + + expect(result.configOptions?.fetchOptions).toBeDefined(); + expect((result.configOptions?.fetchOptions as RequestInit).dispatcher).toBeDefined(); + }); + + it('should handle headers and defaultQuery', () => { + const headers = { 'X-Custom-Header': 'value' }; + const defaultQuery = { customParam: 'value' }; + + const result = getOpenAIConfig(mockApiKey, { + reverseProxyUrl: 'https://api.example.com', + headers, + defaultQuery, + }); + + expect(result.configOptions?.baseURL).toBe('https://api.example.com'); + expect(result.configOptions?.defaultHeaders).toEqual(headers); + expect(result.configOptions?.defaultQuery).toEqual(defaultQuery); + }); + + it('should handle verbosity parameter in modelKwargs', () => { + const modelOptions = { + model: 'gpt-5', + temperature: 0.7, + verbosity: Verbosity.high, + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions }); + + expect(result.llmConfig).toMatchObject({ + model: 'gpt-5', + temperature: 0.7, + }); + expect(result.llmConfig.modelKwargs).toEqual({ + verbosity: Verbosity.high, + }); + }); + + it('should allow addParams to override verbosity in modelKwargs', () => { + const modelOptions = { + model: 'gpt-5', + verbosity: Verbosity.low, + }; + + const addParams = { + temperature: 0.8, + verbosity: Verbosity.high, // This should override the one from modelOptions + customParam: 'value', + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions, addParams }); + + expect(result.llmConfig.temperature).toBe(0.8); + expect(result.llmConfig.modelKwargs).toEqual({ + verbosity: Verbosity.high, // Should be overridden by addParams + customParam: 'value', + }); + }); + + it('should not create modelKwargs if verbosity is empty or null', () => { + const testCases = [ + { verbosity: null }, + { verbosity: Verbosity.none }, + { verbosity: undefined }, + ]; + + testCases.forEach((modelOptions) => { + const result = getOpenAIConfig(mockApiKey, { modelOptions }); + expect(result.llmConfig.modelKwargs).toBeUndefined(); + }); + }); + + it('should nest verbosity under text when useResponsesApi is enabled', () => { + const modelOptions = { + model: 'gpt-5', + temperature: 0.7, + verbosity: Verbosity.low, + useResponsesApi: true, + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions }); + + expect(result.llmConfig).toMatchObject({ + model: 'gpt-5', + temperature: 0.7, + useResponsesApi: true, + }); + expect(result.llmConfig.modelKwargs).toEqual({ + text: { + verbosity: Verbosity.low, + }, + }); + }); + + it('should handle verbosity correctly when addParams overrides with useResponsesApi', () => { + const modelOptions = { + model: 'gpt-5', + verbosity: Verbosity.low, + useResponsesApi: true, + }; + + const addParams = { + verbosity: Verbosity.high, + customParam: 'value', + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions, addParams }); + + expect(result.llmConfig.modelKwargs).toEqual({ + text: { + verbosity: Verbosity.high, // Should be overridden by addParams + }, + customParam: 'value', + }); + }); + + it('should move maxTokens to modelKwargs.max_completion_tokens for GPT-5+ models', () => { + const modelOptions = { + model: 'gpt-5', + temperature: 0.7, + max_tokens: 2048, + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions }); + + expect(result.llmConfig).toMatchObject({ + model: 'gpt-5', + temperature: 0.7, + }); + expect(result.llmConfig.maxTokens).toBeUndefined(); + expect(result.llmConfig.modelKwargs).toEqual({ + max_completion_tokens: 2048, + }); + }); + + it('should handle GPT-5+ models with existing modelKwargs', () => { + const modelOptions = { + model: 'gpt-6', + max_tokens: 1000, + verbosity: Verbosity.low, + }; + + const addParams = { + customParam: 'value', + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions, addParams }); + + expect(result.llmConfig.maxTokens).toBeUndefined(); + expect(result.llmConfig.modelKwargs).toEqual({ + verbosity: Verbosity.low, + customParam: 'value', + max_completion_tokens: 1000, + }); + }); + + it('should not move maxTokens for non-GPT-5+ models', () => { + const modelOptions = { + model: 'gpt-4', + temperature: 0.7, + max_tokens: 2048, + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions }); + + expect(result.llmConfig).toMatchObject({ + model: 'gpt-4', + temperature: 0.7, + maxTokens: 2048, + }); + expect(result.llmConfig.modelKwargs).toBeUndefined(); + }); + + it('should handle GPT-5+ models with verbosity and useResponsesApi', () => { + const modelOptions = { + model: 'gpt-5', + max_tokens: 1500, + verbosity: Verbosity.medium, + useResponsesApi: true, + }; + + const result = getOpenAIConfig(mockApiKey, { modelOptions }); + + expect(result.llmConfig.maxTokens).toBeUndefined(); + expect(result.llmConfig.modelKwargs).toEqual({ + text: { + verbosity: Verbosity.medium, + }, + max_completion_tokens: 1500, + }); + }); + + it('should handle complex addParams with mixed known and unknown params', () => { + const addParams = { + // Known params + model: 'gpt-4-turbo', + temperature: 0.8, + topP: 0.95, + frequencyPenalty: 0.2, + presencePenalty: 0.1, + maxTokens: 2048, + stop: ['\\n\\n', 'END'], + stream: false, + // Unknown params + custom_instruction: 'Be concise', + response_style: 'formal', + domain_specific: { + medical: true, + terminology: 'advanced', + }, + }; + + const result = getOpenAIConfig(mockApiKey, { addParams }); + + // Check known params are in llmConfig + expect(result.llmConfig).toMatchObject({ + model: 'gpt-4-turbo', + temperature: 0.8, + topP: 0.95, + frequencyPenalty: 0.2, + presencePenalty: 0.1, + maxTokens: 2048, + stop: ['\\n\\n', 'END'], + stream: false, + }); + + // Check unknown params are in modelKwargs + expect(result.llmConfig.modelKwargs).toEqual({ + custom_instruction: 'Be concise', + response_style: 'formal', + domain_specific: { + medical: true, + terminology: 'advanced', + }, + }); + }); +}); diff --git a/packages/api/src/endpoints/openai/llm.ts b/packages/api/src/endpoints/openai/llm.ts index 5f3b5b827..e5c1b49c5 100644 --- a/packages/api/src/endpoints/openai/llm.ts +++ b/packages/api/src/endpoints/openai/llm.ts @@ -8,6 +8,62 @@ import type * as t from '~/types'; import { sanitizeModelName, constructAzureURL } from '~/utils/azure'; import { isEnabled } from '~/utils/common'; +export const knownOpenAIParams = new Set([ + // Constructor/Instance Parameters + 'model', + 'modelName', + 'temperature', + 'topP', + 'frequencyPenalty', + 'presencePenalty', + 'n', + 'logitBias', + 'stop', + 'stopSequences', + 'user', + 'timeout', + 'stream', + 'maxTokens', + 'maxCompletionTokens', + 'logprobs', + 'topLogprobs', + 'apiKey', + 'organization', + 'audio', + 'modalities', + 'reasoning', + 'zdrEnabled', + 'service_tier', + 'supportsStrictToolCalling', + 'useResponsesApi', + 'configuration', + // Call-time Options + 'tools', + 'tool_choice', + 'functions', + 'function_call', + 'response_format', + 'seed', + 'stream_options', + 'parallel_tool_calls', + 'strict', + 'prediction', + 'promptIndex', + // Responses API specific + 'text', + 'truncation', + 'include', + 'previous_response_id', + // LangChain specific + '__includeRawResponse', + 'maxConcurrency', + 'maxRetries', + 'verbose', + 'streaming', + 'streamUsage', + 'disableStreaming', +]); + function hasReasoningParams({ reasoning_effort, reasoning_summary, @@ -44,7 +100,7 @@ export function getOpenAIConfig( addParams, dropParams, } = options; - const { reasoning_effort, reasoning_summary, ...modelOptions } = _modelOptions; + const { reasoning_effort, reasoning_summary, verbosity, ...modelOptions } = _modelOptions; const llmConfig: Partial & Partial & Partial = Object.assign( @@ -55,8 +111,23 @@ export function getOpenAIConfig( modelOptions, ); + const modelKwargs: Record = {}; + let hasModelKwargs = false; + + if (verbosity != null && verbosity !== '') { + modelKwargs.verbosity = verbosity; + hasModelKwargs = true; + } + if (addParams && typeof addParams === 'object') { - Object.assign(llmConfig, addParams); + for (const [key, value] of Object.entries(addParams)) { + if (knownOpenAIParams.has(key)) { + (llmConfig as Record)[key] = value; + } else { + hasModelKwargs = true; + modelKwargs[key] = value; + } + } } let useOpenRouter = false; @@ -223,6 +294,21 @@ export function getOpenAIConfig( }); } + if (modelKwargs.verbosity && llmConfig.useResponsesApi === true) { + modelKwargs.text = { verbosity: modelKwargs.verbosity }; + delete modelKwargs.verbosity; + } + + if (llmConfig.model && /\bgpt-[5-9]\b/i.test(llmConfig.model) && llmConfig.maxTokens != null) { + modelKwargs.max_completion_tokens = llmConfig.maxTokens; + delete llmConfig.maxTokens; + hasModelKwargs = true; + } + + if (hasModelKwargs) { + llmConfig.modelKwargs = modelKwargs; + } + const result: t.LLMConfigResult = { llmConfig, configOptions, diff --git a/packages/data-provider/src/parameterSettings.ts b/packages/data-provider/src/parameterSettings.ts index bebfa8377..45604d885 100644 --- a/packages/data-provider/src/parameterSettings.ts +++ b/packages/data-provider/src/parameterSettings.ts @@ -1,4 +1,5 @@ import { + Verbosity, ImageDetail, EModelEndpoint, openAISettings, @@ -286,6 +287,25 @@ const openAIParams: Record = { optionType: 'model', columnSpan: 4, }, + verbosity: { + key: 'verbosity', + label: 'com_endpoint_verbosity', + labelCode: true, + description: 'com_endpoint_openai_verbosity', + descriptionCode: true, + type: 'enum', + default: Verbosity.none, + component: 'slider', + options: [Verbosity.none, Verbosity.low, Verbosity.medium, Verbosity.high], + enumMappings: { + [Verbosity.none]: 'com_ui_none', + [Verbosity.low]: 'com_ui_low', + [Verbosity.medium]: 'com_ui_medium', + [Verbosity.high]: 'com_ui_high', + }, + optionType: 'model', + columnSpan: 4, + }, disableStreaming: { key: 'disableStreaming', label: 'com_endpoint_disable_streaming_label', @@ -641,6 +661,7 @@ const openAI: SettingsConfiguration = [ openAIParams.reasoning_effort, openAIParams.useResponsesApi, openAIParams.reasoning_summary, + openAIParams.verbosity, openAIParams.disableStreaming, ]; @@ -662,6 +683,7 @@ const openAICol2: SettingsConfiguration = [ baseDefinitions.imageDetail, openAIParams.reasoning_effort, openAIParams.reasoning_summary, + openAIParams.verbosity, openAIParams.useResponsesApi, openAIParams.web_search, openAIParams.disableStreaming, diff --git a/packages/data-provider/src/parsers.ts b/packages/data-provider/src/parsers.ts index 42379ad02..7d4016449 100644 --- a/packages/data-provider/src/parsers.ts +++ b/packages/data-provider/src/parsers.ts @@ -18,7 +18,6 @@ import { compactAssistantSchema, } from './schemas'; import { bedrockInputSchema } from './bedrock'; -import { extractEnvVariable } from './utils'; import { alternateName } from './config'; type EndpointSchema = diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 1b18ea24b..71eae3824 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -126,6 +126,13 @@ export enum ReasoningSummary { detailed = 'detailed', } +export enum Verbosity { + none = '', + low = 'low', + medium = 'medium', + high = 'high', +} + export const imageDetailNumeric = { [ImageDetail.low]: 0, [ImageDetail.auto]: 1, @@ -141,6 +148,7 @@ export const imageDetailValue = { export const eImageDetailSchema = z.nativeEnum(ImageDetail); export const eReasoningEffortSchema = z.nativeEnum(ReasoningEffort); export const eReasoningSummarySchema = z.nativeEnum(ReasoningSummary); +export const eVerbositySchema = z.nativeEnum(Verbosity); export const defaultAssistantFormValues = { assistant: '', @@ -636,6 +644,8 @@ export const tConversationSchema = z.object({ /* OpenAI: Reasoning models only */ reasoning_effort: eReasoningEffortSchema.optional().nullable(), reasoning_summary: eReasoningSummarySchema.optional().nullable(), + /* OpenAI: Verbosity control */ + verbosity: eVerbositySchema.optional().nullable(), /* OpenAI: use Responses API */ useResponsesApi: z.boolean().optional(), /* OpenAI Responses API / Anthropic API / Google API */ @@ -743,6 +753,8 @@ export const tQueryParamsSchema = tConversationSchema /** @endpoints openAI, custom, azureOpenAI */ reasoning_summary: true, /** @endpoints openAI, custom, azureOpenAI */ + verbosity: true, + /** @endpoints openAI, custom, azureOpenAI */ useResponsesApi: true, /** @endpoints openAI, anthropic, google */ web_search: true, @@ -1078,6 +1090,7 @@ export const openAIBaseSchema = tConversationSchema.pick({ max_tokens: true, reasoning_effort: true, reasoning_summary: true, + verbosity: true, useResponsesApi: true, web_search: true, disableStreaming: true, diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 5799ca50e..b26e93ec8 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -40,6 +40,7 @@ export type TEndpointOption = Pick< | 'resendFiles' | 'imageDetail' | 'reasoning_effort' + | 'verbosity' | 'instructions' | 'additional_instructions' | 'append_current_datetime' diff --git a/packages/data-schemas/src/schema/defaults.ts b/packages/data-schemas/src/schema/defaults.ts index da986d729..b432f283f 100644 --- a/packages/data-schemas/src/schema/defaults.ts +++ b/packages/data-schemas/src/schema/defaults.ts @@ -148,4 +148,8 @@ export const conversationPreset = { reasoning_summary: { type: String, }, + /** Verbosity control */ + verbosity: { + type: String, + }, }; diff --git a/packages/data-schemas/src/schema/preset.ts b/packages/data-schemas/src/schema/preset.ts index c6ee8e612..d95e7d132 100644 --- a/packages/data-schemas/src/schema/preset.ts +++ b/packages/data-schemas/src/schema/preset.ts @@ -47,6 +47,7 @@ export interface IPreset extends Document { max_tokens?: number; reasoning_effort?: string; reasoning_summary?: string; + verbosity?: string; useResponsesApi?: boolean; web_search?: boolean; disableStreaming?: boolean; diff --git a/packages/data-schemas/src/types/convo.ts b/packages/data-schemas/src/types/convo.ts index cf2e1d2ef..fb8392c0f 100644 --- a/packages/data-schemas/src/types/convo.ts +++ b/packages/data-schemas/src/types/convo.ts @@ -46,6 +46,7 @@ export interface IConversation extends Document { max_tokens?: number; reasoning_effort?: string; reasoning_summary?: string; + verbosity?: string; useResponsesApi?: boolean; web_search?: boolean; disableStreaming?: boolean; From 0939250f07ec37f95b725d8e7a8b8026505aee2f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 Aug 2025 11:15:29 -0400 Subject: [PATCH 071/224] =?UTF-8?q?=F0=9F=9B=A3=EF=B8=8F=20fix:=20Remove?= =?UTF-8?q?=20Title=20Tokens=20Limit=20for=20GPT-5=20Models=20(#8948)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🛣️ fix: Remove Title Tokens Limit for GPT-5 Models * 🛣️ fix: Remove max_completion_tokens from modelKwargs when maxTokens is disabled * chore: Add test-image* to .gitignore for CI/CD data --- .gitignore | 3 +++ api/server/controllers/agents/client.js | 15 ++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 461eef9d2..38b9bbc9e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ pids *.seed .git +# CI/CD data +test-image* + # Directory for instrumented libs generated by jscoverage/JSCover lib-cov diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 7a6d0cce1..4f4b84d53 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -1122,17 +1122,14 @@ class AgentClient extends BaseClient { clientOptions.configuration = options.configOptions; } - // Ensure maxTokens is set for non-o1 models - if (!/\b(o\d)\b/i.test(clientOptions.model) && !clientOptions.maxTokens) { + const shouldRemoveMaxTokens = /\b(o\d|gpt-[5-9])\b/i.test(clientOptions.model); + if (shouldRemoveMaxTokens && clientOptions.maxTokens != null) { + delete clientOptions.maxTokens; + } else if (!shouldRemoveMaxTokens && !clientOptions.maxTokens) { clientOptions.maxTokens = 75; - } else if (/\b(o\d)\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { - delete clientOptions.maxTokens; } - - if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) { - clientOptions.modelKwargs = clientOptions.modelKwargs ?? {}; - clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens; - delete clientOptions.maxTokens; + if (shouldRemoveMaxTokens && clientOptions?.modelKwargs?.max_completion_tokens != null) { + delete clientOptions.modelKwargs.max_completion_tokens; } clientOptions = Object.assign( From 01a95229f2d3cab6b5e016ddc2b092fcb2d1f493 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 Aug 2025 12:19:36 -0400 Subject: [PATCH 072/224] =?UTF-8?q?=F0=9F=93=A6=20chore:=20Bump=20`@librec?= =?UTF-8?q?hat/agents`=20to=20v2.4.73=20(#8949)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/package.json | 2 +- package-lock.json | 10 +++++----- packages/api/package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/package.json b/api/package.json index 8b1e6459e..7563e3e91 100644 --- a/api/package.json +++ b/api/package.json @@ -49,7 +49,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.72", + "@librechat/agents": "^2.4.73", "@librechat/api": "*", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", diff --git a/package-lock.json b/package-lock.json index dede8c7c8..ac8abd99b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.72", + "@librechat/agents": "^2.4.73", "@librechat/api": "*", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", @@ -21527,9 +21527,9 @@ } }, "node_modules/@librechat/agents": { - "version": "2.4.72", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.72.tgz", - "integrity": "sha512-4AaTN3JcPa0hdIsMlGDOm6nT1YkFByo/P9i2V9ji1nlrgcamL2PTMrUPcLB3QFmnNupHFxb62/3t3GaviZ94Bw==", + "version": "2.4.73", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.73.tgz", + "integrity": "sha512-LRVKN9WIgVXJC+KFAZIRiOXOOtatDHKS5w/nFzf19kEVrR7TOhBiKhLH11gpvtE4heBLLTag5M1keMy4f1s75Q==", "license": "MIT", "dependencies": { "@langchain/anthropic": "^0.3.26", @@ -51378,7 +51378,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.72", + "@librechat/agents": "^2.4.73", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.8.2", diff --git a/packages/api/package.json b/packages/api/package.json index f33854d46..2146ce62e 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -70,7 +70,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.72", + "@librechat/agents": "^2.4.73", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.8.2", From e7d6100fe481acc10f36575ba00b8e4b4742c469 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:21:27 -0400 Subject: [PATCH 073/224] =?UTF-8?q?=F0=9F=8C=8D=20i18n:=20Update=20transla?= =?UTF-8?q?tion.json=20with=20latest=20translations=20(#8934)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- client/src/locales/en/translation.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 023e703c9..3c847ade3 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -231,7 +231,6 @@ "com_endpoint_openai_prompt_prefix_placeholder": "Set custom instructions to include in System Message. Default: none", "com_endpoint_openai_reasoning_effort": "Reasoning models only: constrains effort on reasoning. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. 'Minimal' produces very few reasoning tokens for fastest time-to-first-token, especially well-suited for coding and instruction following.", "com_endpoint_openai_reasoning_summary": "Responses API only: A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. Set to none,auto, concise, or detailed.", - "com_endpoint_openai_verbosity": "Constrains the verbosity of the model's response. Lower values will result in more concise responses, while higher values will result in more verbose responses. Currently supported values are low, medium, and high.", "com_endpoint_openai_resend": "Resend all previously attached images. Note: this can significantly increase token cost and you may experience errors with many image attachments.", "com_endpoint_openai_resend_files": "Resend all previously attached files. Note: this will increase token cost and you may experience errors with many attachments.", "com_endpoint_openai_stop": "Up to 4 sequences where the API will stop generating further tokens.", @@ -239,6 +238,7 @@ "com_endpoint_openai_topp": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We recommend altering this or temperature but not both.", "com_endpoint_openai_use_responses_api": "Use the Responses API instead of Chat Completions, which includes extended features from OpenAI. Required for o1-pro, o3-pro, and to enable reasoning summaries.", "com_endpoint_openai_use_web_search": "Enable web search functionality using OpenAI's built-in search capabilities. This allows the model to search the web for up-to-date information and provide more accurate, current responses.", + "com_endpoint_openai_verbosity": "Constrains the verbosity of the model's response. Lower values will result in more concise responses, while higher values will result in more verbose responses. Currently supported values are low, medium, and high.", "com_endpoint_output": "Output", "com_endpoint_plug_image_detail": "Image Detail", "com_endpoint_plug_resend_files": "Resend Files", @@ -270,7 +270,6 @@ "com_endpoint_prompt_prefix_placeholder": "Set custom instructions or context. Ignored if empty.", "com_endpoint_reasoning_effort": "Reasoning Effort", "com_endpoint_reasoning_summary": "Reasoning Summary", - "com_endpoint_verbosity": "Verbosity", "com_endpoint_save_as_preset": "Save As Preset", "com_endpoint_search": "Search endpoint by name", "com_endpoint_search_endpoint_models": "Search {{0}} models...", @@ -288,6 +287,7 @@ "com_endpoint_use_active_assistant": "Use Active Assistant", "com_endpoint_use_responses_api": "Use Responses API", "com_endpoint_use_search_grounding": "Grounding with Google Search", + "com_endpoint_verbosity": "Verbosity", "com_error_expired_user_key": "Provided key for {{0}} expired at {{1}}. Please provide a new key and try again.", "com_error_files_dupe": "Duplicate file detected.", "com_error_files_empty": "Empty files are not allowed.", From 5d0bc951931a09ddf47e7603de821d0b82ebf651 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 Aug 2025 15:49:58 -0400 Subject: [PATCH 074/224] =?UTF-8?q?=F0=9F=A7=AA=20fix:=20Editor=20Styling,?= =?UTF-8?q?=20Incomplete=20Artifact=20Editing,=20Optimize=20Artifact=20Con?= =?UTF-8?q?text=20(#8953)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: optimize artifacts context for improved performance * fix: layout classes for artifacts editor * chore: linting * fix: enhance artifact mutation handling in CodeEditor to prevent infinite retries * fix: handle incomplete artifacts in replaceArtifactContent and add regression tests --- api/server/services/Artifacts/update.js | 25 ++- api/server/services/Artifacts/update.spec.js | 182 +++++++++++++++++- client/src/Providers/ArtifactsContext.tsx | 46 +++++ client/src/Providers/index.ts | 1 + .../Artifacts/ArtifactCodeEditor.tsx | 18 +- client/src/components/Chat/Presentation.tsx | 10 +- client/src/hooks/Artifacts/useArtifacts.ts | 38 ++-- client/src/mobile.css | 36 ++-- 8 files changed, 311 insertions(+), 45 deletions(-) create mode 100644 client/src/Providers/ArtifactsContext.tsx diff --git a/api/server/services/Artifacts/update.js b/api/server/services/Artifacts/update.js index 69cb4bb5c..d068593f8 100644 --- a/api/server/services/Artifacts/update.js +++ b/api/server/services/Artifacts/update.js @@ -60,7 +60,14 @@ const replaceArtifactContent = (originalText, artifact, original, updated) => { // Find boundaries between ARTIFACT_START and ARTIFACT_END const contentStart = artifactContent.indexOf('\n', artifactContent.indexOf(ARTIFACT_START)) + 1; - const contentEnd = artifactContent.lastIndexOf(ARTIFACT_END); + let contentEnd = artifactContent.lastIndexOf(ARTIFACT_END); + + // Special case: if contentEnd is 0, it means the only ::: found is at the start of :::artifact + // This indicates an incomplete artifact (no closing :::) + // We need to check that it's exactly at position 0 (the beginning of artifactContent) + if (contentEnd === 0 && artifactContent.indexOf(ARTIFACT_START) === 0) { + contentEnd = artifactContent.length; + } if (contentStart === -1 || contentEnd === -1) { return null; @@ -72,12 +79,20 @@ const replaceArtifactContent = (originalText, artifact, original, updated) => { // Determine where to look for the original content let searchStart, searchEnd; - if (codeBlockStart !== -1 && codeBlockEnd !== -1) { - // If code blocks exist, search between them + if (codeBlockStart !== -1) { + // Code block starts searchStart = codeBlockStart + 4; // after ```\n - searchEnd = codeBlockEnd; + + if (codeBlockEnd !== -1 && codeBlockEnd > codeBlockStart) { + // Code block has proper ending + searchEnd = codeBlockEnd; + } else { + // No closing backticks found or they're before the opening (shouldn't happen) + // This might be an incomplete artifact - search to contentEnd + searchEnd = contentEnd; + } } else { - // Otherwise search in the whole artifact content + // No code blocks at all searchStart = contentStart; searchEnd = contentEnd; } diff --git a/api/server/services/Artifacts/update.spec.js b/api/server/services/Artifacts/update.spec.js index 2f5b9d7bf..2a3e0bbe3 100644 --- a/api/server/services/Artifacts/update.spec.js +++ b/api/server/services/Artifacts/update.spec.js @@ -89,9 +89,9 @@ describe('replaceArtifactContent', () => { }; test('should replace content within artifact boundaries', () => { - const original = 'console.log(\'hello\')'; + const original = "console.log('hello')"; const artifact = createTestArtifact(original); - const updated = 'console.log(\'updated\')'; + const updated = "console.log('updated')"; const result = replaceArtifactContent(artifact.text, artifact, original, updated); expect(result).toContain(updated); @@ -317,4 +317,182 @@ console.log(greeting);`; expect(result).not.toContain('\n\n```'); expect(result).not.toContain('```\n\n'); }); + + describe('incomplete artifacts', () => { + test('should handle incomplete artifacts (missing closing ::: and ```)', () => { + const original = ` + + + + +Pomodoro + +