mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00
feat: Add category and support contact fields to Agent schema and UI components
This commit is contained in:
parent
e86842fd19
commit
c43a52b4c9
15 changed files with 581 additions and 11 deletions
|
@ -12,6 +12,61 @@ const {
|
||||||
} = require('./Project');
|
} = require('./Project');
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
|
|
||||||
|
// Category values - must match the frontend values in client/src/constants/agentCategories.ts
|
||||||
|
const CATEGORY_VALUES = {
|
||||||
|
GENERAL: 'general',
|
||||||
|
HR: 'hr',
|
||||||
|
RD: 'rd',
|
||||||
|
FINANCE: 'finance',
|
||||||
|
IT: 'it',
|
||||||
|
SALES: 'sales',
|
||||||
|
AFTERSALES: 'aftersales',
|
||||||
|
};
|
||||||
|
|
||||||
|
const VALID_CATEGORIES = Object.values(CATEGORY_VALUES);
|
||||||
|
|
||||||
|
// Add category field to the Agent schema if it doesn't already exist
|
||||||
|
if (!agentSchema.paths.category) {
|
||||||
|
agentSchema.add({
|
||||||
|
category: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
enum: {
|
||||||
|
values: VALID_CATEGORIES,
|
||||||
|
message:
|
||||||
|
'"{VALUE}" is not a supported agent category. Valid categories are: ' +
|
||||||
|
VALID_CATEGORIES.join(', ') +
|
||||||
|
'.',
|
||||||
|
},
|
||||||
|
index: true,
|
||||||
|
default: CATEGORY_VALUES.GENERAL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add support_contact field to the Agent schema if it doesn't already exist
|
||||||
|
if (!agentSchema.paths.support_contact) {
|
||||||
|
agentSchema.add({
|
||||||
|
support_contact: {
|
||||||
|
type: Object,
|
||||||
|
default: {},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
minlength: [3, 'Support contact name must be at least 3 characters.'],
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
match: [
|
||||||
|
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
|
||||||
|
'Please enter a valid email address.',
|
||||||
|
],
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const Agent = mongoose.model('agent', agentSchema);
|
const Agent = mongoose.model('agent', agentSchema);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,7 +76,12 @@ const Agent = mongoose.model('agent', agentSchema);
|
||||||
* @throws {Error} If the agent creation fails.
|
* @throws {Error} If the agent creation fails.
|
||||||
*/
|
*/
|
||||||
const createAgent = async (agentData) => {
|
const createAgent = async (agentData) => {
|
||||||
return (await Agent.create(agentData)).toObject();
|
// Ensure the agent has a category (default to 'general' if none provided)
|
||||||
|
const dataWithCategory = {
|
||||||
|
...agentData,
|
||||||
|
category: agentData.category || 'general',
|
||||||
|
};
|
||||||
|
return (await Agent.create(dataWithCategory)).toObject();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -280,6 +340,7 @@ const getListAgents = async (searchParameter) => {
|
||||||
projectIds: 1,
|
projectIds: 1,
|
||||||
description: 1,
|
description: 1,
|
||||||
isCollaborative: 1,
|
isCollaborative: 1,
|
||||||
|
category: 1,
|
||||||
}).lean()
|
}).lean()
|
||||||
).map((agent) => {
|
).map((agent) => {
|
||||||
if (agent.author?.toString() !== author) {
|
if (agent.author?.toString() !== author) {
|
||||||
|
|
|
@ -16,6 +16,11 @@ export type TAgentCapabilities = {
|
||||||
[AgentCapabilities.hide_sequential_outputs]?: boolean;
|
[AgentCapabilities.hide_sequential_outputs]?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SupportContact = {
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AgentForm = {
|
export type AgentForm = {
|
||||||
agent?: TAgentOption;
|
agent?: TAgentOption;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -29,4 +34,5 @@ export type AgentForm = {
|
||||||
agent_ids?: string[];
|
agent_ids?: string[];
|
||||||
[AgentCapabilities.artifacts]?: ArtifactModes | string;
|
[AgentCapabilities.artifacts]?: ArtifactModes | string;
|
||||||
recursion_limit?: number;
|
recursion_limit?: number;
|
||||||
|
support_contact?: SupportContact;
|
||||||
} & TAgentCapabilities;
|
} & TAgentCapabilities;
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
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;
|
|
@ -0,0 +1,94 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
useFormContext,
|
||||||
|
Controller,
|
||||||
|
useWatch,
|
||||||
|
ControllerRenderProps,
|
||||||
|
FieldValues,
|
||||||
|
FieldPath,
|
||||||
|
} from 'react-hook-form';
|
||||||
|
import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||||
|
import { useAgentCategories } from '~/hooks/Agents';
|
||||||
|
import { OptionWithIcon } from '~/common/types';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component for selecting agent categories with form validation
|
||||||
|
*/
|
||||||
|
const AgentCategorySelector: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const formContext = useFormContext();
|
||||||
|
const { categories } = useAgentCategories();
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const handleCategorySync = (
|
||||||
|
field: ControllerRenderProps<FieldValues, FieldPath<FieldValues>>,
|
||||||
|
agent_id: string | null,
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (agent_id === '') {
|
||||||
|
// Form has been reset, ensure field is set to default
|
||||||
|
if (field.value !== 'general') {
|
||||||
|
field.onChange('general');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If value is empty or undefined, default to 'general'
|
||||||
|
if (!field.value) {
|
||||||
|
field.onChange('general');
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [field]); // Removing agent_id from dependencies as suggested by ESLint
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryDisplayValue = (value: string) => {
|
||||||
|
const categoryItem = comboboxItems.find((c) => c.value === value);
|
||||||
|
return categoryItem?.label || comboboxItems.find((c) => c.value === 'general')?.label;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Always call useWatch
|
||||||
|
const agent_id = useWatch({
|
||||||
|
name: 'id',
|
||||||
|
control: formContext.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform categories to the format expected by ControlCombobox
|
||||||
|
const comboboxItems = categories.map((category) => ({
|
||||||
|
label: category.label,
|
||||||
|
value: category.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const searchPlaceholder = t('com_ui_search_agent_category', 'Search categories...');
|
||||||
|
const ariaLabel = t('com_ui_agent_category_selector_aria', "Agent's category selector");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name="category"
|
||||||
|
control={formContext.control}
|
||||||
|
defaultValue="general"
|
||||||
|
render={({ field }) => {
|
||||||
|
handleCategorySync(field, agent_id);
|
||||||
|
|
||||||
|
const displayValue = getCategoryDisplayValue(field.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ControlCombobox
|
||||||
|
selectedValue={field.value || 'general'}
|
||||||
|
displayValue={displayValue}
|
||||||
|
searchPlaceholder={searchPlaceholder}
|
||||||
|
setValue={(value) => {
|
||||||
|
field.onChange(value || 'general');
|
||||||
|
}}
|
||||||
|
items={comboboxItems}
|
||||||
|
className=""
|
||||||
|
ariaLabel={ariaLabel}
|
||||||
|
isCollapsed={false}
|
||||||
|
showCarat={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgentCategorySelector;
|
|
@ -18,6 +18,7 @@ import FileSearch from './FileSearch';
|
||||||
import Artifacts from './Artifacts';
|
import Artifacts from './Artifacts';
|
||||||
import AgentTool from './AgentTool';
|
import AgentTool from './AgentTool';
|
||||||
import CodeForm from './Code/Form';
|
import CodeForm from './Code/Form';
|
||||||
|
import AgentCategorySelector from './AgentCategorySelector';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
||||||
|
@ -228,6 +229,14 @@ export default function AgentConfig({
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Category */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className={labelClass} htmlFor="category-selector">
|
||||||
|
{localize('com_ui_category')} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<AgentCategorySelector className="w-full" />
|
||||||
|
</div>
|
||||||
{/* Instructions */}
|
{/* Instructions */}
|
||||||
<Instructions />
|
<Instructions />
|
||||||
{/* Model and Provider */}
|
{/* Model and Provider */}
|
||||||
|
@ -329,6 +338,88 @@ export default function AgentConfig({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Support Contact (Optional) */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="mb-1.5 flex items-center gap-2">
|
||||||
|
<span>
|
||||||
|
<label className="text-token-text-primary block font-medium">
|
||||||
|
{localize('com_ui_support_contact')}
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Support Contact Name */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="flex items-center justify-between mb-1" htmlFor="support-contact-name">
|
||||||
|
<span className="text-sm">{localize('com_ui_support_contact_name')}</span>
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
name="support_contact.name"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
minLength: {
|
||||||
|
value: 3,
|
||||||
|
message: localize('com_ui_support_contact_name_min_length', { minLength: 3 }),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ''}
|
||||||
|
className={cn(inputClass, error ? 'border-2 border-red-500' : '')}
|
||||||
|
id="support-contact-name"
|
||||||
|
type="text"
|
||||||
|
placeholder={localize('com_ui_support_contact_name_placeholder')}
|
||||||
|
aria-label="Support contact name"
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
|
||||||
|
{error.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Support Contact Email */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="flex items-center justify-between mb-1" htmlFor="support-contact-email">
|
||||||
|
<span className="text-sm">{localize('com_ui_support_contact_email')}</span>
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
name="support_contact.email"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
pattern: {
|
||||||
|
value: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
|
||||||
|
message: localize('com_ui_support_contact_email_invalid'),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ''}
|
||||||
|
className={cn(inputClass, error ? 'border-2 border-red-500' : '')}
|
||||||
|
id="support-contact-email"
|
||||||
|
type="email"
|
||||||
|
placeholder={localize('com_ui_support_contact_email_placeholder')}
|
||||||
|
aria-label="Support contact email"
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
|
||||||
|
{error.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToolSelectDialog
|
<ToolSelectDialog
|
||||||
isOpen={showToolDialog}
|
isOpen={showToolDialog}
|
||||||
|
|
|
@ -140,6 +140,8 @@ export default function AgentPanel({
|
||||||
end_after_tools,
|
end_after_tools,
|
||||||
hide_sequential_outputs,
|
hide_sequential_outputs,
|
||||||
recursion_limit,
|
recursion_limit,
|
||||||
|
category,
|
||||||
|
support_contact,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const model = _model ?? '';
|
const model = _model ?? '';
|
||||||
|
@ -162,6 +164,8 @@ export default function AgentPanel({
|
||||||
end_after_tools,
|
end_after_tools,
|
||||||
hide_sequential_outputs,
|
hide_sequential_outputs,
|
||||||
recursion_limit,
|
recursion_limit,
|
||||||
|
category,
|
||||||
|
support_contact,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -187,6 +191,8 @@ export default function AgentPanel({
|
||||||
end_after_tools,
|
end_after_tools,
|
||||||
hide_sequential_outputs,
|
hide_sequential_outputs,
|
||||||
recursion_limit,
|
recursion_limit,
|
||||||
|
category,
|
||||||
|
support_contact,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[agent_id, create, update, showToast, localize],
|
[agent_id, create, update, showToast, localize],
|
||||||
|
@ -220,7 +226,7 @@ export default function AgentPanel({
|
||||||
className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden"
|
className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden"
|
||||||
aria-label="Agent configuration form"
|
aria-label="Agent configuration form"
|
||||||
>
|
>
|
||||||
<div className="mt-2 flex w-full flex-wrap gap-2">
|
<div className="mx-1 mt-2 flex w-full flex-wrap gap-2">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<AgentSelect
|
<AgentSelect
|
||||||
createMutation={create}
|
createMutation={create}
|
||||||
|
|
|
@ -73,6 +73,10 @@ export default function AgentSelect({
|
||||||
agent: update,
|
agent: update,
|
||||||
model: update.model,
|
model: update.model,
|
||||||
tools: agentTools,
|
tools: agentTools,
|
||||||
|
// Ensure the category is properly set for the form
|
||||||
|
category: fullAgent.category || 'general',
|
||||||
|
// Make sure support_contact is properly loaded
|
||||||
|
support_contact: fullAgent.support_contact,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(fullAgent).forEach(([name, value]) => {
|
Object.entries(fullAgent).forEach(([name, value]) => {
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
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">Icon</span>,
|
||||||
|
className: 'w-full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'hr',
|
||||||
|
label: 'HR',
|
||||||
|
icon: <span data-testid="icon-hr">Icon</span>,
|
||||||
|
className: 'w-full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'rd',
|
||||||
|
label: 'R&D',
|
||||||
|
icon: <span data-testid="icon-rd">Icon</span>,
|
||||||
|
className: 'w-full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'finance',
|
||||||
|
label: 'Finance',
|
||||||
|
icon: <span data-testid="icon-finance">Icon</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');
|
||||||
|
});
|
||||||
|
});
|
48
client/src/constants/agentCategories.ts
Normal file
48
client/src/constants/agentCategories.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { TranslationKeys } from '~/hooks/useLocalize';
|
||||||
|
|
||||||
|
export interface AgentCategory {
|
||||||
|
label: TranslationKeys;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category values - must match the backend values in Agent.js
|
||||||
|
export const CATEGORY_VALUES = {
|
||||||
|
GENERAL: 'general',
|
||||||
|
HR: 'hr',
|
||||||
|
RD: 'rd',
|
||||||
|
FINANCE: 'finance',
|
||||||
|
IT: 'it',
|
||||||
|
SALES: 'sales',
|
||||||
|
AFTERSALES: 'aftersales',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Type for category values to ensure type safety
|
||||||
|
export type AgentCategoryValue = (typeof CATEGORY_VALUES)[keyof typeof CATEGORY_VALUES];
|
||||||
|
|
||||||
|
// Display labels for each category
|
||||||
|
const CATEGORY_LABELS = {
|
||||||
|
[CATEGORY_VALUES.GENERAL]: 'com_ui_agent_category_general',
|
||||||
|
[CATEGORY_VALUES.HR]: 'com_ui_agent_category_hr',
|
||||||
|
[CATEGORY_VALUES.RD]: 'com_ui_agent_category_rd', // R&D
|
||||||
|
[CATEGORY_VALUES.FINANCE]: 'com_ui_agent_category_finance',
|
||||||
|
[CATEGORY_VALUES.IT]: 'com_ui_agent_category_it',
|
||||||
|
[CATEGORY_VALUES.SALES]: 'com_ui_agent_category_sales',
|
||||||
|
[CATEGORY_VALUES.AFTERSALES]: 'com_ui_agent_category_aftersales',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// The categories array used in the UI
|
||||||
|
export const AGENT_CATEGORIES: AgentCategory[] = [
|
||||||
|
{ value: CATEGORY_VALUES.GENERAL, label: CATEGORY_LABELS[CATEGORY_VALUES.GENERAL] },
|
||||||
|
{ value: CATEGORY_VALUES.HR, label: CATEGORY_LABELS[CATEGORY_VALUES.HR] },
|
||||||
|
{ value: CATEGORY_VALUES.RD, label: CATEGORY_LABELS[CATEGORY_VALUES.RD] },
|
||||||
|
{ value: CATEGORY_VALUES.FINANCE, label: CATEGORY_LABELS[CATEGORY_VALUES.FINANCE] },
|
||||||
|
{ value: CATEGORY_VALUES.IT, label: CATEGORY_LABELS[CATEGORY_VALUES.IT] },
|
||||||
|
{ value: CATEGORY_VALUES.SALES, label: CATEGORY_LABELS[CATEGORY_VALUES.SALES] },
|
||||||
|
{ value: CATEGORY_VALUES.AFTERSALES, label: CATEGORY_LABELS[CATEGORY_VALUES.AFTERSALES] },
|
||||||
|
];
|
||||||
|
|
||||||
|
// The empty category placeholder
|
||||||
|
export const EMPTY_AGENT_CATEGORY: AgentCategory = {
|
||||||
|
value: '',
|
||||||
|
label: 'com_ui_agent_category_general',
|
||||||
|
};
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import useAgentCategories from '../useAgentCategories';
|
||||||
|
import { AGENT_CATEGORIES, EMPTY_AGENT_CATEGORY } from '~/constants/agentCategories';
|
||||||
|
|
||||||
|
// Mock the useLocalize hook
|
||||||
|
jest.mock('~/hooks/useLocalize', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => (key: string) => {
|
||||||
|
// Simple mock implementation that returns the key as the translation
|
||||||
|
return key === 'com_ui_agent_category_general'
|
||||||
|
? 'General (Translated)'
|
||||||
|
: key;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useAgentCategories', () => {
|
||||||
|
it('should return processed categories with correct structure', () => {
|
||||||
|
const { result } = renderHook(() => useAgentCategories());
|
||||||
|
|
||||||
|
// Check that we have the expected number of categories
|
||||||
|
expect(result.current.categories.length).toBe(AGENT_CATEGORIES.length);
|
||||||
|
|
||||||
|
// Check that the first category has the expected structure
|
||||||
|
const firstCategory = result.current.categories[0];
|
||||||
|
const firstOriginalCategory = AGENT_CATEGORIES[0];
|
||||||
|
|
||||||
|
expect(firstCategory.value).toBe(firstOriginalCategory.value);
|
||||||
|
|
||||||
|
// Check that labels are properly translated
|
||||||
|
expect(firstCategory.label).toBe('General (Translated)');
|
||||||
|
expect(firstCategory.className).toBe('w-full');
|
||||||
|
|
||||||
|
// Check the empty category
|
||||||
|
expect(result.current.emptyCategory.value).toBe(EMPTY_AGENT_CATEGORY.value);
|
||||||
|
expect(result.current.emptyCategory.label).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,2 +1,4 @@
|
||||||
export { default as useAgentsMap } from './useAgentsMap';
|
export { default as useAgentsMap } from './useAgentsMap';
|
||||||
export { default as useSelectAgent } from './useSelectAgent';
|
export { default as useSelectAgent } from './useSelectAgent';
|
||||||
|
export { default as useAgentCategories } from './useAgentCategories';
|
||||||
|
export type { ProcessedAgentCategory } from './useAgentCategories';
|
||||||
|
|
44
client/src/hooks/Agents/useAgentCategories.tsx
Normal file
44
client/src/hooks/Agents/useAgentCategories.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
|
import { AGENT_CATEGORIES, EMPTY_AGENT_CATEGORY } from '~/constants/agentCategories';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
// This interface matches the structure used by the ControlCombobox component
|
||||||
|
export interface ProcessedAgentCategory {
|
||||||
|
label: string; // Translated label
|
||||||
|
value: string; // Category value
|
||||||
|
className?: string;
|
||||||
|
icon?: ReactNode; // Optional icon for the category
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook that provides processed and translated agent categories
|
||||||
|
*
|
||||||
|
* @returns Object containing categories and emptyCategory
|
||||||
|
*/
|
||||||
|
const useAgentCategories = () => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
const categories = useMemo((): ProcessedAgentCategory[] => {
|
||||||
|
return AGENT_CATEGORIES.map((category) => ({
|
||||||
|
label: localize(category.label),
|
||||||
|
value: category.value,
|
||||||
|
className: 'w-full',
|
||||||
|
// Note: Icons for categories should be handled separately
|
||||||
|
// This fixes the interface but doesn't implement icons
|
||||||
|
}));
|
||||||
|
}, [localize]);
|
||||||
|
|
||||||
|
const emptyCategory = useMemo(
|
||||||
|
(): ProcessedAgentCategory => ({
|
||||||
|
label: localize(EMPTY_AGENT_CATEGORY.label),
|
||||||
|
value: EMPTY_AGENT_CATEGORY.value,
|
||||||
|
className: 'w-full',
|
||||||
|
}),
|
||||||
|
[localize],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { categories, emptyCategory };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAgentCategories;
|
|
@ -29,7 +29,7 @@
|
||||||
"com_assistants_actions_info": "Let your Assistant retrieve information or take actions via API's",
|
"com_assistants_actions_info": "Let your Assistant retrieve information or take actions via API's",
|
||||||
"com_assistants_add_actions": "Add Actions",
|
"com_assistants_add_actions": "Add Actions",
|
||||||
"com_assistants_add_tools": "Add Tools",
|
"com_assistants_add_tools": "Add Tools",
|
||||||
"com_assistants_allow_sites_you_trust": "Only allow sites you trust",
|
"com_assistants_allow_sites_you_trust": "Only allow sites you trust.",
|
||||||
"com_assistants_append_date": "Append Current Date & Time",
|
"com_assistants_append_date": "Append Current Date & Time",
|
||||||
"com_assistants_append_date_tooltip": "When enabled, the current client date and time will be appended to the assistant system instructions.",
|
"com_assistants_append_date_tooltip": "When enabled, the current client date and time will be appended to the assistant system instructions.",
|
||||||
"com_assistants_attempt_info": "Assistant wants to send the following:",
|
"com_assistants_attempt_info": "Assistant wants to send the following:",
|
||||||
|
@ -523,6 +523,9 @@
|
||||||
"com_ui_backup_codes_regenerated": "Backup codes have been regenerated successfully",
|
"com_ui_backup_codes_regenerated": "Backup codes have been regenerated successfully",
|
||||||
"com_ui_basic": "Basic",
|
"com_ui_basic": "Basic",
|
||||||
"com_ui_basic_auth_header": "Basic authorization header",
|
"com_ui_basic_auth_header": "Basic authorization header",
|
||||||
|
"com_ui_client_credential_flow": "Client Credential Flow",
|
||||||
|
"com_ui_auth_code_flow": "Authorization Code Flow",
|
||||||
|
"com_ui_oauth_flow": "OAuth Flow",
|
||||||
"com_ui_bearer": "Bearer",
|
"com_ui_bearer": "Bearer",
|
||||||
"com_ui_bookmark_delete_confirm": "Are you sure you want to delete this bookmark?",
|
"com_ui_bookmark_delete_confirm": "Are you sure you want to delete this bookmark?",
|
||||||
"com_ui_bookmarks": "Bookmarks",
|
"com_ui_bookmarks": "Bookmarks",
|
||||||
|
@ -546,6 +549,15 @@
|
||||||
"com_ui_callback_url": "Callback URL",
|
"com_ui_callback_url": "Callback URL",
|
||||||
"com_ui_cancel": "Cancel",
|
"com_ui_cancel": "Cancel",
|
||||||
"com_ui_category": "Category",
|
"com_ui_category": "Category",
|
||||||
|
"com_ui_category_required_agent": "Category is required",
|
||||||
|
"com_ui_agent_category_selector_aria": "Agent's category selector",
|
||||||
|
"com_ui_agent_category_general": "General",
|
||||||
|
"com_ui_agent_category_hr": "HR",
|
||||||
|
"com_ui_agent_category_rd": "R&D",
|
||||||
|
"com_ui_agent_category_finance": "Finance",
|
||||||
|
"com_ui_agent_category_it": "IT",
|
||||||
|
"com_ui_agent_category_sales": "Sales",
|
||||||
|
"com_ui_agent_category_aftersales": "After Sales",
|
||||||
"com_ui_chat": "Chat",
|
"com_ui_chat": "Chat",
|
||||||
"com_ui_chat_history": "Chat History",
|
"com_ui_chat_history": "Chat History",
|
||||||
"com_ui_clear": "Clear",
|
"com_ui_clear": "Clear",
|
||||||
|
@ -784,6 +796,7 @@
|
||||||
"com_ui_schema": "Schema",
|
"com_ui_schema": "Schema",
|
||||||
"com_ui_scope": "Scope",
|
"com_ui_scope": "Scope",
|
||||||
"com_ui_search": "Search",
|
"com_ui_search": "Search",
|
||||||
|
"com_ui_search_agent_category": "Search categories...",
|
||||||
"com_ui_secret_key": "Secret Key",
|
"com_ui_secret_key": "Secret Key",
|
||||||
"com_ui_select": "Select",
|
"com_ui_select": "Select",
|
||||||
"com_ui_select_file": "Select a file",
|
"com_ui_select_file": "Select a file",
|
||||||
|
@ -826,6 +839,13 @@
|
||||||
"com_ui_stop": "Stop",
|
"com_ui_stop": "Stop",
|
||||||
"com_ui_storage": "Storage",
|
"com_ui_storage": "Storage",
|
||||||
"com_ui_submit": "Submit",
|
"com_ui_submit": "Submit",
|
||||||
|
"com_ui_support_contact": "Support Contact",
|
||||||
|
"com_ui_support_contact_name": "Name",
|
||||||
|
"com_ui_support_contact_name_placeholder": "Support contact name",
|
||||||
|
"com_ui_support_contact_name_min_length": "Name must be at least {{minLength}} characters",
|
||||||
|
"com_ui_support_contact_email": "Email",
|
||||||
|
"com_ui_support_contact_email_placeholder": "support@example.com",
|
||||||
|
"com_ui_support_contact_email_invalid": "Please enter a valid email address",
|
||||||
"com_ui_teach_or_explain": "Learning",
|
"com_ui_teach_or_explain": "Learning",
|
||||||
"com_ui_temporary": "Temporary Chat",
|
"com_ui_temporary": "Temporary Chat",
|
||||||
"com_ui_terms_and_conditions": "Terms and Conditions",
|
"com_ui_terms_and_conditions": "Terms and Conditions",
|
||||||
|
@ -871,13 +891,6 @@
|
||||||
"com_ui_x_selected": "{{0}} selected",
|
"com_ui_x_selected": "{{0}} selected",
|
||||||
"com_ui_yes": "Yes",
|
"com_ui_yes": "Yes",
|
||||||
"com_ui_zoom": "Zoom",
|
"com_ui_zoom": "Zoom",
|
||||||
"com_ui_getting_started": "Getting Started",
|
|
||||||
"com_ui_creating_image": "Creating image. May take a moment",
|
|
||||||
"com_ui_adding_details": "Adding details",
|
|
||||||
"com_ui_final_touch": "Final touch",
|
|
||||||
"com_ui_image_created": "Image created",
|
|
||||||
"com_ui_edit_editing_image": "Editing image",
|
|
||||||
"com_ui_image_edited": "Image edited",
|
|
||||||
"com_user_message": "You",
|
"com_user_message": "You",
|
||||||
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
||||||
}
|
}
|
|
@ -177,6 +177,11 @@ export const defaultAgentFormValues = {
|
||||||
recursion_limit: undefined,
|
recursion_limit: undefined,
|
||||||
[Tools.execute_code]: false,
|
[Tools.execute_code]: false,
|
||||||
[Tools.file_search]: false,
|
[Tools.file_search]: false,
|
||||||
|
category: 'general',
|
||||||
|
support_contact: {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageVisionTool: FunctionTool = {
|
export const ImageVisionTool: FunctionTool = {
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
import { Schema, Document, Types } from 'mongoose';
|
import { Schema, Document, Types } from 'mongoose';
|
||||||
|
export interface ISupportContact {
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAgent extends Omit<Document, 'model'> {
|
export interface IAgent extends Omit<Document, 'model'> {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
@ -26,6 +31,8 @@ export interface IAgent extends Omit<Document, 'model'> {
|
||||||
conversation_starters?: string[];
|
conversation_starters?: string[];
|
||||||
tool_resources?: unknown;
|
tool_resources?: unknown;
|
||||||
projectIds?: Types.ObjectId[];
|
projectIds?: Types.ObjectId[];
|
||||||
|
category: string;
|
||||||
|
support_contact?: ISupportContact;
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentSchema = new Schema<IAgent>(
|
const agentSchema = new Schema<IAgent>(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue