Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c31309a1db Add comprehensive documentation for special variables feature
- Add detailed JSDoc comments to replaceSpecialVars function
- Create SPECIAL_VARIABLES.md with complete usage guide
- Document all available special variables including new local timezone ones
- Provide examples and technical implementation details
- Include migration guide for existing users

Co-authored-by: berry-13 <81851188+berry-13@users.noreply.github.com>
2025-10-21 21:34:15 +00:00
copilot-swe-agent[bot]
3c4c4d53dd Fix linting issues in timezone implementation
Co-authored-by: berry-13 <81851188+berry-13@users.noreply.github.com>
2025-10-21 21:32:01 +00:00
copilot-swe-agent[bot]
9fe4554a70 Add local timezone datetime support in agent prompts
- Add timezone support to replaceSpecialVars function with dayjs timezone plugin
- Add new special variables: {{local_datetime}} and {{local_date}}
- Update specialVariables config to include new variables
- Pass timezone from client via Intl.DateTimeFormat API
- Add timezone field to TPayload type
- Update all client-side replaceSpecialVars calls to include timezone
- Add comprehensive tests for timezone-aware special variables
- Update web_search tool context to include local datetime when timezone available

Co-authored-by: berry-13 <81851188+berry-13@users.noreply.github.com>
2025-10-21 21:26:26 +00:00
copilot-swe-agent[bot]
c1dcabae20 Initial plan 2025-10-21 21:04:56 +00:00
182 changed files with 773 additions and 2706 deletions

View File

@@ -1,4 +1,4 @@
# v0.8.1-rc1
# v0.8.0
# Base node image
FROM node:20-alpine AS node

View File

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

View File

@@ -2,7 +2,7 @@ const { z } = require('zod');
const axios = require('axios');
const { Ollama } = require('ollama');
const { sleep } = require('@librechat/agents');
const { resolveHeaders } = require('@librechat/api');
const { logAxiosError } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { Constants } = require('librechat-data-provider');
const { deriveBaseURL } = require('~/utils');
@@ -44,7 +44,6 @@ class OllamaClient {
constructor(options = {}) {
const host = deriveBaseURL(options.baseURL ?? 'http://localhost:11434');
this.streamRate = options.streamRate ?? Constants.DEFAULT_STREAM_RATE;
this.headers = options.headers ?? {};
/** @type {Ollama} */
this.client = new Ollama({ host });
}
@@ -52,32 +51,27 @@ class OllamaClient {
/**
* Fetches Ollama models from the specified base API path.
* @param {string} baseURL
* @param {Object} [options] - Optional configuration
* @param {Partial<IUser>} [options.user] - User object for header resolution
* @param {Record<string, string>} [options.headers] - Headers to include in the request
* @returns {Promise<string[]>} The Ollama models.
* @throws {Error} Throws if the Ollama API request fails
*/
static async fetchModels(baseURL, options = {}) {
static async fetchModels(baseURL) {
let models = [];
if (!baseURL) {
return models;
}
try {
const ollamaEndpoint = deriveBaseURL(baseURL);
/** @type {Promise<AxiosResponse<OllamaListResponse>>} */
const response = await axios.get(`${ollamaEndpoint}/api/tags`, {
timeout: 5000,
});
models = response.data.models.map((tag) => tag.name);
return models;
} catch (error) {
const logMessage =
"Failed to fetch models from Ollama API. If you are not using Ollama directly, and instead, through some aggregator or reverse proxy that handles fetching via OpenAI spec, ensure the name of the endpoint doesn't start with `ollama` (case-insensitive).";
logAxiosError({ message: logMessage, error });
return [];
}
const ollamaEndpoint = deriveBaseURL(baseURL);
const resolvedHeaders = resolveHeaders({
headers: options.headers,
user: options.user,
});
/** @type {Promise<AxiosResponse<OllamaListResponse>>} */
const response = await axios.get(`${ollamaEndpoint}/api/tags`, {
headers: resolvedHeaders,
timeout: 5000,
});
const models = response.data.models.map((tag) => tag.name);
return models;
}
/**

View File

@@ -5,7 +5,6 @@ const FormData = require('form-data');
const { ProxyAgent } = require('undici');
const { tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { logAxiosError, oaiToolkit } = require('@librechat/api');
const { ContentTypes, EImageOutputType } = require('librechat-data-provider');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
@@ -349,7 +348,16 @@ Error Message: ${error.message}`);
};
if (process.env.PROXY) {
axiosConfig.httpsAgent = new HttpsProxyAgent(process.env.PROXY);
try {
const url = new URL(process.env.PROXY);
axiosConfig.proxy = {
host: url.hostname.replace(/^\[|\]$/g, ''),
port: url.port ? parseInt(url.port, 10) : undefined,
protocol: url.protocol.replace(':', ''),
};
} catch (error) {
logger.error('Error parsing proxy URL:', error);
}
}
if (process.env.IMAGE_GEN_OAI_AZURE_API_VERSION && process.env.IMAGE_GEN_OAI_BASEURL) {

View File

@@ -327,7 +327,7 @@ const loadTools = async ({
const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {};
requestedTools[tool] = async () => {
toolContextMap[tool] = `# \`${tool}\`:
Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}
Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}${options.req?.body?.timezone ? `\nLocal Date & Time: ${replaceSpecialVars({ text: '{{local_datetime}}', timezone: options.req.body.timezone })}` : ''}
1. **Execute immediately without preface** when using \`${tool}\`.
2. **After the search, begin with a brief summary** that directly addresses the query without headers or explaining your process.
3. **Structure your response clearly** using Markdown formatting (Level 2 headers for sections, lists for multiple points, tables for comparisons).

View File

@@ -62,37 +62,25 @@ const getAgents = async (searchParameter) => await Agent.find(searchParameter).l
*
* @param {Object} params
* @param {ServerRequest} params.req
* @param {string} params.spec
* @param {string} params.agent_id
* @param {string} params.endpoint
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
*/
const loadEphemeralAgent = async ({ req, spec, agent_id, endpoint, model_parameters: _m }) => {
const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _m }) => {
const { model, ...model_parameters } = _m;
const modelSpecs = req.config?.modelSpecs?.list;
/** @type {TModelSpec | null} */
let modelSpec = null;
if (spec != null && spec !== '') {
modelSpec = modelSpecs?.find((s) => s.name === spec) || null;
}
/** @type {TEphemeralAgent | null} */
const ephemeralAgent = req.body.ephemeralAgent;
const mcpServers = new Set(ephemeralAgent?.mcp);
if (modelSpec?.mcpServers) {
for (const mcpServer of modelSpec.mcpServers) {
mcpServers.add(mcpServer);
}
}
/** @type {string[]} */
const tools = [];
if (ephemeralAgent?.execute_code === true || modelSpec?.executeCode === true) {
if (ephemeralAgent?.execute_code === true) {
tools.push(Tools.execute_code);
}
if (ephemeralAgent?.file_search === true || modelSpec?.fileSearch === true) {
if (ephemeralAgent?.file_search === true) {
tools.push(Tools.file_search);
}
if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) {
if (ephemeralAgent?.web_search === true) {
tools.push(Tools.web_search);
}
@@ -134,18 +122,17 @@ const loadEphemeralAgent = async ({ req, spec, agent_id, endpoint, model_paramet
*
* @param {Object} params
* @param {ServerRequest} params.req
* @param {string} params.spec
* @param {string} params.agent_id
* @param {string} params.endpoint
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
*/
const loadAgent = async ({ req, spec, agent_id, endpoint, model_parameters }) => {
const loadAgent = async ({ req, agent_id, endpoint, model_parameters }) => {
if (!agent_id) {
return null;
}
if (agent_id === EPHEMERAL_AGENT_ID) {
return await loadEphemeralAgent({ req, spec, agent_id, endpoint, model_parameters });
return await loadEphemeralAgent({ req, agent_id, endpoint, model_parameters });
}
const agent = await getAgent({
id: agent_id,

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
"version": "v0.8.1-rc1",
"version": "v0.8.0",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",
@@ -48,7 +48,7 @@
"@langchain/google-genai": "^0.2.13",
"@langchain/google-vertexai": "^0.2.13",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.90",
"@librechat/agents": "^2.4.85",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",

View File

@@ -8,7 +8,6 @@ const {
Tokenizer,
checkAccess,
logAxiosError,
sanitizeTitle,
resolveHeaders,
getBalanceConfig,
memoryInstructions,
@@ -776,7 +775,6 @@ class AgentClient extends BaseClient {
const agentsEConfig = appConfig.endpoints?.[EModelEndpoint.agents];
config = {
runName: 'AgentRun',
configurable: {
thread_id: this.conversationId,
last_agent_index: this.agentConfigs?.size ?? 0,
@@ -1235,10 +1233,6 @@ class AgentClient extends BaseClient {
handleLLMEnd,
},
],
configurable: {
thread_id: this.conversationId,
user_id: this.user ?? this.options.req.user?.id,
},
},
});
@@ -1276,7 +1270,7 @@ class AgentClient extends BaseClient {
);
});
return sanitizeTitle(titleResult.title);
return titleResult.title;
} catch (err) {
logger.error('[api/server/controllers/agents/client.js #titleConvo] Error', err);
return;

View File

@@ -10,10 +10,6 @@ jest.mock('@librechat/agents', () => ({
}),
}));
jest.mock('@librechat/api', () => ({
...jest.requireActual('@librechat/api'),
}));
describe('AgentClient - titleConvo', () => {
let client;
let mockRun;
@@ -256,38 +252,6 @@ describe('AgentClient - titleConvo', () => {
expect(result).toBe('Generated Title');
});
it('should sanitize the generated title by removing think blocks', async () => {
const titleWithThinkBlock = '<think>reasoning about the title</think> User Hi Greeting';
mockRun.generateTitle.mockResolvedValue({
title: titleWithThinkBlock,
});
const text = 'Test conversation text';
const abortController = new AbortController();
const result = await client.titleConvo({ text, abortController });
// Should remove the <think> block and return only the clean title
expect(result).toBe('User Hi Greeting');
expect(result).not.toContain('<think>');
expect(result).not.toContain('</think>');
});
it('should return fallback title when sanitization results in empty string', async () => {
const titleOnlyThinkBlock = '<think>only reasoning no actual title</think>';
mockRun.generateTitle.mockResolvedValue({
title: titleOnlyThinkBlock,
});
const text = 'Test conversation text';
const abortController = new AbortController();
const result = await client.titleConvo({ text, abortController });
// Should return the fallback title since sanitization would result in empty string
expect(result).toBe('Untitled Conversation');
});
it('should handle errors gracefully and return undefined', async () => {
mockRun.generateTitle.mockRejectedValue(new Error('Title generation failed'));

View File

@@ -57,7 +57,7 @@ async function loadConfigModels(req) {
for (let i = 0; i < customEndpoints.length; i++) {
const endpoint = customEndpoints[i];
const { models, name: configName, baseURL, apiKey, headers: endpointHeaders } = endpoint;
const { models, name: configName, baseURL, apiKey } = endpoint;
const name = normalizeEndpointName(configName);
endpointsMap[name] = endpoint;
@@ -76,8 +76,6 @@ async function loadConfigModels(req) {
apiKey: API_KEY,
baseURL: BASE_URL,
user: req.user.id,
userObject: req.user,
headers: endpointHeaders,
direct: endpoint.directEndpoint,
userIdQuery: models.userIdQuery,
});

View File

@@ -134,10 +134,10 @@ const initializeAgent = async ({
});
const tokensModel =
agent.provider === EModelEndpoint.azureOpenAI ? agent.model : options.llmConfig?.model;
const maxOutputTokens = optionalChainWithEmptyCheck(
options.llmConfig?.maxOutputTokens,
options.llmConfig?.maxTokens,
agent.provider === EModelEndpoint.azureOpenAI ? agent.model : modelOptions.model;
const maxTokens = optionalChainWithEmptyCheck(
modelOptions.maxOutputTokens,
modelOptions.maxTokens,
0,
);
const agentMaxContextTokens = optionalChainWithEmptyCheck(
@@ -185,6 +185,7 @@ const initializeAgent = async ({
agent.instructions = replaceSpecialVars({
text: agent.instructions,
user: req.user,
timezone: req.body?.timezone,
});
}
@@ -203,7 +204,7 @@ const initializeAgent = async ({
userMCPAuthMap,
toolContextMap,
useLegacyContent: !!options.useLegacyContent,
maxContextTokens: Math.round((agentMaxContextTokens - maxOutputTokens) * 0.9),
maxContextTokens: Math.round((agentMaxContextTokens - maxTokens) * 0.9),
};
};

View File

@@ -3,10 +3,9 @@ const { isAgentsEndpoint, removeNullishValues, Constants } = require('librechat-
const { loadAgent } = require('~/models/Agent');
const buildOptions = (req, endpoint, parsedBody, endpointType) => {
const { spec, iconURL, agent_id, ...model_parameters } = parsedBody;
const { spec, iconURL, agent_id, instructions, ...model_parameters } = parsedBody;
const agentPromise = loadAgent({
req,
spec,
agent_id: isAgentsEndpoint(endpoint) ? agent_id : Constants.EPHEMERAL_AGENT_ID,
endpoint,
model_parameters,
@@ -21,6 +20,7 @@ const buildOptions = (req, endpoint, parsedBody, endpointType) => {
endpoint,
agent_id,
endpointType,
instructions,
model_parameters,
agent: agentPromise,
});

View File

@@ -1,3 +1,4 @@
const { Providers } = require('@librechat/agents');
const {
resolveHeaders,
isUserProvided,
@@ -142,27 +143,39 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
if (optionsOnly) {
const modelOptions = endpointOption?.model_parameters ?? {};
clientOptions = Object.assign(
{
modelOptions,
},
clientOptions,
);
clientOptions.modelOptions.user = req.user.id;
const options = getOpenAIConfig(apiKey, clientOptions, endpoint);
if (options != null) {
options.useLegacyContent = true;
options.endpointTokenConfig = endpointTokenConfig;
}
if (!clientOptions.streamRate) {
if (endpoint !== Providers.OLLAMA) {
clientOptions = Object.assign(
{
modelOptions,
},
clientOptions,
);
clientOptions.modelOptions.user = req.user.id;
const options = getOpenAIConfig(apiKey, clientOptions, endpoint);
if (options != null) {
options.useLegacyContent = true;
options.endpointTokenConfig = endpointTokenConfig;
}
if (!clientOptions.streamRate) {
return options;
}
options.llmConfig.callbacks = [
{
handleLLMNewToken: createHandleLLMNewToken(clientOptions.streamRate),
},
];
return options;
}
options.llmConfig.callbacks = [
{
handleLLMNewToken: createHandleLLMNewToken(clientOptions.streamRate),
},
];
return options;
if (clientOptions.reverseProxyUrl) {
modelOptions.baseUrl = clientOptions.reverseProxyUrl.split('/v1')[0];
delete clientOptions.reverseProxyUrl;
}
return {
useLegacyContent: true,
llmConfig: modelOptions,
};
}
const client = new OpenAIClient(apiKey, clientOptions);

View File

@@ -143,7 +143,7 @@ const initializeClient = async ({
modelOptions.model = modelName;
clientOptions = Object.assign({ modelOptions }, clientOptions);
clientOptions.modelOptions.user = req.user.id;
const options = getOpenAIConfig(apiKey, clientOptions, endpoint);
const options = getOpenAIConfig(apiKey, clientOptions);
if (options != null && serverless === true) {
options.useLegacyContent = true;
}

View File

@@ -42,26 +42,18 @@ async function getCustomConfigSpeech(req, res) {
settings.advancedMode = speechTab.advancedMode;
}
if (speechTab.speechToText !== undefined) {
if (typeof speechTab.speechToText === 'boolean') {
settings.speechToText = speechTab.speechToText;
} else {
for (const key in speechTab.speechToText) {
if (speechTab.speechToText[key] !== undefined) {
settings[key] = speechTab.speechToText[key];
}
if (speechTab.speechToText) {
for (const key in speechTab.speechToText) {
if (speechTab.speechToText[key] !== undefined) {
settings[key] = speechTab.speechToText[key];
}
}
}
if (speechTab.textToSpeech !== undefined) {
if (typeof speechTab.textToSpeech === 'boolean') {
settings.textToSpeech = speechTab.textToSpeech;
} else {
for (const key in speechTab.textToSpeech) {
if (speechTab.textToSpeech[key] !== undefined) {
settings[key] = speechTab.textToSpeech[key];
}
if (speechTab.textToSpeech) {
for (const key in speechTab.textToSpeech) {
if (speechTab.textToSpeech[key] !== undefined) {
settings[key] = speechTab.textToSpeech[key];
}
}
}

View File

@@ -159,7 +159,7 @@ const searchEntraIdPrincipals = async (accessToken, sub, query, type = 'all', li
/**
* Get current user's Entra ID group memberships from Microsoft Graph
* Uses /me/getMemberGroups endpoint to get transitive groups the user is a member of
* Uses /me/memberOf endpoint to get groups the user is a member of
* @param {string} accessToken - OpenID Connect access token
* @param {string} sub - Subject identifier
* @returns {Promise<Array<string>>} Array of group ID strings (GUIDs)
@@ -167,12 +167,10 @@ const searchEntraIdPrincipals = async (accessToken, sub, query, type = 'all', li
const getUserEntraGroups = async (accessToken, sub) => {
try {
const graphClient = await createGraphClient(accessToken, sub);
const response = await graphClient
.api('/me/getMemberGroups')
.post({ securityEnabledOnly: false });
const groupIds = Array.isArray(response?.value) ? response.value : [];
return [...new Set(groupIds.map((groupId) => String(groupId)))];
const groupsResponse = await graphClient.api('/me/memberOf').select('id').get();
return (groupsResponse.value || []).map((group) => group.id);
} catch (error) {
logger.error('[getUserEntraGroups] Error fetching user groups:', error);
return [];
@@ -189,22 +187,13 @@ const getUserEntraGroups = async (accessToken, sub) => {
const getUserOwnedEntraGroups = async (accessToken, sub) => {
try {
const graphClient = await createGraphClient(accessToken, sub);
const allGroupIds = [];
let nextLink = '/me/ownedObjects/microsoft.graph.group';
while (nextLink) {
const response = await graphClient.api(nextLink).select('id').top(999).get();
const groups = response?.value || [];
allGroupIds.push(...groups.map((group) => group.id));
const groupsResponse = await graphClient
.api('/me/ownedObjects/microsoft.graph.group')
.select('id')
.get();
nextLink = response['@odata.nextLink']
? response['@odata.nextLink']
.replace(/^https:\/\/graph\.microsoft\.com\/v1\.0/, '')
.trim() || null
: null;
}
return allGroupIds;
return (groupsResponse.value || []).map((group) => group.id);
} catch (error) {
logger.error('[getUserOwnedEntraGroups] Error fetching user owned groups:', error);
return [];
@@ -222,27 +211,21 @@ const getUserOwnedEntraGroups = async (accessToken, sub) => {
const getGroupMembers = async (accessToken, sub, groupId) => {
try {
const graphClient = await createGraphClient(accessToken, sub);
const allMembers = new Set();
let nextLink = `/groups/${groupId}/transitiveMembers`;
const allMembers = [];
let nextLink = `/groups/${groupId}/members`;
while (nextLink) {
const membersResponse = await graphClient.api(nextLink).select('id').top(999).get();
const members = membersResponse?.value || [];
members.forEach((member) => {
if (typeof member?.id === 'string' && member['@odata.type'] === '#microsoft.graph.user') {
allMembers.add(member.id);
}
});
const members = membersResponse.value || [];
allMembers.push(...members.map((member) => member.id));
nextLink = membersResponse['@odata.nextLink']
? membersResponse['@odata.nextLink']
.replace(/^https:\/\/graph\.microsoft\.com\/v1\.0/, '')
.trim() || null
? membersResponse['@odata.nextLink'].split('/v1.0')[1]
: null;
}
return Array.from(allMembers);
return allMembers;
} catch (error) {
logger.error('[getGroupMembers] Error fetching group members:', error);
return [];

View File

@@ -73,7 +73,6 @@ describe('GraphApiService', () => {
header: jest.fn().mockReturnThis(),
top: jest.fn().mockReturnThis(),
get: jest.fn(),
post: jest.fn(),
};
Client.init.mockReturnValue(mockGraphClient);
@@ -515,33 +514,31 @@ describe('GraphApiService', () => {
});
describe('getUserEntraGroups', () => {
it('should fetch user groups using getMemberGroups endpoint', async () => {
it('should fetch user groups from memberOf endpoint', async () => {
const mockGroupsResponse = {
value: ['group-1', 'group-2'],
value: [
{
id: 'group-1',
},
{
id: 'group-2',
},
],
};
mockGraphClient.post.mockResolvedValue(mockGroupsResponse);
mockGraphClient.get.mockResolvedValue(mockGroupsResponse);
const result = await GraphApiService.getUserEntraGroups('token', 'user');
expect(mockGraphClient.api).toHaveBeenCalledWith('/me/getMemberGroups');
expect(mockGraphClient.post).toHaveBeenCalledWith({ securityEnabledOnly: false });
expect(result).toEqual(['group-1', 'group-2']);
});
it('should deduplicate returned group ids', async () => {
mockGraphClient.post.mockResolvedValue({
value: ['group-1', 'group-2', 'group-1'],
});
const result = await GraphApiService.getUserEntraGroups('token', 'user');
expect(mockGraphClient.api).toHaveBeenCalledWith('/me/memberOf');
expect(mockGraphClient.select).toHaveBeenCalledWith('id');
expect(result).toHaveLength(2);
expect(result).toEqual(['group-1', 'group-2']);
});
it('should return empty array on error', async () => {
mockGraphClient.post.mockRejectedValue(new Error('API error'));
mockGraphClient.get.mockRejectedValue(new Error('API error'));
const result = await GraphApiService.getUserEntraGroups('token', 'user');
@@ -553,7 +550,7 @@ describe('GraphApiService', () => {
value: [],
};
mockGraphClient.post.mockResolvedValue(mockGroupsResponse);
mockGraphClient.get.mockResolvedValue(mockGroupsResponse);
const result = await GraphApiService.getUserEntraGroups('token', 'user');
@@ -561,7 +558,7 @@ describe('GraphApiService', () => {
});
it('should handle missing value property', async () => {
mockGraphClient.post.mockResolvedValue({});
mockGraphClient.get.mockResolvedValue({});
const result = await GraphApiService.getUserEntraGroups('token', 'user');
@@ -569,89 +566,6 @@ describe('GraphApiService', () => {
});
});
describe('getUserOwnedEntraGroups', () => {
it('should fetch owned groups with pagination support', async () => {
const firstPage = {
value: [
{
id: 'owned-group-1',
},
],
'@odata.nextLink':
'https://graph.microsoft.com/v1.0/me/ownedObjects/microsoft.graph.group?$skiptoken=xyz',
};
const secondPage = {
value: [
{
id: 'owned-group-2',
},
],
};
mockGraphClient.get.mockResolvedValueOnce(firstPage).mockResolvedValueOnce(secondPage);
const result = await GraphApiService.getUserOwnedEntraGroups('token', 'user');
expect(mockGraphClient.api).toHaveBeenNthCalledWith(
1,
'/me/ownedObjects/microsoft.graph.group',
);
expect(mockGraphClient.api).toHaveBeenNthCalledWith(
2,
'/me/ownedObjects/microsoft.graph.group?$skiptoken=xyz',
);
expect(mockGraphClient.top).toHaveBeenCalledWith(999);
expect(mockGraphClient.get).toHaveBeenCalledTimes(2);
expect(result).toEqual(['owned-group-1', 'owned-group-2']);
});
it('should return empty array on error', async () => {
mockGraphClient.get.mockRejectedValue(new Error('API error'));
const result = await GraphApiService.getUserOwnedEntraGroups('token', 'user');
expect(result).toEqual([]);
});
});
describe('getGroupMembers', () => {
it('should fetch transitive members and include only users', async () => {
const firstPage = {
value: [
{ id: 'user-1', '@odata.type': '#microsoft.graph.user' },
{ id: 'child-group', '@odata.type': '#microsoft.graph.group' },
],
'@odata.nextLink':
'https://graph.microsoft.com/v1.0/groups/group-id/transitiveMembers?$skiptoken=abc',
};
const secondPage = {
value: [{ id: 'user-2', '@odata.type': '#microsoft.graph.user' }],
};
mockGraphClient.get.mockResolvedValueOnce(firstPage).mockResolvedValueOnce(secondPage);
const result = await GraphApiService.getGroupMembers('token', 'user', 'group-id');
expect(mockGraphClient.api).toHaveBeenNthCalledWith(1, '/groups/group-id/transitiveMembers');
expect(mockGraphClient.api).toHaveBeenNthCalledWith(
2,
'/groups/group-id/transitiveMembers?$skiptoken=abc',
);
expect(mockGraphClient.top).toHaveBeenCalledWith(999);
expect(result).toEqual(['user-1', 'user-2']);
});
it('should return empty array on error', async () => {
mockGraphClient.get.mockRejectedValue(new Error('API error'));
const result = await GraphApiService.getGroupMembers('token', 'user', 'group-id');
expect(result).toEqual([]);
});
});
describe('testGraphApiAccess', () => {
beforeEach(() => {
jest.clearAllMocks();

View File

@@ -39,8 +39,6 @@ const { openAIApiKey, userProvidedOpenAI } = require('./Config/EndpointService')
* @param {boolean} [params.userIdQuery=false] - Whether to send the user ID as a query parameter.
* @param {boolean} [params.createTokenConfig=true] - Whether to create a token configuration from the API response.
* @param {string} [params.tokenKey] - The cache key to save the token configuration. Uses `name` if omitted.
* @param {Record<string, string>} [params.headers] - Optional headers for the request.
* @param {Partial<IUser>} [params.userObject] - Optional user object for header resolution.
* @returns {Promise<string[]>} A promise that resolves to an array of model identifiers.
* @async
*/
@@ -54,8 +52,6 @@ const fetchModels = async ({
userIdQuery = false,
createTokenConfig = true,
tokenKey,
headers,
userObject,
}) => {
let models = [];
const baseURL = direct ? extractBaseURL(_baseURL) : _baseURL;
@@ -69,13 +65,7 @@ const fetchModels = async ({
}
if (name && name.toLowerCase().startsWith(Providers.OLLAMA)) {
try {
return await OllamaClient.fetchModels(baseURL, { headers, user: userObject });
} catch (ollamaError) {
const logMessage =
'Failed to fetch models from Ollama API. Attempting to fetch via OpenAI-compatible endpoint.';
logAxiosError({ message: logMessage, error: ollamaError });
}
return await OllamaClient.fetchModels(baseURL);
}
try {

View File

@@ -1,5 +1,5 @@
const axios = require('axios');
const { logAxiosError, resolveHeaders } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { EModelEndpoint, defaultModels } = require('librechat-data-provider');
const {
@@ -18,8 +18,6 @@ jest.mock('@librechat/api', () => {
processModelData: jest.fn((...args) => {
return originalUtils.processModelData(...args);
}),
logAxiosError: jest.fn(),
resolveHeaders: jest.fn((options) => options?.headers || {}),
};
});
@@ -279,51 +277,12 @@ describe('fetchModels with Ollama specific logic', () => {
expect(models).toEqual(['Ollama-Base', 'Ollama-Advanced']);
expect(axios.get).toHaveBeenCalledWith('https://api.ollama.test.com/api/tags', {
headers: {},
timeout: 5000,
});
});
it('should pass headers and user object to Ollama fetchModels', async () => {
const customHeaders = {
'Content-Type': 'application/json',
Authorization: 'Bearer custom-token',
};
const userObject = {
id: 'user789',
email: 'test@example.com',
};
resolveHeaders.mockReturnValueOnce(customHeaders);
const models = await fetchModels({
user: 'user789',
apiKey: 'testApiKey',
baseURL: 'https://api.ollama.test.com',
name: 'ollama',
headers: customHeaders,
userObject,
});
expect(models).toEqual(['Ollama-Base', 'Ollama-Advanced']);
expect(resolveHeaders).toHaveBeenCalledWith({
headers: customHeaders,
user: userObject,
});
expect(axios.get).toHaveBeenCalledWith('https://api.ollama.test.com/api/tags', {
headers: customHeaders,
timeout: 5000,
});
});
it('should handle errors gracefully when fetching Ollama models fails and fallback to OpenAI-compatible fetch', async () => {
axios.get.mockRejectedValueOnce(new Error('Ollama API error'));
axios.get.mockResolvedValueOnce({
data: {
data: [{ id: 'fallback-model-1' }, { id: 'fallback-model-2' }],
},
});
it('should handle errors gracefully when fetching Ollama models fails', async () => {
axios.get.mockRejectedValue(new Error('Network error'));
const models = await fetchModels({
user: 'user789',
apiKey: 'testApiKey',
@@ -331,13 +290,8 @@ describe('fetchModels with Ollama specific logic', () => {
name: 'OllamaAPI',
});
expect(models).toEqual(['fallback-model-1', 'fallback-model-2']);
expect(logAxiosError).toHaveBeenCalledWith({
message:
'Failed to fetch models from Ollama API. Attempting to fetch via OpenAI-compatible endpoint.',
error: expect.any(Error),
});
expect(axios.get).toHaveBeenCalledTimes(2);
expect(models).toEqual([]);
expect(logger.error).toHaveBeenCalled();
});
it('should return an empty array if no baseURL is provided', async () => {

View File

@@ -357,18 +357,16 @@ async function setupOpenId() {
};
const appConfig = await getAppConfig();
/** Azure AD sometimes doesn't return email, use preferred_username as fallback */
const email = userinfo.email || userinfo.preferred_username || userinfo.upn;
if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
if (!isEmailDomainAllowed(userinfo.email, appConfig?.registration?.allowedDomains)) {
logger.error(
`[OpenID Strategy] Authentication blocked - email domain not allowed [Email: ${email}]`,
`[OpenID Strategy] Authentication blocked - email domain not allowed [Email: ${userinfo.email}]`,
);
return done(null, false, { message: 'Email domain not allowed' });
}
const result = await findOpenIDUser({
findUser,
email: email,
email: claims.email,
openidId: claims.sub,
idOnTheSource: claims.oid,
strategyName: 'openidStrategy',
@@ -435,7 +433,7 @@ async function setupOpenId() {
provider: 'openid',
openidId: userinfo.sub,
username,
email: email || '',
email: userinfo.email || '',
emailVerified: userinfo.email_verified || false,
name: fullName,
idOnTheSource: userinfo.oid,
@@ -449,8 +447,8 @@ async function setupOpenId() {
user.username = username;
user.name = fullName;
user.idOnTheSource = userinfo.oid;
if (email && email !== user.email) {
user.email = email;
if (userinfo.email && userinfo.email !== user.email) {
user.email = userinfo.email;
user.emailVerified = userinfo.email_verified || false;
}
}

View File

@@ -1,4 +1,4 @@
/** v0.8.1-rc1 */
/** v0.8.0 */
module.exports = {
roots: ['<rootDir>/src'],
testEnvironment: 'jsdom',

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/frontend",
"version": "v0.8.1-rc1",
"version": "v0.8.0",
"description": "",
"type": "module",
"scripts": {
@@ -149,7 +149,7 @@
"tailwindcss": "^3.4.1",
"ts-jest": "^29.2.5",
"typescript": "^5.3.3",
"vite": "^6.4.1",
"vite": "^6.3.6",
"vite-plugin-compression2": "^2.2.1",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.2"

View File

@@ -35,7 +35,9 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
enabled: !isEphemeralAgent(agent_id),
});
const { data: regularTools } = useAvailableToolsQuery(EModelEndpoint.agents);
const { data: regularTools } = useAvailableToolsQuery(EModelEndpoint.agents, {
enabled: !isEphemeralAgent(agent_id),
});
const { data: mcpData } = useMCPToolsQuery({
enabled: !isEphemeralAgent(agent_id) && startupConfig?.mcpServers != null,

View File

@@ -11,9 +11,9 @@ import {
AgentListResponse,
} from 'librechat-data-provider';
import type t from 'librechat-data-provider';
import { renderAgentAvatar, clearMessagesCache } from '~/utils';
import { useLocalize, useDefaultConvo } from '~/hooks';
import { useChatContext } from '~/Providers';
import { renderAgentAvatar } from '~/utils';
interface SupportContact {
name?: string;
@@ -56,7 +56,10 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, agent.id);
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.setQueryData<t.TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
queryClient.invalidateQueries([QueryKeys.messages]);
/** Template with agent configuration */

View File

@@ -4,7 +4,7 @@ import { useOutletContext } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useSearchParams, useParams, useNavigate } from 'react-router-dom';
import { TooltipAnchor, Button, NewChatIcon, useMediaQuery } from '@librechat/client';
import { PermissionTypes, Permissions, QueryKeys } from 'librechat-data-provider';
import { PermissionTypes, Permissions, QueryKeys, Constants } from 'librechat-data-provider';
import type t from 'librechat-data-provider';
import type { ContextType } from '~/common';
import { useDocumentTitle, useHasAccess, useLocalize, TranslationKeys } from '~/hooks';
@@ -13,11 +13,11 @@ import MarketplaceAdminSettings from './MarketplaceAdminSettings';
import { SidePanelProvider, useChatContext } from '~/Providers';
import { SidePanelGroup } from '~/components/SidePanel';
import { OpenSidebar } from '~/components/Chat/Menus';
import { cn, clearMessagesCache } from '~/utils';
import CategoryTabs from './CategoryTabs';
import AgentDetail from './AgentDetail';
import SearchBar from './SearchBar';
import AgentGrid from './AgentGrid';
import { cn } from '~/utils';
import store from '~/store';
interface AgentMarketplaceProps {
@@ -224,7 +224,10 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
window.open('/c/new', '_blank');
return;
}
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.setQueryData<t.TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
queryClient.invalidateQueries([QueryKeys.messages]);
newConversation();
};

View File

@@ -58,7 +58,6 @@ const LabelController: React.FC<LabelControllerProps> = ({
checked={field.value}
onCheckedChange={field.onChange}
value={field.value.toString()}
aria-label={label}
/>
)}
/>

View File

@@ -194,7 +194,7 @@ describe('Virtual Scrolling Performance', () => {
// Performance check: rendering should be fast
const renderTime = endTime - startTime;
expect(renderTime).toBeLessThan(740);
expect(renderTime).toBeLessThan(720);
console.log(`Rendered 1000 agents in ${renderTime.toFixed(2)}ms`);
console.log(`Only ${renderedCards.length} DOM nodes created for 1000 agents`);

View File

@@ -129,11 +129,7 @@ const BookmarkForm = ({
</div>
<div className="mt-4 grid w-full items-center gap-2">
<Label
id="bookmark-description-label"
htmlFor="bookmark-description"
className="text-left text-sm font-medium"
>
<Label htmlFor="bookmark-description" className="text-left text-sm font-medium">
{localize('com_ui_bookmarks_description')}
</Label>
<TextareaAutosize
@@ -151,7 +147,6 @@ const BookmarkForm = ({
className={cn(
'flex h-10 max-h-[250px] min-h-[100px] w-full resize-none rounded-lg border border-input bg-transparent px-3 py-2 text-sm ring-offset-background focus-visible:outline-none',
)}
aria-labelledby="bookmark-description-label"
/>
</div>
{conversationId != null && conversationId && (
@@ -166,7 +161,6 @@ const BookmarkForm = ({
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value?.toString()}
aria-label={localize('com_ui_bookmarks_add_to_conversation')}
/>
)}
/>

View File

@@ -12,7 +12,6 @@ import {
import {
useTextarea,
useAutoSave,
useLocalize,
useRequiresKey,
useHandleKeyUp,
useQueryParams,
@@ -39,7 +38,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
const submitButtonRef = useRef<HTMLButtonElement>(null);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useFocusChatEffect(textAreaRef);
const localize = useLocalize();
const [isCollapsed, setIsCollapsed] = useState(false);
const [, setIsScrollable] = useState(false);
@@ -222,7 +220,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
<div className={cn('flex w-full items-center', isRTL && 'flex-row-reverse')}>
{showPlusPopover && !isAssistantsEndpoint(endpoint) && (
<Mention
conversation={conversation}
setShowMentionPopover={setShowPlusPopover}
newConversation={generateConversation}
textAreaRef={textAreaRef}
@@ -233,7 +230,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
)}
{showMentionPopover && (
<Mention
conversation={conversation}
setShowMentionPopover={setShowMentionPopover}
newConversation={newConversation}
textAreaRef={textAreaRef}
@@ -281,7 +277,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
setIsTextAreaFocused(true);
}}
onBlur={setIsTextAreaFocused.bind(null, false)}
aria-label={localize('com_ui_message_input')}
onClick={handleFocusOrClick}
style={{ height: 44, overflowY: 'auto' }}
className={cn(

View File

@@ -62,28 +62,17 @@ const FileUpload: React.FC<FileUploadProps> = ({
statusText = invalidText ?? localize('com_ui_upload_invalid');
}
const handleClick = () => {
const fileInput = document.getElementById(`file-upload-${id}`) as HTMLInputElement;
if (fileInput) {
fileInput.click();
}
};
return (
<>
<button
type="button"
onClick={handleClick}
className={cn(
'mr-1 flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-normal transition-colors hover:bg-gray-100 hover:text-green-600 focus:ring-ring dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-green-500',
statusColor,
containerClassName,
)}
aria-label={statusText}
>
<FileUp className="mr-1 flex w-[22px] items-center stroke-1" aria-hidden="true" />
<span className="flex text-xs">{statusText}</span>
</button>
<label
htmlFor={`file-upload-${id}`}
className={cn(
'mr-1 flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-normal transition-colors hover:bg-gray-100 hover:text-green-600 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-green-500',
statusColor,
containerClassName,
)}
>
<FileUp className="mr-1 flex w-[22px] items-center stroke-1" />
<span className="flex text-xs">{statusText}</span>
<input
id={`file-upload-${id}`}
value=""
@@ -91,9 +80,8 @@ const FileUpload: React.FC<FileUploadProps> = ({
className={cn('hidden', className)}
accept=".json"
onChange={handleFileChange}
tabIndex={-1}
/>
</>
</label>
);
};

View File

@@ -122,11 +122,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
aria-label={localize('com_files_filter_by')}
className={cn('min-w-[40px]', isSmallScreen && 'px-2 py-1')}
>
<Button variant="outline" className={cn('min-w-[40px]', isSmallScreen && 'px-2 py-1')}>
<ListFilter className="size-3.5 sm:size-4" />
</Button>
</DropdownMenuTrigger>

View File

@@ -2,7 +2,6 @@ import { useState, useRef, useEffect } from 'react';
import { useCombobox } from '@librechat/client';
import { AutoSizer, List } from 'react-virtualized';
import { EModelEndpoint } from 'librechat-data-provider';
import type { TConversation } from 'librechat-data-provider';
import type { MentionOption, ConvoGenerator } from '~/common';
import type { SetterOrUpdater } from 'recoil';
import useSelectMention from '~/hooks/Input/useSelectMention';
@@ -15,7 +14,6 @@ import MentionItem from './MentionItem';
const ROW_HEIGHT = 40;
export default function Mention({
conversation,
setShowMentionPopover,
newConversation,
textAreaRef,
@@ -23,7 +21,6 @@ export default function Mention({
placeholder = 'com_ui_mention',
includeAssistants = true,
}: {
conversation: TConversation | null;
setShowMentionPopover: SetterOrUpdater<boolean>;
newConversation: ConvoGenerator;
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
@@ -45,7 +42,6 @@ export default function Mention({
const { onSelectMention } = useSelectMention({
presets,
modelSpecs,
conversation,
assistantsMap,
endpointsConfig,
newConversation,

View File

@@ -1,5 +1,5 @@
import React, { createContext, useContext, useMemo } from 'react';
import type { EModelEndpoint, TConversation } from 'librechat-data-provider';
import type { EModelEndpoint } from 'librechat-data-provider';
import { useChatContext } from '~/Providers/ChatContext';
interface ModelSelectorChatContextValue {
@@ -8,7 +8,6 @@ interface ModelSelectorChatContextValue {
spec?: string | null;
agent_id?: string | null;
assistant_id?: string | null;
conversation: TConversation | null;
newConversation: ReturnType<typeof useChatContext>['newConversation'];
}
@@ -27,10 +26,16 @@ export function ModelSelectorChatProvider({ children }: { children: React.ReactN
spec: conversation?.spec,
agent_id: conversation?.agent_id,
assistant_id: conversation?.assistant_id,
conversation,
newConversation,
}),
[conversation, newConversation],
[
conversation?.endpoint,
conversation?.model,
conversation?.spec,
conversation?.agent_id,
conversation?.assistant_id,
newConversation,
],
);
return (

View File

@@ -57,7 +57,7 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
const agentsMap = useAgentsMapContext();
const assistantsMap = useAssistantsMapContext();
const { data: endpointsConfig } = useGetEndpointsQuery();
const { endpoint, model, spec, agent_id, assistant_id, conversation, newConversation } =
const { endpoint, model, spec, agent_id, assistant_id, newConversation } =
useModelSelectorChatContext();
const modelSpecs = useMemo(() => {
const specs = startupConfig?.modelSpecs?.list ?? [];
@@ -96,7 +96,6 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
const { onSelectEndpoint, onSelectSpec } = useSelectMention({
// presets,
modelSpecs,
conversation,
assistantsMap,
endpointsConfig,
newConversation,

View File

@@ -1,8 +1,8 @@
import { QueryKeys } from 'librechat-data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import { TooltipAnchor, Button, NewChatIcon } from '@librechat/client';
import type { TMessage } from 'librechat-data-provider';
import { useChatContext } from '~/Providers';
import { clearMessagesCache } from '~/utils';
import { useLocalize } from '~/hooks';
export default function HeaderNewChat() {
@@ -15,7 +15,10 @@ export default function HeaderNewChat() {
window.open('/c/new', '_blank');
return;
}
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
queryClient.invalidateQueries([QueryKeys.messages]);
newConversation();
};

View File

@@ -59,10 +59,9 @@ const PresetItems: FC<{
</label>
<Dialog>
<DialogTrigger asChild>
<button
type="button"
className="mr-1 flex h-[32px] cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-red-700 focus:ring-ring dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-red-700"
aria-label={localize('com_ui_clear') + ' ' + localize('com_ui_all')}
<label
htmlFor="file-upload"
className="mr-1 flex h-[32px] cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-red-700"
>
<svg
width="24"
@@ -71,12 +70,11 @@ const PresetItems: FC<{
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
className="mr-1 flex w-[22px] items-center"
aria-hidden="true"
>
<path d="M9.293 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0M9.5 3.5v-2l3 3h-2a1 1 0 0 1-1-1M6.854 7.146 8 8.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 9l1.147 1.146a.5.5 0 0 1-.708.708L8 9.707l-1.146 1.147a.5.5 0 0 1-.708-.708L7.293 9 6.146 7.854a.5.5 0 1 1 .708-.708"></path>
</svg>
{localize('com_ui_clear')} {localize('com_ui_all')}
</button>
</label>
</DialogTrigger>
<DialogTemplate
showCloseButton={false}

View File

@@ -168,7 +168,6 @@ const EditMessage = ({
'max-h-[65vh] pr-3 md:max-h-[75vh] md:pr-4',
removeFocusRings,
)}
aria-label={localize('com_ui_message_input')}
dir={isRTL ? 'rtl' : 'ltr'}
/>
</div>

View File

@@ -170,7 +170,6 @@ const EditTextPart = ({
'max-h-[65vh] pr-3 md:max-h-[75vh] md:pr-4',
removeFocusRings,
)}
aria-label={localize('com_ui_editable_message')}
dir={isRTL ? 'rtl' : 'ltr'}
/>
</div>

View File

@@ -201,6 +201,7 @@ const Conversations: FC<ConversationsProps> = ({
overscanRowCount={10}
className="outline-none"
style={{ outline: 'none' }}
role="list"
aria-label="Conversations"
onRowsRendered={handleRowsRendered}
tabIndex={-1}

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { QueryKeys } from 'librechat-data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
@@ -82,7 +82,7 @@ export function DeleteConversationDialog({
<OGDialogHeader>
<OGDialogTitle>{localize('com_ui_delete_conversation')}</OGDialogTitle>
</OGDialogHeader>
<div className="w-full truncate">
<div>
{localize('com_ui_delete_confirm')} <strong>{title}</strong> ?
</div>
<div className="flex justify-end gap-4 pt-4">

View File

@@ -77,13 +77,7 @@ export default function ShareButton({
<div className="relative items-center rounded-lg p-2">
{showQR && (
<div className="mb-4 flex flex-col items-center">
<QRCodeSVG
value={sharedLink}
size={200}
marginSize={2}
className="rounded-2xl"
title={localize('com_ui_share_qr_code_description')}
/>
<QRCodeSVG value={sharedLink} size={200} marginSize={2} className="rounded-2xl" />
</div>
)}
@@ -93,7 +87,6 @@ export default function ShareButton({
<Button
size="sm"
variant="outline"
aria-label={localize('com_ui_copy_link')}
onClick={() => {
if (isCopying) {
return;

View File

@@ -34,8 +34,6 @@ const RenameForm: React.FC<RenameFormProps> = ({
case 'Enter':
onSubmit(titleInput);
break;
case 'Tab':
break;
}
};
@@ -52,23 +50,22 @@ const RenameForm: React.FC<RenameFormProps> = ({
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={() => onSubmit(titleInput)}
maxLength={100}
aria-label={localize('com_ui_new_conversation_title')}
/>
<div className="flex gap-1" role="toolbar">
<button
onClick={() => onCancel()}
className="rounded-md p-1 hover:opacity-70 focus:outline-none focus:ring-2 focus:ring-ring"
className="p-1 hover:opacity-70 focus:outline-none focus:ring-2"
aria-label={localize('com_ui_cancel')}
type="button"
>
<X className="h-4 w-4" aria-hidden="true" />
</button>
<button
onClick={() => onSubmit(titleInput)}
className="rounded-md p-1 hover:opacity-70 focus:outline-none focus:ring-2 focus:ring-ring"
className="p-1 hover:opacity-70 focus:outline-none focus:ring-2"
aria-label={localize('com_ui_save')}
type="button"
>
<Check className="h-4 w-4" aria-hidden="true" />
</button>

View File

@@ -151,7 +151,6 @@ export default function Settings({
min={0}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="temp-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={optionEndpoint ?? ''} type="temp" side={ESide.Left} />
@@ -161,9 +160,7 @@ export default function Settings({
<div className="flex justify-between">
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
{localize('com_endpoint_top_p')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', { 0: '1' })})
</small>
<small className="opacity-40">({localize('com_endpoint_default')}: 1)</small>
</Label>
<InputNumber
id="top-p-int"
@@ -192,7 +189,6 @@ export default function Settings({
min={0}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="top-p-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={optionEndpoint ?? ''} type="topp" side={ESide.Left} />
@@ -203,9 +199,7 @@ export default function Settings({
<div className="flex justify-between">
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
{localize('com_endpoint_frequency_penalty')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', { 0: '0' })})
</small>
<small className="opacity-40">({localize('com_endpoint_default')}: 0)</small>
</Label>
<InputNumber
id="freq-penalty-int"
@@ -234,7 +228,6 @@ export default function Settings({
min={-2}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="freq-penalty-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={optionEndpoint ?? ''} type="freq" side={ESide.Left} />
@@ -245,9 +238,7 @@ export default function Settings({
<div className="flex justify-between">
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
{localize('com_endpoint_presence_penalty')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', { 0: '0' })})
</small>
<small className="opacity-40">({localize('com_endpoint_default')}: 0)</small>
</Label>
<InputNumber
id="pres-penalty-int"
@@ -276,7 +267,6 @@ export default function Settings({
min={-2}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="pres-penalty-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={optionEndpoint ?? ''} type="pres" side={ESide.Left} />
@@ -316,7 +306,6 @@ export default function Settings({
onCheckedChange={(checked: boolean) => setResendFiles(checked)}
disabled={readonly}
className="flex"
aria-label={localize('com_endpoint_plug_resend_files')}
/>
<OptionHover endpoint={optionEndpoint ?? ''} type="resend" side={ESide.Bottom} />
</HoverCardTrigger>
@@ -334,7 +323,6 @@ export default function Settings({
max={2}
min={0}
step={1}
aria-label={localize('com_endpoint_plug_image_detail')}
/>
<OptionHover endpoint={optionEndpoint ?? ''} type="detail" side={ESide.Bottom} />
</HoverCardTrigger>

View File

@@ -53,9 +53,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
<div className="flex justify-between">
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
{localize('com_endpoint_temperature')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', { 0: '0' })})
</small>
<small className="opacity-40">({localize('com_endpoint_default')}: 0)</small>
</Label>
<InputNumber
id="temp-int"
@@ -84,7 +82,6 @@ export default function Settings({ conversation, setOption, models, readonly }:
min={0}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="temp-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="temp" side={ESide.Left} />
@@ -104,7 +101,6 @@ export default function Settings({ conversation, setOption, models, readonly }:
onCheckedChange={onCheckedChangeAgent}
disabled={readonly}
className="ml-4 mt-2"
aria-label={localize('com_endpoint_plug_use_functions')}
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="func" side={ESide.Bottom} />
@@ -123,7 +119,6 @@ export default function Settings({ conversation, setOption, models, readonly }:
onCheckedChange={onCheckedChangeSkip}
disabled={readonly}
className="ml-4 mt-2"
aria-label={localize('com_endpoint_plug_skip_completion')}
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="skip" side={ESide.Bottom} />

View File

@@ -171,7 +171,6 @@ export default function Settings({ conversation, setOption, models, readonly }:
min={google.temperature.min}
step={google.temperature.step}
className="flex h-4 w-full"
aria-labelledby="temp-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="temp" side={ESide.Left} />
@@ -212,7 +211,6 @@ export default function Settings({ conversation, setOption, models, readonly }:
min={google.topP.min}
step={google.topP.step}
className="flex h-4 w-full"
aria-labelledby="top-p-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="topp" side={ESide.Left} />
@@ -254,7 +252,6 @@ export default function Settings({ conversation, setOption, models, readonly }:
min={google.topK.min}
step={google.topK.step}
className="flex h-4 w-full"
aria-labelledby="top-k-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="topk" side={ESide.Left} />
@@ -299,7 +296,6 @@ export default function Settings({ conversation, setOption, models, readonly }:
min={google.maxOutputTokens.min}
step={google.maxOutputTokens.step}
className="flex h-4 w-full"
aria-labelledby="max-tokens-int"
/>
</HoverCardTrigger>
<OptionHover

View File

@@ -256,7 +256,6 @@ export default function Settings({
min={0}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="temp-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="temp" side={ESide.Left} />
@@ -297,7 +296,6 @@ export default function Settings({
min={0}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="top-p-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="topp" side={ESide.Left} />
@@ -339,7 +337,6 @@ export default function Settings({
min={-2}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="freq-penalty-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="freq" side={ESide.Left} />
@@ -381,7 +378,6 @@ export default function Settings({
min={-2}
step={0.01}
className="flex h-4 w-full"
aria-labelledby="pres-penalty-int"
/>
</HoverCardTrigger>
<OptionHover endpoint={conversation.endpoint ?? ''} type="pres" side={ESide.Left} />

View File

@@ -124,15 +124,13 @@ export default function ExportModal({
disabled={!exportOptionsSupport}
checked={includeOptions}
onCheckedChange={setIncludeOptions}
aria-labelledby="includeOptions-label"
/>
<label
id="includeOptions-label"
htmlFor="includeOptions"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
>
{exportOptionsSupport
? localize('com_nav_export_include_endpoint_options')
? localize('com_nav_enabled')
: localize('com_nav_not_supported')}
</label>
</div>
@@ -148,15 +146,13 @@ export default function ExportModal({
disabled={!exportBranchesSupport}
checked={exportBranches}
onCheckedChange={setExportBranches}
aria-labelledby="exportBranches-label"
/>
<label
id="exportBranches-label"
htmlFor="exportBranches"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
>
{exportBranchesSupport
? localize('com_nav_export_all_message_branches')
? localize('com_nav_enabled')
: localize('com_nav_not_supported')}
</label>
</div>
@@ -167,14 +163,8 @@ export default function ExportModal({
{localize('com_nav_export_recursive_or_sequential')}
</Label>
<div className="flex h-[40px] w-full items-center space-x-3">
<Checkbox
id="recursive"
checked={recursive}
onCheckedChange={setRecursive}
aria-labelledby="recursive-label"
/>
<Checkbox id="recursive" checked={recursive} onCheckedChange={setRecursive} />
<label
id="recursive-label"
htmlFor="recursive"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
>

View File

@@ -5,7 +5,6 @@ import { QueryKeys, Constants } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import type { Dispatch, SetStateAction } from 'react';
import { useLocalize, useNewConvo } from '~/hooks';
import { clearMessagesCache } from '~/utils';
import store from '~/store';
export default function MobileNav({
@@ -58,7 +57,10 @@ export default function MobileNav({
aria-label={localize('com_ui_new_chat')}
className="m-1 inline-flex size-10 items-center justify-center rounded-full hover:bg-surface-hover"
onClick={() => {
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
queryClient.invalidateQueries([QueryKeys.messages]);
newConversation();
}}

View File

@@ -5,7 +5,6 @@ import { QueryKeys, Constants } from 'librechat-data-provider';
import { TooltipAnchor, NewChatIcon, MobileSidebar, Sidebar, Button } from '@librechat/client';
import type { TMessage } from 'librechat-data-provider';
import { useLocalize, useNewConvo } from '~/hooks';
import { clearMessagesCache } from '~/utils';
import store from '~/store';
export default function NewChat({
@@ -34,7 +33,10 @@ export default function NewChat({
window.open('/c/new', '_blank');
return;
}
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
queryClient.invalidateQueries([QueryKeys.messages]);
newConvo();
navigate('/c/new', { state: { focusChat: true } });

View File

@@ -30,7 +30,6 @@ export default function SaveBadgesState({
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid="saveBadgesState"
aria-label={localize('com_nav_save_badges_state')}
/>
</div>
);

View File

@@ -30,7 +30,6 @@ export default function SaveDraft({
onCheckedChange={handleCheckedChange}
className="ml-4"
data-testid="showThinking"
aria-label={localize('com_nav_show_thinking')}
/>
</div>
);

View File

@@ -9,10 +9,12 @@ import { useLocalize } from '~/hooks';
import { cn, logger } from '~/utils';
function ImportConversations() {
const localize = useLocalize();
const queryClient = useQueryClient();
const { showToast } = useToastContext();
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
const localize = useLocalize();
const fileInputRef = useRef<HTMLInputElement>(null);
const { showToast } = useToastContext();
const [isUploading, setIsUploading] = useState(false);
const handleSuccess = useCallback(() => {
@@ -51,8 +53,7 @@ function ImportConversations() {
const handleFileUpload = useCallback(
async (file: File) => {
try {
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
const maxFileSize = startupConfig?.conversationImportMaxFileSize;
const maxFileSize = (startupConfig as any)?.conversationImportMaxFileSize;
if (maxFileSize && file.size > maxFileSize) {
const size = (maxFileSize / (1024 * 1024)).toFixed(2);
showToast({
@@ -75,7 +76,7 @@ function ImportConversations() {
});
}
},
[uploadFile, showToast, localize, queryClient],
[uploadFile, showToast, localize, startupConfig],
);
const handleFileChange = useCallback(

View File

@@ -13,6 +13,7 @@ import {
useMediaQuery,
OGDialogHeader,
OGDialogTitle,
TooltipAnchor,
DataTable,
Spinner,
Button,
@@ -245,27 +246,37 @@ export default function SharedLinks() {
},
cell: ({ row }) => (
<div className="flex items-center gap-2">
<Button
variant="ghost"
className="h-8 w-8 p-0 hover:bg-surface-hover"
onClick={() => {
window.open(`/c/${row.original.conversationId}`, '_blank');
}}
aria-label={`${localize('com_ui_view_source')} - ${row.original.title || localize('com_ui_untitled')}`}
>
<MessageSquare className="size-4" aria-hidden="true" />
</Button>
<Button
variant="ghost"
className="h-8 w-8 p-0 hover:bg-surface-hover"
onClick={() => {
setDeleteRow(row.original);
setIsDeleteOpen(true);
}}
aria-label={`${localize('com_ui_delete')} - ${row.original.title || localize('com_ui_untitled')}`}
>
<TrashIcon className="size-4" aria-hidden="true" />
</Button>
<TooltipAnchor
description={localize('com_ui_view_source')}
render={
<Button
variant="ghost"
className="h-8 w-8 p-0 hover:bg-surface-hover"
onClick={() => {
window.open(`/c/${row.original.conversationId}`, '_blank');
}}
title={localize('com_ui_view_source')}
>
<MessageSquare className="size-4" />
</Button>
}
/>
<TooltipAnchor
description={localize('com_ui_delete')}
render={
<Button
variant="ghost"
className="h-8 w-8 p-0 hover:bg-surface-hover"
onClick={() => {
setDeleteRow(row.original);
setIsDeleteOpen(true);
}}
title={localize('com_ui_delete')}
>
<TrashIcon className="size-4" />
</Button>
}
/>
</div>
),
},

View File

@@ -53,7 +53,6 @@ const LabelController: React.FC<LabelControllerProps> = ({
}
}}
value={field.value.toString()}
aria-label={label}
/>
)}
/>
@@ -217,12 +216,7 @@ const AdminSettings = () => {
))}
</div>
<div className="flex justify-end">
<Button
type="submit"
disabled={isSubmitting || isLoading}
variant="submit"
aria-label={localize('com_ui_save')}
>
<Button type="submit" disabled={isSubmitting || isLoading} variant="submit">
{localize('com_ui_save')}
</Button>
</div>

View File

@@ -28,7 +28,7 @@ export default function AlwaysMakeProd({
checked={alwaysMakeProd}
onCheckedChange={handleCheckedChange}
data-testid="alwaysMakeProd"
aria-label={localize('com_nav_always_make_prod')}
aria-label="Always make prompt production"
/>
<div>{localize('com_nav_always_make_prod')} </div>
</div>

View File

@@ -30,7 +30,7 @@ export default function AutoSendPrompt({
>
<div> {localize('com_nav_auto_send_prompts')} </div>
<Switch
aria-label={localize('com_nav_auto_send_prompts')}
aria-label="toggle-auto-send-prompts"
id="autoSendPrompts"
checked={autoSendPrompts}
onCheckedChange={handleCheckedChange}

View File

@@ -102,9 +102,6 @@ function ChatGroupItem({
e.stopPropagation();
setPreviewDialogOpen(true);
}}
onKeyDown={(e) => {
e.stopPropagation();
}}
className="w-full cursor-pointer rounded-lg text-text-primary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed"
>
<TextSearch className="mr-2 h-4 w-4 text-text-primary" aria-hidden="true" />
@@ -119,9 +116,6 @@ function ChatGroupItem({
e.stopPropagation();
onEditClick(e);
}}
onKeyDown={(e) => {
e.stopPropagation();
}}
>
<EditIcon className="mr-2 h-4 w-4 text-text-primary" aria-hidden="true" />
<span>{localize('com_ui_edit')}</span>

View File

@@ -151,7 +151,6 @@ const CreatePromptForm = ({
className="w-full rounded border border-border-medium px-2 py-1 focus:outline-none dark:bg-transparent dark:text-gray-200"
minRows={6}
tabIndex={0}
aria-label={localize('com_ui_prompt_input_field')}
/>
<div
className={`mt-1 text-sm text-red-500 ${

View File

@@ -34,7 +34,6 @@ export default function List({
variant="outline"
className={`w-full bg-transparent ${isChatRoute ? '' : 'mx-2'}`}
onClick={() => navigate('/d/prompts/new')}
aria-label={localize('com_ui_create_prompt')}
>
<Plus className="size-4" aria-hidden />
{localize('com_ui_create_prompt')}

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Label } from '@librechat/client';
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
import { useLocalize } from '~/hooks';
export default function ListCard({
category,
@@ -16,7 +15,6 @@ export default function ListCard({
onClick?: React.MouseEventHandler<HTMLDivElement | HTMLButtonElement>;
children?: React.ReactNode;
}) {
const localize = useLocalize();
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement | HTMLButtonElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
@@ -33,7 +31,7 @@ export default function ListCard({
tabIndex={0}
aria-labelledby={`card-title-${name}`}
aria-describedby={`card-snippet-${name}`}
aria-label={`${name} Prompt, ${category ? `${localize('com_ui_category')}: ${category}` : ''}`}
aria-label={`Card for ${name}`}
>
<div className="flex w-full justify-between gap-2">
<div className="flex flex-row gap-2">

View File

@@ -17,7 +17,6 @@ export default function NoPromptGroup() {
onClick={() => {
navigate('/d/prompts');
}}
aria-label={localize('com_ui_back_to_prompts')}
>
{localize('com_ui_back_to_prompts')}
</Button>

View File

@@ -73,7 +73,8 @@ export default function VariableForm({
const mainText = useMemo(() => {
const initialText = group.productionPrompt?.prompt ?? '';
return replaceSpecialVars({ text: initialText, user });
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return replaceSpecialVars({ text: initialText, user, timezone });
}, [group.productionPrompt?.prompt, user]);
const { allVariables, uniqueVariables, variableIndexMap } = useMemo(
@@ -193,7 +194,6 @@ export default function VariableForm({
)}
placeholder={field.config.variable}
maxRows={8}
aria-label={field.config.variable}
/>
);
}}
@@ -202,7 +202,7 @@ export default function VariableForm({
))}
</div>
<div className="flex justify-end">
<Button type="submit" variant="submit" aria-label={localize('com_ui_submit')}>
<Button type="submit" variant="submit">
{localize('com_ui_submit')}
</Button>
</div>

View File

@@ -22,7 +22,8 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
const mainText = useMemo(() => {
const initialText = group?.productionPrompt?.prompt ?? '';
return replaceSpecialVars({ text: initialText, user });
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return replaceSpecialVars({ text: initialText, user, timezone });
}, [group?.productionPrompt?.prompt, user]);
if (!group) {

View File

@@ -118,7 +118,6 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
setIsEditing(false);
}
}}
aria-label={localize('com_ui_prompt_input')}
/>
) : (
<div

View File

@@ -370,11 +370,7 @@ export default function GenericGrantAccessDialog({
<div className="flex gap-2">
<PeoplePickerAdminSettings />
<OGDialogClose asChild>
<Button
variant="outline"
onClick={handleCancel}
aria-label={localize('com_ui_cancel')}
>
<Button variant="outline" onClick={handleCancel}>
{localize('com_ui_cancel')}
</Button>
</OGDialogClose>
@@ -386,7 +382,6 @@ export default function GenericGrantAccessDialog({
(hasChanges && !hasAtLeastOneOwner)
}
className="min-w-[120px]"
aria-label={localize('com_ui_save_changes')}
>
{updatePermissionsMutation.isLoading ? (
<div className="flex items-center gap-2">

View File

@@ -60,7 +60,6 @@ const LabelController: React.FC<LabelControllerProps> = ({
checked={field.value}
onCheckedChange={field.onChange}
value={field.value.toString()}
aria-label={label}
/>
)}
/>
@@ -159,7 +158,6 @@ const PeoplePickerAdminSettings = () => {
<Button
variant={'outline'}
className="btn btn-neutral border-token-border-light relative gap-1 rounded-lg font-medium"
aria-label={localize('com_ui_admin_settings')}
>
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
{localize('com_ui_admin_settings')}

View File

@@ -56,7 +56,6 @@ const LabelController: React.FC<LabelControllerProps> = ({
checked={field.value}
onCheckedChange={field.onChange}
value={field.value.toString()}
aria-label={label}
/>
)}
/>
@@ -153,7 +152,6 @@ const AdminSettings = () => {
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative h-9 w-full gap-1 rounded-lg font-medium"
aria-label={localize('com_ui_admin_settings')}
>
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
{localize('com_ui_admin_settings')}

View File

@@ -17,7 +17,6 @@ const AdvancedButton: React.FC<AdvancedButtonProps> = ({ setActivePanel }) => {
variant={'outline'}
className="btn btn-neutral border-token-border-light relative h-9 w-full gap-1 rounded-lg font-medium"
onClick={() => setActivePanel(Panel.advanced)}
aria-label={localize('com_ui_advanced')}
>
<Settings2 className="h-4 w-4 cursor-pointer" aria-hidden="true" />
{localize('com_ui_advanced')}

View File

@@ -31,7 +31,6 @@ export default function AdvancedPanel() {
onClick={() => {
setActivePanel(Panel.builder);
}}
aria-label={localize('com_ui_back_to_builder')}
>
<div className="advanced-panel-content flex w-full items-center justify-center gap-2">
<ChevronLeft />

View File

@@ -146,9 +146,6 @@ const AgentChain: React.FC<AgentChainProps> = ({ field, currentAgentId }) => {
<button
className="rounded-xl p-1 transition hover:bg-surface-hover"
onClick={() => removeAgentAt(idx)}
aria-label={localize('com_ui_remove_agent_from_chain', {
0: getAgentDetails(agentId)?.name || localize('com_ui_agent'),
})}
>
<X size={18} className="text-text-secondary" />
</button>

View File

@@ -186,11 +186,7 @@ function Avatar({
<Popover.Root open={menuOpen} onOpenChange={setMenuOpen}>
<div className="flex w-full items-center justify-center gap-4">
<Popover.Trigger asChild>
<button
type="button"
className="f h-20 w-20 focus:rounded-full focus:ring-2 focus:ring-ring"
aria-label={localize('com_ui_upload_agent_avatar_label')}
>
<button type="button" className="h-20 w-20">
{previewUrl ? <AgentAvatarRender url={previewUrl} progress={progress} /> : <NoImage />}
</button>
</Popover.Trigger>

View File

@@ -420,16 +420,9 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
type="text"
placeholder={localize('com_ui_support_contact_name_placeholder')}
aria-label="Support contact name"
aria-invalid={error ? 'true' : 'false'}
aria-describedby={error ? 'support-contact-name-error' : undefined}
/>
{error && (
<span
id="support-contact-name-error"
className="text-sm text-red-500 transition duration-300 ease-in-out"
role="alert"
aria-live="polite"
>
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
{error.message}
</span>
)}
@@ -462,16 +455,9 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
type="email"
placeholder={localize('com_ui_support_contact_email_placeholder')}
aria-label="Support contact email"
aria-invalid={error ? 'true' : 'false'}
aria-describedby={error ? 'support-contact-email-error' : undefined}
/>
{error && (
<span
id="support-contact-email-error"
className="text-sm text-red-500 transition duration-300 ease-in-out"
role="alert"
aria-live="polite"
>
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
{error.message}
</span>
)}

View File

@@ -283,13 +283,6 @@ export default function AgentPanel() {
setCurrentAgentId(undefined);
}}
disabled={agentQuery.isInitialLoading}
aria-label={
localize('com_ui_create') +
' ' +
localize('com_ui_new') +
' ' +
localize('com_ui_agent')
}
>
<Plus className="mr-1 h-4 w-4" />
{localize('com_ui_create') +

View File

@@ -117,7 +117,6 @@ function SwitchItem({
className="ml-4"
data-testid={id}
disabled={disabled}
aria-label={label}
/>
</div>
</HoverCard>

View File

@@ -61,7 +61,6 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()}
disabled={runCodeIsEnabled ? false : !isToolAuthenticated}
aria-label={localize('com_ui_run_code')}
/>
)}
/>
@@ -82,11 +81,7 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
</button>
<div className="ml-2 flex gap-2">
{isUserProvided && (isToolAuthenticated || runCodeIsEnabled) && (
<button
type="button"
onClick={() => setIsDialogOpen(true)}
aria-label={localize('com_ui_add_api_key')}
>
<button type="button" onClick={() => setIsDialogOpen(true)}>
<KeyRoundIcon className="h-5 w-5 text-text-primary" />
</button>
)}

View File

@@ -104,7 +104,6 @@ export default function ApiKeyDialog({
<Button
onClick={onRevoke}
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
aria-label={localize('com_ui_revoke')}
>
{localize('com_ui_revoke')}
</Button>

View File

@@ -32,7 +32,6 @@ function FileSearchCheckbox() {
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()}
aria-label={localize('com_agents_enable_file_search')}
/>
)}
/>

View File

@@ -104,16 +104,15 @@ export function AvatarMenu({
className="flex min-w-[100px] max-w-xs flex-col rounded-xl border border-gray-400 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-850 dark:text-white"
sideOffset={5}
>
<button
type="button"
<div
role="menuitem"
className="group m-1.5 flex cursor-pointer gap-2 rounded-lg p-2.5 text-sm hover:bg-gray-100 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-800 dark:hover:bg-white/5"
tabIndex={0}
tabIndex={-1}
data-orientation="vertical"
onClick={onItemClick}
>
{localize('com_ui_upload_image')}
</button>
</div>
{/* <Popover.Close
role="menuitem"
className="group m-1.5 flex cursor-pointer gap-2 rounded p-2.5 text-sm hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-white/5"

View File

@@ -210,15 +210,10 @@ export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
control={control}
rules={{ required: true }}
render={({ field }) => (
<Checkbox
id="trust-checkbox"
checked={field.value}
onCheckedChange={field.onChange}
aria-labelledby="trust-label"
/>
<Checkbox id="trust" checked={field.value} onCheckedChange={field.onChange} />
)}
/>
<Label id="trust-label" htmlFor="trust-checkbox" className="flex flex-col">
<Label htmlFor="trust" className="flex flex-col">
{localize('com_ui_trust_app')}
<span className="text-xs text-text-secondary">
{localize('com_agents_mcp_trust_subtext')}
@@ -274,10 +269,6 @@ export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
checked={selectedTools.includes(tool)}
onCheckedChange={() => handleToolToggle(tool)}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
aria-label={tool
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')}
/>
<span className="text-token-text-primary">
{tool

View File

@@ -162,12 +162,6 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo })
}
}}
tabIndex={isExpanded ? 0 : -1}
aria-label={
selectedTools.length === serverInfo.tools?.length &&
selectedTools.length > 0
? localize('com_ui_deselect_all')
: localize('com_ui_select_all')
}
/>
</div>
@@ -258,7 +252,6 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo })
className={cn(
'relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer rounded border border-border-medium transition-[border-color] duration-200 hover:border-border-heavy focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background',
)}
aria-label={subTool.metadata.name}
/>
<span className="text-token-text-primary select-none">
{subTool.metadata.name}

View File

@@ -102,7 +102,6 @@ export default function ModelPanel({
onClick={() => {
setActivePanel(Panel.builder);
}}
aria-label={localize('com_ui_back_to_builder')}
>
<div className="model-panel-content flex w-full items-center justify-center gap-2">
<ChevronLeft />

View File

@@ -69,7 +69,6 @@ export default function Action({
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()}
disabled={webSearchIsEnabled ? false : !isToolAuthenticated}
aria-label={localize('com_ui_web_search')}
/>
)}
/>

View File

@@ -250,11 +250,7 @@ export default function ApiKeyDialog({
}}
buttons={
isToolAuthenticated && (
<Button
onClick={onRevoke}
className="bg-red-500 text-white hover:bg-red-600"
aria-label={localize('com_ui_revoke')}
>
<Button onClick={onRevoke} className="bg-red-500 text-white hover:bg-red-600">
{localize('com_ui_revoke')}
</Button>
)

View File

@@ -16,7 +16,6 @@ const VersionButton = ({ setActivePanel }: VersionButtonProps) => {
variant={'outline'}
className="btn btn-neutral border-token-border-light relative h-9 w-full gap-1 rounded-lg font-medium"
onClick={() => setActivePanel(Panel.version)}
aria-label={localize('com_ui_agent_version')}
>
<History className="h-4 w-4 cursor-pointer" aria-hidden="true" />
{localize('com_ui_agent_version')}

View File

@@ -112,7 +112,6 @@ const BookmarkTable = () => {
variant="outline"
size="sm"
className="w-full gap-2 text-sm"
aria-label={localize('com_ui_bookmarks_new')}
onClick={() => setOpen(!open)}
>
<BookmarkPlusIcon className="size-4" />

View File

@@ -213,11 +213,7 @@ function Avatar({
<Popover.Root open={menuOpen} onOpenChange={setMenuOpen}>
<div className="flex w-full items-center justify-center gap-4">
<Popover.Trigger asChild>
<button
type="button"
className="h-20 w-20"
aria-label={localize('com_ui_upload_avatar_label')}
>
<button type="button" className="h-20 w-20">
{previewUrl ? <AssistantAvatar url={previewUrl} progress={progress} /> : <NoImage />}
</button>
</Popover.Trigger>

View File

@@ -31,7 +31,6 @@ export default function Code({ version }: { version: number | string }) {
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()}
aria-labelledby={Capabilities.code_interpreter}
/>
)}
/>
@@ -45,7 +44,6 @@ export default function Code({ version }: { version: number | string }) {
}
>
<label
id={Capabilities.code_interpreter}
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={Capabilities.code_interpreter}
>

View File

@@ -21,12 +21,10 @@ export default function ImageVision() {
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()}
aria-labelledby={Capabilities.image_vision}
/>
)}
/>
<label
id={Capabilities.image_vision}
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={Capabilities.image_vision}
onClick={() =>

View File

@@ -60,13 +60,11 @@ export default function Retrieval({
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()}
aria-labelledby={Capabilities.retrieval}
/>
)}
/>
<div className="flex items-center space-x-2">
<label
id={Capabilities.retrieval}
className={cn(
'form-check-label text-token-text-primary w-full select-none',
isDisabled ? 'cursor-no-drop opacity-50' : 'cursor-pointer',

View File

@@ -17,7 +17,6 @@ export const columns: ColumnDef<TFile | undefined>[] = [
variant="ghost"
className="hover:bg-surface-hover"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
aria-label={localize('com_ui_name')}
>
{localize('com_ui_name')}
<ArrowUpDown className="ml-2 h-4 w-4" />
@@ -41,7 +40,6 @@ export const columns: ColumnDef<TFile | undefined>[] = [
variant="ghost"
className="hover:bg-surface-hover"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
aria-label={localize('com_ui_date')}
>
{localize('com_ui_date')}
<ArrowUpDown className="ml-2 h-4 w-4" />

View File

@@ -127,12 +127,7 @@ function MCPPanelContent() {
return (
<div className="h-auto max-w-full space-y-4 overflow-x-hidden py-2">
<Button
variant="outline"
onClick={handleGoBackToList}
size="sm"
aria-label={localize('com_ui_back')}
>
<Button variant="outline" onClick={handleGoBackToList} size="sm">
<ChevronLeft className="mr-1 h-4 w-4" />
{localize('com_ui_back')}
</Button>
@@ -171,7 +166,6 @@ function MCPPanelContent() {
size="sm"
variant="destructive"
onClick={() => handleConfigRevoke(selectedServerNameForEditing)}
aria-label={localize('com_ui_oauth_revoke')}
>
<Trash2 className="h-4 w-4" />
{localize('com_ui_oauth_revoke')}
@@ -194,7 +188,6 @@ function MCPPanelContent() {
variant="outline"
className="flex-1 justify-start dark:hover:bg-gray-700"
onClick={() => handleServerClickToEdit(server.serverName)}
aria-label={localize('com_ui_edit') + ' ' + server.serverName}
>
<div className="flex items-center gap-2">
<span>{server.serverName}</span>

View File

@@ -39,7 +39,6 @@ const LabelController: React.FC<LabelControllerProps> = ({ control, memoryPerm,
checked={field.value}
onCheckedChange={field.onChange}
value={field.value.toString()}
aria-label={label}
/>
)}
/>
@@ -142,7 +141,6 @@ const AdminSettings = () => {
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative h-9 w-full gap-1 rounded-lg font-medium"
aria-label={localize('com_ui_admin_settings')}
>
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
{localize('com_ui_admin_settings')}

View File

@@ -147,7 +147,6 @@ export default function MemoryCreateDialog({
onClick={handleSave}
disabled={isLoading || !key.trim() || !value.trim()}
className="text-white"
aria-label={localize('com_ui_create_memory')}
>
{isLoading ? <Spinner className="size-4" /> : localize('com_ui_create')}
</Button>

View File

@@ -192,7 +192,6 @@ export default function MemoryEditDialog({
type="button"
variant="submit"
onClick={handleSave}
aria-label={localize('com_ui_save')}
disabled={isLoading || !key.trim() || !value.trim()}
className="text-white"
>

View File

@@ -305,11 +305,7 @@ export default function MemoryViewer() {
<div className="flex w-full justify-end">
<MemoryCreateDialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<OGDialogTrigger asChild>
<Button
variant="outline"
className="w-full bg-transparent"
aria-label={localize('com_ui_create_memory')}
>
<Button variant="outline" className="w-full bg-transparent">
<Plus className="size-4" aria-hidden />
{localize('com_ui_create_memory')}
</Button>

View File

@@ -76,7 +76,6 @@ function DynamicCheckbox({
checked={selectedValue}
onCheckedChange={handleCheckedChange}
className="mt-[2px] focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0"
aria-label={localize(label as TranslationKeys)}
/>
</div>
</HoverCardTrigger>

View File

@@ -179,7 +179,6 @@ function DynamicSlider({
min={range ? range.min : 0}
step={range ? (range.step ?? 1) : 1}
controls={false}
aria-label={localize(label as TranslationKeys)}
className={cn(
defaultTextProps,
cn(
@@ -193,7 +192,6 @@ function DynamicSlider({
id={`${settingKey}-dynamic-setting-input`}
disabled={readonly}
value={getDisplayValue(selectedValue)}
aria-label={localize(label as TranslationKeys)}
onChange={() => ({})}
className={cn(
defaultTextProps,
@@ -216,7 +214,6 @@ function DynamicSlider({
onValueChange={(value) => handleValueChange(value[0])}
onDoubleClick={() => setInputValue(defaultValue as string | number)}
max={max}
aria-label={localize(label as TranslationKeys)}
min={range ? range.min : 0}
step={range ? (range.step ?? 1) : 1}
className="flex h-4 w-full"

View File

@@ -67,9 +67,6 @@ function DynamicSwitch({
onCheckedChange={handleCheckedChange}
disabled={readonly}
className="flex"
aria-label={
labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey
}
/>
</HoverCardTrigger>
{description && (

View File

@@ -75,7 +75,6 @@ function DynamicTextarea({
disabled={readonly}
value={inputValue ?? ''}
onChange={setInputValue}
aria-label={localize(label as TranslationKeys)}
placeholder={
placeholderCode
? (localize(placeholder as TranslationKeys) ?? placeholder)

View File

@@ -6,4 +6,3 @@ export { default as useAgentCapabilities } from './useAgentCapabilities';
export { default as useGetAgentsConfig } from './useGetAgentsConfig';
export { default as useAgentDefaultPermissionLevel } from './useAgentDefaultPermissionLevel';
export { default as useAgentToolPermissions } from './useAgentToolPermissions';
export * from './useApplyModelSpecAgents';

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