LibreChat/client/src/utils/forms.tsx
Marco Beretta 8907bd5d7c
👤 feat: Agent Avatar Removal and Decouple upload/reset from Agent Updates (#10527)
*  feat: Enhance agent avatar management with upload and reset functionality

*  feat: Refactor AvatarMenu to use DropdownPopup for improved UI and functionality

*  feat: Improve avatar upload handling in AgentPanel to suppress misleading "no changes" toast

*  feat: Refactor toast message handling and payload composition in AgentPanel for improved clarity and functionality

*  feat: Enhance agent avatar functionality with upload, reset, and validation improvements

*  feat: Refactor agent avatar upload handling and enhance related components for improved functionality and user experience

* feat(agents): tighten ACL, harden GETs/search, and sanitize action metadata
stop persisting refreshed S3 URLs on GET; compute per-response only
enforce ACL EDIT on revert route; remove legacy admin/author/collab checks
sanitize action metadata before persisting during duplication (api_key, oauth_client_id, oauth_client_secret)
escape user search input, cap length (100), and use Set for public flag mapping
add explicit req.file guard in avatar upload; fix empty catch lint; remove unused imports

* feat: Remove outdated avatar-related translation keys

* feat: Improve error logging for avatar updates and streamline file input handling

* feat(agents): implement caching for S3 avatar refresh in agent list responses

* fix: replace unconventional 'void e' with explicit comment to clarify intentionally ignored error

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat(agents): enhance avatar handling and improve search functionality

* fix: clarify intentionally ignored error in agent list handler

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-17 17:04:01 -05:00

168 lines
4.5 KiB
TypeScript

import { EarthIcon } from 'lucide-react';
import {
FileSources,
alternateName,
EModelEndpoint,
EToolResources,
LocalStorageKeys,
defaultAgentFormValues,
} from 'librechat-data-provider';
import type { Agent, TFile } from 'librechat-data-provider';
import type { DropdownValueSetter, TAgentOption, ExtendedFile } from '~/common';
/**
* Creates a Dropdown value setter that always passes a string value,
* for when options (object with label/value fields) are used for the
* available values, and a string value is expected when selected.
*
* Only necessary when the available values are objects with label/value fields
* and the selected value is expected to be a string.
**/
export const createDropdownSetter = (setValue: (value: string) => void): DropdownValueSetter => {
return (value) => {
if (!value) {
setValue('');
return;
}
if (typeof value === 'string') {
setValue(value);
return;
}
if (value.value) {
setValue(value.value + '');
}
};
};
/**
* Creates an Option object for a provider dropdown.
**/
export const createProviderOption = (provider: string) => ({
label: (alternateName[provider] as string | undefined) ?? provider,
value: provider,
});
/**
* Gets default agent form values with localStorage values for model and provider.
* This is used to initialize agent forms with the last used model and provider.
**/
export const getDefaultAgentFormValues = () => ({
...defaultAgentFormValues,
model: localStorage.getItem(LocalStorageKeys.LAST_AGENT_MODEL) ?? '',
provider: createProviderOption(localStorage.getItem(LocalStorageKeys.LAST_AGENT_PROVIDER) ?? ''),
avatar_file: null,
avatar_preview: '',
avatar_action: null,
});
export const processAgentOption = ({
agent: _agent,
fileMap,
}: {
agent?: Agent;
fileMap?: Record<string, TFile | undefined>;
}): TAgentOption => {
const isGlobal = _agent?.isPublic ?? false;
const context_files = _agent?.tool_resources?.context?.file_ids ?? [];
if (_agent?.tool_resources?.ocr?.file_ids) {
/** Backwards-compatibility */
context_files.push(..._agent.tool_resources.ocr.file_ids);
}
const agent: TAgentOption = {
...(_agent ?? ({} as Agent)),
label: _agent?.name ?? '',
value: _agent?.id ?? '',
icon: isGlobal ? <EarthIcon className="icon-md text-green-400" /> : null,
context_files: context_files.length > 0 ? ([] as Array<[string, ExtendedFile]>) : undefined,
knowledge_files: _agent?.tool_resources?.file_search?.file_ids
? ([] as Array<[string, ExtendedFile]>)
: undefined,
code_files: _agent?.tool_resources?.execute_code?.file_ids
? ([] as Array<[string, ExtendedFile]>)
: undefined,
};
if (!fileMap) {
return agent;
}
const handleFile = ({
file_id,
tool_resource,
list,
}: {
file_id: string;
tool_resource: EToolResources;
list?: Array<[string, ExtendedFile]>;
}) => {
const file = fileMap[file_id];
const source =
tool_resource === EToolResources.file_search
? FileSources.vectordb
: (file?.source ?? FileSources.local);
if (file) {
list?.push([
file_id,
{
file_id: file.file_id,
type: file.type,
filepath: file.filepath,
filename: file.filename,
width: file.width,
height: file.height,
size: file.bytes,
preview: file.filepath,
metadata: file.metadata,
progress: 1,
source,
},
]);
} else {
list?.push([
file_id,
{
file_id,
type: '',
filename: '',
size: 1,
progress: 1,
filepath: EModelEndpoint.agents,
source,
},
]);
}
};
if (agent.context_files && context_files.length > 0) {
context_files.forEach((file_id) =>
handleFile({
file_id,
list: agent.context_files,
tool_resource: EToolResources.context,
}),
);
}
if (agent.knowledge_files && _agent?.tool_resources?.file_search?.file_ids) {
_agent.tool_resources.file_search.file_ids.forEach((file_id) =>
handleFile({
file_id,
list: agent.knowledge_files,
tool_resource: EToolResources.file_search,
}),
);
}
if (agent.code_files && _agent?.tool_resources?.execute_code?.file_ids) {
_agent.tool_resources.execute_code.file_ids.forEach((file_id) =>
handleFile({ file_id, list: agent.code_files, tool_resource: EToolResources.execute_code }),
);
}
return agent;
};