From 2c6874013af46e6e6349d35639a8c0e04cbb1357 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 4 Sep 2024 21:02:01 -0400 Subject: [PATCH] feat: visual indicator for global agent, remove author when serving to non-author --- api/models/Agent.js | 1 + api/server/controllers/agents/v1.js | 4 ++ client/src/common/agents-types.ts | 6 +-- .../SidePanel/Agents/AgentSelect.tsx | 22 ++++++-- client/src/components/ui/SelectDropDown.tsx | 52 +++++++++++++------ client/src/utils/{forms.ts => forms.tsx} | 21 +++++--- 6 files changed, 77 insertions(+), 29 deletions(-) rename client/src/utils/{forms.ts => forms.tsx} (79%) diff --git a/api/models/Agent.js b/api/models/Agent.js index 9cab7d4b3f..2112a44991 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -83,6 +83,7 @@ const getListAgents = async (searchParameter) => { id: 1, name: 1, avatar: 1, + projectIds: 1, }).lean(); const hasMore = agents.length > 0; diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 01692bd08d..65e37f2618 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -80,6 +80,10 @@ const getAgentHandler = async (req, res) => { return res.status(404).json({ error: 'Agent not found' }); } + if (agent.author !== author) { + delete agent.author; + } + return res.status(200).json(agent); } catch (error) { logger.error('[/Agents/:id] Error retrieving agent', error); diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts index eaf64f4c6c..07633a68db 100644 --- a/client/src/common/agents-types.ts +++ b/client/src/common/agents-types.ts @@ -1,8 +1,8 @@ import { Capabilities } 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 & { files?: Array<[string, ExtendedFile]>; code_files?: Array<[string, ExtendedFile]>; @@ -23,5 +23,5 @@ export type AgentForm = { model: string | null; model_parameters: AgentModelParameters; tools?: string[]; - provider?: AgentProvider | Option; + provider?: AgentProvider | OptionWithIcon; } & AgentCapabilities; diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx index 5d36b358be..7803cca464 100644 --- a/client/src/components/SidePanel/Agents/AgentSelect.tsx +++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx @@ -1,10 +1,11 @@ -import { Plus } from 'lucide-react'; +import { Plus, EarthIcon } from 'lucide-react'; import { useCallback, useEffect, useRef } from 'react'; +import { useGetStartupConfig } from 'librechat-data-provider/react-query'; import { Capabilities, defaultAgentFormValues } from 'librechat-data-provider'; -import type { AgentCapabilities, AgentForm, TAgentOption } from '~/common'; import type { Agent, AgentCreateParams } from 'librechat-data-provider'; import type { UseMutationResult } from '@tanstack/react-query'; import type { UseFormReset } from 'react-hook-form'; +import type { AgentCapabilities, AgentForm, TAgentOption } from '~/common'; import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils'; import { useListAgentsQuery, useGetAgentByIdQuery } from '~/data-provider'; import SelectDropDown from '~/components/ui/SelectDropDown'; @@ -31,8 +32,16 @@ export default function AgentSelect({ // const fileMap = useFileMapContext(); const lastSelectedAgent = useRef(null); + const { data: startupConfig } = useGetStartupConfig(); 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 ?? '', { @@ -41,11 +50,15 @@ export default function AgentSelect({ const resetAgentForm = useCallback( (fullAgent: Agent) => { + const { instanceProjectId } = startupConfig ?? {}; + const isGlobal = + (instanceProjectId != null && fullAgent.projectIds?.includes(instanceProjectId)) ?? false; const update = { ...fullAgent, provider: createProviderOption(fullAgent.provider), label: fullAgent.name ?? '', value: fullAgent.id || '', + icon: isGlobal ? : null, }; const actions: AgentCapabilities = { @@ -138,6 +151,7 @@ export default function AgentSelect({ const hasAgentValue = !!(typeof currentAgentValue === 'object' ? currentAgentValue.value != null && currentAgentValue.value !== '' : typeof currentAgentValue !== 'undefined'); + return ( React.ReactNode; containerClassName?: string; currentValueClass?: string; @@ -43,12 +44,12 @@ function SelectDropDown({ value, disabled, setValue, - tabIndex, availableValues, showAbove = false, showLabel = true, emptyTitle = false, iconSide = 'right', + optionIconSide = 'left', placeholder, containerClassName, optionsListClass, @@ -59,7 +60,7 @@ function SelectDropDown({ renderOption, searchClassName, searchPlaceholder, - showOptionIcon, + showOptionIcon = false, }: SelectDropDownProps) { const localize = useLocalize(); const transitionProps = { className: 'top-full mt-3' }; @@ -71,7 +72,7 @@ function SelectDropDown({ if (emptyTitle) { title = ''; - } else if (!title) { + } else if (!(title ?? '')) { title = localize('com_ui_model'); } @@ -81,11 +82,12 @@ function SelectDropDown({ const [filteredValues, searchRender] = useMultiSearch({ availableOptions: availableValues, placeholder: searchPlaceholder, - getTextKeyOverride: (option) => ((option as Option)?.label || '').toUpperCase(), + getTextKeyOverride: (option) => ((option as Option).label ?? '').toUpperCase(), className: searchClassName, }); const hasSearchRender = Boolean(searchRender); const options = hasSearchRender ? filteredValues : availableValues; + const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null; return (
@@ -121,20 +123,27 @@ function SelectDropDown({ {!showLabel && !emptyTitle && ( {title}: )} - {showOptionIcon && value && (value as OptionWithIcon)?.icon && ( + {renderIcon && optionIconSide !== 'right' && ( {(value as OptionWithIcon).icon} )} - {value ? ( - typeof value !== 'string' ? ( - value?.label ?? '' - ) : ( - value - ) - ) : ( - {placeholder} + {renderIcon && ( + + {(value as OptionWithIcon).icon} + )} + {(() => { + if (!value) { + return {placeholder}; + } + + if (typeof value !== 'string') { + return value.label ?? ''; + } + + return value; + })()} @@ -188,10 +197,10 @@ function SelectDropDown({ } const currentLabel = - typeof option === 'string' ? option : option?.label ?? option?.value ?? ''; - const currentValue = typeof option === 'string' ? option : option?.value ?? ''; + typeof option === 'string' ? option : option.label ?? option.value ?? ''; + const currentValue = typeof option === 'string' ? option : option.value ?? ''; 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; if (typeof activeValue !== 'string') { activeValue = activeValue?.value ?? ''; @@ -217,7 +226,16 @@ function SelectDropDown({ iconSide === 'left' ? 'ml-4' : '', )} > - {currentIcon && {currentIcon}} + {currentIcon != null && ( + + {currentIcon} + + )} {currentLabel} {currentValue === activeValue && ( diff --git a/client/src/utils/forms.ts b/client/src/utils/forms.tsx similarity index 79% rename from client/src/utils/forms.ts rename to client/src/utils/forms.tsx index 0362f576ed..2447a234dd 100644 --- a/client/src/utils/forms.ts +++ b/client/src/utils/forms.tsx @@ -1,3 +1,4 @@ +import { EarthIcon } from 'lucide-react'; import { alternateName } from 'librechat-data-provider'; import type { Agent, TFile } from 'librechat-data-provider'; import type { DropdownValueSetter, TAgentOption } from '~/common'; @@ -39,14 +40,22 @@ export const createProviderOption = (provider: string) => ({ type FileTuple = [string, Partial]; type FileList = Array; -export const processAgentOption = ( - _agent?: Agent, - fileMap?: Record, -): TAgentOption => { +export const processAgentOption = ({ + agent: _agent, + fileMap, + instanceProjectId, +}: { + agent?: Agent; + fileMap?: Record; + instanceProjectId?: string; +}): TAgentOption => { + const isGlobal = + (instanceProjectId != null && _agent?.projectIds?.includes(instanceProjectId)) ?? false; const agent: TAgentOption = { ...(_agent ?? ({} as Agent)), label: _agent?.name ?? '', value: _agent?.id ?? '', + icon: isGlobal ? : null, // files: _agent?.file_ids ? ([] as FileList) : undefined, // code_files: _agent?.tool_resources?.code_interpreter?.file_ids // ? ([] as FileList) @@ -58,7 +67,7 @@ export const processAgentOption = ( } const handleFile = (file_id: string, list?: FileList) => { - const file = fileMap?.[file_id]; + const file = fileMap[file_id]; if (file) { list?.push([file_id, file]); } else { @@ -82,7 +91,7 @@ export const processAgentOption = ( } 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), ); }