Compare commits
16 Commits
refactor/o
...
v0.7.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b118d42de | ||
|
|
792ae03017 | ||
|
|
3fbbcb1cfe | ||
|
|
d68c874db4 | ||
|
|
f873587e5f | ||
|
|
000641c619 | ||
|
|
3ceb227507 | ||
|
|
22a87b6162 | ||
|
|
e8bde332c2 | ||
|
|
649c7a6032 | ||
|
|
d3cafeee96 | ||
|
|
18ad89be2c | ||
|
|
16eed5f32d | ||
|
|
e391347b9e | ||
|
|
0a97ad3915 | ||
|
|
6ef05dd2e6 |
@@ -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 #
|
||||
|
||||
40
.eslintrc.js
40
.eslintrc.js
@@ -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: {
|
||||
|
||||
5
.github/workflows/backend-review.yml
vendored
5
.github/workflows/backend-review.yml
vendored
@@ -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: |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# v0.7.5
|
||||
# v0.7.6
|
||||
|
||||
# Base node image
|
||||
FROM node:20-alpine AS node
|
||||
|
||||
@@ -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"]
|
||||
@@ -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**:
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
75
api/server/services/Config/getEndpointsConfig.js
Normal file
75
api/server/services/Config/getEndpointsConfig.js
Normal 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 };
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
57
api/server/services/MCP.js
Normal file
57
api/server/services/MCP.js
Normal 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,
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.7.5",
|
||||
"version": "v0.7.6",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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}
|
||||
|
||||
90
client/src/components/Chat/Input/Files/DragDropModal.tsx
Normal file
90
client/src/components/Chat/Input/Files/DragDropModal.tsx
Normal 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;
|
||||
29
client/src/components/Chat/Input/Files/DragDropWrapper.tsx
Normal file
29
client/src/components/Chat/Input/Files/DragDropWrapper.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useLocalize, useLocalStorage } from '~/hooks';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { TooltipAnchor } from '~/components/ui';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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={
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
50
client/src/components/SidePanel/Agents/DuplicateAgent.tsx
Normal file
50
client/src/components/SidePanel/Agents/DuplicateAgent.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './queries';
|
||||
export * from './mutations';
|
||||
|
||||
302
client/src/data-provider/Agents/mutations.ts
Normal file
302
client/src/data-provider/Agents/mutations.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: 'الرجاء اختيار مساعد',
|
||||
};
|
||||
|
||||
@@ -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?',
|
||||
};
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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?',
|
||||
};
|
||||
|
||||
@@ -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é ?',
|
||||
};
|
||||
|
||||
@@ -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?',
|
||||
};
|
||||
|
||||
@@ -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: '共有リンクを削除しますか?',
|
||||
};
|
||||
|
||||
@@ -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: '공유 링크를 삭제하시겠습니까?',
|
||||
};
|
||||
|
||||
@@ -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: 'Удалить общую ссылку?',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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: '請選擇代理',
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- v0.7.5 -->
|
||||
<!-- v0.7.6 -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
806
package-lock.json
generated
806
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user