Compare commits

...

44 Commits

Author SHA1 Message Date
Ruben Talstra
f1a69e8b6b Merge branch 'main' into refactor/openid-strategy 2025-05-14 21:14:39 +02:00
github-actions[bot]
5b402a755e 🌍 i18n: Update translation.json with latest translations (#7375)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-14 13:09:52 -04:00
Ruben Talstra
b0405be9ea 🌍 i18n: Add Danish and Czech and Catalan localization support (#7373)
* 🌍 i18n: Add Danish and Czech localization support

* 🌍 i18n: Correct Czech language code from 'sc-CZ' to 'cs-CZ'

* 🌍 i18n: Add Catalan localization support
2025-05-14 13:08:06 -04:00
github-actions[bot]
3f4dd08589 📜 docs: Unreleased Changelog (#7321)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-14 13:07:21 -04:00
Ruben Talstra
7faff2c75f Merge branch 'main' into refactor/openid-strategy 2025-05-14 18:46:45 +02:00
Danny Avila
d5b399550e 📦 chore: Update API Package Dependencies (#7359)
* chore: temporarily remove @librechat/agents

* chore: bump @langchain/google-genai to v0.2.8

* chore: bump @langchain/google-vertexai to v0.2.8

* chore: bump @langchain/core to v0.3.55

* chore: bump @librechat/agents to v2.4.316

* chore: bump @librechat/agents to v2.4.317

* chore: update title for Unreleased Changelog PR to include documentation emoji

* chore: add workflow_dispatch trigger and update Pull Request title for changelog
2025-05-13 15:31:06 -04:00
Danny Avila
a5ff8253a4 🎏 feat: Add MCP support for Streamable HTTP Transport [2/2] (#7353)
- fixes type/packages issues not resolved in #7353
2025-05-13 13:26:37 -04:00
Ben Verhees
0b44142383 🎏 feat: Add MCP support for Streamable HTTP Transport (#7353) 2025-05-13 13:14:15 -04:00
matt burnett
502617db24 🔄 fix: update navigation logic in useFocusChatEffect to ensure correct search parameters are used (#7340) 2025-05-13 08:24:40 -04:00
Danny Avila
f2f285ca1e 🔑 fix: use apiKey instead of openAIApiKey in OpenAI-like Config (#7337) 2025-05-12 14:35:14 -04:00
Marco Beretta
6dd1b39886 💬 fix: update aria-label for accessibility in ConvoLink (#7320) 2025-05-12 08:12:51 -04:00
github-actions[bot]
5a43f87584 📜 docs: CHANGELOG for release v0.7.8 (#7290)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-12 08:10:58 -04:00
matt burnett
4af72aac9b feat: implement search parameter updates (#7151)
* feat: implement search parameter updates

* Update url params when values change

reset params on new chat

move logic to families.ts

revert unchanged files

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2025-05-09 13:03:33 -04:00
Danny Avila
f7777a2723 v0.7.8 (#7287)
*  v0.7.8

* chore: bump data-provider to v0.7.82

* chore: update CONFIG_VERSION to 1.2.5

* chore: bump librechat-mcp version to 1.2.2

* chore: bump @librechat/data-schemas version to 0.0.7
2025-05-08 13:28:40 -04:00
github-actions[bot]
e5b234bc72 📜 docs: Unreleased Changelog (#7214)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-08 13:09:30 -04:00
Josh Nichols
4f2ed46450 🐋 feat: Add python to Dockerfile for increased MCP compatibility (#7270)
Without this, it's not possible to run any MCPs that use python, only node.

So, add these to enable using things that use `uvx` similar to what
the documentation already talks about for `npx`.
2025-05-08 12:32:12 -04:00
Danny Avila
66093b1eb3 💬 refactor: MCP Chat Visibility Option, Google Rates, Remove OpenAPI Plugins (#7286)
* fix: Update Gemini 2.5 Pro Preview Model Name in Token Values

* refactor: Update DeleteButton to close menu when deletion is successful

* refactor: Add unmountOnHide prop to DropdownPopup in multiple components

* chore: linting

* chore: linting

* feat: Add `chatMenu` option for MCP Servers to control visibility in MCPSelect dropdown

* refactor: Update loadManifestTools to return combined tool manifest with MCP tools first

* chore: remove deprecated openapi plugins

* chore: linting

* chore(AgentClient): linting, remove unnecessary `checkVisionRequest` logger

* refactor(AuthService): change logoutUser logging from error to debug level

* chore: new Gemini models token values and rates

* chore(AskController): linting
2025-05-08 12:12:36 -04:00
Danny Avila
d7390d24ec 🔄 fix: Ollama Think Tag Edge Case with Tools (#7275) 2025-05-07 17:49:42 -04:00
Danny Avila
71105cd49c 🔄 fix: Assistants Endpoint & Minor Issues (#7274)
* 🔄 fix: Include usage in stream options for OpenAI and Azure endpoints

* fix: Agents support for Azure serverless endpoints

* fix: Refactor condition for assistants and azureAssistants endpoint handling

* AWS Titan via Bedrock: model doesn't support system messages, Closes #6456

* fix: Add EndpointSchemaKey type to endpoint parameters in buildDefaultConvo and ensure assistantId is always defined

* fix: Handle new conversation state for assistants endpoint in finalHandler

* fix: Add spec and iconURL parameters to `saveAssistantMessage` to persist modelSpec fields

* fix: Handle assistant unlinking even if no valid files to delete

* chore: move type definitions from callbacks.js to typedefs.js

* chore: Add StandardGraph typedef to typedefs.js

* chore: Update parameter type for graph in ModelEndHandler to StandardGraph

---------

Co-authored-by: Andres Restrepo <andres@enric.ai>
2025-05-07 17:11:33 -04:00
Marlon
3606349a0f 📝 docs: Update .env.example Google models (#7254)
This pull request updates the GOOGLE_MODELS and GOOGLE_TITLE_MODEL examples in the .env.example file to reflect the currently available models on Google AI Studio (Gemini API) and Vertex AI.
Many of the models previously listed in the example file have since been deprecated or are no longer the primary recommended versions. This discrepancy could lead to confusion for new users setting up the project, potentially causing them to select non-functional or outdated model identifiers, resulting in errors or suboptimal performance.
The changes in this PR ensure that:
- The model lists for both Gemini API (AI Studio) and Vertex AI are synchronized with the current offerings.
- New users have a more accurate and reliable starting point when configuring their environment.
- The likelihood of encountering issues due to deprecated model names during initial setup is significantly reduced.
2025-05-07 11:19:06 -04:00
glowforge-opensource
e3e796293c 🔍 feat: Additional Tavily API Tool Parameters (#7232)
* feat: expose additional Tavily API parameters for tool

The following parameters are part of Tavily API but were previously not exposed for agents to use via the tool. Now they are. The source documentation is here: https://docs.tavily.com/documentation/api-reference/endpoint/search

include_raw_content - returns the full text of found web pages (default is false)
include_domains - limit search to this list of domains (default is none)
exclude_domains - exclude this list of domains form search (default is none)
topic - enum of "general", "news", or "finance" (default is "general")
time_range - enum of "day", "week", "month", or "year" (default unlimited)
days - number of days to search (default is 7, but only applicable to topic == "news")
include_image_descriptions - include a description of the image in the search results (default is false)

It is a little odd that they have both time_range and days, but there it is.

I have noticed that this change requires a little bit of care in prompting to make sure that it doesn't use "news" when you wanted "general". I've attemtped to hint that in the tool description.

* correct lint error

* more lint

---------

Co-authored-by: Michael Natkin <michaeln@glowforge.com>
2025-05-06 22:50:11 -04:00
Danny Avila
7c4c3a8796 🔄 fix: URL Param Race Condition and File Draft Persistence (#7257)
* chore(useAutoSave): linting

* fix: files attached during streaming disappear when stream finishes

* fix(useQueryParams): query parameter processing race condition with submission handling, add JSDocs to all functions/hooks

* test(useQueryParams): add comprehensive tests for query parameter handling and submission logic
2025-05-06 22:49:12 -04:00
andresgit
20c9f1a783 🎨 style: Improve KaTeX Rendering for LaTeX Equations (#7223) 2025-05-06 10:50:09 -04:00
Danny Avila
8e1012c5aa 🛡️ fix: Deep Clone MCPOptions for User MCP Connections (#7247)
* Fix: Prevent side effects in `processMCPEnv` by deep cloning MCPOptions

The `processMCPEnv` function was modifying the original `MCPOptions` object, leading to unintended side effects where `LIBRECHAT_USER_ID` could be incorrectly shared across different users. This commit addresses this issue by performing a deep clone of the `MCPOptions` object before processing, ensuring that modifications are isolated and do not affect other users.

* ci: Add tests for processMCPEnv to ensure deep cloning, user ID isolation and environment variable processing

---------

Co-authored-by: Alex C <viennadd@users.noreply.github.com>
2025-05-06 10:29:05 -04:00
Danny Avila
7c92cef2b7 🔖 fix: Custom Headers for Initial MCP SSE Connection (#7246)
* refactor: add custom  to  as workaround to include custom headers to the initial connection request

* chore: bump MCP client version to 1.2.1 in package-lock and package.json for librechat-mcp
2025-05-06 10:14:17 -04:00
Danny Avila
4fbb81c774 🔄 fix: o-Series Model Regex for System Messages (#7245)
* fix: no system message only for o1-preview and o1-mini

* chore(OpenAIClient): linting

* fix: update regex to include o1-preview and o1-mini in noSystemModelRegex

* refactor: rename variable for consistency with AgentClient

---------

Co-authored-by: Andres <9771158+andresgit@users.noreply.github.com>
2025-05-06 08:40:00 -04:00
Marco Beretta
fc6e14efe2 feat: Enhance form submission for touch screens (#7198)
*  feat: Enhance form submission for touch screens

* chore: add comment

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: add comment

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: linting in AnthropicClient

* chore: Add anthropic model outputs for Claude 3.7

* refactor: Simplify touch-screen detection in message submission

* fix: Correct button rendering order for chat collapse/expand icons

* Revert "refactor: Simplify touch-screen detection in message submission"

This reverts commit 8638442a4c.

* refactor: Improve touchscreen detection for focus handling in ChatForm and useFocusChatEffect

* chore: EditMessage linting

* refactor: Reorder dropdown items in ExportAndShareMenu

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-05-05 09:23:38 -04:00
Danny Avila
6e663b2480 🛠️ fix: Conversation Navigation State (#7210)
* refactor: Enhance initial conversation query condition for better state management and prevent unused network requests

* ifx: Add Prettier plugin to ESLint configuration

* chore: linting and typing in convos.spec.ts

* fix: add back fresh data fetching and improve error handling for  conversation navigation

* fix: set conversation only with  conversation state change intent, to prevent double queries for messages
2025-05-04 10:44:40 -04:00
matt burnett
ddb2141eac 🧰 chore: ESLint configuration to enforce Prettier formatting rules (#7186) 2025-05-02 15:13:31 -04:00
Danny Avila
37b50736bc 🔧 fix: Google Gemma Support & OpenAI Reasoning Instructions (#7196)
* 🔄 chore: Update @langchain/google-vertexai to version 0.2.5 in package.json and package-lock.json

* chore: temp remove agents

* 🔄 chore: Update @langchain/google-genai to version 0.2.5 in package.json and package-lock.json

* 🔄 chore: Update @langchain/community to version 0.3.42 in package.json and package-lock.json

* 🔄 chore: Add license information for @langchain/textsplitters in package-lock.json

* 🔄 chore: Update @langchain/core to version 0.3.51 in package.json and package-lock.json

* 🔄 chore: Update openai dependency to version 4.96.2 in package.json and package-lock.json

* chore: @librechat/agents to v2.4.30

* fix: streaming condition in ModelEndHandler to account for boundModel `disableStreaming` setting

* fix: update regex for noSystemModel and refactor message handling in AgentClient

* feat: Google Gemma models

* chore: remove unnecessary empty JSX fragment in PopoverButtons component
2025-05-02 15:11:50 -04:00
Danny Avila
5d6d13efe8 🌿 refactor: Unmount Fork Popover on Hide for Performance (#7189) 2025-05-02 02:43:59 -04:00
Danny Avila
5efad8f646 📦 chore: Bump Package Security (#7183)
* 🔄 chore: bump supertest to 7.1.0, resolves CVE-2025-46653

* 🔄 chore: update vite to version 6.3.4 and add fdir, picomatch, and tinyglobby as dev dependencies

* 🔄 chore: npm audit fix: remove unused dependencies fdir, picomatch, and tinyglobby from package-lock.json
2025-05-01 15:02:51 -04:00
Danny Avila
9a7f763714 🔄 refactor: Artifact Visibility Management (#7181)
* fix: Reset artifacts on unmount and remove useIdChangeEffect hook

* feat: Replace SVG icons with Lucide icons for improved consistency

* fix: Refactor artifact reset logic on unmount and conversation change

* refactor: Rename artifactsVisible to artifactsVisibility for consistency

* feat: Replace custom SVG icons with Lucide icons for improved consistency

* feat: Add visibleArtifacts atom for managing visibility state

* feat: Implement debounced visibility state management for artifacts

* refactor: Add useIdChangeEffect hook to reset visible artifacts on conversation ID change

* refactor: Remove unnecessary dependency from useMemo in TextPart component

* refactor: Enhance artifact visibility management by incorporating location checks for search path

* refactor: Improve transition effects for artifact visibility in Artifacts component

* chore: Remove preprocessCodeArtifacts function and related tests

* fix: Update regex for detecting enclosed artifacts in latest message

* refactor: Update artifact visibility checks to be more generic (not just search)

* chore: Enhance artifact visibility logging

* refactor: Extract closeArtifacts function to improve button click handling

* refactor: remove nested logic from use artifacts effect

* refactor: Update regex for detecting enclosed artifacts to handle new line variations
2025-05-01 14:40:39 -04:00
github-actions[bot]
e6e7935fd8 📜 docs: CHANGELOG for release v0.7.8-rc1 (#7153)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-30 08:54:43 -04:00
Ruben Talstra
083710d4c9 Merge branch 'main' into refactor/openid-strategy 2025-04-10 19:17:05 +02:00
Ruben Talstra
c9b04ef1b4 🔧 chore: Bump version to 0.7.790 in package.json and package-lock.json 2025-04-10 19:07:34 +02:00
Ruben Talstra
81fe64da05 🔧 feat: Enhance role extraction logic in OpenID strategy to support multiple sources and improve error handling 2025-04-05 14:49:25 +02:00
Ruben Talstra
b0ebc265a3 🔧 docs: Add comments for supported algorithms in openidStrategy.js 2025-04-05 14:31:28 +02:00
Ruben Talstra
e5743a0b10 🔧 refactor: Clean up imports in openidStrategy.js for improved readability 2025-04-05 14:16:08 +02:00
Ruben Talstra
ec5c9fef48 🔧 feat: Add support for PKCE in OpenID strategy configuration 2025-04-05 14:14:36 +02:00
Ruben Talstra
f74b9a3018 🔧 test: Add fallback test for userinfo roles with invalid id_token 2025-04-05 14:04:40 +02:00
Ruben Talstra
1083014464 🔧 feat: Enhance OpenID strategy with improved error handling, role extraction, and user creation logic 2025-04-05 14:04:15 +02:00
Ruben Talstra
124533f09f 🔧 test: Update OpenID strategy tests with simulated JWT tokens and improved assertions 2025-04-05 13:36:42 +02:00
Ruben Talstra
c77d13d269 🔧 feat: Enhance OpenID role extraction and validation logic 2025-04-05 13:30:31 +02:00
117 changed files with 5185 additions and 1568 deletions

View File

@@ -20,8 +20,8 @@ DOMAIN_CLIENT=http://localhost:3080
DOMAIN_SERVER=http://localhost:3080
NO_INDEX=true
# Use the address that is at most n number of hops away from the Express application.
# req.socket.remoteAddress is the first hop, and the rest are looked for in the X-Forwarded-For header from right to left.
# Use the address that is at most n number of hops away from the Express application.
# req.socket.remoteAddress is the first hop, and the rest are looked for in the X-Forwarded-For header from right to left.
# A value of 0 means that the first untrusted address would be req.socket.remoteAddress, i.e. there is no reverse proxy.
# Defaulted to 1.
TRUST_PROXY=1
@@ -142,12 +142,12 @@ GOOGLE_KEY=user_provided
# GOOGLE_AUTH_HEADER=true
# Gemini API (AI Studio)
# GOOGLE_MODELS=gemini-2.5-pro-exp-03-25,gemini-2.0-flash-exp,gemini-2.0-flash-thinking-exp-1219,gemini-exp-1121,gemini-exp-1114,gemini-1.5-flash-latest,gemini-1.0-pro,gemini-1.0-pro-001,gemini-1.0-pro-latest,gemini-1.0-pro-vision-latest,gemini-1.5-pro-latest,gemini-pro,gemini-pro-vision
# GOOGLE_MODELS=gemini-2.5-pro-preview-05-06,gemini-2.5-flash-preview-04-17,gemini-2.0-flash-001,gemini-2.0-flash-exp,gemini-2.0-flash-lite-001,gemini-1.5-pro-002,gemini-1.5-flash-002
# Vertex AI
# GOOGLE_MODELS=gemini-1.5-flash-preview-0514,gemini-1.5-pro-preview-0514,gemini-1.0-pro-vision-001,gemini-1.0-pro-002,gemini-1.0-pro-001,gemini-pro-vision,gemini-1.0-pro
# GOOGLE_MODELS=gemini-2.5-pro-preview-05-06,gemini-2.5-flash-preview-04-17,gemini-2.0-flash-001,gemini-2.0-flash-exp,gemini-2.0-flash-lite-001,gemini-1.5-pro-002,gemini-1.5-flash-002
# GOOGLE_TITLE_MODEL=gemini-pro
# GOOGLE_TITLE_MODEL=gemini-2.0-flash-lite-001
# GOOGLE_LOC=us-central1
@@ -428,9 +428,12 @@ OPENID_CLIENT_ID=
OPENID_CLIENT_SECRET=
OPENID_ISSUER=
OPENID_SESSION_SECRET=
# OPENID_USE_PKCE=
OPENID_SCOPE="openid profile email"
OPENID_CALLBACK_URL=/oauth/openid/callback
OPENID_REQUIRED_ROLE=
# Set to 'userinfo' or 'token' to determine witch role source to use, Default is 'token'
OPENID_REQUIRED_ROLE_SOURCE=
OPENID_REQUIRED_ROLE_TOKEN_KIND=
OPENID_REQUIRED_ROLE_PARAMETER_PATH=
# Set to determine which user info property returned from OpenID Provider to store as the User's username

View File

@@ -4,6 +4,7 @@ on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
jobs:
generate-release-changelog-pr:
@@ -88,7 +89,7 @@ jobs:
base: main
branch: "changelog/${{ github.ref_name }}"
reviewers: danny-avila
title: "chore: update CHANGELOG for release ${{ github.ref_name }}"
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.
- This PR updates the CHANGELOG.md by removing the "Unreleased" section and adding new release notes for release ${{ github.ref_name }} above previous releases.

View File

@@ -99,9 +99,9 @@ jobs:
branch: "changelog/unreleased-update"
sign-commits: true
commit-message: "action: update Unreleased changelog"
title: "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.
regenerates the Unreleased changelog, removes any old Unreleased block, and inserts the new content.

View File

@@ -3,10 +3,76 @@
All notable changes to this project will be documented in this file.
## [Unreleased]
### ✨ New Features
- ✨ feat: implement search parameter updates by **@mawburn** in [#7151](https://github.com/danny-avila/LibreChat/pull/7151)
- 🎏 feat: Add MCP support for Streamable HTTP Transport by **@benverhees** in [#7353](https://github.com/danny-avila/LibreChat/pull/7353)
### 🔧 Fixes
- 💬 fix: update aria-label for accessibility in ConvoLink component by **@berry-13** in [#7320](https://github.com/danny-avila/LibreChat/pull/7320)
- 🔑 fix: use `apiKey` instead of `openAIApiKey` in OpenAI-like Config by **@danny-avila** in [#7337](https://github.com/danny-avila/LibreChat/pull/7337)
- 🔄 fix: update navigation logic in `useFocusChatEffect` to ensure correct search parameters are used by **@mawburn** in [#7340](https://github.com/danny-avila/LibreChat/pull/7340)
### ⚙️ Other Changes
- 📜 docs: CHANGELOG for release v0.7.8 by **@github-actions[bot]** in [#7290](https://github.com/danny-avila/LibreChat/pull/7290)
- 📦 chore: Update API Package Dependencies by **@danny-avila** in [#7359](https://github.com/danny-avila/LibreChat/pull/7359)
---
## [v0.7.8] -
Changes from v0.7.8-rc1 to v0.7.8.
### ✨ New Features
- ✨ feat: Enhance form submission for touch screens by **@berry-13** in [#7198](https://github.com/danny-avila/LibreChat/pull/7198)
- 🔍 feat: Additional Tavily API Tool Parameters by **@glowforge-opensource** in [#7232](https://github.com/danny-avila/LibreChat/pull/7232)
- 🐋 feat: Add python to Dockerfile for increased MCP compatibility by **@technicalpickles** in [#7270](https://github.com/danny-avila/LibreChat/pull/7270)
### 🔧 Fixes
- 🔧 fix: Google Gemma Support & OpenAI Reasoning Instructions by **@danny-avila** in [#7196](https://github.com/danny-avila/LibreChat/pull/7196)
- 🛠️ fix: Conversation Navigation State by **@danny-avila** in [#7210](https://github.com/danny-avila/LibreChat/pull/7210)
- 🔄 fix: o-Series Model Regex for System Messages by **@danny-avila** in [#7245](https://github.com/danny-avila/LibreChat/pull/7245)
- 🔖 fix: Custom Headers for Initial MCP SSE Connection by **@danny-avila** in [#7246](https://github.com/danny-avila/LibreChat/pull/7246)
- 🛡️ fix: Deep Clone `MCPOptions` for User MCP Connections by **@danny-avila** in [#7247](https://github.com/danny-avila/LibreChat/pull/7247)
- 🔄 fix: URL Param Race Condition and File Draft Persistence by **@danny-avila** in [#7257](https://github.com/danny-avila/LibreChat/pull/7257)
- 🔄 fix: Assistants Endpoint & Minor Issues by **@danny-avila** in [#7274](https://github.com/danny-avila/LibreChat/pull/7274)
- 🔄 fix: Ollama Think Tag Edge Case with Tools by **@danny-avila** in [#7275](https://github.com/danny-avila/LibreChat/pull/7275)
### ⚙️ Other Changes
- 📜 docs: CHANGELOG for release v0.7.8-rc1 by **@github-actions[bot]** in [#7153](https://github.com/danny-avila/LibreChat/pull/7153)
- 🔄 refactor: Artifact Visibility Management by **@danny-avila** in [#7181](https://github.com/danny-avila/LibreChat/pull/7181)
- 📦 chore: Bump Package Security by **@danny-avila** in [#7183](https://github.com/danny-avila/LibreChat/pull/7183)
- 🌿 refactor: Unmount Fork Popover on Hide for Better Performance by **@danny-avila** in [#7189](https://github.com/danny-avila/LibreChat/pull/7189)
- 🧰 chore: ESLint configuration to enforce Prettier formatting rules by **@mawburn** in [#7186](https://github.com/danny-avila/LibreChat/pull/7186)
- 🎨 style: Improve KaTeX Rendering for LaTeX Equations by **@andresgit** in [#7223](https://github.com/danny-avila/LibreChat/pull/7223)
- 📝 docs: Update `.env.example` Google models by **@marlonka** in [#7254](https://github.com/danny-avila/LibreChat/pull/7254)
- 💬 refactor: MCP Chat Visibility Option, Google Rates, Remove OpenAPI Plugins by **@danny-avila** in [#7286](https://github.com/danny-avila/LibreChat/pull/7286)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7214](https://github.com/danny-avila/LibreChat/pull/7214)
[See full release details][release-v0.7.8]
[release-v0.7.8]: https://github.com/danny-avila/LibreChat/releases/tag/v0.7.8
---
## [v0.7.8-rc1] -
## [v0.7.8-rc1] -
Changes from v0.7.7 to v0.7.8-rc1.
### ✨ New Features
- 🔍 feat: Mistral OCR API / Upload Files as Text by **@danny-avila** in [#6274](https://github.com/danny-avila/LibreChat/pull/6274)
- 🤖 feat: Support OpenAI Web Search models by **@danny-avila** in [#6313](https://github.com/danny-avila/LibreChat/pull/6313)
- 🔗 feat: Agent Chain (Mixture-of-Agents) by **@danny-avila** in [#6374](https://github.com/danny-avila/LibreChat/pull/6374)
@@ -136,7 +202,12 @@ All notable changes to this project will be documented in this file.
- 🧭 refactor: Modernize Nav/Header by **@danny-avila** in [#7094](https://github.com/danny-avila/LibreChat/pull/7094)
- 🪶 refactor: Chat Input Focus for Conversation Navigations & ChatForm Optimizations by **@danny-avila** in [#7100](https://github.com/danny-avila/LibreChat/pull/7100)
- 🔃 refactor: Streamline Navigation, Message Loading UX by **@danny-avila** in [#7118](https://github.com/danny-avila/LibreChat/pull/7118)
- 📜 docs: Unreleased changelog by **@github-actions[bot]** in [#6265](https://github.com/danny-avila/LibreChat/pull/6265)
[See full release details][release-v0.7.8-rc1]
[release-v0.7.8-rc1]: https://github.com/danny-avila/LibreChat/releases/tag/v0.7.8-rc1
---

View File

@@ -1,10 +1,11 @@
# v0.7.8-rc1
# v0.7.8
# Base node image
FROM node:20-alpine AS node
# Install jemalloc
RUN apk add --no-cache jemalloc
RUN apk add --no-cache python3 py3-pip uv
# Set environment variable to use jemalloc
ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2

View File

@@ -1,5 +1,5 @@
# Dockerfile.multi
# v0.7.8-rc1
# v0.7.8
# Base for all builds
FROM node:20-alpine AS base-min

View File

@@ -396,13 +396,13 @@ class AnthropicClient extends BaseClient {
const formattedMessages = orderedMessages.map((message, i) => {
const formattedMessage = this.useMessages
? formatMessage({
message,
endpoint: EModelEndpoint.anthropic,
})
message,
endpoint: EModelEndpoint.anthropic,
})
: {
author: message.isCreatedByUser ? this.userLabel : this.assistantLabel,
content: message?.content ?? message.text,
};
author: message.isCreatedByUser ? this.userLabel : this.assistantLabel,
content: message?.content ?? message.text,
};
const needsTokenCount = this.contextStrategy && !orderedMessages[i].tokenCount;
/* If tokens were never counted, or, is a Vision request and the message has files, count again */
@@ -680,7 +680,7 @@ class AnthropicClient extends BaseClient {
}
getCompletion() {
logger.debug('AnthropicClient doesn\'t use getCompletion (all handled in sendCompletion)');
logger.debug("AnthropicClient doesn't use getCompletion (all handled in sendCompletion)");
}
/**
@@ -888,7 +888,7 @@ class AnthropicClient extends BaseClient {
}
getBuildMessagesOptions() {
logger.debug('AnthropicClient doesn\'t use getBuildMessagesOptions');
logger.debug("AnthropicClient doesn't use getBuildMessagesOptions");
}
getEncoding() {

View File

@@ -63,15 +63,15 @@ class BaseClient {
}
setOptions() {
throw new Error('Method \'setOptions\' must be implemented.');
throw new Error("Method 'setOptions' must be implemented.");
}
async getCompletion() {
throw new Error('Method \'getCompletion\' must be implemented.');
throw new Error("Method 'getCompletion' must be implemented.");
}
async sendCompletion() {
throw new Error('Method \'sendCompletion\' must be implemented.');
throw new Error("Method 'sendCompletion' must be implemented.");
}
getSaveOptions() {
@@ -237,11 +237,11 @@ class BaseClient {
const userMessage = opts.isEdited
? this.currentMessages[this.currentMessages.length - 2]
: this.createUserMessage({
messageId: userMessageId,
parentMessageId,
conversationId,
text: message,
});
messageId: userMessageId,
parentMessageId,
conversationId,
text: message,
});
if (typeof opts?.getReqData === 'function') {
opts.getReqData({

View File

@@ -140,8 +140,7 @@ class GoogleClient extends BaseClient {
this.options.attachments?.then((attachments) => this.checkVisionRequest(attachments));
/** @type {boolean} Whether using a "GenerativeAI" Model */
this.isGenerativeModel =
this.modelOptions.model.includes('gemini') || this.modelOptions.model.includes('learnlm');
this.isGenerativeModel = /gemini|learnlm|gemma/.test(this.modelOptions.model);
this.maxContextTokens =
this.options.maxContextTokens ??

View File

@@ -475,7 +475,9 @@ class OpenAIClient extends BaseClient {
promptPrefix = this.augmentedPrompt + promptPrefix;
}
if (promptPrefix && this.isOmni !== true) {
const noSystemModelRegex = /\b(o1-preview|o1-mini)\b/i.test(this.modelOptions.model);
if (promptPrefix && !noSystemModelRegex) {
promptPrefix = `Instructions:\n${promptPrefix.trim()}`;
instructions = {
role: 'system',
@@ -503,7 +505,7 @@ class OpenAIClient extends BaseClient {
};
/** EXPERIMENTAL */
if (promptPrefix && this.isOmni === true) {
if (promptPrefix && noSystemModelRegex) {
const lastUserMessageIndex = payload.findLastIndex((message) => message.role === 'user');
if (lastUserMessageIndex !== -1) {
if (Array.isArray(payload[lastUserMessageIndex].content)) {
@@ -1227,9 +1229,9 @@ ${convo}
opts.baseURL = this.langchainProxy
? constructAzureURL({
baseURL: this.langchainProxy,
azureOptions: this.azure,
})
baseURL: this.langchainProxy,
azureOptions: this.azure,
})
: this.azureEndpoint.split(/(?<!\/)\/(chat|completion)\//)[0];
opts.defaultQuery = { 'api-version': this.azure.azureOpenAIApiVersion };
@@ -1283,6 +1285,14 @@ ${convo}
modelOptions.messages[0].role = 'user';
}
if (
(this.options.endpoint === EModelEndpoint.openAI ||
this.options.endpoint === EModelEndpoint.azureOpenAI) &&
modelOptions.stream === true
) {
modelOptions.stream_options = { include_usage: true };
}
if (this.options.addParams && typeof this.options.addParams === 'object') {
const addParams = { ...this.options.addParams };
modelOptions = {
@@ -1385,12 +1395,6 @@ ${convo}
...modelOptions,
stream: true,
};
if (
this.options.endpoint === EModelEndpoint.openAI ||
this.options.endpoint === EModelEndpoint.azureOpenAI
) {
params.stream_options = { include_usage: true };
}
const stream = await openai.beta.chat.completions
.stream(params)
.on('abort', () => {

View File

@@ -43,9 +43,39 @@ class TavilySearchResults extends Tool {
.boolean()
.optional()
.describe('Whether to include answers in the search results. Default is False.'),
// include_raw_content: z.boolean().optional().describe('Whether to include raw content in the search results. Default is False.'),
// include_domains: z.array(z.string()).optional().describe('A list of domains to specifically include in the search results.'),
// exclude_domains: z.array(z.string()).optional().describe('A list of domains to specifically exclude from the search results.'),
include_raw_content: z
.boolean()
.optional()
.describe('Whether to include raw content in the search results. Default is False.'),
include_domains: z
.array(z.string())
.optional()
.describe('A list of domains to specifically include in the search results.'),
exclude_domains: z
.array(z.string())
.optional()
.describe('A list of domains to specifically exclude from the search results.'),
topic: z
.enum(['general', 'news', 'finance'])
.optional()
.describe(
'The category of the search. Use news ONLY if query SPECIFCALLY mentions the word "news".',
),
time_range: z
.enum(['day', 'week', 'month', 'year', 'd', 'w', 'm', 'y'])
.optional()
.describe('The time range back from the current date to filter results.'),
days: z
.number()
.min(1)
.optional()
.describe('Number of days back from the current date to include. Only if topic is news.'),
include_image_descriptions: z
.boolean()
.optional()
.describe(
'When include_images is true, also add a descriptive text for each image. Default is false.',
),
});
}

View File

@@ -1,30 +0,0 @@
const { loadSpecs } = require('./loadSpecs');
function transformSpec(input) {
return {
name: input.name_for_human,
pluginKey: input.name_for_model,
description: input.description_for_human,
icon: input?.logo_url ?? 'https://placehold.co/70x70.png',
// TODO: add support for authentication
isAuthRequired: 'false',
authConfig: [],
};
}
async function addOpenAPISpecs(availableTools) {
try {
const specs = (await loadSpecs({})).map(transformSpec);
if (specs.length > 0) {
return [...specs, ...availableTools];
}
return availableTools;
} catch (error) {
return availableTools;
}
}
module.exports = {
transformSpec,
addOpenAPISpecs,
};

View File

@@ -1,76 +0,0 @@
const { addOpenAPISpecs, transformSpec } = require('./addOpenAPISpecs');
const { loadSpecs } = require('./loadSpecs');
const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin');
jest.mock('./loadSpecs');
jest.mock('../dynamic/OpenAPIPlugin');
describe('transformSpec', () => {
it('should transform input spec to a desired format', () => {
const input = {
name_for_human: 'Human Name',
name_for_model: 'Model Name',
description_for_human: 'Human Description',
logo_url: 'https://example.com/logo.png',
};
const expectedOutput = {
name: 'Human Name',
pluginKey: 'Model Name',
description: 'Human Description',
icon: 'https://example.com/logo.png',
isAuthRequired: 'false',
authConfig: [],
};
expect(transformSpec(input)).toEqual(expectedOutput);
});
it('should use default icon if logo_url is not provided', () => {
const input = {
name_for_human: 'Human Name',
name_for_model: 'Model Name',
description_for_human: 'Human Description',
};
const expectedOutput = {
name: 'Human Name',
pluginKey: 'Model Name',
description: 'Human Description',
icon: 'https://placehold.co/70x70.png',
isAuthRequired: 'false',
authConfig: [],
};
expect(transformSpec(input)).toEqual(expectedOutput);
});
});
describe('addOpenAPISpecs', () => {
it('should add specs to available tools', async () => {
const availableTools = ['Tool1', 'Tool2'];
const specs = [
{
name_for_human: 'Human Name',
name_for_model: 'Model Name',
description_for_human: 'Human Description',
logo_url: 'https://example.com/logo.png',
},
];
loadSpecs.mockResolvedValue(specs);
createOpenAPIPlugin.mockReturnValue('Plugin');
const result = await addOpenAPISpecs(availableTools);
expect(result).toEqual([...specs.map(transformSpec), ...availableTools]);
});
it('should return available tools if specs loading fails', async () => {
const availableTools = ['Tool1', 'Tool2'];
loadSpecs.mockRejectedValue(new Error('Failed to load specs'));
const result = await addOpenAPISpecs(availableTools);
expect(result).toEqual(availableTools);
});
});

View File

@@ -24,7 +24,6 @@ const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/pro
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { createMCPTool } = require('~/server/services/MCP');
const { loadSpecs } = require('./loadSpecs');
const { logger } = require('~/config');
const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`);
@@ -232,7 +231,6 @@ const loadTools = async ({
/** @type {Record<string, string>} */
const toolContextMap = {};
const remainingTools = [];
const appTools = options.req?.app?.locals?.availableTools ?? {};
for (const tool of tools) {
@@ -292,30 +290,6 @@ const loadTools = async ({
requestedTools[tool] = toolInstance;
continue;
}
if (functions === true) {
remainingTools.push(tool);
}
}
let specs = null;
if (useSpecs === true && functions === true && remainingTools.length > 0) {
specs = await loadSpecs({
llm: model,
user,
message: options.message,
memory: options.memory,
signal: options.signal,
tools: remainingTools,
map: true,
verbose: false,
});
}
for (const tool of remainingTools) {
if (specs && specs[tool]) {
requestedTools[tool] = specs[tool];
}
}
if (returnMap) {

View File

@@ -1,117 +0,0 @@
const fs = require('fs');
const path = require('path');
const { z } = require('zod');
const { logger } = require('~/config');
const { createOpenAPIPlugin } = require('~/app/clients/tools/dynamic/OpenAPIPlugin');
// The minimum Manifest definition
const ManifestDefinition = z.object({
schema_version: z.string().optional(),
name_for_human: z.string(),
name_for_model: z.string(),
description_for_human: z.string(),
description_for_model: z.string(),
auth: z.object({}).optional(),
api: z.object({
// Spec URL or can be the filename of the OpenAPI spec yaml file,
// located in api\app\clients\tools\.well-known\openapi
url: z.string(),
type: z.string().optional(),
is_user_authenticated: z.boolean().nullable().optional(),
has_user_authentication: z.boolean().nullable().optional(),
}),
// use to override any params that the LLM will consistently get wrong
params: z.object({}).optional(),
logo_url: z.string().optional(),
contact_email: z.string().optional(),
legal_info_url: z.string().optional(),
});
function validateJson(json) {
try {
return ManifestDefinition.parse(json);
} catch (error) {
logger.debug('[validateJson] manifest parsing error', error);
return false;
}
}
// omit the LLM to return the well known jsons as objects
async function loadSpecs({ llm, user, message, tools = [], map = false, memory, signal }) {
const directoryPath = path.join(__dirname, '..', '.well-known');
let files = [];
for (let i = 0; i < tools.length; i++) {
const filePath = path.join(directoryPath, tools[i] + '.json');
try {
// If the access Promise is resolved, it means that the file exists
// Then we can add it to the files array
await fs.promises.access(filePath, fs.constants.F_OK);
files.push(tools[i] + '.json');
} catch (err) {
logger.error(`[loadSpecs] File ${tools[i] + '.json'} does not exist`, err);
}
}
if (files.length === 0) {
files = (await fs.promises.readdir(directoryPath)).filter(
(file) => path.extname(file) === '.json',
);
}
const validJsons = [];
const constructorMap = {};
logger.debug('[validateJson] files', files);
for (const file of files) {
if (path.extname(file) === '.json') {
const filePath = path.join(directoryPath, file);
const fileContent = await fs.promises.readFile(filePath, 'utf8');
const json = JSON.parse(fileContent);
if (!validateJson(json)) {
logger.debug('[validateJson] Invalid json', json);
continue;
}
if (llm && map) {
constructorMap[json.name_for_model] = async () =>
await createOpenAPIPlugin({
data: json,
llm,
message,
memory,
signal,
user,
});
continue;
}
if (llm) {
validJsons.push(createOpenAPIPlugin({ data: json, llm }));
continue;
}
validJsons.push(json);
}
}
if (map) {
return constructorMap;
}
const plugins = (await Promise.all(validJsons)).filter((plugin) => plugin);
// logger.debug('[validateJson] plugins', plugins);
// logger.debug(plugins[0].name);
return plugins;
}
module.exports = {
loadSpecs,
validateJson,
ManifestDefinition,
};

View File

@@ -1,101 +0,0 @@
const fs = require('fs');
const { validateJson, loadSpecs, ManifestDefinition } = require('./loadSpecs');
const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin');
jest.mock('../dynamic/OpenAPIPlugin');
describe('ManifestDefinition', () => {
it('should validate correct json', () => {
const json = {
name_for_human: 'Test',
name_for_model: 'Test',
description_for_human: 'Test',
description_for_model: 'Test',
api: {
url: 'http://test.com',
},
};
expect(() => ManifestDefinition.parse(json)).not.toThrow();
});
it('should not validate incorrect json', () => {
const json = {
name_for_human: 'Test',
name_for_model: 'Test',
description_for_human: 'Test',
description_for_model: 'Test',
api: {
url: 123, // incorrect type
},
};
expect(() => ManifestDefinition.parse(json)).toThrow();
});
});
describe('validateJson', () => {
it('should return parsed json if valid', () => {
const json = {
name_for_human: 'Test',
name_for_model: 'Test',
description_for_human: 'Test',
description_for_model: 'Test',
api: {
url: 'http://test.com',
},
};
expect(validateJson(json)).toEqual(json);
});
it('should return false if json is not valid', () => {
const json = {
name_for_human: 'Test',
name_for_model: 'Test',
description_for_human: 'Test',
description_for_model: 'Test',
api: {
url: 123, // incorrect type
},
};
expect(validateJson(json)).toEqual(false);
});
});
describe('loadSpecs', () => {
beforeEach(() => {
jest.spyOn(fs.promises, 'readdir').mockResolvedValue(['test.json']);
jest.spyOn(fs.promises, 'readFile').mockResolvedValue(
JSON.stringify({
name_for_human: 'Test',
name_for_model: 'Test',
description_for_human: 'Test',
description_for_model: 'Test',
api: {
url: 'http://test.com',
},
}),
);
createOpenAPIPlugin.mockResolvedValue({});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should return plugins', async () => {
const plugins = await loadSpecs({ llm: true, verbose: false });
expect(plugins).toHaveLength(1);
expect(createOpenAPIPlugin).toHaveBeenCalledTimes(1);
});
it('should return constructorMap if map is true', async () => {
const plugins = await loadSpecs({ llm: {}, map: true, verbose: false });
expect(plugins).toHaveProperty('Test');
expect(createOpenAPIPlugin).not.toHaveBeenCalled();
});
});

View File

@@ -111,10 +111,15 @@ const tokenValues = Object.assign(
/* cohere doesn't have rates for the older command models,
so this was from https://artificialanalysis.ai/models/command-light/providers */
command: { prompt: 0.38, completion: 0.38 },
gemma: { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemma-2': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemma-3': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemma-3-27b': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemini-2.0-flash-lite': { prompt: 0.075, completion: 0.3 },
'gemini-2.0-flash': { prompt: 0.1, completion: 0.4 },
'gemini-2.0': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemini-2.5-pro-preview-03-25': { prompt: 1.25, completion: 10 },
'gemini-2.5-pro': { prompt: 1.25, completion: 10 },
'gemini-2.5-flash': { prompt: 0.15, completion: 3.5 },
'gemini-2.5': { prompt: 0, completion: 0 }, // Free for a period of time
'gemini-1.5-flash-8b': { prompt: 0.075, completion: 0.3 },
'gemini-1.5-flash': { prompt: 0.15, completion: 0.6 },

View File

@@ -488,6 +488,9 @@ describe('getCacheMultiplier', () => {
describe('Google Model Tests', () => {
const googleModels = [
'gemini-2.5-pro-preview-05-06',
'gemini-2.5-flash-preview-04-17',
'gemini-2.5-exp',
'gemini-2.0-flash-lite-preview-02-05',
'gemini-2.0-flash-001',
'gemini-2.0-flash-exp',
@@ -525,6 +528,9 @@ describe('Google Model Tests', () => {
it('should map to the correct model keys', () => {
const expected = {
'gemini-2.5-pro-preview-05-06': 'gemini-2.5-pro',
'gemini-2.5-flash-preview-04-17': 'gemini-2.5-flash',
'gemini-2.5-exp': 'gemini-2.5',
'gemini-2.0-flash-lite-preview-02-05': 'gemini-2.0-flash-lite',
'gemini-2.0-flash-001': 'gemini-2.0-flash',
'gemini-2.0-flash-exp': 'gemini-2.0-flash',

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
"version": "v0.7.8-rc1",
"version": "v0.7.8",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",
@@ -43,12 +43,12 @@
"@google/generative-ai": "^0.23.0",
"@googleapis/youtube": "^20.0.0",
"@keyv/redis": "^4.3.3",
"@langchain/community": "^0.3.39",
"@langchain/core": "^0.3.43",
"@langchain/google-genai": "^0.2.2",
"@langchain/google-vertexai": "^0.2.3",
"@langchain/community": "^0.3.42",
"@langchain/core": "^0.3.55",
"@langchain/google-genai": "^0.2.8",
"@langchain/google-vertexai": "^0.2.8",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.22",
"@librechat/agents": "^2.4.317",
"@librechat/data-schemas": "*",
"@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.8.2",
@@ -90,7 +90,7 @@
"nanoid": "^3.3.7",
"nodemailer": "^6.9.15",
"ollama": "^0.5.0",
"openai": "^4.47.1",
"openai": "^4.96.2",
"openai-chat-tokens": "^0.2.8",
"openid-client": "^5.4.2",
"passport": "^0.6.0",
@@ -116,6 +116,6 @@
"jest": "^29.7.0",
"mongodb-memory-server": "^10.1.3",
"nodemon": "^3.0.3",
"supertest": "^7.0.0"
"supertest": "^7.1.0"
}
}

View File

@@ -228,7 +228,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
if (!client?.skipSaveUserMessage && latestUserMessage) {
await saveMessage(req, latestUserMessage, {
context: 'api/server/controllers/AskController.js - don\'t skip saving user message',
context: "api/server/controllers/AskController.js - don't skip saving user message",
});
}

View File

@@ -1,5 +1,4 @@
const { CacheKeys, AuthType } = require('librechat-data-provider');
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
const { getToolkitKey } = require('~/server/services/ToolService');
const { getCustomConfig } = require('~/server/services/Config');
const { availableTools } = require('~/app/clients/tools');
@@ -70,7 +69,7 @@ const getAvailablePluginsController = async (req, res) => {
);
}
let plugins = await addOpenAPISpecs(authenticatedPlugins);
let plugins = authenticatedPlugins;
if (includedTools.length > 0) {
plugins = plugins.filter((plugin) => includedTools.includes(plugin.pluginKey));
@@ -106,11 +105,11 @@ const getAvailableTools = async (req, res) => {
return;
}
const pluginManifest = availableTools;
let pluginManifest = availableTools;
const customConfig = await getCustomConfig();
if (customConfig?.mcpServers != null) {
const mcpManager = getMCPManager();
await mcpManager.loadManifestTools(pluginManifest);
pluginManifest = await mcpManager.loadManifestTools(pluginManifest);
}
/** @type {TPlugin[]} */

View File

@@ -14,15 +14,6 @@ const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { saveBase64Image } = require('~/server/services/Files/process');
const { logger, sendEvent } = require('~/config');
/** @typedef {import('@librechat/agents').Graph} Graph */
/** @typedef {import('@librechat/agents').EventHandler} EventHandler */
/** @typedef {import('@librechat/agents').ModelEndData} ModelEndData */
/** @typedef {import('@librechat/agents').ToolEndData} ToolEndData */
/** @typedef {import('@librechat/agents').ToolEndCallback} ToolEndCallback */
/** @typedef {import('@librechat/agents').ChatModelStreamHandler} ChatModelStreamHandler */
/** @typedef {import('@librechat/agents').ContentAggregatorResult['aggregateContent']} ContentAggregator */
/** @typedef {import('@librechat/agents').GraphEvents} GraphEvents */
class ModelEndHandler {
/**
* @param {Array<UsageMetadata>} collectedUsage
@@ -38,7 +29,7 @@ class ModelEndHandler {
* @param {string} event
* @param {ModelEndData | undefined} data
* @param {Record<string, unknown> | undefined} metadata
* @param {Graph} graph
* @param {StandardGraph} graph
* @returns
*/
handle(event, data, metadata, graph) {
@@ -61,7 +52,10 @@ class ModelEndHandler {
}
this.collectedUsage.push(usage);
if (!graph.clientOptions?.disableStreaming) {
const streamingDisabled = !!(
graph.clientOptions?.disableStreaming || graph?.boundModel?.disableStreaming
);
if (!streamingDisabled) {
return;
}
if (!data.output.content) {

View File

@@ -58,7 +58,7 @@ const payloadParser = ({ req, agent, endpoint }) => {
const legacyContentEndpoints = new Set([KnownEndpoints.groq, KnownEndpoints.deepseek]);
const noSystemModelRegex = [/\b(o\d)\b/gi];
const noSystemModelRegex = [/\b(o1-preview|o1-mini|amazon\.titan-text)\b/gi];
// const { processMemory, memoryInstructions } = require('~/server/services/Endpoints/agents/memory');
// const { getFormattedMemories } = require('~/models/Memory');
@@ -148,19 +148,13 @@ class AgentClient extends BaseClient {
* @param {MongoFile[]} attachments
*/
checkVisionRequest(attachments) {
logger.info(
'[api/server/controllers/agents/client.js #checkVisionRequest] not implemented',
attachments,
);
// if (!attachments) {
// return;
// }
// const availableModels = this.options.modelsConfig?.[this.options.endpoint];
// if (!availableModels) {
// return;
// }
// let visionRequestDetected = false;
// for (const file of attachments) {
// if (file?.type?.includes('image')) {
@@ -171,13 +165,11 @@ class AgentClient extends BaseClient {
// if (!visionRequestDetected) {
// return;
// }
// this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
// if (this.isVisionModel) {
// delete this.modelOptions.stop;
// return;
// }
// for (const model of availableModels) {
// if (!validateVisionModel({ model, availableModels })) {
// continue;
@@ -187,14 +179,12 @@ class AgentClient extends BaseClient {
// delete this.modelOptions.stop;
// return;
// }
// if (!availableModels.includes(this.defaultVisionModel)) {
// return;
// }
// if (!validateVisionModel({ model: this.defaultVisionModel, availableModels })) {
// return;
// }
// this.modelOptions.model = this.defaultVisionModel;
// this.isVisionModel = true;
// delete this.modelOptions.stop;
@@ -728,12 +718,14 @@ class AgentClient extends BaseClient {
}
if (noSystemMessages === true && systemContent?.length) {
let latestMessage = _messages.pop().content;
const latestMessageContent = _messages.pop().content;
if (typeof latestMessage !== 'string') {
latestMessage = latestMessage[0].text;
latestMessageContent[0].text = [systemContent, latestMessageContent[0].text].join('\n');
_messages.push(new HumanMessage({ content: latestMessageContent }));
} else {
const text = [systemContent, latestMessageContent].join('\n');
_messages.push(new HumanMessage(text));
}
latestMessage = [systemContent, latestMessage].join('\n');
_messages.push(new HumanMessage(latestMessage));
}
let messages = _messages;

View File

@@ -119,7 +119,7 @@ const chatV1 = async (req, res) => {
} else if (/Files.*are invalid/.test(error.message)) {
const errorMessage = `Files are invalid, or may not have uploaded yet.${
endpoint === EModelEndpoint.azureAssistants
? ' If using Azure OpenAI, files are only available in the region of the assistant\'s model at the time of upload.'
? " If using Azure OpenAI, files are only available in the region of the assistant's model at the time of upload."
: ''
}`;
return sendResponse(req, res, messageData, errorMessage);
@@ -379,8 +379,8 @@ const chatV1 = async (req, res) => {
body.additional_instructions ? `${body.additional_instructions}\n` : ''
}The user has uploaded ${imageCount} image${pluralized}.
Use the \`${ImageVisionTool.function.name}\` tool to retrieve ${
plural ? '' : 'a '
}detailed text description${pluralized} for ${plural ? 'each' : 'the'} image${pluralized}.`;
plural ? '' : 'a '
}detailed text description${pluralized} for ${plural ? 'each' : 'the'} image${pluralized}.`;
return files;
};
@@ -576,6 +576,8 @@ const chatV1 = async (req, res) => {
thread_id,
model: assistant_id,
endpoint,
spec: endpointOption.spec,
iconURL: endpointOption.iconURL,
};
sendMessage(res, {

View File

@@ -428,6 +428,8 @@ const chatV2 = async (req, res) => {
thread_id,
model: assistant_id,
endpoint,
spec: endpointOption.spec,
iconURL: endpointOption.iconURL,
};
sendMessage(res, {

View File

@@ -21,6 +21,7 @@ const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud');
const { getFiles, batchUpdateFiles } = require('~/models/File');
const { getAssistant } = require('~/models/Assistant');
const { getAgent } = require('~/models/Agent');
const { getLogStores } = require('~/cache');
const { logger } = require('~/config');
@@ -94,7 +95,7 @@ router.delete('/', async (req, res) => {
});
}
/* Handle entity unlinking even if no valid files to delete */
/* Handle agent unlinking even if no valid files to delete */
if (req.body.agent_id && req.body.tool_resource && dbFiles.length === 0) {
const agent = await getAgent({
id: req.body.agent_id,
@@ -104,7 +105,21 @@ router.delete('/', async (req, res) => {
const agentFiles = files.filter((f) => toolResourceFiles.includes(f.file_id));
await processDeleteRequest({ req, files: agentFiles });
res.status(200).json({ message: 'File associations removed successfully' });
res.status(200).json({ message: 'File associations removed successfully from agent' });
return;
}
/* Handle assistant unlinking even if no valid files to delete */
if (req.body.assistant_id && req.body.tool_resource && dbFiles.length === 0) {
const assistant = await getAssistant({
id: req.body.assistant_id,
});
const toolResourceFiles = assistant.tool_resources?.[req.body.tool_resource]?.file_ids ?? [];
const assistantFiles = files.filter((f) => toolResourceFiles.includes(f.file_id));
await processDeleteRequest({ req, files: assistantFiles });
res.status(200).json({ message: 'File associations removed successfully from assistant' });
return;
}

View File

@@ -56,7 +56,7 @@ const logoutUser = async (req, refreshToken) => {
try {
req.session.destroy();
} catch (destroyErr) {
logger.error('[logoutUser] Failed to destroy session.', destroyErr);
logger.debug('[logoutUser] Failed to destroy session.', destroyErr);
}
return { status: 200, message: 'Logout successful' };

View File

@@ -233,6 +233,13 @@ const initializeAgentOptions = async ({
endpointOption: _endpointOption,
});
if (
agent.endpoint === EModelEndpoint.azureOpenAI &&
options.llmConfig?.azureOpenAIApiInstanceName == null
) {
agent.provider = Providers.OPENAI;
}
if (options.provider != null) {
agent.provider = options.provider;
}

View File

@@ -3,7 +3,6 @@ const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getAssistant } = require('~/models/Assistant');
const buildOptions = async (endpoint, parsedBody) => {
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
parsedBody;
const endpointOption = removeNullishValues({

View File

@@ -136,7 +136,7 @@ function getLLMConfig(apiKey, options = {}, endpoint = null) {
Object.assign(llmConfig, azure);
llmConfig.model = llmConfig.azureOpenAIApiDeploymentName;
} else {
llmConfig.openAIApiKey = apiKey;
llmConfig.apiKey = apiKey;
// Object.assign(llmConfig, {
// configuration: { apiKey },
// });

View File

@@ -132,6 +132,8 @@ async function saveUserMessage(req, params) {
* @param {string} params.endpoint - The conversation endpoint
* @param {string} params.parentMessageId - The latest user message that triggered this response.
* @param {string} [params.instructions] - Optional: from preset for `instructions` field.
* @param {string} [params.spec] - Optional: Model spec identifier.
* @param {string} [params.iconURL]
* Overrides the instructions of the assistant.
* @param {string} [params.promptPrefix] - Optional: from preset for `additional_instructions` field.
* @return {Promise<Run>} A promise that resolves to the created run object.
@@ -154,6 +156,8 @@ async function saveAssistantMessage(req, params) {
text: params.text,
unfinished: false,
// tokenCount,
iconURL: params.iconURL,
spec: params.spec,
});
await saveConvo(
@@ -165,6 +169,8 @@ async function saveAssistantMessage(req, params) {
instructions: params.instructions,
assistant_id: params.assistant_id,
model: params.model,
iconURL: params.iconURL,
spec: params.spec,
},
{ context: 'api/server/services/Threads/manage.js #saveAssistantMessage' },
);

View File

@@ -17,12 +17,15 @@ try {
}
/**
* Downloads an image from a URL using an access token.
* @param {string} url
* @param {string} accessToken
* @returns {Promise<Buffer>}
* Downloads an image from a URL using an access token, returning a Buffer.
*
* @async
* @function downloadImage
* @param {string} url - The image URL
* @param {string} accessToken - The OAuth2 access token, if required by the server
* @returns {Promise<Buffer|string>} A Buffer if successful, or an empty string on failure
*/
const downloadImage = async (url, accessToken) => {
async function downloadImage(url, accessToken) {
if (!url) {
return '';
}
@@ -30,34 +33,33 @@ const downloadImage = async (url, accessToken) => {
try {
const options = {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
headers: { Authorization: `Bearer ${accessToken}` },
};
if (process.env.PROXY) {
options.agent = new HttpsProxyAgent(process.env.PROXY);
}
const response = await fetch(url, options);
if (response.ok) {
const buffer = await response.buffer();
return buffer;
} else {
if (!response.ok) {
throw new Error(`${response.statusText} (HTTP ${response.status})`);
}
return await response.buffer();
} catch (error) {
logger.error(
`[openidStrategy] downloadImage: Error downloading image at URL "${url}": ${error}`,
);
logger.error(`[openidStrategy] downloadImage: Failed to fetch "${url}": ${error}`);
return '';
}
};
}
/**
* Determines the full name of a user based on OpenID userinfo and environment configuration.
* Derives a user's "full name" from userinfo or environment-specified claim.
*
* Priority:
* 1) process.env.OPENID_NAME_CLAIM
* 2) userinfo.given_name + userinfo.family_name
* 3) userinfo.given_name OR userinfo.family_name
* 4) userinfo.username or userinfo.email
*
* @function getFullName
* @param {Object} userinfo - The user information object from OpenID Connect
* @param {string} [userinfo.given_name] - The user's first name
* @param {string} [userinfo.family_name] - The user's last name
@@ -66,153 +68,252 @@ const downloadImage = async (url, accessToken) => {
* @returns {string} The determined full name of the user
*/
function getFullName(userinfo) {
if (process.env.OPENID_NAME_CLAIM) {
if (process.env.OPENID_NAME_CLAIM && userinfo[process.env.OPENID_NAME_CLAIM]) {
return userinfo[process.env.OPENID_NAME_CLAIM];
}
if (userinfo.given_name && userinfo.family_name) {
return `${userinfo.given_name} ${userinfo.family_name}`;
}
if (userinfo.given_name) {
return userinfo.given_name;
}
if (userinfo.family_name) {
return userinfo.family_name;
}
return userinfo.username || userinfo.email;
return userinfo.username || userinfo.email || '';
}
/**
* Converts an input into a string suitable for a username.
* If the input is a string, it will be returned as is.
* If the input is an array, elements will be joined with underscores.
* In case of undefined or other falsy values, a default value will be returned.
*
* @param {string | string[] | undefined} input - The input value to be converted into a username.
* @param {string} [defaultValue=''] - The default value to return if the input is falsy.
* @returns {string} The processed input as a string suitable for a username.
* @function convertToUsername
* @param {string|string[]|undefined} input - Could be a string or array of strings
* @param {string} [defaultValue=''] - Fallback if input is invalid or not provided
* @returns {string} A processed username string
*/
function convertToUsername(input, defaultValue = '') {
if (typeof input === 'string') {
return input;
} else if (Array.isArray(input)) {
}
if (Array.isArray(input)) {
return input.join('_');
}
return defaultValue;
}
/**
* Safely extracts an array of roles from an object using dot notation (e.g. realm_access.roles).
*
* @function extractRolesFrom
* @param {Object} obj
* @param {string} path
* @returns {string[]} Array of roles, or empty array if not found
*/
function extractRolesFrom(obj, path) {
try {
let current = obj;
for (const part of path.split('.')) {
if (!current || typeof current !== 'object') {
return [];
}
current = current[part];
}
return Array.isArray(current) ? current : [];
} catch {
return [];
}
}
/**
* Retrieves user roles from either a token, the userinfo object, or both.
*
* Supports three strategies based on the roleSource:
* - 'token': Extract roles from the token (access or id token), fallback to userinfo if extraction fails.
* - 'userinfo': Extract roles solely from the userinfo object.
* - 'both': Extract roles from both token and userinfo and merge them.
*
* Also supports encrypted tokens by falling back to userinfo if the token is not JWT-decodable.
*
* @function getUserRoles
* @param {import('openid-client').TokenSet} tokenSet
* @param {Object} userinfo
* @param {string} rolePath - Dot-notation path to where roles are stored
* @param {'access'|'id'} tokenKind - Which token to parse for roles
* @param {'token'|'userinfo'|'both'} roleSource - Source of roles for extraction
* @returns {string[]} Array of roles, possibly empty
*/
function getUserRoles(tokenSet, userinfo, rolePath, tokenKind, roleSource) {
if (!tokenSet) {
return extractRolesFrom(userinfo, rolePath);
}
if (roleSource === 'userinfo') {
const roles = extractRolesFrom(userinfo, rolePath);
if (!roles.length) {
logger.warn(`[openidStrategy] Key '${rolePath}' not found in userinfo.`);
}
return roles;
} else if (roleSource === 'both') {
let tokenRoles = [];
try {
let tokenToDecode = tokenKind === 'access' ? tokenSet.access_token : tokenSet.id_token;
if (tokenToDecode && tokenToDecode.includes('.')) {
const tokenData = jwtDecode(tokenToDecode);
tokenRoles = extractRolesFrom(tokenData, rolePath);
} else {
logger.warn(
'[openidStrategy] Token is not a valid JWT for decoding, skipping token roles extraction.',
);
}
} catch (err) {
logger.error(`[openidStrategy] Failed to decode ${tokenKind} token: ${err}.`);
}
const userinfoRoles = extractRolesFrom(userinfo, rolePath);
const combinedRoles = Array.from(new Set([...tokenRoles, ...userinfoRoles]));
if (!combinedRoles.length) {
logger.warn(`[openidStrategy] Key '${rolePath}' not found in both token and userinfo.`);
}
return combinedRoles;
} else {
// default 'token' strategy
try {
let tokenToDecode = tokenKind === 'access' ? tokenSet.access_token : tokenSet.id_token;
if (!tokenToDecode || !tokenToDecode.includes('.')) {
throw new Error('Token is not a valid JWT for decoding.');
}
const tokenData = jwtDecode(tokenToDecode);
const roles = extractRolesFrom(tokenData, rolePath);
if (!roles.length) {
logger.warn(
`[openidStrategy] Key '${rolePath}' not found in ${tokenKind} token. Falling back to userinfo.`,
);
return extractRolesFrom(userinfo, rolePath);
}
return roles;
} catch (err) {
logger.error(`[openidStrategy] ${err}. Falling back to userinfo for role extraction.`);
return extractRolesFrom(userinfo, rolePath);
}
}
}
/**
* Registers and configures the OpenID Connect strategy with Passport, enabling PKCE when toggled.
*
* @async
* @function setupOpenId
* @returns {Promise<void>}
*/
async function setupOpenId() {
try {
// Set up a proxy if specified
if (process.env.PROXY) {
const proxyAgent = new HttpsProxyAgent(process.env.PROXY);
custom.setHttpOptionsDefaults({
agent: proxyAgent,
});
logger.info(`[openidStrategy] proxy agent added: ${process.env.PROXY}`);
custom.setHttpOptionsDefaults({ agent: proxyAgent });
logger.info(`[openidStrategy] Using proxy: ${process.env.PROXY}`);
}
// Discover issuer configuration
const issuer = await Issuer.discover(process.env.OPENID_ISSUER);
/* Supported Algorithms, openid-client v5 doesn't set it automatically as discovered from server.
- id_token_signed_response_alg // defaults to 'RS256'
- request_object_signing_alg // defaults to 'RS256'
- userinfo_signed_response_alg // not in v5
- introspection_signed_response_alg // not in v5
- authorization_signed_response_alg // not in v5
*/
logger.info(`[openidStrategy] Discovered issuer: ${issuer.issuer}`);
/**
* Supported Algorithms, openid-client v5 doesn't set it automatically as discovered from server.
* - id_token_signed_response_alg // defaults to 'RS256'
* - request_object_signing_alg // defaults to 'RS256'
* - userinfo_signed_response_alg // not in v5
* - introspection_signed_response_alg // not in v5
* - authorization_signed_response_alg // not in v5
*/
/** @type {import('openid-client').ClientMetadata} */
const clientMetadata = {
client_id: process.env.OPENID_CLIENT_ID,
client_secret: process.env.OPENID_CLIENT_SECRET,
client_secret: process.env.OPENID_CLIENT_SECRET || '',
redirect_uris: [process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL],
};
// Optionally force the first supported signing algorithm
if (isEnabled(process.env.OPENID_SET_FIRST_SUPPORTED_ALGORITHM)) {
clientMetadata.id_token_signed_response_alg =
issuer.id_token_signing_alg_values_supported?.[0] || 'RS256';
}
const client = new issuer.Client(clientMetadata);
// Determine whether to enable PKCE
const usePKCE = process.env.OPENID_USE_PKCE === 'true';
// Set up authorization parameters. Include code_challenge_method if PKCE is enabled.
const openidScope = process.env.OPENID_SCOPE || 'openid profile email';
/** @type {import('openid-client').AuthorizationParameters} */
const params = {
scope: openidScope,
response_type: 'code',
};
if (usePKCE) {
params.code_challenge_method = 'S256'; // Enable PKCE by specifying the code challenge method
}
// Role-based config
const requiredRole = process.env.OPENID_REQUIRED_ROLE;
const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND;
const openidLogin = new OpenIDStrategy(
const rolePath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
const tokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND || 'id'; // 'id'|'access'
const roleSource = process.env.OPENID_REQUIRED_ROLE_SOURCE || 'both'; // 'token'|'userinfo'|'both'
// Create the Passport strategy using the new type-correct instantiation and toggle for PKCE
const openidStrategy = new OpenIDStrategy(
{
client,
params: {
scope: process.env.OPENID_SCOPE,
},
params,
usePKCE,
},
async (tokenset, userinfo, done) => {
async (tokenSet, userinfo, done) => {
try {
logger.info(`[openidStrategy] verify login openidId: ${userinfo.sub}`);
logger.debug('[openidStrategy] very login tokenset and userinfo', { tokenset, userinfo });
logger.info(`[openidStrategy] Verifying login for sub=${userinfo.sub}`);
// Find user by openidId or fallback to email
let user = await findUser({ openidId: userinfo.sub });
logger.info(
`[openidStrategy] user ${user ? 'found' : 'not found'} with openidId: ${userinfo.sub}`,
);
if (!user) {
if (!user && userinfo.email) {
user = await findUser({ email: userinfo.email });
logger.info(
`[openidStrategy] user ${user ? 'found' : 'not found'} with email: ${
userinfo.email
} for openidId: ${userinfo.sub}`,
`[openidStrategy] User ${user ? 'found' : 'not found'} by email=${userinfo.email}.`,
);
}
const fullName = getFullName(userinfo);
if (requiredRole) {
let decodedToken = '';
if (requiredRoleTokenKind === 'access') {
decodedToken = jwtDecode(tokenset.access_token);
} else if (requiredRoleTokenKind === 'id') {
decodedToken = jwtDecode(tokenset.id_token);
}
const pathParts = requiredRoleParameterPath.split('.');
let found = true;
let roles = pathParts.reduce((o, key) => {
if (o === null || o === undefined || !(key in o)) {
found = false;
return [];
}
return o[key];
}, decodedToken);
if (!found) {
logger.error(
`[openidStrategy] Key '${requiredRoleParameterPath}' not found in ${requiredRoleTokenKind} token!`,
);
}
// If a role is required, check user roles
if (requiredRole && rolePath) {
const roles = getUserRoles(tokenSet, userinfo, rolePath, tokenKind, roleSource);
if (!roles.includes(requiredRole)) {
logger.warn(
`[openidStrategy] Missing required role "${requiredRole}". Roles: [${roles.join(', ')}]`,
);
return done(null, false, {
message: `You must have the "${requiredRole}" role to log in.`,
});
}
}
let username = '';
if (process.env.OPENID_USERNAME_CLAIM) {
username = userinfo[process.env.OPENID_USERNAME_CLAIM];
} else {
username = convertToUsername(
userinfo.username || userinfo.given_name || userinfo.email,
);
}
// Derive name and username
const fullName = getFullName(userinfo);
const username = process.env.OPENID_USERNAME_CLAIM
? convertToUsername(userinfo[process.env.OPENID_USERNAME_CLAIM])
: convertToUsername(userinfo.username || userinfo.given_name || userinfo.email);
// Create or update user
if (!user) {
user = {
provider: 'openid',
openidId: userinfo.sub,
username,
email: userinfo.email || '',
emailVerified: userinfo.email_verified || false,
name: fullName,
};
user = await createUser(user, true, true);
logger.info(`[openidStrategy] Creating a new user for sub=${userinfo.sub}`);
user = await createUser(
{
provider: 'openid',
openidId: userinfo.sub,
username,
email: userinfo.email || '',
emailVerified: Boolean(userinfo.email_verified) || false,
name: fullName,
},
true,
true,
);
} else {
user.provider = 'openid';
user.openidId = userinfo.sub;
@@ -220,54 +321,44 @@ async function setupOpenId() {
user.name = fullName;
}
if (userinfo.picture && !user.avatar?.includes('manual=true')) {
/** @type {string | undefined} */
const imageUrl = userinfo.picture;
let fileName;
if (crypto) {
fileName = (await hashToken(userinfo.sub)) + '.png';
} else {
fileName = userinfo.sub + '.png';
}
const imageBuffer = await downloadImage(imageUrl, tokenset.access_token);
// Fetch avatar if not manually overridden
if (userinfo.picture && !String(user.avatar || '').includes('manual=true')) {
const imageBuffer = await downloadImage(userinfo.picture, tokenSet.access_token);
if (imageBuffer) {
const { saveBuffer } = getStrategyFunctions(process.env.CDN_PROVIDER);
const fileHash = crypto ? await hashToken(userinfo.sub) : userinfo.sub;
const fileName = `${fileHash}.png`;
const imagePath = await saveBuffer({
fileName,
userId: user._id.toString(),
buffer: imageBuffer,
});
user.avatar = imagePath ?? '';
if (imagePath) {
user.avatar = imagePath;
}
}
}
// Persist user changes
user = await updateUser(user._id, user);
// Success
logger.info(
`[openidStrategy] login success openidId: ${user.openidId} | email: ${user.email} | username: ${user.username} `,
{
user: {
openidId: user.openidId,
username: user.username,
email: user.email,
name: user.name,
},
},
`[openidStrategy] Login success for sub=${user.openidId}, email=${user.email}, username=${user.username}`,
);
done(null, user);
return done(null, user);
} catch (err) {
logger.error('[openidStrategy] login failed', err);
done(err);
logger.error('[openidStrategy] Login verification failed:', err);
return done(err);
}
},
);
passport.use('openid', openidLogin);
// Register the strategy under the 'openid' name
passport.use('openid', openidStrategy);
} catch (err) {
logger.error('[openidStrategy]', err);
logger.error('[openidStrategy] Error setting up OpenID strategy:', err);
}
}

View File

@@ -10,7 +10,6 @@ jest.mock('openid-client');
jest.mock('jsonwebtoken/decode');
jest.mock('~/server/services/Files/strategies', () => ({
getStrategyFunctions: jest.fn(() => ({
// You can modify this mock as needed (here returning a dummy function)
saveBuffer: jest.fn().mockResolvedValue('/fake/path/to/avatar.png'),
})),
}));
@@ -23,18 +22,20 @@ jest.mock('~/server/utils/crypto', () => ({
hashToken: jest.fn().mockResolvedValue('hashed-token'),
}));
jest.mock('~/server/utils', () => ({
isEnabled: jest.fn(() => false), // default to false, override per test if needed
isEnabled: jest.fn(() => false), // default to false; override per test if needed
}));
jest.mock('~/config', () => ({
logger: {
info: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
},
}));
// Mock Issuer.discover so that setupOpenId gets a fake issuer and client
// Update Issuer.discover mock so that the returned issuer has an 'issuer' property.
Issuer.discover = jest.fn().mockResolvedValue({
issuer: 'https://fake-issuer.com',
id_token_signing_alg_values_supported: ['RS256'],
Client: jest.fn().mockImplementation((clientMetadata) => {
return {
@@ -43,7 +44,7 @@ Issuer.discover = jest.fn().mockResolvedValue({
}),
});
// To capture the verify callback from the strategy, we grab it from the mock constructor
// To capture the verify callback from the strategy, we grab it from the mock constructor.
let verifyCallback;
OpenIDStrategy.mockImplementation((options, verify) => {
verifyCallback = verify;
@@ -51,21 +52,21 @@ OpenIDStrategy.mockImplementation((options, verify) => {
});
describe('setupOpenId', () => {
// Helper to wrap the verify callback in a promise
// Helper to wrap the verify callback in a promise.
const validate = (tokenset, userinfo) =>
new Promise((resolve, reject) => {
verifyCallback(tokenset, userinfo, (err, user, details) => {
if (err) {
reject(err);
} else {
resolve({ user, details });
return reject(err);
}
resolve({ user, details });
});
});
const tokenset = {
id_token: 'fake_id_token',
access_token: 'fake_access_token',
// Default tokenset: tokens include a period to simulate a JWT.
const validTokenSet = {
id_token: 'header.payload.signature',
access_token: 'header.payload.signature',
};
const baseUserinfo = {
@@ -77,13 +78,14 @@ describe('setupOpenId', () => {
name: 'My Full',
username: 'flast',
picture: 'https://example.com/avatar.png',
roles: ['requiredRole'],
};
beforeEach(async () => {
// Clear previous mock calls and reset implementations
// Clear previous mock calls and reset implementations.
jest.clearAllMocks();
// Reset environment variables needed by the strategy
// Reset environment variables needed by the strategy.
process.env.OPENID_ISSUER = 'https://fake-issuer.com';
process.env.OPENID_CLIENT_ID = 'fake_client_id';
process.env.OPENID_CLIENT_SECRET = 'fake_client_secret';
@@ -93,26 +95,29 @@ describe('setupOpenId', () => {
process.env.OPENID_REQUIRED_ROLE = 'requiredRole';
process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH = 'roles';
process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'id';
process.env.OPENID_REQUIRED_ROLE_SOURCE = 'token';
delete process.env.OPENID_USERNAME_CLAIM;
delete process.env.OPENID_NAME_CLAIM;
delete process.env.PROXY;
delete process.env.OPENID_USE_PKCE;
delete process.env.OPENID_SET_FIRST_SUPPORTED_ALGORITHM;
// Default jwtDecode mock returns a token that includes the required role.
// By default, jwtDecode returns a token that includes the required role.
jwtDecode.mockReturnValue({
roles: ['requiredRole'],
});
// By default, assume that no user is found, so createUser will be called
// By default, assume that no user is found so that createUser will be called.
findUser.mockResolvedValue(null);
createUser.mockImplementation(async (userData) => {
// simulate created user with an _id property
// Simulate created user with an _id property.
return { _id: 'newUserId', ...userData };
});
updateUser.mockImplementation(async (id, userData) => {
return { _id: id, ...userData };
});
// For image download, simulate a successful response
// For image download, simulate a successful response.
const fakeBuffer = Buffer.from('fake image');
const fakeResponse = {
ok: true,
@@ -120,18 +125,13 @@ describe('setupOpenId', () => {
};
fetch.mockResolvedValue(fakeResponse);
// Finally, call the setup function so that passport.use gets called
// (Re)initialize the strategy with current env settings.
await setupOpenId();
});
it('should create a new user with correct username when username claim exists', async () => {
// Arrange our userinfo already has username 'flast'
const userinfo = { ...baseUserinfo };
// Act
const { user } = await validate(tokenset, userinfo);
// Assert
const { user } = await validate(validTokenSet, userinfo);
expect(user.username).toBe(userinfo.username);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({
@@ -147,16 +147,10 @@ describe('setupOpenId', () => {
});
it('should use given_name as username when username claim is missing', async () => {
// Arrange remove username from userinfo
const userinfo = { ...baseUserinfo };
delete userinfo.username;
// Expect the username to be the given name (unchanged case)
const expectUsername = userinfo.given_name;
// Act
const { user } = await validate(tokenset, userinfo);
// Assert
const { user } = await validate(validTokenSet, userinfo);
expect(user.username).toBe(expectUsername);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({ username: expectUsername }),
@@ -166,16 +160,11 @@ describe('setupOpenId', () => {
});
it('should use email as username when username and given_name are missing', async () => {
// Arrange remove username and given_name
const userinfo = { ...baseUserinfo };
delete userinfo.username;
delete userinfo.given_name;
const expectUsername = userinfo.email;
// Act
const { user } = await validate(tokenset, userinfo);
// Assert
const { user } = await validate(validTokenSet, userinfo);
expect(user.username).toBe(expectUsername);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({ username: expectUsername }),
@@ -185,14 +174,10 @@ describe('setupOpenId', () => {
});
it('should override username with OPENID_USERNAME_CLAIM when set', async () => {
// Arrange set OPENID_USERNAME_CLAIM so that the sub claim is used
process.env.OPENID_USERNAME_CLAIM = 'sub';
const userinfo = { ...baseUserinfo };
// Act
const { user } = await validate(tokenset, userinfo);
// Assert username should equal the sub (converted as-is)
await setupOpenId();
const { user } = await validate(validTokenSet, userinfo);
expect(user.username).toBe(userinfo.sub);
expect(createUser).toHaveBeenCalledWith(
expect.objectContaining({ username: userinfo.sub }),
@@ -202,31 +187,21 @@ describe('setupOpenId', () => {
});
it('should set the full name correctly when given_name and family_name exist', async () => {
// Arrange
const userinfo = { ...baseUserinfo };
const expectedFullName = `${userinfo.given_name} ${userinfo.family_name}`;
// Act
const { user } = await validate(tokenset, userinfo);
// Assert
const { user } = await validate(validTokenSet, userinfo);
expect(user.name).toBe(expectedFullName);
});
it('should override full name with OPENID_NAME_CLAIM when set', async () => {
// Arrange use the name claim as the full name
process.env.OPENID_NAME_CLAIM = 'name';
const userinfo = { ...baseUserinfo, name: 'Custom Name' };
// Act
const { user } = await validate(tokenset, userinfo);
// Assert
await setupOpenId();
const { user } = await validate(validTokenSet, userinfo);
expect(user.name).toBe('Custom Name');
});
it('should update an existing user on login', async () => {
// Arrange simulate that a user already exists
const existingUser = {
_id: 'existingUserId',
provider: 'local',
@@ -241,13 +216,8 @@ describe('setupOpenId', () => {
}
return null;
});
const userinfo = { ...baseUserinfo };
// Act
await validate(tokenset, userinfo);
// Assert updateUser should be called and the user object updated
await validate(validTokenSet, userinfo);
expect(updateUser).toHaveBeenCalledWith(
existingUser._id,
expect.objectContaining({
@@ -260,43 +230,154 @@ describe('setupOpenId', () => {
});
it('should enforce the required role and reject login if missing', async () => {
// Arrange simulate a token without the required role.
jwtDecode.mockReturnValue({
roles: ['SomeOtherRole'],
});
jwtDecode.mockReturnValue({ roles: ['SomeOtherRole'] });
const userinfo = { ...baseUserinfo };
// Act
const { user, details } = await validate(tokenset, userinfo);
// Assert verify that the strategy rejects login
const { user, details } = await validate(validTokenSet, userinfo);
expect(user).toBe(false);
expect(details.message).toBe('You must have the "requiredRole" role to log in.');
});
it('should attempt to download and save the avatar if picture is provided', async () => {
// Arrange ensure userinfo contains a picture URL
const userinfo = { ...baseUserinfo };
// Act
const { user } = await validate(tokenset, userinfo);
// Assert verify that download was attempted and the avatar field was set via updateUser
const { user } = await validate(validTokenSet, userinfo);
expect(fetch).toHaveBeenCalled();
// Our mock getStrategyFunctions.saveBuffer returns '/fake/path/to/avatar.png'
expect(user.avatar).toBe('/fake/path/to/avatar.png');
});
it('should not attempt to download avatar if picture is not provided', async () => {
// Arrange remove picture
const userinfo = { ...baseUserinfo };
delete userinfo.picture;
// Act
await validate(tokenset, userinfo);
// Assert fetch should not be called and avatar should remain undefined or empty
await validate(validTokenSet, userinfo);
expect(fetch).not.toHaveBeenCalled();
// Depending on your implementation, user.avatar may be undefined or an empty string.
});
it('should fallback to userinfo roles if the id_token is invalid (missing a period)', async () => {
const invalidTokenSet = { ...validTokenSet, id_token: 'invalidtoken' };
const userinfo = { ...baseUserinfo, roles: ['requiredRole'] };
const { user } = await validate(invalidTokenSet, userinfo);
expect(user).toBeDefined();
expect(createUser).toHaveBeenCalled();
});
it('should handle downloadImage failure gracefully and not set an avatar', async () => {
fetch.mockRejectedValue(new Error('network error'));
const userinfo = { ...baseUserinfo };
const { user } = await validate(validTokenSet, userinfo);
expect(fetch).toHaveBeenCalled();
expect(user.avatar).toBeUndefined();
});
it('should allow login if no required role is specified', async () => {
delete process.env.OPENID_REQUIRED_ROLE;
delete process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
jwtDecode.mockReturnValue({});
const userinfo = { ...baseUserinfo };
const { user } = await validate(validTokenSet, userinfo);
expect(user).toBeDefined();
expect(createUser).toHaveBeenCalled();
});
it('should use roles from userinfo when OPENID_REQUIRED_ROLE_SOURCE is set to "userinfo"', async () => {
process.env.OPENID_REQUIRED_ROLE_SOURCE = 'userinfo';
jwtDecode.mockReturnValue({});
const userinfo = { ...baseUserinfo, roles: ['requiredRole'] };
await setupOpenId();
const { user } = await validate(validTokenSet, userinfo);
expect(user).toBeDefined();
expect(createUser).toHaveBeenCalled();
});
it('should merge roles from both token and userinfo when OPENID_REQUIRED_ROLE_SOURCE is "both"', async () => {
process.env.OPENID_REQUIRED_ROLE_SOURCE = 'both';
jwtDecode.mockReturnValue({ roles: ['extraRole'] });
const userinfo = { ...baseUserinfo, roles: ['requiredRole'] };
await setupOpenId();
const { user } = await validate(validTokenSet, userinfo);
expect(user).toBeDefined();
expect(createUser).toHaveBeenCalled();
});
it('should fall back to userinfo roles when token decode fails and roleSource is "both"', async () => {
process.env.OPENID_REQUIRED_ROLE_SOURCE = 'both';
jwtDecode.mockImplementation(() => {
throw new Error('Decode error');
});
const userinfo = { ...baseUserinfo, roles: ['requiredRole'] };
await setupOpenId();
const { user } = await validate(validTokenSet, userinfo);
expect(user).toBeDefined();
expect(createUser).toHaveBeenCalled();
});
it('should merge roles from both token and userinfo when token is invalid and roleSource is "both"', async () => {
process.env.OPENID_REQUIRED_ROLE_SOURCE = 'both';
const invalidTokenSet = { ...validTokenSet, id_token: 'invalidtoken' };
const userinfo = { ...baseUserinfo, roles: ['requiredRole'] };
await setupOpenId();
const { user } = await validate(invalidTokenSet, userinfo);
expect(user).toBeDefined();
expect(createUser).toHaveBeenCalled();
});
it('should reject login if merged roles from both token and userinfo do not include required role', async () => {
process.env.OPENID_REQUIRED_ROLE_SOURCE = 'both';
jwtDecode.mockReturnValue({ roles: ['SomeOtherRole'] });
const userinfo = { ...baseUserinfo, roles: ['AnotherRole'] };
await setupOpenId();
const { user, details } = await validate(validTokenSet, userinfo);
expect(user).toBe(false);
expect(details.message).toBe('You must have the "requiredRole" role to log in.');
});
it('should pass usePKCE true and set code_challenge_method in params when OPENID_USE_PKCE is "true"', async () => {
process.env.OPENID_USE_PKCE = 'true';
await setupOpenId();
const callOptions = OpenIDStrategy.mock.calls[OpenIDStrategy.mock.calls.length - 1][0];
expect(callOptions.usePKCE).toBe(true);
expect(callOptions.params.code_challenge_method).toBe('S256');
});
it('should pass usePKCE false and not set code_challenge_method in params when OPENID_USE_PKCE is "false"', async () => {
process.env.OPENID_USE_PKCE = 'false';
await setupOpenId();
const callOptions = OpenIDStrategy.mock.calls[OpenIDStrategy.mock.calls.length - 1][0];
expect(callOptions.usePKCE).toBe(false);
expect(callOptions.params.code_challenge_method).toBeUndefined();
});
it('should default to usePKCE false when OPENID_USE_PKCE is not defined', async () => {
delete process.env.OPENID_USE_PKCE;
await setupOpenId();
const callOptions = OpenIDStrategy.mock.calls[OpenIDStrategy.mock.calls.length - 1][0];
expect(callOptions.usePKCE).toBe(false);
expect(callOptions.params.code_challenge_method).toBeUndefined();
});
it('should set id_token_signed_response_alg if OPENID_SET_FIRST_SUPPORTED_ALGORITHM is enabled', async () => {
process.env.OPENID_SET_FIRST_SUPPORTED_ALGORITHM = 'true';
// Override isEnabled so that it returns true.
const { isEnabled } = require('~/server/utils');
isEnabled.mockReturnValue(true);
await setupOpenId();
const callOptions = OpenIDStrategy.mock.calls[OpenIDStrategy.mock.calls.length - 1][0];
expect(callOptions.client.metadata.id_token_signed_response_alg).toBe('RS256');
});
it('should use access token when OPENID_REQUIRED_ROLE_TOKEN_KIND is set to "access"', async () => {
process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND = 'access';
// Reinitialize strategy so that the new token kind is used.
await setupOpenId();
jwtDecode.mockClear();
jwtDecode.mockReturnValue({ roles: ['requiredRole'] });
const userinfo = { ...baseUserinfo };
await validate(validTokenSet, userinfo);
expect(jwtDecode).toHaveBeenCalledWith(validTokenSet.access_token);
});
it('should use proxy agent if PROXY is provided', async () => {
process.env.PROXY = 'http://fake-proxy.com';
await setupOpenId();
const { logger } = require('~/config');
expect(logger.info).toHaveBeenCalledWith(`[openidStrategy] Using proxy: ${process.env.PROXY}`);
});
});

View File

@@ -43,6 +43,60 @@
* @memberof typedefs
*/
/**
* @exports Graph
* @typedef {import('@librechat/agents').Graph} Graph
* @memberof typedefs
*/
/**
* @exports StandardGraph
* @typedef {import('@librechat/agents').StandardGraph} StandardGraph
* @memberof typedefs
*/
/**
* @exports EventHandler
* @typedef {import('@librechat/agents').EventHandler} EventHandler
* @memberof typedefs
*/
/**
* @exports ModelEndData
* @typedef {import('@librechat/agents').ModelEndData} ModelEndData
* @memberof typedefs
*/
/**
* @exports ToolEndData
* @typedef {import('@librechat/agents').ToolEndData} ToolEndData
* @memberof typedefs
*/
/**
* @exports ToolEndCallback
* @typedef {import('@librechat/agents').ToolEndCallback} ToolEndCallback
* @memberof typedefs
*/
/**
* @exports ChatModelStreamHandler
* @typedef {import('@librechat/agents').ChatModelStreamHandler} ChatModelStreamHandler
* @memberof typedefs
*/
/**
* @exports ContentAggregator
* @typedef {import('@librechat/agents').ContentAggregatorResult['aggregateContent']} ContentAggregator
* @memberof typedefs
*/
/**
* @exports GraphEvents
* @typedef {import('@librechat/agents').GraphEvents} GraphEvents
* @memberof typedefs
*/
/**
* @exports AgentRun
* @typedef {import('@librechat/agents').Run} AgentRun
@@ -97,12 +151,6 @@
* @memberof typedefs
*/
/**
* @exports ToolEndData
* @typedef {import('@librechat/agents').ToolEndData} ToolEndData
* @memberof typedefs
*/
/**
* @exports BaseMessage
* @typedef {import('@langchain/core/messages').BaseMessage} BaseMessage

View File

@@ -60,10 +60,16 @@ const cohereModels = {
const googleModels = {
/* Max I/O is combined so we subtract the amount from max response tokens for actual total */
gemma: 8196,
'gemma-2': 32768,
'gemma-3': 32768,
'gemma-3-27b': 131072,
gemini: 30720, // -2048 from max
'gemini-pro-vision': 12288,
'gemini-exp': 2000000,
'gemini-2.5': 1000000, // 1M input tokens, 64k output tokens
'gemini-2.5-pro': 1000000,
'gemini-2.5-flash': 1000000,
'gemini-2.0': 2000000,
'gemini-2.0-flash': 1000000,
'gemini-2.0-flash-lite': 1000000,
@@ -235,12 +241,15 @@ const modelMaxOutputs = {
system_default: 1024,
};
/** Outputs from https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-names */
const anthropicMaxOutputs = {
'claude-3-haiku': 4096,
'claude-3-sonnet': 4096,
'claude-3-opus': 4096,
'claude-3.5-sonnet': 8192,
'claude-3-5-sonnet': 8192,
'claude-3.7-sonnet': 128000,
'claude-3-7-sonnet': 128000,
};
const maxOutputTokensMap = {

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/frontend",
"version": "v0.7.8-rc1",
"version": "v0.7.8",
"description": "",
"type": "module",
"scripts": {
@@ -141,7 +141,7 @@
"tailwindcss": "^3.4.1",
"ts-jest": "^29.2.5",
"typescript": "^5.3.3",
"vite": "^6.2.5",
"vite": "^6.3.4",
"vite-plugin-compression2": "^1.3.3",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.2"

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useCallback, useRef, useState } from 'react';
import throttle from 'lodash/throttle';
import { visit } from 'unist-util-visit';
import { useSetRecoilState } from 'recoil';
import { useLocation } from 'react-router-dom';
import type { Pluggable } from 'unified';
import type { Artifact } from '~/common';
import { useMessageContext, useArtifactContext } from '~/Providers';
@@ -45,6 +46,7 @@ export function Artifact({
children: React.ReactNode | { props: { children: React.ReactNode } };
node: unknown;
}) {
const location = useLocation();
const { messageId } = useMessageContext();
const { getNextIndex, resetCounter } = useArtifactContext();
const artifactIndex = useRef(getNextIndex(false)).current;
@@ -86,6 +88,10 @@ export function Artifact({
lastUpdateTime: now,
};
if (!location.pathname.includes('/c/')) {
return setArtifact(currentArtifact);
}
setArtifacts((prevArtifacts) => {
if (
prevArtifacts?.[artifactKey] != null &&
@@ -110,6 +116,7 @@ export function Artifact({
props.identifier,
messageId,
artifactIndex,
location.pathname,
]);
useEffect(() => {

View File

@@ -1,15 +1,52 @@
import { useSetRecoilState, useResetRecoilState } from 'recoil';
import { useEffect, useRef } from 'react';
import debounce from 'lodash/debounce';
import { useLocation } from 'react-router-dom';
import { useRecoilState, useSetRecoilState, useResetRecoilState } from 'recoil';
import type { Artifact } from '~/common';
import FilePreview from '~/components/Chat/Input/Files/FilePreview';
import { getFileType, logger } from '~/utils';
import { useLocalize } from '~/hooks';
import { getFileType } from '~/utils';
import store from '~/store';
const ArtifactButton = ({ artifact }: { artifact: Artifact | null }) => {
const localize = useLocalize();
const setVisible = useSetRecoilState(store.artifactsVisible);
const location = useLocation();
const setVisible = useSetRecoilState(store.artifactsVisibility);
const [artifacts, setArtifacts] = useRecoilState(store.artifactsState);
const setCurrentArtifactId = useSetRecoilState(store.currentArtifactId);
const resetCurrentArtifactId = useResetRecoilState(store.currentArtifactId);
const [visibleArtifacts, setVisibleArtifacts] = useRecoilState(store.visibleArtifacts);
const debouncedSetVisibleRef = useRef(
debounce((artifactToSet: Artifact) => {
logger.log(
'artifacts_visibility',
'Setting artifact to visible state from Artifact button',
artifactToSet,
);
setVisibleArtifacts((prev) => ({
...prev,
[artifactToSet.id]: artifactToSet,
}));
}, 750),
);
useEffect(() => {
if (artifact == null || artifact?.id == null || artifact.id === '') {
return;
}
if (!location.pathname.includes('/c/')) {
return;
}
const debouncedSetVisible = debouncedSetVisibleRef.current;
debouncedSetVisible(artifact);
return () => {
debouncedSetVisible.cancel();
};
}, [artifact, location.pathname]);
if (artifact === null || artifact === undefined) {
return null;
}
@@ -20,8 +57,14 @@ const ArtifactButton = ({ artifact }: { artifact: Artifact | null }) => {
<button
type="button"
onClick={() => {
if (!location.pathname.includes('/c/')) {
return;
}
resetCurrentArtifactId();
setVisible(true);
if (artifacts?.[artifact.id] == null) {
setArtifacts(visibleArtifacts);
}
setTimeout(() => {
setCurrentArtifactId(artifact.id);
}, 15);

View File

@@ -1,7 +1,7 @@
import { useRef, useState, useEffect } from 'react';
import { RefreshCw } from 'lucide-react';
import { useSetRecoilState } from 'recoil';
import * as Tabs from '@radix-ui/react-tabs';
import { ArrowLeft, ChevronLeft, ChevronRight, RefreshCw, X } from 'lucide-react';
import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-react';
import useArtifacts from '~/hooks/Artifacts/useArtifacts';
import DownloadArtifact from './DownloadArtifact';
@@ -18,7 +18,7 @@ export default function Artifacts() {
const previewRef = useRef<SandpackPreviewRef>();
const [isVisible, setIsVisible] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const setArtifactsVisible = useSetRecoilState(store.artifactsVisible);
const setArtifactsVisible = useSetRecoilState(store.artifactsVisibility);
useEffect(() => {
setIsVisible(true);
@@ -48,37 +48,26 @@ export default function Artifacts() {
setTimeout(() => setIsRefreshing(false), 750);
};
const closeArtifacts = () => {
setIsVisible(false);
setTimeout(() => setArtifactsVisible(false), 300);
};
return (
<Tabs.Root value={activeTab} onValueChange={setActiveTab} asChild>
{/* Main Parent */}
<div className="flex h-full w-full items-center justify-center">
{/* Main Container */}
<div
className={`flex h-full w-full flex-col overflow-hidden border border-border-medium bg-surface-primary text-xl text-text-primary shadow-xl transition-all duration-300 ease-in-out ${
isVisible
? 'translate-x-0 scale-100 opacity-100'
: 'translate-x-full scale-95 opacity-0'
className={`flex h-full w-full flex-col overflow-hidden border border-border-medium bg-surface-primary text-xl text-text-primary shadow-xl transition-all duration-500 ease-in-out ${
isVisible ? 'scale-100 opacity-100 blur-0' : 'scale-105 opacity-0 blur-sm'
}`}
>
{/* Header */}
<div className="flex items-center justify-between border-b border-border-medium bg-surface-primary-alt p-2">
<div className="flex items-center">
<button
className="mr-2 text-text-secondary"
onClick={() => {
setIsVisible(false);
setTimeout(() => setArtifactsVisible(false), 300);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 256 256"
>
<path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z" />
</svg>
<button className="mr-2 text-text-secondary" onClick={closeArtifacts}>
<ArrowLeft className="h-4 w-4" />
</button>
<h3 className="truncate text-sm text-text-primary">{currentArtifact.title}</h3>
</div>
@@ -118,22 +107,8 @@ export default function Artifacts() {
{localize('com_ui_code')}
</Tabs.Trigger>
</Tabs.List>
<button
className="text-text-secondary"
onClick={() => {
setIsVisible(false);
setTimeout(() => setArtifactsVisible(false), 300);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 256 256"
>
<path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z" />
</svg>
<button className="text-text-secondary" onClick={closeArtifacts}>
<X className="h-4 w-4" />
</button>
</div>
</div>
@@ -149,29 +124,13 @@ export default function Artifacts() {
<div className="flex items-center justify-between border-t border-border-medium bg-surface-primary-alt p-2 text-sm text-text-secondary">
<div className="flex items-center">
<button onClick={() => cycleArtifact('prev')} className="mr-2 text-text-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 256 256"
>
<path d="M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z" />
</svg>
<ChevronLeft className="h-4 w-4" />
</button>
<span className="text-xs">{`${currentIndex + 1} / ${
orderedArtifactIds.length
}`}</span>
<button onClick={() => cycleArtifact('next')} className="ml-2 text-text-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 256 256"
>
<path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z" />
</svg>
<ChevronRight className="h-4 w-4" />
</button>
</div>
<div className="flex items-center gap-2">

View File

@@ -35,7 +35,7 @@ export const CodeMarkdown = memo(
const [userScrolled, setUserScrolled] = useState(false);
const currentContent = content;
const rehypePlugins = [
[rehypeKatex, { output: 'mathml' }],
[rehypeKatex],
[
rehypeHighlight,
{

View File

@@ -44,15 +44,6 @@ export default function ExportAndShareMenu({
};
const dropdownItems: t.MenuItemProps[] = [
{
label: localize('com_endpoint_export'),
onClick: exportHandler,
icon: <Upload className="icon-md mr-2 text-text-secondary" />,
/** NOTE: THE FOLLOWING PROPS ARE REQUIRED FOR MENU ITEMS THAT OPEN DIALOGS */
hideOnClick: false,
ref: exportButtonRef,
render: (props) => <button {...props} />,
},
{
label: localize('com_ui_share'),
onClick: shareHandler,
@@ -63,6 +54,15 @@ export default function ExportAndShareMenu({
ref: shareButtonRef,
render: (props) => <button {...props} />,
},
{
label: localize('com_endpoint_export'),
onClick: exportHandler,
icon: <Upload className="icon-md mr-2 text-text-secondary" />,
/** NOTE: THE FOLLOWING PROPS ARE REQUIRED FOR MENU ITEMS THAT OPEN DIALOGS */
hideOnClick: false,
ref: exportButtonRef,
render: (props) => <button {...props} />,
},
];
return (
@@ -70,6 +70,7 @@ export default function ExportAndShareMenu({
<DropdownPopup
menuId={menuId}
focusLoop={true}
unmountOnHide={true}
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}
trigger={
@@ -81,7 +82,7 @@ export default function ExportAndShareMenu({
aria-label="Export options"
className="inline-flex size-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
>
<Upload
<Share2
className="icon-md text-text-secondary"
aria-hidden="true"
focusable="false"

View File

@@ -108,6 +108,10 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
);
const handleContainerClick = useCallback(() => {
/** Check if the device is a touchscreen */
if (window.matchMedia?.('(pointer: coarse)').matches) {
return;
}
textAreaRef.current?.focus();
}, []);
@@ -126,6 +130,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
});
const { submitMessage, submitPrompt } = useSubmitMessage();
const handleKeyUp = useHandleKeyUp({
index,
textAreaRef,

View File

@@ -41,9 +41,9 @@ const CollapseChat = ({
)}
>
{isCollapsed ? (
<ChevronDown className="h-full w-full" />
) : (
<ChevronUp className="h-full w-full" />
) : (
<ChevronDown className="h-full w-full" />
)}
</button>
}

View File

@@ -119,6 +119,7 @@ const AttachFile = ({ disabled }: AttachFileProps) => {
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}
modal={true}
unmountOnHide={true}
trigger={menuTrigger}
items={dropdownItems}
iconClassName="mr-0"

View File

@@ -31,7 +31,8 @@ function MCPSelect({ conversationId }: { conversationId?: string | null }) {
select: (data) => {
const serverNames = new Set<string>();
data.forEach((tool) => {
if (tool.pluginKey.includes(Constants.mcp_delimiter)) {
const isMCP = tool.pluginKey.includes(Constants.mcp_delimiter);
if (isMCP && tool.chatMenu !== false) {
const parts = tool.pluginKey.split(Constants.mcp_delimiter);
serverNames.add(parts[parts.length - 1]);
}

View File

@@ -44,7 +44,7 @@ export default function PopoverButtons({
const endpoint = overrideEndpoint ?? endpointType ?? _endpoint ?? '';
const model = overrideModel ?? _model;
const isGenerativeModel = model?.toLowerCase().includes('gemini') ?? false;
const isGenerativeModel = /gemini|learnlm|gemma/.test(model ?? '') ?? false;
const isChatModel = (!isGenerativeModel && model?.toLowerCase().includes('chat')) ?? false;
const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? '');
@@ -133,7 +133,6 @@ export default function PopoverButtons({
</Button>
))}
</div>
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{disabled ? null : (
<div className="flex w-[150px] items-center justify-end">
{additionalButtons[settingsView].map((button, index) => (

View File

@@ -160,6 +160,7 @@ const BookmarkMenu: FC = () => {
focusLoop={true}
menuId={menuId}
isOpen={isMenuOpen}
unmountOnHide={true}
setIsOpen={setIsMenuOpen}
keyPrefix={`${conversationId}-bookmark-`}
trigger={

View File

@@ -113,9 +113,9 @@ const EditMessage = ({
messages.map((msg) =>
msg.messageId === messageId
? {
...msg,
text: data.text,
}
...msg,
text: data.text,
}
: msg,
),
);

View File

@@ -184,7 +184,7 @@ const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => {
const rehypePlugins = useMemo(
() => [
[rehypeKatex, { output: 'mathml' }],
[rehypeKatex],
[
rehypeHighlight,
{

View File

@@ -13,7 +13,7 @@ import { langSubset } from '~/utils';
const MarkdownLite = memo(
({ content = '', codeExecution = true }: { content?: string; codeExecution?: boolean }) => {
const rehypePlugins: PluggableList = [
[rehypeKatex, { output: 'mathml' }],
[rehypeKatex],
[
rehypeHighlight,
{

View File

@@ -35,7 +35,7 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) =>
} else {
return <>{text}</>;
}
}, [isCreatedByUser, enableUserMsgMarkdown, text, showCursorState, isLatestMessage]);
}, [isCreatedByUser, enableUserMsgMarkdown, text, isLatestMessage]);
return (
<div

View File

@@ -12,7 +12,7 @@ import store from '~/store';
export default function Presentation({ children }: { children: React.ReactNode }) {
const artifacts = useRecoilValue(store.artifactsState);
const artifactsVisible = useRecoilValue(store.artifactsVisible);
const artifactsVisibility = useRecoilValue(store.artifactsVisibility);
const setFilesToDelete = useSetFilesToDelete();
@@ -64,7 +64,7 @@ export default function Presentation({ children }: { children: React.ReactNode }
fullPanelCollapse={fullCollapse}
defaultCollapsed={defaultCollapsed}
artifacts={
artifactsVisible === true && Object.keys(artifacts ?? {}).length > 0 ? (
artifactsVisibility === true && Object.keys(artifacts ?? {}).length > 0 ? (
<EditorProvider>
<Artifacts />
</EditorProvider>

View File

@@ -41,7 +41,7 @@ const ConvoLink: React.FC<ConvoLinkProps> = ({
onRename();
}}
role="button"
aria-label={isSmallScreen ? undefined : localize('com_ui_double_click_to_rename')}
aria-label={isSmallScreen ? undefined : title || localize('com_ui_untitled')}
>
{title || localize('com_ui_untitled')}
</div>

View File

@@ -235,10 +235,11 @@ function ConvoOptions({
<DeleteButton
title={title ?? ''}
retainView={retainView}
conversationId={conversationId ?? ''}
showDeleteDialog={showDeleteDialog}
setShowDeleteDialog={setShowDeleteDialog}
triggerRef={deleteButtonRef}
setMenuOpen={setIsPopoverActive}
showDeleteDialog={showDeleteDialog}
conversationId={conversationId ?? ''}
setShowDeleteDialog={setShowDeleteDialog}
/>
)}
</>

View File

@@ -4,13 +4,12 @@ import { useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import type { TMessage } from 'librechat-data-provider';
import {
Label,
OGDialog,
OGDialogTitle,
OGDialogContent,
OGDialogHeader,
Button,
Spinner,
OGDialog,
OGDialogTitle,
OGDialogHeader,
OGDialogContent,
} from '~/components';
import { useDeleteConversationMutation } from '~/data-provider';
import { useLocalize, useNewConvo } from '~/hooks';
@@ -24,14 +23,17 @@ type DeleteButtonProps = {
showDeleteDialog?: boolean;
setShowDeleteDialog?: (value: boolean) => void;
triggerRef?: React.RefObject<HTMLButtonElement>;
setMenuOpen?: React.Dispatch<React.SetStateAction<boolean>>;
};
export function DeleteConversationDialog({
setShowDeleteDialog,
conversationId,
setMenuOpen,
retainView,
title,
}: {
setMenuOpen?: React.Dispatch<React.SetStateAction<boolean>>;
setShowDeleteDialog: (value: boolean) => void;
conversationId: string;
retainView: () => void;
@@ -51,6 +53,7 @@ export function DeleteConversationDialog({
newConversation();
navigate('/c/new', { replace: true });
}
setMenuOpen?.(false);
retainView();
},
onError: () => {
@@ -98,6 +101,7 @@ export default function DeleteButton({
conversationId,
retainView,
title,
setMenuOpen,
showDeleteDialog,
setShowDeleteDialog,
triggerRef,
@@ -115,6 +119,7 @@ export default function DeleteButton({
<DeleteConversationDialog
setShowDeleteDialog={setShowDeleteDialog}
conversationId={conversationId}
setMenuOpen={setMenuOpen}
retainView={retainView}
title={title}
/>

View File

@@ -95,6 +95,7 @@ const PopoverButton: React.FC<PopoverButtonProps> = ({
gutter={16}
className="z-[999] w-80 rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
portal={true}
unmountOnHide={true}
>
<div className="space-y-2">
<p className="flex flex-col gap-2 text-sm text-text-secondary">
@@ -179,33 +180,38 @@ export default function Fork({
return (
<>
<Ariakit.PopoverAnchor store={popoverStore}>
<button
className={cn(
'hover-button active rounded-md p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible',
'data-[state=open]:active focus:opacity-100 data-[state=open]:bg-gray-100 data-[state=open]:text-gray-500 data-[state=open]:dark:bg-gray-700 data-[state=open]:dark:text-gray-200',
!isLast ? 'data-[state=open]:opacity-100 md:opacity-0 md:group-hover:opacity-100' : '',
)}
onClick={(e) => {
if (rememberGlobal) {
e.preventDefault();
forkConvo.mutate({
messageId,
splitAtTarget,
conversationId,
option: forkSetting,
latestMessageId,
});
} else {
popoverStore.toggle();
}
}}
type="button"
aria-label={localize('com_ui_fork')}
>
<GitFork className="h-4 w-4 hover:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400" />
</button>
</Ariakit.PopoverAnchor>
<Ariakit.PopoverAnchor
store={popoverStore}
render={
<button
className={cn(
'hover-button active rounded-md p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible',
'data-[state=open]:active focus:opacity-100 data-[state=open]:bg-gray-100 data-[state=open]:text-gray-500 data-[state=open]:dark:bg-gray-700 data-[state=open]:dark:text-gray-200',
!isLast
? 'data-[state=open]:opacity-100 md:opacity-0 md:group-hover:opacity-100'
: '',
)}
onClick={(e) => {
if (rememberGlobal) {
e.preventDefault();
forkConvo.mutate({
messageId,
splitAtTarget,
conversationId,
option: forkSetting,
latestMessageId,
});
} else {
popoverStore.toggle();
}
}}
type="button"
aria-label={localize('com_ui_fork')}
>
<GitFork className="h-4 w-4 hover:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400" />
</button>
}
/>
<Ariakit.Popover
store={popoverStore}
gutter={5}
@@ -216,6 +222,7 @@ export default function Fork({
zIndex: 50,
}}
portal={true}
unmountOnHide={true}
>
<div className="flex h-8 w-full items-center justify-center text-sm text-text-primary">
{localize(activeSetting)}
@@ -240,6 +247,7 @@ export default function Fork({
gutter={19}
className="z-[999] w-80 rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
portal={true}
unmountOnHide={true}
>
<div className="flex flex-col gap-2 space-y-2 text-sm text-text-secondary">
<span>{localize('com_ui_fork_info_1')}</span>
@@ -336,6 +344,7 @@ export default function Fork({
gutter={32}
className="z-[999] w-80 select-none rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
portal={true}
unmountOnHide={true}
>
<div className="space-y-2">
<p className="text-sm text-text-secondary">{localize('com_ui_fork_info_start')}</p>
@@ -386,6 +395,7 @@ export default function Fork({
gutter={14}
className="z-[999] w-80 rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
portal={true}
unmountOnHide={true}
>
<div className="space-y-2">
<p className="text-sm text-text-secondary">{localize('com_ui_fork_info_remember')}</p>

View File

@@ -34,10 +34,7 @@ function getOpenAIColor(_model: string | null | undefined) {
function getGoogleIcon(model: string | null | undefined, size: number) {
if (model?.toLowerCase().includes('code') === true) {
return <CodeyIcon size={size * 0.75} />;
} else if (
model?.toLowerCase().includes('gemini') === true ||
model?.toLowerCase().includes('learnlm') === true
) {
} else if (/gemini|learnlm|gemma/.test(model?.toLowerCase() ?? '')) {
return <GeminiIcon size={size * 0.7} />;
} else {
return <PaLMIcon size={size * 0.7} />;
@@ -52,6 +49,8 @@ function getGoogleModelName(model: string | null | undefined) {
model?.toLowerCase().includes('learnlm') === true
) {
return 'Gemini';
} else if (model?.toLowerCase().includes('gemma') === true) {
return 'Gemma';
} else {
return 'PaLM2';
}

View File

@@ -80,8 +80,10 @@ export const LangSelector = ({
{ value: 'zh-Hans', label: localize('com_nav_lang_chinese') },
{ value: 'zh-Hant', label: localize('com_nav_lang_traditional_chinese') },
{ value: 'ar-EG', label: localize('com_nav_lang_arabic') },
{ value: 'da-DK', label: localize('com_nav_lang_danish') },
{ value: 'de-DE', label: localize('com_nav_lang_german') },
{ value: 'es-ES', label: localize('com_nav_lang_spanish') },
{ value: 'ca-ES', label: localize('com_nav_lang_catalan') },
{ value: 'et-EE', label: localize('com_nav_lang_estonian') },
{ value: 'fa-IR', label: localize('com_nav_lang_persian') },
{ value: 'fr-FR', label: localize('com_nav_lang_french') },
@@ -94,6 +96,7 @@ export const LangSelector = ({
{ value: 'ru-RU', label: localize('com_nav_lang_russian') },
{ value: 'ja-JP', label: localize('com_nav_lang_japanese') },
{ value: 'ka-GE', label: localize('com_nav_lang_georgian') },
{ value: 'cs-CZ', label: localize('com_nav_lang_czech') },
{ value: 'sv-SE', label: localize('com_nav_lang_swedish') },
{ value: 'ko-KR', label: localize('com_nav_lang_korean') },
{ value: 'vi-VN', label: localize('com_nav_lang_vietnamese') },

View File

@@ -66,7 +66,7 @@ const AdminSettings = () => {
const [confirmAdminUseChange, setConfirmAdminUseChange] = useState<{
newValue: boolean;
callback: (value: boolean) => void;
} | null>(null);
} | null>(null);
const { mutate, isLoading } = useUpdatePromptPermissionsMutation({
onSuccess: () => {
showToast({ status: 'success', message: localize('com_ui_saved') });
@@ -166,6 +166,7 @@ const AdminSettings = () => {
<div className="flex items-center gap-2">
<span className="font-medium">{localize('com_ui_role_select')}:</span>
<DropdownPopup
unmountOnHide={true}
menuId="prompt-role-dropdown"
isOpen={isRoleMenuOpen}
setIsOpen={setIsRoleMenuOpen}
@@ -191,11 +192,11 @@ const AdminSettings = () => {
setValue={setValue}
{...(selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE
? {
confirmChange: (
newValue: boolean,
onChange: (value: boolean) => void,
) => setConfirmAdminUseChange({ newValue, callback: onChange }),
}
confirmChange: (
newValue: boolean,
onChange: (value: boolean) => void,
) => setConfirmAdminUseChange({ newValue, callback: onChange }),
}
: {})}
/>
{selectedRole === SystemRoles.ADMIN && promptPerm === Permissions.USE && (

View File

@@ -146,7 +146,7 @@ export default function VariableForm({
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
rehypePlugins={[
/** @ts-ignore */
[rehypeKatex, { output: 'mathml' }],
[rehypeKatex],
/** @ts-ignore */
[rehypeHighlight, { ignoreMissing: true }],
]}

View File

@@ -59,7 +59,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
]}
rehypePlugins={[
/** @ts-ignore */
[rehypeKatex, { output: 'mathml' }],
[rehypeKatex],
/** @ts-ignore */
[rehypeHighlight, { ignoreMissing: true }],
]}

View File

@@ -43,7 +43,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
}, [isEditing, prompt]);
const rehypePlugins: PluggableList = [
[rehypeKatex, { output: 'mathml' }],
[rehypeKatex],
[
rehypeHighlight,
{

View File

@@ -157,6 +157,7 @@ const AdminSettings = () => {
<div className="flex items-center gap-2">
<span className="font-medium">{localize('com_ui_role_select')}:</span>
<DropdownPopup
unmountOnHide={true}
menuId="role-dropdown"
isOpen={isRoleMenuOpen}
setIsOpen={setIsRoleMenuOpen}

View File

@@ -30,6 +30,7 @@ import type {
SharedLinksResponse,
} from 'librechat-data-provider';
import type { ConversationCursorData } from '~/utils/convos';
import { findConversationInInfinite } from '~/utils';
export const useGetPresetsQuery = (
config?: UseQueryOptions<TPreset[]>,
@@ -68,14 +69,13 @@ export const useGetConvoIdQuery = (
[QueryKeys.conversation, id],
() => {
// Try to find in all fetched infinite pages
const convosQuery = queryClient.getQueryData<InfiniteData<ConversationCursorData>>([
QueryKeys.allConversations,
]);
const found = convosQuery?.pages
.flatMap((page) => page.conversations)
.find((c) => c.conversationId === id);
const convosQuery = queryClient.getQueryData<InfiniteData<ConversationCursorData>>(
[QueryKeys.allConversations],
{ exact: false },
);
const found = findConversationInInfinite(convosQuery, id);
if (found) {
if (found && found.messages != null) {
return found;
}
// Otherwise, fetch from API

View File

@@ -1,9 +1,9 @@
import { useMemo, useState, useEffect, useRef } from 'react';
import { Constants } from 'librechat-data-provider';
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
import { getLatestText, logger } from '~/utils';
import { useChatContext } from '~/Providers';
import { getKey } from '~/utils/artifacts';
import { getLatestText } from '~/utils';
import store from '~/store';
export default function useArtifacts() {
@@ -37,16 +37,20 @@ export default function useArtifacts() {
hasEnclosedArtifactRef.current = false;
};
if (
conversation &&
conversation.conversationId !== prevConversationIdRef.current &&
conversation?.conversationId !== prevConversationIdRef.current &&
prevConversationIdRef.current != null
) {
resetState();
} else if (conversation && conversation.conversationId === Constants.NEW_CONVO) {
} else if (conversation?.conversationId === Constants.NEW_CONVO) {
resetState();
}
prevConversationIdRef.current = conversation?.conversationId ?? null;
}, [conversation, resetArtifacts, resetCurrentArtifactId]);
/** Resets artifacts when unmounting */
return () => {
logger.log('artifacts_visibility', 'Unmounting artifacts');
resetState();
};
}, [conversation?.conversationId, resetArtifacts, resetCurrentArtifactId]);
useEffect(() => {
if (orderedArtifactIds.length > 0) {
@@ -56,30 +60,39 @@ export default function useArtifacts() {
}, [setCurrentArtifactId, orderedArtifactIds]);
useEffect(() => {
if (isSubmitting && orderedArtifactIds.length > 0 && latestMessage) {
const latestArtifactId = orderedArtifactIds[orderedArtifactIds.length - 1];
const latestArtifact = artifacts?.[latestArtifactId];
if (!isSubmitting) {
return;
}
if (orderedArtifactIds.length === 0) {
return;
}
if (latestMessage == null) {
return;
}
const latestArtifactId = orderedArtifactIds[orderedArtifactIds.length - 1];
const latestArtifact = artifacts?.[latestArtifactId];
if (latestArtifact?.content === lastContentRef.current) {
return;
}
if (latestArtifact?.content !== lastContentRef.current) {
setCurrentArtifactId(latestArtifactId);
lastContentRef.current = latestArtifact?.content ?? null;
setCurrentArtifactId(latestArtifactId);
lastContentRef.current = latestArtifact?.content ?? null;
const latestMessageText = getLatestText(latestMessage);
const hasEnclosedArtifact = /:::artifact[\s\S]*?(```|:::)\s*$/.test(
latestMessageText.trim(),
);
const latestMessageText = getLatestText(latestMessage);
const hasEnclosedArtifact =
/:::artifact(?:\{[^}]*\})?(?:\s|\n)*(?:```[\s\S]*?```(?:\s|\n)*)?:::/m.test(
latestMessageText.trim(),
);
if (hasEnclosedArtifact && !hasEnclosedArtifactRef.current) {
setActiveTab('preview');
hasEnclosedArtifactRef.current = true;
hasAutoSwitchedToCodeRef.current = false;
} else if (!hasEnclosedArtifactRef.current && !hasAutoSwitchedToCodeRef.current) {
const artifactStartContent = latestArtifact?.content?.slice(0, 50) ?? '';
if (artifactStartContent.length > 0 && latestMessageText.includes(artifactStartContent)) {
setActiveTab('code');
hasAutoSwitchedToCodeRef.current = true;
}
}
if (hasEnclosedArtifact && !hasEnclosedArtifactRef.current) {
setActiveTab('preview');
hasEnclosedArtifactRef.current = true;
hasAutoSwitchedToCodeRef.current = false;
} else if (!hasEnclosedArtifactRef.current && !hasAutoSwitchedToCodeRef.current) {
const artifactStartContent = latestArtifact?.content?.slice(0, 50) ?? '';
if (artifactStartContent.length > 0 && latestMessageText.includes(artifactStartContent)) {
setActiveTab('code');
hasAutoSwitchedToCodeRef.current = true;
}
}
}, [setCurrentArtifactId, isSubmitting, orderedArtifactIds, artifacts, latestMessage]);

View File

@@ -0,0 +1,397 @@
const mockNavigate = jest.fn();
const mockTextAreaRef = { current: { focus: jest.fn() } };
let mockLog: jest.SpyInstance;
jest.mock('react-router-dom', () => ({
useLocation: jest.fn(),
useNavigate: jest.fn(),
}));
// Import the component under test and its dependencies
import { renderHook } from '@testing-library/react';
import { useLocation, useNavigate } from 'react-router-dom';
import useFocusChatEffect from '../useFocusChatEffect';
import { logger } from '~/utils';
describe('useFocusChatEffect', () => {
// Reset mocks before each test
beforeEach(() => {
mockLog = jest.spyOn(logger, 'log').mockImplementation(() => {});
jest.clearAllMocks();
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
// Mock window.matchMedia
window.matchMedia = jest.fn().mockImplementation(() => ({
matches: false,
media: '',
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}));
// Set default location mock
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '',
state: { focusChat: true },
});
// Mock window.location
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '',
},
writable: true,
});
});
describe('Basic functionality', () => {
test('should focus textarea when location.state.focusChat is true', () => {
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockTextAreaRef.current.focus).toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith('/c/new', {
replace: true,
state: {},
});
expect(mockLog).toHaveBeenCalled();
});
test('should not focus textarea when location.state.focusChat is false', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '',
state: { focusChat: false },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockTextAreaRef.current.focus).not.toHaveBeenCalled();
expect(mockNavigate).not.toHaveBeenCalled();
});
test('should not focus textarea when textAreaRef.current is null', () => {
const nullTextAreaRef = { current: null };
renderHook(() => useFocusChatEffect(nullTextAreaRef as any));
expect(mockNavigate).not.toHaveBeenCalled();
});
test('should not focus textarea on touchscreen devices', () => {
window.matchMedia = jest.fn().mockImplementation(() => ({
matches: true, // This indicates a touchscreen
media: '',
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}));
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockTextAreaRef.current.focus).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalled();
});
});
describe('URL parameter handling', () => {
// Helper function to run tests with different URL scenarios
const testUrlScenario = ({
windowLocationSearch,
reactRouterSearch,
expectedUrl,
testDescription,
}: {
windowLocationSearch: string;
reactRouterSearch: string;
expectedUrl: string;
testDescription: string;
}) => {
test(`${testDescription}`, () => {
// Mock window.location
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: windowLocationSearch,
},
writable: true,
});
// Mock React Router's location
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: reactRouterSearch,
state: { focusChat: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
expectedUrl,
expect.objectContaining({
replace: true,
state: {},
}),
);
});
};
test('should use window.location.search instead of location.search', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '?agent_id=test_agent_id',
},
writable: true,
});
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '?endpoint=openAI&model=gpt-4o-mini',
state: { focusChat: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
// Should use window.location.search, not location.search
'/c/new?agent_id=test_agent_id',
expect.objectContaining({
replace: true,
state: {},
}),
);
});
testUrlScenario({
windowLocationSearch: '?agent_id=agent123',
reactRouterSearch: '?endpoint=openAI&model=gpt-4',
expectedUrl: '/c/new?agent_id=agent123',
testDescription: 'should prioritize window.location.search with agent_id parameter',
});
testUrlScenario({
windowLocationSearch: '',
reactRouterSearch: '?endpoint=openAI&model=gpt-4',
expectedUrl: '/c/new',
testDescription: 'should use empty path when window.location.search is empty',
});
testUrlScenario({
windowLocationSearch: '?agent_id=agent123&prompt=test',
reactRouterSearch: '',
expectedUrl: '/c/new?agent_id=agent123&prompt=test',
testDescription: 'should use window.location.search when React Router search is empty',
});
testUrlScenario({
windowLocationSearch: '?agent_id=agent123',
reactRouterSearch: '?agent_id=differentAgent',
expectedUrl: '/c/new?agent_id=agent123',
testDescription:
'should use window.location.search even when both have agent_id but with different values',
});
testUrlScenario({
windowLocationSearch: '?agent_id=agent/with%20spaces&prompt=test%20query',
reactRouterSearch: '?endpoint=openAI',
expectedUrl: '/c/new?agent_id=agent/with%20spaces&prompt=test%20query',
testDescription: 'should handle URL parameters with special characters correctly',
});
testUrlScenario({
windowLocationSearch:
'?agent_id=agent123&prompt=test&model=gpt-4&temperature=0.7&max_tokens=1000',
reactRouterSearch: '?endpoint=openAI',
expectedUrl:
'/c/new?agent_id=agent123&prompt=test&model=gpt-4&temperature=0.7&max_tokens=1000',
testDescription: 'should handle multiple URL parameters correctly',
});
testUrlScenario({
windowLocationSearch: '?agent_id=agent123&broken=param=with=equals',
reactRouterSearch: '?endpoint=openAI',
expectedUrl: '/c/new?agent_id=agent123&broken=param=with=equals',
testDescription: 'should pass through malformed URL parameters unchanged',
});
test('should handle navigation immediately after URL parameter changes', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '?endpoint=openAI&model=gpt-4',
},
writable: true,
});
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '?endpoint=openAI&model=gpt-4',
state: { focusChat: true },
});
const { rerender } = renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
'/c/new?endpoint=openAI&model=gpt-4',
expect.objectContaining({
replace: true,
state: {},
}),
);
jest.clearAllMocks();
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '?agent_id=agent123',
},
writable: true,
});
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new_changed',
search: '?endpoint=openAI&model=gpt-4',
state: { focusChat: true },
});
rerender();
expect(mockNavigate).toHaveBeenCalledWith(
'/c/new_changed?agent_id=agent123',
expect.objectContaining({
replace: true,
state: {},
}),
);
});
test('should handle undefined or null search params gracefully', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: undefined,
},
writable: true,
});
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: undefined,
state: { focusChat: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
'/c/new',
expect.objectContaining({
replace: true,
state: {},
}),
);
jest.clearAllMocks();
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: null,
},
writable: true,
});
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: null,
state: { focusChat: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
'/c/new',
expect.objectContaining({
replace: true,
state: {},
}),
);
});
test('should handle navigation when location.state is null', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '?agent_id=agent123',
},
writable: true,
});
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '?endpoint=openAI&model=gpt-4',
state: null,
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).not.toHaveBeenCalled();
expect(mockTextAreaRef.current.focus).not.toHaveBeenCalled();
});
test('should handle navigation when location.state.focusChat is undefined', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '?agent_id=agent123',
},
writable: true,
});
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '?endpoint=openAI&model=gpt-4',
state: { someOtherProp: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).not.toHaveBeenCalled();
expect(mockTextAreaRef.current.focus).not.toHaveBeenCalled();
});
test('should handle navigation when both search params are empty', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/c/new',
search: '',
},
writable: true,
});
(useLocation as jest.Mock).mockReturnValue({
pathname: '/c/new',
search: '',
state: { focusChat: true },
});
renderHook(() => useFocusChatEffect(mockTextAreaRef as any));
expect(mockNavigate).toHaveBeenCalledWith(
'/c/new',
expect.objectContaining({
replace: true,
state: {},
}),
);
});
});
});

View File

@@ -11,8 +11,16 @@ export default function useFocusChatEffect(textAreaRef: React.RefObject<HTMLText
'conversation',
`Focusing textarea on location state change: ${location.pathname}`,
);
textAreaRef.current?.focus();
navigate(`${location.pathname}${location.search ?? ''}`, { replace: true, state: {} });
/** Check if the device is not a touchscreen */
if (!window.matchMedia?.('(pointer: coarse)').matches) {
textAreaRef.current?.focus();
}
navigate(`${location.pathname}${window.location.search ?? ''}`, {
replace: true,
state: {},
});
}
}, [navigate, textAreaRef, location.pathname, location.state?.focusChat, location.search]);
}, [navigate, textAreaRef, location.pathname, location.state?.focusChat]);
}

View File

@@ -4,18 +4,18 @@ import { logger } from '~/utils';
import store from '~/store';
/**
* Hook to reset artifacts when the conversation ID changes
* Hook to reset visible artifacts when the conversation ID changes
* @param conversationId - The current conversation ID
*/
export default function useIdChangeEffect(conversationId: string) {
const lastConvoId = useRef<string | null>(null);
const resetArtifacts = useResetRecoilState(store.artifactsState);
const resetVisibleArtifacts = useResetRecoilState(store.visibleArtifacts);
useEffect(() => {
if (conversationId !== lastConvoId.current) {
logger.log('conversation', 'Conversation ID change');
resetArtifacts();
resetVisibleArtifacts();
}
lastConvoId.current = conversationId;
}, [conversationId, resetArtifacts]);
}, [conversationId, resetVisibleArtifacts]);
}

View File

@@ -1,7 +1,7 @@
import { useSetRecoilState } from 'recoil';
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import { QueryKeys, Constants, dataService } from 'librechat-data-provider';
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils';
import store from '~/store';
@@ -14,6 +14,27 @@ const useNavigateToConvo = (index = 0) => {
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
const { hasSetConversation, setConversation } = store.useCreateConversationAtom(index);
const fetchFreshData = async (conversation?: Partial<TConversation>) => {
const conversationId = conversation?.conversationId;
if (!conversationId) {
return;
}
try {
const data = await queryClient.fetchQuery([QueryKeys.conversation, conversationId], () =>
dataService.getConversationById(conversationId),
);
logger.log('conversation', 'Fetched fresh conversation data', data);
setConversation(data);
navigate(`/c/${conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
} catch (error) {
console.error('Error fetching conversation data on navigation', error);
if (conversation) {
setConversation(conversation as TConversation);
navigate(`/c/${conversationId}`, { state: { focusChat: true } });
}
}
};
const navigateToConvo = (
conversation?: TConversation | null,
options?: {
@@ -58,9 +79,14 @@ const useNavigateToConvo = (index = 0) => {
});
}
clearAllConversations(true);
setConversation(convo);
queryClient.setQueryData([QueryKeys.messages, currentConvoId], []);
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
if (convo.conversationId !== Constants.NEW_CONVO && convo.conversationId) {
queryClient.invalidateQueries([QueryKeys.conversation, convo.conversationId]);
fetchFreshData(convo);
} else {
setConversation(convo);
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
}
};
return {

View File

@@ -75,9 +75,9 @@ export const useAutoSave = ({
const { fileToRecover, fileIdToRecover } = fileData
? { fileToRecover: fileData, fileIdToRecover: fileId }
: {
fileToRecover: tempFileData,
fileIdToRecover: (tempFileData?.temp_file_id ?? '') || fileId,
};
fileToRecover: tempFileData,
fileIdToRecover: (tempFileData?.temp_file_id ?? '') || fileId,
};
if (fileToRecover) {
setFiles((currentFiles) => {
@@ -188,7 +188,7 @@ export const useAutoSave = ({
`${LocalStorageKeys.TEXT_DRAFT}${Constants.PENDING_CONVO}`,
);
// Clear the pending draft, if it exists, and save the current draft to the new conversationId;
// Clear the pending text draft, if it exists, and save the current draft to the new conversationId;
// otherwise, save the current text area value to the new conversationId
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${Constants.PENDING_CONVO}`);
if (pendingDraft) {
@@ -199,6 +199,21 @@ export const useAutoSave = ({
encodeBase64(textAreaRef.current.value),
);
}
const pendingFileDraft = localStorage.getItem(
`${LocalStorageKeys.FILES_DRAFT}${Constants.PENDING_CONVO}`,
);
if (pendingFileDraft) {
localStorage.setItem(
`${LocalStorageKeys.FILES_DRAFT}${conversationId}`,
pendingFileDraft,
);
localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${Constants.PENDING_CONVO}`);
const filesDraft = JSON.parse(pendingFileDraft || '[]') as string[];
if (filesDraft.length > 0) {
restoreFiles(conversationId);
}
}
} else if (currentConversationId != null && currentConversationId) {
saveText(currentConversationId);
}

View File

@@ -0,0 +1,489 @@
// useQueryParams.spec.ts
jest.mock('recoil', () => {
const originalModule = jest.requireActual('recoil');
return {
...originalModule,
atom: jest.fn().mockImplementation((config) => ({
key: config.key,
default: config.default,
})),
useRecoilValue: jest.fn(),
};
});
// Move mock store definition after the mocks
jest.mock('~/store', () => ({
modularChat: { key: 'modularChat', default: false },
availableTools: { key: 'availableTools', default: [] },
}));
import { renderHook, act } from '@testing-library/react';
import { useSearchParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useRecoilValue } from 'recoil';
import useQueryParams from './useQueryParams';
import { useChatContext, useChatFormContext } from '~/Providers';
import useSubmitMessage from '~/hooks/Messages/useSubmitMessage';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import store from '~/store';
// Other mocks
jest.mock('react-router-dom', () => ({
useSearchParams: jest.fn(),
}));
jest.mock('@tanstack/react-query', () => ({
useQueryClient: jest.fn(),
}));
jest.mock('~/Providers', () => ({
useChatContext: jest.fn(),
useChatFormContext: jest.fn(),
}));
jest.mock('~/hooks/Messages/useSubmitMessage', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('~/hooks/Conversations/useDefaultConvo', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('~/utils', () => ({
getConvoSwitchLogic: jest.fn(() => ({
template: {},
shouldSwitch: false,
isNewModular: false,
newEndpointType: null,
isCurrentModular: false,
isExistingConversation: false,
})),
getModelSpecIconURL: jest.fn(() => 'icon-url'),
removeUnavailableTools: jest.fn((preset) => preset),
logger: { log: jest.fn() },
}));
// Mock the tQueryParamsSchema
jest.mock('librechat-data-provider', () => ({
...jest.requireActual('librechat-data-provider'),
tQueryParamsSchema: {
shape: {
model: { parse: jest.fn((value) => value) },
endpoint: { parse: jest.fn((value) => value) },
temperature: { parse: jest.fn((value) => value) },
// Add other schema shapes as needed
},
},
isAgentsEndpoint: jest.fn(() => false),
isAssistantsEndpoint: jest.fn(() => false),
QueryKeys: { startupConfig: 'startupConfig', endpoints: 'endpoints' },
EModelEndpoint: { custom: 'custom', assistants: 'assistants', agents: 'agents' },
}));
// Mock global window.history
global.window = Object.create(window);
global.window.history = {
replaceState: jest.fn(),
pushState: jest.fn(),
go: jest.fn(),
back: jest.fn(),
forward: jest.fn(),
length: 1,
scrollRestoration: 'auto',
state: null,
};
describe('useQueryParams', () => {
// Setup common mocks before each test
beforeEach(() => {
jest.useFakeTimers();
// Reset mock for window.history.replaceState
jest.spyOn(window.history, 'replaceState').mockClear();
// Create mocks for all dependencies
const mockSearchParams = new URLSearchParams();
(useSearchParams as jest.Mock).mockReturnValue([mockSearchParams, jest.fn()]);
const mockQueryClient = {
getQueryData: jest.fn().mockImplementation((key) => {
if (key === 'startupConfig') {
return { modelSpecs: { list: [] } };
}
if (key === 'endpoints') {
return {};
}
return null;
}),
};
(useQueryClient as jest.Mock).mockReturnValue(mockQueryClient);
(useRecoilValue as jest.Mock).mockImplementation((atom) => {
if (atom === store.modularChat) return false;
if (atom === store.availableTools) return [];
return null;
});
const mockConversation = { model: null, endpoint: null };
const mockNewConversation = jest.fn();
(useChatContext as jest.Mock).mockReturnValue({
conversation: mockConversation,
newConversation: mockNewConversation,
});
const mockMethods = {
setValue: jest.fn(),
getValues: jest.fn().mockReturnValue(''),
handleSubmit: jest.fn((callback) => () => callback({ text: 'test message' })),
};
(useChatFormContext as jest.Mock).mockReturnValue(mockMethods);
const mockSubmitMessage = jest.fn();
(useSubmitMessage as jest.Mock).mockReturnValue({
submitMessage: mockSubmitMessage,
});
const mockGetDefaultConversation = jest.fn().mockReturnValue({});
(useDefaultConvo as jest.Mock).mockReturnValue(mockGetDefaultConversation);
});
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
// Helper function to set URL parameters for testing
const setUrlParams = (params: Record<string, string>) => {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
searchParams.set(key, value);
});
(useSearchParams as jest.Mock).mockReturnValue([searchParams, jest.fn()]);
};
// Test cases remain the same
it('should process query parameters on initial render', () => {
// Setup
const mockSetValue = jest.fn();
const mockTextAreaRef = {
current: {
focus: jest.fn(),
setSelectionRange: jest.fn(),
} as unknown as HTMLTextAreaElement,
};
(useChatFormContext as jest.Mock).mockReturnValue({
setValue: mockSetValue,
getValues: jest.fn().mockReturnValue(''),
handleSubmit: jest.fn((callback) => () => callback({ text: 'test message' })),
});
// Mock startup config to allow processing
(useQueryClient as jest.Mock).mockReturnValue({
getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }),
});
setUrlParams({ q: 'hello world' });
// Execute
renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef }));
// Advance timer to trigger interval
act(() => {
jest.advanceTimersByTime(100);
});
// Assert
expect(mockSetValue).toHaveBeenCalledWith(
'text',
'hello world',
expect.objectContaining({ shouldValidate: true }),
);
expect(window.history.replaceState).toHaveBeenCalled();
});
it('should auto-submit message when submit=true and no settings to apply', () => {
// Setup
const mockSetValue = jest.fn();
const mockHandleSubmit = jest.fn((callback) => () => callback({ text: 'test message' }));
const mockSubmitMessage = jest.fn();
const mockTextAreaRef = {
current: {
focus: jest.fn(),
setSelectionRange: jest.fn(),
} as unknown as HTMLTextAreaElement,
};
(useChatFormContext as jest.Mock).mockReturnValue({
setValue: mockSetValue,
getValues: jest.fn().mockReturnValue(''),
handleSubmit: mockHandleSubmit,
});
(useSubmitMessage as jest.Mock).mockReturnValue({
submitMessage: mockSubmitMessage,
});
// Mock startup config to allow processing
(useQueryClient as jest.Mock).mockReturnValue({
getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }),
});
setUrlParams({ q: 'hello world', submit: 'true' });
// Execute
renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef }));
// Advance timer to trigger interval
act(() => {
jest.advanceTimersByTime(100);
});
// Assert
expect(mockSetValue).toHaveBeenCalledWith(
'text',
'hello world',
expect.objectContaining({ shouldValidate: true }),
);
expect(mockHandleSubmit).toHaveBeenCalled();
expect(mockSubmitMessage).toHaveBeenCalled();
});
it('should defer submission when settings need to be applied first', () => {
// Setup
const mockSetValue = jest.fn();
const mockHandleSubmit = jest.fn((callback) => () => callback({ text: 'test message' }));
const mockSubmitMessage = jest.fn();
const mockNewConversation = jest.fn();
const mockTextAreaRef = {
current: {
focus: jest.fn(),
setSelectionRange: jest.fn(),
} as unknown as HTMLTextAreaElement,
};
// Mock getQueryData to return array format for startupConfig
const mockGetQueryData = jest.fn().mockImplementation((key) => {
if (Array.isArray(key) && key[0] === 'startupConfig') {
return { modelSpecs: { list: [] } };
}
if (key === 'startupConfig') {
return { modelSpecs: { list: [] } };
}
return null;
});
(useChatFormContext as jest.Mock).mockReturnValue({
setValue: mockSetValue,
getValues: jest.fn().mockReturnValue(''),
handleSubmit: mockHandleSubmit,
});
(useSubmitMessage as jest.Mock).mockReturnValue({
submitMessage: mockSubmitMessage,
});
(useChatContext as jest.Mock).mockReturnValue({
conversation: { model: null, endpoint: null },
newConversation: mockNewConversation,
});
(useQueryClient as jest.Mock).mockReturnValue({
getQueryData: mockGetQueryData,
});
setUrlParams({ q: 'hello world', submit: 'true', model: 'gpt-4' });
// Execute
const { rerender } = renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef }));
// First interval tick should process params but not submit
act(() => {
jest.advanceTimersByTime(100);
});
// Assert initial state
expect(mockGetQueryData).toHaveBeenCalledWith(expect.anything());
expect(mockNewConversation).toHaveBeenCalled();
expect(mockSubmitMessage).not.toHaveBeenCalled(); // Not submitted yet
// Now mock conversation update to trigger settings application check
(useChatContext as jest.Mock).mockReturnValue({
conversation: { model: 'gpt-4', endpoint: null },
newConversation: mockNewConversation,
});
// Re-render to trigger the effect that watches for settings
rerender();
// Now the message should be submitted
expect(mockSetValue).toHaveBeenCalledWith(
'text',
'hello world',
expect.objectContaining({ shouldValidate: true }),
);
expect(mockHandleSubmit).toHaveBeenCalled();
expect(mockSubmitMessage).toHaveBeenCalled();
});
it('should submit after timeout if settings never get applied', () => {
// Setup
const mockSetValue = jest.fn();
const mockHandleSubmit = jest.fn((callback) => () => callback({ text: 'test message' }));
const mockSubmitMessage = jest.fn();
const mockNewConversation = jest.fn();
const mockTextAreaRef = {
current: {
focus: jest.fn(),
setSelectionRange: jest.fn(),
} as unknown as HTMLTextAreaElement,
};
(useChatFormContext as jest.Mock).mockReturnValue({
setValue: mockSetValue,
getValues: jest.fn().mockReturnValue(''),
handleSubmit: mockHandleSubmit,
});
(useSubmitMessage as jest.Mock).mockReturnValue({
submitMessage: mockSubmitMessage,
});
(useChatContext as jest.Mock).mockReturnValue({
conversation: { model: null, endpoint: null },
newConversation: mockNewConversation,
});
// Mock startup config to allow processing
(useQueryClient as jest.Mock).mockReturnValue({
getQueryData: jest.fn().mockImplementation((key) => {
if (Array.isArray(key) && key[0] === 'startupConfig') {
return { modelSpecs: { list: [] } };
}
if (key === 'startupConfig') {
return { modelSpecs: { list: [] } };
}
return null;
}),
});
setUrlParams({ q: 'hello world', submit: 'true', model: 'non-existent-model' });
// Execute
renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef }));
// First interval tick should process params but not submit
act(() => {
jest.advanceTimersByTime(100);
});
// Assert initial state
expect(mockSubmitMessage).not.toHaveBeenCalled(); // Not submitted yet
// Let the timeout happen naturally
act(() => {
// Advance timer to trigger the timeout in the hook
jest.advanceTimersByTime(3000); // MAX_SETTINGS_WAIT_MS
});
// Now the message should be submitted due to timeout
expect(mockSubmitMessage).toHaveBeenCalled();
});
it('should mark as submitted when no submit parameter is present', () => {
// Setup
const mockSetValue = jest.fn();
const mockHandleSubmit = jest.fn((callback) => () => callback({ text: 'test message' }));
const mockSubmitMessage = jest.fn();
const mockTextAreaRef = {
current: {
focus: jest.fn(),
setSelectionRange: jest.fn(),
} as unknown as HTMLTextAreaElement,
};
(useChatFormContext as jest.Mock).mockReturnValue({
setValue: mockSetValue,
getValues: jest.fn().mockReturnValue(''),
handleSubmit: mockHandleSubmit,
});
(useSubmitMessage as jest.Mock).mockReturnValue({
submitMessage: mockSubmitMessage,
});
// Mock startup config to allow processing
(useQueryClient as jest.Mock).mockReturnValue({
getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }),
});
setUrlParams({ model: 'gpt-4' }); // No submit=true
// Execute
renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef }));
// First interval tick should process params
act(() => {
jest.advanceTimersByTime(100);
});
// Assert initial state - submission should be marked as handled
expect(mockSubmitMessage).not.toHaveBeenCalled();
// Try to advance timer past the timeout
act(() => {
jest.advanceTimersByTime(4000);
});
// Submission still shouldn't happen
expect(mockSubmitMessage).not.toHaveBeenCalled();
});
it('should handle empty query parameters', () => {
// Setup
const mockSetValue = jest.fn();
const mockHandleSubmit = jest.fn();
const mockSubmitMessage = jest.fn();
// Force replaceState to be called
window.history.replaceState = jest.fn();
(useChatFormContext as jest.Mock).mockReturnValue({
setValue: mockSetValue,
getValues: jest.fn().mockReturnValue(''),
handleSubmit: mockHandleSubmit,
});
(useSubmitMessage as jest.Mock).mockReturnValue({
submitMessage: mockSubmitMessage,
});
// Mock startup config to allow processing
(useQueryClient as jest.Mock).mockReturnValue({
getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }),
});
setUrlParams({}); // Empty params
const mockTextAreaRef = {
current: {
focus: jest.fn(),
setSelectionRange: jest.fn(),
} as unknown as HTMLTextAreaElement,
};
// Execute
renderHook(() => useQueryParams({ textAreaRef: mockTextAreaRef }));
act(() => {
jest.advanceTimersByTime(100);
});
// Assert
expect(mockSetValue).not.toHaveBeenCalled();
expect(mockHandleSubmit).not.toHaveBeenCalled();
expect(mockSubmitMessage).not.toHaveBeenCalled();
expect(window.history.replaceState).toHaveBeenCalled();
});
});

View File

@@ -17,6 +17,10 @@ import { useChatContext, useChatFormContext } from '~/Providers';
import useSubmitMessage from '~/hooks/Messages/useSubmitMessage';
import store from '~/store';
/**
* Parses query parameter values, converting strings to their appropriate types.
* Handles boolean strings, numbers, and preserves regular strings.
*/
const parseQueryValue = (value: string) => {
if (value === 'true') {
return true;
@@ -30,6 +34,11 @@ const parseQueryValue = (value: string) => {
return value;
};
/**
* Processes and validates URL query parameters using schema definitions.
* Extracts valid settings based on tQueryParamsSchema and handles special endpoint cases
* for assistants and agents.
*/
const processValidSettings = (queryParams: Record<string, string>) => {
const validSettings = {} as TPreset;
@@ -64,6 +73,11 @@ const processValidSettings = (queryParams: Record<string, string>) => {
return validSettings;
};
/**
* Hook that processes URL query parameters to initialize chat with specified settings and prompt.
* Handles model switching, prompt auto-filling, and optional auto-submission with race condition protection.
* Supports immediate or deferred submission based on whether settings need to be applied first.
*/
export default function useQueryParams({
textAreaRef,
}: {
@@ -71,9 +85,17 @@ export default function useQueryParams({
}) {
const maxAttempts = 50;
const attemptsRef = useRef(0);
const MAX_SETTINGS_WAIT_MS = 3000;
const processedRef = useRef(false);
const pendingSubmitRef = useRef(false);
const settingsAppliedRef = useRef(false);
const submissionHandledRef = useRef(false);
const promptTextRef = useRef<string | null>(null);
const validSettingsRef = useRef<TPreset | null>(null);
const settingsTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const methods = useChatFormContext();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const getDefaultConversation = useDefaultConvo();
const modularChat = useRecoilValue(store.modularChat);
const availableTools = useRecoilValue(store.availableTools);
@@ -82,6 +104,11 @@ export default function useQueryParams({
const queryClient = useQueryClient();
const { conversation, newConversation } = useChatContext();
/**
* Applies settings from URL query parameters to create a new conversation.
* Handles model spec lookup, endpoint normalization, and conversation switching logic.
* Ensures tools compatibility and preserves existing conversation when appropriate.
*/
const newQueryConvo = useCallback(
(_newPreset?: TPreset) => {
if (!_newPreset) {
@@ -181,6 +208,85 @@ export default function useQueryParams({
],
);
/**
* Checks if all settings from URL parameters have been successfully applied to the conversation.
* Compares values from validSettings against the current conversation state, handling special properties.
* Returns true only when all relevant settings match the target values.
*/
const areSettingsApplied = useCallback(() => {
if (!validSettingsRef.current || !conversation) {
return false;
}
for (const [key, value] of Object.entries(validSettingsRef.current)) {
if (['presetOverride', 'iconURL', 'spec', 'modelLabel'].includes(key)) {
continue;
}
if (conversation[key] !== value) {
return false;
}
}
return true;
}, [conversation]);
/**
* Processes message submission exactly once, preventing duplicate submissions.
* Sets the prompt text, submits the message, and cleans up URL parameters afterward.
* Has internal guards to ensure it only executes once regardless of how many times it's called.
*/
const processSubmission = useCallback(() => {
if (submissionHandledRef.current || !pendingSubmitRef.current || !promptTextRef.current) {
return;
}
submissionHandledRef.current = true;
pendingSubmitRef.current = false;
methods.setValue('text', promptTextRef.current, { shouldValidate: true });
methods.handleSubmit((data) => {
if (data.text?.trim()) {
submitMessage(data);
const newUrl = window.location.pathname;
window.history.replaceState({}, '', newUrl);
console.log('Message submitted with conversation state:', conversation);
}
})();
}, [methods, submitMessage, conversation]);
useEffect(() => {
// Only proceed if we've already processed URL parameters but haven't yet handled submission
if (
!processedRef.current ||
submissionHandledRef.current ||
settingsAppliedRef.current ||
!validSettingsRef.current ||
!conversation
) {
return;
}
const allSettingsApplied = areSettingsApplied();
if (allSettingsApplied) {
settingsAppliedRef.current = true;
if (pendingSubmitRef.current) {
if (settingsTimeoutRef.current) {
clearTimeout(settingsTimeoutRef.current);
settingsTimeoutRef.current = null;
}
console.log('Settings fully applied, processing submission');
processSubmission();
}
}
}, [conversation, processSubmission, areSettingsApplied]);
useEffect(() => {
const processQueryParams = () => {
const queryParams: Record<string, string> = {};
@@ -217,31 +323,74 @@ export default function useQueryParams({
if (!startupConfig) {
return;
}
const { decodedPrompt, validSettings, shouldAutoSubmit } = processQueryParams();
const currentText = methods.getValues('text');
/** Clean up URL parameters after successful processing */
const { decodedPrompt, validSettings, shouldAutoSubmit } = processQueryParams();
if (!shouldAutoSubmit) {
submissionHandledRef.current = true;
}
/** Mark processing as complete and clean up as needed */
const success = () => {
const newUrl = window.location.pathname;
window.history.replaceState({}, '', newUrl);
const currentParams = new URLSearchParams(searchParams.toString());
currentParams.delete('prompt');
currentParams.delete('q');
currentParams.delete('submit');
setSearchParams(currentParams, { replace: true });
processedRef.current = true;
console.log('Parameters processed successfully');
clearInterval(intervalId);
// Only clean URL if there's no pending submission
if (!pendingSubmitRef.current) {
const newUrl = window.location.pathname;
window.history.replaceState({}, '', newUrl);
}
};
if (!currentText && decodedPrompt) {
methods.setValue('text', decodedPrompt, { shouldValidate: true });
textAreaRef.current.focus();
textAreaRef.current.setSelectionRange(decodedPrompt.length, decodedPrompt.length);
// Store settings for later comparison
if (Object.keys(validSettings).length > 0) {
validSettingsRef.current = validSettings;
}
// Save the prompt text for later use if needed
if (decodedPrompt) {
promptTextRef.current = decodedPrompt;
}
// Handle auto-submission
if (shouldAutoSubmit && decodedPrompt) {
if (Object.keys(validSettings).length > 0) {
// Settings are changing, defer submission
pendingSubmitRef.current = true;
// Set a timeout to handle the case where settings might never fully apply
settingsTimeoutRef.current = setTimeout(() => {
if (!submissionHandledRef.current && pendingSubmitRef.current) {
console.warn(
'Settings application timeout reached, proceeding with submission anyway',
);
processSubmission();
}
}, MAX_SETTINGS_WAIT_MS);
} else {
methods.setValue('text', decodedPrompt, { shouldValidate: true });
textAreaRef.current.focus();
textAreaRef.current.setSelectionRange(decodedPrompt.length, decodedPrompt.length);
// Auto-submit if the submit parameter is true
if (shouldAutoSubmit) {
methods.handleSubmit((data) => {
if (data.text?.trim()) {
submitMessage(data);
}
})();
}
} else if (decodedPrompt) {
methods.setValue('text', decodedPrompt, { shouldValidate: true });
textAreaRef.current.focus();
textAreaRef.current.setSelectionRange(decodedPrompt.length, decodedPrompt.length);
} else {
submissionHandledRef.current = true;
}
if (Object.keys(validSettings).length > 0) {
@@ -253,6 +402,19 @@ export default function useQueryParams({
return () => {
clearInterval(intervalId);
if (settingsTimeoutRef.current) {
clearTimeout(settingsTimeoutRef.current);
}
};
}, [searchParams, methods, textAreaRef, newQueryConvo, newConversation, submitMessage]);
}, [
searchParams,
methods,
textAreaRef,
newQueryConvo,
newConversation,
submitMessage,
setSearchParams,
queryClient,
processSubmission,
]);
}

View File

@@ -55,8 +55,12 @@ export default function useSideNavLinks({
const links: NavLink[] = [];
if (
isAssistantsEndpoint(endpoint) &&
endpointsConfig?.[EModelEndpoint.assistants] &&
endpointsConfig[EModelEndpoint.assistants].disableBuilder !== true &&
((endpoint === EModelEndpoint.assistants &&
endpointsConfig?.[EModelEndpoint.assistants] &&
endpointsConfig[EModelEndpoint.assistants].disableBuilder !== true) ||
(endpoint === EModelEndpoint.azureAssistants &&
endpointsConfig?.[EModelEndpoint.azureAssistants] &&
endpointsConfig[EModelEndpoint.azureAssistants].disableBuilder !== true)) &&
keyProvided
) {
links.push({

View File

@@ -467,6 +467,14 @@ export default function useEventHandlers({
[QueryKeys.messages, conversation.conversationId],
finalMessages,
);
} else if (
isAssistantsEndpoint(submissionConvo.endpoint) &&
(!submissionConvo.conversationId || submissionConvo.conversationId === Constants.NEW_CONVO)
) {
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation.conversationId],
[...currentMessages],
);
}
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

View File

@@ -47,10 +47,10 @@
"com_assistants_delete_actions_error": "Beim Löschen der Aktion ist ein Fehler aufgetreten.",
"com_assistants_delete_actions_success": "Aktion erfolgreich vom Assistenten gelöscht",
"com_assistants_description_placeholder": "Optional: Beschreibe deinen Assistenten hier",
"com_assistants_domain_info": "Assistent hat diese Information an {{0}} gesendet",
"com_assistants_domain_info": "Agent hat diese Information an {{0}} gesendet",
"com_assistants_file_search": "Dateisuche",
"com_assistants_file_search_info": "Die Dateisuche ermöglicht dem Assistenten, Wissen aus Dateien zu nutzen, die du oder deine Benutzer hochladen. Sobald eine Datei hochgeladen wurde, entscheidet der Assistent automatisch, wann er basierend auf Benutzeranfragen Inhalte abruft. Das Anhängen von Vektor-Speichern für die Dateisuche wird noch nicht unterstützt. Sie können sie vom Provider-Playground aus anhängen oder Dateien für die Dateisuche auf Thread-Basis an Nachrichten anhängen.",
"com_assistants_function_use": "Assistent hat {{0}} verwendet",
"com_assistants_function_use": "Agent hat {{0}} verwendet",
"com_assistants_image_vision": "Bildanalyse",
"com_assistants_instructions_placeholder": "Die Systemanweisungen, die der Assistent verwendet",
"com_assistants_knowledge": "Wissen",
@@ -478,8 +478,8 @@
"com_ui_agent_shared_to_all": "Hier muss etwas eingegeben werden. War leer.",
"com_ui_agent_var": "{{0}} Agent",
"com_ui_agents": "Agenten",
"com_ui_agents_allow_create": "Erstellung von Assistenten erlauben",
"com_ui_agents_allow_share_global": "Assistenten für alle Nutzenden freigeben",
"com_ui_agents_allow_create": "Erlaube Agents zu erstellen",
"com_ui_agents_allow_share_global": "Erlaube das Teilen von Agenten mit allen Nutzern",
"com_ui_agents_allow_use": "Verwendung von Agenten erlauben",
"com_ui_all": "alle",
"com_ui_all_proper": "Alle",
@@ -487,6 +487,7 @@
"com_ui_analyzing_finished": "Analyse abgeschlossen",
"com_ui_api_key": "API-Schlüssel",
"com_ui_archive": "Archivieren",
"com_ui_archive_delete_error": "Archivierter Chat konnte nicht gelöscht werden.",
"com_ui_archive_error": "Konversation konnte nicht archiviert werden",
"com_ui_artifact_click": "Zum Öffnen klicken",
"com_ui_artifacts": "Artefakte",
@@ -559,6 +560,7 @@
"com_ui_context": "Kontext",
"com_ui_continue": "Fortfahren",
"com_ui_controls": "Steuerung",
"com_ui_convo_delete_error": "Unterhaltung konnte nicht gelöscht werden.",
"com_ui_copied": "Kopiert!",
"com_ui_copied_to_clipboard": "In die Zwischenablage kopiert",
"com_ui_copy_code": "Code kopieren",
@@ -648,10 +650,15 @@
"com_ui_fork_info_2": "\"Abzweigen\" bezieht sich auf das Erstellen einer neuen Konversation, die von bestimmten Nachrichten in der aktuellen Konversation ausgeht/endet und eine Kopie gemäß den ausgewählten Optionen erstellt.",
"com_ui_fork_info_3": "Die \"Zielnachricht\" bezieht sich entweder auf die Nachricht, von der aus dieses Popup geöffnet wurde, oder, wenn du \"{{0}}\" aktivierst, auf die letzte Nachricht in der Konversation.",
"com_ui_fork_info_branches": "Diese Option zweigt die sichtbaren Nachrichten zusammen mit zugehörigen Verzweigungen ab; mit anderen Worten, den direkten Pfad zur Zielnachricht, einschließlich der Verzweigungen entlang des Pfades.",
"com_ui_fork_info_button_label": "Informationen zum Abspalten von Chats anzeigen",
"com_ui_fork_info_remember": "Aktiviere dies, um sich die von dir ausgewählten Optionen für zukünftige Verwendung zu merken, um das Abzweigen von Konversationen nach deinen Vorlieben zu beschleunigen.",
"com_ui_fork_info_start": "Wenn aktiviert, beginnt das Abzweigen von dieser Nachricht bis zur letzten Nachricht in der Konversation, gemäß dem oben ausgewählten Verhalten.",
"com_ui_fork_info_target": "Diese Option zweigt alle Nachrichten ab, die zur Zielnachricht führen, einschließlich ihrer Nachbarn; mit anderen Worten, alle Nachrichtenverzweigungen werden einbezogen, unabhängig davon, ob sie sichtbar sind oder sich auf demselben Pfad befinden.",
"com_ui_fork_info_visible": "Diese Option zweigt nur die sichtbaren Nachrichten ab; mit anderen Worten, den direkten Pfad zur Zielnachricht, ohne jegliche Verzweigungen.",
"com_ui_fork_more_details_about": "Zusätzliche Informationen und Details zur Abspaltungsoption '{{0}}' anzeigen",
"com_ui_fork_more_info_options": "Detaillierte Erklärung aller Abspaltungsoptionen und ihres Verhaltens anzeigen",
"com_ui_fork_more_info_remember": "Erklärung anzeigen, wie die Option \"{{0}}\" deine Einstellungen für zukünftige Abspaltungen speichert",
"com_ui_fork_more_info_split_target": "Erklärung anzeigen, wie die Option \"{{0}}\" beeinflusst, welche Nachrichten in deiner Abspaltung enthalten sind",
"com_ui_fork_processing": "Konversation wird abgezweigt...",
"com_ui_fork_remember": "Merken",
"com_ui_fork_remember_checked": "Ihre Auswahl wird nach der Verwendung gespeichert. Du kannst dies jederzeit in den Einstellungen ändern.",
@@ -705,6 +712,7 @@
"com_ui_name": "Name",
"com_ui_new": "Neu",
"com_ui_new_chat": "Neuer Chat",
"com_ui_new_conversation_title": "Neuer Titel des Chats",
"com_ui_next": "Weiter",
"com_ui_no": "Nein",
"com_ui_no_backup_codes": "Keine Backup-Codes verfügbar. Bitte erstelle neue.",
@@ -748,6 +756,8 @@
"com_ui_regenerating": "Generiere neu ...",
"com_ui_region": "Region",
"com_ui_rename": "Umbenennen",
"com_ui_rename_conversation": "Chat umbenennen",
"com_ui_rename_failed": "Chat konnte nicht umbenannt werden.",
"com_ui_rename_prompt": "Prompt umbenennen",
"com_ui_requires_auth": "Authentifizierung erforderlich",
"com_ui_reset_var": "{{0}} zurücksetzen",
@@ -800,8 +810,14 @@
"com_ui_sign_in_to_domain": "Anmelden bei {{0}}",
"com_ui_simple": "Einfach",
"com_ui_size": "Größe",
"com_ui_special_var_current_date": "Aktuelles Datum",
"com_ui_special_var_current_datetime": "Aktuelles Datum & Uhrzeit",
"com_ui_special_var_current_user": "Aktueller Nutzer",
"com_ui_special_var_iso_datetime": "UTC ISO Datum/Zeit",
"com_ui_special_variables": "Spezielle Variablen:",
"com_ui_special_variables_more_info": "Du kannst spezielle Variablen aus den Dropdown-Menüs auswählen: `{{current_date}}` (heutiges Datum und Wochentag), `{{current_datetime}}` (offizielles Datum und Uhrzeit), `{{utc_iso_datetime}}` (UTC ISO Datum/Zeit) und `{{current_user}}` (dein Kontoname).",
"com_ui_speech_while_submitting": "Spracheingabe nicht möglich während eine Antwort generiert wird",
"com_ui_sr_actions_menu": "Aktionsmenü für \"{{0}}\" öffnen",
"com_ui_stop": "Stopp",
"com_ui_storage": "Speicher",
"com_ui_submit": "Absenden",
@@ -818,6 +834,7 @@
"com_ui_unarchive": "Aus Archiv holen",
"com_ui_unarchive_error": "Konversation konnte nicht aus dem Archiv geholt werden",
"com_ui_unknown": "Unbekannt",
"com_ui_untitled": "Unbenannt",
"com_ui_update": "Aktualisieren",
"com_ui_upload": "Hochladen",
"com_ui_upload_code_files": "Hochladen für Code-Interpreter",

View File

@@ -211,7 +211,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 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": "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_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.",
@@ -361,7 +361,10 @@
"com_nav_lang_arabic": "العربية",
"com_nav_lang_auto": "Auto detect",
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
"com_nav_lang_catalan": "Català",
"com_nav_lang_chinese": "中文",
"com_nav_lang_czech": "Čeština",
"com_nav_lang_danish": "Dansk",
"com_nav_lang_dutch": "Nederlands",
"com_nav_lang_english": "English",
"com_nav_lang_estonian": "Eesti keel",

View File

@@ -11,6 +11,7 @@
"com_agents_create_error": "Hubo un error al crear su agente.",
"com_agents_description_placeholder": "Opcional: Describa su Agente aquí",
"com_agents_enable_file_search": "Habilitar búsqueda de archivos",
"com_agents_file_context_disabled": "Es necesario crear el Agente antes de subir archivos.",
"com_agents_file_search_disabled": "Es necesario crear el Agente antes de subir archivos para la Búsqueda de Archivos.",
"com_agents_file_search_info": "Cuando está habilitado, se informará al agente sobre los nombres exactos de los archivos listados a continuación, permitiéndole recuperar el contexto relevante de estos archivos.",
"com_agents_instructions_placeholder": "Las instrucciones del sistema que utiliza el agente",
@@ -37,7 +38,7 @@
"com_assistants_code_interpreter_info": "El Intérprete de Código permite al asistente escribir y ejecutar código. Esta herramienta puede procesar archivos con diversos formatos y datos, y generar archivos como gráficos.",
"com_assistants_completed_action": "Hablé con {{0}}",
"com_assistants_completed_function": "Ejecuté {{0}}",
"com_assistants_conversation_starters": "Iniciadores de Conversación",
"com_assistants_conversation_starters": "Iniciadores de conversación",
"com_assistants_conversation_starters_placeholder": "Ingrese un iniciador de conversación",
"com_assistants_create_error": "Hubo un error al crear su asistente.",
"com_assistants_create_success": "Creado con éxito",
@@ -239,11 +240,14 @@
"com_endpoint_prompt_prefix_placeholder": "Configurar instrucciones personalizadas o contexto. Se ignora si está vacío.",
"com_endpoint_save_as_preset": "Guardar como configuración preestablecida",
"com_endpoint_search": "Buscar punto de conexión por nombre",
"com_endpoint_search_models": "Buscar modelos...",
"com_endpoint_search_var": "Buscar {{0}}...",
"com_endpoint_set_custom_name": "Establece un nombre personalizado, en caso de que puedas encontrar esta configuración preestablecida",
"com_endpoint_skip_hover": "Habilitar omitir el paso de finalización, que revisa la respuesta final y los pasos generados",
"com_endpoint_stop": "Secuencias de detención",
"com_endpoint_stop_placeholder": "Separe los valores presionando `Intro`",
"com_endpoint_temperature": "Temperatura",
"com_endpoint_thinking": "Pensando",
"com_endpoint_top_k": "Top K",
"com_endpoint_top_p": "Top P",
"com_endpoint_use_active_assistant": "Utilizar asistente activo",
@@ -265,11 +269,12 @@
"com_files_number_selected": "{{0}} de {{1}} archivo(s) seleccionado(s)",
"com_generated_files": "Archivos generados:",
"com_hide_examples": "Ocultar ejemplos",
"com_nav_2fa": "Autenticación en dos pasos",
"com_nav_account_settings": "Configuración de la cuenta",
"com_nav_always_make_prod": "Convertir siempre las nuevas versiones en producción",
"com_nav_archive_created_at": "CreadoEn",
"com_nav_archive_name": "Nombre",
"com_nav_archived_chats": "Archivadas",
"com_nav_archived_chats": "Conversaciones archivadas",
"com_nav_at_command": "Comando @",
"com_nav_at_command_description": "Alternar comando \"@\" para cambiar entre puntos de conexión, modelos, ajustes predefinidos, etc.",
"com_nav_audio_play_error": "Error al reproducir el audio: {{0}}",
@@ -418,6 +423,8 @@
"com_sidepanel_hide_panel": "Ocultar Panel",
"com_sidepanel_manage_files": "Administrar Archivos",
"com_sidepanel_parameters": "Parámetros",
"com_ui_2fa_disable": "Deshabilitar 2FA",
"com_ui_2fa_disable_error": "Hubo en error deshabilitando la autenticación en dos pasos",
"com_ui_2fa_enable": "Activa 2FA",
"com_ui_2fa_enabled": "2FA ha sido activada",
"com_ui_accept": "Acepto",
@@ -428,18 +435,22 @@
"com_ui_admin_access_warning": "Deshabilitar el acceso de Administrador a esta función puede causar problemas inesperados en la interfaz que requieran actualizar la página. Si se guarda este cambio, la única forma de revertirlo es mediante la configuración de interfaz en el archivo librechat.yaml, lo cual afectará a todos los roles.",
"com_ui_admin_settings": "Configuración de Administrador",
"com_ui_advanced": "Avanzado",
"com_ui_advanced_settings": "Configuración avanzada",
"com_ui_agent": "Agente",
"com_ui_agent_delete_error": "Se produjo un error al eliminar el agente",
"com_ui_agent_deleted": "Asistente eliminado exitosamente",
"com_ui_agent_duplicate_error": "Se produjo un error al duplicar el asistente",
"com_ui_agent_duplicated": "Agente duplicado exitosamente",
"com_ui_agent_editing_allowed": "Otros usuarios ya pueden editar este agente",
"com_ui_agent_var": "{{0}} agente",
"com_ui_agents": "Agentes",
"com_ui_agents_allow_create": "Permitir la creación de Agentes",
"com_ui_agents_allow_share_global": "Permitir compartir Agentes con todos los usuarios",
"com_ui_agents_allow_use": "Permitir el uso de Agentes",
"com_ui_all": "todas",
"com_ui_all_proper": "Todos",
"com_ui_analyzing": "Analizando",
"com_ui_analyzing_finished": "Acabando el análisis",
"com_ui_archive": "Archivar",
"com_ui_archive_error": "Error al archivar la conversación",
"com_ui_artifact_click": "Haga clic para abrir",
@@ -455,6 +466,7 @@
"com_ui_attach_error_openai": "No se pueden adjuntar archivos del Asistente a otros puntos de conexión",
"com_ui_attach_error_size": "Se excedió el límite de tamaño de archivo para el endpoint:",
"com_ui_attach_error_type": "Tipo de archivo no admitido para el endpoint:",
"com_ui_attach_remove": "Eliminar archivo",
"com_ui_attach_warn_endpoint": "Es posible que los archivos no compatibles con la herramienta sean ignorados",
"com_ui_attachment": "Adjunto",
"com_ui_authentication": "Autenticación",
@@ -485,11 +497,13 @@
"com_ui_clear": "Limpiar",
"com_ui_clear_all": "Limpiar todo",
"com_ui_close": "Cerrar",
"com_ui_close_menu": "Cerrar menú",
"com_ui_code": "Código",
"com_ui_collapse_chat": "Contraer Chat",
"com_ui_command_placeholder": "Opcional: Ingrese un comando para el prompt o se utilizará el nombre",
"com_ui_command_usage_placeholder": "Seleccione un Prompt por comando o nombre",
"com_ui_confirm_action": "Confirmar Acción",
"com_ui_confirm_change": "Confirmar cambio",
"com_ui_context": "Contexto",
"com_ui_continue": "Continuar",
"com_ui_controls": "Controles",
@@ -536,6 +550,7 @@
"com_ui_descending": "Desc",
"com_ui_description": "Descripción",
"com_ui_description_placeholder": "Opcional: Ingrese una descripción para mostrar en el prompt",
"com_ui_download": "Descargar",
"com_ui_download_error": "Hubo un error al descargar el archivo. Es posible que el archivo haya sido eliminado.",
"com_ui_dropdown_variables": "Variables desplegables:",
"com_ui_dropdown_variables_info": "Cree menús desplegables personalizados para sus prompts: `{{nombre_variable:opción1|opción2|opción3}}`",
@@ -556,7 +571,9 @@
"com_ui_examples": "Ejemplos",
"com_ui_export_convo_modal": "Exportar Conversación",
"com_ui_field_required": "Este campo es obligatorio",
"com_ui_filter_prompts": "Filtrar Prompts",
"com_ui_filter_prompts_name": "Filtrar prompts por nombre",
"com_ui_finance": "Finanzas",
"com_ui_fork": "Bifurcar",
"com_ui_fork_all_target": "Incluir todo desde/hacia aquí",
"com_ui_fork_branches": "Incluir ramas relacionadas",
@@ -579,9 +596,15 @@
"com_ui_fork_split_target_setting": "Iniciar bifurcación desde el mensaje objetivo de forma predeterminada",
"com_ui_fork_success": "Se ha bifurcado la conversación con éxito",
"com_ui_fork_visible": "Mostrar únicamente mensajes visibles",
"com_ui_generating": "Generando...",
"com_ui_go_back": "Volver",
"com_ui_go_to_conversation": "Ir a la conversación",
"com_ui_good_afternoon": "Buenas tardes",
"com_ui_good_evening": "Buenas noches",
"com_ui_good_morning": "Buenos días",
"com_ui_happy_birthday": "¡Es mi primer cumpleaños!",
"com_ui_host": "Host",
"com_ui_idea": "Ideas",
"com_ui_image_gen": "Gen Imágenes",
"com_ui_import_conversation_error": "Hubo un error al importar tus chats",
"com_ui_import_conversation_file_type_error": "com_ui_import_conversation_file_type_error: Tipo de archivo no compatible para importar",
@@ -591,9 +614,11 @@
"com_ui_input": "Entrada",
"com_ui_instructions": "Instrucciones",
"com_ui_latest_footer": "IA para todos.",
"com_ui_latest_version": "Última versión",
"com_ui_librechat_code_api_key": "Obtenga su clave API del Intérprete de Código de LibreChat",
"com_ui_librechat_code_api_subtitle": "Seguro. Multilenguaje. Archivos de entrada/salida.",
"com_ui_librechat_code_api_title": "Ejecutar Código IA",
"com_ui_loading": "Cargando...",
"com_ui_locked": "Bloqueado",
"com_ui_logo": "Logotipo de {{0}}",
"com_ui_manage": "Administrar",
@@ -605,6 +630,7 @@
"com_ui_more_info": "Más información",
"com_ui_my_prompts": "Mis Prompts",
"com_ui_name": "Nombre",
"com_ui_new": "Nuevo",
"com_ui_new_chat": "Nuevo Chat",
"com_ui_next": "Sig",
"com_ui_no": "No",
@@ -612,6 +638,7 @@
"com_ui_no_category": "Sin categoría",
"com_ui_no_changes": "No hay cambios para actualizar",
"com_ui_no_terms_content": "No hay contenido de términos y condiciones para mostrar",
"com_ui_none": "Ninguno",
"com_ui_nothing_found": "No se encontró nada",
"com_ui_of": "de",
"com_ui_off": "Desactivado",
@@ -636,8 +663,11 @@
"com_ui_provider": "Proveedor",
"com_ui_read_aloud": "Leer en voz alta",
"com_ui_regenerate": "Regenerar",
"com_ui_regenerating": "Regenerando...",
"com_ui_region": "Región",
"com_ui_rename": "Renombrar",
"com_ui_rename_conversation": "Renombrar conversación",
"com_ui_rename_prompt": "Renombrar prompt",
"com_ui_reset_var": "Restablecer {{0}}",
"com_ui_result": "Resultado",
"com_ui_revoke": "Revocar",
@@ -653,6 +683,7 @@
"com_ui_save_submit": "Guardar y Enviar",
"com_ui_saved": "¡Guardado!",
"com_ui_schema": "Esquema",
"com_ui_search": "Buscar",
"com_ui_select": "Seleccionar",
"com_ui_select_file": "Seleccionar un archivo",
"com_ui_select_model": "Seleccionar un modelo",
@@ -673,6 +704,8 @@
"com_ui_share_var": "Compartir {{0}}",
"com_ui_shared_link_not_found": "Enlace compartido no encontrado",
"com_ui_shared_prompts": "Prompts Compartidos",
"com_ui_shop": "Compras",
"com_ui_show": "Mostrar",
"com_ui_show_all": "Mostrar Todo",
"com_ui_simple": "Simple",
"com_ui_size": "Tamaño",
@@ -681,9 +714,12 @@
"com_ui_stop": "Detener",
"com_ui_storage": "Almacenamiento",
"com_ui_submit": "Enviar",
"com_ui_teach_or_explain": "Aprendizaje",
"com_ui_terms_and_conditions": "Términos y Condiciones",
"com_ui_terms_of_service": "Términos de servicio",
"com_ui_thinking": "Pensando...",
"com_ui_tools": "Herramientas",
"com_ui_travel": "Viaje",
"com_ui_unarchive": "Desarchivar",
"com_ui_unarchive_error": "Error al desarchivar la conversación",
"com_ui_unknown": "Desconocido",
@@ -704,8 +740,12 @@
"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",
"com_ui_version_var": "Versión {{0}}",
"com_ui_versions": "Versiones",
"com_ui_weekend_morning": "Feliz fin de semana",
"com_ui_write": "Escribiendo",
"com_ui_x_selected": "{{0}} seleccionado",
"com_ui_yes": "Sí",
"com_ui_zoom": "Zoom",
"com_user_message": "Usted",

View File

@@ -375,6 +375,7 @@
"com_nav_lang_italian": "Italiano",
"com_nav_lang_japanese": "日本語",
"com_nav_lang_korean": "한국어",
"com_nav_lang_persian": "فارسی",
"com_nav_lang_polish": "Polski",
"com_nav_lang_portuguese": "Português",
"com_nav_lang_russian": "Русский",
@@ -487,6 +488,7 @@
"com_ui_analyzing_finished": "Analüüs lõpetatud",
"com_ui_api_key": "API võti",
"com_ui_archive": "Arhiveeri",
"com_ui_archive_delete_error": "Arhiveeritud vestluse kustutamine ebaõnnestus",
"com_ui_archive_error": "Vestluse arhiveerimine ebaõnnestus",
"com_ui_artifact_click": "Klõpsa avamiseks",
"com_ui_artifacts": "Artefaktid",
@@ -540,6 +542,7 @@
"com_ui_bulk_delete_error": "Jagatud linkide kustutamine ebaõnnestus",
"com_ui_callback_url": "Tagasikutsumise URL",
"com_ui_cancel": "Tühista",
"com_ui_category": "Kategooria",
"com_ui_chat": "Vestlus",
"com_ui_chat_history": "Vestluse ajalugu",
"com_ui_clear": "Tühjenda",
@@ -559,6 +562,7 @@
"com_ui_context": "Kontekst",
"com_ui_continue": "Jätka",
"com_ui_controls": "Juhtelemendid",
"com_ui_convo_delete_error": "Vestluse kustutamine ebaõnnestus",
"com_ui_copied": "Kopeeritud!",
"com_ui_copied_to_clipboard": "Kopeeritud lõikepuhvrisse",
"com_ui_copy_code": "Kopeeri kood",
@@ -648,10 +652,15 @@
"com_ui_fork_info_2": "\"Hargnemine\" viitab uue vestluse loomisele, mis algab/lõpeb praeguse vestluse konkreetsetest sõnumitest, luues koopia vastavalt valitud valikutele.",
"com_ui_fork_info_3": "\"Sihtsõnum\" viitab kas sõnumile, millest see hüpikaken avati, või, kui märgid \"{{0}}\", vestluse viimasele sõnumile.",
"com_ui_fork_info_branches": "See valik hargneb nähtavad sõnumid koos seotud harudega; teisisõnu, otsene tee sihtsõnumini, sealhulgas harud mööda teed.",
"com_ui_fork_info_button_label": "Vaata teavet vestluste hargnemise kohta",
"com_ui_fork_info_remember": "Märgi see, et jätta meelde valitud valikud edaspidiseks kasutamiseks, muutes vestluste hargnemise eelistatud viisil kiiremaks.",
"com_ui_fork_info_start": "Kui see on märgitud, algab hargnemine sellest sõnumist vestluse viimase sõnumini vastavalt ülalvalitud käitumisele.",
"com_ui_fork_info_target": "See valik hargneb kõik sõnumid, mis viivad sihtsõnumini, kaasa arvatud selle naabrid; teisisõnu, kõik sõnumiharud, olenemata sellest, kas need on nähtavad või samal teel, on kaasatud.",
"com_ui_fork_info_visible": "See valik hargneb ainult nähtavad sõnumid; teisisõnu, otsene tee sihtsõnumini, ilma harudeta.",
"com_ui_fork_more_details_about": "Vaata lisateavet ja üksikasju \"{{0}}\" hargnemisvaliku kohta",
"com_ui_fork_more_info_options": "Vaata kõigi hargnemisvalikute ja nende käitumise üksikasjalikku selgitust",
"com_ui_fork_more_info_remember": "Vaata selgitust, kuidas \"{{0}}\" valik salvestab sinu eelistused tulevasteks hargnemisteks",
"com_ui_fork_more_info_split_target": "Vaata selgitust, kuidas valik \"{{0}}\" mõjutab, millised sõnumid kaasatakse sinu hargnemisse",
"com_ui_fork_processing": "Vestlust hargnetakse...",
"com_ui_fork_remember": "Jäta meelde",
"com_ui_fork_remember_checked": "Sinu valik jäetakse pärast kasutamist meelde. Muuda seda igal ajal seadetes.",
@@ -705,6 +714,7 @@
"com_ui_name": "Nimi",
"com_ui_new": "Uus",
"com_ui_new_chat": "Uus vestlus",
"com_ui_new_conversation_title": "Uus vestluse pealkiri",
"com_ui_next": "Järgmine",
"com_ui_no": "Ei",
"com_ui_no_backup_codes": "Varukoodid puuduvad. Palun loo uued",
@@ -748,6 +758,8 @@
"com_ui_regenerating": "Uuesti loomine...",
"com_ui_region": "Piirkond",
"com_ui_rename": "Nimeta ümber",
"com_ui_rename_conversation": "Nimeta vestlus ümber",
"com_ui_rename_failed": "Ei õnnestunud vestlust ümber nimetada",
"com_ui_rename_prompt": "Nimeta sisend ümber",
"com_ui_requires_auth": "Vajab autentimist",
"com_ui_reset_var": "Lähtesta {{0}}",
@@ -800,8 +812,14 @@
"com_ui_sign_in_to_domain": "Logi sisse {{0}}",
"com_ui_simple": "Lihtne",
"com_ui_size": "Suurus",
"com_ui_special_var_current_date": "Praegune kuupäev",
"com_ui_special_var_current_datetime": "Praegune kuupäev ja kellaaeg",
"com_ui_special_var_current_user": "Praegune kasutaja",
"com_ui_special_var_iso_datetime": "UTC ISO kuupäev ja kellaaeg",
"com_ui_special_variables": "Erilised muutujad:",
"com_ui_special_variables_more_info": "Saad rippmenüüst valida erilisi muutujaid: `{{current_date}}` (tänane kuupäev ja nädalapäev), `{{current_datetime}}` (kohalik kuupäev ja kellaaeg), `{{utc_iso_datetime}}` (UTC ISO kuupäev ja kellaaeg) ja `{{current_user}}` (sinu kasutaja nimi).",
"com_ui_speech_while_submitting": "Kõnet ei saa esitada, kui vastust genereeritakse",
"com_ui_sr_actions_menu": "Ava tegevuste menüü \"{{0}}\" jaoks",
"com_ui_stop": "Peata",
"com_ui_storage": "Salvestusruum",
"com_ui_submit": "Esita",
@@ -818,6 +836,7 @@
"com_ui_unarchive": "Arhiveeri lahti",
"com_ui_unarchive_error": "Vestluse arhiveerimine lahti ebaõnnestus",
"com_ui_unknown": "Tundmatu",
"com_ui_untitled": "Pealkirjata",
"com_ui_update": "Uuenda",
"com_ui_upload": "Laadi üles",
"com_ui_upload_code_files": "Laadi üles koodiinterpreteerija jaoks",

View File

@@ -9,6 +9,9 @@
"com_agents_create_error": "Une erreur s'est produite lors de la création de votre agent.",
"com_agents_description_placeholder": "Décrivez votre Agent ici (facultatif)",
"com_agents_enable_file_search": "Activer la recherche de fichiers",
"com_agents_file_context": "Contexte du fichier (OCR)",
"com_agents_file_context_disabled": "L'agent doit être créé avant de charger des fichiers pour le contexte de fichiers.",
"com_agents_file_context_info": "Les fichiers téléchargés en tant que \"Contexte\" sont traités à l'aide de l'OCR pour en extraire le texte, qui est ensuite ajouté aux instructions de l'agent. Idéal pour les documents, les images contenant du texte ou les PDF pour lesquels vous avez besoin du contenu textuel complet d'un fichier.",
"com_agents_file_search_disabled": "L'agent doit être créé avant de pouvoir télécharger des fichiers pour la Recherche de Fichiers.",
"com_agents_file_search_info": "Lorsque cette option est activée, l'agent sera informé des noms exacts des fichiers listés ci-dessous, lui permettant d'extraire le contexte pertinent de ces fichiers.",
"com_agents_instructions_placeholder": "Les instructions système que l'agent utilise",
@@ -18,11 +21,13 @@
"com_agents_not_available": "Agent non disponible",
"com_agents_search_name": "Rechercher des agents par nom",
"com_agents_update_error": "Une erreur s'est produite lors de la mise à jour de votre agent",
"com_assistants_action_attempt": "L'assistant souhaite s'entretenir avec {{0}}",
"com_assistants_actions": "Actions",
"com_assistants_actions_disabled": "Vous devez créer un assistant avant d'ajouter des actions.",
"com_assistants_actions_info": "Permettez à votre Assistant de récupérer des informations ou d'effectuer des actions via des API",
"com_assistants_add_actions": "Ajouter des actions",
"com_assistants_add_tools": "Ajouter des outils",
"com_assistants_allow_sites_you_trust": "N'autorisez que les sites en lesquels vous avez confiance.",
"com_assistants_append_date": "Ajouter la date et l'heure actuelles",
"com_assistants_append_date_tooltip": "Lorsque activé, la date et l'heure actuelles du client seront ajoutées aux instructions du système de l'assistant.",
"com_assistants_available_actions": "Actions disponibles",
@@ -82,6 +87,7 @@
"com_auth_email_verification_redirecting": "Redirection dans {{0}} secondes...",
"com_auth_email_verification_resend_prompt": "Vous n'avez pas reçu de courriel ?",
"com_auth_email_verification_success": "Courriel vérifié avec succès",
"com_auth_email_verifying_ellipsis": "Vérification...",
"com_auth_error_create": "Il y a eu une erreur lors de la tentative d'enregistrement de votre compte. Veuillez réessayer.",
"com_auth_error_invalid_reset_token": "Ce jeton de réinitialisation de mot de passe n'est plus valide.",
"com_auth_error_login": "Impossible de se connecter avec les informations fournies. Veuillez vérifier vos identifiants et réessayer.",
@@ -118,9 +124,11 @@
"com_auth_submit_registration": "Soumettre l'inscription",
"com_auth_to_reset_your_password": "pour réinitialiser votre mot de passe.",
"com_auth_to_try_again": "pour réessayer.",
"com_auth_two_factor": "Consultez votre application préférée de mot de passe à usage unique pour obtenir un code.",
"com_auth_username": "Nom d'utilisateur",
"com_auth_username_max_length": "Le nom d'utilisateur doit être inférieur à 20 caractères",
"com_auth_username_min_length": "Le nom d'utilisateur doit comporter au moins 3 caractères",
"com_auth_verify_your_identity": "Vérifiez votre identité",
"com_auth_welcome_back": "Content de te revoir",
"com_click_to_download": "(cliquez ici pour télécharger)",
"com_download_expired": "Téléchargement expiré",
@@ -133,6 +141,8 @@
"com_endpoint_anthropic_maxoutputtokens": "Nombre maximum de jetons qui peuvent être générés dans la réponse. Spécifiez une valeur plus faible pour des réponses plus courtes et une valeur plus élevée pour des réponses plus longues.",
"com_endpoint_anthropic_prompt_cache": "La mise en cache des prompts permet de réutiliser les contextes ou instructions volumineux entre les appels API, réduisant ainsi les coûts et la latence",
"com_endpoint_anthropic_temp": "Varie de 0 à 1. Utilisez une température proche de 0 pour l'analyse / le choix multiple, et proche de 1 pour les tâches créatives et génératives. Nous vous recommandons de modifier ceci ou Top P mais pas les deux.",
"com_endpoint_anthropic_thinking": "Activez le raisonnement pour les modèles Claude pris en charge (3.7 Sonnet). Note : nécessite que le \"l'option de réflexion\" soit activée et inférieur au \"nombre maximum de jetons de sortie\".",
"com_endpoint_anthropic_thinking_budget": "Détermine le nombre maximum de jetons que Claude est autorisé à utiliser pour son processus de raisonnement interne. Des budgets plus importants peuvent améliorer la qualité des réponses en permettant une analyse plus approfondie des problèmes complexes, bien que Claude puisse ne pas utiliser la totalité du budget alloué, en particulier dans les plages supérieures à 32K. Ce paramètre doit être inférieur à \"Max Output Tokens\".",
"com_endpoint_anthropic_topk": "Top-k change la façon dont le modèle sélectionne les jetons pour la sortie. Un top-k de 1 signifie que le jeton sélectionné est le plus probable parmi tous les jetons du vocabulaire du modèle (également appelé décodage glouton), tandis qu'un top-k de 3 signifie que le jeton suivant est sélectionné parmi les 3 jetons les plus probables (en utilisant la température).",
"com_endpoint_anthropic_topp": "Top-p change la façon dont le modèle sélectionne les jetons pour la sortie. Les jetons sont sélectionnés du plus K (voir le paramètre topK) probable au moins jusqu'à ce que la somme de leurs probabilités égale la valeur top-p.",
"com_endpoint_assistant": "Assistant de point de terminaison",
@@ -169,6 +179,8 @@
"com_endpoint_default_blank": "par défaut : vide",
"com_endpoint_default_empty": "par défaut : vide",
"com_endpoint_default_with_num": "par défaut : {{0}}",
"com_endpoint_deprecated_info": "Ce point de terminaison est obsolète et pourrait être supprimé dans les versions futures, veuillez utiliser le point de terminaison de l'agent à la place.",
"com_endpoint_deprecated_info_a11y": "Le point de terminaison du plugin est obsolète et pourrait être supprimé dans les versions futures, veuillez utiliser le point de terminaison de l'agent à la place.",
"com_endpoint_examples": " Exemples",
"com_endpoint_export": "Exporter",
"com_endpoint_export_share": "Exporter/Partager",
@@ -209,6 +221,7 @@
"com_endpoint_plug_use_functions": "Utiliser les fonctions",
"com_endpoint_presence_penalty": "Pénalité de présence",
"com_endpoint_preset": "préréglage",
"com_endpoint_preset_custom_name_placeholder": "il faut mettre quelque chose ici. c'était vide",
"com_endpoint_preset_default": "est maintenant le préréglage par défaut.",
"com_endpoint_preset_default_item": "Par défaut :",
"com_endpoint_preset_default_none": "Aucun préréglage par défaut actif.",
@@ -232,11 +245,16 @@
"com_endpoint_reasoning_effort": "Effort de raisonnement",
"com_endpoint_save_as_preset": "Enregistrer comme préréglage",
"com_endpoint_search": "Rechercher un endpoint par nom",
"com_endpoint_search_endpoint_models": "Recherche {{0}} modèles...",
"com_endpoint_search_models": "Recherche de modèles...",
"com_endpoint_search_var": "Recherche {{0}}...",
"com_endpoint_set_custom_name": "Définir un nom personnalisé, au cas où vous trouveriez ce préréglage",
"com_endpoint_skip_hover": "Activer le saut de l'étape de complétion, qui examine la réponse finale et les étapes générées",
"com_endpoint_stop": "Séquences d'arrêt",
"com_endpoint_stop_placeholder": "Séparez les valeurs en appuyant sur `Entrée`",
"com_endpoint_temperature": "Température",
"com_endpoint_thinking": "Je réfléchi",
"com_endpoint_thinking_budget": "Budget de réflexion",
"com_endpoint_top_k": "Top K",
"com_endpoint_top_p": "Top P",
"com_endpoint_use_active_assistant": "Utiliser l'assistant actif",
@@ -249,6 +267,7 @@
"com_error_files_upload_canceled": "La demande de téléversement du fichier a été annulée. Remarque : le téléversement peut être toujours en cours de traitement et devra être supprimé manuellement.",
"com_error_files_validation": "Une erreur s'est produite lors de la validation du fichier.",
"com_error_input_length": "Le nombre de jetons du dernier message est trop élevé et dépasse la limite autorisée ({{0}}). Veuillez raccourcir votre message, ajuster la taille maximale du contexte dans les paramètres de conversation, ou créer une nouvelle conversation pour continuer.",
"com_error_invalid_agent_provider": "Le \"fournisseur {{0}} \" n'est pas disponible pour les agents. Veuillez vous rendre dans les paramètres de votre agent et sélectionner un fournisseur actuellement disponible.",
"com_error_invalid_user_key": "Clé fournie non valide. Veuillez fournir une clé valide et réessayer.",
"com_error_moderation": "Il semble que le contenu soumis ait été signalé par notre système de modération pour ne pas être conforme à nos lignes directrices communautaires. Nous ne pouvons pas procéder avec ce sujet spécifique. Si vous avez d'autres questions ou sujets que vous souhaitez explorer, veuillez modifier votre message ou créer une nouvelle conversation.",
"com_error_no_base_url": "Aucune URL de base trouvée. Veuillez en fournir une et réessayer.",
@@ -256,8 +275,10 @@
"com_files_filter": "Filtrer les fichiers...",
"com_files_no_results": "Aucun résultat.",
"com_files_number_selected": "{{0}} sur {{1}} fichier(s) sélectionné(s)",
"com_files_table": "quelquechose doit être renseigné ici. c'était vide",
"com_generated_files": "Fichiers générés :",
"com_hide_examples": "Masquer les exemples",
"com_nav_2fa": "Authentification à deux facteurs (2FA)",
"com_nav_account_settings": "Paramètres du compte",
"com_nav_always_make_prod": "Rendre toujours les nouvelles versions en production",
"com_nav_archive_created_at": "Créé Le",
@@ -296,6 +317,7 @@
"com_nav_delete_cache_storage": "Supprimer le stockage du cache TTS",
"com_nav_delete_data_info": "Toutes vos données seront supprimées.",
"com_nav_delete_warning": "ATTENTION : Cela supprimera définitivement votre compte.",
"com_nav_edit_chat_badges": "Modifier les badges de chat",
"com_nav_enable_cache_tts": "Activer le cache TTS",
"com_nav_enable_cloud_browser_voice": "Utiliser les voix cloud",
"com_nav_enabled": "Activé",
@@ -320,12 +342,14 @@
"com_nav_help_faq": "Aide & FAQ",
"com_nav_hide_panel": "Masquer le panneau latéral le plus à droite",
"com_nav_info_code_artifacts": "Active l'affichage des artéfacts de code expérimentaux à côté du chat",
"com_nav_info_code_artifacts_agent": "Active l'utilisation d'artefacts de code pour cet agent. Par défaut, des instructions supplémentaires spécifiques à l'utilisation des artefacts sont ajoutées, à moins que le \"Mode d'invite personnalisé\" ne soit activé.",
"com_nav_info_custom_prompt_mode": "Lorsqu'activé, le prompt système par défaut pour les artéfacts ne sera pas inclus. Toutes les instructions de génération d'artéfacts doivent être fournies manuellement dans ce mode.",
"com_nav_info_enter_to_send": "Lorsqu'activée, appuyez sur la touche ENTRÉE pour envoyer votre message. Lorsque désactivée, appuyez sur Entrée ajoutera une nouvelle ligne, et vous devrez appuyer sur CTRL + ENTRÉE pour envoyer votre message.",
"com_nav_info_fork_change_default": "Messages visibles uniquement, inclut uniquement le chemin direct vers le message sélectionné. Inclure les branches liées, ajoute des branches tout au long du chemin. Inclure tous depuis/jusque là, inclut tous les messages et branches connectés.",
"com_nav_info_fork_split_target_setting": "Lorsqu'activé, le forking commencera du message cible jusqu'au dernier message de la conversation, selon le comportement sélectionné.",
"com_nav_info_include_shadcnui": "Lorsque cette option est activée, les instructions d'utilisation des composants shadcn/ui seront incluses. shadcn/ui est une collection de composants réutilisables construits avec Radix UI et Tailwind CSS. Note : ces instructions sont détaillées, il est conseillé de ne les activer que si l'indication des importations et des composants corrects est importante pour vous. Pour plus d'informations sur ces composants, visitez : https://ui.shadcn.com/",
"com_nav_info_latex_parsing": "Lorsqu'activé, le code LaTeX dans les messages sera rendu comme des équations mathématiques. Désactiver cela peut améliorer les performances si vous n'avez pas besoin du rendu LaTeX.",
"com_nav_info_save_badges_state": "Lorsque cette option est activée, l'état des badges de chat est sauvegardé. Cela signifie que si vous créez une nouvelle discussion, les badges resteront dans le même état que la discussion précédente. Si vous désactivez cette option, les badges reviendront à leur état par défaut à chaque fois que vous créerez une nouvelle discussion.",
"com_nav_info_save_draft": "Lorsqu'activé, le texte et les pièces jointes que vous entrez dans le formulaire de chat seront automatiquement sauvegardés localement sous forme de brouillons. Ces brouillons seront disponibles même si vous actualisez la page ou passez à une conversation différente. Les brouillons sont stockés localement sur votre appareil et sont supprimés une fois le message envoyé.",
"com_nav_info_show_thinking": "Lorsque cette option est activée, le chat affiche les menus déroulants de réflexion ouverts par défaut, ce qui vous permet de voir le raisonnement de l'IA en temps réel. Lorsqu'ils sont désactivés, les menus déroulants de réflexion restent fermés par défaut, ce qui permet d'obtenir une interface plus propre et plus rationnelle.",
"com_nav_info_user_name_display": "Lorsqu'activé, le nom d'utilisateur de l'expéditeur sera affiché au-dessus de chaque message que vous envoyez. Lorsque désactivé, vous verrez seulement \"Vous\" au-dessus de vos messages.",
@@ -372,6 +396,7 @@
"com_nav_plus_command": "+-Commande",
"com_nav_plus_command_description": "Basculer la commande \"+\" pour ajouter un paramètre de réponses multiples",
"com_nav_profile_picture": "Photo de profil",
"com_nav_save_badges_state": "Sauvegarder l'état des badges",
"com_nav_save_drafts": "Enregistrer les brouillons localement",
"com_nav_scroll_button": "Défilement jusqu'à la touche de fin",
"com_nav_search_placeholder": "Rechercher des messages",
@@ -414,6 +439,16 @@
"com_sidepanel_hide_panel": "Masquer le panneau",
"com_sidepanel_manage_files": "Gérer les fichiers",
"com_sidepanel_parameters": "Paramètres",
"com_ui_2fa_account_security": "L'authentification à deux facteurs ajoute un niveau de sécurité supplémentaire à votre compte",
"com_ui_2fa_disable": "Désactiver l'authentification à deux facteurs (2FA)",
"com_ui_2fa_disable_error": "Une erreur s'est produite lors de la désactivation de l'authentification à deux facteurs.",
"com_ui_2fa_disabled": "L'authentification à deux facteurs (2FA) a été désactivé",
"com_ui_2fa_enable": "Activer l'authentification à deux facteurs (2FA)",
"com_ui_2fa_enabled": "L'authentification à deux facteurs (2FA) a été activée",
"com_ui_2fa_generate_error": "Une erreur s'est produite lors de la génération des paramètres d'authentification à deux facteurs.",
"com_ui_2fa_invalid": "Code d'authentification à deux facteurs invalide",
"com_ui_2fa_setup": "Configuration de l'authentification à deux facteurs (2FA)",
"com_ui_2fa_verified": "Vérification réussie de l'authentification à deux facteurs",
"com_ui_accept": "J'accepte",
"com_ui_add": "Ajouter",
"com_ui_add_model_preset": "Ajouter un modèle ou un préréglage pour une réponse supplémentaire",
@@ -422,12 +457,17 @@
"com_ui_admin_access_warning": "La désactivation de l'accès administrateur à cette fonctionnalité peut entraîner des problèmes d'interface imprévus nécessitant une actualisation. Une fois sauvegardé, le seul moyen de rétablir l'accès est via le paramètre d'interface dans la configuration librechat.yaml, ce qui affecte tous les rôles.",
"com_ui_admin_settings": "Paramètres administratifs",
"com_ui_advanced": "Avancé",
"com_ui_advanced_settings": "Paramètres avancés",
"com_ui_agent": "Agent",
"com_ui_agent_chain": "Chaîne d'agents (mélange d'agents)",
"com_ui_agent_chain_info": "Active la création des séquences d'agents. Chaque agent peut accéder aux résultats des agents précédents de la chaîne. Basé sur l'architecture \"mélange d'agents\" où les agents utilisent les résultats précédents comme information auxiliaire.",
"com_ui_agent_chain_max": "Vous avez atteint le maximum de {{0}} d'agents.",
"com_ui_agent_delete_error": "Une erreur s'est produite lors de la suppression de l'agent",
"com_ui_agent_deleted": "Agent supprimé avec succès",
"com_ui_agent_duplicate_error": "Une erreur s'est produite lors de la duplication de l'agent",
"com_ui_agent_duplicated": "Agent dupliqué avec succès",
"com_ui_agent_editing_allowed": "D'autres utilisateurs peuvent déjà modifier cet agent",
"com_ui_agent_recursion_limit": "Nombre maximal d'étapes de l'agent",
"com_ui_agent_shared_to_all": "il faut faire quelque chose ici. c'était vide",
"com_ui_agents": "Agents",
"com_ui_agents_allow_create": "Autoriser la création d'Agents",

View File

@@ -140,6 +140,7 @@
"com_endpoint_agent": "סוכן",
"com_endpoint_agent_model": "מודל סוכן (מומלץ: GPT-3.5)",
"com_endpoint_agent_placeholder": "אנא בחר סוכן",
"com_endpoint_ai": "בינה מלאכותית",
"com_endpoint_anthropic_maxoutputtokens": "מספר האסימונים המרבי שניתן להפיק בתגובה. ציין ערך נמוך יותר עבור תגובות קצרות יותר וערך גבוה יותר עבור תגובות ארוכות יותר.",
"com_endpoint_anthropic_prompt_cache": "שמירת מטמון מהירה מאפשרת שימוש חוזר בהקשר גדול או בהוראות בקריאות API, תוך הפחתת העלויות וההשהייה",
"com_endpoint_anthropic_temp": "נע בין 0 ל-1. השתמש בטמפ' הקרובה יותר ל-0 עבור בחירה אנליטית / מרובה, וקרוב יותר ל-1 עבור משימות יצירתיות ויצירתיות. אנו ממליצים לשנות את זה או את Top P אבל לא את שניהם.",
@@ -487,6 +488,7 @@
"com_ui_analyzing_finished": "סיים ניתוח",
"com_ui_api_key": "מפתח API",
"com_ui_archive": "ארכיון",
"com_ui_archive_delete_error": "מחיקת השיחה מהארכיון נכשלה",
"com_ui_archive_error": "אירעה שגיאה באירכוב השיחה",
"com_ui_artifact_click": "לחץ לפתיחה",
"com_ui_artifacts": "רכיבי תצוגה",
@@ -502,6 +504,7 @@
"com_ui_attach_error_openai": "לא ניתן לצרף את קבצי הסייען לנקודות קצה אחרות",
"com_ui_attach_error_size": "חרגת ממגבלת גודל הקובץ עבור נקודת הקצה:",
"com_ui_attach_error_type": "סוג קובץ לא נתמך עבור נקודת קצה:",
"com_ui_attach_remove": "הסר קובץ",
"com_ui_attach_warn_endpoint": "עשוי להתעלם מקבצים שאינם של הסייען שאין להם כלי תואם",
"com_ui_attachment": "קובץ מצורף",
"com_ui_auth_type": "סוג אישור",
@@ -538,6 +541,7 @@
"com_ui_bulk_delete_error": "מחיקת קישורים משותפים נכשלה",
"com_ui_callback_url": "כתובת URL להחזרת המידע",
"com_ui_cancel": "בטל",
"com_ui_category": "קָטֵגוֹרִיָה",
"com_ui_chat": "צ'אט",
"com_ui_chat_history": "נקה היסטוריה",
"com_ui_clear": "נקה",
@@ -557,6 +561,7 @@
"com_ui_context": "הקשר",
"com_ui_continue": "המשך",
"com_ui_controls": "פקדים",
"com_ui_convo_delete_error": "מחיקת הצ'אט נכשלה",
"com_ui_copied": "הועתק!",
"com_ui_copied_to_clipboard": "הועתק ללוח",
"com_ui_copy_code": "העתק קוד",
@@ -646,10 +651,15 @@
"com_ui_fork_info_2": "\"הסתעפות\" מתייחסת ליצירת שיחה חדשה המתחילה/מסתיימת מהודעות ספציפיות בשיחה הנוכחית, תוך יצירת העתק בהתאם לאפשרויות שנבחרו.",
"com_ui_fork_info_3": "\"הודעת היעד\" מתייחסת להודעה שממנה נפתחה חלונית זו, או, אם סימנת \"{{0}}\", להודעה האחרונה בשיחה.",
"com_ui_fork_info_branches": "אפשרות זו מפצלת את ההודעות הגלויות, יחד עם ההסתעפויות הקשורות; במילים אחרות, המסלול הישיר להודעת היעד, כולל את ההסתעפויות לאורך המסלול.",
"com_ui_fork_info_button_label": "הצג מידע על פיצול שיחות",
"com_ui_fork_info_remember": "סמן כדי לזכור את האפשרויות שבחרת לשימושים הבאים, כך שתוכל ליצור הסתעפויות בשיחות מהר יותר לפי העדפתך.",
"com_ui_fork_info_start": "כאשר מסומן, ההסתעפות תחל מההודעה זו ותימשך עד להודעה האחרונה בשיחה, על פי ההתנהגות שנבחרה לעיל.",
"com_ui_fork_info_target": "אפשרות זו תיצור הסתעפות שתכלול את כל ההודעות המובילות להודעת היעד, כולל ההודעות הסמוכות; במילים אחרות, כל ההסתעפויות של ההודעות יכללו, בין אם הם גלויות או לא, ובין אם הם נמצאות באותו מסלול או לא.",
"com_ui_fork_info_visible": "אפשרות זו תיצור הסתעפות רק של ההודעות הגלויות; במילים אחרות, רק את המסלול הישיר להודעת היעד, ללא הסתעפויות נוספות.",
"com_ui_fork_more_details_about": "הצג מידע ופרטים נוספים על אפשרות פורק \"{{0}}\"",
"com_ui_fork_more_info_options": "הצג הסבר מפורט על כל אפשרויות המזלג והתנהגויותיהן",
"com_ui_fork_more_info_remember": "ראה הסבר כיצד האפשרות \"{{0}}\" שומרת את ההעדפות שלך עבור פורקים עתידיים",
"com_ui_fork_more_info_split_target": "ראה הסבר כיצד האפשרות \"{{0}}\" משפיעה על ההודעות שיכללו בפורק שלך",
"com_ui_fork_processing": "יוצר הסתעפות בשיחה...",
"com_ui_fork_remember": "זכור",
"com_ui_fork_remember_checked": "הבחירה שלך תישמר אחרי השימוש. תוכל לשנות זאת בכל זמן בהגדרות.",
@@ -692,6 +702,7 @@
"com_ui_logo": "\"לוגו {{0}}\"",
"com_ui_manage": "נהל",
"com_ui_max_tags": "המספר המקסימלי המותר על פי הערכים העדכניים הוא {{0}}.",
"com_ui_mcp_servers": "שרתי MCP",
"com_ui_mention": "ציין נקודת קצה, סייען, או הנחייה (פרופמט) כדי לעבור אליה במהירות",
"com_ui_min_tags": "לא ניתן למחוק ערכים נוספים, יש צורך במינימום {{0}} ערכים.",
"com_ui_misc": "כללי",
@@ -702,6 +713,7 @@
"com_ui_name": "שם",
"com_ui_new": "חדש",
"com_ui_new_chat": "שיחה חדשה",
"com_ui_new_conversation_title": "כותרת חדשה לצ'אט",
"com_ui_next": "הבא",
"com_ui_no": "לא",
"com_ui_no_backup_codes": "אין קודי גיבוי זמינים. אנא צור קודים חדשים",
@@ -744,6 +756,8 @@
"com_ui_regenerating": "יוצר מחדש...",
"com_ui_region": "איזור",
"com_ui_rename": "שנה שם",
"com_ui_rename_conversation": "החלפת שם הצ'אט",
"com_ui_rename_failed": "החלפת שם הצ'אט נכשלה",
"com_ui_rename_prompt": "שנה שם הנחיה (פרומפט)",
"com_ui_requires_auth": "נדרש אימות",
"com_ui_reset_var": "איפוס {{0}}",
@@ -796,8 +810,14 @@
"com_ui_sign_in_to_domain": "היכנס אל {{0}}",
"com_ui_simple": "פשוט",
"com_ui_size": "סוג",
"com_ui_special_var_current_date": "תאריך נוכחי",
"com_ui_special_var_current_datetime": "תאריך ושעה נוכחיים",
"com_ui_special_var_current_user": "משתמש נוכחי",
"com_ui_special_var_iso_datetime": "תאריך ושעה ISO UTC",
"com_ui_special_variables": "משתנים מיוחדים:",
"com_ui_special_variables_more_info": "ניתן לבחור משתנים מיוחדים מהתפריט הנפתח: `{{current_date}}` (תאריך ויום בשבוע של היום), `{{current_datetime}}` (תאריך ושעה מקומיים), `{{utc_iso_datetime}}` (תאריך ושעה UTC ISO) ו-`{{current_user}}` (שם החשבון שלך).",
"com_ui_speech_while_submitting": "לא ניתן לשלוח אודיו בזמן שנוצרת תגובה",
"com_ui_sr_actions_menu": "פתח את תפריט הפעולות עבור \"{{0}}\"",
"com_ui_stop": "עצור",
"com_ui_storage": "אחסון",
"com_ui_submit": "שלח",
@@ -814,6 +834,7 @@
"com_ui_unarchive": "לארכיון",
"com_ui_unarchive_error": "אירעה שגיאה בארכיון השיחה",
"com_ui_unknown": "לא ידוע",
"com_ui_untitled": "ללא כותרת",
"com_ui_update": "עדכון",
"com_ui_upload": "העלה",
"com_ui_upload_code_files": "העלאה עבור מפענח הקוד",
@@ -842,6 +863,7 @@
"com_ui_view_source": "הצג צ'אט מקורי",
"com_ui_weekend_morning": "סוף שבוע נעים!",
"com_ui_write": "כתיבה",
"com_ui_x_selected": "{{0}} נבחר",
"com_ui_yes": "כן",
"com_ui_zoom": "זום",
"com_user_message": "אתה",

View File

@@ -5,6 +5,9 @@ import LanguageDetector from 'i18next-browser-languagedetector';
// Import your JSON translations
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';
@@ -35,8 +38,11 @@ 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 },

View File

@@ -1,4 +1,5 @@
{
"com_agents_by_librechat": "oleh LibreChat",
"com_auth_already_have_account": "Sudah memiliki akun?",
"com_auth_click": "Klik",
"com_auth_click_here": "Klik di sini",

View File

@@ -1,4 +1,6 @@
{
"chat_direction_left_to_right": "ここに何かを入れる必要があります。空でした",
"chat_direction_right_to_left": "ここに何かを入れる必要があります。空でした",
"com_a11y_ai_composing": "AIはまだ作成中です。",
"com_a11y_end": "AIは返信を完了しました。",
"com_a11y_start": "AIが返信を開始しました。",
@@ -9,6 +11,9 @@
"com_agents_create_error": "エージェントの作成中にエラーが発生しました。",
"com_agents_description_placeholder": "オプション: エージェントの説明を入力してください",
"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_search_disabled": "ファイル検索用のファイルをアップロードする前に、エージェントを作成する必要があります。",
"com_agents_file_search_info": "有効にすると、エージェントは以下に表示されているファイル名を正確に認識し、それらのファイルから関連する情報を取得することができます。",
"com_agents_instructions_placeholder": "エージェントが使用するシステムの指示",
@@ -18,13 +23,16 @@
"com_agents_not_available": "エージェントは利用できません",
"com_agents_search_name": "名前でエージェントを検索",
"com_agents_update_error": "エージェントの更新中にエラーが発生しました。",
"com_assistants_action_attempt": "アシスタントは{{0}}と話したいです",
"com_assistants_actions": "アクション",
"com_assistants_actions_disabled": "アクションを追加する前にアシスタントを作成する必要があります。",
"com_assistants_actions_info": "アシスタントが API を介して情報を取得したり、アクションを実行したりできるようにします's",
"com_assistants_add_actions": "アクションを追加",
"com_assistants_add_tools": "ツールを追加",
"com_assistants_allow_sites_you_trust": "信頼できるサイトのみ許可する。",
"com_assistants_append_date": "現在の日付と時刻を追加",
"com_assistants_append_date_tooltip": "有効にすると、現在のクライアントの日付と時刻がアシスタントのシステム指示に追加されます。",
"com_assistants_attempt_info": "アシスタントは次のものを送信したいと考えています:",
"com_assistants_available_actions": "利用可能なアクション",
"com_assistants_capabilities": "機能",
"com_assistants_code_interpreter": "コードインタプリタ",
@@ -59,6 +67,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": "ここをクリック",
@@ -81,6 +90,7 @@
"com_auth_email_verification_redirecting": "{{0}} 秒後にリダイレクトします...",
"com_auth_email_verification_resend_prompt": "メールが届きませんか?",
"com_auth_email_verification_success": "メールが正常に検証されました",
"com_auth_email_verifying_ellipsis": "確認中...",
"com_auth_error_create": "アカウント登録に失敗しました。もう一度お試しください。",
"com_auth_error_invalid_reset_token": "無効なパスワードリセットトークンです。",
"com_auth_error_login": "入力された情報ではログインできませんでした。認証情報を確認した上で再度お試しください。",
@@ -117,9 +127,11 @@
"com_auth_submit_registration": "登録をする",
"com_auth_to_reset_your_password": "パスワードをリセットします。",
"com_auth_to_try_again": "再認証する",
"com_auth_two_factor": "ご希望のワンタイムパスワードアプリケーションでコードを確認してください",
"com_auth_username": "ユーザ名 (オプション)",
"com_auth_username_max_length": "ユーザ名は最大20文字で入力してください",
"com_auth_username_min_length": "ユーザ名は最低2文字で入力してください",
"com_auth_verify_your_identity": "本人確認",
"com_auth_welcome_back": "おかえりなさい",
"com_click_to_download": "(ダウンロードするにはこちらをクリック)",
"com_download_expired": "ダウンロードの期限が切れています",
@@ -132,6 +144,8 @@
"com_endpoint_anthropic_maxoutputtokens": "生成されるレスポンスの最大トークン数。短いレスポンスには低い値を、長いレスポンスには高い値を指定する。",
"com_endpoint_anthropic_prompt_cache": "プロンプトキャッシュを使用すると、API呼び出し間で大きなコンテキストや指示を再利用でき、コストとレイテンシを削減できます",
"com_endpoint_anthropic_temp": "0から1の値。分析的・多岐の選択になる課題には0に近い値を入力する。創造的・生成的な課題には1に近い値を入力する。この値か Top P の変更をおすすめしますが、両方の変更はおすすめしません。",
"com_endpoint_anthropic_thinking": "サポートされているClaudeモデル3.7 Sonnetの内部推論を有効にします。注「思考予算」が「最大出力トークン」よりも低く設定されている必要があります。",
"com_endpoint_anthropic_thinking_budget": "Claude が内部推論プロセスで使用できるトークンの最大数を指定します。予算を大きくすると、複雑な問題に対するより徹底的な分析が可能になり、応答品質が向上します。ただし、特に 32K を超える範囲では、Claude は割り当てられた予算をすべて使用できない可能性があります。この設定は「最大出力トークン」よりも小さくする必要があります。",
"com_endpoint_anthropic_topk": "Top-k はモデルがトークンをどのように選択して出力するかを変更する。top-kが1の場合はモデルの語彙に含まれるすべてのトークンの中で最も確率が高い1つが選択される(greedy decodingと呼ばれている)。top-kが3の場合は上位3つのトークンの中から選択される。(temperatureを使用)",
"com_endpoint_anthropic_topp": "Top-p はモデルがトークンをどのように選択して出力するかを変更する。K(topKを参照)の確率の合計がtop-pの確率と等しくなるまでのトークンが選択される。",
"com_endpoint_assistant": "アシスタント",
@@ -168,6 +182,9 @@
"com_endpoint_default_blank": "デフォルト: 空",
"com_endpoint_default_empty": "デフォルト: 空",
"com_endpoint_default_with_num": "デフォルト: {{0}}",
"com_endpoint_deprecated": "非推奨",
"com_endpoint_deprecated_info": "このエンドポイントは非推奨であり、将来のバージョンで削除される可能性があります。",
"com_endpoint_deprecated_info_a11y": "プラグインエンドポイントは非推奨であり、将来のバージョンで削除される可能性があります。",
"com_endpoint_examples": " プリセット名",
"com_endpoint_export": "エクスポート",
"com_endpoint_export_share": "エクスポート/共有",
@@ -194,6 +211,7 @@
"com_endpoint_openai_max_tokens": "オプションの 'max_tokens' フィールドで、チャット補完時に生成可能な最大トークン数を設定します。入力トークンと生成されたトークンの合計長さは、モデルのコンテキスト長によって制限されています。この数値がコンテキストの最大トークン数を超えると、エラーが発生する可能性があります。",
"com_endpoint_openai_pres": "-2.0から2.0の値。正の値は入力すると、新規トークンの出現に基づいたペナルティを課し、新しいトピックについて話す可能性を高める。",
"com_endpoint_openai_prompt_prefix_placeholder": "システムメッセージに含める Custom Instructions。デフォルト: none",
"com_endpoint_openai_reasoning_effort": "o1 モデルのみ: 推論モデルの推論の努力を制限します。推論の努力を減らすと、応答が速くなり、応答で推論に使用されるトークンが少なくなります。",
"com_endpoint_openai_resend": "これまでに添付した画像を全て再送信します。注意:トークン数が大幅に増加したり、多くの画像を添付するとエラーが発生する可能性があります。",
"com_endpoint_openai_resend_files": "以前に添付されたすべてのファイルを再送信します。注意:これにより、トークンのコストが増加し、多くの添付ファイルでエラーが発生する可能性があります。",
"com_endpoint_openai_stop": "APIがさらにトークンを生成するのを止めるため、最大で4つのシーケンスを設定可能",
@@ -207,6 +225,7 @@
"com_endpoint_plug_use_functions": "Functionsを使用",
"com_endpoint_presence_penalty": "既存性によるペナルティ",
"com_endpoint_preset": "プリセット",
"com_endpoint_preset_custom_name_placeholder": "ここに何かを入れる必要があります。空でした",
"com_endpoint_preset_default": "が有効化されました。",
"com_endpoint_preset_default_item": "デフォルト:",
"com_endpoint_preset_default_none": "現在有効なプリセットはありません。",
@@ -227,13 +246,19 @@
"com_endpoint_prompt_prefix_assistants": "追加の指示",
"com_endpoint_prompt_prefix_assistants_placeholder": "アシスタントの主な指示に加えて、追加の指示やコンテキストを設定します。空欄の場合は無視されます。",
"com_endpoint_prompt_prefix_placeholder": "custom instructions か context を設定する。空の場合は無視されます。",
"com_endpoint_reasoning_effort": "推理の努力",
"com_endpoint_save_as_preset": "プリセット保存",
"com_endpoint_search": "エンドポイントを名前で検索",
"com_endpoint_search_endpoint_models": "{{0}} モデルを検索...",
"com_endpoint_search_models": "モデルを検索...",
"com_endpoint_search_var": "{{0}} を検索...",
"com_endpoint_set_custom_name": "このプリセットを見つけやすいように名前を設定する。",
"com_endpoint_skip_hover": "コンプリーションのステップをスキップする。(最終的な回答と生成されたステップをレビューする機能)",
"com_endpoint_stop": "シーケンスを停止",
"com_endpoint_stop_placeholder": "Enterキー押下で値を区切ります",
"com_endpoint_temperature": "Temperature",
"com_endpoint_temperature": "温度",
"com_endpoint_thinking": "推論",
"com_endpoint_thinking_budget": "推論予算",
"com_endpoint_top_k": "Top K",
"com_endpoint_top_p": "Top P",
"com_endpoint_use_active_assistant": "アクティブなアシスタントを使用",
@@ -246,6 +271,7 @@
"com_error_files_upload_canceled": "ファイルのアップロードがキャンセルされました。注意:アップロード処理が継続している可能性があるため、手動でファイルを削除する必要があるかもしれません。",
"com_error_files_validation": "ファイルの検証中にエラーが発生しました。",
"com_error_input_length": "最新のメッセージトークン数は長すぎます。トークン制限 ({{0}}) を超えています。メッセージを短くするか、会話パラメーターから最大コンテキストサイズを調整するか、会話を分岐して続行してください。",
"com_error_invalid_agent_provider": "その {{0}} プロバイダーはエージェントでは使用できません。エージェントの設定で、現在利用可能なプロバイダを選択してください。",
"com_error_invalid_user_key": "無効なキーが提供されました。キーを入力して再試行してください。",
"com_error_moderation": "送信されたコンテンツは、コミュニティガイドラインに準拠していないとして、投稿監視システムによって検知されました。この特定のトピックについては処理を続行できません。他に質問や調べたいトピックがある場合は、メッセージを編集するか、新しい会話を作成してください。",
"com_error_no_base_url": "ベースURLが見つかりません。ベースURLを入力して再試行してください。",
@@ -253,8 +279,10 @@
"com_files_filter": "ファイルをフィルタリング...",
"com_files_no_results": "結果がありません。",
"com_files_number_selected": "{{0}} of {{1}} ファイルが選択されました",
"com_files_table": "ここに何かを入れる必要があります。空でした",
"com_generated_files": "生成されたファイル:",
"com_hide_examples": "例を非表示",
"com_nav_2fa": "二要素認証 (2FA)",
"com_nav_account_settings": "アカウント設定",
"com_nav_always_make_prod": "常に新しいバージョンを制作する",
"com_nav_archive_created_at": "作成日",
@@ -272,6 +300,7 @@
"com_nav_automatic_playback": "最新メッセージを自動再生",
"com_nav_balance": "バランス",
"com_nav_browser": "ブラウザ",
"com_nav_center_chat_input": "ようこそ画面の中央にチャット入力を配置",
"com_nav_change_picture": "画像を変更",
"com_nav_chat_commands": "チャットコマンド",
"com_nav_chat_commands_info": "メッセージの先頭に特定の文字を入力することで、これらのコマンドが有効になります。各コマンドは、決められた文字(プレフィックス)で起動します。メッセージの先頭にこれらの文字をよく使用する場合は、コマンド機能を無効にすることができます。",
@@ -293,6 +322,7 @@
"com_nav_delete_cache_storage": "TTSキャッシュストレージを削除",
"com_nav_delete_data_info": "すべてのデータが削除されます。",
"com_nav_delete_warning": "警告: この操作により、アカウントが完全に削除されます。",
"com_nav_edit_chat_badges": "チャットバッジの編集",
"com_nav_enable_cache_tts": "キャッシュTTSを有効化",
"com_nav_enable_cloud_browser_voice": "クラウドベースの音声を使用",
"com_nav_enabled": "有効化",
@@ -317,13 +347,16 @@
"com_nav_help_faq": "ヘルプ & FAQ",
"com_nav_hide_panel": "右側のパネルを非表示",
"com_nav_info_code_artifacts": "チャットの横に実験的なコード アーティファクトの表示を有効にします",
"com_nav_info_code_artifacts_agent": "このエージェントのコードアーティファクトの使用を有効にします。デフォルトでは、\"カスタムプロンプトモード\" が有効になっていない限り、アーティファクトの使用に特化した追加の指示が追加されます。",
"com_nav_info_custom_prompt_mode": "有効にすると、デフォルトのアーティファクト システム プロンプトは含まれません。このモードでは、アーティファクト生成指示をすべて手動で提供する必要があります。",
"com_nav_info_enter_to_send": "有効になっている場合、 `ENTER` キーを押すとメッセージが送信されます。無効になっている場合、Enterキーを押すと新しい行が追加され、 `CTRL + ENTER` / `⌘ + ENTER` キーを押してメッセージを送信する必要があります。",
"com_nav_info_fork_change_default": "`表示メッセージのみ` は、選択したメッセージへの直接パスのみが含まれます。 `関連ブランチを含める` は、パスに沿ったブランチを追加します。 `すべてを対象に含める` は、接続されているすべてのメッセージとブランチを含みます。",
"com_nav_info_fork_split_target_setting": "有効になっている場合、選択した動作に従って、対象メッセージから会話内の最新メッセージまで分岐が開始されます。",
"com_nav_info_include_shadcnui": "有効にすると、shadcn/uiコンポーネントを使用するための指示が含まれます。shadcn/uiはRadix UIとTailwind CSSを使用して構築された再利用可能なコンポーネントのコレクションです。注:これらの指示は長文ですので、LLM に正しいインポートとコンポーネントを知らせることが重要でない限り、有効にしないでください。これらのコンポーネントの詳細については、https://ui.shadcn.com/をご覧ください。",
"com_nav_info_latex_parsing": "有効になっている場合、メッセージ内のLaTeXコードが数式としてレンダリングされます。LaTeXレンダリングが必要ない場合は、これを無効にするとパフォーマンスが向上する場合があります。",
"com_nav_info_save_badges_state": "有効にすると、チャットバッジの状態が保存されます。つまり、新しいチャットを作成する場合、バッジは前のチャットと同じ状態のままになります。このオプションを無効にすると、新しいチャットを作成するたびにバッジはデフォルト状態にリセットされます。",
"com_nav_info_save_draft": "有効になっている場合、チャットフォームに入力したテキストと添付ファイルがドラフトとしてローカルに自動保存されます。これらのドラフトは、ページをリロードしたり、別の会話に切り替えても利用できます。ドラフトはデバイスにローカルに保存され、メッセージが送信されると削除されます。",
"com_nav_info_show_thinking": "有効にすると、チャットはデフォルトで思考ドロップダウンを開いて表示し、AIの推論をリアルタイムで見ることができます。無効にすると、思考ドロップダウンはデフォルトで閉じたままになり、よりすっきりとした合理的なインターフェイスになります。",
"com_nav_info_user_name_display": "有効になっている場合、送信者のユーザー名が送信するメッセージの上に表示されます。無効になっている場合、メッセージの上に「あなた」のみが表示されます。",
"com_nav_lang_arabic": "العربية",
"com_nav_lang_auto": "自動検出",
@@ -337,10 +370,12 @@
"com_nav_lang_georgian": "ქართული",
"com_nav_lang_german": "Deutsch",
"com_nav_lang_hebrew": "עברית",
"com_nav_lang_hungarian": "マジャル語",
"com_nav_lang_indonesia": "Indonesia",
"com_nav_lang_italian": "Italiano",
"com_nav_lang_japanese": "日本語",
"com_nav_lang_korean": "한국어",
"com_nav_lang_persian": "ファラオ",
"com_nav_lang_polish": "Polski",
"com_nav_lang_portuguese": "Português",
"com_nav_lang_russian": "Русский",
@@ -368,7 +403,9 @@
"com_nav_plus_command": "+-Command",
"com_nav_plus_command_description": "コマンド\"+\"で複数応答設定を追加する",
"com_nav_profile_picture": "プロフィール画像",
"com_nav_save_badges_state": "バッジの状態を保存する",
"com_nav_save_drafts": "ローカルにドラフトを保存する",
"com_nav_scroll_button": "終了ボタンまでスクロール",
"com_nav_search_placeholder": "メッセージ検索",
"com_nav_send_message": "メッセージを送信する",
"com_nav_setting_account": "アカウント",
@@ -380,6 +417,7 @@
"com_nav_settings": "設定",
"com_nav_shared_links": "共有リンク",
"com_nav_show_code": "Code Interpreter を使用する際は常にコードを表示する",
"com_nav_show_thinking": "デフォルトで推論ドロップダウンを開く",
"com_nav_slash_command": "/-Command",
"com_nav_slash_command_description": "コマンド\"/\"でキーボードでプロンプトを選択する",
"com_nav_speech_to_text": "音声テキスト変換",
@@ -408,6 +446,16 @@
"com_sidepanel_hide_panel": "パネルを隠す",
"com_sidepanel_manage_files": "ファイルを管理",
"com_sidepanel_parameters": "パラメータ",
"com_ui_2fa_account_security": "2要素認証はアカウントのセキュリティをさらに強化します",
"com_ui_2fa_disable": "2FAを無効にする",
"com_ui_2fa_disable_error": "2要素認証を無効にする際にエラーが発生しました",
"com_ui_2fa_disabled": "2FAが無効になっています",
"com_ui_2fa_enable": "2FAを有効にします",
"com_ui_2fa_enabled": "2FAが有効になりました",
"com_ui_2fa_generate_error": "2要素認証設定の生成中にエラーが発生しました",
"com_ui_2fa_invalid": "2要素認証コードが無効です",
"com_ui_2fa_setup": "2FAを設定する",
"com_ui_2fa_verified": "2要素認証の認証に成功しました",
"com_ui_accept": "同意します",
"com_ui_add": "追加",
"com_ui_add_model_preset": "追加の応答のためのモデルまたはプリセットを追加する",
@@ -416,23 +464,36 @@
"com_ui_admin_access_warning": "管理者アクセスをこの機能で無効にすると、予期せぬUI上の問題が発生し、画面の再読み込みが必要になる場合があります。設定を保存した場合、元に戻すには librechat.yaml の設定ファイルを直接編集する必要があり、この変更はすべての権限に影響します。",
"com_ui_admin_settings": "管理者設定",
"com_ui_advanced": "高度",
"com_ui_advanced_settings": "詳細設定",
"com_ui_agent": "エージェント",
"com_ui_agent_chain": "エージェント・チェーンMixture-of-Agents",
"com_ui_agent_chain_info": "エージェントのシーケンスを作成できるようにします。各エージェントは、チェーン内の前のエージェントの出力にアクセスできます。エージェントが前の出力を補助情報として使用する「Mixture-of-Agents」アーキテクチャに基づいています。",
"com_ui_agent_chain_max": "{{0}} エージェントの上限に達しました。",
"com_ui_agent_delete_error": "エージェントの削除中にエラーが発生しました",
"com_ui_agent_deleted": "エージェントを正常に削除しました",
"com_ui_agent_duplicate_error": "アシスタントの複製中にエラーが発生しました",
"com_ui_agent_duplicated": "アシスタントを複製しました",
"com_ui_agent_editing_allowed": "このエージェントは他のユーザーが既に編集可能です",
"com_ui_agent_recursion_limit": "最大エージェントステップ数",
"com_ui_agent_recursion_limit_info": "エージェントが最終応答を返す前に実行できるステップ数を制限します。デフォルトは25ステップです。ステップとは、AI APIリクエストまたはツール使用ラウンドのいずれかです。例えば、基本的なツールインタラクションは、最初のリクエスト、ツール使用、そしてフォローアップリクエストの3ステップで構成されます。",
"com_ui_agent_shared_to_all": "ここに何かを入れる必要があります。空でした",
"com_ui_agent_var": "{{0}}エージェント",
"com_ui_agents": "エージェント",
"com_ui_agents_allow_create": "エージェントの作成を許可",
"com_ui_agents_allow_share_global": "全ユーザーとAgentsの共有を許可",
"com_ui_agents_allow_use": "エージェントの使用を許可",
"com_ui_all": "すべて",
"com_ui_all_proper": "すべて",
"com_ui_analyzing": "分析中",
"com_ui_analyzing_finished": "分析終了",
"com_ui_api_key": "APIキー",
"com_ui_archive": "アーカイブ",
"com_ui_archive_delete_error": "アーカイブされた会話の削除に失敗しました",
"com_ui_archive_error": "アーカイブに失敗しました。",
"com_ui_artifact_click": "クリックして開く",
"com_ui_artifacts": "アーティファクト",
"com_ui_artifacts_toggle": "アーティファクト UI の切替",
"com_ui_artifacts_toggle_agent": "アーティファクトを有効にする",
"com_ui_ascending": "昇順",
"com_ui_assistant": "アシスタント",
"com_ui_assistant_delete_error": "アシスタントの削除中にエラーが発生しました。",
@@ -443,12 +504,23 @@
"com_ui_attach_error_openai": "他のエンドポイントにアシスタントファイルを添付することはできません",
"com_ui_attach_error_size": "エンドポイントのファイルサイズ制限を超えました:",
"com_ui_attach_error_type": "エンドポイントでサポートされていないファイルタイプ:",
"com_ui_attach_remove": "ファイルを削除",
"com_ui_attach_warn_endpoint": "互換性のあるツールがない場合、非アシスタントのファイルは無視される可能性があります",
"com_ui_attachment": "添付ファイル",
"com_ui_auth_type": "認証タイプ",
"com_ui_auth_url": "認証URL",
"com_ui_authentication": "認証",
"com_ui_authentication_type": "認証タイプ",
"com_ui_avatar": "アバター",
"com_ui_azure": "Azure",
"com_ui_back_to_chat": "チャットに戻る",
"com_ui_back_to_prompts": "プロンプトに戻る",
"com_ui_backup_codes": "バックアップコード",
"com_ui_backup_codes_regenerate_error": "バックアップコードの再生成中にエラーが発生しました",
"com_ui_backup_codes_regenerated": "バックアップコードの再生成に成功",
"com_ui_basic": "Basic",
"com_ui_basic_auth_header": "Basic認証ヘッダー",
"com_ui_bearer": "Bearer",
"com_ui_bookmark_delete_confirm": "このブックマークを削除してもよろしいですか?",
"com_ui_bookmarks": "ブックマーク",
"com_ui_bookmarks_add": "ブックマークを追加",
@@ -467,20 +539,29 @@
"com_ui_bookmarks_title": "タイトル",
"com_ui_bookmarks_update_error": "ブックマークの更新中にエラーが発生しました",
"com_ui_bookmarks_update_success": "ブックマークが正常に更新されました",
"com_ui_bulk_delete_error": "共有リンクの削除に失敗しました",
"com_ui_callback_url": "コールバックURL",
"com_ui_cancel": "キャンセル",
"com_ui_chat": "チャット",
"com_ui_chat_history": "チャット履歴",
"com_ui_clear": "削除する",
"com_ui_clear_all": "すべてクリア",
"com_ui_client_id": "クライアントID",
"com_ui_client_secret": "クライアントシークレット",
"com_ui_close": "閉じる",
"com_ui_close_menu": "メニューを閉じる",
"com_ui_code": "コード",
"com_ui_collapse_chat": "チャットを折りたたむ",
"com_ui_command_placeholder": "オプション:プロンプトのコマンドまたは名前を入力",
"com_ui_command_usage_placeholder": "コマンドまたは名前でプロンプトを選択してください",
"com_ui_complete_setup": "セットアップ完了",
"com_ui_confirm_action": "実行する",
"com_ui_confirm_admin_use_change": "この設定を変更すると、あなた自身を含む管理者のアクセスがブロックされます。本当によろしいですか?",
"com_ui_confirm_change": "変更の確認",
"com_ui_context": "コンテキスト",
"com_ui_continue": "続きを生成する",
"com_ui_controls": "管理",
"com_ui_convo_delete_error": "会話の削除に失敗しました",
"com_ui_copied": "コピーしました!",
"com_ui_copied_to_clipboard": "コピーしました",
"com_ui_copy_code": "コードをコピーする",
@@ -489,6 +570,9 @@
"com_ui_create": "作成",
"com_ui_create_link": "リンクを作成する",
"com_ui_create_prompt": "プロンプトを作成する",
"com_ui_currently_production": "現在生産中",
"com_ui_custom": "カスタム",
"com_ui_custom_header_name": "カスタムヘッダー名",
"com_ui_custom_prompt_mode": "カスタムプロンプトモード",
"com_ui_dashboard": "ダッシュボード",
"com_ui_date": "日付",
@@ -509,6 +593,7 @@
"com_ui_date_today": "今日",
"com_ui_date_yesterday": "昨日",
"com_ui_decline": "同意しません",
"com_ui_default_post_request": "デフォルトPOSTリクエスト",
"com_ui_delete": "削除",
"com_ui_delete_action": "アクションを削除",
"com_ui_delete_action_confirm": "このアクションを削除してもよろしいですか?",
@@ -524,7 +609,13 @@
"com_ui_descending": "降順",
"com_ui_description": "概要",
"com_ui_description_placeholder": "オプション:プロンプトを表示するときの説明を入力",
"com_ui_disabling": "無効にしています...",
"com_ui_download": "ダウンロード",
"com_ui_download_artifact": "アーティファクトをダウンロード",
"com_ui_download_backup": "バックアップコードをダウンロードする",
"com_ui_download_backup_tooltip": "続行する前に、バックアップコードをダウンロードしてください。認証デバイスを紛失した場合、アクセスを回復するために必要です。",
"com_ui_download_error": "ファイルのダウンロード中にエラーが発生しました。ファイルが削除された可能性があります。",
"com_ui_drag_drop": "ここに何かを入れる必要があります。空でした",
"com_ui_dropdown_variables": "ドロップダウン変数:",
"com_ui_dropdown_variables_info": "プロンプトのカスタムドロップダウンメニューを作成します: `{{variable_name:option1|option2|option3}}`",
"com_ui_duplicate": "複製",
@@ -532,6 +623,7 @@
"com_ui_duplication_processing": "会話を複製中...",
"com_ui_duplication_success": "会話の複製が完了しました",
"com_ui_edit": "編集",
"com_ui_empty_category": "-",
"com_ui_endpoint": "エンドポイント",
"com_ui_endpoint_menu": "LLMエンドポイントメニュー",
"com_ui_enter": "入力",
@@ -542,9 +634,12 @@
"com_ui_error_connection": "サーバーへの接続中にエラーが発生しました。ページを更新してください。",
"com_ui_error_save_admin_settings": "管理者設定の保存にエラーが発生しました。",
"com_ui_examples": "例",
"com_ui_expand_chat": "チャットを展開",
"com_ui_export_convo_modal": "エクスポート",
"com_ui_field_required": "必須入力項目です",
"com_ui_filter_prompts": "フィルタープロンプト",
"com_ui_filter_prompts_name": "名前でプロンプトをフィルタ",
"com_ui_finance": "財務",
"com_ui_fork": "分岐",
"com_ui_fork_all_target": "すべてを対象に含める",
"com_ui_fork_branches": "関連ブランチを含める",
@@ -567,43 +662,70 @@
"com_ui_fork_split_target_setting": "デフォルトで対象メッセージから分岐を開始する",
"com_ui_fork_success": "会話の分岐に成功しました",
"com_ui_fork_visible": "表示メッセージのみ",
"com_ui_generate_backup": "バックアップコードを生成する",
"com_ui_generate_qrcode": "QRコードを生成する",
"com_ui_generating": "生成中...",
"com_ui_global_group": "ここに何かを入れる必要があります。空でした",
"com_ui_go_back": "戻る",
"com_ui_go_to_conversation": "会話に移動する",
"com_ui_good_afternoon": "こんにちは",
"com_ui_good_evening": "こんばんは",
"com_ui_good_morning": "おはようございます",
"com_ui_happy_birthday": "初めての誕生日です!",
"com_ui_hide_qr": "QRコードを非表示にする",
"com_ui_host": "ホスト",
"com_ui_idea": "アイデア",
"com_ui_image_gen": "画像生成",
"com_ui_import": "読み込む",
"com_ui_import_conversation_error": "会話のインポート時にエラーが発生しました",
"com_ui_import_conversation_file_type_error": "サポートされていないインポート形式です",
"com_ui_import_conversation_info": "JSONファイルから会話をインポートする",
"com_ui_import_conversation_success": "会話のインポートに成功しました",
"com_ui_include_shadcnui": "shadcn/uiコンポーネントの指示を含める",
"com_ui_include_shadcnui_agent": "shadcn/ui の指示を含める",
"com_ui_input": "入力",
"com_ui_instructions": "指示文",
"com_ui_late_night": "遅い夜を楽しんで",
"com_ui_latest_footer": "Every AI for Everyone.",
"com_ui_latest_production_version": "最新の製品バージョン",
"com_ui_latest_version": "最新バージョン",
"com_ui_librechat_code_api_key": "LibreChat コードインタープリター APIキーを取得",
"com_ui_librechat_code_api_subtitle": "セキュア。多言語対応。ファイル入出力。",
"com_ui_librechat_code_api_title": "AIコードを実行",
"com_ui_loading": "読み込み中...",
"com_ui_locked": "ロック",
"com_ui_logo": "{{0}}のロゴ",
"com_ui_manage": "管理",
"com_ui_max_tags": "最新の値を使用した場合、許可される最大数は {{0}} です。",
"com_ui_mcp_servers": "MCP サーバー",
"com_ui_mention": "エンドポイント、アシスタント、またはプリセットを素早く切り替えるには、それらを言及してください。",
"com_ui_min_tags": "これ以上の値を削除できません。少なくとも {{0}} が必要です。",
"com_ui_misc": "その他",
"com_ui_model": "モデル",
"com_ui_model_parameters": "モデルパラメータ",
"com_ui_more_info": "詳細",
"com_ui_my_prompts": "マイ プロンプト",
"com_ui_name": "名前",
"com_ui_new": "New",
"com_ui_new_chat": "新規チャット",
"com_ui_new_conversation_title": "新しい会話タイトル",
"com_ui_next": "次",
"com_ui_no": "いいえ",
"com_ui_no_backup_codes": "バックアップコードがありません。新しいコードを生成してください",
"com_ui_no_bookmarks": "ブックマークがまだないようです。チャットをクリックして新しいブックマークを追加してください",
"com_ui_no_category": "カテゴリなし",
"com_ui_no_changes": "更新する変更はありません",
"com_ui_no_data": "ここに何かを入れる必要があります。空でした",
"com_ui_no_terms_content": "表示する利用規約の内容はありません",
"com_ui_no_valid_items": "ここに何かを入れる必要があります。空でした",
"com_ui_none": "なし",
"com_ui_not_used": "未使用",
"com_ui_nothing_found": "該当するものが見つかりませんでした",
"com_ui_oauth": "OAuth",
"com_ui_of": "of",
"com_ui_off": "オフ",
"com_ui_on": "オン",
"com_ui_openai": "OpenAI",
"com_ui_page": "ページ",
"com_ui_prev": "前",
"com_ui_preview": "プレビュー",
@@ -623,9 +745,17 @@
"com_ui_prompts_allow_use": "プロンプトの使用を許可",
"com_ui_provider": "プロバイダ",
"com_ui_read_aloud": "読み上げる",
"com_ui_redirecting_to_provider": "{{0}}にリダイレクト、 お待ちください...",
"com_ui_refresh_link": "リンクを更新",
"com_ui_regenerate": "再度 生成する",
"com_ui_regenerate_backup": "バックアップコードの再生成",
"com_ui_regenerating": "再生成中...",
"com_ui_region": "地域",
"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_result": "結果",
"com_ui_revoke": "無効にする",
@@ -635,12 +765,17 @@
"com_ui_revoke_keys": "認証キーの無効化",
"com_ui_revoke_keys_confirm": "すべての認証情報を無効にしてもよろしいですか?",
"com_ui_role_select": "役割",
"com_ui_roleplay": "ロールプレイ",
"com_ui_run_code": "コードを実行",
"com_ui_run_code_error": "コードの実行中にエラーが発生しました",
"com_ui_save": "保存",
"com_ui_save_badge_changes": "バッジの変更を保存しますか?",
"com_ui_save_submit": "保存 & 送信",
"com_ui_saved": "保存しました!",
"com_ui_schema": "スキーマ",
"com_ui_scope": "範囲",
"com_ui_search": "検索",
"com_ui_secret_key": "秘密鍵",
"com_ui_select": "選択",
"com_ui_select_file": "ファイルを選択",
"com_ui_select_model": "モデル選択",
@@ -655,13 +790,20 @@
"com_ui_share_create_message": "あなたの名前と共有リンクを作成した後のメッセージは、共有されません。",
"com_ui_share_delete_error": "共有リンクの削除中にエラーが発生しました。",
"com_ui_share_error": "チャットの共有リンクの共有中にエラーが発生しました",
"com_ui_share_form_description": "ここに何かを入れる必要があります。空でした",
"com_ui_share_link_to_chat": "チャットへの共有リンク",
"com_ui_share_to_all_users": "全ユーザーと共有",
"com_ui_share_update_message": "あなたの名前、カスタム指示、共有リンクを作成した後のメッセージは、共有されません。",
"com_ui_share_var": "{{0}} を共有",
"com_ui_shared_link_bulk_delete_success": "共有リンクを正常に削除しました",
"com_ui_shared_link_delete_success": "共有リンクを削除しました",
"com_ui_shared_link_not_found": "共有リンクが見つかりません",
"com_ui_shared_prompts": "共有されたプロンプト",
"com_ui_shop": "買い物",
"com_ui_show": "表示",
"com_ui_show_all": "すべて表示",
"com_ui_show_qr": "QR コードを表示",
"com_ui_sign_in_to_domain": "{{0}}にサインインする",
"com_ui_simple": "シンプル",
"com_ui_size": "サイズ",
"com_ui_special_variables": "特殊変数:",
@@ -669,31 +811,49 @@
"com_ui_stop": "止める",
"com_ui_storage": "ストレージ",
"com_ui_submit": "送信する",
"com_ui_teach_or_explain": "学習",
"com_ui_temporary": "一時チャット",
"com_ui_terms_and_conditions": "利用規約",
"com_ui_terms_of_service": "利用規約",
"com_ui_thinking": "考え中...",
"com_ui_thoughts": "推論",
"com_ui_token_exchange_method": "トークン交換方法",
"com_ui_token_url": "トークンURL",
"com_ui_tools": "ツール",
"com_ui_travel": "旅行",
"com_ui_unarchive": "アーカイブ解除",
"com_ui_unarchive_error": "アーカイブ解除に失敗しました。",
"com_ui_unknown": "不明",
"com_ui_untitled": "名称未設定",
"com_ui_update": "更新",
"com_ui_upload": "アップロード",
"com_ui_upload_code_files": "コードインタープリター用にアップロード",
"com_ui_upload_delay": "ファイル \"{{0}}\"のアップロードに時間がかかっています。ファイルの検索のためのインデックス作成が完了するまでお待ちください。",
"com_ui_upload_error": "ファイルのアップロード中にエラーが発生しました。",
"com_ui_upload_file_context": "ファイルコンテキストをアップロード",
"com_ui_upload_file_search": "ファイル検索用アップロード",
"com_ui_upload_files": "ファイルをアップロード",
"com_ui_upload_image": "画像をアップロード",
"com_ui_upload_image_input": "画像をアップロード",
"com_ui_upload_invalid": "アップロードに無効なファイルです。制限を超えない画像である必要があります。",
"com_ui_upload_invalid_var": "アップロードに無効なファイルです。 {{0}} MBまでの画像である必要があります。",
"com_ui_upload_ocr_text": "テキストとしてアップロード",
"com_ui_upload_success": "アップロード成功",
"com_ui_upload_type": "アップロード種別を選択",
"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}}`のようにすると、プロンプトを使用するときに後で値を埋め込むことができます。",
"com_ui_verify": "確認する",
"com_ui_version_var": "バージョン {{0}}",
"com_ui_versions": "バージョン",
"com_ui_view_source": "ソースチャットを表示",
"com_ui_weekend_morning": "楽しい週末を",
"com_ui_write": "執筆",
"com_ui_x_selected": "{{0}}が選択された",
"com_ui_yes": "はい",
"com_ui_zoom": "ズーム",
"com_user_message": "あなた",

View File

@@ -116,6 +116,7 @@
"com_auth_registration_success_generic": "Sprawdź swoją skrzynkę email, aby zweryfikować adres email.",
"com_auth_registration_success_insecure": "Rejestracja zakończona pomyślnie.",
"com_auth_reset_password": "Zresetuj hasło",
"com_auth_reset_password_if_email_exists": "Jeśli konto z tym adresem e-mail istnieje, wiadomość e-mail z instrukcjami resetowania hasła została wysłana. Sprawdź folder spamu.",
"com_auth_reset_password_link_sent": "Link do resetowania hasła został wysłany",
"com_auth_reset_password_success": "Hasło zostało pomyślnie zresetowane",
"com_auth_sign_in": "Zaloguj się",
@@ -123,9 +124,11 @@
"com_auth_submit_registration": "Zarejestruj się",
"com_auth_to_reset_your_password": "aby zresetować hasło.",
"com_auth_to_try_again": "aby spróbować ponownie.",
"com_auth_two_factor": "Sprawdź preferowaną aplikację do kodów 2FA, aby uzyskać kod.",
"com_auth_username": "Nazwa użytkownika (opcjonalnie)",
"com_auth_username_max_length": "Nazwa użytkownika nie może zawierać więcej niż 20 znaków",
"com_auth_username_min_length": "Nazwa użytkownika musi zawierać co najmniej 2 znaki",
"com_auth_verify_your_identity": "Zweryfikuj swoją tożsamość",
"com_auth_welcome_back": "Witamy z powrotem",
"com_click_to_download": "(kliknij tutaj, aby pobrać)",
"com_download_expired": "(pobieranie wygasło)",
@@ -136,6 +139,7 @@
"com_endpoint_agent_placeholder": "Proszę wybrać agenta",
"com_endpoint_ai": "AI",
"com_endpoint_anthropic_maxoutputtokens": "Maksymalna liczba tokenów, która może zostać wygenerowana w odpowiedzi. Wybierz mniejszą wartość dla krótszych odpowiedzi i większą wartość dla dłuższych odpowiedzi.",
"com_endpoint_anthropic_prompt_cache": "Prompt caching umożliwia ponowne wykorzystanie dużego kontekstu lub instrukcji w wywołaniach API, zmniejszając koszty i opóźnienia.",
"com_endpoint_anthropic_temp": "Zakres od 0 do 1. Użyj wartości bliżej 0 dla analizy/wyboru wielokrotnego, a bliżej 1 dla zadań twórczych i generatywnych. Zalecamy dostosowanie tej wartości lub Top P, ale nie obu jednocześnie.",
"com_endpoint_anthropic_thinking": "Włącza wewnętrzne rozumowanie dla wspieranych modeli Claude (3.7 Sonnet). Notatka: wymaga \"Thinking Budget\" by był włączony oraz mniejszy niż \"Max Output Tokens\".",
"com_endpoint_anthropic_topk": "Top-K wpływa na sposób wyboru tokenów przez model. Top-K równa 1 oznacza, że wybrany token jest najbardziej prawdopodobny spośród wszystkich tokenów w słowniku modelu (tzw. dekodowanie zachłanne), podczas gdy top-K równa 3 oznacza, że następny token zostaje wybrany spośród 3 najbardziej prawdopodobnych tokenów (za pomocą temperatury).",
@@ -155,6 +159,7 @@
"com_endpoint_config_key_encryption": "Twój klucz zostanie zaszyfrowany i usunięty o",
"com_endpoint_config_key_for": "Ustaw klucz API dla",
"com_endpoint_config_key_google_need_to": "Powinieneś ",
"com_endpoint_config_key_google_vertex_ai": "Włącz Vertex AI",
"com_endpoint_config_key_import_json_key": "Importuj klucz JSON konta usługi.",
"com_endpoint_config_key_import_json_key_invalid": "Nieprawidłowy klucz JSON konta usługi. Czy zaimportowano właściwy plik?",
"com_endpoint_config_key_import_json_key_success": "Pomyślnie zaimportowano klucz JSON konta usługi",
@@ -346,6 +351,7 @@
"com_nav_long_audio_warning": "Dłuższe teksty będą potrzebować więcej czasu na przetworzenie.",
"com_nav_maximize_chat_space": "Maksymalizuj przestrzeń czatu",
"com_nav_modular_chat": "Włącz przełączanie punktów końcowych w trakcie rozmowy",
"com_nav_my_files": "Moje pliki",
"com_nav_not_supported": "Nieobsługiwane",
"com_nav_open_sidebar": "Otwórz pasek boczny",
"com_nav_playback_rate": "Szybkość odtwarzania audio",
@@ -399,6 +405,12 @@
"com_sidepanel_hide_panel": "Ukryj Panel",
"com_sidepanel_manage_files": "Zarządzaj Plikami",
"com_sidepanel_parameters": "Parametry",
"com_ui_2fa_account_security": "2FA dodaje dodatkową warstwę bezpieczeństwa do twojego konta.",
"com_ui_2fa_disable": " Wyłącz 2FA",
"com_ui_2fa_disable_error": "Błąd podczas wyłączania 2FA",
"com_ui_2fa_disabled": "2FA zostało wyłączone",
"com_ui_2fa_enable": "Włącz 2FA",
"com_ui_2fa_enabled": "2FA zostało włączone",
"com_ui_accept": "Akceptuję",
"com_ui_add": "Dodaj",
"com_ui_add_model_preset": "Dodaj model lub preset dla dodatkowej odpowiedzi",
@@ -672,6 +684,7 @@
"com_ui_speech_while_submitting": "Nie można przesłać mowy podczas generowania odpowiedzi",
"com_ui_storage": "Przechowywanie",
"com_ui_submit": "Wyślij",
"com_ui_temporary": "Czat tymczasowy",
"com_ui_terms_and_conditions": "Warunki użytkowania",
"com_ui_terms_of_service": "Warunki korzystania z usługi",
"com_ui_thinking": "Myślenie...",

View File

@@ -484,6 +484,7 @@
"com_ui_analyzing_finished": "Анализ завершен",
"com_ui_api_key": "ключ API",
"com_ui_archive": "Архивировать",
"com_ui_archive_delete_error": "Не удалось удалить архивированный чат",
"com_ui_archive_error": "Не удалось заархивировать чат",
"com_ui_artifact_click": "Нажмите, чтобы открыть",
"com_ui_artifacts": "Артефакты",
@@ -536,6 +537,7 @@
"com_ui_bulk_delete_error": "Не удалось удалить общие ссылки",
"com_ui_callback_url": "URL обратного вызова",
"com_ui_cancel": "Отмена",
"com_ui_category": "Категория",
"com_ui_chat": "Чат",
"com_ui_chat_history": "История чатов",
"com_ui_clear": "Удалить",
@@ -555,6 +557,7 @@
"com_ui_context": "Контекст",
"com_ui_continue": "Продолжить",
"com_ui_controls": "Управление",
"com_ui_convo_delete_error": "Не удалось удалить чат",
"com_ui_copied": "Скопировано",
"com_ui_copied_to_clipboard": "Скопировано в буфер обмена",
"com_ui_copy_code": "Копировать код",
@@ -642,10 +645,15 @@
"com_ui_fork_info_2": "\"Форкинг\" означает создание новой ветви разговора, которая начинается или заканчивается на определенных сообщениях текущего разговора, создавая копию в соответствии с выбранными параметрами.",
"com_ui_fork_info_3": "\"Целевое сообщение\" относится либо к сообщению, из которого было открыто это всплывающее окно, либо, если вы отметите \"{{0}}\", к последнему сообщению в диалоге.",
"com_ui_fork_info_branches": "Эта опция создает ветвление видимых сообщений вместе со связанными ветвями; другими словами, прямой путь к целевому сообщению, включая ветви на этом пути.",
"com_ui_fork_info_button_label": "Просмотреть информацию о разветвлении диалогов",
"com_ui_fork_info_remember": "Отметьте это, чтобы запомнить выбранные вами параметры для будущего использования, что позволит быстрее создавать ответвления бесед по вашим предпочтениям.",
"com_ui_fork_info_start": "Если отмечено, ветвление начнется с этого сообщения до последнего сообщения в разговоре в соответствии с выбранным выше поведением.",
"com_ui_fork_info_target": "Эта опция создает ветвление всех сообщений, ведущих к целевому сообщению, включая соседние. Другими словами, включаются все ветви сообщений, независимо от того, видны они или находятся по одному пути.",
"com_ui_fork_info_visible": "Эта опция создает ветвь только для видимых сообщений, то есть прямой путь к целевому сообщению без боковых ветвей.",
"com_ui_fork_more_details_about": "Просмотреть дополнительную информацию и сведения о варианте разветвления «{{0}}»",
"com_ui_fork_more_info_options": "Просмотреть подробное описание всех вариантов разветвления и их поведения",
"com_ui_fork_more_info_remember": "Просмотреть объяснение того, как вариант «{{0}}» сохраняет ваши настройки для будущих разветвлений",
"com_ui_fork_more_info_split_target": "Просмотреть объяснение того, как вариант «{{0}}» влияет на включаемые в разветвление сообщения",
"com_ui_fork_processing": "Разветвление беседы...",
"com_ui_fork_remember": "Запомнить",
"com_ui_fork_remember_checked": "Ваш выбор будет сохранен после использования. Вы можете изменить его в любое время в настройках.",
@@ -698,6 +706,7 @@
"com_ui_name": "Имя",
"com_ui_new": "Новый",
"com_ui_new_chat": "Создать чат",
"com_ui_new_conversation_title": "Название нового чата",
"com_ui_next": "Следующий",
"com_ui_no": "Нет",
"com_ui_no_backup_codes": "Резервные коды отсутствуют. Сгенерируйте новые",
@@ -737,6 +746,8 @@
"com_ui_regenerating": "Повторная генерация...",
"com_ui_region": "Регион",
"com_ui_rename": "Переименовать",
"com_ui_rename_conversation": "Переименовать чат",
"com_ui_rename_failed": "Не удалось переименовать чат",
"com_ui_rename_prompt": "Переименовать промт",
"com_ui_requires_auth": "Требуется аутентификация",
"com_ui_reset_var": "Сбросить {{0}}",
@@ -787,8 +798,14 @@
"com_ui_sign_in_to_domain": "Вход в {{0}}",
"com_ui_simple": "Простой",
"com_ui_size": "Размер",
"com_ui_special_var_current_date": "Текущая дата",
"com_ui_special_var_current_datetime": "Текущая дата и время",
"com_ui_special_var_current_user": "Текущий пользователь",
"com_ui_special_var_iso_datetime": "Дата и время в формате UTC ISO",
"com_ui_special_variables": "Специальные переменные:",
"com_ui_special_variables_more_info": "Вы можете выбрать специальные переменные из выпадающего списка: `{{current_date}}` (сегодняшняя дата и день недели), `{{current_datetime}}` (местные дата и время), `{{utc_iso_datetime}}` (дата и время в формате UTC ISO), `{{current_user}}` (имя вашего аккаунта).",
"com_ui_speech_while_submitting": "Невозможно отправить голосовой ввод во время генерации ответа",
"com_ui_sr_actions_menu": "Открыть меню действий для \"{{0}}\"",
"com_ui_stop": "Остановить генерацию",
"com_ui_storage": "Хранилище",
"com_ui_submit": "Отправить",
@@ -805,6 +822,7 @@
"com_ui_unarchive": "разархивировать",
"com_ui_unarchive_error": "Не удалось восстановить чат из архива",
"com_ui_unknown": "Неизвестно",
"com_ui_untitled": "Без названия",
"com_ui_update": "Обновить",
"com_ui_upload": "Загрузить",
"com_ui_upload_code_files": "Загрузить для Интерпретатора кода",

View File

@@ -1,4 +1,5 @@
{
"com_a11y_end": "AI đã trả lời xong.",
"com_auth_already_have_account": "Đã có tài khoản?",
"com_auth_click": "Nhấp chuột",
"com_auth_click_here": "Nhấp vào đây",

View File

@@ -127,7 +127,7 @@
"com_auth_submit_registration": "注册提交",
"com_auth_to_reset_your_password": "重置密码。",
"com_auth_to_try_again": "再试一次。",
"com_auth_two_factor": "查看您首选的一次性密码应用程序获取码",
"com_auth_two_factor": "查看您首选的一次性密码应用程序获取码",
"com_auth_username": "用户名(可选)",
"com_auth_username_max_length": "用户名最多 20 个字符",
"com_auth_username_min_length": "用户名至少 2 个字符",
@@ -135,7 +135,7 @@
"com_auth_welcome_back": "欢迎",
"com_click_to_download": "(点击此处下载)",
"com_download_expired": "下载已过期",
"com_download_expires": "(点击此处下载 - {{0}}后过期)",
"com_download_expires": "(点击此处下载 - {{0}} 后过期)",
"com_endpoint": "端点",
"com_endpoint_agent": "代理",
"com_endpoint_agent_model": "代理模型(推荐: GPT-3.5",
@@ -282,7 +282,7 @@
"com_hide_examples": "隐藏示例",
"com_nav_2fa": "双重身份验证2FA",
"com_nav_account_settings": "账户设置",
"com_nav_always_make_prod": "始终用新版本",
"com_nav_always_make_prod": "始终使用新版本",
"com_nav_archive_created_at": "归档时间",
"com_nav_archive_name": "名称",
"com_nav_archived_chats": "归档的对话",
@@ -485,6 +485,7 @@
"com_ui_analyzing_finished": "完成分析",
"com_ui_api_key": "API 密钥",
"com_ui_archive": "归档",
"com_ui_archive_delete_error": "删除已归档对话失败",
"com_ui_archive_error": "归档对话失败",
"com_ui_artifact_click": "点击以打开",
"com_ui_artifacts": "Artifacts",
@@ -538,6 +539,7 @@
"com_ui_bulk_delete_error": "删除分享链接时失败",
"com_ui_callback_url": "回调 URL",
"com_ui_cancel": "取消",
"com_ui_category": "类别",
"com_ui_chat": "对话",
"com_ui_chat_history": "对话历史",
"com_ui_clear": "清除",
@@ -557,6 +559,7 @@
"com_ui_context": "上下文",
"com_ui_continue": "继续",
"com_ui_controls": "管理",
"com_ui_convo_delete_error": "删除对话失败",
"com_ui_copied": "已复制!",
"com_ui_copied_to_clipboard": "已复制到剪贴板",
"com_ui_copy_code": "复制代码",
@@ -565,7 +568,7 @@
"com_ui_create": "创建",
"com_ui_create_link": "创建链接",
"com_ui_create_prompt": "创建提示词",
"com_ui_currently_production": "目前正在生产中",
"com_ui_currently_production": "目前正在使用中",
"com_ui_custom": "自定义",
"com_ui_custom_header_name": "自定义 Header 名称",
"com_ui_custom_prompt_mode": "自定义提示词模式",
@@ -622,7 +625,7 @@
"com_ui_endpoint": "端点",
"com_ui_endpoint_menu": "LLM 端点菜单",
"com_ui_enter": "进入",
"com_ui_enter_api_key": "输入API密钥",
"com_ui_enter_api_key": "输入 API 密钥",
"com_ui_enter_openapi_schema": "请在此输入OpenAPI架构",
"com_ui_enter_var": "输入 {{0}}",
"com_ui_error": "错误",
@@ -682,14 +685,14 @@
"com_ui_instructions": "指令",
"com_ui_late_night": "夜深了",
"com_ui_latest_footer": "Every AI for Everyone.",
"com_ui_latest_production_version": "最新的生产版本",
"com_ui_latest_production_version": "最新在用版本",
"com_ui_latest_version": "最新版本",
"com_ui_librechat_code_api_key": "获取您的 LibreChat 代码解释器 API 密钥",
"com_ui_librechat_code_api_subtitle": "安全可靠。多语言支持。文件输入/输出。",
"com_ui_librechat_code_api_title": "运行 AI 代码",
"com_ui_loading": "加载中...",
"com_ui_locked": "已锁定",
"com_ui_logo": "{{0}}标识",
"com_ui_logo": "{{0}} 标识",
"com_ui_manage": "管理",
"com_ui_max_tags": "最多允许 {{0}} 个,用最新值。",
"com_ui_mcp_servers": "MCP 服务器",
@@ -703,6 +706,7 @@
"com_ui_name": "名称",
"com_ui_new": "新",
"com_ui_new_chat": "创建新对话",
"com_ui_new_conversation_title": "新对话标题",
"com_ui_next": "下一页",
"com_ui_no": "否",
"com_ui_no_backup_codes": "无可用的备份代码。请生成新的备份代码。",
@@ -746,9 +750,11 @@
"com_ui_regenerating": "重新生成中...",
"com_ui_region": "区域",
"com_ui_rename": "重命名",
"com_ui_rename_conversation": "重命名对话",
"com_ui_rename_failed": "重命名对话失败",
"com_ui_rename_prompt": "重命名 Prompt",
"com_ui_requires_auth": "需要认证",
"com_ui_reset_var": "重置{{0}}",
"com_ui_reset_var": "重置 {{0}}",
"com_ui_result": "结果",
"com_ui_revoke": "撤销",
"com_ui_revoke_info": "撤销所有用户提供的凭据",
@@ -802,7 +808,7 @@
"com_ui_stop": "停止",
"com_ui_storage": "存储",
"com_ui_submit": "提交",
"com_ui_teach_or_explain": "学习",
"com_ui_teach_or_explain": "学习",
"com_ui_temporary": "临时对话",
"com_ui_terms_and_conditions": "条款和条件",
"com_ui_terms_of_service": "服务政策",
@@ -815,6 +821,7 @@
"com_ui_unarchive": "取消归档",
"com_ui_unarchive_error": "取消归档对话失败",
"com_ui_unknown": "未知",
"com_ui_untitled": "无标题",
"com_ui_update": "更新",
"com_ui_upload": "上传",
"com_ui_upload_code_files": "上传代码解释器文件",

View File

@@ -5,6 +5,7 @@ import App from './App';
import './style.css';
import './mobile.css';
import { ApiErrorBoundaryProvider } from './hooks/ApiErrorBoundaryContext';
import 'katex/dist/katex.min.css';
const container = document.getElementById('root');
const root = createRoot(container);

View File

@@ -43,7 +43,8 @@ export default function ChatRoute() {
refetchOnMount: 'always',
});
const initialConvoQuery = useGetConvoIdQuery(conversationId, {
enabled: isAuthenticated && conversationId !== Constants.NEW_CONVO,
enabled:
isAuthenticated && conversationId !== Constants.NEW_CONVO && !hasSetConversation.current,
});
const endpointsQuery = useGetEndpointsQuery({ enabled: isAuthenticated });
const assistantListMap = useAssistantListMap();

View File

@@ -32,13 +32,28 @@ export const currentArtifactId = atom<string | null>({
] as const,
});
export const artifactsVisible = atom<boolean>({
key: 'artifactsVisible',
export const artifactsVisibility = atom<boolean>({
key: 'artifactsVisibility',
default: true,
effects: [
({ onSet, node }) => {
onSet(async (newValue) => {
logger.log('artifacts', 'Recoil Effect: Setting artifactsVisible', {
logger.log('artifacts', 'Recoil Effect: Setting artifactsVisibility', {
key: node.key,
newValue,
});
});
},
] as const,
});
export const visibleArtifacts = atom<Record<string, Artifact | undefined> | null>({
key: 'visibleArtifacts',
default: null,
effects: [
({ onSet, node }) => {
onSet(async (newValue) => {
logger.log('artifacts', 'Recoil Effect: Setting `visibleArtifacts`', {
key: node.key,
newValue,
});

View File

@@ -14,7 +14,8 @@ import { LocalStorageKeys, Constants } from 'librechat-data-provider';
import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider';
import type { TOptionSettings, ExtendedFile } from '~/common';
import { useSetConvoContext } from '~/Providers/SetConvoContext';
import { storeEndpointSettings, logger } from '~/utils';
import { storeEndpointSettings, logger, createChatSearchParams } from '~/utils';
import { createSearchParams } from 'react-router-dom';
const latestMessageKeysAtom = atom<(string | number)[]>({
key: 'latestMessageKeys',
@@ -73,9 +74,9 @@ const conversationByIndex = atomFamily<TConversation | null, string | number>({
default: null,
effects: [
({ onSet, node }) => {
onSet(async (newValue) => {
onSet(async (newValue, oldValue) => {
const index = Number(node.key.split('__')[1]);
logger.log('conversation', 'Setting conversation:', { index, newValue });
logger.log('conversation', 'Setting conversation:', { index, newValue, oldValue });
if (newValue?.assistant_id != null && newValue.assistant_id) {
localStorage.setItem(
`${LocalStorageKeys.ASST_ID_PREFIX}${index}${newValue.endpoint}`,
@@ -104,6 +105,18 @@ const conversationByIndex = atomFamily<TConversation | null, string | number>({
`${LocalStorageKeys.LAST_CONVO_SETUP}_${index}`,
JSON.stringify(newValue),
);
const shouldUpdateParams =
newValue.createdAt === '' &&
JSON.stringify(newValue) !== JSON.stringify(oldValue) &&
(oldValue as TConversation)?.conversationId === 'new';
if (shouldUpdateParams) {
const newParams = createChatSearchParams(newValue);
const searchParams = createSearchParams(newParams);
const url = `${window.location.pathname}?${searchParams.toString()}`;
window.history.pushState({}, '', url);
}
});
},
] as const,

View File

@@ -1,94 +0,0 @@
import { preprocessCodeArtifacts } from './artifacts';
describe('preprocessCodeArtifacts', () => {
test('should return non-string inputs unchanged', () => {
expect(preprocessCodeArtifacts(123 as unknown as string)).toBe('');
expect(preprocessCodeArtifacts(null as unknown as string)).toBe('');
expect(preprocessCodeArtifacts(undefined)).toBe('');
expect(preprocessCodeArtifacts({} as unknown as string)).toEqual('');
});
test('should remove <thinking> tags and their content', () => {
const input = '<thinking>This should be removed</thinking>Some content';
const expected = 'Some content';
expect(preprocessCodeArtifacts(input)).toBe(expected);
});
test('should remove unclosed <thinking> tags and their content', () => {
const input = '<thinking>This should be removed\nSome content';
const expected = '';
expect(preprocessCodeArtifacts(input)).toBe(expected);
});
test('should remove artifact headers up to and including empty code block', () => {
const input = ':::artifact{identifier="test"}\n```\n```\nSome content';
const expected = ':::artifact{identifier="test"}\n```\n```\nSome content';
expect(preprocessCodeArtifacts(input)).toBe(expected);
});
test('should keep artifact headers when followed by empty code block and content', () => {
const input = ':::artifact{identifier="test"}\n```\n```\nSome content';
const expected = ':::artifact{identifier="test"}\n```\n```\nSome content';
expect(preprocessCodeArtifacts(input)).toBe(expected);
});
test('should handle multiple artifact headers correctly', () => {
const input = ':::artifact{id="1"}\n```\n```\n:::artifact{id="2"}\n```\ncode\n```\nContent';
const expected = ':::artifact{id="1"}\n```\n```\n:::artifact{id="2"}\n```\ncode\n```\nContent';
expect(preprocessCodeArtifacts(input)).toBe(expected);
});
test('should handle complex input with multiple patterns', () => {
const input = `
<thinking>Remove this</thinking>
Some text
:::artifact{id="1"}
\`\`\`
\`\`\`
<thinking>And this</thinking>
:::artifact{id="2"}
\`\`\`
keep this code
\`\`\`
More text
`;
const expected = `
Some text
:::artifact{id="1"}
\`\`\`
\`\`\`
:::artifact{id="2"}
\`\`\`
keep this code
\`\`\`
More text
`;
expect(preprocessCodeArtifacts(input)).toBe(expected);
});
test('should remove artifact headers without code blocks', () => {
const input = ':::artifact{identifier="test"}\nSome content without code block';
const expected = '';
expect(preprocessCodeArtifacts(input)).toBe(expected);
});
test('should remove artifact headers up to incomplete code block', () => {
const input = ':::artifact{identifier="react-cal';
const expected = '';
expect(preprocessCodeArtifacts(input)).toBe(expected);
});
test('should keep artifact headers when any character follows code block', () => {
const input = ':::artifact{identifier="react-calculator"}\n```t';
const expected = ':::artifact{identifier="react-calculator"}\n```t';
expect(preprocessCodeArtifacts(input)).toBe(expected);
});
test('should keep artifact headers when whitespace follows code block', () => {
const input = ':::artifact{identifier="react-calculator"}\n``` ';
const expected = ':::artifact{identifier="react-calculator"}\n``` ';
expect(preprocessCodeArtifacts(input)).toBe(expected);
});
});

View File

@@ -214,23 +214,3 @@ export const sharedFiles = {
</html>
`,
};
export function preprocessCodeArtifacts(text?: string): string {
if (typeof text !== 'string') {
return '';
}
// Remove <thinking> tags and their content
text = text.replace(/<thinking>[\s\S]*?<\/thinking>|<thinking>[\s\S]*/g, '');
// Process artifact headers
const regex = /(^|\n)(:::artifact[\s\S]*?(?:```[\s\S]*?```|$))/g;
return text.replace(regex, (match, newline, artifactBlock) => {
if (artifactBlock.includes('```') === true) {
// Keep artifact headers with code blocks (empty or not)
return newline + artifactBlock;
}
// Remove artifact headers without code blocks, but keep the newline
return newline;
});
}

View File

@@ -4,7 +4,7 @@ import {
isAssistantsEndpoint,
isAgentsEndpoint,
} from 'librechat-data-provider';
import type { TConversation } from 'librechat-data-provider';
import type { TConversation, EndpointSchemaKey } from 'librechat-data-provider';
import { getLocalStorageItems } from './localStorage';
const buildDefaultConvo = ({
@@ -51,8 +51,8 @@ const buildDefaultConvo = ({
}
const convo = parseConvo({
endpoint,
endpointType,
endpoint: endpoint as EndpointSchemaKey,
endpointType: endpointType as EndpointSchemaKey,
conversation: lastConversationSetup,
possibleValues: {
models: possibleModels,
@@ -68,7 +68,7 @@ const buildDefaultConvo = ({
};
// Ensures assistant_id is always defined
const assistantId = convo?.assistant_id ?? '';
const assistantId = convo?.assistant_id ?? conversation?.assistant_id ?? '';
const defaultAssistantId = lastConversationSetup?.assistant_id ?? '';
if (isAssistantsEndpoint(endpoint) && !defaultAssistantId && assistantId) {
defaultConvo.assistant_id = assistantId;

View File

@@ -431,14 +431,14 @@ describe('Conversation Utilities', () => {
pageParams: [],
};
const newConvo = makeConversation('new');
const updated = addConversationToInfinitePages(data, newConvo);
const updated = addConversationToInfinitePages(data, newConvo as TConversation);
expect(updated.pages[0].conversations[0].conversationId).toBe('new');
expect(updated.pages[0].conversations[1].conversationId).toBe('1');
});
it('creates new InfiniteData if data is undefined', () => {
const newConvo = makeConversation('new');
const updated = addConversationToInfinitePages(undefined, newConvo);
const updated = addConversationToInfinitePages(undefined, newConvo as TConversation);
expect(updated.pages[0].conversations[0].conversationId).toBe('new');
expect(updated.pageParams).toEqual([undefined]);
});
@@ -531,12 +531,12 @@ describe('Conversation Utilities', () => {
it('stores model for endpoint', () => {
const conversation = {
conversationId: '1',
endpoint: 'openai',
endpoint: 'openAI',
model: 'gpt-3',
};
storeEndpointSettings(conversation as any);
const stored = JSON.parse(localStorage.getItem('lastModel') || '{}');
expect([undefined, 'gpt-3']).toContain(stored.openai);
expect([undefined, 'gpt-3']).toContain(stored.openAI);
});
it('stores secondaryModel for gptPlugins endpoint', () => {
@@ -574,14 +574,14 @@ describe('Conversation Utilities', () => {
conversationId: 'a',
updatedAt: '2024-01-01T12:00:00Z',
createdAt: '2024-01-01T10:00:00Z',
endpoint: 'openai',
endpoint: 'openAI',
model: 'gpt-3',
title: 'Conversation A',
} as TConversation;
convoB = {
conversationId: 'b',
updatedAt: '2024-01-02T12:00:00Z',
endpoint: 'openai',
endpoint: 'openAI',
model: 'gpt-3',
} as TConversation;
queryClient.setQueryData(['allConversations'], {

View File

@@ -280,11 +280,11 @@ export function updateConvoFieldsInfinite(
pages: data.pages.map((page, pi) =>
pi === pageIdx
? {
...page,
conversations: page.conversations.map((c, ci) =>
ci === convoIdx ? { ...c, ...updatedConversation } : c,
),
}
...page,
conversations: page.conversations.map((c, ci) =>
ci === convoIdx ? { ...c, ...updatedConversation } : c,
),
}
: page,
),
};

View File

@@ -0,0 +1,363 @@
import { EModelEndpoint, Constants } from 'librechat-data-provider';
import type { TConversation, TPreset } from 'librechat-data-provider';
import createChatSearchParams from './createChatSearchParams';
describe('createChatSearchParams', () => {
describe('conversation inputs', () => {
it('handles basic conversation properties', () => {
const conversation: Partial<TConversation> = {
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
temperature: 0.7,
};
const result = createChatSearchParams(conversation as TConversation);
expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(result.get('model')).toBe('gpt-4');
expect(result.get('temperature')).toBe('0.7');
});
it('applies only the endpoint property when other conversation fields are absent', () => {
const endpointOnly = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
} as TConversation);
expect(endpointOnly.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(endpointOnly.has('model')).toBe(false);
expect(endpointOnly.has('endpoint')).toBe(true);
});
it('applies only the model property when other conversation fields are absent', () => {
const modelOnly = createChatSearchParams({ model: 'gpt-4' } as TConversation);
expect(modelOnly.has('endpoint')).toBe(false);
expect(modelOnly.get('model')).toBe('gpt-4');
expect(modelOnly.has('model')).toBe(true);
});
it('includes assistant_id when endpoint is assistants', () => {
const withAssistantId = createChatSearchParams({
endpoint: EModelEndpoint.assistants,
model: 'gpt-4',
assistant_id: 'asst_123',
temperature: 0.7,
} as TConversation);
expect(withAssistantId.get('assistant_id')).toBe('asst_123');
expect(withAssistantId.has('endpoint')).toBe(false);
expect(withAssistantId.has('model')).toBe(false);
expect(withAssistantId.has('temperature')).toBe(false);
});
it('includes agent_id when endpoint is agents', () => {
const withAgentId = createChatSearchParams({
endpoint: EModelEndpoint.agents,
model: 'gpt-4',
agent_id: 'agent_123',
temperature: 0.7,
} as TConversation);
expect(withAgentId.get('agent_id')).toBe('agent_123');
expect(withAgentId.has('endpoint')).toBe(false);
expect(withAgentId.has('model')).toBe(false);
expect(withAgentId.has('temperature')).toBe(false);
});
it('excludes all parameters except assistant_id when endpoint is assistants', () => {
const withAssistantId = createChatSearchParams({
endpoint: EModelEndpoint.assistants,
model: 'gpt-4',
assistant_id: 'asst_123',
temperature: 0.7,
} as TConversation);
expect(withAssistantId.get('assistant_id')).toBe('asst_123');
expect(withAssistantId.has('endpoint')).toBe(false);
expect(withAssistantId.has('model')).toBe(false);
expect(withAssistantId.has('temperature')).toBe(false);
expect([...withAssistantId.entries()].length).toBe(1);
});
it('excludes all parameters except agent_id when endpoint is agents', () => {
const withAgentId = createChatSearchParams({
endpoint: EModelEndpoint.agents,
model: 'gpt-4',
agent_id: 'agent_123',
temperature: 0.7,
} as TConversation);
expect(withAgentId.get('agent_id')).toBe('agent_123');
expect(withAgentId.has('endpoint')).toBe(false);
expect(withAgentId.has('model')).toBe(false);
expect(withAgentId.has('temperature')).toBe(false);
expect([...withAgentId.entries()].length).toBe(1);
});
it('returns empty params when agent endpoint has no agent_id', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.agents,
model: 'gpt-4',
temperature: 0.7,
} as TConversation);
expect(result.toString()).toBe('');
expect([...result.entries()].length).toBe(0);
});
it('returns empty params when assistants endpoint has no assistant_id', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.assistants,
model: 'gpt-4',
temperature: 0.7,
} as TConversation);
expect(result.toString()).toBe('');
expect([...result.entries()].length).toBe(0);
});
it('ignores agent_id when it matches EPHEMERAL_AGENT_ID', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.agents,
model: 'gpt-4',
agent_id: Constants.EPHEMERAL_AGENT_ID,
temperature: 0.7,
} as TConversation);
// The agent_id is ignored but other params are still included
expect(result.has('agent_id')).toBe(false);
expect(result.get('endpoint')).toBe(EModelEndpoint.agents);
expect(result.get('model')).toBe('gpt-4');
expect(result.get('temperature')).toBe('0.7');
});
it('handles stop arrays correctly by joining with commas', () => {
const withStopArray = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
stop: ['stop1', 'stop2'],
} as TConversation);
expect(withStopArray.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(withStopArray.get('model')).toBe('gpt-4');
expect(withStopArray.get('stop')).toBe('stop1,stop2');
});
it('filters out non-supported array properties', () => {
const withOtherArray = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
otherArrayProp: ['value1', 'value2'],
} as any);
expect(withOtherArray.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(withOtherArray.get('model')).toBe('gpt-4');
expect(withOtherArray.has('otherArrayProp')).toBe(false);
});
it('includes empty arrays in output params', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
stop: [],
});
expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(result.has('stop')).toBe(true);
expect(result.get('stop')).toBe('');
});
it('handles non-stop arrays correctly in paramMap', () => {
const conversation: any = {
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
top_p: ['0.7', '0.8'],
};
const result = createChatSearchParams(conversation);
const expectedJson = JSON.stringify(['0.7', '0.8']);
expect(result.get('top_p')).toBe(expectedJson);
expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(result.get('model')).toBe('gpt-4');
});
it('includes empty non-stop arrays as serialized empty arrays', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
temperature: 0.7,
top_p: [],
} as any);
expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(result.get('model')).toBe('gpt-4');
expect(result.get('temperature')).toBe('0.7');
expect(result.has('top_p')).toBe(true);
expect(result.get('top_p')).toBe('[]');
});
it('excludes parameters with null or undefined values from the output', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.openAI,
model: 'gpt-4',
temperature: 0.7,
top_p: undefined,
presence_penalty: undefined,
frequency_penalty: null,
} as any);
expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
expect(result.get('model')).toBe('gpt-4');
expect(result.get('temperature')).toBe('0.7');
expect(result.has('top_p')).toBe(false);
expect(result.has('presence_penalty')).toBe(false);
expect(result.has('frequency_penalty')).toBe(false);
expect(result).toBeDefined();
});
it('handles float parameter values correctly', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.google,
model: 'gemini-pro',
frequency_penalty: 0.25,
temperature: 0.75,
});
expect(result.get('endpoint')).toBe(EModelEndpoint.google);
expect(result.get('model')).toBe('gemini-pro');
expect(result.get('frequency_penalty')).toBe('0.25');
expect(result.get('temperature')).toBe('0.75');
});
it('handles integer parameter values correctly', () => {
const result = createChatSearchParams({
endpoint: EModelEndpoint.google,
model: 'gemini-pro',
topK: 40,
maxOutputTokens: 2048,
});
expect(result.get('endpoint')).toBe(EModelEndpoint.google);
expect(result.get('model')).toBe('gemini-pro');
expect(result.get('topK')).toBe('40');
expect(result.get('maxOutputTokens')).toBe('2048');
});
});
describe('preset inputs', () => {
it('handles preset objects correctly', () => {
const preset: Partial<TPreset> = {
endpoint: EModelEndpoint.google,
model: 'gemini-pro',
temperature: 0.5,
topP: 0.8,
};
const result = createChatSearchParams(preset as TPreset);
expect(result.get('endpoint')).toBe(EModelEndpoint.google);
expect(result.get('model')).toBe('gemini-pro');
expect(result.get('temperature')).toBe('0.5');
expect(result.get('topP')).toBe('0.8');
});
it('returns only spec param when spec property is present', () => {
const preset: Partial<TPreset> = {
endpoint: EModelEndpoint.google,
model: 'gemini-pro',
temperature: 0.5,
spec: 'special_spec',
};
const result = createChatSearchParams(preset as TPreset);
expect(result.get('spec')).toBe('special_spec');
expect(result.has('endpoint')).toBe(false);
expect(result.has('model')).toBe(false);
expect(result.has('temperature')).toBe(false);
expect([...result.entries()].length).toBe(1);
});
});
describe('record inputs', () => {
it('includes allowed parameters from Record inputs', () => {
const record: Record<string, any> = {
endpoint: EModelEndpoint.anthropic,
model: 'claude-2',
temperature: '0.8',
top_p: '0.95',
extraParam: 'should-not-be-included',
invalidParam1: 'value1',
invalidParam2: 'value2',
};
const result = createChatSearchParams(record);
expect(result.get('endpoint')).toBe(EModelEndpoint.anthropic);
expect(result.get('model')).toBe('claude-2');
expect(result.get('temperature')).toBe('0.8');
expect(result.get('top_p')).toBe('0.95');
});
it('excludes disallowed parameters from Record inputs', () => {
const record: Record<string, any> = {
endpoint: EModelEndpoint.anthropic,
model: 'claude-2',
extraParam: 'should-not-be-included',
invalidParam1: 'value1',
invalidParam2: 'value2',
};
const result = createChatSearchParams(record);
expect(result.has('extraParam')).toBe(false);
expect(result.has('invalidParam1')).toBe(false);
expect(result.has('invalidParam2')).toBe(false);
expect(result.toString().includes('invalidParam')).toBe(false);
expect(result.toString().includes('extraParam')).toBe(false);
});
it('includes valid values from Record inputs', () => {
const record: Record<string, any> = {
temperature: '0.7',
top_p: null,
frequency_penalty: undefined,
};
const result = createChatSearchParams(record);
expect(result.get('temperature')).toBe('0.7');
});
it('excludes null or undefined values from Record inputs', () => {
const record: Record<string, any> = {
temperature: '0.7',
top_p: null,
frequency_penalty: undefined,
};
const result = createChatSearchParams(record);
expect(result.has('top_p')).toBe(false);
expect(result.has('frequency_penalty')).toBe(false);
});
it('handles generic object without endpoint or model properties', () => {
const customObject = {
temperature: '0.5',
top_p: '0.7',
customProperty: 'value',
};
const result = createChatSearchParams(customObject);
expect(result.get('temperature')).toBe('0.5');
expect(result.get('top_p')).toBe('0.7');
expect(result.has('customProperty')).toBe(false);
});
});
describe('edge cases', () => {
it('returns an empty URLSearchParams instance when input is null', () => {
const result = createChatSearchParams(null);
expect(result.toString()).toBe('');
expect(result instanceof URLSearchParams).toBe(true);
});
it('returns an empty URLSearchParams instance for an empty object input', () => {
const result = createChatSearchParams({});
expect(result.toString()).toBe('');
expect(result instanceof URLSearchParams).toBe(true);
});
});
});

View File

@@ -0,0 +1,93 @@
import { isAgentsEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider';
import type { TConversation, TPreset } from 'librechat-data-provider';
export default function createChatSearchParams(
input: TConversation | TPreset | Record<string, string> | null,
): URLSearchParams {
if (input == null) {
return new URLSearchParams();
}
const params = new URLSearchParams();
const allowedParams = [
'endpoint',
'model',
'temperature',
'presence_penalty',
'frequency_penalty',
'stop',
'top_p',
'max_tokens',
'topP',
'topK',
'maxOutputTokens',
'promptCache',
'region',
'maxTokens',
'agent_id',
'assistant_id',
];
if (input && typeof input === 'object' && !('endpoint' in input) && !('model' in input)) {
Object.entries(input as Record<string, string>).forEach(([key, value]) => {
if (value != null && allowedParams.includes(key)) {
params.set(key, value);
}
});
return params;
}
const conversation = input as TConversation | TPreset;
const endpoint = conversation.endpoint;
if (conversation.spec) {
return new URLSearchParams({ spec: conversation.spec });
}
if (
isAgentsEndpoint(endpoint) &&
conversation.agent_id &&
conversation.agent_id !== Constants.EPHEMERAL_AGENT_ID
) {
return new URLSearchParams({ agent_id: String(conversation.agent_id) });
} else if (isAssistantsEndpoint(endpoint) && conversation.assistant_id) {
return new URLSearchParams({ assistant_id: String(conversation.assistant_id) });
} else if (isAgentsEndpoint(endpoint) && !conversation.agent_id) {
return params;
} else if (isAssistantsEndpoint(endpoint) && !conversation.assistant_id) {
return params;
}
if (endpoint) {
params.set('endpoint', endpoint);
}
if (conversation.model) {
params.set('model', conversation.model);
}
const paramMap = {
temperature: conversation.temperature,
presence_penalty: conversation.presence_penalty,
frequency_penalty: conversation.frequency_penalty,
stop: conversation.stop,
top_p: conversation.top_p,
max_tokens: conversation.max_tokens,
topP: conversation.topP,
topK: conversation.topK,
maxOutputTokens: conversation.maxOutputTokens,
promptCache: conversation.promptCache,
region: conversation.region,
maxTokens: conversation.maxTokens,
};
return Object.entries(paramMap).reduce((params, [key, value]) => {
if (value != null) {
if (Array.isArray(value)) {
params.set(key, key === 'stop' ? value.join(',') : JSON.stringify(value));
} else {
params.set(key, String(value));
}
}
return params;
}, params);
}

Some files were not shown because too many files have changed in this diff Show More