Compare commits
67 Commits
v0.7.8-rc1
...
feat/openi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32a0998e4d | ||
|
|
535e7798b3 | ||
|
|
621fa6e1aa | ||
|
|
f6cc394eab | ||
|
|
8f460b9f75 | ||
|
|
c925f9f39c | ||
|
|
71effb1a66 | ||
|
|
e3acd18c07 | ||
|
|
b661057b97 | ||
|
|
0d0f408d85 | ||
|
|
eae6a969f4 | ||
|
|
5b402a755e | ||
|
|
b0405be9ea | ||
|
|
3f4dd08589 | ||
|
|
d5b399550e | ||
|
|
a5ff8253a4 | ||
|
|
0b44142383 | ||
|
|
502617db24 | ||
|
|
f2f285ca1e | ||
|
|
6dd1b39886 | ||
|
|
5a43f87584 | ||
|
|
4af72aac9b | ||
|
|
f7777a2723 | ||
|
|
e5b234bc72 | ||
|
|
4f2ed46450 | ||
|
|
66093b1eb3 | ||
|
|
d7390d24ec | ||
|
|
71105cd49c | ||
|
|
3606349a0f | ||
|
|
e3e796293c | ||
|
|
7c4c3a8796 | ||
|
|
20c9f1a783 | ||
|
|
8e1012c5aa | ||
|
|
7c92cef2b7 | ||
|
|
4fbb81c774 | ||
|
|
fc6e14efe2 | ||
|
|
6e663b2480 | ||
|
|
ddb2141eac | ||
|
|
37b50736bc | ||
|
|
5d6d13efe8 | ||
|
|
5efad8f646 | ||
|
|
9a7f763714 | ||
|
|
e6e7935fd8 | ||
|
|
4a72821d55 | ||
|
|
284fc82d8e | ||
|
|
20ad59c6f5 | ||
|
|
f0a42d20a2 | ||
|
|
1cfb9f1b3a | ||
|
|
232cdaa5f7 | ||
|
|
b4f57a18a7 | ||
|
|
291f76207f | ||
|
|
51cfd9a520 | ||
|
|
2267a251fa | ||
|
|
7a5be00f71 | ||
|
|
3628572aea | ||
|
|
b8215c314f | ||
|
|
ec1a31e852 | ||
|
|
5c01eaa36c | ||
|
|
8f783180a6 | ||
|
|
cd922131a9 | ||
|
|
102e79b185 | ||
|
|
65a0e1db54 | ||
|
|
f1e031a9f5 | ||
|
|
244b9f94dc | ||
|
|
17afeb5c36 | ||
|
|
ce407626fd | ||
|
|
2ef6e4462d |
10
.env.example
10
.env.example
@@ -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
|
||||
|
||||
@@ -438,6 +438,10 @@ OPENID_USERNAME_CLAIM=
|
||||
# Set to determine which user info property returned from OpenID Provider to store as the User's name
|
||||
OPENID_NAME_CLAIM=
|
||||
|
||||
OPENID_CUSTOM_DATA=
|
||||
OPENID_PROVIDER=
|
||||
OPENID_ADMIN_ROLE=
|
||||
|
||||
OPENID_BUTTON_LABEL=
|
||||
OPENID_IMAGE_URL=
|
||||
# Set to true to automatically redirect to the OpenID provider when a user visits the login page
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
71
CHANGELOG.md
71
CHANGELOG.md
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Dockerfile.multi
|
||||
# v0.7.8-rc1
|
||||
# v0.7.8
|
||||
|
||||
# Base for all builds
|
||||
FROM node:20-alpine AS base-min
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 ??
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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.',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,14 +43,15 @@
|
||||
"@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",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||
"axios": "^1.8.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cohere-ai": "^7.9.1",
|
||||
@@ -90,9 +91,9 @@
|
||||
"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",
|
||||
"openid-client": "^5.7.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-apple": "^2.0.2",
|
||||
"passport-discord": "^0.1.4",
|
||||
@@ -116,6 +117,6 @@
|
||||
"jest": "^29.7.0",
|
||||
"mongodb-memory-server": "^10.1.3",
|
||||
"nodemon": "^3.0.3",
|
||||
"supertest": "^7.0.0"
|
||||
"supertest": "^7.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]} */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -428,6 +428,8 @@ const chatV2 = async (req, res) => {
|
||||
thread_id,
|
||||
model: assistant_id,
|
||||
endpoint,
|
||||
spec: endpointOption.spec,
|
||||
iconURL: endpointOption.iconURL,
|
||||
};
|
||||
|
||||
sendMessage(res, {
|
||||
|
||||
@@ -75,6 +75,7 @@ router.get('/', async function (req, res) {
|
||||
process.env.SHOW_BIRTHDAY_ICON === '',
|
||||
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
|
||||
interface: req.app.locals.interfaceConfig,
|
||||
turnstile: req.app.locals.turnstileConfig,
|
||||
modelSpecs: req.app.locals.modelSpecs,
|
||||
balance: req.app.locals.balance,
|
||||
sharedLinksEnabled,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ const { initializeFirebase } = require('./Files/Firebase/initialize');
|
||||
const loadCustomConfig = require('./Config/loadCustomConfig');
|
||||
const handleRateLimits = require('./Config/handleRateLimits');
|
||||
const { loadDefaultInterface } = require('./start/interface');
|
||||
const { loadTurnstileConfig } = require('./start/turnstile');
|
||||
const { azureConfigSetup } = require('./start/azureOpenAI');
|
||||
const { processModelSpecs } = require('./start/modelSpecs');
|
||||
const { initializeS3 } = require('./Files/S3/initialize');
|
||||
@@ -23,7 +24,6 @@ const { getMCPManager } = require('~/config');
|
||||
const paths = require('~/config/paths');
|
||||
|
||||
/**
|
||||
*
|
||||
* Loads custom config and initializes app-wide variables.
|
||||
* @function AppService
|
||||
* @param {Express.Application} app - The Express application object.
|
||||
@@ -74,6 +74,7 @@ const AppService = async (app) => {
|
||||
const socialLogins =
|
||||
config?.registration?.socialLogins ?? configDefaults?.registration?.socialLogins;
|
||||
const interfaceConfig = await loadDefaultInterface(config, configDefaults);
|
||||
const turnstileConfig = loadTurnstileConfig(config, configDefaults);
|
||||
|
||||
const defaultLocals = {
|
||||
ocr,
|
||||
@@ -85,6 +86,7 @@ const AppService = async (app) => {
|
||||
availableTools,
|
||||
imageOutputType,
|
||||
interfaceConfig,
|
||||
turnstileConfig,
|
||||
balance,
|
||||
};
|
||||
|
||||
|
||||
@@ -46,6 +46,12 @@ jest.mock('./ToolService', () => ({
|
||||
},
|
||||
}),
|
||||
}));
|
||||
jest.mock('./start/turnstile', () => ({
|
||||
loadTurnstileConfig: jest.fn(() => ({
|
||||
siteKey: 'default-site-key',
|
||||
options: {},
|
||||
})),
|
||||
}));
|
||||
|
||||
const azureGroups = [
|
||||
{
|
||||
@@ -86,6 +92,10 @@ const azureGroups = [
|
||||
|
||||
describe('AppService', () => {
|
||||
let app;
|
||||
const mockedTurnstileConfig = {
|
||||
siteKey: 'default-site-key',
|
||||
options: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
app = { locals: {} };
|
||||
@@ -107,6 +117,7 @@ describe('AppService', () => {
|
||||
sidePanel: true,
|
||||
presets: true,
|
||||
}),
|
||||
turnstileConfig: mockedTurnstileConfig,
|
||||
modelSpecs: undefined,
|
||||
availableTools: {
|
||||
ExampleTool: {
|
||||
|
||||
@@ -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' };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 },
|
||||
// });
|
||||
|
||||
@@ -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' },
|
||||
);
|
||||
|
||||
35
api/server/services/start/turnstile.js
Normal file
35
api/server/services/start/turnstile.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const { removeNullishValues } = require('librechat-data-provider');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Loads and maps the Cloudflare Turnstile configuration.
|
||||
*
|
||||
* Expected config structure:
|
||||
*
|
||||
* turnstile:
|
||||
* siteKey: "your-site-key-here"
|
||||
* options:
|
||||
* language: "auto" // "auto" or an ISO 639-1 language code (e.g. en)
|
||||
* size: "normal" // Options: "normal", "compact", "flexible", or "invisible"
|
||||
*
|
||||
* @param {TCustomConfig | undefined} config - The loaded custom configuration.
|
||||
* @param {TConfigDefaults} configDefaults - The custom configuration default values.
|
||||
* @returns {TCustomConfig['turnstile']} The mapped Turnstile configuration.
|
||||
*/
|
||||
function loadTurnstileConfig(config, configDefaults) {
|
||||
const { turnstile: customTurnstile = {} } = config ?? {};
|
||||
const { turnstile: defaults = {} } = configDefaults;
|
||||
|
||||
/** @type {TCustomConfig['turnstile']} */
|
||||
const loadedTurnstile = removeNullishValues({
|
||||
siteKey: customTurnstile.siteKey ?? defaults.siteKey,
|
||||
options: customTurnstile.options ?? defaults.options,
|
||||
});
|
||||
|
||||
logger.info('Turnstile configuration loaded:\n' + JSON.stringify(loadedTurnstile, null, 2));
|
||||
return loadedTurnstile;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadTurnstileConfig,
|
||||
};
|
||||
165
api/strategies/OpenId/openidDataMapper.js
Normal file
165
api/strategies/OpenId/openidDataMapper.js
Normal file
@@ -0,0 +1,165 @@
|
||||
const fetch = require('node-fetch');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
// Microsoft SDK
|
||||
const { Client: MicrosoftGraphClient } = require('@microsoft/microsoft-graph-client');
|
||||
|
||||
/**
|
||||
* Base class for provider-specific data mappers.
|
||||
*/
|
||||
class BaseDataMapper {
|
||||
/**
|
||||
* Map custom OpenID data.
|
||||
* @param {string} accessToken - The access token to authenticate the request.
|
||||
* @param {string|Array<string>} customQuery - Either a full query string (if it contains operators)
|
||||
* or an array of fields to select.
|
||||
* @returns {Promise<Record<string, unknown>>} A promise that resolves to an object of custom fields.
|
||||
* @throws {Error} Throws an error if not implemented in the subclass.
|
||||
*/
|
||||
async mapCustomData(accessToken, customQuery) {
|
||||
throw new Error('mapCustomData() must be implemented by subclasses');
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally handle proxy settings for HTTP requests.
|
||||
* @returns {Object} Configuration object with proxy settings if PROXY is set.
|
||||
*/
|
||||
getProxyOptions() {
|
||||
if (process.env.PROXY) {
|
||||
const agent = new HttpsProxyAgent(process.env.PROXY);
|
||||
return { agent };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Microsoft-specific data mapper using the Microsoft Graph SDK.
|
||||
*/
|
||||
class MicrosoftDataMapper extends BaseDataMapper {
|
||||
/**
|
||||
* Initializes the MicrosoftGraphClient once for reuse.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.accessToken = null;
|
||||
|
||||
this.client = MicrosoftGraphClient.init({
|
||||
defaultVersion: 'beta',
|
||||
authProvider: (done) => {
|
||||
// The authProvider will be called for each request to get the token
|
||||
if (this.accessToken) {
|
||||
done(null, this.accessToken);
|
||||
} else {
|
||||
done(new Error('Access token is not set.'), null);
|
||||
}
|
||||
},
|
||||
fetch: fetch,
|
||||
...this.getProxyOptions(),
|
||||
});
|
||||
|
||||
// Bind methods to maintain context
|
||||
this.mapCustomData = this.mapCustomData.bind(this);
|
||||
this.cleanData = this.cleanData.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the access token for the client.
|
||||
* This method should be called before making any requests.
|
||||
*
|
||||
* @param {string} accessToken - The access token.
|
||||
*/
|
||||
setAccessToken(accessToken) {
|
||||
if (!accessToken || typeof accessToken !== 'string') {
|
||||
throw new Error('[MicrosoftDataMapper] Invalid access token provided.');
|
||||
}
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map custom OpenID data using the Microsoft Graph SDK.
|
||||
*
|
||||
* @param {string} accessToken - The access token to authenticate the request.
|
||||
* @param {string|Array<string>} customQuery - Fields to select from the Microsoft Graph API.
|
||||
* @returns {Promise<Record<string, unknown>>} A promise that resolves to an object of custom fields.
|
||||
*/
|
||||
async mapCustomData(accessToken, customQuery) {
|
||||
try {
|
||||
this.setAccessToken(accessToken);
|
||||
|
||||
if (!customQuery) {
|
||||
logger.warn('[MicrosoftDataMapper] No customQuery provided.');
|
||||
return {};
|
||||
}
|
||||
|
||||
// Convert customQuery to a comma-separated string if it's an array
|
||||
const fields = Array.isArray(customQuery) ? customQuery.join(',') : customQuery;
|
||||
|
||||
if (!fields) {
|
||||
logger.warn('[MicrosoftDataMapper] No fields specified in customQuery.');
|
||||
return {};
|
||||
}
|
||||
|
||||
const result = await this.client.api('/me').select(fields).get();
|
||||
|
||||
// Clean and return the data as a plain object
|
||||
return this.cleanData(result);
|
||||
} catch (error) {
|
||||
// Handle specific Microsoft Graph errors if needed
|
||||
logger.error(`[MicrosoftDataMapper] Error fetching user data: ${error.message}`, {
|
||||
stack: error.stack,
|
||||
});
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove all keys starting with @odata. from an object or array.
|
||||
*
|
||||
* @param {object|Array} obj - The object or array to clean.
|
||||
* @returns {object|Array} - The cleaned object or array.
|
||||
*/
|
||||
cleanData(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(this.cleanData);
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (!key.startsWith('@odata.')) {
|
||||
acc[key] = this.cleanData(value);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map provider names to their specific data mappers.
|
||||
*/
|
||||
const PROVIDER_MAPPERS = {
|
||||
microsoft: MicrosoftDataMapper,
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstraction layer that returns a provider-specific mapper instance.
|
||||
*/
|
||||
class OpenIdDataMapper {
|
||||
/**
|
||||
* Retrieve an instance of the mapper for the specified provider.
|
||||
*
|
||||
* @param {string} provider - The name of the provider (e.g., 'microsoft').
|
||||
* @returns {BaseDataMapper} An instance of the specific data mapper for the provider.
|
||||
* @throws {Error} Throws an error if no mapper is found for the specified provider.
|
||||
*/
|
||||
static getMapper(provider) {
|
||||
const MapperClass = PROVIDER_MAPPERS[provider.toLowerCase()];
|
||||
if (!MapperClass) {
|
||||
throw new Error(`No mapper found for provider: ${provider}`);
|
||||
}
|
||||
return new MapperClass();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OpenIdDataMapper;
|
||||
@@ -8,6 +8,8 @@ const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
||||
const { hashToken } = require('~/server/utils/crypto');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
const { SystemRoles } = require('librechat-data-provider');
|
||||
const OpenIdDataMapper = require('./OpenId/openidDataMapper');
|
||||
|
||||
let crypto;
|
||||
try {
|
||||
@@ -20,37 +22,27 @@ try {
|
||||
* Downloads an image from a URL using an access token.
|
||||
* @param {string} url
|
||||
* @param {string} accessToken
|
||||
* @returns {Promise<Buffer>}
|
||||
* @returns {Promise<Buffer|string>}
|
||||
*/
|
||||
const downloadImage = async (url, accessToken) => {
|
||||
if (!url) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
...(process.env.PROXY && { agent: new HttpsProxyAgent(process.env.PROXY) }),
|
||||
};
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
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] Error downloading image at URL "${url}": ${error}`);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
@@ -65,25 +57,21 @@ const downloadImage = async (url, accessToken) => {
|
||||
* @param {string} [userinfo.email] - The user's email address
|
||||
* @returns {string} The determined full name of the user
|
||||
*/
|
||||
function getFullName(userinfo) {
|
||||
const getFullName = (userinfo) => {
|
||||
if (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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an input into a string suitable for a username.
|
||||
@@ -95,26 +83,93 @@ function getFullName(userinfo) {
|
||||
* @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(input, defaultValue = '') {
|
||||
const convertToUsername = (input, defaultValue = '') => {
|
||||
if (typeof input === 'string') {
|
||||
return input;
|
||||
} else if (Array.isArray(input)) {
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
return input.join('_');
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely decodes a JWT token.
|
||||
* @param {string} token
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
const safeDecode = (token) => {
|
||||
try {
|
||||
const decoded = jwtDecode(token);
|
||||
if (decoded && typeof decoded === 'object') {
|
||||
return decoded;
|
||||
}
|
||||
logger.error('[openidStrategy] Decoded token is not an object.');
|
||||
} catch (error) {
|
||||
logger.error('[openidStrategy] Error decoding token:', error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts roles from a decoded token based on the provided path.
|
||||
* @param {Object} decodedToken
|
||||
* @param {string} parameterPath
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const extractRolesFromToken = (decodedToken, parameterPath) => {
|
||||
if (!decodedToken) {
|
||||
return [];
|
||||
}
|
||||
if (!parameterPath) {
|
||||
return [];
|
||||
}
|
||||
const roles = parameterPath.split('.').reduce((obj, key) => obj?.[key] ?? null, decodedToken);
|
||||
if (!Array.isArray(roles)) {
|
||||
logger.error('[openidStrategy] Roles extracted from token are not in array format.');
|
||||
return [];
|
||||
}
|
||||
return roles;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the user's avatar if a valid picture URL is provided.
|
||||
* @param {Object} user
|
||||
* @param {string | undefined} pictureUrl - The URL of the user's avatar.
|
||||
* @param {string} accessToken
|
||||
* @returns {Promise<Object>} The updated user object.
|
||||
*/
|
||||
const updateUserAvatar = async (user, pictureUrl, accessToken) => {
|
||||
if (!pictureUrl || (user.avatar && user.avatar.includes('manual=true'))) {
|
||||
return user;
|
||||
}
|
||||
|
||||
const fileName = crypto ? (await hashToken(user.openidId)) + '.png' : `${user.openidId}.png`;
|
||||
|
||||
const imageBuffer = await downloadImage(pictureUrl, accessToken);
|
||||
if (imageBuffer) {
|
||||
const { saveBuffer } = getStrategyFunctions(process.env.CDN_PROVIDER);
|
||||
const imagePath = await saveBuffer({
|
||||
fileName,
|
||||
userId: user._id.toString(),
|
||||
buffer: imageBuffer,
|
||||
});
|
||||
user.avatar = imagePath ?? '';
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
async function setupOpenId() {
|
||||
try {
|
||||
// Configure proxy if defined.
|
||||
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] Proxy agent added: ${process.env.PROXY}`);
|
||||
}
|
||||
|
||||
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'
|
||||
@@ -128,125 +183,113 @@ async function setupOpenId() {
|
||||
client_secret: process.env.OPENID_CLIENT_SECRET,
|
||||
redirect_uris: [process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL],
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
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 adminRolesEnv = process.env.OPENID_ADMIN_ROLE;
|
||||
const adminRoles = adminRolesEnv ? adminRolesEnv.split(',').map((role) => role.trim()) : [];
|
||||
|
||||
const openidLogin = new OpenIDStrategy(
|
||||
{
|
||||
client,
|
||||
params: {
|
||||
scope: process.env.OPENID_SCOPE,
|
||||
},
|
||||
params: { scope: process.env.OPENID_SCOPE },
|
||||
},
|
||||
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 openidId: ${userinfo.sub}`);
|
||||
logger.debug('[openidStrategy] Tokenset and userinfo:', { tokenset, userinfo });
|
||||
|
||||
let user = await findUser({ openidId: userinfo.sub });
|
||||
logger.info(
|
||||
`[openidStrategy] user ${user ? 'found' : 'not found'} with openidId: ${userinfo.sub}`,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
user = await findUser({ email: userinfo.email });
|
||||
logger.info(
|
||||
`[openidStrategy] user ${user ? 'found' : 'not found'} with email: ${
|
||||
userinfo.email
|
||||
} for openidId: ${userinfo.sub}`,
|
||||
);
|
||||
}
|
||||
// Find an existing user by openidId or email.
|
||||
let user =
|
||||
(await findUser({ openidId: userinfo.sub })) ||
|
||||
(await findUser({ email: userinfo.email }));
|
||||
|
||||
const fullName = getFullName(userinfo);
|
||||
const username = process.env.OPENID_USERNAME_CLAIM
|
||||
? userinfo[process.env.OPENID_USERNAME_CLAIM]
|
||||
: convertToUsername(userinfo.username || userinfo.given_name || userinfo.email);
|
||||
|
||||
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);
|
||||
// Use the token specified by configuration to extract roles.
|
||||
const token =
|
||||
requiredRoleTokenKind === 'access' ? tokenset.access_token : tokenset.id_token;
|
||||
const decodedToken = safeDecode(token);
|
||||
const tokenBasedRoles = extractRolesFromToken(decodedToken, requiredRoleParameterPath);
|
||||
|
||||
if (!found) {
|
||||
logger.error(
|
||||
`[openidStrategy] Key '${requiredRoleParameterPath}' not found in ${requiredRoleTokenKind} token!`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!roles.includes(requiredRole)) {
|
||||
return done(null, false, {
|
||||
message: `You must have the "${requiredRole}" role to log in.`,
|
||||
});
|
||||
}
|
||||
// Ensure the required role exists.
|
||||
if (requiredRole && !tokenBasedRoles.includes(requiredRole)) {
|
||||
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,
|
||||
// Determine system role.
|
||||
const isAdmin = tokenBasedRoles.some((role) => adminRoles.includes(role));
|
||||
const assignedRole = isAdmin ? SystemRoles.ADMIN : SystemRoles.USER;
|
||||
logger.debug(
|
||||
`[openidStrategy] Assigned system role: ${assignedRole} (isAdmin: ${isAdmin})`,
|
||||
);
|
||||
|
||||
// Map custom OpenID data if configured.
|
||||
let customOpenIdData = {};
|
||||
if (process.env.OPENID_CUSTOM_DATA) {
|
||||
const dataMapper = OpenIdDataMapper.getMapper(
|
||||
process.env.OPENID_PROVIDER.toLowerCase(),
|
||||
);
|
||||
customOpenIdData = await dataMapper.mapCustomData(
|
||||
tokenset.access_token,
|
||||
process.env.OPENID_CUSTOM_DATA,
|
||||
);
|
||||
if (tokenBasedRoles.length) {
|
||||
customOpenIdData.roles = tokenBasedRoles;
|
||||
} else {
|
||||
logger.warn('[openidStrategy] tokenBasedRoles is missing or invalid.');
|
||||
}
|
||||
}
|
||||
|
||||
// Create or update the user.
|
||||
if (!user) {
|
||||
user = await createUser(
|
||||
{
|
||||
provider: 'openid',
|
||||
openidId: userinfo.sub,
|
||||
username,
|
||||
email: userinfo.email || '',
|
||||
emailVerified: userinfo.email_verified || false,
|
||||
name: fullName,
|
||||
role: assignedRole,
|
||||
customOpenIdData,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
user = {
|
||||
...user,
|
||||
provider: 'openid',
|
||||
openidId: userinfo.sub,
|
||||
username,
|
||||
email: userinfo.email || '',
|
||||
emailVerified: userinfo.email_verified || false,
|
||||
name: fullName,
|
||||
role: assignedRole,
|
||||
customOpenIdData,
|
||||
};
|
||||
user = await createUser(user, true, true);
|
||||
} else {
|
||||
user.provider = 'openid';
|
||||
user.openidId = userinfo.sub;
|
||||
user.username = username;
|
||||
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);
|
||||
if (imageBuffer) {
|
||||
const { saveBuffer } = getStrategyFunctions(process.env.CDN_PROVIDER);
|
||||
const imagePath = await saveBuffer({
|
||||
fileName,
|
||||
userId: user._id.toString(),
|
||||
buffer: imageBuffer,
|
||||
});
|
||||
user.avatar = imagePath ?? '';
|
||||
}
|
||||
}
|
||||
// Update the user's avatar if available.
|
||||
user = await updateUserAvatar(user, userinfo.picture, tokenset.access_token);
|
||||
|
||||
// Persist updated user data.
|
||||
user = await updateUser(user._id, user);
|
||||
|
||||
logger.info(
|
||||
`[openidStrategy] login success openidId: ${user.openidId} | email: ${user.email} | username: ${user.username} `,
|
||||
`[openidStrategy] Login success for openidId: ${user.openidId} | email: ${user.email} | username: ${user.username}`,
|
||||
{
|
||||
user: {
|
||||
openidId: user.openidId,
|
||||
@@ -256,10 +299,9 @@ async function setupOpenId() {
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
done(null, user);
|
||||
} catch (err) {
|
||||
logger.error('[openidStrategy] login failed', err);
|
||||
logger.error('[openidStrategy] Login failed', err);
|
||||
done(err);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ const jwtDecode = require('jsonwebtoken/decode');
|
||||
const { Issuer, Strategy: OpenIDStrategy } = require('openid-client');
|
||||
const { findUser, createUser, updateUser } = require('~/models/userMethods');
|
||||
const setupOpenId = require('./openidStrategy');
|
||||
const OpenIdDataMapper = require('./OpenId/openidDataMapper');
|
||||
|
||||
// --- Mocks ---
|
||||
jest.mock('node-fetch');
|
||||
@@ -10,7 +11,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,7 +23,7 @@ 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),
|
||||
}));
|
||||
jest.mock('~/config', () => ({
|
||||
logger: {
|
||||
@@ -43,7 +43,7 @@ Issuer.discover = jest.fn().mockResolvedValue({
|
||||
}),
|
||||
});
|
||||
|
||||
// To capture the verify callback from the strategy, we grab it from the mock constructor
|
||||
// Capture the verify callback from the strategy via the mock constructor
|
||||
let verifyCallback;
|
||||
OpenIDStrategy.mockImplementation((options, verify) => {
|
||||
verifyCallback = verify;
|
||||
@@ -80,7 +80,6 @@ describe('setupOpenId', () => {
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clear previous mock calls and reset implementations
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Reset environment variables needed by the strategy
|
||||
@@ -96,6 +95,8 @@ describe('setupOpenId', () => {
|
||||
delete process.env.OPENID_USERNAME_CLAIM;
|
||||
delete process.env.OPENID_NAME_CLAIM;
|
||||
delete process.env.PROXY;
|
||||
delete process.env.OPENID_CUSTOM_DATA;
|
||||
delete process.env.OPENID_PROVIDER;
|
||||
|
||||
// Default jwtDecode mock returns a token that includes the required role.
|
||||
jwtDecode.mockReturnValue({
|
||||
@@ -125,13 +126,8 @@ describe('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
|
||||
expect(user.username).toBe(userinfo.username);
|
||||
expect(createUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -147,16 +143,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
|
||||
expect(user.username).toBe(expectUsername);
|
||||
expect(createUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ username: expectUsername }),
|
||||
@@ -166,16 +156,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
|
||||
expect(user.username).toBe(expectUsername);
|
||||
expect(createUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ username: expectUsername }),
|
||||
@@ -185,14 +170,9 @@ 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)
|
||||
expect(user.username).toBe(userinfo.sub);
|
||||
expect(createUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ username: userinfo.sub }),
|
||||
@@ -202,31 +182,20 @@ 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
|
||||
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
|
||||
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 +210,8 @@ describe('setupOpenId', () => {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const userinfo = { ...baseUserinfo };
|
||||
|
||||
// Act
|
||||
await validate(tokenset, userinfo);
|
||||
|
||||
// Assert – updateUser should be called and the user object updated
|
||||
expect(updateUser).toHaveBeenCalledWith(
|
||||
existingUser._id,
|
||||
expect.objectContaining({
|
||||
@@ -260,43 +224,41 @@ 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'],
|
||||
});
|
||||
const userinfo = { ...baseUserinfo };
|
||||
|
||||
// Act
|
||||
const { user, details } = await validate(tokenset, userinfo);
|
||||
|
||||
// Assert – verify that the strategy rejects login
|
||||
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
|
||||
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
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
// Depending on your implementation, user.avatar may be undefined or an empty string.
|
||||
expect(user.avatar).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should map customOpenIdData as an object when OPENID_CUSTOM_DATA is set', async () => {
|
||||
process.env.OPENID_CUSTOM_DATA = 'some,fields';
|
||||
process.env.OPENID_PROVIDER = 'microsoft';
|
||||
const fakeCustomData = { foo: 'bar' };
|
||||
const fakeDataMapper = { mapCustomData: jest.fn().mockResolvedValue(fakeCustomData) };
|
||||
OpenIdDataMapper.getMapper = jest.fn(() => fakeDataMapper);
|
||||
|
||||
const userinfo = { ...baseUserinfo };
|
||||
const { user } = await validate(tokenset, userinfo);
|
||||
expect(OpenIdDataMapper.getMapper).toHaveBeenCalledWith('microsoft');
|
||||
expect(fakeDataMapper.mapCustomData).toHaveBeenCalledWith(tokenset.access_token, 'some,fields');
|
||||
expect(user.customOpenIdData).toEqual({ ...fakeCustomData, roles: ['requiredRole'] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.7.8-rc1",
|
||||
"version": "v0.7.8",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -34,6 +34,7 @@
|
||||
"@dicebear/collection": "^9.2.2",
|
||||
"@dicebear/core": "^9.2.2",
|
||||
"@headlessui/react": "^2.1.2",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.3",
|
||||
@@ -141,7 +142,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"
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -35,7 +35,7 @@ export const CodeMarkdown = memo(
|
||||
const [userScrolled, setUserScrolled] = useState(false);
|
||||
const currentContent = content;
|
||||
const rehypePlugins = [
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[rehypeKatex],
|
||||
[
|
||||
rehypeHighlight,
|
||||
{
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Turnstile } from '@marsidev/react-turnstile';
|
||||
import type { TLoginUser, TStartupConfig } from 'librechat-data-provider';
|
||||
import type { TAuthContext } from '~/common';
|
||||
import { useResendVerificationEmail, useGetStartupConfig } from '~/data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { ThemeContext, useLocalize } from '~/hooks';
|
||||
|
||||
type TLoginFormProps = {
|
||||
onSubmit: (data: TLoginUser) => void;
|
||||
@@ -14,6 +15,7 @@ type TLoginFormProps = {
|
||||
|
||||
const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error, setError }) => {
|
||||
const localize = useLocalize();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const {
|
||||
register,
|
||||
getValues,
|
||||
@@ -21,9 +23,11 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||
formState: { errors },
|
||||
} = useForm<TLoginUser>();
|
||||
const [showResendLink, setShowResendLink] = useState<boolean>(false);
|
||||
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
|
||||
|
||||
const { data: config } = useGetStartupConfig();
|
||||
const useUsernameLogin = config?.ldap?.username;
|
||||
const validTheme = theme === 'dark' ? 'dark' : 'light';
|
||||
|
||||
useEffect(() => {
|
||||
if (error && error.includes('422') && !showResendLink) {
|
||||
@@ -159,11 +163,29 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||
{localize('com_auth_password_forgot')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{/* Render Turnstile only if enabled in startupConfig */}
|
||||
{startupConfig.turnstile && (
|
||||
<div className="my-4 flex justify-center">
|
||||
<Turnstile
|
||||
siteKey={startupConfig.turnstile.siteKey}
|
||||
options={{
|
||||
...startupConfig.turnstile.options,
|
||||
theme: validTheme,
|
||||
}}
|
||||
onSuccess={(token) => setTurnstileToken(token)}
|
||||
onError={() => setTurnstileToken(null)}
|
||||
onExpire={() => setTurnstileToken(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<button
|
||||
aria-label={localize('com_auth_continue')}
|
||||
data-testid="login-button"
|
||||
type="submit"
|
||||
disabled={startupConfig.turnstile ? !turnstileToken : false}
|
||||
className="
|
||||
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
||||
transition-colors hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Turnstile } from '@marsidev/react-turnstile';
|
||||
import { useNavigate, useOutletContext, useLocation } from 'react-router-dom';
|
||||
import { useRegisterUserMutation } from 'librechat-data-provider/react-query';
|
||||
import type { TRegisterUser, TError } from 'librechat-data-provider';
|
||||
import type { TLoginLayoutContext } from '~/common';
|
||||
import { ErrorMessage } from './ErrorMessage';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||
import { useLocalize, TranslationKeys, ThemeContext } from '~/hooks';
|
||||
|
||||
const Registration: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { startupConfig, startupConfigError, isFetching } = useOutletContext<TLoginLayoutContext>();
|
||||
|
||||
const {
|
||||
@@ -24,10 +26,12 @@ const Registration: React.FC = () => {
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [countdown, setCountdown] = useState<number>(3);
|
||||
const [turnstileToken, setTurnstileToken] = useState<string | null>(null);
|
||||
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const token = queryParams.get('token');
|
||||
const validTheme = theme === 'dark' ? 'dark' : 'light';
|
||||
|
||||
const registerUser = useRegisterUserMutation({
|
||||
onMutate: () => {
|
||||
@@ -178,17 +182,38 @@ const Registration: React.FC = () => {
|
||||
validate: (value: string) =>
|
||||
value === password || localize('com_auth_password_not_match'),
|
||||
})}
|
||||
|
||||
{/* Render Turnstile only if enabled in startupConfig */}
|
||||
{startupConfig?.turnstile && (
|
||||
<div className="my-4 flex justify-center">
|
||||
<Turnstile
|
||||
siteKey={startupConfig.turnstile.siteKey}
|
||||
options={{
|
||||
...startupConfig.turnstile.options,
|
||||
theme: validTheme,
|
||||
}}
|
||||
onSuccess={(token) => setTurnstileToken(token)}
|
||||
onError={() => setTurnstileToken(null)}
|
||||
onExpire={() => setTurnstileToken(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<button
|
||||
disabled={Object.keys(errors).length > 0}
|
||||
disabled={
|
||||
Object.keys(errors).length > 0 ||
|
||||
isSubmitting ||
|
||||
(startupConfig?.turnstile ? !turnstileToken : false)
|
||||
}
|
||||
type="submit"
|
||||
aria-label="Submit registration"
|
||||
className="
|
||||
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
||||
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
|
||||
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
|
||||
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
|
||||
"
|
||||
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
||||
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
|
||||
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
|
||||
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
|
||||
"
|
||||
>
|
||||
{isSubmitting ? <Spinner /> : localize('com_auth_continue')}
|
||||
</button>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ const AttachFile = ({ disabled }: AttachFileProps) => {
|
||||
isOpen={isPopoverActive}
|
||||
setIsOpen={setIsPopoverActive}
|
||||
modal={true}
|
||||
unmountOnHide={true}
|
||||
trigger={menuTrigger}
|
||||
items={dropdownItems}
|
||||
iconClassName="mr-0"
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -160,6 +160,7 @@ const BookmarkMenu: FC = () => {
|
||||
focusLoop={true}
|
||||
menuId={menuId}
|
||||
isOpen={isMenuOpen}
|
||||
unmountOnHide={true}
|
||||
setIsOpen={setIsMenuOpen}
|
||||
keyPrefix={`${conversationId}-bookmark-`}
|
||||
trigger={
|
||||
|
||||
@@ -113,9 +113,9 @@ const EditMessage = ({
|
||||
messages.map((msg) =>
|
||||
msg.messageId === messageId
|
||||
? {
|
||||
...msg,
|
||||
text: data.text,
|
||||
}
|
||||
...msg,
|
||||
text: data.text,
|
||||
}
|
||||
: msg,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -184,7 +184,7 @@ const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => {
|
||||
|
||||
const rehypePlugins = useMemo(
|
||||
() => [
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[rehypeKatex],
|
||||
[
|
||||
rehypeHighlight,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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') },
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 }],
|
||||
]}
|
||||
|
||||
@@ -59,7 +59,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||
]}
|
||||
rehypePlugins={[
|
||||
/** @ts-ignore */
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[rehypeKatex],
|
||||
/** @ts-ignore */
|
||||
[rehypeHighlight, { ignoreMissing: true }],
|
||||
]}
|
||||
|
||||
@@ -43,7 +43,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||
}, [isEditing, prompt]);
|
||||
|
||||
const rehypePlugins: PluggableList = [
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[rehypeKatex],
|
||||
[
|
||||
rehypeHighlight,
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -50,10 +50,12 @@ export default function AgentFooter({
|
||||
return localize('com_ui_create');
|
||||
};
|
||||
|
||||
const showButtons = activePanel === Panel.builder;
|
||||
|
||||
return (
|
||||
<div className="mx-1 mb-1 flex w-full flex-col gap-2">
|
||||
{activePanel !== Panel.advanced && <AdvancedButton setActivePanel={setActivePanel} />}
|
||||
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
|
||||
{showButtons && <AdvancedButton setActivePanel={setActivePanel} />}
|
||||
{user?.role === SystemRoles.ADMIN && showButtons && <AdminSettings />}
|
||||
{/* Context Button */}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<DeleteButton
|
||||
@@ -63,13 +65,13 @@ export default function AgentFooter({
|
||||
/>
|
||||
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) &&
|
||||
hasAccessToShareAgents && (
|
||||
<ShareAgent
|
||||
agent_id={agent_id}
|
||||
agentName={agent?.name ?? ''}
|
||||
projectIds={agent?.projectIds ?? []}
|
||||
isCollaborative={agent?.isCollaborative}
|
||||
/>
|
||||
)}
|
||||
<ShareAgent
|
||||
agent_id={agent_id}
|
||||
agentName={agent?.name ?? ''}
|
||||
projectIds={agent?.projectIds ?? []}
|
||||
isCollaborative={agent?.isCollaborative}
|
||||
/>
|
||||
)}
|
||||
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
397
client/src/hooks/Chat/__tests__/useFocusChatEffect.spec.tsx
Normal file
397
client/src/hooks/Chat/__tests__/useFocusChatEffect.spec.tsx
Normal 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: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
489
client/src/hooks/Input/useQueryParams.spec.ts
Normal file
489
client/src/hooks/Input/useQueryParams.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
1
client/src/locales/ca/translation.json
Normal file
1
client/src/locales/ca/translation.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
client/src/locales/da/translation.json
Normal file
1
client/src/locales/da/translation.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "אתה",
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "あなた",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "Загрузить для Интерпретатора кода",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "上传代码解释器文件",
|
||||
|
||||
@@ -5,6 +5,8 @@ import App from './App';
|
||||
import './style.css';
|
||||
import './mobile.css';
|
||||
import { ApiErrorBoundaryProvider } from './hooks/ApiErrorBoundaryContext';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import 'katex/dist/contrib/copy-tex.js';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user