feat: visual indicator for global agent, remove author when serving to non-author

This commit is contained in:
Danny Avila 2024-09-04 21:02:01 -04:00
parent 1bc0689134
commit 2c6874013a
No known key found for this signature in database
GPG key ID: 2DD9CC89B9B50364
6 changed files with 77 additions and 29 deletions

View file

@ -83,6 +83,7 @@ const getListAgents = async (searchParameter) => {
id: 1, id: 1,
name: 1, name: 1,
avatar: 1, avatar: 1,
projectIds: 1,
}).lean(); }).lean();
const hasMore = agents.length > 0; const hasMore = agents.length > 0;

View file

@ -80,6 +80,10 @@ const getAgentHandler = async (req, res) => {
return res.status(404).json({ error: 'Agent not found' }); return res.status(404).json({ error: 'Agent not found' });
} }
if (agent.author !== author) {
delete agent.author;
}
return res.status(200).json(agent); return res.status(200).json(agent);
} catch (error) { } catch (error) {
logger.error('[/Agents/:id] Error retrieving agent', error); logger.error('[/Agents/:id] Error retrieving agent', error);

View file

@ -1,8 +1,8 @@
import { Capabilities } from 'librechat-data-provider'; import { Capabilities } from 'librechat-data-provider';
import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider'; import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider';
import type { Option, ExtendedFile } from './types'; import type { OptionWithIcon, ExtendedFile } from './types';
export type TAgentOption = Option & export type TAgentOption = OptionWithIcon &
Agent & { Agent & {
files?: Array<[string, ExtendedFile]>; files?: Array<[string, ExtendedFile]>;
code_files?: Array<[string, ExtendedFile]>; code_files?: Array<[string, ExtendedFile]>;
@ -23,5 +23,5 @@ export type AgentForm = {
model: string | null; model: string | null;
model_parameters: AgentModelParameters; model_parameters: AgentModelParameters;
tools?: string[]; tools?: string[];
provider?: AgentProvider | Option; provider?: AgentProvider | OptionWithIcon;
} & AgentCapabilities; } & AgentCapabilities;

View file

@ -1,10 +1,11 @@
import { Plus } from 'lucide-react'; import { Plus, EarthIcon } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { Capabilities, defaultAgentFormValues } from 'librechat-data-provider'; import { Capabilities, defaultAgentFormValues } from 'librechat-data-provider';
import type { AgentCapabilities, AgentForm, TAgentOption } from '~/common';
import type { Agent, AgentCreateParams } from 'librechat-data-provider'; import type { Agent, AgentCreateParams } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query'; import type { UseMutationResult } from '@tanstack/react-query';
import type { UseFormReset } from 'react-hook-form'; import type { UseFormReset } from 'react-hook-form';
import type { AgentCapabilities, AgentForm, TAgentOption } from '~/common';
import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils'; import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils';
import { useListAgentsQuery, useGetAgentByIdQuery } from '~/data-provider'; import { useListAgentsQuery, useGetAgentByIdQuery } from '~/data-provider';
import SelectDropDown from '~/components/ui/SelectDropDown'; import SelectDropDown from '~/components/ui/SelectDropDown';
@ -31,8 +32,16 @@ export default function AgentSelect({
// const fileMap = useFileMapContext(); // const fileMap = useFileMapContext();
const lastSelectedAgent = useRef<string | null>(null); const lastSelectedAgent = useRef<string | null>(null);
const { data: startupConfig } = useGetStartupConfig();
const { data: agents = null } = useListAgentsQuery(undefined, { const { data: agents = null } = useListAgentsQuery(undefined, {
select: (res) => res.data.map((agent) => processAgentOption(agent /*, fileMap */)), select: (res) =>
res.data.map((agent) =>
processAgentOption({
agent,
instanceProjectId: startupConfig?.instanceProjectId,
/* fileMap */
}),
),
}); });
const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', { const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', {
@ -41,11 +50,15 @@ export default function AgentSelect({
const resetAgentForm = useCallback( const resetAgentForm = useCallback(
(fullAgent: Agent) => { (fullAgent: Agent) => {
const { instanceProjectId } = startupConfig ?? {};
const isGlobal =
(instanceProjectId != null && fullAgent.projectIds?.includes(instanceProjectId)) ?? false;
const update = { const update = {
...fullAgent, ...fullAgent,
provider: createProviderOption(fullAgent.provider), provider: createProviderOption(fullAgent.provider),
label: fullAgent.name ?? '', label: fullAgent.name ?? '',
value: fullAgent.id || '', value: fullAgent.id || '',
icon: isGlobal ? <EarthIcon className={'icon-lg text-green-400'} /> : null,
}; };
const actions: AgentCapabilities = { const actions: AgentCapabilities = {
@ -138,6 +151,7 @@ export default function AgentSelect({
const hasAgentValue = !!(typeof currentAgentValue === 'object' const hasAgentValue = !!(typeof currentAgentValue === 'object'
? currentAgentValue.value != null && currentAgentValue.value !== '' ? currentAgentValue.value != null && currentAgentValue.value !== ''
: typeof currentAgentValue !== 'undefined'); : typeof currentAgentValue !== 'undefined');
return ( return (
<SelectDropDown <SelectDropDown
value={!hasAgentValue ? createAgent : (currentAgentValue as TAgentOption)} value={!hasAgentValue ? createAgent : (currentAgentValue as TAgentOption)}
@ -151,9 +165,11 @@ export default function AgentSelect({
] ]
} }
iconSide="left" iconSide="left"
optionIconSide="right"
showAbove={false} showAbove={false}
showLabel={false} showLabel={false}
emptyTitle={true} emptyTitle={true}
showOptionIcon={true}
containerClassName="flex-grow" containerClassName="flex-grow"
searchClassName="dark:from-gray-850" searchClassName="dark:from-gray-850"
searchPlaceholder={localize('com_agents_search_name')} searchPlaceholder={localize('com_agents_search_name')}

View file

@ -25,6 +25,7 @@ type SelectDropDownProps = {
showAbove?: boolean; showAbove?: boolean;
showLabel?: boolean; showLabel?: boolean;
iconSide?: 'left' | 'right'; iconSide?: 'left' | 'right';
optionIconSide?: 'left' | 'right';
renderOption?: () => React.ReactNode; renderOption?: () => React.ReactNode;
containerClassName?: string; containerClassName?: string;
currentValueClass?: string; currentValueClass?: string;
@ -43,12 +44,12 @@ function SelectDropDown({
value, value,
disabled, disabled,
setValue, setValue,
tabIndex,
availableValues, availableValues,
showAbove = false, showAbove = false,
showLabel = true, showLabel = true,
emptyTitle = false, emptyTitle = false,
iconSide = 'right', iconSide = 'right',
optionIconSide = 'left',
placeholder, placeholder,
containerClassName, containerClassName,
optionsListClass, optionsListClass,
@ -59,7 +60,7 @@ function SelectDropDown({
renderOption, renderOption,
searchClassName, searchClassName,
searchPlaceholder, searchPlaceholder,
showOptionIcon, showOptionIcon = false,
}: SelectDropDownProps) { }: SelectDropDownProps) {
const localize = useLocalize(); const localize = useLocalize();
const transitionProps = { className: 'top-full mt-3' }; const transitionProps = { className: 'top-full mt-3' };
@ -71,7 +72,7 @@ function SelectDropDown({
if (emptyTitle) { if (emptyTitle) {
title = ''; title = '';
} else if (!title) { } else if (!(title ?? '')) {
title = localize('com_ui_model'); title = localize('com_ui_model');
} }
@ -81,11 +82,12 @@ function SelectDropDown({
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({ const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
availableOptions: availableValues, availableOptions: availableValues,
placeholder: searchPlaceholder, placeholder: searchPlaceholder,
getTextKeyOverride: (option) => ((option as Option)?.label || '').toUpperCase(), getTextKeyOverride: (option) => ((option as Option).label ?? '').toUpperCase(),
className: searchClassName, className: searchClassName,
}); });
const hasSearchRender = Boolean(searchRender); const hasSearchRender = Boolean(searchRender);
const options = hasSearchRender ? filteredValues : availableValues; const options = hasSearchRender ? filteredValues : availableValues;
const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null;
return ( return (
<div className={cn('flex items-center justify-center gap-2 ', containerClassName ?? '')}> <div className={cn('flex items-center justify-center gap-2 ', containerClassName ?? '')}>
@ -121,20 +123,27 @@ function SelectDropDown({
{!showLabel && !emptyTitle && ( {!showLabel && !emptyTitle && (
<span className="text-xs text-gray-700 dark:text-gray-500">{title}:</span> <span className="text-xs text-gray-700 dark:text-gray-500">{title}:</span>
)} )}
{showOptionIcon && value && (value as OptionWithIcon)?.icon && ( {renderIcon && optionIconSide !== 'right' && (
<span className="icon-md flex items-center"> <span className="icon-md flex items-center">
{(value as OptionWithIcon).icon} {(value as OptionWithIcon).icon}
</span> </span>
)} )}
{value ? ( {renderIcon && (
typeof value !== 'string' ? ( <span className="icon-md absolute right-0 mr-8 flex items-center">
value?.label ?? '' {(value as OptionWithIcon).icon}
) : ( </span>
value
)
) : (
<span className="text-gray-500 dark:text-gray-400">{placeholder}</span>
)} )}
{(() => {
if (!value) {
return <span className="text-text-secondary">{placeholder}</span>;
}
if (typeof value !== 'string') {
return value.label ?? '';
}
return value;
})()}
</span> </span>
</span> </span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
@ -188,10 +197,10 @@ function SelectDropDown({
} }
const currentLabel = const currentLabel =
typeof option === 'string' ? option : option?.label ?? option?.value ?? ''; typeof option === 'string' ? option : option.label ?? option.value ?? '';
const currentValue = typeof option === 'string' ? option : option?.value ?? ''; const currentValue = typeof option === 'string' ? option : option.value ?? '';
const currentIcon = const currentIcon =
typeof option === 'string' ? null : (option?.icon as React.ReactNode) ?? null; typeof option === 'string' ? null : (option.icon as React.ReactNode) ?? null;
let activeValue: string | number | null | Option = value; let activeValue: string | number | null | Option = value;
if (typeof activeValue !== 'string') { if (typeof activeValue !== 'string') {
activeValue = activeValue?.value ?? ''; activeValue = activeValue?.value ?? '';
@ -217,7 +226,16 @@ function SelectDropDown({
iconSide === 'left' ? 'ml-4' : '', iconSide === 'left' ? 'ml-4' : '',
)} )}
> >
{currentIcon && <span className="mr-1">{currentIcon}</span>} {currentIcon != null && (
<span
className={cn(
'mr-1',
optionIconSide === 'right' ? 'absolute right-0 pr-2' : '',
)}
>
{currentIcon}
</span>
)}
{currentLabel} {currentLabel}
</span> </span>
{currentValue === activeValue && ( {currentValue === activeValue && (

View file

@ -1,3 +1,4 @@
import { EarthIcon } from 'lucide-react';
import { alternateName } from 'librechat-data-provider'; import { alternateName } from 'librechat-data-provider';
import type { Agent, TFile } from 'librechat-data-provider'; import type { Agent, TFile } from 'librechat-data-provider';
import type { DropdownValueSetter, TAgentOption } from '~/common'; import type { DropdownValueSetter, TAgentOption } from '~/common';
@ -39,14 +40,22 @@ export const createProviderOption = (provider: string) => ({
type FileTuple = [string, Partial<TFile>]; type FileTuple = [string, Partial<TFile>];
type FileList = Array<FileTuple>; type FileList = Array<FileTuple>;
export const processAgentOption = ( export const processAgentOption = ({
_agent?: Agent, agent: _agent,
fileMap?: Record<string, TFile>, fileMap,
): TAgentOption => { instanceProjectId,
}: {
agent?: Agent;
fileMap?: Record<string, TFile | undefined>;
instanceProjectId?: string;
}): TAgentOption => {
const isGlobal =
(instanceProjectId != null && _agent?.projectIds?.includes(instanceProjectId)) ?? false;
const agent: TAgentOption = { const agent: TAgentOption = {
...(_agent ?? ({} as Agent)), ...(_agent ?? ({} as Agent)),
label: _agent?.name ?? '', label: _agent?.name ?? '',
value: _agent?.id ?? '', value: _agent?.id ?? '',
icon: isGlobal ? <EarthIcon className="icon-md text-green-400" /> : null,
// files: _agent?.file_ids ? ([] as FileList) : undefined, // files: _agent?.file_ids ? ([] as FileList) : undefined,
// code_files: _agent?.tool_resources?.code_interpreter?.file_ids // code_files: _agent?.tool_resources?.code_interpreter?.file_ids
// ? ([] as FileList) // ? ([] as FileList)
@ -58,7 +67,7 @@ export const processAgentOption = (
} }
const handleFile = (file_id: string, list?: FileList) => { const handleFile = (file_id: string, list?: FileList) => {
const file = fileMap?.[file_id]; const file = fileMap[file_id];
if (file) { if (file) {
list?.push([file_id, file]); list?.push([file_id, file]);
} else { } else {
@ -82,7 +91,7 @@ export const processAgentOption = (
} }
if (agent.code_files && _agent?.tool_resources?.code_interpreter?.file_ids) { if (agent.code_files && _agent?.tool_resources?.code_interpreter?.file_ids) {
_agent.tool_resources?.code_interpreter?.file_ids?.forEach((file_id) => _agent.tool_resources.code_interpreter.file_ids.forEach((file_id) =>
handleFile(file_id, agent.code_files), handleFile(file_id, agent.code_files),
); );
} }