Compare commits

...

16 Commits

Author SHA1 Message Date
Danny Avila
9b118d42de v0.7.6 (#5064)
* docs: Update README to include Model Context Protocol support and enhance access descriptions

* fix: Update civic integrity threshold to use 'BLOCK_NONE' as default

* fix: Update GOOGLE_MODELS in .env.example and adjust civic integrity threshold for new model compatibility

*  v0.7.6

* feat: Add 'gemini-2.0-flash-thinking-exp' model to googleModels context windows
2024-12-20 11:43:37 -05:00
Danny Avila
792ae03017 🌍 i18n: Updated Localizations (#5050)
*  feat: Add Arabic localization for API key input and related UI elements

* i18n: updated translations
2024-12-19 14:27:53 -05:00
Danny Avila
3fbbcb1cfe refactor: Integrate Capabilities into Agent File Uploads and Tool Handling (#5048)
* refactor: support drag/drop files for agents, handle undefined tool_resource edge cases

* refactor: consolidate endpoints config logic to dedicated getter

* refactor: Enhance agent tools loading logic to respect capabilities and filter tools accordingly

* refactor: Integrate endpoint capabilities into file upload dropdown for dynamic resource handling

* refactor: Implement capability checks for agent file upload operations

* fix: non-image tool_resource check
2024-12-19 13:04:48 -05:00
Danny Avila
d68c874db4 🤖 feat: Support new o1 model (#5039) 2024-12-18 14:40:58 -05:00
Danny Avila
f873587e5f 🐛 fix: Correct Model Parameters Merging and Panel UI (#5038)
* fix: Model Panel, watching wrong form field

* fix: Refactor agent initialization to merge model parameters correctly
2024-12-18 13:53:59 -05:00
Alex Torregrosa
000641c619 🐛 fix: Gemini system instructions not sent with null RAG_API_URL (#4920)
System instructions were not being sent to gemini models when RAG_API_URL was not set, as the original promptPrefix was not being populated.
2024-12-18 13:26:54 -05:00
Danny Avila
3ceb227507 🛡️ feat: Google Reverse Proxy support, CIVIC_INTEGRITY harm category (#5037)
* 🛡️ feat: Google Reverse Proxy support, `CIVIC_INTEGRITY` harm category

* 🔧 chore: Update @langchain/google-vertexai to version 0.1.4 in package.json and package-lock.json

* fix: revert breaking Vertex AI changes

---------

Co-authored-by: KiGamji <maloyh44@gmail.com>
2024-12-18 12:13:16 -05:00
Danny Avila
22a87b6162 🔧 fix: Update maxContextTokens calculation to use correct model identifier for Azure (#5035) 2024-12-18 11:11:38 -05:00
Danny Avila
e8bde332c2 feat: Implement Conversation Duplication & UI Improvements (#5036)
* feat(ui): enhance conversation components and add duplication

- feat: add conversation duplication functionality
- fix: resolve OGDialogTemplate display issues
- style: improve mobile dropdown component design
- chore: standardize shared link title formatting

* style: update active item background color in select-item

* feat(conversation): add duplicate conversation functionality and UI integration

* feat(conversation): enable title renaming on double-click and improve input focus styles

* fix(conversation): remove "(Copy)" suffix from duplicated conversation title in logging

* fix(RevokeKeysButton): correct className duration property for smoother transitions

* refactor(conversation): ensure proper parent-child relationships and timestamps when message cloning

---------

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
2024-12-18 11:10:34 -05:00
Danny Avila
649c7a6032 🔧 fix: Model Key Retrieval to Account for Bedrock Regions (#5029)
* 🔧 fix: model key retrieval logic to account for Bedrock region

* fix: edit preset dialog styling and potential max depth error with agents endpoint
2024-12-17 23:04:51 -05:00
Danny Avila
d3cafeee96 🔍 feat: Add Entity ID Support for File Search Shared Resources (#5028) 2024-12-17 22:11:18 -05:00
Danny Avila
18ad89be2c 🤖 feat: Add Agent Duplication Functionality with Permission (#5022)
* 🤖 feat: Add Agent Duplication Functionality with Permission

* 🐛 fix: Enhance Agent Duplication Logic and Filter Sensitive Data

* refactor(agents/v1): reorganized variables and error logging

* refactor: remove duplication permission

* chore: update librechat-data-provider version to 0.7.64

* fix: optimize agent duplication

---------

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
2024-12-17 19:47:39 -05:00
Danny Avila
16eed5f32d 🦙 feat: update AWS Bedrock pricing and token metadata for Meta models (#5024) 2024-12-17 17:18:49 -05:00
Danny Avila
e391347b9e 🔧 feat: Initial MCP Support (Tools) (#5015)
* 📝 chore: Add comment to clarify purpose of check_updates.sh script

* feat: mcp package

* feat: add librechat-mcp package and update dependencies

* feat: refactor MCPConnectionSingleton to handle transport initialization and connection management

* feat: change private methods to public in MCPConnectionSingleton for improved accessibility

* feat: filesystem demo

* chore: everything demo and move everything under mcp workspace

* chore: move ts-node to mcp workspace

* feat: mcp examples

* feat: working sse MCP example

* refactor: rename MCPConnectionSingleton to MCPConnection for clarity

* refactor: replace MCPConnectionSingleton with MCPConnection for consistency

* refactor: manager/connections

* refactor: update MCPConnection to use type definitions from mcp types

* refactor: update MCPManager to use winston logger and enhance server initialization

* refactor: share logger between connections and manager

* refactor: add schema definitions and update MCPManager to accept logger parameter

* feat: map available MCP tools

* feat: load manifest tools

* feat: add MCP tools delimiter constant and update plugin key generation

* feat: call MCP tools

* feat: update librechat-data-provider version to 0.7.63 and enhance StdioOptionsSchema with additional properties

* refactor: simplify typing

* chore: update types/packages

* feat: MCP Tool Content parsing

* chore: update dependencies and improve package configurations

* feat: add 'mcp' directory to package and update configurations

* refactor: return CONTENT_AND_ARTIFACT format for MCP callTool

* chore: bump @librechat/agents

* WIP: MCP artifacts

* chore: bump @librechat/agents to v1.8.7

* fix: ensure filename has extension when saving base64 image

* fix: move base64 buffer conversion before filename extension check

* chore: update backend review workflow to install MCP package

* fix: use correct `mime` method

* fix: enhance file metadata with message and tool call IDs in image saving process

* fix: refactor ToolCall component to handle MCP tool calls and improve domain extraction

* fix: update ToolItem component for default isInstalled value and improve localization in ToolSelectDialog

* fix: update ToolItem component to use consistent text color for tool description

* style: add theming to ToolSelectDialog

* fix: improve domain extraction logic in ToolCall component

* refactor: conversation item theming, fix rename UI bug, optimize props, add missing types

* feat: enhance MCP options schema with base options (iconPath to start) and make transport type optional, infer based on other option fields

* fix: improve reconnection logic with parallel init and exponential backoff and enhance transport debug logging

* refactor: improve logging format

* refactor: improve logging of available tools by displaying tool names

* refactor: improve reconnection/connection logic

* feat: add MCP package build process to Dockerfile

* feat: add fallback icon for tools without an image in ToolItem component

* feat: Assistants Support for MCP Tools

* fix(build): configure rollup to use output.dir for dynamic imports

* chore: update @librechat/agents to version 1.8.8 and add @langchain/anthropic dependency

* fix: update CONFIG_VERSION to 1.2.0
2024-12-17 13:12:57 -05:00
Danny Avila
0a97ad3915 🛠️ Fix: Update Agent Cache and Improve Actions UI (#5020)
* style: improve a11y, localization, and styling consistency of actions input form

* refactor: move agent mutations to dedicated module

* fix: update agent cache on agent deletion + delete and update actions
2024-12-17 12:45:58 -05:00
Danny Avila
6ef05dd2e6 🔧 fix: Add modelLabel to OpenAIClient and PluginsClient options (#4995) 2024-12-14 15:31:50 -05:00
133 changed files with 6896 additions and 996 deletions

View File

@@ -138,10 +138,13 @@ BINGAI_TOKEN=user_provided
#============#
GOOGLE_KEY=user_provided
# GOOGLE_REVERSE_PROXY=
# Some reverse proxies do not support the X-goog-api-key header, uncomment to pass the API key in Authorization header instead.
# GOOGLE_AUTH_HEADER=true
# Gemini API (AI Studio)
# GOOGLE_MODELS=gemini-2.0-flash-exp,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.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
# 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
@@ -167,6 +170,7 @@ GOOGLE_KEY=user_provided
# GOOGLE_SAFETY_HATE_SPEECH=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_HARASSMENT=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_DANGEROUS_CONTENT=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_CIVIC_INTEGRITY=BLOCK_ONLY_HIGH
#============#
# OpenAI #

View File

@@ -18,6 +18,10 @@ module.exports = {
'client/dist/**/*',
'client/public/**/*',
'e2e/playwright-report/**/*',
'packages/mcp/types/**/*',
'packages/mcp/dist/**/*',
'packages/mcp/test_bundle/**/*',
'api/demo/**/*',
'packages/data-provider/types/**/*',
'packages/data-provider/dist/**/*',
'packages/data-provider/test_bundle/**/*',
@@ -136,6 +140,30 @@ module.exports = {
},
],
},
{
files: './api/demo/**/*.ts',
overrides: [
{
files: '**/*.ts',
parser: '@typescript-eslint/parser',
parserOptions: {
project: './packages/data-provider/tsconfig.json',
},
},
],
},
{
files: './packages/mcp/**/*.ts',
overrides: [
{
files: '**/*.ts',
parser: '@typescript-eslint/parser',
parserOptions: {
project: './packages/mcp/tsconfig.json',
},
},
],
},
{
files: './config/translations/**/*.ts',
parser: '@typescript-eslint/parser',
@@ -149,6 +177,18 @@ module.exports = {
project: './packages/data-provider/tsconfig.spec.json',
},
},
{
files: ['./api/demo/specs/**/*.ts'],
parserOptions: {
project: './packages/data-provider/tsconfig.spec.json',
},
},
{
files: ['./packages/mcp/specs/**/*.ts'],
parserOptions: {
project: './packages/mcp/tsconfig.spec.json',
},
},
],
settings: {
react: {

View File

@@ -33,8 +33,11 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Install Data Provider
- name: Install Data Provider Package
run: npm run build:data-provider
- name: Install MCP Package
run: npm run build:mcp
- name: Create empty auth.json file
run: |

View File

@@ -1,4 +1,4 @@
# v0.7.5
# v0.7.6
# Base node image
FROM node:20-alpine AS node

View File

@@ -1,5 +1,5 @@
# Dockerfile.multi
# v0.7.5
# v0.7.6
# Base for all builds
FROM node:20-alpine AS base
@@ -10,6 +10,7 @@ RUN npm config set fetch-retry-maxtimeout 600000 && \
npm config set fetch-retry-mintimeout 15000
COPY package*.json ./
COPY packages/data-provider/package*.json ./packages/data-provider/
COPY packages/mcp/package*.json ./packages/mcp/
COPY client/package*.json ./client/
COPY api/package*.json ./api/
RUN npm ci
@@ -21,6 +22,14 @@ COPY packages/data-provider ./
RUN npm run build
RUN npm prune --production
# Build mcp package
FROM base AS mcp-build
WORKDIR /app/packages/mcp
COPY packages/mcp ./
COPY --from=data-provider-build /app/packages/data-provider/dist /app/packages/data-provider/dist
RUN npm run build
RUN npm prune --production
# Client build
FROM base AS client-build
WORKDIR /app/client
@@ -36,9 +45,10 @@ WORKDIR /app
COPY api ./api
COPY config ./config
COPY --from=data-provider-build /app/packages/data-provider/dist ./packages/data-provider/dist
COPY --from=mcp-build /app/packages/mcp/dist ./packages/mcp/dist
COPY --from=client-build /app/client/dist ./client/dist
WORKDIR /app/api
RUN npm prune --production
EXPOSE 3080
ENV HOST=0.0.0.0
CMD ["node", "server/index.js"]
CMD ["node", "server/index.js"]

View File

@@ -59,6 +59,7 @@
- No-Code Custom Assistants: Build specialized, AI-driven helpers without coding
- Flexible & Extensible: Attach tools like DALL-E-3, file search, code execution, and more
- Compatible with Custom Endpoints, OpenAI, Azure, Anthropic, AWS Bedrock, and more
- [Model Context Protocol (MCP) Support](https://modelcontextprotocol.io/clients#librechat) for Tools
- Use LibreChat Agents and OpenAI Assistants with Files, Code Interpreter, Tools, and API Actions
- 🪄 **Generative UI with Code Artifacts**:
@@ -81,9 +82,6 @@
- 🎨 **Customizable Interface**:
- Customizable Dropdown & Interface that adapts to both power users and newcomers
- 📧 **Secure Access**:
- Verify your email to ensure secure access
- 🗣️ **Speech & Audio**:
- Chat hands-free with Speech-to-Text and Text-to-Speech
- Automatically send and play Audio
@@ -96,8 +94,8 @@
- 🔍 **Search & Discovery**:
- Search all messages/conversations
- 👥 **Multi-User & Secure**:
- Multi-User, Secure Authentication with OAuth2 & Email Login Support
- 👥 **Multi-User & Secure Access**:
- Multi-User, Secure Authentication with OAuth2, LDAP, & Email Login Support
- Built-in Moderation, and Token spend tools
- ⚙️ **Configuration & Deployment**:

View File

@@ -30,8 +30,7 @@ const BaseClient = require('./BaseClient');
const loc = process.env.GOOGLE_LOC || 'us-central1';
const publisher = 'google';
const endpointPrefix = `https://${loc}-aiplatform.googleapis.com`;
// const apiEndpoint = loc + '-aiplatform.googleapis.com';
const endpointPrefix = `${loc}-aiplatform.googleapis.com`;
const tokenizersCache = {};
const settings = endpointSettings[EModelEndpoint.google];
@@ -58,6 +57,10 @@ class GoogleClient extends BaseClient {
this.apiKey = creds[AuthKeys.GOOGLE_API_KEY];
this.reverseProxyUrl = options.reverseProxyUrl;
this.authHeader = options.authHeader;
if (options.skipSetOptions) {
return;
}
@@ -66,7 +69,7 @@ class GoogleClient extends BaseClient {
/* Google specific methods */
constructUrl() {
return `${endpointPrefix}/v1/projects/${this.project_id}/locations/${loc}/publishers/${publisher}/models/${this.modelOptions.model}:serverStreamingPredict`;
return `https://${endpointPrefix}/v1/projects/${this.project_id}/locations/${loc}/publishers/${publisher}/models/${this.modelOptions.model}:serverStreamingPredict`;
}
async getClient() {
@@ -595,7 +598,21 @@ class GoogleClient extends BaseClient {
createLLM(clientOptions) {
const model = clientOptions.modelName ?? clientOptions.model;
clientOptions.location = loc;
clientOptions.endpoint = `${loc}-aiplatform.googleapis.com`;
clientOptions.endpoint = endpointPrefix;
let requestOptions = null;
if (this.reverseProxyUrl) {
requestOptions = {
baseUrl: this.reverseProxyUrl,
};
if (this.authHeader) {
requestOptions.customHeaders = {
Authorization: `Bearer ${this.apiKey}`,
};
}
}
if (this.project_id && this.isTextModel) {
logger.debug('Creating Google VertexAI client');
return new GoogleVertexAI(clientOptions);
@@ -607,10 +624,7 @@ class GoogleClient extends BaseClient {
return new ChatVertexAI(clientOptions);
} else if (!EXCLUDED_GENAI_MODELS.test(model)) {
logger.debug('Creating GenAI client');
return new GenAI(this.apiKey).getGenerativeModel({
...clientOptions,
model,
});
return new GenAI(this.apiKey).getGenerativeModel({ ...clientOptions, model }, requestOptions);
}
logger.debug('Creating Chat Google Generative AI client');
@@ -683,7 +697,7 @@ class GoogleClient extends BaseClient {
promptPrefix = `${promptPrefix ?? ''}\n${this.options.artifactsPrompt}`.trim();
}
if (this.options?.promptPrefix?.length) {
if (promptPrefix.length) {
requestOptions.systemInstruction = {
parts: [
{
@@ -901,6 +915,14 @@ class GoogleClient extends BaseClient {
threshold:
process.env.GOOGLE_SAFETY_DANGEROUS_CONTENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
},
{
category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
/**
* Note: this was added since `gemini-2.0-flash-thinking-exp-1219` does not
* accept 'HARM_BLOCK_THRESHOLD_UNSPECIFIED' for 'HARM_CATEGORY_CIVIC_INTEGRITY'
* */
threshold: process.env.GOOGLE_SAFETY_CIVIC_INTEGRITY || 'BLOCK_NONE',
},
];
}

View File

@@ -107,7 +107,8 @@ class OpenAIClient extends BaseClient {
this.checkVisionRequest(this.options.attachments);
}
this.isO1Model = /\bo1\b/i.test(this.modelOptions.model);
const o1Pattern = /\bo1\b/i;
this.isO1Model = o1Pattern.test(this.modelOptions.model);
const { OPENROUTER_API_KEY, OPENAI_FORCE_PROMPT } = process.env ?? {};
if (OPENROUTER_API_KEY && !this.azure) {
@@ -147,7 +148,7 @@ class OpenAIClient extends BaseClient {
const { model } = this.modelOptions;
this.isChatCompletion =
/\bo1\b/i.test(model) || model.includes('gpt') || this.useOpenRouter || !!reverseProxy;
o1Pattern.test(model) || model.includes('gpt') || this.useOpenRouter || !!reverseProxy;
this.isChatGptModel = this.isChatCompletion;
if (
model.includes('text-davinci') ||
@@ -423,6 +424,7 @@ class OpenAIClient extends BaseClient {
promptPrefix: this.options.promptPrefix,
resendFiles: this.options.resendFiles,
imageDetail: this.options.imageDetail,
modelLabel: this.options.modelLabel,
iconURL: this.options.iconURL,
greeting: this.options.greeting,
spec: this.options.spec,
@@ -1324,7 +1326,11 @@ ${convo}
/** @type {(value: void | PromiseLike<void>) => void} */
let streamResolve;
if (this.isO1Model === true && this.azure && modelOptions.stream) {
if (
this.isO1Model === true &&
(this.azure || /o1(?!-(?:mini|preview)).*$/.test(modelOptions.model)) &&
modelOptions.stream
) {
delete modelOptions.stream;
delete modelOptions.stop;
}

View File

@@ -43,6 +43,7 @@ class PluginsClient extends OpenAIClient {
return {
artifacts: this.options.artifacts,
chatGptLabel: this.options.chatGptLabel,
modelLabel: this.options.modelLabel,
promptPrefix: this.options.promptPrefix,
tools: this.options.tools,
...this.modelOptions,

View File

@@ -412,6 +412,7 @@ describe('OpenAIClient', () => {
it('should return the correct save options', () => {
const options = client.getSaveOptions();
expect(options).toHaveProperty('chatGptLabel');
expect(options).toHaveProperty('modelLabel');
expect(options).toHaveProperty('promptPrefix');
});
});

View File

@@ -50,9 +50,10 @@ const primeFiles = async (options) => {
* @param {Object} options
* @param {ServerRequest} options.req
* @param {Array<{ file_id: string; filename: string }>} options.files
* @param {string} [options.entity_id]
* @returns
*/
const createFileSearchTool = async ({ req, files }) => {
const createFileSearchTool = async ({ req, files, entity_id }) => {
return tool(
async ({ query }) => {
if (files.length === 0) {
@@ -62,27 +63,36 @@ const createFileSearchTool = async ({ req, files }) => {
if (!jwtToken) {
return 'There was an error authenticating the file search request.';
}
/**
*
* @param {import('librechat-data-provider').TFile} file
* @returns {{ file_id: string, query: string, k: number, entity_id?: string }}
*/
const createQueryBody = (file) => {
const body = {
file_id: file.file_id,
query,
k: 5,
};
if (!entity_id) {
return body;
}
body.entity_id = entity_id;
logger.debug(`[${Tools.file_search}] RAG API /query body`, body);
return body;
};
const queryPromises = files.map((file) =>
axios
.post(
`${process.env.RAG_API_URL}/query`,
{
file_id: file.file_id,
query,
k: 5,
.post(`${process.env.RAG_API_URL}/query`, createQueryBody(file), {
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
{
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
},
)
})
.catch((error) => {
logger.error(
`Error encountered in \`file_search\` while querying file_id ${file._id}:`,
error,
);
logger.error('Error encountered in `file_search` while querying file:', error);
return null;
}),
);

View File

@@ -1,4 +1,4 @@
const { Tools } = require('librechat-data-provider');
const { Tools, Constants } = require('librechat-data-provider');
const { SerpAPI } = require('@langchain/community/tools/serpapi');
const { Calculator } = require('@langchain/community/tools/calculator');
const { createCodeExecutionTool, EnvVar } = require('@librechat/agents');
@@ -17,9 +17,12 @@ const {
} = require('../');
const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process');
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
const { createMCPTool } = require('~/server/services/MCP');
const { loadSpecs } = require('./loadSpecs');
const { logger } = require('~/config');
const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`);
/**
* Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values.
* Tools without required authentication or with valid authentication are considered valid.
@@ -142,10 +145,25 @@ const loadToolWithAuth = (userId, authFields, ToolConstructor, options = {}) =>
};
};
/**
*
* @param {object} object
* @param {string} object.user
* @param {Agent} [object.agent]
* @param {string} [object.model]
* @param {EModelEndpoint} [object.endpoint]
* @param {LoadToolOptions} [object.options]
* @param {boolean} [object.useSpecs]
* @param {Array<string>} object.tools
* @param {boolean} [object.functions]
* @param {boolean} [object.returnMap]
* @returns {Promise<{ loadedTools: Tool[], toolContextMap: Object<string, any> } | Record<string,Tool>>}
*/
const loadTools = async ({
user,
agent,
model,
isAgent,
endpoint,
useSpecs,
tools = [],
options = {},
@@ -182,8 +200,9 @@ const loadTools = async ({
toolConstructors.dalle = DALLE3;
}
/** @type {ImageGenOptions} */
const imageGenOptions = {
isAgent,
isAgent: !!agent,
req: options.req,
fileStrategy: options.fileStrategy,
processFileURL: options.processFileURL,
@@ -209,6 +228,7 @@ const loadTools = async ({
const toolContextMap = {};
const remainingTools = [];
const appTools = options.req?.app?.locals?.availableTools ?? {};
for (const tool of tools) {
if (tool === Tools.execute_code) {
@@ -237,9 +257,18 @@ const loadTools = async ({
if (toolContext) {
toolContextMap[tool] = toolContext;
}
return createFileSearchTool({ req: options.req, files });
return createFileSearchTool({ req: options.req, files, entity_id: agent?.id });
};
continue;
} else if (tool && appTools[tool] && mcpToolPattern.test(tool)) {
requestedTools[tool] = async () =>
createMCPTool({
req: options.req,
toolKey: tool,
model: agent?.model ?? model,
provider: agent?.provider ?? endpoint,
});
continue;
}
if (customConstructors[tool]) {

View File

@@ -1,5 +1,22 @@
const { EventSource } = require('eventsource');
const logger = require('./winston');
global.EventSource = EventSource;
let mcpManager = null;
/**
* @returns {Promise<MCPManager>}
*/
async function getMCPManager() {
if (!mcpManager) {
const { MCPManager } = await import('librechat-mcp');
mcpManager = MCPManager.getInstance(logger);
}
return mcpManager;
}
module.exports = {
logger,
getMCPManager,
};

View File

@@ -20,7 +20,7 @@ const Agent = mongoose.model('agent', agentSchema);
* @throws {Error} If the agent creation fails.
*/
const createAgent = async (agentData) => {
return await Agent.create(agentData);
return (await Agent.create(agentData)).toObject();
};
/**

View File

@@ -220,4 +220,94 @@ describe('Conversation Structure Tests', () => {
}
expect(currentNode.children.length).toBe(0); // Last message should have no children
});
test('Random order dates between parent and children messages', async () => {
const userId = 'testUser';
const conversationId = 'testConversation';
// Create messages with deliberately out-of-order timestamps but sequential creation
const messages = [
{
messageId: 'parent',
parentMessageId: null,
text: 'Parent Message',
createdAt: new Date('2023-01-01T00:00:00Z'), // Make parent earliest
},
{
messageId: 'child1',
parentMessageId: 'parent',
text: 'Child Message 1',
createdAt: new Date('2023-01-01T00:01:00Z'),
},
{
messageId: 'child2',
parentMessageId: 'parent',
text: 'Child Message 2',
createdAt: new Date('2023-01-01T00:02:00Z'),
},
{
messageId: 'grandchild1',
parentMessageId: 'child1',
text: 'Grandchild Message 1',
createdAt: new Date('2023-01-01T00:03:00Z'),
},
];
// Add common properties to all messages
messages.forEach((msg) => {
msg.conversationId = conversationId;
msg.user = userId;
msg.isCreatedByUser = false;
msg.error = false;
msg.unfinished = false;
});
// Save messages with overrideTimestamp set to true
await bulkSaveMessages(messages, true);
// Retrieve messages
const retrievedMessages = await getMessages({ conversationId, user: userId });
// Debug log to see what's being returned
console.log(
'Retrieved Messages:',
retrievedMessages.map((msg) => ({
messageId: msg.messageId,
parentMessageId: msg.parentMessageId,
createdAt: msg.createdAt,
})),
);
// Build tree
const tree = buildTree({ messages: retrievedMessages });
// Debug log to see the tree structure
console.log(
'Tree structure:',
tree.map((root) => ({
messageId: root.messageId,
children: root.children.map((child) => ({
messageId: child.messageId,
children: child.children.map((grandchild) => ({
messageId: grandchild.messageId,
})),
})),
})),
);
// Verify the structure before making assertions
expect(retrievedMessages.length).toBe(4); // Should have all 4 messages
// Check if messages are properly linked
const parentMsg = retrievedMessages.find((msg) => msg.messageId === 'parent');
expect(parentMsg.parentMessageId).toBeNull(); // Parent should have null parentMessageId
const childMsg1 = retrievedMessages.find((msg) => msg.messageId === 'child1');
expect(childMsg1.parentMessageId).toBe('parent');
// Then check tree structure
expect(tree.length).toBe(1); // Should have only one root message
expect(tree[0].messageId).toBe('parent');
expect(tree[0].children.length).toBe(2); // Should have two children
});
});

View File

@@ -1,22 +1,50 @@
const { matchModelName } = require('../utils');
const defaultRate = 6;
/** AWS Bedrock pricing */
/**
* AWS Bedrock pricing
* source: https://aws.amazon.com/bedrock/pricing/
* */
const bedrockValues = {
// Basic llama2 patterns
'llama2-13b': { prompt: 0.75, completion: 1.0 },
'llama2-70b': { prompt: 1.95, completion: 2.56 },
'llama3-8b': { prompt: 0.3, completion: 0.6 },
'llama3-70b': { prompt: 2.65, completion: 3.5 },
'llama3-1-8b': { prompt: 0.3, completion: 0.6 },
'llama3-1-70b': { prompt: 2.65, completion: 3.5 },
'llama3-1-405b': { prompt: 5.32, completion: 16.0 },
'llama2:13b': { prompt: 0.75, completion: 1.0 },
'llama2:70b': { prompt: 1.95, completion: 2.56 },
'llama2-70b': { prompt: 1.95, completion: 2.56 },
// Basic llama3 patterns
'llama3-8b': { prompt: 0.3, completion: 0.6 },
'llama3:8b': { prompt: 0.3, completion: 0.6 },
'llama3-70b': { prompt: 2.65, completion: 3.5 },
'llama3:70b': { prompt: 2.65, completion: 3.5 },
'llama3.1:8b': { prompt: 0.3, completion: 0.6 },
'llama3.1:70b': { prompt: 2.65, completion: 3.5 },
'llama3.1:405b': { prompt: 5.32, completion: 16.0 },
// llama3-x-Nb pattern
'llama3-1-8b': { prompt: 0.22, completion: 0.22 },
'llama3-1-70b': { prompt: 0.72, completion: 0.72 },
'llama3-1-405b': { prompt: 2.4, completion: 2.4 },
'llama3-2-1b': { prompt: 0.1, completion: 0.1 },
'llama3-2-3b': { prompt: 0.15, completion: 0.15 },
'llama3-2-11b': { prompt: 0.16, completion: 0.16 },
'llama3-2-90b': { prompt: 0.72, completion: 0.72 },
// llama3.x:Nb pattern
'llama3.1:8b': { prompt: 0.22, completion: 0.22 },
'llama3.1:70b': { prompt: 0.72, completion: 0.72 },
'llama3.1:405b': { prompt: 2.4, completion: 2.4 },
'llama3.2:1b': { prompt: 0.1, completion: 0.1 },
'llama3.2:3b': { prompt: 0.15, completion: 0.15 },
'llama3.2:11b': { prompt: 0.16, completion: 0.16 },
'llama3.2:90b': { prompt: 0.72, completion: 0.72 },
// llama-3.x-Nb pattern
'llama-3.1-8b': { prompt: 0.22, completion: 0.22 },
'llama-3.1-70b': { prompt: 0.72, completion: 0.72 },
'llama-3.1-405b': { prompt: 2.4, completion: 2.4 },
'llama-3.2-1b': { prompt: 0.1, completion: 0.1 },
'llama-3.2-3b': { prompt: 0.15, completion: 0.15 },
'llama-3.2-11b': { prompt: 0.16, completion: 0.16 },
'llama-3.2-90b': { prompt: 0.72, completion: 0.72 },
'llama-3.3-70b': { prompt: 2.65, completion: 3.5 },
'mistral-7b': { prompt: 0.15, completion: 0.2 },
'mistral-small': { prompt: 0.15, completion: 0.2 },
'mixtral-8x7b': { prompt: 0.45, completion: 0.7 },

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
"version": "v0.7.5",
"version": "v0.7.6",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",
@@ -42,9 +42,9 @@
"@langchain/community": "^0.3.14",
"@langchain/core": "^0.3.18",
"@langchain/google-genai": "^0.1.4",
"@langchain/google-vertexai": "^0.1.2",
"@langchain/google-vertexai": "^0.1.4",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^1.8.5",
"@librechat/agents": "^1.8.8",
"axios": "^1.7.7",
"bcryptjs": "^2.4.3",
"cheerio": "^1.0.0-rc.12",
@@ -73,6 +73,7 @@
"klona": "^2.0.6",
"langchain": "^0.2.19",
"librechat-data-provider": "*",
"librechat-mcp": "*",
"lodash": "^4.17.21",
"meilisearch": "^0.38.0",
"mime": "^3.0.0",

View File

@@ -1,69 +1,7 @@
const { CacheKeys, EModelEndpoint, orderEndpointsConfig } = require('librechat-data-provider');
const { loadDefaultEndpointsConfig, loadConfigEndpoints } = require('~/server/services/Config');
const { getLogStores } = require('~/cache');
const { getEndpointsConfig } = require('~/server/services/Config');
async function endpointController(req, res) {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cachedEndpointsConfig = await cache.get(CacheKeys.ENDPOINT_CONFIG);
if (cachedEndpointsConfig) {
res.send(cachedEndpointsConfig);
return;
}
const defaultEndpointsConfig = await loadDefaultEndpointsConfig(req);
const customConfigEndpoints = await loadConfigEndpoints(req);
/** @type {TEndpointsConfig} */
const mergedConfig = { ...defaultEndpointsConfig, ...customConfigEndpoints };
if (mergedConfig[EModelEndpoint.assistants] && req.app.locals?.[EModelEndpoint.assistants]) {
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
req.app.locals[EModelEndpoint.assistants];
mergedConfig[EModelEndpoint.assistants] = {
...mergedConfig[EModelEndpoint.assistants],
version,
retrievalModels,
disableBuilder,
capabilities,
};
}
if (mergedConfig[EModelEndpoint.agents] && req.app.locals?.[EModelEndpoint.agents]) {
const { disableBuilder, capabilities, ..._rest } = req.app.locals[EModelEndpoint.agents];
mergedConfig[EModelEndpoint.agents] = {
...mergedConfig[EModelEndpoint.agents],
disableBuilder,
capabilities,
};
}
if (
mergedConfig[EModelEndpoint.azureAssistants] &&
req.app.locals?.[EModelEndpoint.azureAssistants]
) {
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
req.app.locals[EModelEndpoint.azureAssistants];
mergedConfig[EModelEndpoint.azureAssistants] = {
...mergedConfig[EModelEndpoint.azureAssistants],
version,
retrievalModels,
disableBuilder,
capabilities,
};
}
if (mergedConfig[EModelEndpoint.bedrock] && req.app.locals?.[EModelEndpoint.bedrock]) {
const { availableRegions } = req.app.locals[EModelEndpoint.bedrock];
mergedConfig[EModelEndpoint.bedrock] = {
...mergedConfig[EModelEndpoint.bedrock],
availableRegions,
};
}
const endpointsConfig = orderEndpointsConfig(mergedConfig);
await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig);
const endpointsConfig = await getEndpointsConfig(req);
res.send(JSON.stringify(endpointsConfig));
}

View File

@@ -1,6 +1,8 @@
const { promises: fs } = require('fs');
const { CacheKeys, AuthType } = require('librechat-data-provider');
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
const { getCustomConfig } = require('~/server/services/Config');
const { getMCPManager } = require('~/config');
const { getLogStores } = require('~/cache');
/**
@@ -107,6 +109,12 @@ const getAvailableTools = async (req, res) => {
const pluginManifest = await fs.readFile(req.app.locals.paths.pluginManifest, 'utf8');
const jsonData = JSON.parse(pluginManifest);
const customConfig = await getCustomConfig();
if (customConfig?.mcpServers != null) {
const mcpManager = await getMCPManager();
await mcpManager.loadManifestTools(jsonData);
}
/** @type {TPlugin[]} */
const uniquePlugins = filterUniquePlugins(jsonData);

View File

@@ -1,4 +1,4 @@
const { Tools, StepTypes, imageGenTools } = require('librechat-data-provider');
const { Tools, StepTypes, imageGenTools, FileContext } = require('librechat-data-provider');
const {
EnvVar,
GraphEvents,
@@ -6,6 +6,7 @@ const {
ChatModelStreamHandler,
} = require('@librechat/agents');
const { processCodeOutput } = require('~/server/services/Files/Code/process');
const { saveBase64Image } = require('~/server/services/Files/process');
const { loadAuthValues } = require('~/app/clients/tools/util');
const { logger } = require('~/config');
@@ -191,7 +192,11 @@ function createToolEndCallback({ req, res, artifactPromises }) {
return;
}
if (imageGenTools.has(output.name) && output.artifact) {
if (!output.artifact) {
return;
}
if (imageGenTools.has(output.name)) {
artifactPromises.push(
(async () => {
const fileMetadata = Object.assign(output.artifact, {
@@ -217,10 +222,53 @@ function createToolEndCallback({ req, res, artifactPromises }) {
return;
}
if (output.name !== Tools.execute_code) {
if (output.artifact.content) {
/** @type {FormattedContent[]} */
const content = output.artifact.content;
for (const part of content) {
if (part.type !== 'image_url') {
continue;
}
const { url } = part.image_url;
artifactPromises.push(
(async () => {
const filename = `${output.tool_call_id}-image-${new Date().getTime()}`;
const file = await saveBase64Image(url, {
req,
filename,
endpoint: metadata.provider,
context: FileContext.image_generation,
});
const fileMetadata = Object.assign(file, {
messageId: metadata.run_id,
toolCallId: output.tool_call_id,
conversationId: metadata.thread_id,
});
if (!res.headersSent) {
return fileMetadata;
}
if (!fileMetadata) {
return null;
}
res.write(`event: attachment\ndata: ${JSON.stringify(fileMetadata)}\n\n`);
return fileMetadata;
})().catch((error) => {
logger.error('Error processing artifact content:', error);
return null;
}),
);
}
return;
}
{
if (output.name !== Tools.execute_code) {
return;
}
}
if (!output.artifact.files) {
return;
}

View File

@@ -1,6 +1,12 @@
const fs = require('fs').promises;
const { nanoid } = require('nanoid');
const { FileContext, Constants, Tools, SystemRoles } = require('librechat-data-provider');
const {
FileContext,
Constants,
Tools,
SystemRoles,
actionDelimiter,
} = require('librechat-data-provider');
const {
getAgent,
createAgent,
@@ -10,6 +16,7 @@ const {
} = require('~/models/Agent');
const { uploadImageBuffer, filterFile } = require('~/server/services/Files/process');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { updateAction, getActions } = require('~/models/Action');
const { getProjectByName } = require('~/models/Project');
const { updateAgentProjects } = require('~/models/Agent');
const { deleteFileByFilter } = require('~/models/File');
@@ -173,6 +180,99 @@ const updateAgentHandler = async (req, res) => {
}
};
/**
* Duplicates an Agent based on the provided ID.
* @route POST /Agents/:id/duplicate
* @param {object} req - Express Request
* @param {object} req.params - Request params
* @param {string} req.params.id - Agent identifier.
* @returns {Agent} 201 - success response - application/json
*/
const duplicateAgentHandler = async (req, res) => {
const { id } = req.params;
const { id: userId } = req.user;
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
try {
const agent = await getAgent({ id });
if (!agent) {
return res.status(404).json({
error: 'Agent not found',
status: 'error',
});
}
const {
_id: __id,
id: _id,
author: _author,
createdAt: _createdAt,
updatedAt: _updatedAt,
...cloneData
} = agent;
const newAgentId = `agent_${nanoid()}`;
const newAgentData = Object.assign(cloneData, {
id: newAgentId,
author: userId,
});
const newActionsList = [];
const originalActions = (await getActions({ agent_id: id }, true)) ?? [];
const promises = [];
/**
* Duplicates an action and returns the new action ID.
* @param {Action} action
* @returns {Promise<string>}
*/
const duplicateAction = async (action) => {
const newActionId = nanoid();
const [domain] = action.action_id.split(actionDelimiter);
const fullActionId = `${domain}${actionDelimiter}${newActionId}`;
const newAction = await updateAction(
{ action_id: newActionId },
{
metadata: action.metadata,
agent_id: newAgentId,
user: userId,
},
);
const filteredMetadata = { ...newAction.metadata };
for (const field of sensitiveFields) {
delete filteredMetadata[field];
}
newAction.metadata = filteredMetadata;
newActionsList.push(newAction);
return fullActionId;
};
for (const action of originalActions) {
promises.push(
duplicateAction(action).catch((error) => {
logger.error('[/agents/:id/duplicate] Error duplicating Action:', error);
}),
);
}
const agentActions = await Promise.all(promises);
newAgentData.actions = agentActions;
const newAgent = await createAgent(newAgentData);
return res.status(201).json({
agent: newAgent,
actions: newActionsList,
});
} catch (error) {
logger.error('[/Agents/:id/duplicate] Error duplicating Agent:', error);
res.status(500).json({ error: error.message });
}
};
/**
* Deletes an Agent based on the provided ID.
* @route DELETE /Agents/:id
@@ -292,6 +392,7 @@ module.exports = {
createAgent: createAgentHandler,
getAgent: getAgentHandler,
updateAgent: updateAgentHandler,
duplicateAgent: duplicateAgentHandler,
deleteAgent: deleteAgentHandler,
getListAgents: getListAgentsHandler,
uploadAgentAvatar: uploadAgentAvatarHandler,

View File

@@ -1,5 +1,4 @@
const {
CacheKeys,
SystemRoles,
EModelEndpoint,
defaultOrderQuery,
@@ -9,7 +8,7 @@ const {
initializeClient: initAzureClient,
} = require('~/server/services/Endpoints/azureAssistants');
const { initializeClient } = require('~/server/services/Endpoints/assistants');
const { getLogStores } = require('~/cache');
const { getEndpointsConfig } = require('~/server/services/Config');
/**
* @param {Express.Request} req
@@ -23,11 +22,8 @@ const getCurrentVersion = async (req, endpoint) => {
version = `v${req.body.version}`;
}
if (!version && endpoint) {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cachedEndpointsConfig = await cache.get(CacheKeys.ENDPOINT_CONFIG);
version = `v${
cachedEndpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint]
}`;
const endpointsConfig = await getEndpointsConfig(req);
version = `v${endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint]}`;
}
if (!version?.startsWith('v') && version.length !== 2) {
throw new Error(`[${req.baseUrl}] Invalid version: ${version}`);

View File

@@ -62,6 +62,14 @@ router.get('/:id', checkAgentAccess, v1.getAgent);
*/
router.patch('/:id', checkGlobalAgentShare, v1.updateAgent);
/**
* Duplicates an agent.
* @route POST /agents/:id/duplicate
* @param {string} req.params.id - Agent identifier.
* @returns {Agent} 201 - Success response - application/json
*/
router.post('/:id/duplicate', checkAgentCreate, v1.duplicateAgent);
/**
* Deletes an agent.
* @route DELETE /agents/:id

View File

@@ -2,9 +2,9 @@ const multer = require('multer');
const express = require('express');
const { CacheKeys, EModelEndpoint } = require('librechat-data-provider');
const { getConvosByPage, deleteConvos, getConvo, saveConvo } = require('~/models/Conversation');
const { forkConversation, duplicateConversation } = require('~/server/utils/import/fork');
const { storage, importFileFilter } = require('~/server/routes/files/multer');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const { forkConversation } = require('~/server/utils/import/fork');
const { importConversations } = require('~/server/utils/import');
const { createImportLimiters } = require('~/server/middleware');
const { deleteToolCalls } = require('~/models/ToolCall');
@@ -182,9 +182,25 @@ router.post('/fork', async (req, res) => {
res.json(result);
} catch (error) {
logger.error('Error forking conversation', error);
logger.error('Error forking conversation:', error);
res.status(500).send('Error forking conversation');
}
});
router.post('/duplicate', async (req, res) => {
const { conversationId, title } = req.body;
try {
const result = await duplicateConversation({
userId: req.user.id,
conversationId,
title,
});
res.status(201).json(result);
} catch (error) {
logger.error('Error duplicating conversation:', error);
res.status(500).send('Error duplicating conversation');
}
});
module.exports = router;

View File

@@ -9,6 +9,7 @@ const { azureConfigSetup } = require('./start/azureOpenAI');
const { loadAndFormatTools } = require('./ToolService');
const { agentsConfigSetup } = require('./start/agents');
const { initializeRoles } = require('~/models/Role');
const { getMCPManager } = require('~/config');
const paths = require('~/config/paths');
/**
@@ -39,11 +40,17 @@ const AppService = async (app) => {
/** @type {Record<string, FunctionTool} */
const availableTools = loadAndFormatTools({
directory: paths.structuredTools,
adminFilter: filteredTools,
adminIncluded: includedTools,
directory: paths.structuredTools,
});
if (config.mcpServers != null) {
const mcpManager = await getMCPManager();
await mcpManager.initializeMCP(config.mcpServers);
await mcpManager.mapAvailableTools(availableTools);
}
const socialLogins =
config?.registration?.socialLogins ?? configDefaults?.registration?.socialLogins;
const interfaceConfig = await loadDefaultInterface(config, configDefaults);

View File

@@ -0,0 +1,75 @@
const { CacheKeys, EModelEndpoint, orderEndpointsConfig } = require('librechat-data-provider');
const loadDefaultEndpointsConfig = require('./loadDefaultEConfig');
const loadConfigEndpoints = require('./loadConfigEndpoints');
const getLogStores = require('~/cache/getLogStores');
/**
*
* @param {ServerRequest} req
* @returns {Promise<TEndpointsConfig>}
*/
async function getEndpointsConfig(req) {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cachedEndpointsConfig = await cache.get(CacheKeys.ENDPOINT_CONFIG);
if (cachedEndpointsConfig) {
return cachedEndpointsConfig;
}
const defaultEndpointsConfig = await loadDefaultEndpointsConfig(req);
const customConfigEndpoints = await loadConfigEndpoints(req);
/** @type {TEndpointsConfig} */
const mergedConfig = { ...defaultEndpointsConfig, ...customConfigEndpoints };
if (mergedConfig[EModelEndpoint.assistants] && req.app.locals?.[EModelEndpoint.assistants]) {
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
req.app.locals[EModelEndpoint.assistants];
mergedConfig[EModelEndpoint.assistants] = {
...mergedConfig[EModelEndpoint.assistants],
version,
retrievalModels,
disableBuilder,
capabilities,
};
}
if (mergedConfig[EModelEndpoint.agents] && req.app.locals?.[EModelEndpoint.agents]) {
const { disableBuilder, capabilities, ..._rest } = req.app.locals[EModelEndpoint.agents];
mergedConfig[EModelEndpoint.agents] = {
...mergedConfig[EModelEndpoint.agents],
disableBuilder,
capabilities,
};
}
if (
mergedConfig[EModelEndpoint.azureAssistants] &&
req.app.locals?.[EModelEndpoint.azureAssistants]
) {
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
req.app.locals[EModelEndpoint.azureAssistants];
mergedConfig[EModelEndpoint.azureAssistants] = {
...mergedConfig[EModelEndpoint.azureAssistants],
version,
retrievalModels,
disableBuilder,
capabilities,
};
}
if (mergedConfig[EModelEndpoint.bedrock] && req.app.locals?.[EModelEndpoint.bedrock]) {
const { availableRegions } = req.app.locals[EModelEndpoint.bedrock];
mergedConfig[EModelEndpoint.bedrock] = {
...mergedConfig[EModelEndpoint.bedrock],
availableRegions,
};
}
const endpointsConfig = orderEndpointsConfig(mergedConfig);
await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig);
return endpointsConfig;
}
module.exports = { getEndpointsConfig };

View File

@@ -3,10 +3,9 @@ const getCustomConfig = require('./getCustomConfig');
const loadCustomConfig = require('./loadCustomConfig');
const loadConfigModels = require('./loadConfigModels');
const loadDefaultModels = require('./loadDefaultModels');
const getEndpointsConfig = require('./getEndpointsConfig');
const loadOverrideConfig = require('./loadOverrideConfig');
const loadAsyncEndpoints = require('./loadAsyncEndpoints');
const loadConfigEndpoints = require('./loadConfigEndpoints');
const loadDefaultEndpointsConfig = require('./loadDefaultEConfig');
module.exports = {
config,
@@ -16,6 +15,5 @@ module.exports = {
loadOverrideConfig,
loadAsyncEndpoints,
...getCustomConfig,
loadConfigEndpoints,
loadDefaultEndpointsConfig,
...getEndpointsConfig,
};

View File

@@ -80,8 +80,7 @@ const initializeAgentOptions = async ({
}) => {
const { tools, toolContextMap } = await loadAgentTools({
req,
tools: agent.tools,
agent_id: agent.id,
agent,
tool_resources,
});
@@ -98,12 +97,15 @@ const initializeAgentOptions = async ({
agent.endpoint = provider.toLowerCase();
}
const model_parameters = agent.model_parameters ?? { model: agent.model };
const _endpointOption = isInitialAgent
? endpointOption
: {
model_parameters,
};
const model_parameters = Object.assign(
{},
agent.model_parameters ?? { model: agent.model },
isInitialAgent === true ? endpointOption?.model_parameters : {},
);
const _endpointOption =
isInitialAgent === true
? Object.assign({}, endpointOption, { model_parameters })
: { model_parameters };
const options = await getOptions({
req,
@@ -123,13 +125,16 @@ const initializeAgentOptions = async ({
agent.model_parameters.model = agent.model;
}
const tokensModel =
agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model;
return {
...agent,
tools,
toolContextMap,
maxContextTokens:
agent.max_context_tokens ??
getModelMaxTokens(agent.model_parameters.model, providerEndpointMap[provider]) ??
getModelMaxTokens(tokensModel, providerEndpointMap[provider]) ??
4000,
};
};

View File

@@ -1,9 +1,15 @@
const { EModelEndpoint, AuthKeys } = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { GoogleClient } = require('~/app');
const { isEnabled } = require('~/server/utils');
const initializeClient = async ({ req, res, endpointOption }) => {
const { GOOGLE_KEY, GOOGLE_REVERSE_PROXY, PROXY } = process.env;
const {
GOOGLE_KEY,
GOOGLE_REVERSE_PROXY,
GOOGLE_AUTH_HEADER,
PROXY,
} = process.env;
const isUserProvided = GOOGLE_KEY === 'user_provided';
const { key: expiresAt } = req.body;
@@ -46,6 +52,7 @@ const initializeClient = async ({ req, res, endpointOption }) => {
req,
res,
reverseProxyUrl: GOOGLE_REVERSE_PROXY ?? null,
authHeader: isEnabled(GOOGLE_AUTH_HEADER) ?? null,
proxy: PROXY ?? null,
...clientOptions,
...endpointOption,

View File

@@ -50,13 +50,14 @@ const deleteVectors = async (req, file) => {
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
* have a `path` property that points to the location of the uploaded file.
* @param {string} params.file_id - The file ID.
* @param {string} [params.entity_id] - The entity ID for shared resources.
*
* @returns {Promise<{ filepath: string, bytes: number }>}
* A promise that resolves to an object containing:
* - filepath: The path where the file is saved.
* - bytes: The size of the file in bytes.
*/
async function uploadVectors({ req, file, file_id }) {
async function uploadVectors({ req, file, file_id, entity_id }) {
if (!process.env.RAG_API_URL) {
throw new Error('RAG_API_URL not defined');
}
@@ -66,8 +67,11 @@ async function uploadVectors({ req, file, file_id }) {
const formData = new FormData();
formData.append('file_id', file_id);
formData.append('file', fs.createReadStream(file.path));
if (entity_id != null && entity_id) {
formData.append('entity_id', entity_id);
}
const formHeaders = formData.getHeaders(); // Automatically sets the correct Content-Type
const formHeaders = formData.getHeaders();
const response = await axios.post(`${process.env.RAG_API_URL}/embed`, formData, {
headers: {

View File

@@ -58,7 +58,12 @@ async function resizeImageBuffer(inputBuffer, resolution, endpoint) {
const resizedBuffer = await sharp(inputBuffer).rotate().resize(resizeOptions).toBuffer();
const resizedMetadata = await sharp(resizedBuffer).metadata();
return { buffer: resizedBuffer, width: resizedMetadata.width, height: resizedMetadata.height };
return {
buffer: resizedBuffer,
bytes: resizedMetadata.size,
width: resizedMetadata.width,
height: resizedMetadata.height,
};
}
/**

View File

@@ -12,17 +12,23 @@ const {
EToolResources,
mergeFileConfig,
hostImageIdSuffix,
AgentCapabilities,
checkOpenAIStorage,
removeNullishValues,
hostImageNamePrefix,
isAssistantsEndpoint,
} = require('librechat-data-provider');
const { EnvVar } = require('@librechat/agents');
const {
convertImage,
resizeAndConvert,
resizeImageBuffer,
} = require('~/server/services/Files/images');
const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2');
const { convertImage, resizeAndConvert } = require('~/server/services/Files/images');
const { addAgentResourceFile, removeAgentResourceFiles } = require('~/models/Agent');
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
const { getEndpointsConfig } = require('~/server/services/Config');
const { loadAuthValues } = require('~/app/clients/tools/util');
const { LB_QueueAsyncCall } = require('~/server/utils/queue');
const { getStrategyFunctions } = require('./strategies');
@@ -447,6 +453,17 @@ const processFileUpload = async ({ req, res, metadata }) => {
res.status(200).json({ message: 'File uploaded and processed successfully', ...result });
};
/**
* @param {ServerRequest} req
* @param {AgentCapabilities} capability
* @returns {Promise<boolean>}
*/
const checkCapability = async (req, capability) => {
const endpointsConfig = await getEndpointsConfig(req);
const capabilities = endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [];
return capabilities.includes(capability);
};
/**
* Applies the current strategy for file uploads.
* Saves file metadata to the database with an expiry TTL.
@@ -474,8 +491,20 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
throw new Error('No agent ID provided for agent file upload');
}
const isImage = file.mimetype.startsWith('image');
if (!isImage && !tool_resource) {
/** Note: this needs to be removed when we can support files to providers */
throw new Error('No tool resource provided for non-image agent file upload');
}
let fileInfoMetadata;
const entity_id = messageAttachment === true ? undefined : agent_id;
if (tool_resource === EToolResources.execute_code) {
const isCodeEnabled = await checkCapability(req, AgentCapabilities.execute_code);
if (!isCodeEnabled) {
throw new Error('Code execution is not enabled for Agents');
}
const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions(FileSources.execute_code);
const result = await loadAuthValues({ userId: req.user.id, authFields: [EnvVar.CODE_API_KEY] });
const stream = fs.createReadStream(file.path);
@@ -484,9 +513,14 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
stream,
filename: file.originalname,
apiKey: result[EnvVar.CODE_API_KEY],
entity_id: messageAttachment === true ? undefined : agent_id,
entity_id,
});
fileInfoMetadata = { fileIdentifier };
} else if (tool_resource === EToolResources.file_search) {
const isFileSearchEnabled = await checkCapability(req, AgentCapabilities.file_search);
if (!isFileSearchEnabled) {
throw new Error('File search is not enabled for Agents');
}
}
const source =
@@ -508,6 +542,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
req,
file,
file_id,
entity_id,
});
let filepath = _filepath;
@@ -521,7 +556,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
});
}
if (file.mimetype.startsWith('image')) {
if (isImage) {
const result = await processImageFile({
req,
file,
@@ -736,6 +771,73 @@ async function retrieveAndProcessFile({
}
}
/**
* Converts a base64 string to a buffer.
* @param {string} base64String
* @returns {Buffer<ArrayBufferLike>}
*/
function base64ToBuffer(base64String) {
try {
const typeMatch = base64String.match(/^data:([A-Za-z-+/]+);base64,/);
const type = typeMatch ? typeMatch[1] : '';
const base64Data = base64String.replace(/^data:([A-Za-z-+/]+);base64,/, '');
if (!base64Data) {
throw new Error('Invalid base64 string');
}
return {
buffer: Buffer.from(base64Data, 'base64'),
type,
};
} catch (error) {
throw new Error(`Failed to convert base64 to buffer: ${error.message}`);
}
}
async function saveBase64Image(
url,
{ req, file_id: _file_id, filename: _filename, endpoint, context, resolution = 'high' },
) {
const file_id = _file_id ?? v4();
let filename = _filename;
const { buffer: inputBuffer, type } = base64ToBuffer(url);
if (!path.extname(_filename)) {
const extension = mime.getExtension(type);
if (extension) {
filename += `.${extension}`;
} else {
throw new Error(`Could not determine file extension from MIME type: ${type}`);
}
}
const image = await resizeImageBuffer(inputBuffer, resolution, endpoint);
const source = req.app.locals.fileStrategy;
const { saveBuffer } = getStrategyFunctions(source);
const filepath = await saveBuffer({
userId: req.user.id,
fileName: filename,
buffer: image.buffer,
});
return await createFile(
{
type,
source,
context,
file_id,
filepath,
filename,
user: req.user.id,
bytes: image.bytes,
width: image.width,
height: image.height,
},
true,
);
}
/**
* Filters a file based on its size and the endpoint origin.
*
@@ -810,6 +912,7 @@ module.exports = {
filterFile,
processFiles,
processFileURL,
saveBase64Image,
processImageFile,
uploadImageBuffer,
processFileUpload,

View File

@@ -0,0 +1,57 @@
const { tool } = require('@langchain/core/tools');
const { Constants: AgentConstants } = require('@librechat/agents');
const {
Constants,
convertJsonSchemaToZod,
isAssistantsEndpoint,
} = require('librechat-data-provider');
const { logger, getMCPManager } = require('~/config');
/**
* Creates a general tool for an entire action set.
*
* @param {Object} params - The parameters for loading action sets.
* @param {ServerRequest} params.req - The name of the tool.
* @param {string} params.toolKey - The toolKey for the tool.
* @param {import('@librechat/agents').Providers | EModelEndpoint} params.provider - The provider for the tool.
* @param {string} params.model - The model for the tool.
* @returns { Promise<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
*/
async function createMCPTool({ req, toolKey, provider }) {
const toolDefinition = req.app.locals.availableTools[toolKey]?.function;
if (!toolDefinition) {
logger.error(`Tool ${toolKey} not found in available tools`);
return null;
}
/** @type {LCTool} */
const { description, parameters } = toolDefinition;
const schema = convertJsonSchemaToZod(parameters);
const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter);
/** @type {(toolInput: Object | string) => Promise<unknown>} */
const _call = async (toolInput) => {
try {
const mcpManager = await getMCPManager();
const result = await mcpManager.callTool(serverName, toolName, provider, toolInput);
if (isAssistantsEndpoint(provider) && Array.isArray(result)) {
return result[0];
}
return result;
} catch (error) {
logger.error(`${toolName} MCP server tool call failed`, error);
return `${toolName} MCP server tool call failed.`;
}
};
const toolInstance = tool(_call, {
schema,
name: toolKey,
description: description || '',
responseFormat: AgentConstants.CONTENT_AND_ARTIFACT,
});
toolInstance.mcp = true;
return toolInstance;
}
module.exports = {
createMCPTool,
};

View File

@@ -8,13 +8,16 @@ const {
ErrorTypes,
ContentTypes,
imageGenTools,
EModelEndpoint,
actionDelimiter,
ImageVisionTool,
openapiToFunction,
AgentCapabilities,
validateAndParseOpenAPISpec,
} = require('librechat-data-provider');
const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process');
const { loadActionSets, createActionTool, domainParser } = require('./ActionService');
const { getEndpointsConfig } = require('~/server/services/Config');
const { recordUsage } = require('~/server/services/Threads');
const { loadTools } = require('~/app/clients/tools/util');
const { redactMessage } = require('~/config/parsers');
@@ -176,6 +179,7 @@ async function processRequiredActions(client, requiredActions) {
model: client.req.body.model ?? 'gpt-4o-mini',
tools,
functions: true,
endpoint: client.req.body.endpoint,
options: {
processFileURL,
req: client.req,
@@ -374,22 +378,45 @@ async function processRequiredActions(client, requiredActions) {
* Processes the runtime tool calls and returns the tool classes.
* @param {Object} params - Run params containing user and request information.
* @param {ServerRequest} params.req - The request object.
* @param {string} params.agent_id - The agent ID.
* @param {Agent['tools']} params.tools - The agent's available tools.
* @param {Agent['tool_resources']} params.tool_resources - The agent's available tool resources.
* @param {Agent} params.agent - The agent to load tools for.
* @param {string | undefined} [params.openAIApiKey] - The OpenAI API key.
* @returns {Promise<{ tools?: StructuredTool[] }>} The agent tools.
*/
async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiKey }) {
if (!tools || tools.length === 0) {
async function loadAgentTools({ req, agent, tool_resources, openAIApiKey }) {
if (!agent.tools || agent.tools.length === 0) {
return {};
}
const endpointsConfig = await getEndpointsConfig(req);
const capabilities = endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [];
const areToolsEnabled = capabilities.includes(AgentCapabilities.tools);
if (!areToolsEnabled) {
logger.debug('Tools are not enabled for this agent.');
return {};
}
const isFileSearchEnabled = capabilities.includes(AgentCapabilities.file_search);
const isCodeEnabled = capabilities.includes(AgentCapabilities.execute_code);
const areActionsEnabled = capabilities.includes(AgentCapabilities.actions);
const _agentTools = agent.tools?.filter((tool) => {
if (tool === Tools.file_search && !isFileSearchEnabled) {
return false;
} else if (tool === Tools.execute_code && !isCodeEnabled) {
return false;
}
return true;
});
if (!_agentTools || _agentTools.length === 0) {
return {};
}
const { loadedTools, toolContextMap } = await loadTools({
user: req.user.id,
// model: req.body.model ?? 'gpt-4o-mini',
tools,
agent,
functions: true,
isAgent: agent_id != null,
user: req.user.id,
tools: _agentTools,
options: {
req,
openAIApiKey,
@@ -409,6 +436,11 @@ async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiK
continue;
}
if (tool.mcp === true) {
agentTools.push(tool);
continue;
}
const toolDefinition = {
name: tool.name,
schema: tool.schema,
@@ -431,62 +463,74 @@ async function loadAgentTools({ req, agent_id, tools, tool_resources, openAIApiK
return map;
}, {});
if (!areActionsEnabled) {
return {
tools: agentTools,
toolContextMap,
};
}
let actionSets = [];
const ActionToolMap = {};
for (const toolName of tools) {
if (!ToolMap[toolName]) {
if (!actionSets.length) {
actionSets = (await loadActionSets({ agent_id })) ?? [];
}
for (const toolName of _agentTools) {
if (ToolMap[toolName]) {
continue;
}
let actionSet = null;
let currentDomain = '';
for (let action of actionSets) {
const domain = await domainParser(req, action.metadata.domain, true);
if (toolName.includes(domain)) {
currentDomain = domain;
actionSet = action;
break;
}
}
if (!actionSets.length) {
actionSets = (await loadActionSets({ agent_id: agent.id })) ?? [];
}
if (actionSet) {
const validationResult = validateAndParseOpenAPISpec(actionSet.metadata.raw_spec);
if (validationResult.spec) {
const { requestBuilders, functionSignatures, zodSchemas } = openapiToFunction(
validationResult.spec,
true,
let actionSet = null;
let currentDomain = '';
for (let action of actionSets) {
const domain = await domainParser(req, action.metadata.domain, true);
if (toolName.includes(domain)) {
currentDomain = domain;
actionSet = action;
break;
}
}
if (!actionSet) {
continue;
}
const validationResult = validateAndParseOpenAPISpec(actionSet.metadata.raw_spec);
if (validationResult.spec) {
const { requestBuilders, functionSignatures, zodSchemas } = openapiToFunction(
validationResult.spec,
true,
);
const functionName = toolName.replace(`${actionDelimiter}${currentDomain}`, '');
const functionSig = functionSignatures.find((sig) => sig.name === functionName);
const requestBuilder = requestBuilders[functionName];
const zodSchema = zodSchemas[functionName];
if (requestBuilder) {
const tool = await createActionTool({
action: actionSet,
requestBuilder,
zodSchema,
name: toolName,
description: functionSig.description,
});
if (!tool) {
logger.warn(
`Invalid action: user: ${req.user.id} | agent_id: ${agent.id} | toolName: ${toolName}`,
);
const functionName = toolName.replace(`${actionDelimiter}${currentDomain}`, '');
const functionSig = functionSignatures.find((sig) => sig.name === functionName);
const requestBuilder = requestBuilders[functionName];
const zodSchema = zodSchemas[functionName];
if (requestBuilder) {
const tool = await createActionTool({
action: actionSet,
requestBuilder,
zodSchema,
name: toolName,
description: functionSig.description,
});
if (!tool) {
logger.warn(
`Invalid action: user: ${req.user.id} | agent_id: ${agent_id} | toolName: ${toolName}`,
);
throw new Error(`{"type":"${ErrorTypes.INVALID_ACTION}"}`);
}
agentTools.push(tool);
ActionToolMap[toolName] = tool;
}
throw new Error(`{"type":"${ErrorTypes.INVALID_ACTION}"}`);
}
agentTools.push(tool);
ActionToolMap[toolName] = tool;
}
}
}
if (tools.length > 0 && agentTools.length === 0) {
throw new Error('No tools found for the specified tool calls.');
if (_agentTools.length > 0 && agentTools.length === 0) {
logger.warn(`No tools found for the specified tool calls: ${_agentTools.join(', ')}`);
return {};
}
return {

View File

@@ -6,6 +6,69 @@ const { getConvo } = require('~/models/Conversation');
const { getMessages } = require('~/models/Message');
const logger = require('~/config/winston');
/**
* Helper function to clone messages with proper parent-child relationships and timestamps
* @param {TMessage[]} messagesToClone - Original messages to clone
* @param {ImportBatchBuilder} importBatchBuilder - Instance of ImportBatchBuilder
* @returns {Map<string, string>} Map of original messageIds to new messageIds
*/
function cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder) {
const idMapping = new Map();
// First pass: create ID mapping and sort messages by parentMessageId
const sortedMessages = [...messagesToClone].sort((a, b) => {
if (a.parentMessageId === Constants.NO_PARENT) {
return -1;
}
if (b.parentMessageId === Constants.NO_PARENT) {
return 1;
}
return 0;
});
// Helper function to ensure date object
const ensureDate = (dateValue) => {
if (!dateValue) {
return new Date();
}
return dateValue instanceof Date ? dateValue : new Date(dateValue);
};
// Second pass: clone messages while maintaining proper timestamps
for (const message of sortedMessages) {
const newMessageId = uuidv4();
idMapping.set(message.messageId, newMessageId);
const parentId =
message.parentMessageId && message.parentMessageId !== Constants.NO_PARENT
? idMapping.get(message.parentMessageId)
: Constants.NO_PARENT;
// If this message has a parent, ensure its timestamp is after the parent's
let createdAt = ensureDate(message.createdAt);
if (parentId !== Constants.NO_PARENT) {
const parentMessage = importBatchBuilder.messages.find((msg) => msg.messageId === parentId);
if (parentMessage) {
const parentDate = ensureDate(parentMessage.createdAt);
if (createdAt <= parentDate) {
createdAt = new Date(parentDate.getTime() + 1);
}
}
}
const clonedMessage = {
...message,
messageId: newMessageId,
parentMessageId: parentId,
createdAt,
};
importBatchBuilder.saveMessage(clonedMessage);
}
return idMapping;
}
/**
*
* @param {object} params - The parameters for the importer.
@@ -65,23 +128,7 @@ async function forkConversation({
messagesToClone = getMessagesUpToTargetLevel(originalMessages, targetMessageId);
}
const idMapping = new Map();
for (const message of messagesToClone) {
const newMessageId = uuidv4();
idMapping.set(message.messageId, newMessageId);
const clonedMessage = {
...message,
messageId: newMessageId,
parentMessageId:
message.parentMessageId && message.parentMessageId !== Constants.NO_PARENT
? idMapping.get(message.parentMessageId)
: Constants.NO_PARENT,
};
importBatchBuilder.saveMessage(clonedMessage);
}
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);
const result = importBatchBuilder.finishConversation(
newTitle || originalConvo.title,
@@ -306,9 +353,63 @@ function splitAtTargetLevel(messages, targetMessageId) {
return filteredMessages;
}
/**
* Duplicates a conversation and all its messages.
* @param {object} params - The parameters for duplicating the conversation.
* @param {string} params.userId - The ID of the user duplicating the conversation.
* @param {string} params.conversationId - The ID of the conversation to duplicate.
* @returns {Promise<{ conversation: TConversation, messages: TMessage[] }>} The duplicated conversation and messages.
*/
async function duplicateConversation({ userId, conversationId }) {
// Get original conversation
const originalConvo = await getConvo(userId, conversationId);
if (!originalConvo) {
throw new Error('Conversation not found');
}
// Get original messages
const originalMessages = await getMessages({
user: userId,
conversationId,
});
const messagesToClone = getMessagesUpToTargetLevel(
originalMessages,
originalMessages[originalMessages.length - 1].messageId,
);
const importBatchBuilder = createImportBatchBuilder(userId);
importBatchBuilder.startConversation(originalConvo.endpoint ?? EModelEndpoint.openAI);
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);
const result = importBatchBuilder.finishConversation(
originalConvo.title,
new Date(),
originalConvo,
);
await importBatchBuilder.saveBatch();
logger.debug(
`user: ${userId} | New conversation "${originalConvo.title}" duplicated from conversation ID ${conversationId}`,
);
const conversation = await getConvo(userId, result.conversation.conversationId);
const messages = await getMessages({
user: userId,
conversationId: conversation.conversationId,
});
return {
conversation,
messages,
};
}
module.exports = {
forkConversation,
splitAtTargetLevel,
duplicateConversation,
getAllMessagesUpToParent,
getMessagesUpToTargetLevel,
cloneMessagesWithTimestamps,
};

View File

@@ -25,9 +25,11 @@ const {
splitAtTargetLevel,
getAllMessagesUpToParent,
getMessagesUpToTargetLevel,
cloneMessagesWithTimestamps,
} = require('./fork');
const { getConvo, bulkSaveConvos } = require('~/models/Conversation');
const { getMessages, bulkSaveMessages } = require('~/models/Message');
const { createImportBatchBuilder } = require('./importBatchBuilder');
const BaseClient = require('~/app/clients/BaseClient');
/**
@@ -104,7 +106,8 @@ describe('forkConversation', () => {
expect(bulkSaveMessages).toHaveBeenCalledWith(
expect.arrayContaining(
expectedMessagesTexts.map((text) => expect.objectContaining({ text })),
), true,
),
true,
);
});
@@ -122,7 +125,8 @@ describe('forkConversation', () => {
expect(bulkSaveMessages).toHaveBeenCalledWith(
expect.arrayContaining(
expectedMessagesTexts.map((text) => expect.objectContaining({ text })),
), true,
),
true,
);
});
@@ -141,7 +145,8 @@ describe('forkConversation', () => {
expect(bulkSaveMessages).toHaveBeenCalledWith(
expect.arrayContaining(
expectedMessagesTexts.map((text) => expect.objectContaining({ text })),
), true,
),
true,
);
});
@@ -160,7 +165,8 @@ describe('forkConversation', () => {
expect(bulkSaveMessages).toHaveBeenCalledWith(
expect.arrayContaining(
expectedMessagesTexts.map((text) => expect.objectContaining({ text })),
), true,
),
true,
);
});
@@ -572,3 +578,308 @@ describe('splitAtTargetLevel', () => {
expect(result.length).toBe(0);
});
});
describe('cloneMessagesWithTimestamps', () => {
test('should maintain proper timestamp order between parent and child messages', () => {
// Create messages with out-of-order timestamps
const messagesToClone = [
{
messageId: 'parent',
parentMessageId: Constants.NO_PARENT,
text: 'Parent Message',
createdAt: '2023-01-01T00:02:00Z', // Later timestamp
},
{
messageId: 'child1',
parentMessageId: 'parent',
text: 'Child Message 1',
createdAt: '2023-01-01T00:01:00Z', // Earlier timestamp
},
{
messageId: 'child2',
parentMessageId: 'parent',
text: 'Child Message 2',
createdAt: '2023-01-01T00:03:00Z',
},
];
const importBatchBuilder = createImportBatchBuilder('testUser');
importBatchBuilder.startConversation();
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);
// Verify timestamps are properly ordered
const clonedMessages = importBatchBuilder.messages;
expect(clonedMessages.length).toBe(3);
// Find cloned messages (they'll have new IDs)
const parent = clonedMessages.find((msg) => msg.parentMessageId === Constants.NO_PARENT);
const children = clonedMessages.filter((msg) => msg.parentMessageId === parent.messageId);
// Verify parent timestamp is earlier than all children
children.forEach((child) => {
expect(new Date(child.createdAt).getTime()).toBeGreaterThan(
new Date(parent.createdAt).getTime(),
);
});
});
test('should handle multi-level message chains', () => {
const messagesToClone = [
{
messageId: 'root',
parentMessageId: Constants.NO_PARENT,
text: 'Root',
createdAt: '2023-01-01T00:03:00Z', // Latest
},
{
messageId: 'parent',
parentMessageId: 'root',
text: 'Parent',
createdAt: '2023-01-01T00:01:00Z', // Earliest
},
{
messageId: 'child',
parentMessageId: 'parent',
text: 'Child',
createdAt: '2023-01-01T00:02:00Z', // Middle
},
];
const importBatchBuilder = createImportBatchBuilder('testUser');
importBatchBuilder.startConversation();
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);
const clonedMessages = importBatchBuilder.messages;
expect(clonedMessages.length).toBe(3);
// Verify the chain of timestamps
const root = clonedMessages.find((msg) => msg.parentMessageId === Constants.NO_PARENT);
const parent = clonedMessages.find((msg) => msg.parentMessageId === root.messageId);
const child = clonedMessages.find((msg) => msg.parentMessageId === parent.messageId);
expect(new Date(parent.createdAt).getTime()).toBeGreaterThan(
new Date(root.createdAt).getTime(),
);
expect(new Date(child.createdAt).getTime()).toBeGreaterThan(
new Date(parent.createdAt).getTime(),
);
});
test('should handle messages with identical timestamps', () => {
const sameTimestamp = '2023-01-01T00:00:00Z';
const messagesToClone = [
{
messageId: 'parent',
parentMessageId: Constants.NO_PARENT,
text: 'Parent',
createdAt: sameTimestamp,
},
{
messageId: 'child',
parentMessageId: 'parent',
text: 'Child',
createdAt: sameTimestamp,
},
];
const importBatchBuilder = createImportBatchBuilder('testUser');
importBatchBuilder.startConversation();
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);
const clonedMessages = importBatchBuilder.messages;
const parent = clonedMessages.find((msg) => msg.parentMessageId === Constants.NO_PARENT);
const child = clonedMessages.find((msg) => msg.parentMessageId === parent.messageId);
expect(new Date(child.createdAt).getTime()).toBeGreaterThan(
new Date(parent.createdAt).getTime(),
);
});
test('should preserve original timestamps when already properly ordered', () => {
const messagesToClone = [
{
messageId: 'parent',
parentMessageId: Constants.NO_PARENT,
text: 'Parent',
createdAt: '2023-01-01T00:00:00Z',
},
{
messageId: 'child',
parentMessageId: 'parent',
text: 'Child',
createdAt: '2023-01-01T00:01:00Z',
},
];
const importBatchBuilder = createImportBatchBuilder('testUser');
importBatchBuilder.startConversation();
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);
const clonedMessages = importBatchBuilder.messages;
const parent = clonedMessages.find((msg) => msg.parentMessageId === Constants.NO_PARENT);
const child = clonedMessages.find((msg) => msg.parentMessageId === parent.messageId);
expect(parent.createdAt).toEqual(new Date(messagesToClone[0].createdAt));
expect(child.createdAt).toEqual(new Date(messagesToClone[1].createdAt));
});
test('should handle complex multi-branch scenario with out-of-order timestamps', () => {
const complexMessages = [
// Branch 1: Root -> A -> (B, C) -> D
{
messageId: 'root1',
parentMessageId: Constants.NO_PARENT,
text: 'Root 1',
createdAt: '2023-01-01T00:05:00Z', // Root is later than children
},
{
messageId: 'A1',
parentMessageId: 'root1',
text: 'A1',
createdAt: '2023-01-01T00:02:00Z',
},
{
messageId: 'B1',
parentMessageId: 'A1',
text: 'B1',
createdAt: '2023-01-01T00:01:00Z', // Earlier than parent
},
{
messageId: 'C1',
parentMessageId: 'A1',
text: 'C1',
createdAt: '2023-01-01T00:03:00Z',
},
{
messageId: 'D1',
parentMessageId: 'B1',
text: 'D1',
createdAt: '2023-01-01T00:04:00Z',
},
// Branch 2: Root -> (X, Y, Z) where Z has children but X is latest
{
messageId: 'root2',
parentMessageId: Constants.NO_PARENT,
text: 'Root 2',
createdAt: '2023-01-01T00:06:00Z',
},
{
messageId: 'X2',
parentMessageId: 'root2',
text: 'X2',
createdAt: '2023-01-01T00:09:00Z', // Latest of siblings
},
{
messageId: 'Y2',
parentMessageId: 'root2',
text: 'Y2',
createdAt: '2023-01-01T00:07:00Z',
},
{
messageId: 'Z2',
parentMessageId: 'root2',
text: 'Z2',
createdAt: '2023-01-01T00:08:00Z',
},
{
messageId: 'Z2Child',
parentMessageId: 'Z2',
text: 'Z2 Child',
createdAt: '2023-01-01T00:04:00Z', // Earlier than all parents
},
// Branch 3: Root with alternating early/late timestamps
{
messageId: 'root3',
parentMessageId: Constants.NO_PARENT,
text: 'Root 3',
createdAt: '2023-01-01T00:15:00Z', // Latest of all
},
{
messageId: 'E3',
parentMessageId: 'root3',
text: 'E3',
createdAt: '2023-01-01T00:10:00Z',
},
{
messageId: 'F3',
parentMessageId: 'E3',
text: 'F3',
createdAt: '2023-01-01T00:14:00Z', // Later than parent
},
{
messageId: 'G3',
parentMessageId: 'F3',
text: 'G3',
createdAt: '2023-01-01T00:11:00Z', // Earlier than parent
},
{
messageId: 'H3',
parentMessageId: 'G3',
text: 'H3',
createdAt: '2023-01-01T00:13:00Z',
},
];
const importBatchBuilder = createImportBatchBuilder('testUser');
importBatchBuilder.startConversation();
cloneMessagesWithTimestamps(complexMessages, importBatchBuilder);
const clonedMessages = importBatchBuilder.messages;
console.debug(
'Complex multi-branch scenario\nOriginal messages:\n',
printMessageTree(complexMessages),
);
console.debug('Cloned messages:\n', printMessageTree(clonedMessages));
// Helper function to verify timestamp order
const verifyTimestampOrder = (parentId, messages) => {
const parent = messages.find((msg) => msg.messageId === parentId);
const children = messages.filter((msg) => msg.parentMessageId === parentId);
children.forEach((child) => {
const parentTime = new Date(parent.createdAt).getTime();
const childTime = new Date(child.createdAt).getTime();
expect(childTime).toBeGreaterThan(parentTime);
// Recursively verify child's children
verifyTimestampOrder(child.messageId, messages);
});
};
// Verify each branch
const roots = clonedMessages.filter((msg) => msg.parentMessageId === Constants.NO_PARENT);
roots.forEach((root) => verifyTimestampOrder(root.messageId, clonedMessages));
// Additional specific checks
const getMessageByText = (text) => clonedMessages.find((msg) => msg.text === text);
// Branch 1 checks
const root1 = getMessageByText('Root 1');
const b1 = getMessageByText('B1');
const d1 = getMessageByText('D1');
expect(new Date(b1.createdAt).getTime()).toBeGreaterThan(new Date(root1.createdAt).getTime());
expect(new Date(d1.createdAt).getTime()).toBeGreaterThan(new Date(b1.createdAt).getTime());
// Branch 2 checks
const root2 = getMessageByText('Root 2');
const x2 = getMessageByText('X2');
const z2Child = getMessageByText('Z2 Child');
const z2 = getMessageByText('Z2');
expect(new Date(x2.createdAt).getTime()).toBeGreaterThan(new Date(root2.createdAt).getTime());
expect(new Date(z2Child.createdAt).getTime()).toBeGreaterThan(new Date(z2.createdAt).getTime());
// Branch 3 checks
const f3 = getMessageByText('F3');
const g3 = getMessageByText('G3');
expect(new Date(g3.createdAt).getTime()).toBeGreaterThan(new Date(f3.createdAt).getTime());
// Verify all messages are present
expect(clonedMessages.length).toBe(complexMessages.length);
});
});

View File

@@ -62,6 +62,12 @@
* @memberof typedefs
*/
/**
* @exports ConversationSummaryBufferMemory
* @typedef {import('langchain/memory').ConversationSummaryBufferMemory} ConversationSummaryBufferMemory
* @memberof typedefs
*/
/**
* @exports UsageMetadata
* @typedef {import('@langchain/core/messages').UsageMetadata} UsageMetadata
@@ -746,6 +752,33 @@
* @memberof typedefs
*/
/**
*
* @typedef {Object} ImageGenOptions
* @property {ServerRequest} req - The request object.
* @property {boolean} isAgent - Whether the request is from an agent.
* @property {FileSources} fileStrategy - The file strategy to use.
* @property {processFileURL} processFileURL - The function to process a file URL.
* @property {boolean} returnMetadata - Whether to return metadata.
* @property {uploadImageBuffer} uploadImageBuffer - The function to upload an image buffer.
* @memberof typedefs
*/
/**
* @typedef {Partial<ImageGenOptions> & {
* message?: string,
* signal?: AbortSignal
* memory?: ConversationSummaryBufferMemory
* }} LoadToolOptions
* @memberof typedefs
*/
/**
* @exports EModelEndpoint
* @typedef {import('librechat-data-provider').EModelEndpoint} EModelEndpoint
* @memberof typedefs
*/
/**
* @exports TAttachment
* @typedef {import('librechat-data-provider').TAttachment} TAttachment
@@ -866,6 +899,42 @@
* @memberof typedefs
*/
/**
* @exports JsonSchemaType
* @typedef {import('librechat-data-provider').JsonSchemaType} JsonSchemaType
* @memberof typedefs
*/
/**
* @exports MCPServers
* @typedef {import('librechat-mcp').MCPServers} MCPServers
* @memberof typedefs
*/
/**
* @exports MCPManager
* @typedef {import('librechat-mcp').MCPManager} MCPManager
* @memberof typedefs
*/
/**
* @exports LCAvailableTools
* @typedef {import('librechat-mcp').LCAvailableTools} LCAvailableTools
* @memberof typedefs
*/
/**
* @exports LCTool
* @typedef {import('librechat-mcp').LCTool} LCTool
* @memberof typedefs
*/
/**
* @exports FormattedContent
* @typedef {import('librechat-mcp').FormattedContent} FormattedContent
* @memberof typedefs
*/
/**
* Represents details of the message creation by the run step, including the ID of the created message.
*

View File

@@ -2,7 +2,7 @@ const z = require('zod');
const { EModelEndpoint } = require('librechat-data-provider');
const openAIModels = {
o1: 127500, // -500 from max
o1: 195000, // -5000 from max
'o1-mini': 127500, // -500 from max
'o1-preview': 127500, // -500 from max
'gpt-4': 8187, // -5 from max
@@ -50,6 +50,7 @@ const googleModels = {
gemini: 30720, // -2048 from max
'gemini-pro-vision': 12288, // -4096 from max
'gemini-exp': 8000,
'gemini-2.0-flash-thinking-exp': 30720, // -2048 from max
'gemini-2.0': 1048576,
'gemini-1.5': 1048576,
'text-bison-32k': 32758, // -10 from max
@@ -85,16 +86,58 @@ const deepseekModels = {
};
const metaModels = {
// Basic patterns
llama3: 8000,
llama2: 4000,
'llama-3': 8000,
'llama-2': 4000,
// llama3.x pattern
'llama3.1': 127500,
'llama3.2': 127500,
'llama3.3': 127500,
// llama3-x pattern
'llama3-1': 127500,
'llama3-2': 127500,
'llama3-3': 127500,
// llama-3.x pattern
'llama-3.1': 127500,
'llama-3.2': 127500,
'llama-3.3': 127500,
// llama3.x:Nb pattern
'llama3.1:405b': 127500,
'llama3.1:70b': 127500,
'llama3.1:8b': 127500,
'llama3.2:1b': 127500,
'llama3.2:3b': 127500,
'llama3.2:11b': 127500,
'llama3.2:90b': 127500,
'llama3.3:70b': 127500,
// llama3-x-Nb pattern
'llama3-1-405b': 127500,
'llama3-1-70b': 127500,
'llama3-1-8b': 127500,
'llama3-2-1b': 127500,
'llama3-2-3b': 127500,
'llama3-2-11b': 127500,
'llama3-2-90b': 127500,
'llama3-3-70b': 127500,
// llama-3.x-Nb pattern
'llama-3.1-405b': 127500,
'llama-3.1-70b': 127500,
'llama-3.1-8b': 127500,
'llama-3.2-1b': 127500,
'llama-3.2-3b': 127500,
'llama-3.2-11b': 127500,
'llama-3.2-90b': 127500,
'llama-3.3-70b': 127500,
// Original llama2/3 patterns
'llama3-70b': 8000,
'llama3-8b': 8000,
'llama2-70b': 4000,

View File

@@ -248,6 +248,32 @@ describe('getModelMaxTokens', () => {
test('should return undefined for a model when using an unsupported endpoint', () => {
expect(getModelMaxTokens('azure-gpt-3', 'unsupportedEndpoint')).toBeUndefined();
});
test('should return correct max context tokens for o1-series models', () => {
// Standard o1 variations
const o1Tokens = maxTokensMap[EModelEndpoint.openAI]['o1'];
expect(getModelMaxTokens('o1')).toBe(o1Tokens);
expect(getModelMaxTokens('o1-latest')).toBe(o1Tokens);
expect(getModelMaxTokens('o1-2024-12-17')).toBe(o1Tokens);
expect(getModelMaxTokens('o1-something-else')).toBe(o1Tokens);
expect(getModelMaxTokens('openai/o1-something-else')).toBe(o1Tokens);
// Mini variations
const o1MiniTokens = maxTokensMap[EModelEndpoint.openAI]['o1-mini'];
expect(getModelMaxTokens('o1-mini')).toBe(o1MiniTokens);
expect(getModelMaxTokens('o1-mini-latest')).toBe(o1MiniTokens);
expect(getModelMaxTokens('o1-mini-2024-09-12')).toBe(o1MiniTokens);
expect(getModelMaxTokens('o1-mini-something')).toBe(o1MiniTokens);
expect(getModelMaxTokens('openai/o1-mini-something')).toBe(o1MiniTokens);
// Preview variations
const o1PreviewTokens = maxTokensMap[EModelEndpoint.openAI]['o1-preview'];
expect(getModelMaxTokens('o1-preview')).toBe(o1PreviewTokens);
expect(getModelMaxTokens('o1-preview-latest')).toBe(o1PreviewTokens);
expect(getModelMaxTokens('o1-preview-2024-09-12')).toBe(o1PreviewTokens);
expect(getModelMaxTokens('o1-preview-something')).toBe(o1PreviewTokens);
expect(getModelMaxTokens('openai/o1-preview-something')).toBe(o1PreviewTokens);
});
});
describe('matchModelName', () => {

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/frontend",
"version": "v0.7.5",
"version": "v0.7.6",
"description": "",
"type": "module",
"scripts": {

View File

@@ -177,10 +177,10 @@ export type AgentPanelProps = {
};
export type AgentModelPanelProps = {
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
providers: Option[];
models: Record<string, string[]>;
agent_id?: string;
providers: Option[];
models: Record<string, string[] | undefined>;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
};
export type AugmentedColumnDef<TData, TValue> = ColumnDef<TData, TValue> & DataColumnMeta;
@@ -464,6 +464,7 @@ export interface ExtendedFile {
source?: FileSources;
attached?: boolean;
embedded?: boolean;
tool_resource?: string;
}
export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void };

View File

@@ -1,7 +1,8 @@
import * as Ariakit from '@ariakit/react';
import React, { useRef, useState } from 'react';
import React, { useRef, useState, useMemo } from 'react';
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
import { EToolResources } from 'librechat-data-provider';
import { EToolResources, EModelEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { FileUpload, TooltipAnchor, DropdownPopup } from '~/components/ui';
import { AttachmentIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
@@ -19,6 +20,12 @@ const AttachFile = ({ isRTL, disabled, setToolResource, handleFileChange }: Atta
const isUploadDisabled = disabled ?? false;
const inputRef = useRef<HTMLInputElement>(null);
const [isPopoverActive, setIsPopoverActive] = useState(false);
const { data: endpointsConfig } = useGetEndpointsQuery();
const capabilities = useMemo(
() => endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [],
[endpointsConfig],
);
const handleUploadClick = (isImage?: boolean) => {
if (!inputRef.current) {
@@ -30,32 +37,42 @@ const AttachFile = ({ isRTL, disabled, setToolResource, handleFileChange }: Atta
inputRef.current.accept = '';
};
const dropdownItems = [
{
label: localize('com_ui_upload_image_input'),
onClick: () => {
setToolResource?.(undefined);
handleUploadClick(true);
const dropdownItems = useMemo(() => {
const items = [
{
label: localize('com_ui_upload_image_input'),
onClick: () => {
setToolResource?.(undefined);
handleUploadClick(true);
},
icon: <ImageUpIcon className="icon-md" />,
},
icon: <ImageUpIcon className="icon-md" />,
},
{
label: localize('com_ui_upload_file_search'),
onClick: () => {
setToolResource?.(EToolResources.file_search);
handleUploadClick();
},
icon: <FileSearch className="icon-md" />,
},
{
label: localize('com_ui_upload_code_files'),
onClick: () => {
setToolResource?.(EToolResources.execute_code);
handleUploadClick();
},
icon: <TerminalSquareIcon className="icon-md" />,
},
];
];
if (capabilities.includes(EToolResources.file_search)) {
items.push({
label: localize('com_ui_upload_file_search'),
onClick: () => {
setToolResource?.(EToolResources.file_search);
handleUploadClick();
},
icon: <FileSearch className="icon-md" />,
});
}
if (capabilities.includes(EToolResources.execute_code)) {
items.push({
label: localize('com_ui_upload_code_files'),
onClick: () => {
setToolResource?.(EToolResources.execute_code);
handleUploadClick();
},
icon: <TerminalSquareIcon className="icon-md" />,
});
}
return items;
}, [capabilities, localize, setToolResource]);
const menuTrigger = (
<TooltipAnchor
@@ -82,7 +99,7 @@ const AttachFile = ({ isRTL, disabled, setToolResource, handleFileChange }: Atta
return (
<FileUpload ref={inputRef} handleFileChange={handleFileChange}>
<div className="relative">
<div className="relative select-none">
<DropdownPopup
menuId="attach-file-menu"
isOpen={isPopoverActive}

View File

@@ -0,0 +1,90 @@
import React, { useMemo } from 'react';
import { EModelEndpoint, EToolResources } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import useLocalize from '~/hooks/useLocalize';
import { OGDialog } from '~/components/ui';
interface DragDropModalProps {
onOptionSelect: (option: string | undefined) => void;
files: File[];
isVisible: boolean;
setShowModal: (showModal: boolean) => void;
}
interface FileOption {
label: string;
value?: EToolResources;
icon: React.JSX.Element;
condition?: boolean;
}
const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragDropModalProps) => {
const localize = useLocalize();
const { data: endpointsConfig } = useGetEndpointsQuery();
const capabilities = useMemo(
() => endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [],
[endpointsConfig],
);
const options = useMemo(() => {
const _options: FileOption[] = [
{
label: localize('com_ui_upload_image_input'),
value: undefined,
icon: <ImageUpIcon className="icon-md" />,
condition: files.every((file) => file.type.startsWith('image/')),
},
];
for (const capability of capabilities) {
if (capability === EToolResources.file_search) {
_options.push({
label: localize('com_ui_upload_file_search'),
value: EToolResources.file_search,
icon: <FileSearch className="icon-md" />,
});
} else if (capability === EToolResources.execute_code) {
_options.push({
label: localize('com_ui_upload_code_files'),
value: EToolResources.execute_code,
icon: <TerminalSquareIcon className="icon-md" />,
});
}
}
return _options;
}, [capabilities, files, localize]);
if (!isVisible) {
return null;
}
return (
<OGDialog open={isVisible} onOpenChange={setShowModal}>
<OGDialogTemplate
title={localize('com_ui_upload_type')}
className="w-11/12 sm:w-[440px] md:w-[400px] lg:w-[360px]"
main={
<div className="flex flex-col gap-2">
{options.map(
(option, index) =>
option.condition !== false && (
<button
key={index}
onClick={() => onOptionSelect(option.value)}
className="flex items-center gap-2 rounded-lg p-2 hover:bg-surface-active-alt"
>
{option.icon}
<span>{option.label}</span>
</button>
),
)}
</div>
}
/>
</OGDialog>
);
};
export default DragDropModal;

View File

@@ -0,0 +1,29 @@
import { useDragHelpers } from '~/hooks';
import DragDropOverlay from '~/components/Chat/Input/Files/DragDropOverlay';
import DragDropModal from '~/components/Chat/Input/Files/DragDropModal';
import { cn } from '~/utils';
interface DragDropWrapperProps {
children: React.ReactNode;
className?: string;
}
export default function DragDropWrapper({ children, className }: DragDropWrapperProps) {
const { isOver, canDrop, drop, showModal, setShowModal, draggedFiles, handleOptionSelect } =
useDragHelpers();
const isActive = canDrop && isOver;
return (
<div ref={drop} className={cn('relative flex h-full w-full', className)}>
{children}
{isActive && <DragDropOverlay />}
<DragDropModal
files={draggedFiles}
isVisible={showModal}
setShowModal={setShowModal}
onOptionSelect={handleOptionSelect}
/>
</div>
);
}

View File

@@ -41,11 +41,11 @@ export default function PopoverButtons({
const { model: _model, endpoint: _endpoint, endpointType } = conversation ?? {};
const overrideEndpoint = overrideEndpointType ?? _overrideEndpoint;
const endpoint = overrideEndpoint ?? endpointType ?? _endpoint;
const endpoint = overrideEndpoint ?? endpointType ?? _endpoint ?? '';
const model = overrideModel ?? _model;
const isGenerativeModel = model?.toLowerCase()?.includes('gemini');
const isChatModel = !isGenerativeModel && model?.toLowerCase()?.includes('chat');
const isGenerativeModel = model?.toLowerCase()?.includes('gemini') ?? false;
const isChatModel = (!isGenerativeModel && model?.toLowerCase()?.includes('chat')) ?? false;
const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? '');
const { showExamples } = optionSettings;
@@ -53,14 +53,14 @@ export default function PopoverButtons({
const triggerExamples = () => {
setSettingsView(SettingsViews.default);
setOptionSettings((prev) => ({ ...prev, showExamples: !prev.showExamples }));
setOptionSettings((prev) => ({ ...prev, showExamples: !(prev.showExamples ?? false) }));
};
const endpointSpecificbuttons: { [key: string]: TPopoverButton[] } = {
[EModelEndpoint.google]: [
{
label: localize(showExamples ? 'com_hide_examples' : 'com_show_examples'),
buttonClass: isGenerativeModel || isTextModel ? 'disabled' : '',
label: localize(showExamples === true ? 'com_hide_examples' : 'com_show_examples'),
buttonClass: isGenerativeModel === true || isTextModel ? 'disabled' : '',
handler: triggerExamples,
icon: <MessagesSquared className={cn('mr-1 w-[14px]', iconClass)} />,
},
@@ -109,7 +109,7 @@ export default function PopoverButtons({
],
};
const endpointButtons = endpointSpecificbuttons[endpoint] ?? [];
const endpointButtons = (endpointSpecificbuttons[endpoint] as TPopoverButton[] | null) ?? [];
const disabled = true;
@@ -123,7 +123,7 @@ export default function PopoverButtons({
className={cn(
button.buttonClass,
'border border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
'ml-1 h-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white',
'ml-1 h-full bg-transparent px-2 py-1 text-xs font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white',
buttonClass ?? '',
)}
onClick={button.handler}
@@ -133,6 +133,7 @@ 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) => (
@@ -142,7 +143,7 @@ export default function PopoverButtons({
className={cn(
button.buttonClass,
'flex justify-center border border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
'h-full w-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white',
'h-full w-full bg-transparent px-2 py-1 text-xs font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white',
buttonClass ?? '',
)}
onClick={button.handler}

View File

@@ -1,7 +1,7 @@
import { useRecoilState } from 'recoil';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys } from 'librechat-data-provider';
import { QueryKeys, isAgentsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TModelsConfig, TEndpointsConfig } from 'librechat-data-provider';
import {
@@ -37,15 +37,26 @@ const EditPresetDialog = ({
});
const [presetModalVisible, setPresetModalVisible] = useRecoilState(store.presetModalVisible);
const { data: availableEndpoints = [] } = useGetEndpointsQuery({
const { data: _endpoints = [] } = useGetEndpointsQuery({
select: mapEndpoints,
});
const availableEndpoints = useMemo(() => {
return _endpoints.filter((endpoint) => !isAgentsEndpoint(endpoint));
}, [_endpoints]);
useEffect(() => {
if (!preset) {
return;
}
if (!preset.endpoint) {
if (isAgentsEndpoint(preset.endpoint)) {
return;
}
const presetEndpoint = preset.endpoint ?? '';
if (!presetEndpoint) {
return;
}
@@ -54,7 +65,7 @@ const EditPresetDialog = ({
return;
}
const models = modelsConfig[preset.endpoint];
const models = modelsConfig[presetEndpoint] as string[] | undefined;
if (!models) {
return;
}
@@ -75,7 +86,11 @@ const EditPresetDialog = ({
return;
}
if (preset.agentOptions?.model && !models.includes(preset.agentOptions.model)) {
if (
preset.agentOptions?.model != null &&
preset.agentOptions.model &&
!models.includes(preset.agentOptions.model)
) {
console.log('setting agent model', models[0]);
setAgentOption('model')(models[0]);
}
@@ -102,9 +117,12 @@ const EditPresetDialog = ({
[queryClient, setOptions],
);
const { endpoint, endpointType, model } = preset || {};
const { endpoint: _endpoint, endpointType, model } = preset || {};
const endpoint = _endpoint ?? '';
if (!endpoint) {
return null;
} else if (isAgentsEndpoint(endpoint)) {
return null;
}
return (
@@ -121,7 +139,7 @@ const EditPresetDialog = ({
title={`${localize('com_ui_edit') + ' ' + localize('com_endpoint_preset')} - ${
preset?.title
}`}
className="h-full max-w-full overflow-y-auto pb-4 sm:w-[680px] sm:pb-0 md:h-[720px] md:w-[750px] md:overflow-y-hidden md:overflow-y-hidden lg:w-[950px] xl:h-[720px]"
className="h-full max-w-full overflow-y-auto pb-4 sm:w-[680px] sm:pb-0 md:h-[720px] md:w-[750px] md:overflow-y-hidden lg:w-[950px] xl:h-[720px]"
main={
<div className="flex w-full flex-col items-center gap-2 md:h-[550px] md:overflow-y-auto">
<div className="grid w-full">
@@ -165,7 +183,7 @@ const EditPresetDialog = ({
{''}
</Label>
<PopoverButtons
buttonClass="ml-0 w-full border border-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600 p-2 h-[40px] justify-center mt-0"
buttonClass="ml-0 w-full border border-border-medium p-2 h-[40px] justify-center mt-0"
iconClass="hidden lg:block w-4 "
endpoint={endpoint}
endpointType={endpointType}
@@ -174,13 +192,13 @@ const EditPresetDialog = ({
</div>
</div>
</div>
<div className="my-4 w-full border-t border-gray-300 dark:border-gray-600" />
<div className="my-4 w-full border-t border-border-medium" />
<div className="w-full p-0">
<EndpointSettings
conversation={preset}
setOption={setOption}
isPreset={true}
className="h-full md:mb-4 md:h-[440px]"
className="h-full text-text-primary md:mb-4 md:h-[440px]"
/>
</div>
</div>

View File

@@ -35,8 +35,30 @@ export default function ToolCall({
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference;
const [function_name, _domain] = name.split(actionDelimiter) as [string, string | undefined];
const domain = _domain?.replaceAll(actionDomainSeparator, '.') ?? null;
const { function_name, domain, isMCPToolCall } = useMemo(() => {
if (typeof name !== 'string') {
return { function_name: '', domain: null, isMCPToolCall: false };
}
if (name.includes(Constants.mcp_delimiter)) {
const [func, server] = name.split(Constants.mcp_delimiter);
return {
function_name: func || '',
domain: server && (server.replaceAll(actionDomainSeparator, '.') || null),
isMCPToolCall: true,
};
}
const [func, _domain] = name.includes(actionDelimiter)
? name.split(actionDelimiter)
: [name, ''];
return {
function_name: func || '',
domain: _domain && (_domain.replaceAll(actionDomainSeparator, '.') || null),
isMCPToolCall: false,
};
}, [name]);
const error =
typeof output === 'string' && output.toLowerCase().includes('error processing tool');
@@ -83,6 +105,9 @@ export default function ToolCall({
};
const getFinishedText = () => {
if (isMCPToolCall === true) {
return localize('com_assistants_completed_function', function_name);
}
if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) {
return localize('com_assistants_completed_action', domain);
}

View File

@@ -3,11 +3,11 @@ import { useEffect, useMemo } from 'react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { FileSources, LocalStorageKeys, getConfigDefaults } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import { useDragHelpers, useSetFilesToDelete } from '~/hooks';
import DragDropOverlay from './Input/Files/DragDropOverlay';
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
import { useDeleteFilesMutation } from '~/data-provider';
import Artifacts from '~/components/Artifacts/Artifacts';
import { SidePanel } from '~/components/SidePanel';
import { useSetFilesToDelete } from '~/hooks';
import store from '~/store';
const defaultInterface = getConfigDefaults().interface;
@@ -33,7 +33,6 @@ export default function Presentation({
);
const setFilesToDelete = useSetFilesToDelete();
const { isOver, canDrop, drop } = useDragHelpers();
const { mutateAsync } = useDeleteFilesMutation({
onSuccess: () => {
@@ -66,8 +65,6 @@ export default function Presentation({
mutateAsync({ files });
}, [mutateAsync]);
const isActive = canDrop && isOver;
const defaultLayout = useMemo(() => {
const resizableLayout = localStorage.getItem('react-resizable-panels:layout');
return typeof resizableLayout === 'string' ? JSON.parse(resizableLayout) : undefined;
@@ -79,20 +76,16 @@ export default function Presentation({
const fullCollapse = useMemo(() => localStorage.getItem('fullPanelCollapse') === 'true', []);
const layout = () => (
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden bg-white pt-0 dark:bg-gray-800">
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden bg-presentation pt-0">
<div className="flex h-full flex-col" role="presentation">
{children}
{isActive && <DragDropOverlay />}
</div>
</div>
);
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel === true) {
return (
<div
ref={drop}
className="relative flex w-full grow overflow-hidden bg-white dark:bg-gray-800"
>
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
<SidePanel
defaultLayout={defaultLayout}
defaultCollapsed={defaultCollapsed}
@@ -107,17 +100,16 @@ export default function Presentation({
>
<main className="flex h-full flex-col" role="main">
{children}
{isActive && <DragDropOverlay />}
</main>
</SidePanel>
</div>
</DragDropWrapper>
);
}
return (
<div ref={drop} className="relative flex w-full grow overflow-hidden bg-white dark:bg-gray-800">
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
{layout()}
{panel != null && panel}
</div>
</DragDropWrapper>
);
}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { Check, X } from 'lucide-react';
import { useParams } from 'react-router-dom';
@@ -6,7 +6,7 @@ import { Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
import type { TConversation } from 'librechat-data-provider';
import { useConversations, useNavigateToConvo, useMediaQuery } from '~/hooks';
import { useConversations, useNavigateToConvo, useMediaQuery, useLocalize } from '~/hooks';
import { useUpdateConversationMutation } from '~/data-provider';
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
import { NotificationSeverity } from '~/common';
@@ -14,7 +14,6 @@ import { useToastContext } from '~/Providers';
import { ConvoOptions } from './ConvoOptions';
import { cn } from '~/utils';
import store from '~/store';
import { useLocalize } from '~/hooks'
type KeyEvent = KeyboardEvent<HTMLInputElement>;
@@ -54,6 +53,7 @@ export default function Conversation({
}
event.preventDefault();
if (currentConvoId === conversationId || isPopoverActive) {
return;
}
@@ -71,11 +71,11 @@ export default function Conversation({
);
};
const renameHandler: (e: MouseEvent<HTMLButtonElement>) => void = () => {
const renameHandler = useCallback(() => {
setIsPopoverActive(false);
setTitleInput(title);
setRenaming(true);
};
}, [title]);
useEffect(() => {
if (renaming && inputRef.current) {
@@ -83,79 +83,103 @@ export default function Conversation({
}
}, [renaming]);
const onRename = (e: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement> | KeyEvent) => {
e.preventDefault();
setRenaming(false);
if (titleInput === title) {
return;
}
if (typeof conversationId !== 'string' || conversationId === '') {
return;
}
const onRename = useCallback(
(e: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement> | KeyEvent) => {
e.preventDefault();
setRenaming(false);
if (titleInput === title) {
return;
}
if (typeof conversationId !== 'string' || conversationId === '') {
return;
}
updateConvoMutation.mutate(
{ conversationId, title: titleInput ?? '' },
{
onSuccess: () => refreshConversations(),
onError: () => {
setTitleInput(title);
showToast({
message: 'Failed to rename conversation',
severity: NotificationSeverity.ERROR,
showIcon: true,
});
updateConvoMutation.mutate(
{ conversationId, title: titleInput ?? '' },
{
onSuccess: () => refreshConversations(),
onError: () => {
setTitleInput(title);
showToast({
message: 'Failed to rename conversation',
severity: NotificationSeverity.ERROR,
showIcon: true,
});
},
},
},
);
};
);
},
[title, titleInput, conversationId, showToast, refreshConversations, updateConvoMutation],
);
const handleKeyDown = (e: KeyEvent) => {
if (e.key === 'Escape') {
const handleKeyDown = useCallback(
(e: KeyEvent) => {
if (e.key === 'Escape') {
setTitleInput(title);
setRenaming(false);
} else if (e.key === 'Enter') {
onRename(e);
}
},
[title, onRename],
);
const cancelRename = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setTitleInput(title);
setRenaming(false);
} else if (e.key === 'Enter') {
onRename(e);
}
};
},
[title],
);
const cancelRename = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setTitleInput(title);
setRenaming(false);
};
const isActiveConvo: boolean =
currentConvoId === conversationId ||
(isLatestConvo &&
currentConvoId === 'new' &&
activeConvos[0] != null &&
activeConvos[0] !== 'new');
const isActiveConvo: boolean = useMemo(
() =>
currentConvoId === conversationId ||
(isLatestConvo &&
currentConvoId === 'new' &&
activeConvos[0] != null &&
activeConvos[0] !== 'new'),
[currentConvoId, conversationId, isLatestConvo, activeConvos],
);
return (
<div
className={cn(
'group relative mt-2 flex h-9 w-full items-center rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700',
isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '',
'group relative mt-2 flex h-9 w-full items-center rounded-lg hover:bg-surface-active-alt',
isActiveConvo ? 'bg-surface-active-alt' : '',
isSmallScreen ? 'h-12' : '',
)}
>
{renaming ? (
<div className="absolute inset-0 z-20 flex w-full items-center rounded-lg bg-gray-200 p-1.5 dark:bg-gray-700">
<div className="absolute inset-0 z-20 flex w-full items-center rounded-lg bg-surface-active-alt p-1.5">
<input
ref={inputRef}
type="text"
className="w-full rounded bg-transparent p-0.5 text-sm leading-tight outline-none"
className="w-full rounded bg-transparent p-0.5 text-sm leading-tight focus-visible:outline-none"
value={titleInput ?? ''}
onChange={(e) => setTitleInput(e.target.value)}
onKeyDown={handleKeyDown}
aria-label={`${localize('com_ui_rename')} ${localize('com_ui_chat')}`}
/>
<div className="flex gap-1">
<button onClick={cancelRename} aria-label={`${localize('com_ui_cancel')} ${localize('com_ui_rename')}`}>
<X aria-hidden={true} className="transition-colors h-4 w-4 duration-200 ease-in-out hover:opacity-70" />
<button
onClick={cancelRename}
aria-label={`${localize('com_ui_cancel')} ${localize('com_ui_rename')}`}
>
<X
aria-hidden={true}
className="h-4 w-4 transition-colors duration-200 ease-in-out hover:opacity-70"
/>
</button>
<button onClick={onRename} aria-label={`${localize('com_ui_submit')} ${localize('com_ui_rename')}`}>
<Check aria-hidden={true} className="transition-colors h-4 w-4 duration-200 ease-in-out hover:opacity-70" />
<button
onClick={onRename}
aria-label={`${localize('com_ui_submit')} ${localize('com_ui_rename')}`}
>
<Check
aria-hidden={true}
className="h-4 w-4 transition-colors duration-200 ease-in-out hover:opacity-70"
/>
</button>
</div>
</div>
@@ -166,7 +190,7 @@ export default function Conversation({
onClick={clickHandler}
className={cn(
'flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg px-2 py-2',
isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '',
isActiveConvo ? 'bg-surface-active-alt' : '',
)}
title={title ?? ''}
>
@@ -176,11 +200,21 @@ export default function Conversation({
size={20}
context="menu-item"
/>
<div className="relative line-clamp-1 flex-1 grow overflow-hidden">{title}</div>
<div
className="relative line-clamp-1 flex-1 grow overflow-hidden"
onDoubleClick={(e) => {
e.preventDefault();
e.stopPropagation();
setTitleInput(title);
setRenaming(true);
}}
>
{title}
</div>
{isActiveConvo ? (
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l" />
) : (
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l from-gray-50 from-0% to-transparent group-hover:from-gray-200 group-hover:from-40% dark:from-gray-850 dark:group-hover:from-gray-700" />
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l from-surface-primary-alt from-0% to-transparent group-hover:from-surface-active-alt group-hover:from-40%" />
)}
</a>
)}
@@ -192,14 +226,17 @@ export default function Conversation({
: 'hidden group-focus-within:flex group-hover:flex',
)}
>
<ConvoOptions
conversation={conversation}
retainView={retainView}
renameHandler={renameHandler}
isPopoverActive={isPopoverActive}
setIsPopoverActive={setIsPopoverActive}
isActiveConvo={isActiveConvo}
/>
{!renaming && (
<ConvoOptions
title={title}
retainView={retainView}
renameHandler={renameHandler}
isActiveConvo={isActiveConvo}
conversationId={conversationId}
isPopoverActive={isPopoverActive}
setIsPopoverActive={setIsPopoverActive}
/>
)}
</div>
</div>
);

View File

@@ -1,27 +1,65 @@
import { useState, useId } from 'react';
import * as Ariakit from '@ariakit/react';
import { Ellipsis, Share2, Archive, Pen, Trash } from 'lucide-react';
import * as Menu from '@ariakit/react/menu';
import { Ellipsis, Share2, Copy, Archive, Pen, Trash } from 'lucide-react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { useLocalize, useArchiveHandler } from '~/hooks';
import type { MouseEvent } from 'react';
import { useLocalize, useArchiveHandler, useNavigateToConvo } from '~/hooks';
import { useToastContext, useChatContext } from '~/Providers';
import { useDuplicateConversationMutation } from '~/data-provider';
import { DropdownPopup } from '~/components/ui';
import DeleteButton from './DeleteButton';
import ShareButton from './ShareButton';
import { cn } from '~/utils';
export default function ConvoOptions({
conversation,
conversationId,
title,
retainView,
renameHandler,
isPopoverActive,
setIsPopoverActive,
isActiveConvo,
}: {
conversationId: string | null;
title: string | null;
retainView: () => void;
renameHandler: (e: MouseEvent) => void;
isPopoverActive: boolean;
setIsPopoverActive: React.Dispatch<React.SetStateAction<boolean>>;
isActiveConvo: boolean;
}) {
const localize = useLocalize();
const { index } = useChatContext();
const { data: startupConfig } = useGetStartupConfig();
const { conversationId, title } = conversation;
const archiveHandler = useArchiveHandler(conversationId, true, retainView);
const { navigateToConvo } = useNavigateToConvo(index);
const { showToast } = useToastContext();
const [showShareDialog, setShowShareDialog] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const archiveHandler = useArchiveHandler(conversationId, true, retainView);
const duplicateConversation = useDuplicateConversationMutation({
onSuccess: (data) => {
if (data != null) {
navigateToConvo(data.conversation);
showToast({
message: localize('com_ui_duplication_success'),
status: 'success',
});
}
},
onMutate: () => {
showToast({
message: localize('com_ui_duplication_processing'),
status: 'info',
});
},
onError: () => {
showToast({
message: localize('com_ui_duplication_error'),
status: 'error',
});
},
});
const shareHandler = () => {
setIsPopoverActive(false);
@@ -33,27 +71,39 @@ export default function ConvoOptions({
setShowDeleteDialog(true);
};
const duplicateHandler = () => {
setIsPopoverActive(false);
duplicateConversation.mutate({
conversationId: conversationId ?? '',
});
};
const dropdownItems = [
{
label: localize('com_ui_rename'),
onClick: renameHandler,
icon: <Pen className="icon-md mr-2 text-text-secondary" />,
},
{
label: localize('com_ui_share'),
onClick: shareHandler,
icon: <Share2 className="icon-md mr-2 text-text-secondary" />,
icon: <Share2 className="icon-sm mr-2 text-text-primary" />,
show: startupConfig && startupConfig.sharedLinksEnabled,
},
{
label: localize('com_ui_rename'),
onClick: renameHandler,
icon: <Pen className="icon-sm mr-2 text-text-primary" />,
},
{
label: localize('com_ui_duplicate'),
onClick: duplicateHandler,
icon: <Copy className="icon-sm mr-2 text-text-primary" />,
},
{
label: localize('com_ui_archive'),
onClick: archiveHandler,
icon: <Archive className="icon-md mr-2 text-text-secondary" />,
icon: <Archive className="icon-sm mr-2 text-text-primary" />,
},
{
label: localize('com_ui_delete'),
onClick: deleteHandler,
icon: <Trash className="icon-md mr-2 text-text-secondary" />,
icon: <Trash className="icon-sm mr-2 text-text-primary" />,
},
];
@@ -65,7 +115,7 @@ export default function ConvoOptions({
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}
trigger={
<Ariakit.MenuButton
<Menu.MenuButton
id="conversation-menu-button"
aria-label={localize('com_nav_convo_menu_options')}
className={cn(
@@ -76,24 +126,24 @@ export default function ConvoOptions({
)}
>
<Ellipsis className="icon-md text-text-secondary" aria-hidden={true} />
</Ariakit.MenuButton>
</Menu.MenuButton>
}
items={dropdownItems}
menuId={menuId}
/>
{showShareDialog && (
<ShareButton
conversationId={conversationId}
title={title}
title={title ?? ''}
conversationId={conversationId ?? ''}
showShareDialog={showShareDialog}
setShowShareDialog={setShowShareDialog}
/>
)}
{showDeleteDialog && (
<DeleteButton
conversationId={conversationId}
title={title ?? ''}
retainView={retainView}
title={title}
conversationId={conversationId ?? ''}
showDeleteDialog={showDeleteDialog}
setShowDeleteDialog={setShowDeleteDialog}
/>

View File

@@ -6,7 +6,6 @@ import { useUpdateSharedLinkMutation } from '~/data-provider';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { Spinner } from '~/components/svg';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
export default function SharedLinkButton({
@@ -112,7 +111,7 @@ export default function SharedLinkButton({
onClick={() => {
handlers.handler();
}}
className="btn btn-primary flex items-center"
className="btn btn-primary flex items-center justify-center"
>
{isCopying && (
<>

View File

@@ -177,11 +177,14 @@ const Nav = ({
<SearchBar clearSearch={clearSearch} isSmallScreen={isSmallScreen} />
)}
{hasAccessToBookmarks === true && (
<BookmarkNav
tags={tags}
setTags={setTags}
isSmallScreen={isSmallScreen}
/>
<>
<div className="mt-1.5" />
<BookmarkNav
tags={tags}
setTags={setTags}
isSmallScreen={isSmallScreen}
/>
</>
)}
</>
}

View File

@@ -1,4 +1,4 @@
import { useLocalize, useLocalStorage } from '~/hooks';
import { useLocalize } from '~/hooks';
import { TooltipAnchor } from '~/components/ui';
import { cn } from '~/utils';

View File

@@ -73,7 +73,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
}
<input
type="text"
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight placeholder-text-secondary placeholder-opacity-100 outline-none group-focus-within:placeholder-text-primary group-hover:placeholder-text-primary"
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight placeholder-text-secondary placeholder-opacity-100 focus-visible:outline-none group-focus-within:placeholder-text-primary group-hover:placeholder-text-primary"
value={text}
onChange={onChange}
onKeyDown={(e) => {

View File

@@ -88,7 +88,7 @@ function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) {
</OGDialogTrigger>
<OGDialogTemplate
showCloseButton={false}
title={localize('com_ui_delete_conversation')}
title={localize('com_ui_delete_shared_link')}
className="max-w-[450px]"
main={
<>

View File

@@ -21,7 +21,7 @@ export default function SharedLinks() {
title={localize('com_nav_shared_links')}
className="max-w-[1000px]"
showCancelButton={false}
main={<ShareLinkTable />}
main={<ShareLinkTable className="w-full" />}
/>
</OGDialog>
</div>

View File

@@ -166,8 +166,7 @@ const AdminSettings = () => {
</Ariakit.MenuButton>
}
items={roleDropdownItems}
className="border border-border-light bg-surface-primary"
itemClassName="hover:bg-surface-tertiary items-center justify-center"
itemClassName="items-center justify-center"
sameWidth={true}
/>
</div>

View File

@@ -16,10 +16,10 @@ import type { ActionAuthForm } from '~/common';
import type { Spec } from './ActionsTable';
import { ActionsTable, columns } from './ActionsTable';
import { useUpdateAgentAction } from '~/data-provider';
import { cn, removeFocusOutlines } from '~/utils';
import { useToastContext } from '~/Providers';
import useLocalize from '~/hooks/useLocalize';
import { Spinner } from '~/components/svg';
import { logger } from '~/utils';
const debouncedValidation = debounce(
(input: string, callback: (result: ValidationResult) => void) => {
@@ -56,12 +56,13 @@ export default function ActionsInput({
const [functions, setFunctions] = useState<FunctionTool[] | null>(null);
useEffect(() => {
if (!action?.metadata?.raw_spec) {
const rawSpec = action?.metadata.raw_spec ?? '';
if (!rawSpec) {
return;
}
setInputValue(action.metadata.raw_spec);
debouncedValidation(action.metadata.raw_spec, handleResult);
}, [action?.metadata?.raw_spec]);
setInputValue(rawSpec);
debouncedValidation(rawSpec, handleResult);
}, [action?.metadata.raw_spec]);
useEffect(() => {
if (!validationResult || !validationResult.status || !validationResult.spec) {
@@ -94,15 +95,16 @@ export default function ActionsInput({
},
onError(error) {
showToast({
message: (error as Error)?.message ?? localize('com_assistants_update_actions_error'),
message: (error as Error).message || localize('com_assistants_update_actions_error'),
status: 'error',
});
},
});
const saveAction = handleSubmit((authFormData) => {
console.log('authFormData', authFormData);
if (!agent_id) {
logger.log('actions', 'saving action', authFormData);
const currentAgentId = agent_id ?? '';
if (!currentAgentId) {
// alert user?
return;
}
@@ -171,7 +173,7 @@ export default function ActionsInput({
action_id,
metadata,
functions,
agent_id,
agent_id: currentAgentId,
});
});
@@ -186,17 +188,34 @@ export default function ActionsInput({
debouncedValidation(newValue, handleResult);
};
const getButtonContent = () => {
if (updateAgentAction.isLoading) {
return <Spinner className="icon-md" />;
}
if (action?.action_id != null && action.action_id) {
return localize('com_ui_update');
}
return localize('com_ui_create');
};
return (
<>
<div className="">
<div className="mb-1 flex flex-wrap items-center justify-between gap-4">
<label className="text-token-text-primary whitespace-nowrap font-medium">Schema</label>
<label
htmlFor="schemaInput"
className="text-token-text-primary whitespace-nowrap font-medium"
>
Schema
</label>
<div className="flex items-center gap-2">
{/* <button className="btn btn-neutral border-token-border-light relative h-8 min-w-[100px] rounded-lg font-medium">
<div className="flex w-full items-center justify-center text-xs">Import from URL</div>
</button> */}
<select
onChange={(e) => console.log(e.target.value)}
onChange={(e) => logger.log('actions', 'selecting example action', e.target.value)}
className="border-token-border-medium h-8 min-w-[100px] rounded-lg border bg-transparent px-2 py-0 text-sm"
>
<option value="label">{localize('com_ui_examples')}</option>
@@ -207,17 +226,15 @@ export default function ActionsInput({
</select>
</div>
</div>
<div className="border-token-border-light mb-4 overflow-hidden rounded-lg border">
<div className="border-token-border-medium bg-token-surface-primary hover:border-token-border-hover mb-4 w-full overflow-hidden rounded-lg border ring-0">
<div className="relative">
<textarea
id="schemaInput"
value={inputValue}
onChange={handleInputChange}
spellCheck="false"
placeholder="Enter your OpenAPI schema here"
className={cn(
'text-token-text-primary block h-96 w-full border-none bg-transparent p-2 font-mono text-xs',
removeFocusOutlines,
)}
placeholder={localize('com_ui_enter_openapi_schema')}
className="text-token-text-primary block h-96 w-full bg-transparent p-2 font-mono text-xs outline-none focus:ring-1 focus:ring-border-light"
/>
{/* TODO: format input button */}
</div>
@@ -240,28 +257,18 @@ export default function ActionsInput({
<ActionsTable columns={columns} data={data} />
</div>
)}
<div className="mt-4">
<div className="relative my-1">
<div className="mb-1.5 flex items-center">
<span className="" data-state="closed">
<label className="text-token-text-primary block font-medium">
{localize('com_ui_privacy_policy')}
</label>
</span>
<label className="text-token-text-primary block font-medium">
{localize('com_ui_privacy_policy_url')}
</label>
</div>
<div className="rounded-md border border-gray-300 px-3 py-2 shadow-none focus-within:border-gray-800 focus-within:ring-1 focus-within:ring-gray-800 dark:border-gray-700 dark:bg-gray-700 dark:focus-within:border-gray-500 dark:focus-within:ring-gray-500">
<label
htmlFor="privacyPolicyUrl"
className="block text-xs font-medium text-gray-900 dark:text-gray-100"
<div className="border-token-border-medium bg-token-surface-primary hover:border-token-border-hover flex h-9 w-full rounded-lg border">
<input
type="text"
placeholder="https://api.example-weather-app.com/privacy"
className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-border-light"
/>
<div className="relative">
<input
name="privacyPolicyUrl"
id="privacyPolicyUrl"
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 shadow-none outline-none focus-within:shadow-none focus-within:outline-none focus-within:ring-0 focus:border-none focus:ring-0 dark:bg-gray-700 dark:text-gray-100 sm:text-sm"
placeholder="https://api.example-weather-app.com/privacy"
// value=""
/>
</div>
</div>
</div>
<div className="flex items-center justify-end">
@@ -271,13 +278,7 @@ export default function ActionsInput({
className="focus:shadow-outline mt-1 flex min-w-[100px] items-center justify-center rounded bg-green-500 px-4 py-2 font-semibold text-white hover:bg-green-400 focus:border-green-500 focus:outline-none focus:ring-0 disabled:bg-green-400"
type="button"
>
{updateAgentAction.isLoading ? (
<Spinner className="icon-md" />
) : action?.action_id ? (
localize('com_ui_update')
) : (
localize('com_ui_create')
)}
{getButtonContent()}
</button>
</div>
</>

View File

@@ -39,7 +39,7 @@ export default function ActionsPanel({
},
onError(error) {
showToast({
message: (error as Error)?.message ?? localize('com_assistants_delete_actions_error'),
message: (error as Error).message ?? localize('com_assistants_delete_actions_error'),
status: 'error',
});
},
@@ -68,7 +68,7 @@ export default function ActionsPanel({
const type = watch('type');
useEffect(() => {
if (action?.metadata?.auth) {
if (action?.metadata.auth) {
reset({
type: action.metadata.auth.type || AuthTypeEnum.None,
saved_auth_fields: false,
@@ -149,16 +149,16 @@ export default function ActionsPanel({
)}
<div className="text-xl font-medium">{(action ? 'Edit' : 'Add') + ' ' + 'actions'}</div>
<div className="text-token-text-tertiary text-sm">
<div className="text-xs text-text-secondary">
{localize('com_assistants_actions_info')}
</div>
{/* <div className="text-sm text-token-text-tertiary">
{/* <div className="text-sm text-text-secondary">
<a href="https://help.openai.com/en/articles/8554397-creating-a-gpt" target="_blank" rel="noreferrer" className="font-medium">Learn more.</a>
</div> */}
</div>
<Dialog open={openAuthDialog} onOpenChange={setOpenAuthDialog}>
<DialogTrigger asChild>
<div className="relative mb-6">
<div className="relative mb-4">
<div className="mb-1.5 flex items-center">
<label className="text-token-text-primary block font-medium">
{localize('com_ui_authentication')}

View File

@@ -142,7 +142,7 @@ const AdminSettings = () => {
<Button
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative my-1 h-9 w-full gap-1 rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative mb-4 h-9 w-full gap-1 rounded-lg font-medium"
>
<ShieldEllipsis className="cursor-pointer" />
{localize('com_ui_admin_settings')}
@@ -166,8 +166,7 @@ const AdminSettings = () => {
</Ariakit.MenuButton>
}
items={roleDropdownItems}
className="border border-border-light bg-surface-primary"
itemClassName="hover:bg-surface-tertiary items-center justify-center"
itemClassName="items-center justify-center"
sameWidth={true}
/>
</div>

View File

@@ -18,11 +18,12 @@ import { useToastContext, useFileMapContext } from '~/Providers';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import Action from '~/components/SidePanel/Builder/Action';
import { ToolSelectDialog } from '~/components/Tools';
import DuplicateAgent from './DuplicateAgent';
import { processAgentOption } from '~/utils';
import AdminSettings from './AdminSettings';
import { Spinner } from '~/components/svg';
import DeleteButton from './DeleteButton';
import AgentAvatar from './AgentAvatar';
import { Spinner } from '~/components';
import FileSearch from './FileSearch';
import ShareAgent from './ShareAgent';
import AgentTool from './AgentTool';
@@ -421,6 +422,7 @@ export default function AgentConfig({
isCollaborative={agent?.isCollaborative}
/>
)}
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex h-9 w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"

View File

@@ -20,6 +20,7 @@ import { createProviderOption } from '~/utils';
import { useToastContext } from '~/Providers';
import AgentConfig from './AgentConfig';
import AgentSelect from './AgentSelect';
import { Button } from '~/components';
import ModelPanel from './ModelPanel';
import { Panel } from '~/common';
@@ -208,7 +209,7 @@ export default function AgentPanel({
className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden"
aria-label="Agent configuration form"
>
<div className="flex w-full flex-wrap">
<div className="mt-2 flex w-full flex-wrap gap-2">
<Controller
name="agent"
control={control}
@@ -225,15 +226,17 @@ export default function AgentPanel({
/>
{/* Select Button */}
{agent_id && (
<button
className="btn btn-primary focus:shadow-outline mx-2 mt-1 h-[40px] rounded bg-green-500 px-4 py-2 font-semibold text-white hover:bg-green-400 focus:border-green-500 focus:outline-none focus:ring-0"
type="button"
<Button
variant="submit"
disabled={!agent_id}
onClick={handleSelectAgent}
onClick={(e) => {
e.preventDefault();
handleSelectAgent();
}}
aria-label="Select agent"
>
{localize('com_ui_select')}
</button>
</Button>
)}
</div>
{!canEditAgent && (

View File

@@ -6,8 +6,8 @@ export default function AgentPanelSkeleton() {
<div className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden">
{/* Agent Select and Button */}
<div className="mt-1 flex w-full gap-2">
<Skeleton className="h-[40px] w-3/4 rounded" />
<Skeleton className="h-[40px] w-1/4 rounded" />
<Skeleton className="h-[40px] w-4/5 rounded-lg" />
<Skeleton className="h-[40px] w-1/5 rounded-lg" />
</div>
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
@@ -17,52 +17,60 @@ export default function AgentPanelSkeleton() {
<Skeleton className="relative h-20 w-20 rounded-full" />
</div>
{/* Name */}
<Skeleton className="mb-2 h-5 w-1/5 rounded" />
<Skeleton className="mb-1 h-[40px] w-full rounded" />
<Skeleton className="h-3 w-1/4 rounded" />
<Skeleton className="mb-2 h-5 w-1/5 rounded-lg" />
<Skeleton className="mb-1 h-[40px] w-full rounded-lg" />
<Skeleton className="h-3 w-1/4 rounded-lg" />
</div>
{/* Description */}
<div className="mb-4">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="h-[40px] w-full rounded" />
<Skeleton className="mb-2 h-5 w-1/4 rounded-lg" />
<Skeleton className="h-[40px] w-full rounded-lg" />
</div>
{/* Instructions */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="h-[100px] w-full rounded" />
<Skeleton className="mb-2 h-5 w-1/4 rounded-lg" />
<Skeleton className="h-[100px] w-full rounded-lg" />
</div>
{/* Model and Provider */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="h-[40px] w-full rounded" />
<Skeleton className="mb-2 h-5 w-1/4 rounded-lg" />
<Skeleton className="h-[40px] w-full rounded-lg" />
</div>
{/* Capabilities */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="mb-2 h-[40px] w-full rounded" />
<Skeleton className="h-[40px] w-full rounded" />
<Skeleton className="mb-2 h-5 w-1/4 rounded-lg" />
<Skeleton className="mb-2 h-5 w-36 rounded-lg" />
<Skeleton className="mb-4 h-[35px] w-full rounded-lg" />
<Skeleton className="mb-2 h-5 w-24 rounded-lg" />
<Skeleton className="h-[35px] w-full rounded-lg" />
</div>
{/* Tools & Actions */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="mb-2 h-[40px] w-full rounded" />
<Skeleton className="mb-2 h-[40px] w-full rounded" />
<Skeleton className="mb-2 h-5 w-1/4 rounded-lg" />
<Skeleton className="mb-2 h-[35px] w-full rounded-lg" />
<Skeleton className="mb-2 h-[35px] w-full rounded-lg" />
<div className="flex space-x-2">
<Skeleton className="h-8 w-1/2 rounded" />
<Skeleton className="h-8 w-1/2 rounded" />
<Skeleton className="h-8 w-1/2 rounded-lg" />
<Skeleton className="h-8 w-1/2 rounded-lg" />
</div>
</div>
{/* Admin Settings */}
<div className="mb-6">
<Skeleton className="h-[35px] w-full rounded-lg" />
</div>
{/* Bottom Buttons */}
<div className="flex items-center justify-end gap-2">
<Skeleton className="h-[40px] w-[100px] rounded" />
<Skeleton className="h-[40px] w-[100px] rounded" />
<Skeleton className="h-[40px] w-[100px] rounded" />
<Skeleton className="h-[35px] w-16 rounded-lg" />
<Skeleton className="h-[35px] w-16 rounded-lg" />
<Skeleton className="h-[35px] w-16 rounded-lg" />
<Skeleton className="h-[35px] w-full rounded-lg" />
</div>
</div>
</div>

View File

@@ -185,8 +185,8 @@ export default function AgentSelect({
hasAgentValue ? 'text-gray-500' : '',
)}
className={cn(
'mt-1 rounded-md dark:border-gray-700 dark:bg-gray-850',
'z-50 flex h-[40px] w-full flex-none items-center justify-center px-4 hover:cursor-pointer hover:border-green-500 focus:border-gray-400',
'rounded-md dark:border-gray-700 dark:bg-gray-850',
'z-50 flex h-[40px] w-full flex-none items-center justify-center truncate px-4 hover:cursor-pointer hover:border-green-500 focus:border-gray-400',
)}
renderOption={() => (
<span className="flex items-center gap-1.5 truncate">

View File

@@ -0,0 +1,50 @@
import { CopyIcon } from 'lucide-react';
import { useDuplicateAgentMutation } from '~/data-provider';
import { cn, removeFocusOutlines } from '~/utils';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
export default function DuplicateAgent({ agent_id }: { agent_id: string }) {
const localize = useLocalize();
const { showToast } = useToastContext();
const duplicateAgent = useDuplicateAgentMutation({
onSuccess: () => {
showToast({
message: localize('com_ui_agent_duplicated'),
status: 'success',
});
},
onError: (error) => {
console.error(error);
showToast({
message: localize('com_ui_agent_duplicate_error'),
status: 'error',
});
},
});
if (!agent_id) {
return null;
}
const handleDuplicate = () => {
duplicateAgent.mutate({ agent_id });
};
return (
<button
className={cn(
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
removeFocusOutlines,
)}
aria-label={localize('com_ui_duplicate') + ' ' + localize('com_ui_agent')}
type="button"
onClick={handleDuplicate}
>
<div className="flex w-full items-center justify-center gap-2 text-primary">
<CopyIcon className="size-4" />
</div>
</button>
);
}

View File

@@ -1,7 +1,7 @@
import React, { useMemo, useEffect } from 'react';
import { ChevronLeft } from 'lucide-react';
import { ChevronLeft, RotateCcw } from 'lucide-react';
import { getSettingsKeys } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import { useFormContext, useWatch, Controller } from 'react-hook-form';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type * as t from 'librechat-data-provider';
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
@@ -19,10 +19,11 @@ export default function Parameters({
}: AgentModelPanelProps) {
const localize = useLocalize();
const { control, setValue, watch } = useFormContext<AgentForm>();
const modelParameters = watch('model_parameters');
const providerOption = watch('provider');
const model = watch('model');
const { control, setValue } = useFormContext<AgentForm>();
const model = useWatch({ control, name: 'model' });
const providerOption = useWatch({ control, name: 'provider' });
const modelParameters = useWatch({ control, name: 'model_parameters' });
const provider = useMemo(() => {
const value =
@@ -71,6 +72,10 @@ export default function Parameters({
setValue(`model_parameters.${optionKey}`, value);
};
const handleResetParameters = () => {
setValue('model_parameters', {} as t.AgentModelParameters);
};
return (
<div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto pb-12 text-sm">
<div className="model-panel relative flex flex-col items-center px-16 py-6 text-center">
@@ -209,6 +214,17 @@ export default function Parameters({
);
})}
</div>
{/* Reset Parameters Button */}
<div className="mt-6 flex justify-center">
<button
type="button"
onClick={handleResetParameters}
className="btn btn-neutral flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
<RotateCcw className="h-4 w-4" />
{localize('com_ui_reset_var', localize('com_ui_model_parameters'))}
</button>
</div>
</div>
)}
</div>

View File

@@ -18,7 +18,6 @@ import type { Spec } from './ActionsTable';
import { useAssistantsMapContext, useToastContext } from '~/Providers';
import { ActionsTable, columns } from './ActionsTable';
import { useUpdateAction } from '~/data-provider';
import { cn, removeFocusOutlines } from '~/utils';
import useLocalize from '~/hooks/useLocalize';
import { Spinner } from '~/components/svg';
@@ -219,7 +218,7 @@ export default function ActionsInput({
htmlFor="example-schema"
className="text-token-text-primary whitespace-nowrap font-medium"
>
Schema
{localize('com_ui_schema')}
</label>
<div className="flex items-center gap-2">
{/* <button className="btn btn-neutral border-token-border-light relative h-8 min-w-[100px] rounded-lg font-medium">
@@ -238,17 +237,15 @@ export default function ActionsInput({
</select>
</div>
</div>
<div className="border-token-border-light mb-4 overflow-hidden rounded-lg border">
<div className="border-token-border-medium bg-token-surface-primary hover:border-token-border-hover mb-4 w-full overflow-hidden rounded-lg border ring-0">
<div className="relative">
<textarea
id="schemaInput"
value={inputValue}
onChange={handleInputChange}
spellCheck="false"
placeholder="Enter your OpenAPI schema here"
className={cn(
'text-token-text-primary block h-96 w-full border-none bg-transparent p-2 font-mono text-xs',
removeFocusOutlines,
)}
placeholder={localize('com_ui_enter_openapi_schema')}
className="text-token-text-primary block h-96 w-full bg-transparent p-2 font-mono text-xs outline-none focus:ring-1 focus:ring-border-light"
/>
{/* TODO: format input button */}
</div>
@@ -271,20 +268,18 @@ export default function ActionsInput({
<ActionsTable columns={columns} data={data} />
</div>
)}
<div className="mt-4">
<div className="rounded-md border border-gray-300 px-3 py-2 shadow-none focus-within:border-gray-800 focus-within:ring-1 focus-within:ring-gray-800 dark:border-gray-700 dark:bg-gray-700 dark:focus-within:border-gray-500 dark:focus-within:ring-gray-500">
<label htmlFor="privacyPolicyUrl" className="block text-xs text-text-secondary">
Privacy Policy URL
<div className="relative my-1">
<div className="mb-1.5 flex items-center">
<label className="text-token-text-primary block font-medium">
{localize('com_ui_privacy_policy_url')}
</label>
<div className="relative">
<input
name="privacyPolicyUrl"
id="privacyPolicyUrl"
className="block w-full border-0 bg-transparent p-0 placeholder-text-secondary shadow-none outline-none focus-within:shadow-none focus-within:outline-none focus-within:ring-0 focus:border-none focus:ring-0 sm:text-sm"
placeholder="https://api.example-weather-app.com/privacy"
// value=""
/>
</div>
</div>
<div className="border-token-border-medium bg-token-surface-primary hover:border-token-border-hover flex h-9 w-full rounded-lg border">
<input
type="text"
placeholder="https://api.example-weather-app.com/privacy"
className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-border-light"
/>
</div>
</div>
<div className="flex items-center justify-end">

View File

@@ -155,16 +155,16 @@ export default function ActionsPanel({
)}
<div className="text-xl font-medium">{(action ? 'Edit' : 'Add') + ' ' + 'actions'}</div>
<div className="text-token-text-tertiary text-sm">
<div className="text-xs text-text-secondary">
{localize('com_assistants_actions_info')}
</div>
{/* <div className="text-sm text-token-text-tertiary">
{/* <div className="text-sm text-text-secondary">
<a href="https://help.openai.com/en/articles/8554397-creating-a-gpt" target="_blank" rel="noreferrer" className="font-medium">Learn more.</a>
</div> */}
</div>
<Dialog open={openAuthDialog} onOpenChange={setOpenAuthDialog}>
<DialogTrigger asChild>
<div className="relative mb-6">
<div className="relative mb-4">
<div className="mb-1.5 flex items-center">
<label className="text-token-text-primary block font-medium">
{localize('com_ui_authentication')}

View File

@@ -1,5 +1,5 @@
import { TPlugin } from 'librechat-data-provider';
import { XCircle, PlusCircleIcon } from 'lucide-react';
import { XCircle, PlusCircleIcon, Wrench } from 'lucide-react';
import { useLocalize } from '~/hooks';
type ToolItemProps = {
@@ -9,7 +9,7 @@ type ToolItemProps = {
isInstalled?: boolean;
};
function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled }: ToolItemProps) {
function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled = false }: ToolItemProps) {
const localize = useLocalize();
const handleClick = () => {
if (isInstalled) {
@@ -20,20 +20,26 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled }: ToolItemProps)
};
return (
<div className="flex flex-col gap-4 rounded border border-black/10 bg-white p-6 dark:border-white/20 dark:bg-gray-800">
<div className="flex flex-col gap-4 rounded border border-border-medium bg-transparent p-6">
<div className="flex gap-4">
<div className="h-[70px] w-[70px] shrink-0">
<div className="relative h-full w-full">
<img
src={tool.icon}
alt={`${tool.name} logo`}
className="h-full w-full rounded-[5px] bg-white"
/>
{tool.icon != null && tool.icon ? (
<img
src={tool.icon}
alt={localize('com_ui_logo', tool.name)}
className="h-full w-full rounded-[5px] bg-white"
/>
) : (
<div className="flex h-full w-full items-center justify-center rounded-[5px] border border-border-medium bg-transparent">
<Wrench className="h-8 w-8 text-text-secondary" />
</div>
)}
<div className="absolute inset-0 rounded-[5px] ring-1 ring-inset ring-black/10"></div>
</div>
</div>
<div className="flex min-w-0 flex-col items-start justify-between">
<div className="mb-2 line-clamp-1 max-w-full text-lg leading-5 text-gray-700/80 dark:text-gray-50">
<div className="mb-2 line-clamp-1 max-w-full text-lg leading-5 text-text-primary">
{tool.name}
</div>
{!isInstalled ? (
@@ -61,9 +67,7 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled }: ToolItemProps)
)}
</div>
</div>
<div className="line-clamp-3 h-[60px] text-sm text-gray-700/70 dark:text-gray-50/70">
{tool.description}
</div>
<div className="line-clamp-3 h-[60px] text-sm text-text-secondary">{tool.description}</div>
</div>
);
}

View File

@@ -151,22 +151,22 @@ function ToolSelectDialog({
className="relative z-[102]"
>
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<div className="fixed inset-0 bg-gray-600/65 transition-opacity dark:bg-black/80" />
<div className="fixed inset-0 bg-surface-primary opacity-60 transition-opacity dark:opacity-80" />
{/* Full-screen container to center the panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-white text-left shadow-xl transition-all dark:bg-gray-800 max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-surface-secondary text-left shadow-xl transition-all max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
style={{ minHeight: '610px' }}
>
<div className="flex items-center justify-between border-b-[1px] border-black/10 px-4 pb-4 pt-5 dark:border-white/10 sm:p-6">
<div className="flex items-center justify-between border-b-[1px] border-border-medium px-4 pb-4 pt-5 sm:p-6">
<div className="flex items-center">
<div className="text-center sm:text-left">
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
<DialogTitle className="text-lg font-medium leading-6 text-text-primary">
{isAgentTools
? localize('com_nav_tool_dialog_agents')
: localize('com_nav_tool_dialog')}
</DialogTitle>
<Description className="text-sm text-gray-500 dark:text-gray-300">
<Description className="text-sm text-text-secondary">
{localize('com_nav_tool_dialog_description')}
</Description>
</div>
@@ -178,7 +178,7 @@ function ToolSelectDialog({
setIsOpen(false);
setCurrentPage(1);
}}
className="inline-block text-gray-500 hover:text-gray-200"
className="inline-block text-text-tertiary hover:text-text-secondary"
tabIndex={0}
>
<X />
@@ -206,13 +206,13 @@ function ToolSelectDialog({
<div className="p-4 sm:p-6 sm:pt-4">
<div className="mt-4 flex flex-col gap-4">
<div className="flex items-center justify-center space-x-4">
<Search className="h-6 w-6 text-gray-500" />
<Search className="h-6 w-6 text-text-tertiary" />
<input
type="text"
value={searchValue}
onChange={handleSearch}
placeholder={localize('com_nav_plugin_search')}
className="w-64 rounded border border-gray-300 px-2 py-1 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
placeholder={localize('com_nav_tool_search')}
className="w-64 rounded border border-border-medium bg-transparent px-2 py-1 text-text-primary focus:outline-none"
/>
</div>
<div

View File

@@ -4,7 +4,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '~/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
'inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import * as Select from '@ariakit/react/select';
import { cn } from '~/utils/';
import type { Option } from '~/common';
import { cn } from '~/utils/';
interface DropdownProps {
value: string;

View File

@@ -6,7 +6,7 @@ interface DropdownProps {
trigger: React.ReactNode;
items: {
label?: string;
onClick?: () => void;
onClick?: (e: React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => void;
icon?: React.ReactNode;
kbd?: string;
show?: boolean;
@@ -45,10 +45,7 @@ const DropdownPopup: React.FC<DropdownProps> = ({
{trigger}
<Ariakit.Menu
id={menuId}
className={cn(
'absolute z-50 mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary',
className,
)}
className={cn('popover-ui z-50', className)}
gutter={gutter}
modal={modal}
sameWidth={sameWidth}
@@ -62,20 +59,20 @@ const DropdownPopup: React.FC<DropdownProps> = ({
<Ariakit.MenuItem
key={index}
className={cn(
'group flex w-full cursor-pointer items-center gap-2 rounded-lg p-2.5 text-sm text-text-primary outline-none transition-colors duration-200 hover:bg-surface-hover focus:bg-surface-hover',
'group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-3.5 text-sm text-text-primary outline-none transition-colors duration-200 hover:bg-surface-hover focus:bg-surface-hover md:px-2.5 md:py-2',
itemClassName,
)}
disabled={item.disabled}
onClick={(event) => {
event.preventDefault();
if (item.onClick) {
item.onClick();
item.onClick(event);
}
menu.hide();
}}
>
{item.icon != null && (
<span className={cn('mr-2 h-5 w-5', iconClassName)} aria-hidden="true">
<span className={cn('mr-2 size-4', iconClassName)} aria-hidden="true">
{item.icon}
</span>
)}

View File

@@ -59,29 +59,33 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
overlayClassName={overlayClassName}
showCloseButton={showCloseButton}
ref={ref}
className={cn('border-none bg-background text-foreground', className ?? '')}
className={cn('w-11/12 border-none bg-background text-foreground', className ?? '')}
onClick={(e) => e.stopPropagation()}
>
<OGDialogHeader className={cn(headerClassName ?? '')}>
<OGDialogTitle>{title}</OGDialogTitle>
{description && <OGDialogDescription className="">{description}</OGDialogDescription>}
{description && (
<OGDialogDescription className="items-center justify-center">
{description}
</OGDialogDescription>
)}
</OGDialogHeader>
<div className={cn('px-0', mainClassName)}>{main != null ? main : null}</div>
<div className={cn('px-0 py-2', mainClassName)}>{main != null ? main : null}</div>
<OGDialogFooter className={footerClassName}>
<div>{leftButtons != null ? leftButtons : null}</div>
<div className="flex h-auto gap-3">
<div>{leftButtons != null ? <div className="mt-3 sm:mt-0">{leftButtons}</div> : null}</div>
<div className="flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:flex-row">
{buttons != null ? buttons : null}
{showCancelButton && (
<OGDialogClose className="btn btn-neutral border-token-border-light relative rounded-lg text-sm ring-offset-2 focus:ring-2 focus:ring-black dark:ring-offset-0">
<OGDialogClose className="btn btn-neutral border-token-border-light relative justify-center rounded-lg text-sm ring-offset-2 focus:ring-2 focus:ring-black dark:ring-offset-0 max-sm:order-last max-sm:w-full sm:order-first">
{Cancel}
</OGDialogClose>
)}
{buttons != null ? buttons : null}
{selection ? (
<OGDialogClose
onClick={selectHandler}
className={`${
selectClasses ?? defaultSelect
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm`}
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm max-sm:order-first max-sm:w-full sm:order-none`}
>
{selectText}
</OGDialogClose>

View File

@@ -1 +1,2 @@
export * from './queries';
export * from './mutations';

View File

@@ -0,0 +1,302 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { dataService, MutationKeys, QueryKeys, defaultOrderQuery } from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
/**
* AGENTS
*/
/**
* Create a new agent
*/
export const useCreateAgentMutation = (
options?: t.CreateAgentMutationOptions,
): UseMutationResult<t.Agent, Error, t.AgentCreateParams> => {
const queryClient = useQueryClient();
return useMutation((newAgentData: t.AgentCreateParams) => dataService.createAgent(newAgentData), {
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (newAgent, variables, context) => {
const listRes = queryClient.getQueryData<t.AgentListResponse>([
QueryKeys.agents,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(newAgent, variables, context);
}
const currentAgents = [newAgent, ...JSON.parse(JSON.stringify(listRes.data))];
queryClient.setQueryData<t.AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
...listRes,
data: currentAgents,
});
return options?.onSuccess?.(newAgent, variables, context);
},
});
};
/**
* Hook for updating an agent
*/
export const useUpdateAgentMutation = (
options?: t.UpdateAgentMutationOptions,
): UseMutationResult<t.Agent, Error, { agent_id: string; data: t.AgentUpdateParams }> => {
const queryClient = useQueryClient();
return useMutation(
({ agent_id, data }: { agent_id: string; data: t.AgentUpdateParams }) => {
return dataService.updateAgent({
data,
agent_id,
});
},
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (updatedAgent, variables, context) => {
const listRes = queryClient.getQueryData<t.AgentListResponse>([
QueryKeys.agents,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(updatedAgent, variables, context);
}
queryClient.setQueryData<t.AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
...listRes,
data: listRes.data.map((agent) => {
if (agent.id === variables.agent_id) {
return updatedAgent;
}
return agent;
}),
});
queryClient.setQueryData<t.Agent>([QueryKeys.agent, variables.agent_id], updatedAgent);
return options?.onSuccess?.(updatedAgent, variables, context);
},
},
);
};
/**
* Hook for deleting an agent
*/
export const useDeleteAgentMutation = (
options?: t.DeleteAgentMutationOptions,
): UseMutationResult<void, Error, t.DeleteAgentBody> => {
const queryClient = useQueryClient();
return useMutation(
({ agent_id }: t.DeleteAgentBody) => {
return dataService.deleteAgent({ agent_id });
},
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (_data, variables, context) => {
const listRes = queryClient.getQueryData<t.AgentListResponse>([
QueryKeys.agents,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(_data, variables, context);
}
const data = listRes.data.filter((agent) => agent.id !== variables.agent_id);
queryClient.setQueryData<t.AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
...listRes,
data,
});
queryClient.removeQueries([QueryKeys.agent, variables.agent_id]);
return options?.onSuccess?.(_data, variables, data);
},
},
);
};
/**
* Hook for duplicating an agent
*/
export const useDuplicateAgentMutation = (
options?: t.DuplicateAgentMutationOptions,
): UseMutationResult<{ agent: t.Agent; actions: t.Action[] }, Error, t.DuplicateAgentBody> => {
const queryClient = useQueryClient();
return useMutation<{ agent: t.Agent; actions: t.Action[] }, Error, t.DuplicateAgentBody>(
(params: t.DuplicateAgentBody) => dataService.duplicateAgent(params),
{
onMutate: options?.onMutate,
onError: options?.onError,
onSuccess: ({ agent, actions }, variables, context) => {
const listRes = queryClient.getQueryData<t.AgentListResponse>([
QueryKeys.agents,
defaultOrderQuery,
]);
if (listRes) {
const currentAgents = [agent, ...listRes.data];
queryClient.setQueryData<t.AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
...listRes,
data: currentAgents,
});
}
const existingActions = queryClient.getQueryData<t.Action[]>([QueryKeys.actions]) || [];
queryClient.setQueryData<t.Action[]>([QueryKeys.actions], existingActions.concat(actions));
return options?.onSuccess?.({ agent, actions }, variables, context);
},
},
);
};
/**
* Hook for uploading an agent avatar
*/
export const useUploadAgentAvatarMutation = (
options?: t.UploadAgentAvatarOptions,
): UseMutationResult<
t.Agent, // response data
unknown, // error
t.AgentAvatarVariables, // request
unknown // context
> => {
return useMutation([MutationKeys.agentAvatarUpload], {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
mutationFn: ({ postCreation, ...variables }: t.AgentAvatarVariables) =>
dataService.uploadAgentAvatar(variables),
...(options || {}),
});
};
/**
* Hook for updating Agent Actions
*/
export const useUpdateAgentAction = (
options?: t.UpdateAgentActionOptions,
): UseMutationResult<
t.UpdateAgentActionResponse, // response data
unknown, // error
t.UpdateAgentActionVariables, // request
unknown // context
> => {
const queryClient = useQueryClient();
return useMutation([MutationKeys.updateAgentAction], {
mutationFn: (variables: t.UpdateAgentActionVariables) =>
dataService.updateAgentAction(variables),
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (updateAgentActionResponse, variables, context) => {
const listRes = queryClient.getQueryData<t.AgentListResponse>([
QueryKeys.agents,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(updateAgentActionResponse, variables, context);
}
const updatedAgent = updateAgentActionResponse[0];
queryClient.setQueryData<t.AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
...listRes,
data: listRes.data.map((agent) => {
if (agent.id === variables.agent_id) {
return updatedAgent;
}
return agent;
}),
});
queryClient.setQueryData<t.Action[]>([QueryKeys.actions], (prev) => {
return prev
?.map((action) => {
if (action.action_id === variables.action_id) {
return updateAgentActionResponse[1];
}
return action;
})
.concat(
variables.action_id != null && variables.action_id
? []
: [updateAgentActionResponse[1]],
);
});
queryClient.setQueryData<t.Agent>([QueryKeys.agent, variables.agent_id], updatedAgent);
return options?.onSuccess?.(updateAgentActionResponse, variables, context);
},
});
};
/**
* Hook for deleting an Agent Action
*/
export const useDeleteAgentAction = (
options?: t.DeleteAgentActionOptions,
): UseMutationResult<void, Error, t.DeleteAgentActionVariables, unknown> => {
const queryClient = useQueryClient();
return useMutation([MutationKeys.deleteAgentAction], {
mutationFn: (variables: t.DeleteAgentActionVariables) => {
return dataService.deleteAgentAction({
...variables,
});
},
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (_data, variables, context) => {
let domain: string | undefined = '';
queryClient.setQueryData<t.Action[]>([QueryKeys.actions], (prev) => {
return prev?.filter((action) => {
domain = action.metadata.domain;
return action.action_id !== variables.action_id;
});
});
queryClient.setQueryData<t.AgentListResponse>(
[QueryKeys.agents, defaultOrderQuery],
(prev) => {
if (!prev) {
return prev;
}
return {
...prev,
data: prev.data.map((agent) => {
if (agent.id === variables.agent_id) {
return {
...agent,
tools: agent.tools?.filter((tool) => !tool.includes(domain ?? '')),
};
}
return agent;
}),
};
},
);
queryClient.setQueryData<t.Agent>([QueryKeys.agent, variables.agent_id], (prev) => {
if (!prev) {
return prev;
}
return {
...prev,
tools: prev.tools?.filter((tool) => !tool.includes(domain ?? '')),
};
});
return options?.onSuccess?.(_data, variables, context);
},
});
};

View File

@@ -573,6 +573,43 @@ export const useDeleteConversationMutation = (
);
};
export const useDuplicateConversationMutation = (
options?: t.DuplicateConvoOptions,
): UseMutationResult<t.TDuplicateConvoResponse, unknown, t.TDuplicateConvoRequest, unknown> => {
const queryClient = useQueryClient();
const { onSuccess, ..._options } = options ?? {};
return useMutation(
(payload: t.TDuplicateConvoRequest) => dataService.duplicateConversation(payload),
{
onSuccess: (data, vars, context) => {
const originalId = vars.conversationId ?? '';
if (originalId.length === 0) {
return;
}
if (data == null) {
return;
}
queryClient.setQueryData(
[QueryKeys.conversation, data.conversation.conversationId],
data.conversation,
);
queryClient.setQueryData<t.ConversationData>([QueryKeys.allConversations], (convoData) => {
if (!convoData) {
return convoData;
}
return addConversation(convoData, data.conversation);
});
queryClient.setQueryData<t.TMessage[]>(
[QueryKeys.messages, data.conversation.conversationId],
data.messages,
);
onSuccess?.(data, vars, context);
},
..._options,
},
);
};
export const useForkConvoMutation = (
options?: t.ForkConvoOptions,
): UseMutationResult<t.TForkConvoResponse, unknown, t.TForkConvoRequest, unknown> => {
@@ -1056,254 +1093,6 @@ export const useDeleteAction = (
});
};
/**
* AGENTS
*/
/**
* Create a new agent
*/
export const useCreateAgentMutation = (
options?: t.CreateAgentMutationOptions,
): UseMutationResult<t.Agent, Error, t.AgentCreateParams> => {
const queryClient = useQueryClient();
return useMutation((newAgentData: t.AgentCreateParams) => dataService.createAgent(newAgentData), {
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (newAgent, variables, context) => {
const listRes = queryClient.getQueryData<t.AgentListResponse>([
QueryKeys.agents,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(newAgent, variables, context);
}
const currentAgents = [newAgent, ...JSON.parse(JSON.stringify(listRes.data))];
queryClient.setQueryData<t.AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
...listRes,
data: currentAgents,
});
return options?.onSuccess?.(newAgent, variables, context);
},
});
};
/**
* Hook for updating an agent
*/
export const useUpdateAgentMutation = (
options?: t.UpdateAgentMutationOptions,
): UseMutationResult<t.Agent, Error, { agent_id: string; data: t.AgentUpdateParams }> => {
const queryClient = useQueryClient();
return useMutation(
({ agent_id, data }: { agent_id: string; data: t.AgentUpdateParams }) => {
return dataService.updateAgent({
data,
agent_id,
});
},
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (updatedAgent, variables, context) => {
const listRes = queryClient.getQueryData<t.AgentListResponse>([
QueryKeys.agents,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(updatedAgent, variables, context);
}
queryClient.setQueryData<t.AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
...listRes,
data: listRes.data.map((agent) => {
if (agent.id === variables.agent_id) {
return updatedAgent;
}
return agent;
}),
});
queryClient.setQueryData<t.Agent>([QueryKeys.agent, variables.agent_id], updatedAgent);
return options?.onSuccess?.(updatedAgent, variables, context);
},
},
);
};
/**
* Hook for deleting an agent
*/
export const useDeleteAgentMutation = (
options?: t.DeleteAgentMutationOptions,
): UseMutationResult<void, Error, t.DeleteAgentBody> => {
const queryClient = useQueryClient();
return useMutation(
({ agent_id }: t.DeleteAgentBody) => {
return dataService.deleteAgent({ agent_id });
},
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (_data, variables, context) => {
const listRes = queryClient.getQueryData<t.AgentListResponse>([
QueryKeys.agents,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(_data, variables, context);
}
const data = listRes.data.filter((agent) => agent.id !== variables.agent_id);
queryClient.setQueryData<t.AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
...listRes,
data,
});
return options?.onSuccess?.(_data, variables, data);
},
},
);
};
/**
* Hook for uploading an agent avatar
*/
export const useUploadAgentAvatarMutation = (
options?: t.UploadAgentAvatarOptions,
): UseMutationResult<
t.Agent, // response data
unknown, // error
t.AgentAvatarVariables, // request
unknown // context
> => {
return useMutation([MutationKeys.agentAvatarUpload], {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
mutationFn: ({ postCreation, ...variables }: t.AgentAvatarVariables) =>
dataService.uploadAgentAvatar(variables),
...(options || {}),
});
};
/**
* Hook for updating Agent Actions
*/
export const useUpdateAgentAction = (
options?: t.UpdateAgentActionOptions,
): UseMutationResult<
t.UpdateAgentActionResponse, // response data
unknown, // error
t.UpdateAgentActionVariables, // request
unknown // context
> => {
const queryClient = useQueryClient();
return useMutation([MutationKeys.updateAgentAction], {
mutationFn: (variables: t.UpdateAgentActionVariables) =>
dataService.updateAgentAction(variables),
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (updateAgentActionResponse, variables, context) => {
const listRes = queryClient.getQueryData<t.AgentListResponse>([
QueryKeys.agents,
defaultOrderQuery,
]);
if (!listRes) {
return options?.onSuccess?.(updateAgentActionResponse, variables, context);
}
const updatedAgent = updateAgentActionResponse[0];
queryClient.setQueryData<t.AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
...listRes,
data: listRes.data.map((agent) => {
if (agent.id === variables.agent_id) {
return updatedAgent;
}
return agent;
}),
});
queryClient.setQueryData<t.Action[]>([QueryKeys.actions], (prev) => {
return prev
?.map((action) => {
if (action.action_id === variables.action_id) {
return updateAgentActionResponse[1];
}
return action;
})
.concat(
variables.action_id != null && variables.action_id
? []
: [updateAgentActionResponse[1]],
);
});
return options?.onSuccess?.(updateAgentActionResponse, variables, context);
},
});
};
/**
* Hook for deleting an Agent Action
*/
export const useDeleteAgentAction = (
options?: t.DeleteAgentActionOptions,
): UseMutationResult<void, Error, t.DeleteAgentActionVariables, unknown> => {
const queryClient = useQueryClient();
return useMutation([MutationKeys.deleteAgentAction], {
mutationFn: (variables: t.DeleteAgentActionVariables) => {
return dataService.deleteAgentAction({
...variables,
});
},
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (_data, variables, context) => {
let domain: string | undefined = '';
queryClient.setQueryData<t.Action[]>([QueryKeys.actions], (prev) => {
return prev?.filter((action) => {
domain = action.metadata.domain;
return action.action_id !== variables.action_id;
});
});
queryClient.setQueryData<t.AgentListResponse>(
[QueryKeys.agents, defaultOrderQuery],
(prev) => {
if (!prev) {
return prev;
}
return {
...prev,
data: prev.data.map((agent) => {
if (agent.id === variables.agent_id) {
return {
...agent,
tools: agent.tools?.filter((tool) => !tool.includes(domain ?? '')),
};
}
return agent;
}),
};
},
);
return options?.onSuccess?.(_data, variables, context);
},
});
};
/**
* Hook for verifying email address
*/

View File

@@ -8,7 +8,7 @@ import useLocalize from '../useLocalize';
import useNewConvo from '../useNewConvo';
export default function useArchiveHandler(
conversationId: string,
conversationId: string | null,
shouldArchive: boolean,
retainView: () => void,
) {
@@ -19,18 +19,22 @@ export default function useArchiveHandler(
const { refreshConversations } = useConversations();
const { conversationId: currentConvoId } = useParams();
const archiveConvoMutation = useArchiveConversationMutation(conversationId);
const archiveConvoMutation = useArchiveConversationMutation(conversationId ?? '');
return async (e?: MouseEvent | FocusEvent | KeyboardEvent) => {
if (e) {
e.preventDefault();
}
const convoId = conversationId ?? '';
if (!convoId) {
return;
}
const label = shouldArchive ? 'archive' : 'unarchive';
archiveConvoMutation.mutate(
{ conversationId, isArchived: shouldArchive },
{ conversationId: convoId, isArchived: shouldArchive },
{
onSuccess: () => {
if (currentConvoId === conversationId || currentConvoId === 'new') {
if (currentConvoId === convoId || currentConvoId === 'new') {
newConversation();
navigate('/c/new', { replace: true });
}

View File

@@ -1,42 +1,76 @@
import { useState, useMemo } from 'react';
import { useDrop } from 'react-dnd';
import { useRecoilValue } from 'recoil';
import { NativeTypes } from 'react-dnd-html5-backend';
import { useQueryClient } from '@tanstack/react-query';
import {
isAgentsEndpoint,
EModelEndpoint,
AgentCapabilities,
QueryKeys,
} from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import type { DropTargetMonitor } from 'react-dnd';
import useFileHandling from './useFileHandling';
import store from '~/store';
export default function useDragHelpers() {
const { files, handleFiles } = useFileHandling();
const queryClient = useQueryClient();
const { handleFiles } = useFileHandling();
const [showModal, setShowModal] = useState(false);
const [draggedFiles, setDraggedFiles] = useState<File[]>([]);
const conversation = useRecoilValue(store.conversationByIndex(0)) || undefined;
const handleOptionSelect = (toolResource: string | undefined) => {
handleFiles(draggedFiles, toolResource);
setShowModal(false);
setDraggedFiles([]);
};
const isAgents = useMemo(
() => isAgentsEndpoint(conversation?.endpoint),
[conversation?.endpoint],
);
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept: [NativeTypes.FILE],
drop(item: { files: File[] }) {
console.log('drop', item.files);
handleFiles(item.files);
},
canDrop() {
// console.log('canDrop', item.files, item.items);
return true;
},
// hover() {
// // console.log('hover', item.files, item.items);
// },
collect: (monitor: DropTargetMonitor) => {
// const item = monitor.getItem() as File[];
// if (item) {
// console.log('collect', item.files, item.items);
// }
if (!isAgents) {
handleFiles(item.files);
return;
}
return {
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
};
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
const agentsConfig = endpointsConfig?.[EModelEndpoint.agents];
const codeEnabled =
agentsConfig?.capabilities?.includes(AgentCapabilities.execute_code) === true;
const fileSearchEnabled =
agentsConfig?.capabilities?.includes(AgentCapabilities.file_search) === true;
if (!codeEnabled && !fileSearchEnabled) {
handleFiles(item.files);
return;
}
setDraggedFiles(item.files);
setShowModal(true);
},
canDrop: () => true,
collect: (monitor: DropTargetMonitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[files],
[],
);
return {
canDrop,
isOver,
drop,
showModal,
setShowModal,
draggedFiles,
handleOptionSelect,
};
}

View File

@@ -187,8 +187,9 @@ const useFileHandling = (params?: UseFileHandling) => {
if (!agent_id) {
formData.append('message_file', 'true');
}
if (toolResource != null) {
formData.append('tool_resource', toolResource);
const tool_resource = extendedFile.tool_resource ?? toolResource;
if (tool_resource != null) {
formData.append('tool_resource', tool_resource);
}
if (conversation?.agent_id != null && formData.get('agent_id') == null) {
formData.append('agent_id', conversation.agent_id);
@@ -327,7 +328,7 @@ const useFileHandling = (params?: UseFileHandling) => {
img.src = preview;
};
const handleFiles = async (_files: FileList | File[]) => {
const handleFiles = async (_files: FileList | File[], _toolResource?: string) => {
abortControllerRef.current = new AbortController();
const fileList = Array.from(_files);
/* Validate files */
@@ -358,9 +359,22 @@ const useFileHandling = (params?: UseFileHandling) => {
size: originalFile.size,
};
if (_toolResource != null && _toolResource !== '') {
extendedFile.tool_resource = _toolResource;
}
const isImage = originalFile.type.split('/')[0] === 'image';
const tool_resource =
extendedFile.tool_resource ?? params?.additionalMetadata?.tool_resource ?? toolResource;
if (isAgentsEndpoint(endpoint) && !isImage && tool_resource == null) {
/** Note: this needs to be removed when we can support files to providers */
setError('com_error_files_unsupported_capability');
continue;
}
addFile(extendedFile);
if (originalFile.type.split('/')[0] === 'image') {
if (isImage) {
loadImage(extendedFile, preview);
continue;
}

View File

@@ -862,4 +862,47 @@ export default {
com_nav_setting_chat: 'دردشة',
com_nav_no_search_results: 'لم يتم العثور على نتائج البحث',
com_nav_setting_speech: 'الكلام',
com_ui_enter_api_key: 'أدخل مفتاح API',
com_ui_librechat_code_api_title: 'تشغيل كود الذكاء الاصطناعي',
com_ui_librechat_code_api_subtitle: 'آمن. متعدد اللغات. ملفات الإدخال/الإخراج.',
com_ui_librechat_code_api_key: 'احصل على مفتاح واجهة برمجة التطبيقات لمترجم الكود LibreChat',
com_error_files_unsupported_capability: 'لا توجد قدرات مفعّلة تدعم هذا النوع من الملفات.',
com_sidepanel_select_agent: 'اختر وكيلاً',
com_error_invalid_action_error: 'تم رفض الطلب: نطاق الإجراء المحدد غير مسموح به',
com_agents_code_interpreter_title: 'واجهة برمجة مُفسِّر الشفرة',
com_agents_by_librechat: 'بواسطة LibreChat',
com_agents_code_interpreter: 'عند التمكين، يسمح للوكيل الخاص بك باستخدام واجهة برمجة التطبيقات لمفسر الشفرة LibreChat لتشغيل الشفرة المُنشأة، بما في ذلك معالجة الملفات، بشكل آمن. يتطلب مفتاح API صالح.',
com_ui_export_convo_modal: 'نافذة تصدير المحادثة',
com_ui_endpoints_available: 'نقاط النهاية المتاحة',
com_ui_endpoint_menu: 'قائمة نقطة نهاية LLM',
com_ui_reset_var: 'إعادة تعيين {0}',
com_ui_select_search_provider: 'ابحث عن مزود الخدمة باسمه',
com_ui_upload_type: 'اختر نوع التحميل',
com_ui_llm_menu: 'قائمة النماذج اللغوية',
com_ui_llms_available: 'نماذج الذكاء الاصطناعي المتاحة',
com_ui_upload_image_input: 'تحميل صورة',
com_ui_upload_file_search: 'تحميل للبحث في الملفات',
com_ui_upload_code_files: 'تحميل لمفسر الكود',
com_ui_zoom: 'تكبير',
com_ui_role_select: 'الدور',
com_ui_admin_access_warning: 'قد يؤدي تعطيل وصول المسؤول إلى هذه الميزة إلى مشاكل غير متوقعة في واجهة المستخدم تتطلب تحديث الصفحة. في حالة الحفظ، الطريقة الوحيدة للتراجع هي عبر إعداد الواجهة في ملف librechat.yaml والذي يؤثر على جميع الأدوار.',
com_ui_run_code_error: 'حدث خطأ أثناء تشغيل الكود',
com_ui_duplication_success: 'تم نسخ المحادثة بنجاح',
com_ui_duplication_processing: 'جارِ نسخ المحادثة...',
com_ui_logo: 'شعار {0}',
com_ui_duplication_error: 'حدث خطأ أثناء نسخ المحادثة',
com_ui_agents_allow_share_global: 'السماح بمشاركة الوكلاء مع جميع المستخدمين',
com_ui_agents_allow_use: 'السماح باستخدام الوكلاء',
com_ui_agents_allow_create: 'السماح بإنشاء الوكلاء',
com_ui_agent_duplicated: 'تم نسخ العميل بنجاح',
com_ui_agent_duplicate_error: 'حدث خطأ أثناء نسخ المساعد',
com_ui_duplicate: 'نسخ',
com_ui_schema: 'المخطط',
com_ui_more_info: 'مزيد من المعلومات',
com_ui_privacy_policy_url: 'رابط سياسة الخصوصية',
com_ui_duplicate_agent_confirm: 'هل أنت متأكد من رغبتك في نسخ هذا المساعد؟',
com_endpoint_agent_placeholder: 'يرجى تحديد الوكيل',
com_ui_enter_openapi_schema: 'أدخل مخطط OpenAPI هنا',
com_ui_delete_shared_link: 'حذف الرابط المشترك؟',
com_nav_welcome_agent: 'الرجاء اختيار مساعد',
};

View File

@@ -892,4 +892,51 @@ export default {
com_nav_lang_indonesia: 'Indonesia',
com_nav_lang_hebrew: 'עברית',
com_nav_lang_finnish: 'Suomi',
com_ui_enter_api_key: 'API-Schlüssel eingeben',
com_ui_librechat_code_api_subtitle: 'Sicher. Mehrsprachig. Ein-/Ausgabedateien.',
com_ui_librechat_code_api_title: 'KI-Code ausführen',
com_error_invalid_action_error:
'Anfrage verweigert: Die angegebene Aktionsdomäne ist nicht zulässig',
com_ui_librechat_code_api_key: 'Hole dir deinen LibreChat Code Interpreter API-Schlüssel',
com_error_files_unsupported_capability:
'Keine aktivierten Funktionen unterstützen diesen Dateityp',
com_sidepanel_select_agent: 'Wähle einen Agenten',
com_agents_by_librechat: 'von LibreChat',
com_agents_code_interpreter_title: 'Code-Interpreter-API',
com_ui_endpoint_menu: 'LLM-Endpunkt-Menü',
com_agents_code_interpreter:
'Wenn aktiviert, ermöglicht es deinem Agenten, die LibreChat Code Interpreter API zu nutzen, um generierten Code sicher auszuführen, einschließlich der Verarbeitung von Dateien. Erfordert einen gültigen API-Schlüssel.',
com_ui_export_convo_modal: 'Konversation exportieren',
com_ui_endpoints_available: 'Verfügbare Endpunkte',
com_ui_llms_available: 'Verfügbare LLMs',
com_ui_llm_menu: 'LLM-Menü',
com_ui_reset_var: '{0} zurücksetzen',
com_ui_select_search_provider: 'Provider nach Name suchen',
com_ui_upload_file_search: 'Hochladen für Dateisuche',
com_ui_upload_type: 'Upload-Typ auswählen',
com_ui_upload_image_input: 'Bild hochladen',
com_ui_upload_code_files: 'Hochladen für Code-Interpreter',
com_ui_admin_access_warning:
'Das Deaktivieren des Admin-Zugriffs auf diese Funktion kann zu unerwarteten Problemen in der Benutzeroberfläche führen, die ein Neuladen erfordern. Nach dem Speichern kann dies nur über die Schnittstelleneinstellung in der librechat.yaml-Konfiguration rückgängig gemacht werden, was sich auf alle Rollen auswirkt.',
com_ui_zoom: 'Zoom',
com_ui_role_select: 'Rolle auswählen',
com_ui_duplication_processing: 'Konversation wird dupliziert...',
com_ui_duplication_success: 'Unterhaltung erfolgreich dupliziert',
com_ui_run_code_error: 'Bei der Ausführung des Codes ist ein Fehler aufgetreten',
com_ui_duplication_error: 'Beim Duplizieren der Konversation ist ein Fehler aufgetreten',
com_ui_logo: '{0} Logo',
com_ui_agents_allow_share_global: 'Assistenten für alle Nutzenden freigeben',
com_ui_agents_allow_use: 'Verwendung von Agenten erlauben',
com_ui_agents_allow_create: 'Erstellung von Assistenten erlauben',
com_ui_agent_duplicated: 'Agent wurde erfolgreich dupliziert',
com_ui_agent_duplicate_error: 'Beim Duplizieren des Assistenten ist ein Fehler aufgetreten',
com_ui_more_info: 'Mehr Infos',
com_ui_duplicate: 'Duplizieren',
com_ui_schema: 'Schema',
com_ui_enter_openapi_schema: 'Gib hier dein OpenAPI-Schema ein',
com_ui_privacy_policy_url: 'Datenschutzrichtlinie-URL',
com_ui_duplicate_agent_confirm: 'Möchtest du diesen Assistenten wirklich duplizieren?',
com_nav_welcome_agent: 'Bitte wähle einen Agenten',
com_endpoint_agent_placeholder: 'Bitte wähle einen Agenten aus',
com_ui_delete_shared_link: 'Geteilten Link löschen?',
};

View File

@@ -42,6 +42,7 @@ export default {
com_error_files_dupe: 'Duplicate file detected.',
com_error_files_validation: 'An error occurred while validating the file.',
com_error_files_process: 'An error occurred while processing the file.',
com_error_files_unsupported_capability: 'No capabilities enabled that support this file type.',
com_error_files_upload: 'An error occurred while uploading the file.',
com_error_files_upload_canceled:
'The file upload request was canceled. Note: the file upload may still be processing and will need to be manually deleted.',
@@ -187,6 +188,7 @@ export default {
com_ui_provider: 'Provider',
com_ui_model: 'Model',
com_ui_region: 'Region',
com_ui_reset_var: 'Reset {0}',
com_ui_model_parameters: 'Model Parameters',
com_ui_model_save_success: 'Model parameters saved successfully',
com_ui_select_model: 'Select a model',
@@ -202,6 +204,7 @@ export default {
com_ui_next: 'Next',
com_ui_stop: 'Stop',
com_ui_upload_files: 'Upload files',
com_ui_upload_type: 'Select Upload Type',
com_ui_upload_image_input: 'Upload Image',
com_ui_upload_file_search: 'Upload for File Search',
com_ui_upload_code_files: 'Upload for Code Interpreter',
@@ -297,6 +300,9 @@ export default {
com_ui_mention: 'Mention an endpoint, assistant, or preset to quickly switch to it',
com_ui_add_model_preset: 'Add a model or preset for an additional response',
com_assistants_max_starters_reached: 'Max number of conversation starters reached',
com_ui_duplication_success: 'Successfully duplicated conversation',
com_ui_duplication_processing: 'Duplicating conversation...',
com_ui_duplication_error: 'There was an error duplicating the conversation',
com_ui_regenerate: 'Regenerate',
com_ui_continue: 'Continue',
com_ui_edit: 'Edit',
@@ -361,6 +367,8 @@ export default {
com_ui_agents_allow_share_global: 'Allow sharing Agents to all users',
com_ui_agents_allow_use: 'Allow using Agents',
com_ui_agents_allow_create: 'Allow creating Agents',
com_ui_agent_duplicated: 'Agent duplicated successfully',
com_ui_agent_duplicate_error: 'There was an error duplicating the agent',
com_ui_prompt_already_shared_to_all: 'This prompt is already shared to all users',
com_ui_description_placeholder: 'Optional: Enter a description to display for the prompt',
com_ui_command_placeholder: 'Optional: Enter a command for the prompt or name will be used.',
@@ -390,6 +398,7 @@ export default {
'Are you sure you want to delete this Assistant? This cannot be undone.',
com_ui_rename: 'Rename',
com_ui_archive: 'Archive',
com_ui_duplicate: 'Duplicate',
com_ui_archive_error: 'Failed to archive conversation',
com_ui_unarchive: 'Unarchive',
com_ui_unarchive_error: 'Failed to unarchive conversation',
@@ -401,7 +410,10 @@ export default {
com_ui_locked: 'Locked',
com_ui_upload_delay:
'Uploading "{0}" is taking more time than anticipated. Please wait while the file finishes indexing for retrieval.',
com_ui_schema: 'Schema',
com_ui_enter_openapi_schema: 'Enter your OpenAPI schema here',
com_ui_privacy_policy: 'Privacy policy',
com_ui_privacy_policy_url: 'Privacy Policy URL',
com_ui_terms_of_service: 'Terms of service',
com_ui_use_micrphone: 'Use microphone',
com_ui_min_tags: 'Cannot remove more values, a minimum of {0} are required.',
@@ -425,6 +437,7 @@ export default {
com_ui_no_bookmarks: 'it seems like you have no bookmarks yet. Click on a chat and add a new one',
com_ui_no_conversation_id: 'No conversation ID found',
com_ui_add_multi_conversation: 'Add multi-conversation',
com_ui_duplicate_agent_confirm: 'Are you sure you want to duplicate this agent?',
com_auth_error_login:
'Unable to login with the information provided. Please check your credentials and try again.',
com_auth_error_login_rl:
@@ -736,6 +749,7 @@ export default {
com_nav_export_recursive: 'Recursive',
com_nav_export_conversation: 'Export conversation',
com_nav_export: 'Export',
com_ui_delete_shared_link: 'Delete shared link?',
com_nav_shared_links: 'Shared links',
com_nav_shared_links_manage: 'Manage',
com_nav_shared_links_empty: 'You have no shared links.',

View File

@@ -1148,4 +1148,51 @@ export default {
com_nav_setting_chat: 'Configuración del chat',
com_nav_setting_speech: 'Voz y habla',
com_ui_librechat_code_api_title: 'Ejecutar Código IA',
com_ui_librechat_code_api_subtitle: 'Seguro. Multilenguaje. Archivos de entrada/salida.',
com_ui_enter_api_key: 'Ingrese la clave API',
com_ui_librechat_code_api_key: 'Obtenga su clave API del Intérprete de Código de LibreChat',
com_error_invalid_action_error:
'Solicitud denegada: El dominio de acción especificado no está permitido.',
com_error_files_unsupported_capability:
'No hay capacidades habilitadas que admitan este tipo de archivo.',
com_sidepanel_select_agent: 'Seleccione un Agente',
com_agents_code_interpreter_title: 'API del Intérprete de Código',
com_agents_by_librechat: 'por LibreChat',
com_agents_code_interpreter:
'Cuando está habilitado, permite que su agente utilice la API del Intérprete de Código de LibreChat para ejecutar código generado de manera segura, incluyendo el procesamiento de archivos. Requiere una clave de API válida.',
com_ui_endpoint_menu: 'Menú de Punto de Conexión LLM',
com_ui_endpoints_available: 'Puntos de conexión disponibles',
com_ui_export_convo_modal: 'Exportar Conversación',
com_ui_llms_available: 'Modelos LLM disponibles',
com_ui_llm_menu: 'Menú de LLM',
com_ui_reset_var: 'Restablecer {0}',
com_ui_select_search_provider: 'Buscar proveedor por nombre',
com_ui_upload_type: 'Seleccionar tipo de carga',
com_ui_upload_image_input: 'Subir imagen',
com_ui_upload_file_search: 'Subir para búsqueda de archivos',
com_ui_upload_code_files: 'Subir archivo para el Intérprete de Código',
com_ui_role_select: 'Rol',
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_run_code_error: 'Se produjo un error al ejecutar el código',
com_ui_zoom: 'Zoom',
com_ui_duplication_success: 'Conversación duplicada exitosamente',
com_ui_duplication_processing: 'Duplicando conversación...',
com_ui_duplication_error: 'Hubo un error al duplicar la conversación',
com_ui_logo: 'Logotipo de {0}',
com_ui_agents_allow_share_global: 'Permitir compartir Agentes con todos los usuarios',
com_ui_agents_allow_create: 'Permitir la creación de Agentes',
com_ui_agents_allow_use: 'Permitir el uso de Agentes',
com_ui_agent_duplicate_error: 'Se produjo un error al duplicar el asistente',
com_ui_agent_duplicated: 'Agente duplicado exitosamente',
com_ui_duplicate: 'Duplicar',
com_ui_more_info: 'Más información',
com_ui_schema: 'Esquema',
com_ui_enter_openapi_schema: 'Ingrese su esquema OpenAPI aquí',
com_ui_privacy_policy_url: 'URL de la Política de Privacidad',
com_ui_duplicate_agent_confirm: '¿Está seguro de que desea duplicar este Asistente?',
com_endpoint_agent_placeholder: 'Por favor seleccione un Agente',
com_nav_welcome_agent: 'Seleccione un agente',
com_ui_delete_shared_link: '¿Eliminar enlace compartido?',
};

View File

@@ -910,4 +910,51 @@ export default {
'Personnaliser les commandes disponibles dans la conversation',
com_nav_no_search_results: 'Aucun résultat de recherche trouvé',
com_nav_setting_chat: 'Chat',
com_ui_enter_api_key: 'Saisir la clé API',
com_ui_librechat_code_api_title: 'Exécuter le code IA',
com_ui_librechat_code_api_subtitle: 'Sécurisé. Multilingue. Fichiers d\'entrée/sortie.',
com_ui_librechat_code_api_key: 'Obtenir votre clé API pour l\'interpréteur de code LibreChat',
com_error_invalid_action_error:
'Requête refusée : le domaine d\'action spécifié n\'est pas autorisé.',
com_error_files_unsupported_capability:
'Aucune capacité activée ne prend en charge ce type de fichier.',
com_sidepanel_select_agent: 'Sélectionner un assistant',
com_agents_by_librechat: 'par LibreChat',
com_agents_code_interpreter_title: 'API d\'interpréteur de code',
com_ui_endpoint_menu: 'Menu des points de terminaison LLM',
com_agents_code_interpreter:
'Lorsqu\'activé, permet à votre agent d\'utiliser l\'API d\'interpréteur de code LibreChat pour exécuter du code généré de manière sécurisée, y compris le traitement de fichiers. Nécessite une clé API valide.',
com_ui_export_convo_modal: 'Exporter la conversation',
com_ui_llms_available: 'LLMs disponibles',
com_ui_endpoints_available: 'Points de terminaison disponibles',
com_ui_llm_menu: 'Menu LLM',
com_ui_select_search_provider: 'Rechercher un fournisseur par nom',
com_ui_reset_var: 'Réinitialiser {0}',
com_ui_upload_type: 'Sélectionner le type de téléversement',
com_ui_upload_file_search: 'Téléverser pour la recherche de fichiers',
com_ui_upload_image_input: 'Téléverser une image',
com_ui_upload_code_files: 'Téléverser pour l\'Interpréteur de Code',
com_ui_role_select: 'Sélectionner un rôle',
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_zoom: 'Zoom',
com_ui_run_code_error: 'Une erreur s\'est produite lors de l\'exécution du code',
com_ui_duplication_success: 'Conversation dupliquée avec succès',
com_ui_duplication_error: 'Une erreur s\'est produite lors de la duplication de la conversation',
com_ui_duplication_processing: 'Duplication de la conversation en cours...',
com_ui_logo: 'Logo {0}',
com_ui_agents_allow_share_global: 'Autoriser le partage des Agents avec tous les utilisateurs',
com_ui_agents_allow_create: 'Autoriser la création d\'Agents',
com_ui_agents_allow_use: 'Autoriser l\'utilisation des Agents',
com_ui_agent_duplicated: 'Agent dupliqué avec succès',
com_ui_agent_duplicate_error: 'Une erreur s\'est produite lors de la duplication de l\'agent',
com_ui_duplicate: 'Dupliquer',
com_ui_more_info: 'Plus d\'informations',
com_ui_enter_openapi_schema: 'Saisissez votre schéma OpenAPI ici',
com_ui_schema: 'Schéma',
com_ui_duplicate_agent_confirm: 'Êtes-vous sûr de vouloir dupliquer cet agent ?',
com_ui_privacy_policy_url: 'URL de la politique de confidentialité',
com_endpoint_agent_placeholder: 'Veuillez sélectionner un Agent',
com_nav_welcome_agent: 'Veuillez sélectionner un agent',
com_ui_delete_shared_link: 'Supprimer le lien partagé ?',
};

View File

@@ -904,4 +904,51 @@ export default {
com_nav_no_search_results: 'Nessun risultato trovato',
com_nav_setting_chat: 'Chat',
com_nav_external: 'Esterno',
com_ui_librechat_code_api_title: 'Esegui Codice AI',
com_ui_enter_api_key: 'Inserisci API Key',
com_ui_librechat_code_api_subtitle: 'Sicuro. Multilingue. Gestione File.',
com_ui_librechat_code_api_key: 'Ottieni la tua chiave API per l\'Interprete di Codice LibreChat',
com_error_invalid_action_error:
'Richiesta negata: il dominio dell\'azione specificata non è consentito.',
com_error_files_unsupported_capability:
'Nessuna funzionalità abilitata che supporti questo tipo di file.',
com_sidepanel_select_agent: 'Seleziona un Agente',
com_agents_code_interpreter_title: 'API Interprete Codice',
com_agents_by_librechat: 'da LibreChat',
com_agents_code_interpreter:
'Quando abilitato, permette al tuo agente di utilizzare l\'API LibreChat Code Interpreter per eseguire codice generato in modo sicuro, inclusa l\'elaborazione dei file. Richiede una chiave API valida.',
com_ui_endpoint_menu: 'Menu Endpoint LLM',
com_ui_endpoints_available: 'Endpoint Disponibili',
com_ui_export_convo_modal: 'Esporta Conversazione',
com_ui_llms_available: 'LLM Disponibili',
com_ui_reset_var: 'Reimposta {0}',
com_ui_llm_menu: 'Menu LLM',
com_ui_select_search_provider: 'Cerca provider per nome',
com_ui_upload_type: 'Seleziona Tipo di Caricamento',
com_ui_upload_file_search: 'Carica per ricerca file',
com_ui_upload_image_input: 'Carica immagine',
com_ui_upload_code_files: 'Carica per l\'Interprete di Codice',
com_ui_role_select: 'Ruolo',
com_ui_admin_access_warning:
'La disattivazione dell\'accesso amministratore a questa funzionalità potrebbe causare problemi imprevisti all\'interfaccia utente che richiedono un aggiornamento. Una volta salvata, l\'unico modo per ripristinare è attraverso l\'impostazione dell\'interfaccia nel file di configurazione librechat.yaml, che influisce su tutti i ruoli.',
com_ui_zoom: 'Zoom',
com_ui_run_code_error: 'Si è verificato un errore durante l\'esecuzione del codice',
com_ui_duplication_success: 'Conversazione duplicata con successo',
com_ui_duplication_processing: 'Duplicazione conversazione in corso...',
com_ui_duplication_error: 'Si è verificato un errore durante la duplicazione della conversazione',
com_ui_logo: '{0} Logo',
com_ui_agents_allow_share_global: 'Consenti la condivisione degli Agenti con tutti gli utenti',
com_ui_agents_allow_use: 'Consenti utilizzo Agenti',
com_ui_agents_allow_create: 'Consenti creazione Agenti',
com_ui_agent_duplicated: 'Agente duplicato con successo',
com_ui_duplicate: 'Duplica',
com_ui_agent_duplicate_error: 'Si è verificato un errore durante la duplicazione dell\'assistente',
com_ui_more_info: 'Maggiori informazioni',
com_ui_schema: 'Schema',
com_ui_enter_openapi_schema: 'Inserisci qui il tuo schema OpenAPI',
com_ui_privacy_policy_url: 'URL Informativa Privacy',
com_ui_duplicate_agent_confirm: 'Sei sicuro di voler duplicare questo agente?',
com_nav_welcome_agent: 'Seleziona un Assistente',
com_endpoint_agent_placeholder: 'Seleziona un Agente',
com_ui_delete_shared_link: 'Eliminare il link condiviso?',
};

View File

@@ -859,4 +859,50 @@ export default {
com_nav_no_search_results: '検索結果が見つかりません',
com_nav_chat_commands_info:
'メッセージの先頭に特定の文字を入力することで、これらのコマンドが有効になります。各コマンドは、決められた文字(プレフィックス)で起動します。メッセージの先頭にこれらの文字をよく使用する場合は、コマンド機能を無効にすることができます。',
com_ui_librechat_code_api_title: 'AIコードを実行',
com_ui_librechat_code_api_subtitle: 'セキュア。多言語対応。ファイル入出力。',
com_ui_librechat_code_api_key: 'LibreChat コードインタープリター APIキーを取得',
com_ui_enter_api_key: 'APIキーを入力',
com_error_invalid_action_error:
'リクエストが拒否されました:指定されたアクションドメインは許可されていません。',
com_sidepanel_select_agent: 'エージェントを選択',
com_error_files_unsupported_capability: 'このファイル形式に対応する機能が有効になっていません',
com_agents_code_interpreter_title: 'コードインタープリター API',
com_agents_by_librechat: 'LibreChatより',
com_agents_code_interpreter:
'有効にすると、エージェントがLibreChat Code Interpreter APIを使用して、ファイル処理を含む生成されたコードを安全に実行できるようになります。有効な APIキーが必要です。',
com_ui_endpoints_available: '利用可能なエンドポイント',
com_ui_endpoint_menu: 'LLMエンドポイントメニュー',
com_ui_llm_menu: 'LLMメニュー',
com_ui_export_convo_modal: 'エクスポート',
com_ui_select_search_provider: 'プロバイダー名で検索',
com_ui_upload_type: 'アップロード種別を選択',
com_ui_upload_image_input: '画像をアップロード',
com_ui_reset_var: '{0}をリセット',
com_ui_llms_available: '利用可能なLLM',
com_ui_upload_code_files: 'コードインタープリター用にアップロード',
com_ui_zoom: 'ズーム',
com_ui_upload_file_search: 'ファイル検索用アップロード',
com_ui_role_select: '役割',
com_ui_admin_access_warning:
'管理者アクセスをこの機能で無効にすると、予期せぬUI上の問題が発生し、画面の再読み込みが必要になる場合があります。設定を保存した場合、元に戻すには librechat.yaml の設定ファイルを直接編集する必要があり、この変更はすべての権限に影響します。',
com_ui_run_code_error: 'コードの実行中にエラーが発生しました',
com_ui_duplication_success: '会話の複製が完了しました',
com_ui_duplication_processing: '会話を複製中...',
com_ui_duplication_error: '会話の複製中にエラーが発生しました',
com_ui_logo: '{0}のロゴ',
com_ui_agents_allow_share_global: '全ユーザーとAgentsの共有を許可',
com_ui_agents_allow_create: 'エージェントの作成を許可',
com_ui_agent_duplicate_error: 'アシスタントの複製中にエラーが発生しました',
com_ui_agent_duplicated: 'アシスタントを複製しました',
com_ui_duplicate: '複製',
com_ui_enter_openapi_schema: 'OpenAPIスキーマを入力してください',
com_ui_schema: 'スキーマ',
com_ui_agents_allow_use: 'エージェントの使用を許可',
com_ui_more_info: '詳細',
com_ui_privacy_policy_url: 'プライバシーポリシーURL',
com_endpoint_agent_placeholder: 'エージェントを選択してください',
com_ui_duplicate_agent_confirm: 'このエージェントを複製しますか?',
com_nav_welcome_agent: 'エージェントを選択してください',
com_ui_delete_shared_link: '共有リンクを削除しますか?',
};

View File

@@ -1098,4 +1098,49 @@ export default {
com_nav_no_search_results: '검색 결과가 없습니다',
com_nav_chat_commands: '채팅 명령어',
com_ui_enter_api_key: 'API 키 입력',
com_ui_librechat_code_api_subtitle: '안전한 보안. 다국어 지원. 파일 입출력.',
com_ui_librechat_code_api_title: 'AI 코드 실행',
com_ui_librechat_code_api_key: 'LibreChat 코드 인터프리터 API 키 받기',
com_error_files_unsupported_capability: '이 파일 형식을 지원하는 기능이 활성화되어 있지 않습니다',
com_error_invalid_action_error: '요청이 거부되었습니다: 지정된 작업 도메인이 허용되지 않습니다',
com_sidepanel_select_agent: '에이전트 선택',
com_agents_by_librechat: 'LibreChat 제공',
com_agents_code_interpreter_title: '코드 인터프리터 API',
com_agents_code_interpreter:
'활성화하면 에이전트가 LibreChat 코드 인터프리터 API를 사용하여 파일 처리를 포함한 생성된 코드를 안전하게 실행할 수 있습니다. 유효한 API 키가 필요합니다.',
com_ui_endpoints_available: '사용 가능한 엔드포인트',
com_ui_endpoint_menu: 'LLM 엔드포인트 메뉴',
com_ui_export_convo_modal: '대화 내보내기',
com_ui_llms_available: '사용 가능한 LLM',
com_ui_llm_menu: 'LLM 메뉴',
com_ui_reset_var: '{0} 초기화',
com_ui_select_search_provider: '이름으로 공급자 검색',
com_ui_upload_type: '업로드 유형 선택',
com_ui_upload_image_input: '이미지 업로드',
com_ui_upload_file_search: '파일 검색용 업로드',
com_ui_upload_code_files: '코드 인터프리터용 파일 업로드',
com_ui_role_select: '역할',
com_ui_admin_access_warning:
'관리자 접근 권한을 비활성화하면 예기치 않은 UI 문제가 발생할 수 있으며 페이지 새로고침이 필요할 수 있습니다. 저장하면 librechat.yaml 설정 파일에서 모든 역할에 영향을 미치는 인터페이스 설정을 통해서만 되돌릴 수 있습니다.',
com_ui_zoom: '확대/축소',
com_ui_duplication_success: '대화가 성공적으로 복제되었습니다',
com_ui_run_code_error: '코드 실행 중 오류가 발생했습니다',
com_ui_duplication_error: '대화를 복제하는 중 오류가 발생했습니다',
com_ui_duplication_processing: '대화 복제 중...',
com_ui_logo: '{0} 로고',
com_ui_agents_allow_share_global: '에이전트를 모든 사용자와 공유 허용',
com_ui_agents_allow_create: '에이전트 생성 허용',
com_ui_agent_duplicate_error: '에이전트 복제 중 오류가 발생했습니다',
com_ui_agent_duplicated: '에이전트가 성공적으로 복제되었습니다',
com_ui_agents_allow_use: '에이전트 사용 허용',
com_ui_duplicate: '복제',
com_ui_schema: '스키마',
com_ui_enter_openapi_schema: 'OpenAPI 스키마를 입력하세요',
com_ui_privacy_policy_url: '개인정보 보호정책 URL',
com_ui_more_info: '자세히 보기',
com_endpoint_agent_placeholder: '에이전트를 선택해 주세요',
com_ui_duplicate_agent_confirm: '이 에이전트를 복제하시겠습니까?',
com_nav_welcome_agent: '에이전트를 선택해 주세요',
com_ui_delete_shared_link: '공유 링크를 삭제하시겠습니까?',
};

View File

@@ -1124,4 +1124,49 @@ export default {
com_nav_no_search_results: 'Ничего не найдено',
com_nav_setting_speech: 'Голос',
com_ui_enter_api_key: 'Введите API-ключ',
com_ui_librechat_code_api_title: 'Запустить AI-код',
com_ui_librechat_code_api_key: 'Получить ключ API интерпретатора кода LibreChat',
com_error_invalid_action_error: 'Запрос отклонен: указанная область действий не разрешена.',
com_ui_librechat_code_api_subtitle: 'Безопасно. Многоязычно. Работа с файлами.',
com_error_files_unsupported_capability: 'Отсутствуют разрешения для работы с данным типом файлов',
com_agents_code_interpreter_title: 'API Интерпретатора кода',
com_sidepanel_select_agent: 'Выбрать Ассистента',
com_agents_by_librechat: 'от LibreChat',
com_agents_code_interpreter:
'При включении позволяет агенту использовать API интерпретатора кода LibreChat для безопасного выполнения сгенерированного кода, включая обработку файлов. Требуется действующий ключ API.',
com_ui_endpoints_available: 'Доступные эндпоинты',
com_ui_endpoint_menu: 'Меню настроек LLM',
com_ui_export_convo_modal: 'Экспорт беседы',
com_ui_select_search_provider: 'Поиск провайдера по названию',
com_ui_llm_menu: 'Меню LLM',
com_ui_llms_available: 'Доступные языковые модели',
com_ui_reset_var: 'Сбросить {0}',
com_ui_upload_type: 'Выберите тип загрузки',
com_ui_upload_image_input: 'Загрузить изображение',
com_ui_upload_file_search: 'Загрузить для поиска по файлам',
com_ui_upload_code_files: 'Загрузить для Интерпретатора кода',
com_ui_admin_access_warning:
'Отключение административного доступа к этой функции может вызвать непредвиденные проблемы с интерфейсом, требующие обновления страницы. После сохранения изменений вернуть настройку можно будет только через параметр interface в конфигурационном файле librechat.yaml, что повлияет на все роли.',
com_ui_role_select: 'Роль',
com_ui_zoom: 'Масштаб',
com_ui_run_code_error: 'Произошла ошибка при выполнении кода',
com_ui_duplication_success: 'Разговор успешно скопирован',
com_ui_duplication_processing: 'Создание копии беседы...',
com_ui_logo: 'Логотип {0}',
com_ui_duplication_error: 'Не удалось создать копию разговора',
com_ui_agents_allow_use: 'Разрешить использование ассистентов',
com_ui_agents_allow_share_global: 'Разрешить доступ к Агентам всем пользователям',
com_ui_agents_allow_create: 'Разрешить создание ассистентов',
com_ui_agent_duplicate_error: 'Произошла ошибка при дублировании ассистента',
com_ui_duplicate: 'Дублировать',
com_ui_agent_duplicated: 'Ассистент успешно скопирован',
com_ui_more_info: 'Подробнее',
com_ui_schema: 'Схема',
com_ui_privacy_policy_url: 'Ссылка на политику конфиденциальности',
com_endpoint_agent_placeholder: 'Выберите Агента',
com_ui_enter_openapi_schema: 'Введите вашу OpenAPI схему',
com_nav_welcome_agent: 'Выберите агента',
com_ui_duplicate_agent_confirm: 'Вы действительно хотите создать копию этого агента?',
com_ui_delete_shared_link: 'Удалить общую ссылку?',
};

View File

@@ -852,4 +852,49 @@ export default {
com_nav_chat_commands_info:
'这些命令通过在您的消息开头输入特定字符来激活。每个命令都由其指定的前缀触发。如果您经常在消息开头使用这些字符,可以选择禁用这些命令。',
com_ui_enter_api_key: '输入API密钥',
com_ui_librechat_code_api_title: '运行AI代码',
com_error_invalid_action_error: '请求被拒绝:不允许使用指定的操作域。',
com_ui_librechat_code_api_subtitle: '安全可靠。多语言支持。文件输入/输出。',
com_error_files_unsupported_capability: '未启用支持此类文件的功能',
com_sidepanel_select_agent: '选择助手',
com_ui_librechat_code_api_key: '获取您的 LibreChat 代码解释器 API 密钥',
com_agents_by_librechat: '由 LibreChat 提供',
com_agents_code_interpreter_title: '代码解释器 API',
com_agents_code_interpreter:
'启用后您的代理可以安全地使用LibreChat代码解释器API来运行生成的代码包括文件处理功能。需要有效的API密钥。',
com_ui_endpoint_menu: 'LLM 终端菜单',
com_ui_endpoints_available: '可用渠道',
com_ui_llms_available: '可用的LLM模型',
com_ui_export_convo_modal: '导出对话窗口',
com_ui_llm_menu: 'LLM菜单',
com_ui_select_search_provider: '以名称搜索服务商',
com_ui_upload_type: '选择上传类型',
com_ui_reset_var: '重置{0}',
com_ui_upload_image_input: '上传图片',
com_ui_upload_file_search: '上传文件搜索',
com_ui_role_select: '角色',
com_ui_upload_code_files: '上传代码解释器文件',
com_ui_zoom: '缩放',
com_ui_duplication_success: '成功复制对话',
com_ui_admin_access_warning:
'禁用管理员对此功能的访问可能会导致界面出现异常,需要刷新页面。如果保存此设置,唯一的恢复方式是通过 librechat.yaml 配置文件中的界面设置进行修改,这将影响所有角色。',
com_ui_duplication_processing: '正在复制对话...',
com_ui_run_code_error: '代码运行出错',
com_ui_duplication_error: '复制对话时出现错误',
com_ui_logo: '{0}标识',
com_ui_agents_allow_create: '允许创建助手',
com_ui_agents_allow_use: '允许使用助手',
com_ui_agents_allow_share_global: '允许与所有用户共享助手',
com_ui_agent_duplicated: '助手复制成功',
com_ui_agent_duplicate_error: '复制助手时发生错误',
com_ui_schema: '架构',
com_ui_duplicate: '复制',
com_ui_more_info: '更多信息',
com_ui_privacy_policy_url: '隐私政策链接',
com_ui_enter_openapi_schema: '请在此输入OpenAPI架构',
com_ui_duplicate_agent_confirm: '您确定要复制此助手吗?',
com_endpoint_agent_placeholder: '请选择代理',
com_ui_delete_shared_link: '删除分享链接?',
com_nav_welcome_agent: '请选择 Agent',
};

View File

@@ -829,4 +829,49 @@ export default {
com_nav_setting_chat: '聊天',
com_nav_setting_speech: '語音',
com_nav_command_settings: '指令設定',
com_ui_enter_api_key: '輸入 API 金鑰',
com_ui_librechat_code_api_title: '執行 AI 程式碼',
com_ui_librechat_code_api_subtitle: '安全性高。多語言支援。檔案輸入/輸出。',
com_ui_librechat_code_api_key: '取得你的 LibreChat 程式碼解譯器 API 金鑰',
com_error_invalid_action_error: '要求遭拒:指定的操作網域不被允許。',
com_sidepanel_select_agent: '選擇代理',
com_error_files_unsupported_capability: '未啟用支援此檔案類型的功能。',
com_agents_code_interpreter_title: '程式碼解譯器 API',
com_agents_by_librechat: '由 LibreChat 提供',
com_ui_endpoint_menu: '語言模型端點選單',
com_ui_endpoints_available: '可用選項',
com_agents_code_interpreter:
'啟用後,您的代理可以安全地使用 LibreChat 程式碼解譯器 API 來執行產生的程式碼,包括檔案處理功能。需要有效的 API 金鑰。',
com_ui_llms_available: '可用的 LLM 模型',
com_ui_export_convo_modal: '匯出對話視窗',
com_ui_llm_menu: 'LLM 選單',
com_ui_reset_var: '重設 {0}',
com_ui_select_search_provider: '依名稱搜尋供應商',
com_ui_upload_type: '選擇上傳類型',
com_ui_upload_image_input: '上傳圖片',
com_ui_upload_file_search: '上傳檔案以供搜尋',
com_ui_upload_code_files: '上傳程式碼解譯器檔案',
com_ui_role_select: '角色',
com_ui_zoom: '縮放',
com_ui_admin_access_warning:
'停用管理員對此功能的存取權限可能會導致意外的介面問題,需要重新整理頁面。若儲存此設定,唯一的還原方式是透過 librechat.yaml 設定檔中的介面設定,這會影響所有角色。',
com_ui_run_code_error: '執行程式碼時發生錯誤',
com_ui_duplication_processing: '正在複製對話...',
com_ui_logo: '{0} 標誌',
com_ui_duplication_success: '已成功複製對話',
com_ui_duplication_error: '複製對話時發生錯誤',
com_ui_agents_allow_use: '允許使用代理',
com_ui_agents_allow_share_global: '允許與所有使用者共享助理',
com_ui_agents_allow_create: '允許建立代理',
com_ui_agent_duplicate_error: '複製助理時發生錯誤',
com_ui_more_info: '更多資訊',
com_ui_duplicate: '複製',
com_ui_agent_duplicated: '已成功複製助理',
com_ui_schema: '綱要',
com_ui_privacy_policy_url: '隱私權政策網址',
com_ui_enter_openapi_schema: '在此輸入您的 OpenAPI 結構描述',
com_endpoint_agent_placeholder: '請選擇代理',
com_ui_duplicate_agent_confirm: '您確定要複製這個助理嗎?',
com_ui_delete_shared_link: '刪除共享連結?',
com_nav_welcome_agent: '請選擇代理',
};

View File

@@ -39,6 +39,7 @@
--font-size-xl: 1.25rem;
}
html {
--presentation: var(--white);
--text-primary: var(--gray-800);
--text-secondary: var(--gray-600);
--text-secondary-alt: var(--gray-500);
@@ -48,6 +49,7 @@ html {
--header-hover: var(--gray-50);
--header-button-hover: var(--gray-50);
--surface-active: var(--gray-100);
--surface-active-alt: var(--gray-200);
--surface-hover: var(--gray-200);
--surface-primary: var(--white);
--surface-primary-alt: var(--gray-50);
@@ -91,6 +93,7 @@ html {
--switch-unchecked: 0 0% 58%;
}
.dark {
--presentation: var(--gray-800);
--text-primary: var(--gray-100);
--text-secondary: var(--gray-300);
--text-secondary-alt: var(--gray-400);
@@ -99,6 +102,7 @@ html {
--header-hover: var(--gray-600);
--header-button-hover: var(--gray-700);
--surface-active: var(--gray-500);
--surface-active-alt: var(--gray-700);
--surface-hover: var(--gray-600);
--surface-primary: var(--gray-900);
--surface-primary-alt: var(--gray-850);
@@ -2369,7 +2373,7 @@ button.scroll-convo {
}
.popover-ui:where(.dark, .dark *) {
background-color: hsl(var(--background));
background-color: hsl(var(--secondary));
color: var(--text-secondary);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.25), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
@@ -2390,7 +2394,7 @@ button.scroll-convo {
}
.select-item[data-active-item] {
background-color: hsl(var(--accent));
background-color: var(--surface-hover);
color: var(--text-primary);
}

View File

@@ -61,6 +61,7 @@ module.exports = {
900: '#031f29',
},
'brand-purple': '#ab68ff',
'presentation': 'var(--presentation)',
'text-primary': 'var(--text-primary)',
'text-secondary': 'var(--text-secondary)',
'text-secondary-alt': 'var(--text-secondary-alt)',
@@ -70,6 +71,7 @@ module.exports = {
'header-hover': 'var(--header-hover)',
'header-button-hover': 'var(--header-button-hover)',
'surface-active': 'var(--surface-active)',
'surface-active-alt': 'var(--surface-active-alt)',
'surface-hover': 'var(--surface-hover)',
'surface-primary': 'var(--surface-primary)',
'surface-primary-alt': 'var(--surface-primary-alt)',

View File

@@ -9,6 +9,7 @@ const rootDir = path.resolve(__dirname, '..');
const directories = [
rootDir,
path.resolve(rootDir, 'packages', 'data-provider'),
path.resolve(rootDir, 'packages', 'mcp'),
path.resolve(rootDir, 'client'),
path.resolve(rootDir, 'api'),
];

View File

@@ -16,6 +16,7 @@ const rootDir = path.resolve(__dirname, '..');
const directories = [
rootDir,
path.resolve(rootDir, 'packages', 'data-provider'),
path.resolve(rootDir, 'packages', 'mcp'),
path.resolve(rootDir, 'client'),
path.resolve(rootDir, 'api'),
];

View File

@@ -1,3 +1,3 @@
// v0.7.5
// v0.7.6
// See .env.test.example for an example of the '.env.test' file.
require('dotenv').config({ path: './e2e/.env.test' });

View File

@@ -1,4 +1,4 @@
<!-- v0.7.5 -->
<!-- v0.7.6 -->
<!DOCTYPE html>
<html>
<head>

806
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "LibreChat",
"version": "v0.7.5",
"version": "v0.7.6",
"description": "",
"workspaces": [
"api",
@@ -36,7 +36,8 @@
"backend:dev": "cross-env NODE_ENV=development npx nodemon api/server/index.js",
"backend:stop": "node config/stop-backend.js",
"build:data-provider": "cd packages/data-provider && npm run build",
"frontend": "npm run build:data-provider && cd client && npm run build",
"build:mcp": "cd packages/mcp && npm run build",
"frontend": "npm run build:data-provider && npm run build:mcp && cd client && npm run build",
"frontend:ci": "npm run build:data-provider && cd client && npm run build:ci",
"frontend:dev": "cd client && npm run dev",
"e2e": "playwright test --config=e2e/playwright.config.local.ts",

View File

@@ -1,4 +1,5 @@
#!/bin/bash
# SCRIPT USED TO DETERMINE WHICH PACKAGE HAD CHANGES
# Set the directory containing the package.json file
dir=${1:-.}

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