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');
|
||||
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);
|
||||
|
||||
/**
|
||||
|
@ -21,7 +76,12 @@ const Agent = mongoose.model('agent', agentSchema);
|
|||
* @throws {Error} If the agent creation fails.
|
||||
*/
|
||||
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,
|
||||
description: 1,
|
||||
isCollaborative: 1,
|
||||
category: 1,
|
||||
}).lean()
|
||||
).map((agent) => {
|
||||
if (agent.author?.toString() !== author) {
|
||||
|
|
|
@ -16,6 +16,11 @@ export type TAgentCapabilities = {
|
|||
[AgentCapabilities.hide_sequential_outputs]?: boolean;
|
||||
};
|
||||
|
||||
export type SupportContact = {
|
||||
name?: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
export type AgentForm = {
|
||||
agent?: TAgentOption;
|
||||
id: string;
|
||||
|
@ -29,4 +34,5 @@ export type AgentForm = {
|
|||
agent_ids?: string[];
|
||||
[AgentCapabilities.artifacts]?: ArtifactModes | string;
|
||||
recursion_limit?: number;
|
||||
support_contact?: SupportContact;
|
||||
} & 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 AgentTool from './AgentTool';
|
||||
import CodeForm from './Code/Form';
|
||||
import AgentCategorySelector from './AgentCategorySelector';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
const labelClass = 'mb-2 text-token-text-primary block font-medium';
|
||||
|
@ -228,6 +229,14 @@ export default function AgentConfig({
|
|||
)}
|
||||
/>
|
||||
</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 />
|
||||
{/* Model and Provider */}
|
||||
|
@ -329,6 +338,88 @@ export default function AgentConfig({
|
|||
</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>
|
||||
<ToolSelectDialog
|
||||
isOpen={showToolDialog}
|
||||
|
|
|
@ -140,6 +140,8 @@ export default function AgentPanel({
|
|||
end_after_tools,
|
||||
hide_sequential_outputs,
|
||||
recursion_limit,
|
||||
category,
|
||||
support_contact,
|
||||
} = data;
|
||||
|
||||
const model = _model ?? '';
|
||||
|
@ -162,6 +164,8 @@ export default function AgentPanel({
|
|||
end_after_tools,
|
||||
hide_sequential_outputs,
|
||||
recursion_limit,
|
||||
category,
|
||||
support_contact,
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
@ -187,6 +191,8 @@ export default function AgentPanel({
|
|||
end_after_tools,
|
||||
hide_sequential_outputs,
|
||||
recursion_limit,
|
||||
category,
|
||||
support_contact,
|
||||
});
|
||||
},
|
||||
[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"
|
||||
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">
|
||||
<AgentSelect
|
||||
createMutation={create}
|
||||
|
|
|
@ -73,6 +73,10 @@ export default function AgentSelect({
|
|||
agent: update,
|
||||
model: update.model,
|
||||
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]) => {
|
||||
|
|
|
@ -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 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_add_actions": "Add Actions",
|
||||
"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_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:",
|
||||
|
@ -523,6 +523,9 @@
|
|||
"com_ui_backup_codes_regenerated": "Backup codes have been regenerated successfully",
|
||||
"com_ui_basic": "Basic",
|
||||
"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_bookmark_delete_confirm": "Are you sure you want to delete this bookmark?",
|
||||
"com_ui_bookmarks": "Bookmarks",
|
||||
|
@ -546,6 +549,15 @@
|
|||
"com_ui_callback_url": "Callback URL",
|
||||
"com_ui_cancel": "Cancel",
|
||||
"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_history": "Chat History",
|
||||
"com_ui_clear": "Clear",
|
||||
|
@ -784,6 +796,7 @@
|
|||
"com_ui_schema": "Schema",
|
||||
"com_ui_scope": "Scope",
|
||||
"com_ui_search": "Search",
|
||||
"com_ui_search_agent_category": "Search categories...",
|
||||
"com_ui_secret_key": "Secret Key",
|
||||
"com_ui_select": "Select",
|
||||
"com_ui_select_file": "Select a file",
|
||||
|
@ -826,6 +839,13 @@
|
|||
"com_ui_stop": "Stop",
|
||||
"com_ui_storage": "Storage",
|
||||
"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_temporary": "Temporary Chat",
|
||||
"com_ui_terms_and_conditions": "Terms and Conditions",
|
||||
|
@ -871,13 +891,6 @@
|
|||
"com_ui_x_selected": "{{0}} selected",
|
||||
"com_ui_yes": "Yes",
|
||||
"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_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
||||
}
|
|
@ -177,6 +177,11 @@ export const defaultAgentFormValues = {
|
|||
recursion_limit: undefined,
|
||||
[Tools.execute_code]: false,
|
||||
[Tools.file_search]: false,
|
||||
category: 'general',
|
||||
support_contact: {
|
||||
name: '',
|
||||
email: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const ImageVisionTool: FunctionTool = {
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { Schema, Document, Types } from 'mongoose';
|
||||
export interface ISupportContact {
|
||||
name?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface IAgent extends Omit<Document, 'model'> {
|
||||
id: string;
|
||||
name?: string;
|
||||
|
@ -26,6 +31,8 @@ export interface IAgent extends Omit<Document, 'model'> {
|
|||
conversation_starters?: string[];
|
||||
tool_resources?: unknown;
|
||||
projectIds?: Types.ObjectId[];
|
||||
category: string;
|
||||
support_contact?: ISupportContact;
|
||||
}
|
||||
|
||||
const agentSchema = new Schema<IAgent>(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue