mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00
🏷️ 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
This commit is contained in:
parent
94426a3cae
commit
bbfe4002eb
22 changed files with 328 additions and 252 deletions
33
.github/workflows/i18n-unused-keys.yml
vendored
33
.github/workflows/i18n-unused-keys.yml
vendored
|
@ -1,5 +1,10 @@
|
||||||
name: Detect Unused i18next Strings
|
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:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
|
@ -7,6 +12,7 @@ on:
|
||||||
- "api/**"
|
- "api/**"
|
||||||
- "packages/data-provider/src/**"
|
- "packages/data-provider/src/**"
|
||||||
- "packages/client/**"
|
- "packages/client/**"
|
||||||
|
- "packages/data-schemas/src/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
detect-unused-i18n-keys:
|
detect-unused-i18n-keys:
|
||||||
|
@ -24,7 +30,7 @@ jobs:
|
||||||
|
|
||||||
# Define paths
|
# Define paths
|
||||||
I18N_FILE="client/src/locales/en/translation.json"
|
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
|
# Check if translation file exists
|
||||||
if [[ ! -f "$I18N_FILE" ]]; then
|
if [[ ! -f "$I18N_FILE" ]]; then
|
||||||
|
@ -52,6 +58,31 @@ jobs:
|
||||||
fi
|
fi
|
||||||
done
|
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
|
# Also check if the key is directly used somewhere
|
||||||
if [[ "$FOUND" == false ]]; then
|
if [[ "$FOUND" == false ]]; then
|
||||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||||
|
|
|
@ -46,7 +46,7 @@ router.use('/tools', tools);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all agent categories with counts
|
* Get all agent categories with counts
|
||||||
* @route GET /agents/marketplace/categories
|
* @route GET /agents/categories
|
||||||
*/
|
*/
|
||||||
router.get('/categories', v1.getAgentCategories);
|
router.get('/categories', v1.getAgentCategories);
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -225,7 +225,8 @@ export type AgentPanelContextType = {
|
||||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
agentsConfig?: t.TAgentsEndpoint;
|
agentsConfig?: t.TAgentsEndpoint | null;
|
||||||
|
endpointsConfig?: t.TEndpointsConfig | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AgentModelPanelProps = {
|
export type AgentModelPanelProps = {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Label } from '@librechat/client';
|
import { Label } from '@librechat/client';
|
||||||
import type t from 'librechat-data-provider';
|
import type t from 'librechat-data-provider';
|
||||||
|
import { useLocalize, TranslationKeys, useAgentCategories } from '~/hooks';
|
||||||
import { cn, renderAgentAvatar, getContactDisplayName } from '~/utils';
|
import { cn, renderAgentAvatar, getContactDisplayName } from '~/utils';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
|
|
||||||
interface AgentCardProps {
|
interface AgentCardProps {
|
||||||
agent: t.Agent; // The agent data to display
|
agent: t.Agent; // The agent data to display
|
||||||
|
@ -15,6 +15,21 @@ interface AgentCardProps {
|
||||||
*/
|
*/
|
||||||
const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' }) => {
|
const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' }) => {
|
||||||
const localize = useLocalize();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -49,9 +64,7 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' })
|
||||||
{/* Category tag */}
|
{/* Category tag */}
|
||||||
{agent.category && (
|
{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">
|
<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">
|
<Label className="line-clamp-1 font-normal">{categoryLabel}</Label>
|
||||||
{agent.category.charAt(0).toUpperCase() + agent.category.slice(1)}
|
|
||||||
</Label>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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;
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type t from 'librechat-data-provider';
|
|
||||||
import { useMediaQuery } from '@librechat/client';
|
import { useMediaQuery } from '@librechat/client';
|
||||||
|
import type t from 'librechat-data-provider';
|
||||||
|
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||||
import { SmartLoader } from './SmartLoader';
|
import { SmartLoader } from './SmartLoader';
|
||||||
import { useLocalize } from '~/hooks/';
|
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,14 +36,17 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
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) => {
|
const getCategoryDisplayName = (category: t.TCategory) => {
|
||||||
// Special cases for system categories
|
// Special cases for system categories
|
||||||
if (category.value === 'promoted') {
|
if (category.value === 'promoted') {
|
||||||
return localize('com_agents_top_picks');
|
return localize('com_agents_top_picks');
|
||||||
}
|
}
|
||||||
if (category.value === 'all') {
|
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
|
// Use database label or fallback to capitalized value
|
||||||
return category.label || category.value.charAt(0).toUpperCase() + category.value.slice(1);
|
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-selected={activeTab === category.value}
|
||||||
aria-controls={`tabpanel-${category.value}`}
|
aria-controls={`tabpanel-${category.value}`}
|
||||||
tabIndex={activeTab === category.value ? 0 : -1}
|
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)}
|
{getCategoryDisplayName(category)}
|
||||||
{/* Underline for active tab */}
|
{/* Underline for active tab */}
|
||||||
|
|
|
@ -7,8 +7,8 @@ import { TooltipAnchor, Button, NewChatIcon, useMediaQuery } from '@librechat/cl
|
||||||
import { PermissionTypes, Permissions, QueryKeys, Constants } from 'librechat-data-provider';
|
import { PermissionTypes, Permissions, QueryKeys, Constants } from 'librechat-data-provider';
|
||||||
import type t from 'librechat-data-provider';
|
import type t from 'librechat-data-provider';
|
||||||
import type { ContextType } from '~/common';
|
import type { ContextType } from '~/common';
|
||||||
|
import { useDocumentTitle, useHasAccess, useLocalize, TranslationKeys } from '~/hooks';
|
||||||
import { useGetEndpointsQuery, useGetAgentCategoriesQuery } from '~/data-provider';
|
import { useGetEndpointsQuery, useGetAgentCategoriesQuery } from '~/data-provider';
|
||||||
import { useDocumentTitle, useHasAccess, useLocalize } from '~/hooks';
|
|
||||||
import MarketplaceAdminSettings from './MarketplaceAdminSettings';
|
import MarketplaceAdminSettings from './MarketplaceAdminSettings';
|
||||||
import { SidePanelProvider, useChatContext } from '~/Providers';
|
import { SidePanelProvider, useChatContext } from '~/Providers';
|
||||||
import { MarketplaceProvider } from './MarketplaceContext';
|
import { MarketplaceProvider } from './MarketplaceContext';
|
||||||
|
@ -381,8 +381,8 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
||||||
}
|
}
|
||||||
if (displayCategory === 'all') {
|
if (displayCategory === 'all') {
|
||||||
return {
|
return {
|
||||||
name: 'All Agents',
|
name: localize('com_agents_all'),
|
||||||
description: 'Browse all shared agents across all categories',
|
description: localize('com_agents_all_description'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,8 +392,12 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
||||||
);
|
);
|
||||||
if (categoryData) {
|
if (categoryData) {
|
||||||
return {
|
return {
|
||||||
name: categoryData.label,
|
name: categoryData.label?.startsWith('com_')
|
||||||
description: categoryData.description || '',
|
? 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') {
|
if (nextCategory === 'all') {
|
||||||
return {
|
return {
|
||||||
name: 'All Agents',
|
name: localize('com_agents_all'),
|
||||||
description: 'Browse all shared agents across all categories',
|
description: localize('com_agents_all_description'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -466,8 +470,16 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
||||||
);
|
);
|
||||||
if (categoryData) {
|
if (categoryData) {
|
||||||
return {
|
return {
|
||||||
name: categoryData.label,
|
name: categoryData.label?.startsWith('com_')
|
||||||
description: categoryData.description || '',
|
? localize(categoryData.label as TranslationKeys)
|
||||||
|
: categoryData.label,
|
||||||
|
description: categoryData.description?.startsWith('com_')
|
||||||
|
? localize(
|
||||||
|
categoryData.description as Parameters<
|
||||||
|
typeof localize
|
||||||
|
>[0],
|
||||||
|
)
|
||||||
|
: categoryData.description || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,7 @@ const mockLocalize = jest.fn((key: string, options?: any) => {
|
||||||
com_agents_search_empty_heading: 'No search results',
|
com_agents_search_empty_heading: 'No search results',
|
||||||
com_agents_created_by: 'by',
|
com_agents_created_by: 'by',
|
||||||
com_agents_top_picks: 'Top Picks',
|
com_agents_top_picks: 'Top Picks',
|
||||||
|
com_agents_all_category: 'All',
|
||||||
// ErrorDisplay translations
|
// ErrorDisplay translations
|
||||||
com_agents_error_suggestion_generic: 'Try refreshing the page or check your network connection',
|
com_agents_error_suggestion_generic: 'Try refreshing the page or check your network connection',
|
||||||
com_agents_error_network_title: 'Network Error',
|
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
|
// Test arrow key navigation
|
||||||
fireEvent.keyDown(promotedTab, { key: 'ArrowRight' });
|
fireEvent.keyDown(promotedTab, { key: 'ArrowRight' });
|
||||||
|
@ -226,8 +227,8 @@ describe('Accessibility Improvements', () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const promotedTab = screen.getByRole('tab', { name: /Top Picks tab/ });
|
const promotedTab = screen.getByRole('tab', { name: /Top Picks category/ });
|
||||||
const allTab = screen.getByRole('tab', { name: /All tab/ });
|
const allTab = screen.getByRole('tab', { name: /All category/ });
|
||||||
|
|
||||||
// Active tab should be focusable
|
// Active tab should be focusable
|
||||||
expect(promotedTab).toHaveAttribute('tabIndex', '0');
|
expect(promotedTab).toHaveAttribute('tabIndex', '0');
|
||||||
|
|
|
@ -8,10 +8,42 @@ import type t from 'librechat-data-provider';
|
||||||
jest.mock('~/hooks/useLocalize', () => () => (key: string) => {
|
jest.mock('~/hooks/useLocalize', () => () => (key: string) => {
|
||||||
const mockTranslations: Record<string, string> = {
|
const mockTranslations: Record<string, string> = {
|
||||||
com_agents_created_by: 'Created by',
|
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;
|
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', () => {
|
describe('AgentCard', () => {
|
||||||
const mockAgent: t.Agent = {
|
const mockAgent: t.Agent = {
|
||||||
id: '1',
|
id: '1',
|
||||||
|
@ -200,6 +232,49 @@ describe('AgentCard', () => {
|
||||||
|
|
||||||
const card = screen.getByRole('button');
|
const card = screen.getByRole('button');
|
||||||
expect(card).toHaveAttribute('tabIndex', '0');
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -9,7 +9,8 @@ import type t from 'librechat-data-provider';
|
||||||
jest.mock('~/hooks/useLocalize', () => () => (key: string) => {
|
jest.mock('~/hooks/useLocalize', () => () => (key: string) => {
|
||||||
const mockTranslations: Record<string, string> = {
|
const mockTranslations: Record<string, string> = {
|
||||||
com_agents_top_picks: 'Top Picks',
|
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_ui_no_categories: 'No categories available',
|
||||||
com_agents_category_tabs_label: 'Agent Categories',
|
com_agents_category_tabs_label: 'Agent Categories',
|
||||||
com_ui_agent_category_general: 'General',
|
com_ui_agent_category_general: 'General',
|
||||||
|
|
|
@ -89,7 +89,7 @@ const BookmarkEditDialog = ({
|
||||||
<OGDialog open={open} onOpenChange={setOpen} triggerRef={triggerRef}>
|
<OGDialog open={open} onOpenChange={setOpen} triggerRef={triggerRef}>
|
||||||
{children}
|
{children}
|
||||||
<OGDialogTemplate
|
<OGDialogTemplate
|
||||||
title="Bookmark"
|
title={bookmark ? localize('com_ui_bookmarks_edit') : localize('com_ui_bookmarks_new')}
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
className="w-11/12 md:max-w-2xl"
|
className="w-11/12 md:max-w-2xl"
|
||||||
main={
|
main={
|
||||||
|
|
|
@ -38,6 +38,8 @@ const BookmarkForm = ({
|
||||||
control,
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<TConversationTagRequest>({
|
} = useForm<TConversationTagRequest>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
reValidateMode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
tag: bookmark?.tag ?? '',
|
tag: bookmark?.tag ?? '',
|
||||||
description: bookmark?.description ?? '',
|
description: bookmark?.description ?? '',
|
||||||
|
@ -98,23 +100,30 @@ const BookmarkForm = ({
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="bookmark-tag"
|
id="bookmark-tag"
|
||||||
aria-label="Bookmark"
|
aria-label={
|
||||||
|
bookmark ? localize('com_ui_bookmarks_edit') : localize('com_ui_bookmarks_new')
|
||||||
|
}
|
||||||
{...register('tag', {
|
{...register('tag', {
|
||||||
required: 'tag is required',
|
required: localize('com_ui_field_required'),
|
||||||
maxLength: {
|
maxLength: {
|
||||||
value: 128,
|
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) => {
|
validate: (value) => {
|
||||||
return (
|
return (
|
||||||
value === bookmark?.tag ||
|
value === bookmark?.tag ||
|
||||||
bookmarks.every((bookmark) => bookmark.tag !== value) ||
|
bookmarks.every((bookmark) => bookmark.tag !== value) ||
|
||||||
'tag must be unique'
|
localize('com_ui_bookmarks_tag_exists')
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
aria-invalid={!!errors.tag}
|
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>}
|
{errors.tag && <span className="text-sm text-red-500">{errors.tag.message}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -127,7 +136,10 @@ const BookmarkForm = ({
|
||||||
{...register('description', {
|
{...register('description', {
|
||||||
maxLength: {
|
maxLength: {
|
||||||
value: 1048,
|
value: 1048,
|
||||||
message: 'Maximum 1048 characters',
|
message: localize('com_ui_field_max_length', {
|
||||||
|
field: localize('com_ui_bookmarks_description'),
|
||||||
|
length: 1048,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
id="bookmark-description"
|
id="bookmark-description"
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
export default function EmptyPromptPreview() {
|
export default function EmptyPromptPreview() {
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full content-center text-center font-bold dark:text-gray-200">
|
<div className="h-full w-full content-center text-center font-bold text-text-secondary">
|
||||||
Select or Create a Prompt
|
{localize('com_ui_select_or_create_prompt')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { ControlCombobox } from '@librechat/client';
|
import { ControlCombobox } from '@librechat/client';
|
||||||
import {
|
import {
|
||||||
useWatch,
|
useWatch,
|
||||||
|
@ -9,7 +8,7 @@ import {
|
||||||
useFormContext,
|
useFormContext,
|
||||||
ControllerRenderProps,
|
ControllerRenderProps,
|
||||||
} from 'react-hook-form';
|
} from 'react-hook-form';
|
||||||
import { useAgentCategories } from '~/hooks/Agents';
|
import { TranslationKeys, useLocalize, useAgentCategories } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,22 +34,25 @@ const useCategorySync = (agent_id: string | null) => {
|
||||||
* A component for selecting agent categories with form validation
|
* A component for selecting agent categories with form validation
|
||||||
*/
|
*/
|
||||||
const AgentCategorySelector: React.FC<{ className?: string }> = ({ className }) => {
|
const AgentCategorySelector: React.FC<{ className?: string }> = ({ className }) => {
|
||||||
const { t } = useTranslation();
|
const localize = useLocalize();
|
||||||
const formContext = useFormContext();
|
const formContext = useFormContext();
|
||||||
const { categories } = useAgentCategories();
|
const { categories } = useAgentCategories();
|
||||||
|
|
||||||
// Always call useWatch
|
|
||||||
const agent_id = useWatch({
|
const agent_id = useWatch({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
control: formContext.control,
|
control: formContext.control,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use custom hook for category sync
|
|
||||||
const { syncCategory } = useCategorySync(agent_id);
|
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) => ({
|
const comboboxItems = categories.map((category) => ({
|
||||||
label: category.label,
|
label: getCategoryLabel(category),
|
||||||
value: category.value,
|
value: category.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -59,8 +61,8 @@ const AgentCategorySelector: React.FC<{ className?: string }> = ({ className })
|
||||||
return categoryItem?.label || comboboxItems.find((c) => c.value === 'general')?.label;
|
return categoryItem?.label || comboboxItems.find((c) => c.value === 'general')?.label;
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchPlaceholder = t('com_ui_search_agent_category', 'Search categories...');
|
const searchPlaceholder = localize('com_ui_search_agent_category');
|
||||||
const ariaLabel = t('com_ui_agent_category_selector_aria', "Agent's category selector");
|
const ariaLabel = localize('com_ui_agent_category_selector_aria');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
|
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';
|
import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
|
|
||||||
interface UseGetAgentsConfigOptions {
|
interface UseGetAgentsConfigOptions {
|
||||||
|
@ -20,11 +20,12 @@ export default function useGetAgentsConfig(options?: UseGetAgentsConfigOptions):
|
||||||
const endpointsConfig = providedConfig || queriedConfig;
|
const endpointsConfig = providedConfig || queriedConfig;
|
||||||
|
|
||||||
const agentsConfig = useMemo<TAgentsEndpoint | null>(() => {
|
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;
|
if (!config) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(config as TConfig),
|
...config,
|
||||||
capabilities: Array.isArray(config.capabilities)
|
capabilities: Array.isArray(config.capabilities)
|
||||||
? config.capabilities.map((cap) => cap as unknown as AgentCapabilities)
|
? config.capabilities.map((cap) => cap as unknown as AgentCapabilities)
|
||||||
: ([] as AgentCapabilities[]),
|
: ([] as AgentCapabilities[]),
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { QueryKeys } from 'librechat-data-provider';
|
||||||
import type { ConversationListResponse } from 'librechat-data-provider';
|
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||||
import type { InfiniteData } from '@tanstack/react-query';
|
import type { InfiniteData } from '@tanstack/react-query';
|
||||||
import type t from 'librechat-data-provider';
|
import type t from 'librechat-data-provider';
|
||||||
import { updateConvoFieldsInfinite } from '~/utils/convos';
|
|
||||||
|
|
||||||
const useUpdateTagsInConvo = () => {
|
const useUpdateTagsInConvo = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
@ -53,30 +52,31 @@ const useUpdateTagsInConvo = () => {
|
||||||
QueryKeys.allConversations,
|
QueryKeys.allConversations,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const conversationIdsWithTag = [] as string[];
|
if (data) {
|
||||||
|
const newData = JSON.parse(JSON.stringify(data)) as InfiniteData<ConversationListResponse>;
|
||||||
// update tag to newTag in all conversations
|
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
|
||||||
const newData = JSON.parse(JSON.stringify(data)) as InfiniteData<ConversationListResponse>;
|
const page = newData.pages[pageIndex];
|
||||||
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
|
page.conversations = page.conversations.map((conversation) => {
|
||||||
const page = newData.pages[pageIndex];
|
if (
|
||||||
page.conversations = page.conversations.map((conversation) => {
|
conversation.conversationId &&
|
||||||
if (
|
'tags' in conversation &&
|
||||||
conversation.conversationId &&
|
Array.isArray((conversation as { tags?: string[] }).tags) &&
|
||||||
'tags' in conversation &&
|
(conversation as { tags?: string[] }).tags?.includes(tag)
|
||||||
Array.isArray((conversation as { tags?: string[] }).tags) &&
|
) {
|
||||||
(conversation as { tags?: string[] }).tags?.includes(tag)
|
(conversation as { tags: string[] }).tags = (
|
||||||
) {
|
conversation as { tags: string[] }
|
||||||
(conversation as { tags: string[] }).tags = (conversation as { tags: string[] }).tags.map(
|
).tags.map((t: string) => (t === tag ? newTag : t));
|
||||||
(t: string) => (t === tag ? newTag : t),
|
}
|
||||||
);
|
return conversation;
|
||||||
}
|
});
|
||||||
return conversation;
|
}
|
||||||
});
|
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
|
||||||
|
[QueryKeys.allConversations],
|
||||||
|
newData,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
|
|
||||||
[QueryKeys.allConversations],
|
const conversationIdsWithTag = [] as string[];
|
||||||
newData,
|
|
||||||
);
|
|
||||||
|
|
||||||
// update the tag to newTag from the cache of each conversation
|
// update the tag to newTag from the cache of each conversation
|
||||||
for (let i = 0; i < conversationIdsWithTag.length; i++) {
|
for (let i = 0; i < conversationIdsWithTag.length; i++) {
|
||||||
|
|
|
@ -6,8 +6,24 @@
|
||||||
"com_a11y_start": "The AI has started their reply.",
|
"com_a11y_start": "The AI has started their reply.",
|
||||||
"com_agents_agent_card_label": "{{name}} agent. {{description}}",
|
"com_agents_agent_card_label": "{{name}} agent. {{description}}",
|
||||||
"com_agents_all": "All Agents",
|
"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_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_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_tab_label": "{{category}} category, {{position}} of {{total}}",
|
||||||
"com_agents_category_tabs_label": "Agent Categories",
|
"com_agents_category_tabs_label": "Agent Categories",
|
||||||
"com_agents_clear_search": "Clear search",
|
"com_agents_clear_search": "Clear search",
|
||||||
|
@ -724,6 +740,7 @@
|
||||||
"com_ui_bookmarks_edit": "Edit Bookmark",
|
"com_ui_bookmarks_edit": "Edit Bookmark",
|
||||||
"com_ui_bookmarks_filter": "Filter bookmarks...",
|
"com_ui_bookmarks_filter": "Filter bookmarks...",
|
||||||
"com_ui_bookmarks_new": "New Bookmark",
|
"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_title": "Title",
|
||||||
"com_ui_bookmarks_update_error": "There was an error updating the bookmark",
|
"com_ui_bookmarks_update_error": "There was an error updating the bookmark",
|
||||||
"com_ui_bookmarks_update_success": "Bookmark updated successfully",
|
"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_not_matched": "Didn't match my request",
|
||||||
"com_ui_feedback_tag_other": "Other issue",
|
"com_ui_feedback_tag_other": "Other issue",
|
||||||
"com_ui_feedback_tag_unjustified_refusal": "Refused without reason",
|
"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_field_required": "This field is required",
|
||||||
"com_ui_file_size": "File Size",
|
"com_ui_file_size": "File Size",
|
||||||
"com_ui_files": "Files",
|
"com_ui_files": "Files",
|
||||||
|
@ -1118,6 +1136,7 @@
|
||||||
"com_ui_select_search_plugin": "Search plugin by name",
|
"com_ui_select_search_plugin": "Search plugin by name",
|
||||||
"com_ui_select_search_provider": "Search provider by name",
|
"com_ui_select_search_provider": "Search provider by name",
|
||||||
"com_ui_select_search_region": "Search region by name",
|
"com_ui_select_search_region": "Search region by name",
|
||||||
|
"com_ui_select_or_create_prompt": "Select or Create a Prompt",
|
||||||
"com_ui_set": "Set",
|
"com_ui_set": "Set",
|
||||||
"com_ui_share": "Share",
|
"com_ui_share": "Share",
|
||||||
"com_ui_share_create_message": "Your name and any messages you add after sharing stay private.",
|
"com_ui_share_create_message": "Your name and any messages you add after sharing stay private.",
|
||||||
|
|
|
@ -164,11 +164,12 @@ export type TCategory = {
|
||||||
id?: string;
|
id?: string;
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
custom?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TMarketplaceCategory = TCategory & {
|
export type TMarketplaceCategory = TCategory & {
|
||||||
count: number;
|
count: number;
|
||||||
description?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TError = {
|
export type TError = {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Model, Types, DeleteResult } from 'mongoose';
|
import type { Model, Types } from 'mongoose';
|
||||||
import type { IAgentCategory, AgentCategory } from '../types/agentCategory';
|
import type { IAgentCategory } from '~/types';
|
||||||
|
|
||||||
export function createAgentCategoryMethods(mongoose: typeof import('mongoose')) {
|
export function createAgentCategoryMethods(mongoose: typeof import('mongoose')) {
|
||||||
/**
|
/**
|
||||||
|
@ -52,8 +52,9 @@ export function createAgentCategoryMethods(mongoose: typeof import('mongoose'))
|
||||||
label?: string;
|
label?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
|
custom?: boolean;
|
||||||
}>,
|
}>,
|
||||||
): Promise<any> {
|
): Promise<import('mongoose').mongo.BulkWriteResult> {
|
||||||
const AgentCategory = mongoose.models.AgentCategory as Model<IAgentCategory>;
|
const AgentCategory = mongoose.models.AgentCategory as Model<IAgentCategory>;
|
||||||
|
|
||||||
const operations = categories.map((category, index) => ({
|
const operations = categories.map((category, index) => ({
|
||||||
|
@ -66,6 +67,7 @@ export function createAgentCategoryMethods(mongoose: typeof import('mongoose'))
|
||||||
description: category.description || '',
|
description: category.description || '',
|
||||||
order: category.order || index,
|
order: category.order || index,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
custom: category.custom || false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
upsert: true,
|
upsert: true,
|
||||||
|
@ -145,63 +147,104 @@ export function createAgentCategoryMethods(mongoose: typeof import('mongoose'))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure default categories exist, seed them if none are present
|
* Ensure default categories exist and update them if they don't have localization keys
|
||||||
* @returns Promise<boolean> - true if categories were seeded, false if they already existed
|
* @returns Promise<boolean> - true if categories were created/updated, false if no changes
|
||||||
*/
|
*/
|
||||||
async function ensureDefaultCategories(): Promise<boolean> {
|
async function ensureDefaultCategories(): Promise<boolean> {
|
||||||
const existingCategories = await getAllCategories();
|
const AgentCategory = mongoose.models.AgentCategory as Model<IAgentCategory>;
|
||||||
|
|
||||||
if (existingCategories.length > 0) {
|
|
||||||
return false; // Categories already exist
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultCategories = [
|
const defaultCategories = [
|
||||||
{
|
{
|
||||||
value: 'general',
|
value: 'general',
|
||||||
label: 'General',
|
label: 'com_agents_category_general',
|
||||||
description: 'General purpose agents for common tasks and inquiries',
|
description: 'com_agents_category_general_description',
|
||||||
order: 0,
|
order: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'hr',
|
value: 'hr',
|
||||||
label: 'Human Resources',
|
label: 'com_agents_category_hr',
|
||||||
description: 'Agents specialized in HR processes, policies, and employee support',
|
description: 'com_agents_category_hr_description',
|
||||||
order: 1,
|
order: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'rd',
|
value: 'rd',
|
||||||
label: 'Research & Development',
|
label: 'com_agents_category_rd',
|
||||||
description: 'Agents focused on R&D processes, innovation, and technical research',
|
description: 'com_agents_category_rd_description',
|
||||||
order: 2,
|
order: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'finance',
|
value: 'finance',
|
||||||
label: 'Finance',
|
label: 'com_agents_category_finance',
|
||||||
description: 'Agents specialized in financial analysis, budgeting, and accounting',
|
description: 'com_agents_category_finance_description',
|
||||||
order: 3,
|
order: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'it',
|
value: 'it',
|
||||||
label: 'IT',
|
label: 'com_agents_category_it',
|
||||||
description: 'Agents for IT support, technical troubleshooting, and system administration',
|
description: 'com_agents_category_it_description',
|
||||||
order: 4,
|
order: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'sales',
|
value: 'sales',
|
||||||
label: 'Sales',
|
label: 'com_agents_category_sales',
|
||||||
description: 'Agents focused on sales processes, customer relations.',
|
description: 'com_agents_category_sales_description',
|
||||||
order: 5,
|
order: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'aftersales',
|
value: 'aftersales',
|
||||||
label: 'After Sales',
|
label: 'com_agents_category_aftersales',
|
||||||
description: 'Agents specialized in post-sale support, maintenance, and customer service',
|
description: 'com_agents_category_aftersales_description',
|
||||||
order: 6,
|
order: 6,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await seedCategories(defaultCategories);
|
const existingCategories = await getAllCategories();
|
||||||
return true; // Categories were seeded
|
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 {
|
return {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Schema, Document } from 'mongoose';
|
import { Schema } from 'mongoose';
|
||||||
import type { IAgentCategory } from '~/types';
|
import type { IAgentCategory } from '~/types';
|
||||||
|
|
||||||
const agentCategorySchema = new Schema<IAgentCategory>(
|
const agentCategorySchema = new Schema<IAgentCategory>(
|
||||||
|
@ -31,6 +31,10 @@ const agentCategorySchema = new Schema<IAgentCategory>(
|
||||||
default: true,
|
default: true,
|
||||||
index: true,
|
index: true,
|
||||||
},
|
},
|
||||||
|
custom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
|
|
@ -11,6 +11,8 @@ export type AgentCategory = {
|
||||||
order: number;
|
order: number;
|
||||||
/** Whether the category is active and should be displayed */
|
/** Whether the category is active and should be displayed */
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
/** Whether this is a custom user-created category */
|
||||||
|
custom?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IAgentCategory = AgentCategory &
|
export type IAgentCategory = AgentCategory &
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue