Compare commits

..

12 Commits

Author SHA1 Message Date
Danny Avila
e1ad235f17 🌍 i18n: Add missing com_ui_no_changes 2025-08-25 17:59:46 -04:00
Danny Avila
4a0b329e3e v0.8.0-rc3 (#9269)
* chore: bump data-provider to v0.8.004

* 📦 chore: bump @librechat/data-schemas version to 0.0.20

* 📦 chore: bump @librechat/api version to 1.3.4

*  v0.8.0-rc3

* docs: update README

* docs: update README

* docs: enhance multilingual UI section in README
2025-08-25 17:35:21 -04:00
github-actions[bot]
a22359de5e 🌍 i18n: Update translation.json with latest translations (#9267)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-25 17:14:51 -04:00
Danny Avila
bbfe4002eb 🏷️ chore: Add Missing Localizations for Agents, Categories, Bookmarks (#9266)
* fix: error when updating bookmarks if no query data

* feat: localize bookmark dialog, form labels and validation messages, also improve validation

* feat: add localization for EmptyPromptPreview component and update translation.json

* chore: add missing localizations for static UI text

* chore: update AgentPanelContextType and useGetAgentsConfig to support null configurations

* refactor: update agent categories to support localization and custom properties, improve related typing

* ci: add localization for 'All' category and update tab names in accessibility tests

* chore: remove unused AgentCategoryDisplay component and its tests

* chore: add localization handling for agent category selector

* chore: enhance AgentCard to support localized category labels and add related tests

* chore: enhance i18n unused keys detection to include additional source directories and improve handling for agent category keys
2025-08-25 13:54:13 -04:00
Marco Beretta
94426a3cae 🎭 refactor: Avatar Loading UX and Fix Initials Rendering Bugs (#9261)
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-08-25 12:06:00 -04:00
Danny Avila
e559f0f4dc 📜 chore: Add Timestamp to Error logs (#9262) 2025-08-25 11:05:15 -04:00
github-actions[bot]
15c9c7e1f4 🌍 i18n: Update translation.json with latest translations (#9250)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-25 10:52:24 -04:00
Danny Avila
ac641e7cba 🗄️ refactor: Resource Migration Scripts for DocumentDB Compatibility (#9249)
* refactor: Resource Migration Scripts for DocumentDB compatibility

* fix: Correct type annotation for `db` parameter in ensureCollectionExists function
2025-08-25 03:01:50 -04:00
Danny Avila
1915d7b195 🧮 fix: Properly Escape Currency and Prevent Code Block LaTeX Bugs (#9248)
* fix(latex): prevent LaTeX conversion when closing $ is preceded by backtick

When text contained patterns like "$lookup namespace" followed by "`$lookup`",
the regex would match from the first $ to the backtick's $, treating the entire
span as a LaTeX expression. This caused programming constructs to be incorrectly
converted to double dollars.

- Added negative lookbehind (?<!`) to single dollar regex
- Prevents matching when closing $ immediately follows a backtick
- Fixes issues with inline code blocks containing $ symbols

* fix(latex): detect currency amounts with 4+ digits without commas

The currency regex pattern \d{1,3} only matched amounts with 1-3 initial digits,
causing amounts like $1157.90 to be interpreted as LaTeX instead of currency.
This resulted in text like "$1157.90 (text) + $500 (text) = $1657.90" being
incorrectly converted to a single LaTeX expression.

- Changed pattern from \d{1,3} to \d+ to match any number of initial digits
- Now properly escapes $1000, $10000, $123456, etc. without requiring commas
- Maintains support for comma-formatted amounts like $1,234.56

* fix(latex): support currency with unlimited decimal places

The currency regex limited decimal places to 1-2 digits (\.\d{1,2}), which
failed to properly escape amounts with more precision like cryptocurrency
values ($0.00001234), gas prices ($3.999), or exchange rates ($1.23456).

- Changed decimal pattern from \.\d{1,2} to \.\d+
- Now supports any number of decimal places
- Handles edge cases like scientific calculations and high-precision values
2025-08-25 02:44:13 -04:00
github-actions[bot]
c2f4b383f2 🌍 i18n: Update translation.json with latest translations (#9228)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-24 12:42:57 -04:00
Danny Avila
939af59950 🏄‍♂️ refactor: Improve Cancelled Stream Handling for Pending Authentication (#9235) 2025-08-24 12:42:34 -04:00
Danny Avila
7d08da1a8a 🪜 fix: userMCPAuthMap Sequential Agents assignment 2025-08-24 11:37:51 -04:00
64 changed files with 1330 additions and 786 deletions

View File

@@ -1,5 +1,10 @@
name: Detect Unused i18next Strings
# This workflow checks for unused i18n keys in translation files.
# It has special handling for:
# - com_ui_special_var_* keys that are dynamically constructed
# - com_agents_category_* keys that are stored in the database and used dynamically
on:
pull_request:
paths:
@@ -7,6 +12,7 @@ on:
- "api/**"
- "packages/data-provider/src/**"
- "packages/client/**"
- "packages/data-schemas/src/**"
jobs:
detect-unused-i18n-keys:
@@ -24,7 +30,7 @@ jobs:
# Define paths
I18N_FILE="client/src/locales/en/translation.json"
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client")
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client" "packages/data-schemas/src")
# Check if translation file exists
if [[ ! -f "$I18N_FILE" ]]; then
@@ -52,6 +58,31 @@ jobs:
fi
done
# Also check if the key is directly used somewhere
if [[ "$FOUND" == false ]]; then
for DIR in "${SOURCE_DIRS[@]}"; do
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
FOUND=true
break
fi
done
fi
# Special case for agent category keys that are dynamically used from database
elif [[ "$KEY" == com_agents_category_* ]]; then
# Check if agent category localization is being used
for DIR in "${SOURCE_DIRS[@]}"; do
# Check for dynamic category label/description usage
if grep -r --include=\*.{js,jsx,ts,tsx} -E "category\.(label|description).*startsWith.*['\"]com_" "$DIR" > /dev/null 2>&1 || \
# Check for the method that defines these keys
grep -r --include=\*.{js,jsx,ts,tsx} "ensureDefaultCategories" "$DIR" > /dev/null 2>&1 || \
# Check for direct usage in agentCategory.ts
grep -r --include=\*.ts -E "label:.*['\"]$KEY['\"]" "$DIR" > /dev/null 2>&1 || \
grep -r --include=\*.ts -E "description:.*['\"]$KEY['\"]" "$DIR" > /dev/null 2>&1; then
FOUND=true
break
fi
done
# Also check if the key is directly used somewhere
if [[ "$FOUND" == false ]]; then
for DIR in "${SOURCE_DIRS[@]}"; do

View File

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

View File

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

View File

@@ -1,147 +0,0 @@
# Model Spec Subfolder Support
This enhancement adds the ability to organize model specs into subfolders/categories for better organization and user experience.
## Feature Overview
Model specs can now be grouped into folders by adding an optional `folder` field to each spec. This helps organize related models together, making it easier for users to find and select the appropriate model for their needs.
## Configuration
### Basic Usage
Add a `folder` field to any model spec in your `librechat.yaml`:
```yaml
modelSpecs:
list:
- name: "gpt4_turbo"
label: "GPT-4 Turbo"
folder: "OpenAI Models" # This spec will appear under "OpenAI Models" folder
preset:
endpoint: "openAI"
model: "gpt-4-turbo-preview"
```
### Folder Structure
- **With Folder**: Model specs with the `folder` field will be grouped under that folder name
- **Without Folder**: Model specs without the `folder` field appear at the root level
- **Multiple Folders**: You can create as many folders as needed to organize your models
- **Alphabetical Sorting**: Folders are sorted alphabetically, and specs within folders are sorted by their `order` field or label
### Example Configuration
```yaml
modelSpecs:
list:
# OpenAI Models Category
- name: "gpt4_turbo"
label: "GPT-4 Turbo"
folder: "OpenAI Models"
preset:
endpoint: "openAI"
model: "gpt-4-turbo-preview"
- name: "gpt35_turbo"
label: "GPT-3.5 Turbo"
folder: "OpenAI Models"
preset:
endpoint: "openAI"
model: "gpt-3.5-turbo"
# Anthropic Models Category
- name: "claude3_opus"
label: "Claude 3 Opus"
folder: "Anthropic Models"
preset:
endpoint: "anthropic"
model: "claude-3-opus-20240229"
# Root level model (no folder)
- name: "quick_chat"
label: "Quick Chat"
preset:
endpoint: "openAI"
model: "gpt-3.5-turbo"
```
## UI Features
### Folder Display
- Folders are displayed with expand/collapse functionality
- Folder icons change between open/closed states
- Indentation shows the hierarchy clearly
### Search Integration
- When searching for models, the folder path is shown for context
- Search works across all models regardless of folder structure
### User Experience
- Folders start expanded by default for easy access
- Click on folder header to expand/collapse
- Selected model is highlighted with a checkmark
- Folder state is preserved during the session
## Benefits
1. **Better Organization**: Group related models together (e.g., by provider, capability, or use case)
2. **Improved Navigation**: Users can quickly find models in organized categories
3. **Scalability**: Handles large numbers of model specs without overwhelming the UI
4. **Backward Compatible**: Existing configurations without folders continue to work
5. **Flexible Structure**: Mix foldered and non-foldered specs as needed
## Use Cases
### By Provider
```yaml
folder: "OpenAI Models"
folder: "Anthropic Models"
folder: "Google Models"
```
### By Capability
```yaml
folder: "Vision Models"
folder: "Code Models"
folder: "Creative Writing"
```
### By Performance Tier
```yaml
folder: "Premium Models"
folder: "Standard Models"
folder: "Budget Models"
```
### By Department/Team
```yaml
folder: "Engineering Team"
folder: "Marketing Team"
folder: "Research Team"
```
## Implementation Details
### Type Changes
- Added optional `folder?: string` field to `TModelSpec` type
- Updated `tModelSpecSchema` to include the folder field validation
### Components
- Created `ModelSpecFolder` component for rendering folder structure
- Updated `ModelSelector` to use folder-aware rendering
- Enhanced search results to show folder context
### Behavior
- Folders are collapsible with state management
- Models are sorted within folders by order/label
- Root-level models appear after all folders
## Migration
No migration needed - the feature is fully backward compatible. Existing model specs without the `folder` field will continue to work and appear at the root level.
## See Also
- `librechat.example.subfolder.yaml` - Complete example configuration
- GitHub Issue #9165 - Original feature request

View File

@@ -65,8 +65,10 @@
- 🔦 **Agents & Tools Integration**:
- **[LibreChat Agents](https://www.librechat.ai/docs/features/agents)**:
- No-Code Custom Assistants: Build specialized, AI-driven helpers without coding
- Flexible & Extensible: Use MCP Servers, tools, file search, code execution, and more
- No-Code Custom Assistants: Build specialized, AI-driven helpers
- Agent Marketplace: Discover and deploy community-built agents
- Collaborative Sharing: Share agents with specific users and groups
- Flexible & Extensible: Use MCP Servers, tools, file search, code execution, and more
- Compatible with Custom Endpoints, OpenAI, Azure, Anthropic, AWS Bedrock, Google, Vertex AI, Responses API, and more
- [Model Context Protocol (MCP) Support](https://modelcontextprotocol.io/clients#librechat) for Tools
@@ -87,15 +89,18 @@
- Create, Save, & Share Custom Presets
- Switch between AI Endpoints and Presets mid-chat
- Edit, Resubmit, and Continue Messages with Conversation branching
- Create and share prompts with specific users and groups
- [Fork Messages & Conversations](https://www.librechat.ai/docs/features/fork) for Advanced Context control
- 💬 **Multimodal & File Interactions**:
- Upload and analyze images with Claude 3, GPT-4.5, GPT-4o, o1, Llama-Vision, and Gemini 📸
- Chat with Files using Custom Endpoints, OpenAI, Azure, Anthropic, AWS Bedrock, & Google 🗃️
- 🌎 **Multilingual UI**:
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
- 🌎 **Multilingual UI**:
- English, 中文 (简体), 中文 (繁體), العربية, Deutsch, Español, Français, Italiano
- Polski, Português (PT), Português (BR), Русский, 日本語, Svenska, 한국어, Tiếng Việt
- Türkçe, Nederlands, עברית, Català, Čeština, Dansk, Eesti, فارسی
- Suomi, Magyar, Հայերեն, Bahasa Indonesia, ქართული, Latviešu, ไทย, ئۇيغۇرچە
- 🧠 **Reasoning UI**:
- Dynamic Reasoning UI for Chain-of-Thought/Reasoning AI models like DeepSeek-R1

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
"version": "v0.8.0-rc2",
"version": "v0.8.0-rc3",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",

View File

@@ -46,7 +46,7 @@ router.use('/tools', tools);
/**
* Get all agent categories with counts
* @route GET /agents/marketplace/categories
* @route GET /agents/categories
*/
router.get('/categories', v1.getAgentCategories);
/**

View File

@@ -149,7 +149,11 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
endpointOption,
allowedProviders,
});
Object.assign(userMCPAuthMap, config.userMCPAuthMap ?? {});
if (userMCPAuthMap != null) {
Object.assign(userMCPAuthMap, config.userMCPAuthMap ?? {});
} else {
userMCPAuthMap = config.userMCPAuthMap;
}
agentConfigs.set(agentId, config);
}
}

View File

@@ -1,3 +1,4 @@
const mongoose = require('mongoose');
const { logger } = require('@librechat/data-schemas');
const {
logAgentMigrationWarning,
@@ -16,7 +17,8 @@ const { findRoleByIdentifier } = require('~/models');
async function checkMigrations() {
try {
const agentMigrationResult = await checkAgentPermissionsMigration({
db: {
mongoose,
methods: {
findRoleByIdentifier,
getProjectByName,
},
@@ -28,7 +30,8 @@ async function checkMigrations() {
}
try {
const promptMigrationResult = await checkPromptPermissionsMigration({
db: {
mongoose,
methods: {
findRoleByIdentifier,
getProjectByName,
},

View File

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

View File

@@ -225,7 +225,8 @@ export type AgentPanelContextType = {
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
agent_id?: string;
agentsConfig?: t.TAgentsEndpoint;
agentsConfig?: t.TAgentsEndpoint | null;
endpointsConfig?: t.TEndpointsConfig | null;
};
export type AgentModelPanelProps = {

View File

@@ -1,8 +1,8 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Label } from '@librechat/client';
import type t from 'librechat-data-provider';
import { useLocalize, TranslationKeys, useAgentCategories } from '~/hooks';
import { cn, renderAgentAvatar, getContactDisplayName } from '~/utils';
import { useLocalize } from '~/hooks';
interface AgentCardProps {
agent: t.Agent; // The agent data to display
@@ -15,6 +15,21 @@ interface AgentCardProps {
*/
const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' }) => {
const localize = useLocalize();
const { categories } = useAgentCategories();
const categoryLabel = useMemo(() => {
if (!agent.category) return '';
const category = categories.find((cat) => cat.value === agent.category);
if (category) {
if (category.label && category.label.startsWith('com_')) {
return localize(category.label as TranslationKeys);
}
return category.label;
}
return agent.category.charAt(0).toUpperCase() + agent.category.slice(1);
}, [agent.category, categories, localize]);
return (
<div
@@ -49,9 +64,7 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' })
{/* Category tag */}
{agent.category && (
<div className="inline-flex items-center rounded-md border-border-xheavy bg-surface-active-alt px-2 py-1 text-xs font-medium">
<Label className="line-clamp-1 font-normal">
{agent.category.charAt(0).toUpperCase() + agent.category.slice(1)}
</Label>
<Label className="line-clamp-1 font-normal">{categoryLabel}</Label>
</div>
)}
</div>

View File

@@ -1,62 +0,0 @@
import React from 'react';
import { useAgentCategories } from '~/hooks/Agents';
import { cn } from '~/utils';
interface AgentCategoryDisplayProps {
category?: string;
className?: string;
showIcon?: boolean;
iconClassName?: string;
showEmptyFallback?: boolean;
}
/**
* Component to display an agent category with proper translation
*
* @param category - The category value (e.g., "general", "hr", etc.)
* @param className - Optional className for the container
* @param showIcon - Whether to show the category icon
* @param iconClassName - Optional className for the icon
* @param showEmptyFallback - Whether to show a fallback for empty categories
*/
const AgentCategoryDisplay: React.FC<AgentCategoryDisplayProps> = ({
category,
className = '',
showIcon = true,
iconClassName = 'h-4 w-4 mr-2',
showEmptyFallback = false,
}) => {
const { categories, emptyCategory } = useAgentCategories();
// Find the category in our processed categories list
const categoryItem = categories.find((c) => c.value === category);
// Handle empty string case differently than undefined/null
if (category === '') {
if (!showEmptyFallback) {
return null;
}
// Show the empty category placeholder
return (
<div className={cn('flex items-center text-gray-400', className)}>
<span>{emptyCategory.label}</span>
</div>
);
}
// No category or unknown category
if (!category || !categoryItem) {
return null;
}
return (
<div className={cn('flex items-center', className)}>
{showIcon && categoryItem.icon && (
<span className={cn('flex-shrink-0', iconClassName)}>{categoryItem.icon}</span>
)}
<span>{categoryItem.label}</span>
</div>
);
};
export default AgentCategoryDisplay;

View File

@@ -1,8 +1,8 @@
import React from 'react';
import type t from 'librechat-data-provider';
import { useMediaQuery } from '@librechat/client';
import type t from 'librechat-data-provider';
import { useLocalize, TranslationKeys } from '~/hooks';
import { SmartLoader } from './SmartLoader';
import { useLocalize } from '~/hooks/';
import { cn } from '~/utils';
/**
@@ -36,14 +36,17 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
const localize = useLocalize();
const isSmallScreen = useMediaQuery('(max-width: 768px)');
// Helper function to get category display name from database data
/** Helper function to get category display name from database data */
const getCategoryDisplayName = (category: t.TCategory) => {
// Special cases for system categories
if (category.value === 'promoted') {
return localize('com_agents_top_picks');
}
if (category.value === 'all') {
return 'All';
return localize('com_agents_all_category');
}
if (category.label && category.label.startsWith('com_')) {
return localize(category.label as TranslationKeys);
}
// Use database label or fallback to capitalized value
return category.label || category.value.charAt(0).toUpperCase() + category.value.slice(1);
@@ -158,7 +161,11 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
aria-selected={activeTab === category.value}
aria-controls={`tabpanel-${category.value}`}
tabIndex={activeTab === category.value ? 0 : -1}
aria-label={`${getCategoryDisplayName(category)} tab (${index + 1} of ${categories.length})`}
aria-label={localize('com_agents_category_tab_label', {
category: getCategoryDisplayName(category),
position: index + 1,
total: categories.length,
})}
>
{getCategoryDisplayName(category)}
{/* Underline for active tab */}

View File

@@ -7,8 +7,8 @@ import { TooltipAnchor, Button, NewChatIcon, useMediaQuery } from '@librechat/cl
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';
import { useGetEndpointsQuery, useGetAgentCategoriesQuery } from '~/data-provider';
import { useDocumentTitle, useHasAccess, useLocalize } from '~/hooks';
import MarketplaceAdminSettings from './MarketplaceAdminSettings';
import { SidePanelProvider, useChatContext } from '~/Providers';
import { MarketplaceProvider } from './MarketplaceContext';
@@ -381,8 +381,8 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
}
if (displayCategory === 'all') {
return {
name: 'All Agents',
description: 'Browse all shared agents across all categories',
name: localize('com_agents_all'),
description: localize('com_agents_all_description'),
};
}
@@ -392,8 +392,12 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
);
if (categoryData) {
return {
name: categoryData.label,
description: categoryData.description || '',
name: categoryData.label?.startsWith('com_')
? localize(categoryData.label as TranslationKeys)
: categoryData.label,
description: categoryData.description?.startsWith('com_')
? localize(categoryData.description as TranslationKeys)
: categoryData.description || '',
};
}
@@ -455,8 +459,8 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
}
if (nextCategory === 'all') {
return {
name: 'All Agents',
description: 'Browse all shared agents across all categories',
name: localize('com_agents_all'),
description: localize('com_agents_all_description'),
};
}
@@ -466,8 +470,16 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
);
if (categoryData) {
return {
name: categoryData.label,
description: categoryData.description || '',
name: categoryData.label?.startsWith('com_')
? localize(categoryData.label as TranslationKeys)
: categoryData.label,
description: categoryData.description?.startsWith('com_')
? localize(
categoryData.description as Parameters<
typeof localize
>[0],
)
: categoryData.description || '',
};
}

View File

@@ -61,6 +61,7 @@ const mockLocalize = jest.fn((key: string, options?: any) => {
com_agents_search_empty_heading: 'No search results',
com_agents_created_by: 'by',
com_agents_top_picks: 'Top Picks',
com_agents_all_category: 'All',
// ErrorDisplay translations
com_agents_error_suggestion_generic: 'Try refreshing the page or check your network connection',
com_agents_error_network_title: 'Network Error',
@@ -199,7 +200,7 @@ describe('Accessibility Improvements', () => {
/>,
);
const promotedTab = screen.getByRole('tab', { name: /Top Picks tab/ });
const promotedTab = screen.getByRole('tab', { name: /Top Picks category/ });
// Test arrow key navigation
fireEvent.keyDown(promotedTab, { key: 'ArrowRight' });
@@ -226,8 +227,8 @@ describe('Accessibility Improvements', () => {
/>,
);
const promotedTab = screen.getByRole('tab', { name: /Top Picks tab/ });
const allTab = screen.getByRole('tab', { name: /All tab/ });
const promotedTab = screen.getByRole('tab', { name: /Top Picks category/ });
const allTab = screen.getByRole('tab', { name: /All category/ });
// Active tab should be focusable
expect(promotedTab).toHaveAttribute('tabIndex', '0');

View File

@@ -8,10 +8,42 @@ import type t from 'librechat-data-provider';
jest.mock('~/hooks/useLocalize', () => () => (key: string) => {
const mockTranslations: Record<string, string> = {
com_agents_created_by: 'Created by',
com_agents_agent_card_label: '{{name}} agent. {{description}}',
com_agents_category_general: 'General',
com_agents_category_hr: 'Human Resources',
};
return mockTranslations[key] || key;
});
// Mock useAgentCategories hook
jest.mock('~/hooks', () => ({
useLocalize: () => (key: string, values?: Record<string, string>) => {
const mockTranslations: Record<string, string> = {
com_agents_created_by: 'Created by',
com_agents_agent_card_label: '{{name}} agent. {{description}}',
com_agents_category_general: 'General',
com_agents_category_hr: 'Human Resources',
};
let translation = mockTranslations[key] || key;
// Replace placeholders with actual values
if (values) {
Object.entries(values).forEach(([placeholder, value]) => {
translation = translation.replace(new RegExp(`{{${placeholder}}}`, 'g'), value);
});
}
return translation;
},
useAgentCategories: () => ({
categories: [
{ value: 'general', label: 'com_agents_category_general' },
{ value: 'hr', label: 'com_agents_category_hr' },
{ value: 'custom', label: 'Custom Category' }, // Non-localized custom category
],
}),
}));
describe('AgentCard', () => {
const mockAgent: t.Agent = {
id: '1',
@@ -200,6 +232,49 @@ describe('AgentCard', () => {
const card = screen.getByRole('button');
expect(card).toHaveAttribute('tabIndex', '0');
expect(card).toHaveAttribute('aria-label', 'com_agents_agent_card_label');
expect(card).toHaveAttribute(
'aria-label',
'Test Agent agent. A test agent for testing purposes',
);
});
it('displays localized category label', () => {
const agentWithCategory = {
...mockAgent,
category: 'general',
};
render(<AgentCard agent={agentWithCategory} onClick={mockOnClick} />);
expect(screen.getByText('General')).toBeInTheDocument();
});
it('displays custom category label', () => {
const agentWithCustomCategory = {
...mockAgent,
category: 'custom',
};
render(<AgentCard agent={agentWithCustomCategory} onClick={mockOnClick} />);
expect(screen.getByText('Custom Category')).toBeInTheDocument();
});
it('displays capitalized fallback for unknown category', () => {
const agentWithUnknownCategory = {
...mockAgent,
category: 'unknown',
};
render(<AgentCard agent={agentWithUnknownCategory} onClick={mockOnClick} />);
expect(screen.getByText('Unknown')).toBeInTheDocument();
});
it('does not display category tag when category is not provided', () => {
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
expect(screen.queryByText('General')).not.toBeInTheDocument();
expect(screen.queryByText('Unknown')).not.toBeInTheDocument();
});
});

View File

@@ -1,90 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import AgentCategoryDisplay from '../AgentCategoryDisplay';
// Mock the useAgentCategories hook
jest.mock('~/hooks/Agents', () => ({
useAgentCategories: () => ({
categories: [
{
value: 'general',
label: 'General',
icon: <span data-testid="icon-general">{''}</span>,
className: 'w-full',
},
{
value: 'hr',
label: 'HR',
icon: <span data-testid="icon-hr">{''}</span>,
className: 'w-full',
},
{
value: 'rd',
label: 'R&D',
icon: <span data-testid="icon-rd">{''}</span>,
className: 'w-full',
},
{
value: 'finance',
label: 'Finance',
icon: <span data-testid="icon-finance">{''}</span>,
className: 'w-full',
},
],
emptyCategory: {
value: '',
label: 'General',
className: 'w-full',
},
}),
}));
describe('AgentCategoryDisplay', () => {
it('should display the proper label for a category', () => {
render(<AgentCategoryDisplay category="rd" />);
expect(screen.getByText('R&D')).toBeInTheDocument();
});
it('should display the icon when showIcon is true', () => {
render(<AgentCategoryDisplay category="finance" showIcon={true} />);
expect(screen.getByTestId('icon-finance')).toBeInTheDocument();
expect(screen.getByText('Finance')).toBeInTheDocument();
});
it('should not display the icon when showIcon is false', () => {
render(<AgentCategoryDisplay category="hr" showIcon={false} />);
expect(screen.queryByTestId('icon-hr')).not.toBeInTheDocument();
expect(screen.getByText('HR')).toBeInTheDocument();
});
it('should apply custom classnames', () => {
render(<AgentCategoryDisplay category="general" className="test-class" />);
expect(screen.getByText('General').parentElement).toHaveClass('test-class');
});
it('should not render anything for unknown categories', () => {
const { container } = render(<AgentCategoryDisplay category="unknown" />);
expect(container).toBeEmptyDOMElement();
});
it('should not render anything when no category is provided', () => {
const { container } = render(<AgentCategoryDisplay />);
expect(container).toBeEmptyDOMElement();
});
it('should not render anything for empty category when showEmptyFallback is false', () => {
const { container } = render(<AgentCategoryDisplay category="" />);
expect(container).toBeEmptyDOMElement();
});
it('should render empty category placeholder when showEmptyFallback is true', () => {
render(<AgentCategoryDisplay category="" showEmptyFallback={true} />);
expect(screen.getByText('General')).toBeInTheDocument();
});
it('should apply custom iconClassName to the icon', () => {
render(<AgentCategoryDisplay category="general" iconClassName="custom-icon-class" />);
const iconElement = screen.getByTestId('icon-general').parentElement;
expect(iconElement).toHaveClass('custom-icon-class');
});
});

View File

@@ -9,7 +9,8 @@ import type t from 'librechat-data-provider';
jest.mock('~/hooks/useLocalize', () => () => (key: string) => {
const mockTranslations: Record<string, string> = {
com_agents_top_picks: 'Top Picks',
com_agents_all: 'All',
com_agents_all: 'All Agents',
com_agents_all_category: 'All',
com_ui_no_categories: 'No categories available',
com_agents_category_tabs_label: 'Agent Categories',
com_ui_agent_category_general: 'General',

View File

@@ -89,7 +89,7 @@ const BookmarkEditDialog = ({
<OGDialog open={open} onOpenChange={setOpen} triggerRef={triggerRef}>
{children}
<OGDialogTemplate
title="Bookmark"
title={bookmark ? localize('com_ui_bookmarks_edit') : localize('com_ui_bookmarks_new')}
showCloseButton={false}
className="w-11/12 md:max-w-2xl"
main={

View File

@@ -38,6 +38,8 @@ const BookmarkForm = ({
control,
formState: { errors },
} = useForm<TConversationTagRequest>({
mode: 'onBlur',
reValidateMode: 'onChange',
defaultValues: {
tag: bookmark?.tag ?? '',
description: bookmark?.description ?? '',
@@ -98,23 +100,30 @@ const BookmarkForm = ({
<Input
type="text"
id="bookmark-tag"
aria-label="Bookmark"
aria-label={
bookmark ? localize('com_ui_bookmarks_edit') : localize('com_ui_bookmarks_new')
}
{...register('tag', {
required: 'tag is required',
required: localize('com_ui_field_required'),
maxLength: {
value: 128,
message: localize('com_auth_password_max_length'),
message: localize('com_ui_field_max_length', {
field: localize('com_ui_bookmarks_title'),
length: 128,
}),
},
validate: (value) => {
return (
value === bookmark?.tag ||
bookmarks.every((bookmark) => bookmark.tag !== value) ||
'tag must be unique'
localize('com_ui_bookmarks_tag_exists')
);
},
})}
aria-invalid={!!errors.tag}
placeholder="Bookmark"
placeholder={
bookmark ? localize('com_ui_bookmarks_edit') : localize('com_ui_bookmarks_new')
}
/>
{errors.tag && <span className="text-sm text-red-500">{errors.tag.message}</span>}
</div>
@@ -127,7 +136,10 @@ const BookmarkForm = ({
{...register('description', {
maxLength: {
value: 1048,
message: 'Maximum 1048 characters',
message: localize('com_ui_field_max_length', {
field: localize('com_ui_bookmarks_description'),
length: 1048,
}),
},
})}
id="bookmark-description"

View File

@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import type { ModelSelectorProps } from '~/common';
import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext';
import { ModelSelectorChatProvider } from './ModelSelectorChatContext';
import { renderModelSpecsWithFolders, renderEndpoints, renderSearchResults } from './components';
import { renderModelSpecs, renderEndpoints, renderSearchResults } from './components';
import { getSelectedIcon, getDisplayValue } from './utils';
import { CustomMenu as Menu } from './CustomMenu';
import DialogManager from './DialogManager';
@@ -86,7 +86,7 @@ function ModelSelectorContent() {
renderSearchResults(searchResults, localize, searchValue)
) : (
<>
{renderModelSpecsWithFolders(modelSpecs, selectedValues.modelSpec || '')}
{renderModelSpecs(modelSpecs, selectedValues.modelSpec || '')}
{renderEndpoints(mappedEndpoints ?? [])}
</>
)}

View File

@@ -1,132 +0,0 @@
import React, { useState } from 'react';
import { ChevronDown, ChevronRight, Folder, FolderOpen } from 'lucide-react';
import type { TModelSpec } from 'librechat-data-provider';
import { ModelSpecItem } from './ModelSpecItem';
import { cn } from '~/utils';
interface ModelSpecFolderProps {
folderName: string;
specs: TModelSpec[];
selectedSpec: string;
level?: number;
}
export function ModelSpecFolder({
folderName,
specs,
selectedSpec,
level = 0
}: ModelSpecFolderProps) {
const [isExpanded, setIsExpanded] = useState(true);
const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation();
setIsExpanded(!isExpanded);
};
const indent = level * 16;
return (
<div className="w-full">
<button
onClick={handleToggle}
className={cn(
'flex w-full items-center gap-1 rounded-lg px-2 py-1.5 text-sm hover:bg-surface-hover',
'text-text-secondary transition-colors'
)}
style={{ paddingLeft: `${8 + indent}px` }}
>
<span className="flex-shrink-0">
{isExpanded ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
</span>
<span className="flex-shrink-0">
{isExpanded ? (
<FolderOpen className="h-3.5 w-3.5" />
) : (
<Folder className="h-3.5 w-3.5" />
)}
</span>
<span className="truncate text-left font-medium">{folderName}</span>
</button>
{isExpanded && (
<div className="mt-0.5">
{specs.map((spec) => (
<div key={spec.name} style={{ paddingLeft: `${indent}px` }}>
<ModelSpecItem spec={spec} isSelected={selectedSpec === spec.name} />
</div>
))}
</div>
)}
</div>
);
}
interface GroupedSpecs {
[folder: string]: TModelSpec[];
}
export function renderModelSpecsWithFolders(specs: TModelSpec[], selectedSpec: string) {
if (!specs || specs.length === 0) {
return null;
}
// Group specs by folder
const grouped: GroupedSpecs = {};
const rootSpecs: TModelSpec[] = [];
specs.forEach((spec) => {
if (spec.folder) {
if (!grouped[spec.folder]) {
grouped[spec.folder] = [];
}
grouped[spec.folder].push(spec);
} else {
rootSpecs.push(spec);
}
});
// Sort folders alphabetically
const sortedFolders = Object.keys(grouped).sort((a, b) =>
a.toLowerCase().localeCompare(b.toLowerCase())
);
// Sort specs within each folder by order or label
sortedFolders.forEach(folder => {
grouped[folder].sort((a, b) => {
if (a.order !== undefined && b.order !== undefined) {
return a.order - b.order;
}
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
});
});
// Sort root specs
rootSpecs.sort((a, b) => {
if (a.order !== undefined && b.order !== undefined) {
return a.order - b.order;
}
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
});
return (
<>
{/* Render folders first */}
{sortedFolders.map((folder) => (
<ModelSpecFolder
key={folder}
folderName={folder}
specs={grouped[folder]}
selectedSpec={selectedSpec}
/>
))}
{/* Render root level specs */}
{rootSpecs.map((spec) => (
<ModelSpecItem key={spec.name} spec={spec} isSelected={selectedSpec === spec.name} />
))}
</>
);
}

View File

@@ -67,12 +67,7 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
</div>
)}
<div className="flex min-w-0 flex-col gap-1">
<span className="truncate text-left">
{spec.folder && (
<span className="text-xs text-text-tertiary">{spec.folder} / </span>
)}
{spec.label}
</span>
<span className="truncate text-left">{spec.label}</span>
{spec.description && (
<span className="break-words text-xs font-normal">{spec.description}</span>
)}

View File

@@ -1,5 +1,4 @@
export * from './ModelSpecItem';
export * from './ModelSpecFolder';
export * from './EndpointModelItem';
export * from './EndpointItem';
export * from './SearchResults';

View File

@@ -1,10 +1,9 @@
import React, { memo, useState } from 'react';
import { UserIcon } from '@librechat/client';
import { UserIcon, useAvatar } from '@librechat/client';
import type { TUser } from 'librechat-data-provider';
import type { IconProps } from '~/common';
import MessageEndpointIcon from './MessageEndpointIcon';
import { useAuthContext } from '~/hooks/AuthContext';
import useAvatar from '~/hooks/Messages/useAvatar';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';

View File

@@ -2,11 +2,10 @@ import { useState, memo } from 'react';
import { useRecoilState } from 'recoil';
import * as Select from '@ariakit/react/select';
import { FileText, LogOut } from 'lucide-react';
import { LinkIcon, GearIcon, DropdownMenuSeparator, UserIcon } from '@librechat/client';
import { LinkIcon, GearIcon, DropdownMenuSeparator, Avatar } from '@librechat/client';
import { useGetStartupConfig, useGetUserBalance } from '~/data-provider';
import FilesView from '~/components/Chat/Input/Files/FilesView';
import { useAuthContext } from '~/hooks/AuthContext';
import useAvatar from '~/hooks/Messages/useAvatar';
import { useLocalize } from '~/hooks';
import Settings from './Settings';
import store from '~/store';
@@ -21,9 +20,6 @@ function AccountSettings() {
const [showSettings, setShowSettings] = useState(false);
const [showFiles, setShowFiles] = useRecoilState(store.showFiles);
const avatarSrc = useAvatar(user);
const avatarSeed = user?.avatar || user?.name || user?.username || '';
return (
<Select.SelectProvider>
<Select.Select
@@ -33,26 +29,7 @@ function AccountSettings() {
>
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
<div className="relative flex">
{avatarSeed.length === 0 ? (
<div
style={{
backgroundColor: 'rgb(121, 137, 255)',
width: '32px',
height: '32px',
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
}}
className="relative flex items-center justify-center rounded-full p-1 text-text-primary"
aria-hidden="true"
>
<UserIcon />
</div>
) : (
<img
className="rounded-full"
src={(user?.avatar ?? '') || avatarSrc}
alt={`${user?.name || user?.username || user?.email || ''}'s avatar`}
/>
)}
<Avatar user={user} size={32} />
</div>
</div>
<div

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { useLocalize } from '~/hooks';
export default function EmptyPromptPreview() {
const localize = useLocalize();
return (
<div className="h-full w-full content-center text-center font-bold dark:text-gray-200">
Select or Create a Prompt
<div className="h-full w-full content-center text-center font-bold text-text-secondary">
{localize('com_ui_select_or_create_prompt')}
</div>
);
}

View File

@@ -1,5 +1,4 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ControlCombobox } from '@librechat/client';
import {
useWatch,
@@ -9,7 +8,7 @@ import {
useFormContext,
ControllerRenderProps,
} from 'react-hook-form';
import { useAgentCategories } from '~/hooks/Agents';
import { TranslationKeys, useLocalize, useAgentCategories } from '~/hooks';
import { cn } from '~/utils';
/**
@@ -35,22 +34,25 @@ const useCategorySync = (agent_id: string | null) => {
* A component for selecting agent categories with form validation
*/
const AgentCategorySelector: React.FC<{ className?: string }> = ({ className }) => {
const { t } = useTranslation();
const localize = useLocalize();
const formContext = useFormContext();
const { categories } = useAgentCategories();
// Always call useWatch
const agent_id = useWatch({
name: 'id',
control: formContext.control,
});
// Use custom hook for category sync
const { syncCategory } = useCategorySync(agent_id);
const getCategoryLabel = (category: { label: string; value: string }) => {
if (category.label && category.label.startsWith('com_')) {
return localize(category.label as TranslationKeys);
}
return category.label;
};
// Transform categories to the format expected by ControlCombobox
const comboboxItems = categories.map((category) => ({
label: category.label,
label: getCategoryLabel(category),
value: category.value,
}));
@@ -59,8 +61,8 @@ const AgentCategorySelector: React.FC<{ className?: string }> = ({ className })
return categoryItem?.label || comboboxItems.find((c) => c.value === 'general')?.label;
};
const searchPlaceholder = t('com_ui_search_agent_category', 'Search categories...');
const ariaLabel = t('com_ui_agent_category_selector_aria', "Agent's category selector");
const searchPlaceholder = localize('com_ui_search_agent_category');
const ariaLabel = localize('com_ui_agent_category_selector_aria');
return (
<Controller

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
import type { TAgentsEndpoint, TEndpointsConfig, TConfig } from 'librechat-data-provider';
import type { TAgentsEndpoint, TEndpointsConfig } from 'librechat-data-provider';
import { useGetEndpointsQuery } from '~/data-provider';
interface UseGetAgentsConfigOptions {
@@ -20,11 +20,12 @@ export default function useGetAgentsConfig(options?: UseGetAgentsConfigOptions):
const endpointsConfig = providedConfig || queriedConfig;
const agentsConfig = useMemo<TAgentsEndpoint | null>(() => {
const config = endpointsConfig?.[EModelEndpoint.agents] ?? null;
const config: TAgentsEndpoint | null =
(endpointsConfig?.[EModelEndpoint.agents] as TAgentsEndpoint | null) ?? null;
if (!config) return null;
return {
...(config as TConfig),
...config,
capabilities: Array.isArray(config.capabilities)
? config.capabilities.map((cap) => cap as unknown as AgentCapabilities)
: ([] as AgentCapabilities[]),

View File

@@ -3,7 +3,6 @@ import { QueryKeys } from 'librechat-data-provider';
import type { ConversationListResponse } from 'librechat-data-provider';
import type { InfiniteData } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import { updateConvoFieldsInfinite } from '~/utils/convos';
const useUpdateTagsInConvo = () => {
const queryClient = useQueryClient();
@@ -53,30 +52,31 @@ const useUpdateTagsInConvo = () => {
QueryKeys.allConversations,
]);
const conversationIdsWithTag = [] as string[];
// update tag to newTag in all conversations
const newData = JSON.parse(JSON.stringify(data)) as InfiniteData<ConversationListResponse>;
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
const page = newData.pages[pageIndex];
page.conversations = page.conversations.map((conversation) => {
if (
conversation.conversationId &&
'tags' in conversation &&
Array.isArray((conversation as { tags?: string[] }).tags) &&
(conversation as { tags?: string[] }).tags?.includes(tag)
) {
(conversation as { tags: string[] }).tags = (conversation as { tags: string[] }).tags.map(
(t: string) => (t === tag ? newTag : t),
);
}
return conversation;
});
if (data) {
const newData = JSON.parse(JSON.stringify(data)) as InfiniteData<ConversationListResponse>;
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
const page = newData.pages[pageIndex];
page.conversations = page.conversations.map((conversation) => {
if (
conversation.conversationId &&
'tags' in conversation &&
Array.isArray((conversation as { tags?: string[] }).tags) &&
(conversation as { tags?: string[] }).tags?.includes(tag)
) {
(conversation as { tags: string[] }).tags = (
conversation as { tags: string[] }
).tags.map((t: string) => (t === tag ? newTag : t));
}
return conversation;
});
}
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
[QueryKeys.allConversations],
newData,
);
}
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
[QueryKeys.allConversations],
newData,
);
const conversationIdsWithTag = [] as string[];
// update the tag to newTag from the cache of each conversation
for (let i = 0; i < conversationIdsWithTag.length; i++) {

View File

@@ -1,4 +1,3 @@
export { default as useAvatar } from './useAvatar';
export { default as useProgress } from './useProgress';
export { default as useAttachments } from './useAttachments';
export { default as useSubmitMessage } from './useSubmitMessage';

View File

@@ -480,12 +480,13 @@ export default function useEventHandlers({
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, id], _messages);
};
/** Handle edge case where stream is cancelled before any response, which creates a blank page */
if (
!conversation.conversationId &&
const hasNoResponse =
responseMessage?.content?.[0]?.['text']?.value ===
submission.initialResponse?.content?.[0]?.['text']?.value
) {
submission.initialResponse?.content?.[0]?.['text']?.value ||
!!responseMessage?.content?.[0]?.['tool_call']?.auth;
/** Handle edge case where stream is cancelled before any response, which creates a blank page */
if (!conversation.conversationId && hasNoResponse) {
const currentConvoId =
(submissionConvo.conversationId ?? conversation.conversationId) || Constants.NEW_CONVO;
if (isNewConvo && submissionConvo.conversationId) {

View File

@@ -6,8 +6,24 @@
"com_a11y_start": "The AI has started their reply.",
"com_agents_agent_card_label": "{{name}} agent. {{description}}",
"com_agents_all": "All Agents",
"com_agents_all_category": "All",
"com_agents_all_description": "Browse all shared agents across all categories",
"com_agents_by_librechat": "by LibreChat",
"com_agents_category_aftersales": "After Sales",
"com_agents_category_aftersales_description": "Agents specialized in post-sale support, maintenance, and customer service",
"com_agents_category_empty": "No agents found in the {{category}} category",
"com_agents_category_finance": "Finance",
"com_agents_category_finance_description": "Agents specialized in financial analysis, budgeting, and accounting",
"com_agents_category_general": "General",
"com_agents_category_general_description": "General purpose agents for common tasks and inquiries",
"com_agents_category_hr": "Human Resources",
"com_agents_category_hr_description": "Agents specialized in HR processes, policies, and employee support",
"com_agents_category_it": "IT",
"com_agents_category_it_description": "Agents for IT support, technical troubleshooting, and system administration",
"com_agents_category_rd": "Research & Development",
"com_agents_category_rd_description": "Agents focused on R&D processes, innovation, and technical research",
"com_agents_category_sales": "Sales",
"com_agents_category_sales_description": "Agents focused on sales processes, customer relations",
"com_agents_category_tab_label": "{{category}} category, {{position}} of {{total}}",
"com_agents_category_tabs_label": "Agent Categories",
"com_agents_clear_search": "Clear search",
@@ -724,6 +740,7 @@
"com_ui_bookmarks_edit": "Edit Bookmark",
"com_ui_bookmarks_filter": "Filter bookmarks...",
"com_ui_bookmarks_new": "New Bookmark",
"com_ui_bookmarks_tag_exists": "A bookmark with this title already exists",
"com_ui_bookmarks_title": "Title",
"com_ui_bookmarks_update_error": "There was an error updating the bookmark",
"com_ui_bookmarks_update_success": "Bookmark updated successfully",
@@ -868,6 +885,7 @@
"com_ui_feedback_tag_not_matched": "Didn't match my request",
"com_ui_feedback_tag_other": "Other issue",
"com_ui_feedback_tag_unjustified_refusal": "Refused without reason",
"com_ui_field_max_length": "{{field}} must be less than {{length}} characters",
"com_ui_field_required": "This field is required",
"com_ui_file_size": "File Size",
"com_ui_files": "Files",
@@ -996,6 +1014,7 @@
"com_ui_no_bookmarks": "it seems like you have no bookmarks yet. Click on a chat and add a new one",
"com_ui_no_categories": "No categories available",
"com_ui_no_category": "No category",
"com_ui_no_changes": "No changes were made",
"com_ui_no_data": "something needs to go here. was empty",
"com_ui_no_individual_access": "No individual users or groups have access to this agent",
"com_ui_no_personalization_available": "No personalization options are currently available",
@@ -1111,6 +1130,7 @@
"com_ui_select_file": "Select a file",
"com_ui_select_model": "Select a model",
"com_ui_select_options": "Select options...",
"com_ui_select_or_create_prompt": "Select or Create a Prompt",
"com_ui_select_provider": "Select a provider",
"com_ui_select_provider_first": "Select a provider first",
"com_ui_select_region": "Select a region",

View File

@@ -4,31 +4,83 @@
"com_a11y_ai_composing": "Mākslīgais intelekts joprojām veido.",
"com_a11y_end": "Mākslīgais intelekts ir pabeidzis atbildi.",
"com_a11y_start": "Mākslīgais intelekts ir sācis savu atbildi.",
"com_agents_agent_card_label": "{{name}} aģents. {{description}}",
"com_agents_all": "Visi aģenti",
"com_agents_by_librechat": "no LibreChat",
"com_agents_category_empty": "Nav atrasts neviens aģents {{category}} kategorijā",
"com_agents_category_tab_label": "{{category}} kategorija, {{position}} no {{total}}",
"com_agents_category_tabs_label": "Aģentu kategorijas",
"com_agents_clear_search": "Notīrīt meklēšanu",
"com_agents_code_interpreter": "Ja šī opcija ir iespējota, jūsu aģents var izmantot LibreChat koda interpretētāja API, lai droši palaistu ģenerēto kodu, tostarp failu apstrādi. Nepieciešama derīga API atslēga.",
"com_agents_code_interpreter_title": "Koda interpretētāja API",
"com_agents_contact": "Sazinieties ar",
"com_agents_copy_link": "Kopēt saiti",
"com_agents_create_error": "Izveidojot jūsu aģentu, radās kļūda.",
"com_agents_created_by": "izveidojis",
"com_agents_description_placeholder": "Pēc izvēles: aprakstiet savu aģentu šeit",
"com_agents_empty_state_heading": "Nav atrasts neviens aģents",
"com_agents_enable_file_search": "Iespējot failu meklēšanu",
"com_agents_error_bad_request_message": "Pieprasījumu nevarēja apstrādāt.",
"com_agents_error_bad_request_suggestion": "Lūdzu, pārbaudiet ievadītos datus un mēģiniet vēlreiz.",
"com_agents_error_category_title": "Kategorija Kļūda",
"com_agents_error_generic": "Satura ielādēšanas laikā radās problēma.",
"com_agents_error_invalid_request": "Nepareizs pieprasījums",
"com_agents_error_loading": "Kļūda aģentu ielādēšanā",
"com_agents_error_network_message": "Nevar izveidot savienojumu ar serveri.",
"com_agents_error_network_suggestion": "Pārbaudiet interneta savienojumu un mēģiniet vēlreiz.",
"com_agents_error_network_title": "Savienojuma problēmas",
"com_agents_error_not_found_message": "Pieprasīto saturu neizdevās atrast.",
"com_agents_error_not_found_suggestion": "Mēģiniet pārlūkot citas iespējas vai atgriezieties katalogā.",
"com_agents_error_not_found_title": "Nav atrasts",
"com_agents_error_retry": "Mēģiniet vēlreiz",
"com_agents_error_search_title": "Meklēšanas kļūda",
"com_agents_error_searching": "Kļūda meklējot aģentus",
"com_agents_error_server_message": "Serveris uz laiku nav pieejams.",
"com_agents_error_server_suggestion": "Lūdzu, mēģiniet vēlreiz pēc dažām minūtēm.",
"com_agents_error_server_title": "Servera kļūda",
"com_agents_error_suggestion_generic": "Lūdzu, mēģiniet atsvaidzināt lapu vai mēģiniet vēlreiz vēlāk.",
"com_agents_error_timeout_message": "Pieprasījuma izpildei bija nepieciešams pārāk ilgs laiks.",
"com_agents_error_timeout_suggestion": "Lūdzu, pārbaudiet interneta savienojumu un mēģiniet vēlreiz.",
"com_agents_error_timeout_title": "Savienojumu neizdevās izveidot",
"com_agents_error_title": "Kaut kas nogāja greizi",
"com_agents_file_context": "Failu konteksts (OCR)",
"com_agents_file_context_disabled": "Pirms failu augšupielādes failu kontekstam ir jāizveido aģents.",
"com_agents_file_context_info": "Faili, kas augšupielādēti kā “Konteksts”, tiek apstrādāti, izmantojot OCR, lai iegūtu tekstu, kas pēc tam tiek pievienots aģenta norādījumiem. Ideāli piemērots dokumentiem, attēliem ar tekstu vai PDF failiem, kuriem nepieciešams pilns faila teksta saturs.",
"com_agents_file_search_disabled": "Pirms failu augšupielādes failu meklēšanai ir jāizveido aģents.",
"com_agents_file_search_info": "Kad šī opcija ir iespējota, aģents tiks informēts par precīziem tālāk norādītajiem failu nosaukumiem, ļaujot tam izgūt atbilstošu kontekstu no šiem failiem.",
"com_agents_grid_announcement": "Rādu {{count}} aģentus {{category}} kategorijā",
"com_agents_instructions_placeholder": "Sistēmas instrukcijas, ko izmantos aģents",
"com_agents_link_copied": "Saite nokopēta",
"com_agents_link_copy_failed": "Neizdevās nokopēt saiti",
"com_agents_load_more_label": "Ielādēt vairāk aģentus no {{category}} kategorijas",
"com_agents_loading": "Notiek ielāde...",
"com_agents_marketplace": "Aģentu katalogs",
"com_agents_marketplace_subtitle": "Atklājiet un izmantojiet jaudīgus mākslīgā intelekta aģentus, lai uzlabotu darba plūsmu un produktivitāti.",
"com_agents_mcp_description_placeholder": "Dažos vārdos paskaidrojiet, ko tas dara.",
"com_agents_mcp_icon_size": "Minimālais izmērs 128 x 128 px",
"com_agents_mcp_info": "MCP serveru pievienošana savam aģentam, lai tas varētu veikt uzdevumus un mijiedarboties ar ārējiem pakalpojumiem.",
"com_agents_mcp_name_placeholder": "Pielāgotais rīks",
"com_agents_mcp_trust_subtext": "LibreChat nav validējis neoficiālus savienotājus",
"com_agents_mcps_disabled": "Pirms MCP pievienošanas ir jāizveido aģents.",
"com_agents_missing_name": "Pirms aģenta izveidošanas ievadiet aģenta nosaukumu.",
"com_agents_missing_provider_model": "Pirms aģenta izveides izvēlieties pakalpojumu sniedzēju un modeli.",
"com_agents_name_placeholder": "Pēc izvēles: aģenta nosaukums",
"com_agents_no_access": "Jums nav piekļuves šī aģenta rediģēšanai.",
"com_agents_no_agent_id_error": "Nav atrasts aģenta ID. Lūdzu, pārliecinieties, ka aģents ir izveidots.",
"com_agents_no_more_results": "Jūs esat sasniedzis rezultātu beigas",
"com_agents_not_available": "Aģents nav pieejams",
"com_agents_recommended": "Mūsu rekomendētie aģenti",
"com_agents_results_for": "Rezultāti par '{{query}}'",
"com_agents_search_aria": "Meklēt aģentus",
"com_agents_search_empty_heading": "Nav meklēšanas rezultātu",
"com_agents_search_info": "Ja šī opcija ir iespējota, jūsu aģents var meklēt jaunāko informāciju tīmeklī. Nepieciešama derīga API atslēga.",
"com_agents_search_instructions": "Rakstiet lai meklētu aģentus pēc vārda vai apraksta",
"com_agents_search_name": "Meklēt aģentus pēc nosaukuma",
"com_agents_search_no_results": "Nav atrasts neviens aģents \"{{query}}\"",
"com_agents_search_placeholder": "Meklēt aģentus...",
"com_agents_see_more": "Skatīt vairāk",
"com_agents_start_chat": "Sākt sarunu",
"com_agents_top_picks": "Labākās izvēles",
"com_agents_update_error": "Jūsu aģenta atjaunināšanā ir kļūda.",
"com_assistants_action_attempt": "Asistents vēlas runāt ar {{0}}",
"com_assistants_actions": "Darbības",
@@ -308,10 +360,21 @@
"com_error_moderation": "Šķiet, ka mūsu moderācijas sistēma ir atzīmējusi iesniegto saturu kā neatbilstošu mūsu vadlīnijām. Mēs nevaram turpināt darbu ar šo konkrēto tēmu. Ja jums ir vēl kādi jautājumi vai tēmas, kuras vēlaties izpētīt, lūdzu, rediģējiet savu ziņu vai izveidojiet jaunu sarunu.",
"com_error_no_base_url": "Nav atrasts bāzes URL. Lūdzu, norādiet to un mēģiniet vēlreiz.",
"com_error_no_user_key": "Atslēga nav atrasta. Lūdzu, norādiet atslēgu un mēģiniet vēlreiz.",
"com_file_pages": "Lapas: {{pages}}",
"com_file_source": "Fails",
"com_file_unknown": "Nezināms fails",
"com_files_download_failed": "{{0}} failu lejuplāde neizdevās",
"com_files_download_percent_complete": "{{0}}% pabeigts",
"com_files_download_progress": "{{0}} no {{1}} failiem",
"com_files_downloading": "Notiek failu lejuplādēšana",
"com_files_filter": "Filtrēt failus...",
"com_files_no_results": "Nav rezultātu.",
"com_files_number_selected": "{{0}} no {{1}} atlasīti faili",
"com_files_preparing_download": "Sagatavošanās lejupielādei...",
"com_files_sharepoint_picker_title": "Izvēlieties failus",
"com_files_table": "kaut kam šeit ir jānotiek. bija tukšs",
"com_files_upload_local_machine": "No lokālā datora",
"com_files_upload_sharepoint": "No SharePoint",
"com_generated_files": "Ģenerētie faili:",
"com_hide_examples": "Slēpt piemērus",
"com_info_heic_converting": "Konvertēju HEIC attēlu uz JPEG...",
@@ -347,7 +410,7 @@
"com_nav_balance_month": "mēnesis",
"com_nav_balance_months": "mēneši",
"com_nav_balance_next_refill": "Nākamā bilances papildināšana:",
"com_nav_balance_next_refill_info": "Nākamā bilances papildināšana notiks automātiski tikai tad, ja būs izpildīti abi nosacījumi: kopš pēdējās bilances papildināšanas ir pagājis norādītais laika atjaunošanas biežums un uzaicinājuma nosūtīšana izraisītu jūsu atlikuma samazināšanos zem nulles.",
"com_nav_balance_next_refill_info": "Nākamā bilances papildināšana notiks automātiski tikai tad, ja būs izpildīti abi nosacījumi: kopš pēdējās bilances papildināšanas ir pagājis norādītais laika atjaunošanas biežums un vaicājuma nosūtīšana izraisītu jūsu atlikuma samazināšanos zem nulles.",
"com_nav_balance_refill_amount": "Bilances papildināšanas apjoms:",
"com_nav_balance_second": "otrais",
"com_nav_balance_seconds": "sekundes",
@@ -415,6 +478,7 @@
"com_nav_lang_arabic": "العربية",
"com_nav_lang_armenian": "Հայերեն",
"com_nav_lang_auto": "Automātiska noteikšana",
"com_nav_lang_bosnian": "Босански",
"com_nav_lang_brazilian_portuguese": "Brazīlijas portugāļu",
"com_nav_lang_catalan": "Katalā",
"com_nav_lang_chinese": "中文",
@@ -434,6 +498,7 @@
"com_nav_lang_japanese": "日本語",
"com_nav_lang_korean": "한국어",
"com_nav_lang_latvian": "Latviešu",
"com_nav_lang_norwegian_bokmal": "Norsk Bokmål",
"com_nav_lang_persian": "فارسی",
"com_nav_lang_polish": "Poļu",
"com_nav_lang_portuguese": "Portugāļu",
@@ -515,9 +580,21 @@
"com_sidepanel_manage_files": "Pārvaldīt failus",
"com_sidepanel_mcp_no_servers_with_vars": "Nav MCP serveru ar konfigurējamiem mainīgajiem.",
"com_sidepanel_parameters": "Parametri",
"com_sources_agent_file": "Avota dokuments",
"com_sources_agent_files": "Aģentu faili",
"com_sources_download_aria_label": "Lejupielādēt {{filename}}{{status}}",
"com_sources_download_failed": "Lejupielādē neizdevās",
"com_sources_download_local_unavailable": "Neizdevās lejupielādēt: Faili nav saglabāti",
"com_sources_downloading_status": " (lejupielādē...)",
"com_sources_error_fallback": "Nevar ielādēt avotus",
"com_sources_image_alt": "Meklēšanas rezultāta attēls",
"com_sources_more_files": "+{{count}} faili",
"com_sources_more_sources": "+{{count}} avoti",
"com_sources_pages": "Lapas",
"com_sources_region_label": "Meklēšanas rezultāti un avoti",
"com_sources_reload_page": "Atkārtoti ielādēt lapu",
"com_sources_tab_all": "Visi",
"com_sources_tab_files": "Faili",
"com_sources_tab_images": "Attēli",
"com_sources_tab_news": "Ziņas",
"com_sources_title": "Avoti",
@@ -546,6 +623,14 @@
"com_ui_advanced": "Advancēts",
"com_ui_advanced_settings": "Advancētie iestatījumi",
"com_ui_agent": "Aģents",
"com_ui_agent_category_aftersales": "Pēcpārdošanas pakalpojumi",
"com_ui_agent_category_finance": "Finanses",
"com_ui_agent_category_general": "Vispārīgi",
"com_ui_agent_category_hr": "Personāla daļa",
"com_ui_agent_category_it": "IT",
"com_ui_agent_category_rd": "Pētniecība un attīstība",
"com_ui_agent_category_sales": "Pārdošana",
"com_ui_agent_category_selector_aria": "Aģenta kategorijas atlasītājs",
"com_ui_agent_chain": "Aģentu ķēde (aģentu maisījums)",
"com_ui_agent_chain_info": "Ļauj izveidot aģentu secību ķēdes. Katrs aģents var piekļūt iepriekšējo ķēdē esošo aģentu izvades datiem. Balstīts uz \"Aģentu sajaukuma\" arhitektūru, kurā aģenti izmanto iepriekšējos izvades datus kā palīginformāciju.",
"com_ui_agent_chain_max": "Jūs esat sasniedzis maksimālo skaitu {{0}} aģentu.",
@@ -553,8 +638,10 @@
"com_ui_agent_deleted": "Aģents veiksmīgi izdzēsts",
"com_ui_agent_duplicate_error": "Dublējot aģentu, radās kļūda.",
"com_ui_agent_duplicated": "Aģents veiksmīgi dublēts",
"com_ui_agent_name_is_required": "Obligāti jānorāda aģenta nosaukums",
"com_ui_agent_recursion_limit": "Maksimālais aģenta soļu skaits",
"com_ui_agent_recursion_limit_info": "Ierobežo, cik soļus aģents var veikt vienā izpildes reizē, pirms sniedz galīgo atbildi. Noklusējuma vērtība ir 25 soļi. Solis ir vai nu AI API pieprasījums, vai rīka lietošanas kārta. Piemēram, pamata rīka mijiedarbība ietver 3 soļus: sākotnējo pieprasījumu, rīka lietošanu un turpmāko pieprasījumu.",
"com_ui_agent_url_copied": "Aģenta URL kopēts starpliktuvē",
"com_ui_agent_var": "{{0}} aģents",
"com_ui_agent_version": "Versija",
"com_ui_agent_version_active": "Aktīvā versija",
@@ -571,6 +658,7 @@
"com_ui_agent_version_unknown_date": "Nezināms datums",
"com_ui_agents": "Aģenti",
"com_ui_agents_allow_create": "Atļaut aģentu izveidi",
"com_ui_agents_allow_share": "Atļaut aģentu koplietošanu",
"com_ui_agents_allow_use": "Atļaut aģentu izmantošanu",
"com_ui_all": "visi",
"com_ui_all_proper": "Visi",
@@ -591,6 +679,7 @@
"com_ui_assistant_deleted": "Asistents ir veiksmīgi izdzēsts.",
"com_ui_assistants": "Asistenti",
"com_ui_assistants_output": "Asistentu izvade",
"com_ui_at_least_one_owner_required": "Nepieciešams vismaz viens īpašnieks",
"com_ui_attach_error": "Nevar pievienot failu. Izveidojiet vai atlasiet sarunu vai mēģiniet atsvaidzināt lapu.",
"com_ui_attach_error_openai": "Nevar pievienot asistenta failus citiem galapunktiem",
"com_ui_attach_error_size": "Galapunkta faila lieluma ierobežojums ir pārsniegts:",
@@ -607,12 +696,16 @@
"com_ui_available_tools": "Pieejamie rīki",
"com_ui_avatar": "Avatars",
"com_ui_azure": "Azure",
"com_ui_azure_ad": "Azure Entra ID",
"com_ui_back": "Atpakaļ",
"com_ui_back_to_chat": "Atpakaļ uz sarunu",
"com_ui_back_to_prompts": "Atpakaļ pie uzvednēm",
"com_ui_backup_code_number": "Kods #{{number}}",
"com_ui_backup_codes": "Rezerves kodi",
"com_ui_backup_codes_regenerate_error": "Atkārtoti ģenerējot rezerves kodus, radās kļūda.",
"com_ui_backup_codes_regenerated": "Rezerves kodi ir veiksmīgi atjaunoti",
"com_ui_backup_codes_security_info": "Drošības apsvērumu dēļ rezerves kodi tiek parādīti tikai vienu reizi. Lūdzu, saglabājiet tos drošā vietā.",
"com_ui_backup_codes_status": "Rezerves kodu statuss",
"com_ui_basic": "Pamata",
"com_ui_basic_auth_header": "Pamata autorizācijas galvene",
"com_ui_bearer": "Nesējs",
@@ -669,6 +762,7 @@
"com_ui_copy_code": "Kopēt kodu",
"com_ui_copy_link": "Kopēt saiti",
"com_ui_copy_to_clipboard": "Kopēt starpliktuvē",
"com_ui_copy_url_to_clipboard": "URL kopēšana uz starpliktuvi",
"com_ui_create": "Izveidot",
"com_ui_create_link": "Izveidot saiti",
"com_ui_create_memory": "Izveidot atmiņu",
@@ -754,6 +848,7 @@
"com_ui_error_connection": "Kļūda, izveidojot savienojumu ar serveri, mēģiniet atsvaidzināt lapu.",
"com_ui_error_save_admin_settings": "Saglabājot administratora iestatījumus, radās kļūda.",
"com_ui_error_updating_preferences": "Kļūda, atjauninot preferences",
"com_ui_everyone_permission_level": "Visu lietotāju atļaujas līmenis",
"com_ui_examples": "Piemēri",
"com_ui_expand_chat": "Izvērst sarunu",
"com_ui_export_convo_modal": "Eksportēt sarunas modālo logu",
@@ -818,6 +913,7 @@
"com_ui_good_afternoon": "Labdien",
"com_ui_good_evening": "Labvakar",
"com_ui_good_morning": "Labrīt",
"com_ui_group": "Grupa",
"com_ui_happy_birthday": "Man šodien ir pirmā dzimšanas diena!",
"com_ui_hide_image_details": "Slēpt attēla detaļas",
"com_ui_hide_password": "Paslēpt paroli",
@@ -851,6 +947,8 @@
"com_ui_logo": "{{0}} Logotips",
"com_ui_low": "Zems",
"com_ui_manage": "Pārvaldīt",
"com_ui_marketplace": "Katalogs",
"com_ui_marketplace_allow_use": "Atļaut izmantot katalogu",
"com_ui_max_tags": "Maksimālais atļautais skaits ir {{0}}, izmantojot jaunākās vērtības.",
"com_ui_mcp_authenticated_success": "MCP serveris '{{0}}' veiksmīgi autentificēts",
"com_ui_mcp_enter_var": "Ievadiet vērtību {{0}}",
@@ -898,10 +996,13 @@
"com_ui_next": "Nākamais",
"com_ui_no": "Nē",
"com_ui_no_bookmarks": "Šķiet, ka jums vēl nav grāmatzīmju. Noklikšķiniet uz sarunas un pievienojiet jaunu.",
"com_ui_no_categories": "Nav pieejamas nevienas kategorijas",
"com_ui_no_category": "Nav kategorijas",
"com_ui_no_data": "kaut kam šeit ir jānotiek. bija tukšs",
"com_ui_no_individual_access": "Aatsevišķiem lietotājiem vai grupām nav pieejas pie šī aģenta",
"com_ui_no_personalization_available": "Pašlaik nav pieejamas personalizācijas opcijas",
"com_ui_no_read_access": "Jums nav atļaujas skatīt atmiņas",
"com_ui_no_results_found": "Nav atrastu rezultātu",
"com_ui_no_terms_content": "Nav noteikumu un nosacījumu satura, ko parādīt",
"com_ui_no_valid_items": "kaut kam šeit ir jānotiek. bija tukšs",
"com_ui_none": "Neviens",
@@ -924,6 +1025,14 @@
"com_ui_openai": "OpenAI",
"com_ui_optional": "(pēc izvēles)",
"com_ui_page": "Lapa",
"com_ui_people": "cilvēki",
"com_ui_people_picker": "Cilvēku atlasītājs",
"com_ui_people_picker_allow_view_groups": "Atļaut skatīt grupas",
"com_ui_people_picker_allow_view_roles": "Atļaut lomu skatīšanu",
"com_ui_people_picker_allow_view_users": "Atļaut skatīt lietotājus",
"com_ui_permissions_failed_load": "Neizdevās ielādēt pieejas tiesības. Lūdzu, mēģiniet vēlreiz.",
"com_ui_permissions_failed_update": "Neizdevās atjaunināt pieejas tiesības. Lūdzu, mēģiniet vēlreiz.",
"com_ui_permissions_updated_success": "Pieejas tiesības ir veiksmīgi atjauninātas.",
"com_ui_preferences_updated": "Preferences veiksmīgi atjauninātas",
"com_ui_prev": "Iepriekšējais",
"com_ui_preview": "Priekšskatījums",
@@ -938,6 +1047,7 @@
"com_ui_prompt_update_error": "Atjauninot uzvedni, radās kļūda.",
"com_ui_prompts": "Uzvednes",
"com_ui_prompts_allow_create": "Atļaut uzvedņu izveidi",
"com_ui_prompts_allow_share": "Atļaut kopīgošanas uzvednes",
"com_ui_prompts_allow_use": "Atļaut izmantot uzvednes",
"com_ui_provider": "Pakalpojumu sniedzējs",
"com_ui_quality": "Kvalitāte",
@@ -945,12 +1055,14 @@
"com_ui_redirecting_to_provider": "Pārvirzu uz {{0}}, lūdzu, uzgaidiet...",
"com_ui_reference_saved_memories": "References uz saglabātajām atmiņām",
"com_ui_reference_saved_memories_description": "Ļaut asistentam atsaukties uz jūsu saglabātajām atmiņām un izmantot tās atbildot",
"com_ui_refresh": "Atsvaidzināt",
"com_ui_refresh_link": "Atsvaidzināt saiti",
"com_ui_regenerate": "Atjaunot",
"com_ui_regenerate_backup": "Atjaunot rezerves kodus",
"com_ui_regenerating": "Atjaunojas...",
"com_ui_region": "Reģions",
"com_ui_reinitialize": "Reinicializēt",
"com_ui_remove_user": "Noņemt {{0}}",
"com_ui_rename": "Pārdēvēt",
"com_ui_rename_conversation": "Pārdēvēt sarunu",
"com_ui_rename_failed": "Neizdevās pārdēvēt sarunu",
@@ -958,6 +1070,7 @@
"com_ui_requires_auth": "Nepieciešama autentifikācija",
"com_ui_reset_var": "Atiestatīt {{0}}",
"com_ui_reset_zoom": "Atiestatīt tālummaiņu",
"com_ui_resource": "resurss",
"com_ui_result": "Rezultāts",
"com_ui_revoke": "Atsaukt",
"com_ui_revoke_info": "Atcelt visus lietotāja sniegtos lietotāja datus",
@@ -965,24 +1078,41 @@
"com_ui_revoke_key_endpoint": "Atsaukt atslēgu priekš {{0}}",
"com_ui_revoke_keys": "Atsaukt atslēgas",
"com_ui_revoke_keys_confirm": "Vai tiešām vēlaties atsaukt visas atslēgas?",
"com_ui_role": "Loma",
"com_ui_role_editor": "Redaktors",
"com_ui_role_editor_desc": "Var skatīt un rediģēt aģentu",
"com_ui_role_manager": "Vadītājs",
"com_ui_role_manager_desc": "Var skatīt, rediģēt un dzēst aģentu",
"com_ui_role_owner": "Īpašnieks",
"com_ui_role_owner_desc": "Pilnīga kontrole pār aģentu, tostarp tā koplietošana",
"com_ui_role_select": "Loma",
"com_ui_role_viewer": "Skatītājs",
"com_ui_role_viewer_desc": "Var skatīt un izmantot aģentu, bet nevar to rediģēt",
"com_ui_roleplay": "Lomu spēle",
"com_ui_run_code": "Palaišanas kods",
"com_ui_run_code_error": "Radās kļūda, izpildot kodu",
"com_ui_save": "Saglabāt",
"com_ui_save_badge_changes": "Vai saglabāt emblēmas izmaiņas?",
"com_ui_save_changes": "Saglabāt izmaiņas",
"com_ui_save_submit": "Saglabāt un iesniegt",
"com_ui_saved": "Saglabāts!",
"com_ui_saving": "Saglabā...",
"com_ui_schema": "Shēma",
"com_ui_scope": "Mērogs",
"com_ui_search": "Meklēt",
"com_ui_search_above_to_add": "Meklēt augstāk, lai pievienotu lietotājus vai grupas",
"com_ui_search_above_to_add_all": "Meklēt augstāk, lai pievienotu lietotājus, grupas vai lomas",
"com_ui_search_above_to_add_people": "Meklēt augstāk, lai pievienotu personas",
"com_ui_search_agent_category": "Meklēt kategorijas...",
"com_ui_search_default_placeholder": "Meklēt pēc vārda vai e-pasta adreses (vismaz 2 rakstu zīmes)",
"com_ui_search_people_placeholder": "Meklēt personas vai grupas pēc vārda vai e-pasta adreses",
"com_ui_seconds": "sekundes",
"com_ui_secret_key": "Slepenā atslēga",
"com_ui_select": "Atlasīt",
"com_ui_select_all": "Atlasīt visu",
"com_ui_select_file": "Atlasiet failu",
"com_ui_select_model": "Izvēlieties modeli",
"com_ui_select_options": "Izvēlieties opcijas...",
"com_ui_select_provider": "Izvēlieties pakalpojumu sniedzēju",
"com_ui_select_provider_first": "Vispirms izvēlieties pakalpojumu sniedzēju",
"com_ui_select_region": "Izvēlieties reģionu",
@@ -995,6 +1125,8 @@
"com_ui_share_create_message": "Jūsu vārds un visas ziņas, ko pievienojat pēc kopīgošanas, paliek privātas.",
"com_ui_share_delete_error": "Dzēšot koplietoto saiti, radās kļūda.",
"com_ui_share_error": "Kopīgojot sarunas saiti, radās kļūda.",
"com_ui_share_everyone": "Koplietot ar visiem",
"com_ui_share_everyone_description_var": "Šis {{resource}} būs pieejams ikvienam. Lūdzu, pārliecinieties, ka {{resource}} patiesībā ir paredzēts koplietošanai ar visiem. Esiet uzmanīgi ar saviem datiem.",
"com_ui_share_link_to_chat": "Kopīgot saiti sarunai",
"com_ui_share_update_message": "Jūsu vārds, pielāgotie norādījumi un visas ziņas, ko pievienojat pēc kopīgošanas, paliek privātas.",
"com_ui_share_var": "Kopīgot {{0}}",
@@ -1021,6 +1153,13 @@
"com_ui_stop": "Apstāties",
"com_ui_storage": "Uzglabāšana",
"com_ui_submit": "Iesniegt",
"com_ui_support_contact": "Atbalsta kontaktinformācija",
"com_ui_support_contact_email": "E-pasts",
"com_ui_support_contact_email_invalid": "Lūdzu, ievadiet derīgu e-pasta adresi",
"com_ui_support_contact_email_placeholder": "atbalsts@piemers.com",
"com_ui_support_contact_name": "Vārds",
"com_ui_support_contact_name_min_length": "Vārdam jābūt vismaz {{minLength}} rakstu zīmēm",
"com_ui_support_contact_name_placeholder": "Atbalsta kontaktpersonas vārds",
"com_ui_teach_or_explain": "Mācīšanās",
"com_ui_temporary": "Pagaidu saruna",
"com_ui_terms_and_conditions": "Noteikumi un nosacījumi",
@@ -1037,6 +1176,7 @@
"com_ui_tools": "Rīki",
"com_ui_travel": "Ceļošana",
"com_ui_trust_app": "Es uzticos šai lietotnei",
"com_ui_try_adjusting_search": "Mēģiniet pielāgot meklēšanas vaicājumus",
"com_ui_unarchive": "Atarhivēt",
"com_ui_unarchive_error": "Neizdevās atarhivēt sarunu",
"com_ui_unknown": "Nezināms",
@@ -1046,6 +1186,7 @@
"com_ui_update_mcp_error": "Izveidojot vai atjauninot MCP, radās kļūda.",
"com_ui_update_mcp_success": "Veiksmīgi izveidots vai atjaunināts MCP",
"com_ui_upload": "Augšupielādēt",
"com_ui_upload_agent_avatar": "Aģenta avatars veiksmīgi atjaunināts",
"com_ui_upload_code_files": "Augšupielādēt koda interpretētājam",
"com_ui_upload_delay": "Augšupielāde \"{{0}}\" aizņem vairāk laika nekā paredzēts. Lūdzu, uzgaidiet, kamēr faila indeksēšana ir pabeigta izguvei.",
"com_ui_upload_error": "Augšupielādējot failu, radās kļūda.",
@@ -1065,6 +1206,8 @@
"com_ui_use_memory": "Izmantot atmiņu",
"com_ui_use_micrphone": "Izmantot mikrofonu",
"com_ui_used": "Lietots",
"com_ui_user": "Lietotājs",
"com_ui_user_group_permissions": "Lietotāju un grupu atļaujas",
"com_ui_value": "Vērtība",
"com_ui_variables": "Mainīgie",
"com_ui_variables_info": "Mainīgo veidošanai tekstā izmantot dubultās iekavas, piemēram, `{{example variable}}`, lai vēlāk aizpildītu, izmantojot uzvedni.",

View File

@@ -4,6 +4,7 @@
"com_a11y_ai_composing": "De AI is nog bezig met het formuleren van een antwoord.",
"com_a11y_end": "De AI is klaar met het antwoord.",
"com_a11y_start": "De AI is begonnen met antwoorden.",
"com_agents_all": "Alle Agents",
"com_agents_by_librechat": "door LibreChat",
"com_agents_code_interpreter": "Indien ingeschakeld, kan je agent de LibreChat Code Interpreter API gebruiken om gegenereerde code, inclusief het verwerken van bestanden, veilig uit te voeren. Vereist een geldige API-sleutel.",
"com_agents_code_interpreter_title": "Code Interpreter API",

View File

@@ -478,6 +478,7 @@
"com_nav_lang_arabic": "العربية",
"com_nav_lang_armenian": "Հայերեն",
"com_nav_lang_auto": "自动检测语言",
"com_nav_lang_bosnian": "Босански",
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
"com_nav_lang_catalan": "Català",
"com_nav_lang_chinese": "中文",
@@ -497,6 +498,7 @@
"com_nav_lang_japanese": "日本語",
"com_nav_lang_korean": "한국어",
"com_nav_lang_latvian": "Latviski",
"com_nav_lang_norwegian_bokmal": "Norsk Bokmål",
"com_nav_lang_persian": "فارسی",
"com_nav_lang_polish": "Polski",
"com_nav_lang_portuguese": "Português",

View File

@@ -4,12 +4,24 @@
"com_a11y_ai_composing": "AI 仍在撰寫中",
"com_a11y_end": "AI 已完成回覆",
"com_a11y_start": "AI 已開始回覆。",
"com_agents_agent_card_label": "{{name}} agent。{{description}}",
"com_agents_all": "全部 Agent",
"com_agents_by_librechat": "由 LibreChat 提供",
"com_agents_category_empty": "在 {{category}} 類別中找不到 agent",
"com_agents_category_tab_label": "{{category}} 類別,{{position}} / {{total}}",
"com_agents_category_tabs_label": "Agent 類別",
"com_agents_clear_search": "清除搜尋",
"com_agents_code_interpreter": "啟用後,您的代理可以安全地使用 LibreChat 程式碼解譯器 API 來執行產生的程式碼,包括檔案處理功能。需要有效的 API 金鑰。",
"com_agents_code_interpreter_title": "程式碼解譯器 API",
"com_agents_contact": "關聯",
"com_agents_copy_link": "複製連結",
"com_agents_create_error": "建立您的代理時發生錯誤。",
"com_agents_created_by": "來自",
"com_agents_description_placeholder": "選填:在此描述您的代理程式",
"com_agents_empty_state_heading": "未找到 agent",
"com_agents_enable_file_search": "啟用檔案搜尋",
"com_agents_error_bad_request_message": "無法處理該請求",
"com_agents_error_bad_request_suggestion": "請檢查您的輸入並再試一次。",
"com_agents_file_context": "文件內容 (OCR)",
"com_agents_file_context_disabled": "在為檔案上下文上傳檔案之前,必須先建立 Agent。",
"com_agents_file_context_info": "以「Context」標記上傳的檔案會使用 OCR 擷取文字,擷取後的文字會被加入到 Agent 的指示中。適合用於文件、含文字的圖片或需要取得檔案完整文字內容的 PDF。",
@@ -229,7 +241,7 @@
"com_endpoint_openai_max_tokens": "可選的 `max_tokens` 欄位,代表在對話完成中可以生成的最大 token 數。\n\n輸入 token 和生成 token 的總長度受限於模型的上下文長度。如果此數字超過最大上下文 token 數,您可能會遇到錯誤。",
"com_endpoint_openai_pres": "數值範圍介於 -2.0 和 2.0 之間。正值會根據該 token 是否在目前的文字中出現來進行懲罰,增加模型談及新主題的可能性。",
"com_endpoint_openai_prompt_prefix_placeholder": "在系統訊息中設定自訂提示。",
"com_endpoint_openai_reasoning_effort": "僅適用於推理模型:限制推理的投入。降低推理投入可以使回應更快,且在回應中使用較少的推理 token。「Minimal」會產生非常少的推理 token以達到最快的首次 token 產生時間,特別適合程式碼與指令遵循。",
"com_endpoint_openai_reasoning_effort": "僅適用於推理模型:限制推理的投入。降低推理投入可以使回應更快,且在回應中使用較少的推理 token。最小值會產生非常少的推理 token以達到最快的首次 token 產生時間,特別適合程式碼與指令遵循。",
"com_endpoint_openai_reasoning_summary": "僅限 Responses API模型執行推理的摘要。這有助於除錯並理解模型的推理過程。可設定為 無、自動、簡潔或詳細。",
"com_endpoint_openai_resend": "重新傳送之前所有附加的圖片。注意:這可能會大幅增加 token 成本,如果附加了太多圖片,您可能會遇到錯誤。",
"com_endpoint_openai_resend_files": "重新傳送之前附加的所有檔案。注意:這將增加 token 成本,如果附件過多,您可能會遇到錯誤。",
@@ -238,7 +250,7 @@
"com_endpoint_openai_topp": "與溫度取樣的替代方法,稱為核心取樣,其中模型考慮 top_p 機率質量的 token 結果。所以 0.1 表示只考慮佔 top 10% 機率質量的 token。我們建議修改這個或溫度但不建議兩者都修改。",
"com_endpoint_openai_use_responses_api": "使用 Responses API 取代 Chat CompletionsResponses API 包含來自 OpenAI 的延伸功能。使用 o1-pro、o3-pro 或要啟用推理摘要時,必須使用 Responses API。",
"com_endpoint_openai_use_web_search": "啟用 OpenAI 內建的網路搜尋功能,使模型能夠在網路上搜尋最新資訊,從而提供更準確、即時的回應。",
"com_endpoint_openai_verbosity": "限制模型回應的冗長程度。較低的值會得到較簡潔的回應,較高的值會得到較冗長的回應。目前支援的值為 low、medium 與 high。",
"com_endpoint_openai_verbosity": "限制模型回應的詳盡程度。數值較低會產生較精簡的回應,數值較高會產生較詳盡的回應。目前支援的值為低/中/高。",
"com_endpoint_output": "輸出",
"com_endpoint_plug_image_detail": "影像詳細資訊",
"com_endpoint_plug_resend_files": "重新傳送檔案",
@@ -280,6 +292,7 @@
"com_endpoint_top_k": "Top K",
"com_endpoint_top_p": "Top P",
"com_endpoint_use_active_assistant": "使用活躍助理",
"com_endpoint_verbosity": "詳盡程度",
"com_error_expired_user_key": "提供給 {{0}} 的金鑰已於 {{1}} 到期。請提供一個新的金鑰並重試。",
"com_error_files_dupe": "偵測到重複的檔案。",
"com_error_files_empty": "不允許空白檔案。",
@@ -318,6 +331,7 @@
"com_nav_balance": "餘額",
"com_nav_balance_day": "日",
"com_nav_balance_days": "日",
"com_nav_balance_every": "每",
"com_nav_balance_hour": "小時",
"com_nav_balance_hours": "小時",
"com_nav_balance_interval": "間隔",
@@ -386,9 +400,14 @@
"com_nav_info_save_draft": "啟用後,您在聊天表單中輸入的文字和附件將自動儲存為本地草稿。即使重新載入頁面或切換至其他對話,這些草稿仍會保留。草稿僅儲存在您的裝置上,並會在訊息送出後自動刪除。",
"com_nav_info_user_name_display": "啟用時,每則您發送的訊息上方都會顯示您的使用者名稱。停用時,您的訊息上方只會顯示「您」。",
"com_nav_lang_arabic": "العربية",
"com_nav_lang_armenian": "Հայերեն",
"com_nav_lang_auto": "自動偵測",
"com_nav_lang_bosnian": "Босански",
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
"com_nav_lang_catalan": "Català",
"com_nav_lang_chinese": "中文",
"com_nav_lang_czech": "Čeština",
"com_nav_lang_danish": "Dansk",
"com_nav_lang_dutch": "Nederlands",
"com_nav_lang_english": "English",
"com_nav_lang_estonian": "Eesti keel",
@@ -397,18 +416,25 @@
"com_nav_lang_georgian": "ქართული",
"com_nav_lang_german": "Deutsch",
"com_nav_lang_hebrew": "עברית",
"com_nav_lang_hungarian": "Magyar",
"com_nav_lang_indonesia": "Indonesia",
"com_nav_lang_italian": "Italiano",
"com_nav_lang_japanese": "日本語",
"com_nav_lang_korean": "한국어",
"com_nav_lang_latvian": "Latviski",
"com_nav_lang_norwegian_bokmal": "Norsk Bokmål",
"com_nav_lang_persian": "فارسی",
"com_nav_lang_polish": "Polski",
"com_nav_lang_portuguese": "Português",
"com_nav_lang_russian": "Русский",
"com_nav_lang_spanish": "Español",
"com_nav_lang_swedish": "Svenska",
"com_nav_lang_thai": "ไทย",
"com_nav_lang_tibetan": "བོད་སྐད་",
"com_nav_lang_traditional_chinese": "繁體中文",
"com_nav_lang_turkish": "Türkçe",
"com_nav_lang_ukrainian": "Українська",
"com_nav_lang_uyghur": "Uyƣur tili",
"com_nav_lang_vietnamese": "Tiếng Việt",
"com_nav_language": "語言",
"com_nav_latex_parsing": "解析訊息中的 LaTeX 內容(可能影響效能)",
@@ -531,6 +557,7 @@
"com_ui_attachment": "附件",
"com_ui_auth_type": "認證類型",
"com_ui_authentication": "驗證",
"com_ui_auto": "自動",
"com_ui_avatar": "大頭照",
"com_ui_back_to_chat": "返回對話",
"com_ui_back_to_prompts": "返回提示",
@@ -562,6 +589,7 @@
"com_ui_collapse_chat": "收合對話",
"com_ui_command_placeholder": "選填:輸入指令,若未填寫將使用名稱",
"com_ui_command_usage_placeholder": "透過指令或名稱選擇提示",
"com_ui_concise": "簡潔",
"com_ui_confirm_action": "確認操作",
"com_ui_context": "情境",
"com_ui_continue": "繼續",
@@ -613,6 +641,7 @@
"com_ui_description": "描述",
"com_ui_description_placeholder": "選填:輸入要顯示的提示描述",
"com_ui_deselect_all": "全部取消選取",
"com_ui_detailed": "詳細",
"com_ui_download_error": "下載檔案時發生錯誤。該檔案可能已被刪除。",
"com_ui_dropdown_variables": "下拉式變數:",
"com_ui_dropdown_variables_info": "為您的提示建立自訂下拉選單:`{{variable_name:選項1|選項2|選項3}}`",
@@ -621,6 +650,7 @@
"com_ui_duplication_processing": "正在複製對話...",
"com_ui_duplication_success": "已成功複製對話",
"com_ui_edit": "編輯",
"com_ui_empty_category": "-",
"com_ui_endpoint": "端點",
"com_ui_endpoint_menu": "語言模型端點選單",
"com_ui_enter": "輸入",
@@ -658,7 +688,10 @@
"com_ui_generate_qrcode": "產生 QR 碼",
"com_ui_generating": "產生中...",
"com_ui_go_to_conversation": "前往對話",
"com_ui_good_evening": "晚安",
"com_ui_good_morning": "早安",
"com_ui_happy_birthday": "這是我的第一個生日!",
"com_ui_high": "高",
"com_ui_host": "主機",
"com_ui_image_gen": "影像生成",
"com_ui_import_conversation_error": "匯入對話時發生錯誤",
@@ -668,17 +701,20 @@
"com_ui_include_shadcnui": "包含 shadcn/ui 元件說明",
"com_ui_input": "輸入",
"com_ui_instructions": "說明",
"com_ui_key": "鍵",
"com_ui_latest_footer": "讓每個人都能使用 AI",
"com_ui_librechat_code_api_key": "取得你的 LibreChat 程式碼解譯器 API 金鑰",
"com_ui_librechat_code_api_subtitle": "安全性高。多語言支援。檔案輸入/輸出。",
"com_ui_librechat_code_api_title": "執行 AI 程式碼",
"com_ui_locked": "已鎖定",
"com_ui_logo": "{{0}} 標誌",
"com_ui_low": "低",
"com_ui_manage": "管理",
"com_ui_max_tags": "允許的最大數量為 {{0}},已使用最新值。",
"com_ui_mcp_enter_var": "請輸入 {{0}} 的值",
"com_ui_mcp_server_not_found": "找不到伺服器。",
"com_ui_mcp_url": "MCP 伺服器",
"com_ui_medium": "中",
"com_ui_memories": "記憶",
"com_ui_memories_allow_create": "允許建立記憶",
"com_ui_memories_allow_opt_out": "允許用戶選擇不使用記憶功能",
@@ -700,21 +736,25 @@
"com_ui_memory_would_exceed": "無法儲存——會超出限制 {{tokens}} 個 token。請刪除現有記憶以釋放空間。",
"com_ui_mention": "提及端點、助理或預設設定以快速切換",
"com_ui_min_tags": "無法再移除更多值,至少需要 {{0}} 個。",
"com_ui_minimal": "最小值",
"com_ui_model": "模型",
"com_ui_model_parameters": "模型參數",
"com_ui_more_info": "更多資訊",
"com_ui_my_prompts": "我的提示",
"com_ui_name": "名稱",
"com_ui_new": "新",
"com_ui_new_chat": "新對話",
"com_ui_next": "下一個",
"com_ui_no": "否",
"com_ui_no_bookmarks": "看來您還沒有任何書籤。請點選對話並新增一個書籤",
"com_ui_no_category": "無分類",
"com_ui_no_terms_content": "沒有條款和條件內容顯示",
"com_ui_none": "無",
"com_ui_nothing_found": "找不到任何內容",
"com_ui_of": "的",
"com_ui_off": "關閉",
"com_ui_on": "開啟",
"com_ui_openai": "OpenAI",
"com_ui_page": "頁面",
"com_ui_prev": "上一個",
"com_ui_preview": "預覽",
@@ -763,6 +803,7 @@
"com_ui_select_search_plugin": "依名稱搜尋外掛程式",
"com_ui_select_search_provider": "依名稱搜尋供應商",
"com_ui_select_search_region": "依名稱搜尋區域",
"com_ui_set": "設置",
"com_ui_share": "分享",
"com_ui_share_create_message": "您的姓名以及您在共享後新增的任何訊息都會保密。",
"com_ui_share_delete_error": "刪除共享連結時發生錯誤。",
@@ -780,8 +821,11 @@
"com_ui_stop": "停止",
"com_ui_storage": "儲存空間",
"com_ui_submit": "送出",
"com_ui_support_contact_email": "Email",
"com_ui_terms_and_conditions": "條款和條件",
"com_ui_terms_of_service": "服務條款",
"com_ui_thoughts": "思考過程",
"com_ui_token": "token",
"com_ui_tools": "工具",
"com_ui_unarchive": "取消封存",
"com_ui_unarchive_error": "取消封存對話時發生錯誤",
@@ -825,6 +869,7 @@
"com_ui_web_search_searxng_instance_url": "SearXNG Instance URL",
"com_ui_web_searching": "正在搜尋網路",
"com_ui_web_searching_again": "正在重新搜尋網路",
"com_ui_weekend_morning": "週末愉快",
"com_ui_yes": "是",
"com_ui_zoom": "縮放",
"com_user_message": "您"

View File

@@ -207,4 +207,36 @@ y$ which spans lines`;
const expected = 'Set $$\\{x | x > \\$0\\}$$ for positive prices';
expect(preprocessLaTeX(content)).toBe(expected);
});
test('does not convert when closing dollar is preceded by backtick', () => {
const content = 'The error "invalid $lookup namespace" occurs when using `$lookup` operator';
const expected = 'The error "invalid $lookup namespace" occurs when using `$lookup` operator';
expect(preprocessLaTeX(content)).toBe(expected);
});
test('handles mixed backtick and non-backtick cases', () => {
const content = 'Use $x + y$ in math but `$lookup` in code';
const expected = 'Use $$x + y$$ in math but `$lookup` in code';
expect(preprocessLaTeX(content)).toBe(expected);
});
test('escapes currency amounts without commas', () => {
const content =
'The total amount invested is $1157.90 (existing amount) + $500 (new investment) = $1657.90.';
const expected =
'The total amount invested is \\$1157.90 (existing amount) + \\$500 (new investment) = \\$1657.90.';
expect(preprocessLaTeX(content)).toBe(expected);
});
test('handles large currency amounts', () => {
const content = 'You can win $1000000 or even $9999999.99!';
const expected = 'You can win \\$1000000 or even \\$9999999.99!';
expect(preprocessLaTeX(content)).toBe(expected);
});
test('escapes currency with many decimal places', () => {
const content = 'Bitcoin: $0.00001234, Gas: $3.999, Rate: $1.234567890';
const expected = 'Bitcoin: \\$0.00001234, Gas: \\$3.999, Rate: \\$1.234567890';
expect(preprocessLaTeX(content)).toBe(expected);
});
});

View File

@@ -3,9 +3,8 @@ const MHCHEM_CE_REGEX = /\$\\ce\{/g;
const MHCHEM_PU_REGEX = /\$\\pu\{/g;
const MHCHEM_CE_ESCAPED_REGEX = /\$\\\\ce\{[^}]*\}\$/g;
const MHCHEM_PU_ESCAPED_REGEX = /\$\\\\pu\{[^}]*\}\$/g;
const CURRENCY_REGEX =
/(?<![\\$])\$(?!\$)(?=\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?(?:\s|$|[^a-zA-Z\d]))/g;
const SINGLE_DOLLAR_REGEX = /(?<!\\)\$(?!\$)((?:[^$\n]|\\[$])+?)(?<!\\)\$(?!\$)/g;
const CURRENCY_REGEX = /(?<![\\$])\$(?!\$)(?=\d+(?:,\d{3})*(?:\.\d+)?(?:\s|$|[^a-zA-Z\d]))/g;
const SINGLE_DOLLAR_REGEX = /(?<!\\)\$(?!\$)((?:[^$\n]|\\[$])+?)(?<!\\)(?<!`)\$(?!\$)/g;
/**
* Escapes mhchem package notation in LaTeX by converting single dollar delimiters to double dollars

View File

@@ -16,6 +16,38 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
logger.info('Starting Enhanced Agent Permissions Migration', { dryRun, batchSize });
/** Ensurse `aclentries` collection exists for DocumentDB compatibility
* @param {import('mongoose').mongo.Db} db
* @param {string} collectionName
*/
async function ensureCollectionExists(db, collectionName) {
try {
const collections = await db.listCollections({ name: collectionName }).toArray();
if (collections.length === 0) {
await db.createCollection(collectionName);
logger.info(`Created collection: ${collectionName}`);
} else {
logger.info(`Collection already exists: ${collectionName}`);
}
} catch (error) {
logger.error(`'Failed to check/create "${collectionName}" collection:`, error);
// If listCollections fails, try alternative approach
try {
// Try to access the collection directly - this will create it in MongoDB if it doesn't exist
await db.collection(collectionName).findOne({}, { projection: { _id: 1 } });
} catch (createError) {
logger.error(`Could not ensure collection ${collectionName} exists:`, createError);
}
}
}
const mongoose = require('mongoose');
/** @type {import('mongoose').mongo.Db | undefined} */
const db = mongoose.connection.db;
if (db) {
await ensureCollectionExists(db, 'aclentries');
}
// Verify required roles exist
const ownerRole = await findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
const viewerRole = await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
@@ -100,12 +132,19 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
}
});
logger.info('Agent categorization:', {
globalEditAccess: categories.globalEditAccess.length,
globalViewAccess: categories.globalViewAccess.length,
privateAgents: categories.privateAgents.length,
total: agentsToMigrate.length,
});
logger.info(
'Agent categorization:\n' +
JSON.stringify(
{
globalEditAccess: categories.globalEditAccess.length,
globalViewAccess: categories.globalViewAccess.length,
privateAgents: categories.privateAgents.length,
total: agentsToMigrate.length,
},
null,
2,
),
);
if (dryRun) {
return {

View File

@@ -16,6 +16,38 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
logger.info('Starting PromptGroup Permissions Migration', { dryRun, batchSize });
/** Ensurse `aclentries` collection exists for DocumentDB compatibility
* @param {import('mongoose').mongo.Db} db
* @param {string} collectionName
*/
async function ensureCollectionExists(db, collectionName) {
try {
const collections = await db.listCollections({ name: collectionName }).toArray();
if (collections.length === 0) {
await db.createCollection(collectionName);
logger.info(`Created collection: ${collectionName}`);
} else {
logger.info(`Collection already exists: ${collectionName}`);
}
} catch (error) {
logger.error(`'Failed to check/create "${collectionName}" collection:`, error);
// If listCollections fails, try alternative approach
try {
// Try to access the collection directly - this will create it in MongoDB if it doesn't exist
await db.collection(collectionName).findOne({}, { projection: { _id: 1 } });
} catch (createError) {
logger.error(`Could not ensure collection ${collectionName} exists:`, createError);
}
}
}
const mongoose = require('mongoose');
/** @type {import('mongoose').mongo.Db | undefined} */
const db = mongoose.connection.db;
if (db) {
await ensureCollectionExists(db, 'aclentries');
}
// Verify required roles exist
const ownerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER);
const viewerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER);
@@ -91,11 +123,18 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
}
});
logger.info('PromptGroup categorization:', {
globalViewAccess: categories.globalViewAccess.length,
privateGroups: categories.privateGroups.length,
total: promptGroupsToMigrate.length,
});
logger.info(
'PromptGroup categorization:\n' +
JSON.stringify(
{
globalViewAccess: categories.globalViewAccess.length,
privateGroups: categories.privateGroups.length,
total: promptGroupsToMigrate.length,
},
null,
2,
),
);
if (dryRun) {
return {

View File

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

View File

@@ -23,7 +23,7 @@ version: 1.8.10
# It is recommended to use it with quotes.
# renovate: image=ghcr.io/danny-avila/librechat
appVersion: "v0.8.0-rc2"
appVersion: "v0.8.0-rc3"
home: https://www.librechat.ai

View File

@@ -1,149 +0,0 @@
# Example configuration demonstrating model spec subfolder/category support
# This shows how to organize model specs into folders for better organization
version: 1.1.7
modelSpecs:
enforce: false
prioritize: true
list:
# OpenAI Models Category
- name: "gpt4_turbo"
label: "GPT-4 Turbo"
folder: "OpenAI Models" # This spec will appear under "OpenAI Models" folder
preset:
endpoint: "openAI"
model: "gpt-4-turbo-preview"
temperature: 0.7
description: "Latest GPT-4 Turbo model with enhanced capabilities"
iconURL: "openAI"
order: 1
- name: "gpt4_vision"
label: "GPT-4 Vision"
folder: "OpenAI Models" # Same folder as above
preset:
endpoint: "openAI"
model: "gpt-4-vision-preview"
temperature: 0.7
description: "GPT-4 with vision capabilities"
iconURL: "openAI"
order: 2
- name: "gpt35_turbo"
label: "GPT-3.5 Turbo"
folder: "OpenAI Models"
preset:
endpoint: "openAI"
model: "gpt-3.5-turbo"
temperature: 0.7
description: "Fast and efficient model for most tasks"
iconURL: "openAI"
order: 3
# Anthropic Models Category
- name: "claude3_opus"
label: "Claude 3 Opus"
folder: "Anthropic Models" # Different folder
preset:
endpoint: "anthropic"
model: "claude-3-opus-20240229"
temperature: 0.7
description: "Most capable Claude model"
iconURL: "anthropic"
order: 1
- name: "claude3_sonnet"
label: "Claude 3 Sonnet"
folder: "Anthropic Models"
preset:
endpoint: "anthropic"
model: "claude-3-sonnet-20240229"
temperature: 0.7
description: "Balanced performance and cost"
iconURL: "anthropic"
order: 2
- name: "claude3_haiku"
label: "Claude 3 Haiku"
folder: "Anthropic Models"
preset:
endpoint: "anthropic"
model: "claude-3-haiku-20240307"
temperature: 0.7
description: "Fast and affordable"
iconURL: "anthropic"
order: 3
# Specialized Models Category
- name: "code_assistant"
label: "Code Assistant"
folder: "Specialized Models"
preset:
endpoint: "openAI"
model: "gpt-4-turbo-preview"
temperature: 0.2
systemMessage: "You are an expert programmer. Provide clear, well-commented code solutions."
description: "Optimized for coding tasks"
iconURL: "openAI"
- name: "creative_writer"
label: "Creative Writer"
folder: "Specialized Models"
preset:
endpoint: "anthropic"
model: "claude-3-opus-20240229"
temperature: 0.9
systemMessage: "You are a creative writer. Generate engaging and imaginative content."
description: "For creative writing tasks"
iconURL: "anthropic"
- name: "research_analyst"
label: "Research Analyst"
folder: "Specialized Models"
preset:
endpoint: "openAI"
model: "gpt-4-turbo-preview"
temperature: 0.3
systemMessage: "You are a research analyst. Provide thorough, fact-based analysis."
description: "For research and analysis"
iconURL: "openAI"
# Models without folders (appear at root level)
- name: "quick_chat"
label: "Quick Chat"
preset:
endpoint: "openAI"
model: "gpt-3.5-turbo"
temperature: 0.7
description: "Fast general-purpose chat"
iconURL: "openAI"
default: true # This is the default model
- name: "advanced_reasoning"
label: "Advanced Reasoning"
preset:
endpoint: "anthropic"
model: "claude-3-opus-20240229"
temperature: 0.5
description: "For complex reasoning tasks"
iconURL: "anthropic"
# Interface configuration
interface:
endpointsMenu: false # Hide endpoints menu when using model specs
modelSelect: false # Hide traditional model selector
parameters: false # Hide parameter controls (using presets)
presets: false # Hide preset selector (using model specs)
# Endpoints configuration (required for the model specs to work)
endpoints:
openAI:
apiKey: "${OPENAI_API_KEY}"
models:
default: ["gpt-3.5-turbo", "gpt-4-turbo-preview", "gpt-4-vision-preview"]
anthropic:
apiKey: "${ANTHROPIC_API_KEY}"
models:
default: ["claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"]

466
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "LibreChat",
"version": "v0.8.0-rc2",
"version": "v0.8.0-rc3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "LibreChat",
"version": "v0.8.0-rc2",
"version": "v0.8.0-rc3",
"license": "ISC",
"workspaces": [
"api",
@@ -46,7 +46,7 @@
},
"api": {
"name": "@librechat/backend",
"version": "v0.8.0-rc2",
"version": "v0.8.0-rc3",
"license": "ISC",
"dependencies": {
"@anthropic-ai/sdk": "^0.52.0",
@@ -2623,7 +2623,7 @@
},
"client": {
"name": "@librechat/frontend",
"version": "v0.8.0-rc2",
"version": "v0.8.0-rc3",
"license": "ISC",
"dependencies": {
"@ariakit/react": "^0.4.15",
@@ -17654,6 +17654,454 @@
"kuler": "^2.0.0"
}
},
"node_modules/@dicebear/adventurer": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.2.4.tgz",
"integrity": "sha512-Xvboay3VH1qe7lH17T+bA3qPawf5EjccssDiyhCX/VT0P21c65JyjTIUJV36Nsv08HKeyDscyP0kgt9nPTRKvA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/adventurer-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-9.2.4.tgz",
"integrity": "sha512-I9IrB4ZYbUHSOUpWoUbfX3vG8FrjcW8htoQ4bEOR7TYOKKE11Mo1nrGMuHZ7GPfwN0CQeK1YVJhWqLTmtYn7Pg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/avataaars": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-9.2.4.tgz",
"integrity": "sha512-QKNBtA/1QGEzR+JjS4XQyrFHYGbzdOp0oa6gjhGhUDrMegDFS8uyjdRfDQsFTebVkyLWjgBQKZEiDqKqHptB6A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/avataaars-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-9.2.4.tgz",
"integrity": "sha512-HtBvA7elRv50QTOOsBdtYB1GVimCpGEDlDgWsu1snL5Z3d1+3dIESoXQd3mXVvKTVT8Z9ciA4TEaF09WfxDjAA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/big-ears": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-9.2.4.tgz",
"integrity": "sha512-U33tbh7Io6wG6ViUMN5fkWPER7hPKMaPPaYgafaYQlCT4E7QPKF2u8X1XGag3jCKm0uf4SLXfuZ8v+YONcHmNQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/big-ears-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-9.2.4.tgz",
"integrity": "sha512-pPjYu80zMFl43A9sa5+tAKPkhp4n9nd7eN878IOrA1HAowh/XePh5JN8PTkNFS9eM+rnN9m8WX08XYFe30kLYw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/big-smile": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.2.4.tgz",
"integrity": "sha512-zeEfXOOXy7j9tfkPLzfQdLBPyQsctBetTdEfKRArc1k3RUliNPxfJG9j88+cXQC6GXrVW2pcT2X50NSPtugCFQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/bottts": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-9.2.4.tgz",
"integrity": "sha512-4CTqrnVg+NQm6lZ4UuCJish8gGWe8EqSJrzvHQRO5TEyAKjYxbTdVqejpkycG1xkawha4FfxsYgtlSx7UwoVMw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/bottts-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-9.2.4.tgz",
"integrity": "sha512-eMVdofdD/udHsKIaeWEXShDRtiwk7vp4FjY7l0f79vIzfhkIsXKEhPcnvHKOl/yoArlDVS3Uhgjj0crWTO9RJA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/collection": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-9.2.4.tgz",
"integrity": "sha512-I1wCUp0yu5qSIeMQHmDYXQIXKkKjcja/SYBxppPkYFXpR2alxb0k9/swFDdMbkY6a1c9AT1kI1y+Pg6ywQ2rTA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dicebear/adventurer": "9.2.4",
"@dicebear/adventurer-neutral": "9.2.4",
"@dicebear/avataaars": "9.2.4",
"@dicebear/avataaars-neutral": "9.2.4",
"@dicebear/big-ears": "9.2.4",
"@dicebear/big-ears-neutral": "9.2.4",
"@dicebear/big-smile": "9.2.4",
"@dicebear/bottts": "9.2.4",
"@dicebear/bottts-neutral": "9.2.4",
"@dicebear/croodles": "9.2.4",
"@dicebear/croodles-neutral": "9.2.4",
"@dicebear/dylan": "9.2.4",
"@dicebear/fun-emoji": "9.2.4",
"@dicebear/glass": "9.2.4",
"@dicebear/icons": "9.2.4",
"@dicebear/identicon": "9.2.4",
"@dicebear/initials": "9.2.4",
"@dicebear/lorelei": "9.2.4",
"@dicebear/lorelei-neutral": "9.2.4",
"@dicebear/micah": "9.2.4",
"@dicebear/miniavs": "9.2.4",
"@dicebear/notionists": "9.2.4",
"@dicebear/notionists-neutral": "9.2.4",
"@dicebear/open-peeps": "9.2.4",
"@dicebear/personas": "9.2.4",
"@dicebear/pixel-art": "9.2.4",
"@dicebear/pixel-art-neutral": "9.2.4",
"@dicebear/rings": "9.2.4",
"@dicebear/shapes": "9.2.4",
"@dicebear/thumbs": "9.2.4"
},
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/core": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.2.4.tgz",
"integrity": "sha512-hz6zArEcUwkZzGOSJkWICrvqnEZY7BKeiq9rqKzVJIc1tRVv0MkR0FGvIxSvXiK9TTIgKwu656xCWAGAl6oh+w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/json-schema": "^7.0.11"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@dicebear/croodles": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-9.2.4.tgz",
"integrity": "sha512-CqT0NgVfm+5kd+VnjGY4WECNFeOrj5p7GCPTSEA7tCuN72dMQOX47P9KioD3wbExXYrIlJgOcxNrQeb/FMGc3A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/croodles-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-9.2.4.tgz",
"integrity": "sha512-8vAS9lIEKffSUVx256GSRAlisB8oMX38UcPWw72venO/nitLVsyZ6hZ3V7eBdII0Onrjqw1RDndslQODbVcpTw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/dylan": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/dylan/-/dylan-9.2.4.tgz",
"integrity": "sha512-tiih1358djAq0jDDzmW3N3S4C3ynC2yn4hhlTAq/MaUAQtAi47QxdHdFGdxH0HBMZKqA4ThLdVk3yVgN4xsukg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/fun-emoji": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-9.2.4.tgz",
"integrity": "sha512-Od729skczse1HvHekgEFv+mSuJKMC4sl5hENGi/izYNe6DZDqJrrD0trkGT/IVh/SLXUFbq1ZFY9I2LoUGzFZg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/glass": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/glass/-/glass-9.2.4.tgz",
"integrity": "sha512-5lxbJode1t99eoIIgW0iwZMoZU4jNMJv/6vbsgYUhAslYFX5zP0jVRscksFuo89TTtS7YKqRqZAL3eNhz4bTDw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/icons": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-9.2.4.tgz",
"integrity": "sha512-bRsK1qj8u9Z76xs8XhXlgVr/oHh68tsHTJ/1xtkX9DeTQTSamo2tS26+r231IHu+oW3mePtFnwzdG9LqEPRd4A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/identicon": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-9.2.4.tgz",
"integrity": "sha512-R9nw/E8fbu9HltHOqI9iL/o9i7zM+2QauXWMreQyERc39oGR9qXiwgBxsfYGcIS4C85xPyuL5B3I2RXrLBlJPg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/initials": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-9.2.4.tgz",
"integrity": "sha512-4SzHG5WoQZl1TGcpEZR4bdsSkUVqwNQCOwWSPAoBJa3BNxbVsvL08LF7I97BMgrCoknWZjQHUYt05amwTPTKtg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/lorelei": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-9.2.4.tgz",
"integrity": "sha512-eS4mPYUgDpo89HvyFAx/kgqSSKh8W4zlUA8QJeIUCWTB0WpQmeqkSgIyUJjGDYSrIujWi+zEhhckksM5EwW0Dg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/lorelei-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-9.2.4.tgz",
"integrity": "sha512-bWq2/GonbcJULtT+B/MGcM2UnA7kBQoH+INw8/oW83WI3GNTZ6qEwe3/W4QnCgtSOhUsuwuiSULguAFyvtkOZQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/micah": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-9.2.4.tgz",
"integrity": "sha512-XNWJ8Mx+pncIV8Ye0XYc/VkMiax8kTxcP3hLTC5vmELQyMSLXzg/9SdpI+W/tCQghtPZRYTT3JdY9oU9IUlP2g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/miniavs": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-9.2.4.tgz",
"integrity": "sha512-k7IYTAHE/4jSO6boMBRrNlqPT3bh7PLFM1atfe0nOeCDwmz/qJUBP3HdONajbf3fmo8f2IZYhELrNWTOE7Ox3Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/notionists": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-9.2.4.tgz",
"integrity": "sha512-zcvpAJ93EfC0xQffaPZQuJPShwPhnu9aTcoPsaYGmw0oEDLcv2XYmDhUUdX84QYCn6LtCZH053rHLVazRW+OGw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/notionists-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/notionists-neutral/-/notionists-neutral-9.2.4.tgz",
"integrity": "sha512-fskWzBVxQzJhCKqY24DGZbYHSBaauoRa1DgXM7+7xBuksH7mfbTmZTvnUAsAqJYBkla8IPb4ERKduDWtlWYYjQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/open-peeps": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-9.2.4.tgz",
"integrity": "sha512-s6nwdjXFsplqEI7imlsel4Gt6kFVJm6YIgtZSpry0UdwDoxUUudei5bn957j9lXwVpVUcRjJW+TuEKztYjXkKQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/personas": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-9.2.4.tgz",
"integrity": "sha512-JNim8RfZYwb0MfxW6DLVfvreCFIevQg+V225Xe5tDfbFgbcYEp4OU/KaiqqO2476OBjCw7i7/8USbv2acBhjwA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/pixel-art": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-9.2.4.tgz",
"integrity": "sha512-4Ao45asieswUdlCTBZqcoF/0zHR3OWUWB0Mvhlu9b1Fbc6IlPBiOfx2vsp6bnVGVnMag58tJLecx2omeXdECBQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/pixel-art-neutral": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-9.2.4.tgz",
"integrity": "sha512-ZITPLD1cPN4GjKkhWi80s7e5dcbXy34ijWlvmxbc4eb/V7fZSsyRa9EDUW3QStpo+xrCJLcLR+3RBE5iz0PC/A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/rings": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-9.2.4.tgz",
"integrity": "sha512-teZxELYyV2ogzgb5Mvtn/rHptT0HXo9SjUGS4A52mOwhIdHSGGU71MqA1YUzfae9yJThsw6K7Z9kzuY2LlZZHA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/shapes": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-9.2.4.tgz",
"integrity": "sha512-MhK9ZdFm1wUnH4zWeKPRMZ98UyApolf5OLzhCywfu38tRN6RVbwtBRHc/42ZwoN1JU1JgXr7hzjYucMqISHtbA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@dicebear/thumbs": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-9.2.4.tgz",
"integrity": "sha512-EL4sMqv9p2+1Xy3d8e8UxyeKZV2+cgt3X2x2RTRzEOIIhobtkL8u6lJxmJbiGbpVtVALmrt5e7gjmwqpryYDpg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@dicebear/core": "^9.0.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.0.tgz",
@@ -51358,7 +51806,7 @@
},
"packages/api": {
"name": "@librechat/api",
"version": "1.3.3",
"version": "1.3.4",
"license": "ISC",
"devDependencies": {
"@babel/preset-env": "^7.21.5",
@@ -51484,7 +51932,7 @@
},
"packages/client": {
"name": "@librechat/client",
"version": "0.2.6",
"version": "0.2.7",
"devDependencies": {
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^25.0.2",
@@ -51512,6 +51960,8 @@
"peerDependencies": {
"@ariakit/react": "^0.4.16",
"@ariakit/react-core": "^0.4.17",
"@dicebear/collection": "^9.2.2",
"@dicebear/core": "^9.2.2",
"@headlessui/react": "^2.1.2",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.0.2",
@@ -51783,7 +52233,7 @@
},
"packages/data-provider": {
"name": "librechat-data-provider",
"version": "0.8.003",
"version": "0.8.004",
"license": "ISC",
"dependencies": {
"axios": "^1.8.2",
@@ -51888,7 +52338,7 @@
},
"packages/data-schemas": {
"name": "@librechat/data-schemas",
"version": "0.0.19",
"version": "0.0.20",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-alias": "^5.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "LibreChat",
"version": "v0.8.0-rc2",
"version": "v0.8.0-rc3",
"description": "",
"workspaces": [
"api",

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/api",
"version": "1.3.3",
"version": "1.3.4",
"type": "commonjs",
"description": "MCP services for LibreChat",
"main": "dist/index.js",

View File

@@ -1,7 +1,7 @@
import { logger } from '@librechat/data-schemas';
import { AccessRoleIds, ResourceType, PrincipalType, Constants } from 'librechat-data-provider';
import type { AccessRoleMethods, IAgent } from '@librechat/data-schemas';
import type { Model } from 'mongoose';
import type { Model, Mongoose, mongo } from 'mongoose';
const { GLOBAL_PROJECT_NAME } = Constants;
@@ -17,7 +17,8 @@ export interface MigrationCheckDbMethods {
}
export interface MigrationCheckParams {
db: MigrationCheckDbMethods;
mongoose: Mongoose;
methods: MigrationCheckDbMethods;
AgentModel: Model<IAgent>;
}
@@ -46,16 +47,44 @@ export interface MigrationCheckResult {
* This performs a dry-run check similar to the migration script
*/
export async function checkAgentPermissionsMigration({
db,
methods,
mongoose,
AgentModel,
}: MigrationCheckParams): Promise<MigrationCheckResult> {
logger.debug('Checking if agent permissions migration is needed');
try {
/** Ensurse `aclentries` collection exists for DocumentDB compatibility */
async function ensureCollectionExists(db: mongo.Db, collectionName: string) {
try {
const collections = await db.listCollections({ name: collectionName }).toArray();
if (collections.length === 0) {
await db.createCollection(collectionName);
logger.info(`Created collection: ${collectionName}`);
} else {
logger.debug(`Collection already exists: ${collectionName}`);
}
} catch (error) {
logger.error(`'Failed to check/create "${collectionName}" collection:`, error);
// If listCollections fails, try alternative approach
try {
// Try to access the collection directly - this will create it in MongoDB if it doesn't exist
await db.collection(collectionName).findOne({}, { projection: { _id: 1 } });
} catch (createError) {
logger.error(`Could not ensure collection ${collectionName} exists:`, createError);
}
}
}
const db = mongoose.connection.db;
if (db) {
await ensureCollectionExists(db, 'aclentries');
}
// Verify required roles exist
const ownerRole = await db.findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
const viewerRole = await db.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
const editorRole = await db.findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
const ownerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
const viewerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
const editorRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
if (!ownerRole || !viewerRole || !editorRole) {
logger.warn(
@@ -70,7 +99,7 @@ export async function checkAgentPermissionsMigration({
}
// Get global project agent IDs
const globalProject = await db.getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']);
const globalProject = await methods.getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']);
const globalAgentIds = new Set(globalProject?.agentIds || []);
// Find agents without ACL entries (no batching for efficiency on startup)

View File

@@ -1,7 +1,7 @@
import { logger } from '@librechat/data-schemas';
import { AccessRoleIds, ResourceType, PrincipalType, Constants } from 'librechat-data-provider';
import type { AccessRoleMethods, IPromptGroupDocument } from '@librechat/data-schemas';
import type { Model } from 'mongoose';
import type { Model, Mongoose, mongo } from 'mongoose';
const { GLOBAL_PROJECT_NAME } = Constants;
@@ -17,7 +17,8 @@ export interface PromptMigrationCheckDbMethods {
}
export interface PromptMigrationCheckParams {
db: PromptMigrationCheckDbMethods;
mongoose: Mongoose;
methods: PromptMigrationCheckDbMethods;
PromptGroupModel: Model<IPromptGroupDocument>;
}
@@ -44,16 +45,45 @@ export interface PromptMigrationCheckResult {
* This performs a dry-run check similar to the migration script
*/
export async function checkPromptPermissionsMigration({
db,
methods,
mongoose,
PromptGroupModel,
}: PromptMigrationCheckParams): Promise<PromptMigrationCheckResult> {
logger.debug('Checking if prompt permissions migration is needed');
try {
/** Ensurse `aclentries` collection exists for DocumentDB compatibility */
async function ensureCollectionExists(db: mongo.Db, collectionName: string) {
try {
const collections = await db.listCollections({ name: collectionName }).toArray();
if (collections.length === 0) {
await db.createCollection(collectionName);
logger.info(`Created collection: ${collectionName}`);
} else {
logger.debug(`Collection already exists: ${collectionName}`);
}
} catch (error) {
logger.error(`'Failed to check/create "${collectionName}" collection:`, error);
// If listCollections fails, try alternative approach
try {
// Try to access the collection directly - this will create it in MongoDB if it doesn't exist
await db.collection(collectionName).findOne({}, { projection: { _id: 1 } });
} catch (createError) {
logger.error(`Could not ensure collection ${collectionName} exists:`, createError);
}
}
}
/** Native MongoDB database instance */
const db = mongoose.connection.db;
if (db) {
await ensureCollectionExists(db, 'aclentries');
}
// Verify required roles exist
const ownerRole = await db.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER);
const viewerRole = await db.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER);
const editorRole = await db.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR);
const ownerRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER);
const viewerRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER);
const editorRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR);
if (!ownerRole || !viewerRole || !editorRole) {
logger.warn(
@@ -66,8 +96,8 @@ export async function checkPromptPermissionsMigration({
};
}
// Get global project prompt group IDs
const globalProject = await db.getProjectByName(GLOBAL_PROJECT_NAME, ['promptGroupIds']);
/** Global project prompt group IDs */
const globalProject = await methods.getProjectByName(GLOBAL_PROJECT_NAME, ['promptGroupIds']);
const globalPromptGroupIds = new Set(
(globalProject?.promptGroupIds || []).map((id) => id.toString()),
);

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/client",
"version": "0.2.6",
"version": "0.2.7",
"description": "React components for LibreChat",
"main": "dist/index.js",
"module": "dist/index.es.js",
@@ -64,7 +64,9 @@
"react-hook-form": "^7.56.4",
"react-resizable-panels": "^3.0.2",
"react-textarea-autosize": "^8.4.0",
"tailwind-merge": "^1.9.1"
"tailwind-merge": "^1.9.1",
"@dicebear/core": "^9.2.2",
"@dicebear/collection": "^9.2.2"
},
"devDependencies": {
"@rollup/plugin-alias": "^5.1.0",

View File

@@ -0,0 +1,102 @@
import React, { useState, useMemo, useCallback } from 'react';
import type { TUser } from 'librechat-data-provider';
import { Skeleton } from './Skeleton';
import { useAvatar } from '~/hooks';
import { UserIcon } from '~/svgs';
export interface AvatarProps {
user?: TUser;
size?: number;
className?: string;
alt?: string;
showDefaultWhenEmpty?: boolean;
}
const Avatar: React.FC<AvatarProps> = ({
user,
size = 32,
className = '',
alt,
showDefaultWhenEmpty = true,
}) => {
const avatarSrc = useAvatar(user);
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
const avatarSeed = useMemo(
() => user?.avatar || user?.username || user?.email || '',
[user?.avatar, user?.username, user?.email],
);
const altText = useMemo(
() => alt || `${user?.name || user?.username || user?.email || ''}'s avatar`,
[alt, user?.name, user?.username, user?.email],
);
const imageSrc = useMemo(() => {
if (!avatarSeed || imageError) return '';
return (user?.avatar ?? '') || avatarSrc || '';
}, [user?.avatar, avatarSrc, avatarSeed, imageError]);
const handleImageLoad = useCallback(() => {
setImageLoaded(true);
}, []);
const handleImageError = useCallback(() => {
setImageError(true);
setImageLoaded(false);
}, []);
const DefaultAvatar = useCallback(
() => (
<div
style={{
backgroundColor: 'rgb(121, 137, 255)',
width: `${size}px`,
height: `${size}px`,
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
}}
className={`relative flex items-center justify-center rounded-full p-1 text-text-primary ${className}`}
aria-hidden="true"
>
<UserIcon />
</div>
),
[size, className],
);
if (avatarSeed.length === 0 && showDefaultWhenEmpty) {
return <DefaultAvatar />;
}
if (avatarSeed.length > 0 && !imageError) {
return (
<div className="relative" style={{ width: `${size}px`, height: `${size}px` }}>
{!imageLoaded && (
<Skeleton className="rounded-full" style={{ width: `${size}px`, height: `${size}px` }} />
)}
<img
style={{
width: `${size}px`,
height: `${size}px`,
display: imageLoaded ? 'block' : 'none',
}}
className={`rounded-full ${className}`}
src={imageSrc}
alt={altText}
onLoad={handleImageLoad}
onError={handleImageError}
/>
</div>
);
}
if (imageError && showDefaultWhenEmpty) {
return <DefaultAvatar />;
}
return null;
};
export default Avatar;

View File

@@ -33,6 +33,7 @@ export * from './Resizable';
export * from './Select';
export { default as Radio } from './Radio';
export { default as Badge } from './Badge';
export { default as Avatar } from './Avatar';
export { default as Combobox } from './Combobox';
export { default as Dropdown } from './Dropdown';
export { default as SplitText } from './SplitText';

View File

@@ -3,6 +3,7 @@
export type { TranslationKeys } from './useLocalize';
export { default as useToast } from './useToast';
export { default as useAvatar } from './useAvatar';
export { default as useCombobox } from './useCombobox';
export { default as useLocalize } from './useLocalize';
export { default as useMediaQuery } from './useMediaQuery';

View File

@@ -1,6 +1,6 @@
{
"name": "librechat-data-provider",
"version": "0.8.003",
"version": "0.8.004",
"description": "data services for librechat apps",
"main": "dist/index.js",
"module": "dist/index.es.js",

View File

@@ -1520,7 +1520,7 @@ export enum TTSProviders {
/** Enum for app-wide constants */
export enum Constants {
/** Key for the app's version. */
VERSION = 'v0.8.0-rc2',
VERSION = 'v0.8.0-rc3',
/** Key for the Custom Config's version (librechat.yaml). */
CONFIG_VERSION = '1.2.8',
/** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */

View File

@@ -19,7 +19,6 @@ export type TModelSpec = {
showIconInHeader?: boolean;
iconURL?: string | EModelEndpoint; // Allow using project-included icons
authType?: AuthType;
folder?: string; // Optional folder/category for grouping model specs
};
export const tModelSpecSchema = z.object({
@@ -33,7 +32,6 @@ export const tModelSpecSchema = z.object({
showIconInHeader: z.boolean().optional(),
iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),
authType: authTypeSchema.optional(),
folder: z.string().optional(),
});
export const specsConfigSchema = z.object({

View File

@@ -164,11 +164,12 @@ export type TCategory = {
id?: string;
value: string;
label: string;
description?: string;
custom?: boolean;
};
export type TMarketplaceCategory = TCategory & {
count: number;
description?: string;
};
export type TError = {

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/data-schemas",
"version": "0.0.19",
"version": "0.0.20",
"description": "Mongoose schemas and models for LibreChat",
"type": "module",
"main": "dist/index.cjs",

View File

@@ -51,7 +51,7 @@ const transports: winston.transport[] = [
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: fileFormat,
format: winston.format.combine(fileFormat, winston.format.json()),
}),
];

View File

@@ -1,5 +1,5 @@
import type { Model, Types, DeleteResult } from 'mongoose';
import type { IAgentCategory, AgentCategory } from '../types/agentCategory';
import type { Model, Types } from 'mongoose';
import type { IAgentCategory } from '~/types';
export function createAgentCategoryMethods(mongoose: typeof import('mongoose')) {
/**
@@ -52,8 +52,9 @@ export function createAgentCategoryMethods(mongoose: typeof import('mongoose'))
label?: string;
description?: string;
order?: number;
custom?: boolean;
}>,
): Promise<any> {
): Promise<import('mongoose').mongo.BulkWriteResult> {
const AgentCategory = mongoose.models.AgentCategory as Model<IAgentCategory>;
const operations = categories.map((category, index) => ({
@@ -66,6 +67,7 @@ export function createAgentCategoryMethods(mongoose: typeof import('mongoose'))
description: category.description || '',
order: category.order || index,
isActive: true,
custom: category.custom || false,
},
},
upsert: true,
@@ -145,63 +147,104 @@ export function createAgentCategoryMethods(mongoose: typeof import('mongoose'))
}
/**
* Ensure default categories exist, seed them if none are present
* @returns Promise<boolean> - true if categories were seeded, false if they already existed
* Ensure default categories exist and update them if they don't have localization keys
* @returns Promise<boolean> - true if categories were created/updated, false if no changes
*/
async function ensureDefaultCategories(): Promise<boolean> {
const existingCategories = await getAllCategories();
if (existingCategories.length > 0) {
return false; // Categories already exist
}
const AgentCategory = mongoose.models.AgentCategory as Model<IAgentCategory>;
const defaultCategories = [
{
value: 'general',
label: 'General',
description: 'General purpose agents for common tasks and inquiries',
label: 'com_agents_category_general',
description: 'com_agents_category_general_description',
order: 0,
},
{
value: 'hr',
label: 'Human Resources',
description: 'Agents specialized in HR processes, policies, and employee support',
label: 'com_agents_category_hr',
description: 'com_agents_category_hr_description',
order: 1,
},
{
value: 'rd',
label: 'Research & Development',
description: 'Agents focused on R&D processes, innovation, and technical research',
label: 'com_agents_category_rd',
description: 'com_agents_category_rd_description',
order: 2,
},
{
value: 'finance',
label: 'Finance',
description: 'Agents specialized in financial analysis, budgeting, and accounting',
label: 'com_agents_category_finance',
description: 'com_agents_category_finance_description',
order: 3,
},
{
value: 'it',
label: 'IT',
description: 'Agents for IT support, technical troubleshooting, and system administration',
label: 'com_agents_category_it',
description: 'com_agents_category_it_description',
order: 4,
},
{
value: 'sales',
label: 'Sales',
description: 'Agents focused on sales processes, customer relations.',
label: 'com_agents_category_sales',
description: 'com_agents_category_sales_description',
order: 5,
},
{
value: 'aftersales',
label: 'After Sales',
description: 'Agents specialized in post-sale support, maintenance, and customer service',
label: 'com_agents_category_aftersales',
description: 'com_agents_category_aftersales_description',
order: 6,
},
];
await seedCategories(defaultCategories);
return true; // Categories were seeded
const existingCategories = await getAllCategories();
const existingCategoryMap = new Map(existingCategories.map((cat) => [cat.value, cat]));
const updates = [];
let created = 0;
for (const defaultCategory of defaultCategories) {
const existingCategory = existingCategoryMap.get(defaultCategory.value);
if (existingCategory) {
const isNotCustom = !existingCategory.custom;
const needsLocalization = !existingCategory.label.startsWith('com_');
if (isNotCustom && needsLocalization) {
updates.push({
value: defaultCategory.value,
label: defaultCategory.label,
description: defaultCategory.description,
});
}
} else {
await createCategory({
...defaultCategory,
isActive: true,
custom: false,
});
created++;
}
}
if (updates.length > 0) {
const bulkOps = updates.map((update) => ({
updateOne: {
filter: { value: update.value, custom: { $ne: true } },
update: {
$set: {
label: update.label,
description: update.description,
},
},
},
}));
await AgentCategory.bulkWrite(bulkOps, { ordered: false });
}
return updates.length > 0 || created > 0;
}
return {

View File

@@ -1,4 +1,4 @@
import { Schema, Document } from 'mongoose';
import { Schema } from 'mongoose';
import type { IAgentCategory } from '~/types';
const agentCategorySchema = new Schema<IAgentCategory>(
@@ -31,6 +31,10 @@ const agentCategorySchema = new Schema<IAgentCategory>(
default: true,
index: true,
},
custom: {
type: Boolean,
default: false,
},
},
{
timestamps: true,

View File

@@ -11,6 +11,8 @@ export type AgentCategory = {
order: number;
/** Whether the category is active and should be displayed */
isActive: boolean;
/** Whether this is a custom user-created category */
custom?: boolean;
};
export type IAgentCategory = AgentCategory &