🐛 fix: Resolve 'Icon is Not a Function' Error in PresetItems (#5260)

* refactor: improve typing

* fix: "TypeError: Icon is not a function" with proper use of Functional Component and Improved Typing
This commit is contained in:
Danny Avila 2025-01-10 19:00:44 -05:00 committed by GitHub
parent 0855677a36
commit 24beda3d69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 57 additions and 49 deletions

View file

@ -91,7 +91,14 @@ export type IconMapProps = {
size?: number; size?: number;
}; };
export type AgentIconMapProps = IconMapProps & { agentName: string }; export type IconComponent = React.ComponentType<IconMapProps>;
export type AgentIconComponent = React.ComponentType<AgentIconMapProps>;
export type IconComponentTypes = IconComponent | AgentIconComponent;
export type IconsRecord = {
[key in t.EModelEndpoint | 'unknown' | string]: IconComponentTypes | null | undefined;
};
export type AgentIconMapProps = IconMapProps & { agentName?: string };
export type NavLink = { export type NavLink = {
title: string; title: string;

View file

@ -1,5 +1,5 @@
import { EModelEndpoint } from 'librechat-data-provider'; import { EModelEndpoint } from 'librechat-data-provider';
import type { IconMapProps, AgentIconMapProps } from '~/common'; import type { IconMapProps, AgentIconMapProps, IconsRecord } from '~/common';
import { Feather } from 'lucide-react'; import { Feather } from 'lucide-react';
import { import {
MinimalPlugin, MinimalPlugin,
@ -42,7 +42,7 @@ const AssistantAvatar = ({
}; };
const AgentAvatar = ({ className = '', avatar = '', agentName, size }: AgentIconMapProps) => { const AgentAvatar = ({ className = '', avatar = '', agentName, size }: AgentIconMapProps) => {
if (agentName && avatar) { if (agentName != null && agentName && avatar) {
return ( return (
<img <img
src={avatar} src={avatar}
@ -61,7 +61,7 @@ const Bedrock = ({ className = '' }: IconMapProps) => {
return <BedrockIcon className={cn(className, 'h-full w-full')} />; return <BedrockIcon className={cn(className, 'h-full w-full')} />;
}; };
export const icons = { export const icons: IconsRecord = {
[EModelEndpoint.azureOpenAI]: AzureMinimalIcon, [EModelEndpoint.azureOpenAI]: AzureMinimalIcon,
[EModelEndpoint.openAI]: GPTIcon, [EModelEndpoint.openAI]: GPTIcon,
[EModelEndpoint.gptPlugins]: MinimalPlugin, [EModelEndpoint.gptPlugins]: MinimalPlugin,

View file

@ -16,7 +16,7 @@ import { cn } from '~/utils';
import store from '~/store'; import store from '~/store';
const PresetItems: FC<{ const PresetItems: FC<{
presets: TPreset[]; presets?: Array<TPreset | undefined>;
onSetDefaultPreset: (preset: TPreset, remove?: boolean) => void; onSetDefaultPreset: (preset: TPreset, remove?: boolean) => void;
onSelectPreset: (preset: TPreset) => void; onSelectPreset: (preset: TPreset) => void;
onChangePreset: (preset: TPreset) => void; onChangePreset: (preset: TPreset) => void;
@ -110,11 +110,17 @@ const PresetItems: FC<{
</div> </div>
</div> </div>
)} )}
<Flipper flipKey={presets.map(({ presetId }) => presetId).join('.')}> <Flipper
flipKey={presets
?.map((preset) => preset?.presetId)
.filter((p) => p)
.join('.')}
>
{presets && {presets &&
presets.length > 0 && presets.length > 0 &&
presets.map((preset, i) => { presets.map((preset, i) => {
if (!preset || !preset.presetId) { const presetId = preset?.presetId ?? '';
if (!preset || !presetId) {
return null; return null;
} }
@ -122,22 +128,23 @@ const PresetItems: FC<{
const Icon = icons[iconKey]; const Icon = icons[iconKey];
return ( return (
<Close asChild key={`preset-${preset.presetId}`}> <Close asChild key={`preset-${presetId}`}>
<div key={`preset-${preset.presetId}`}> <div key={`preset-${presetId}`}>
<Flipped flipId={preset.presetId}> <Flipped flipId={presetId}>
<MenuItem <MenuItem
key={`preset-item-${preset.presetId}`} key={`preset-item-${presetId}`}
textClassName="text-xs max-w-[150px] sm:max-w-[200px] truncate md:max-w-full " textClassName="text-xs max-w-[150px] sm:max-w-[200px] truncate md:max-w-full "
title={getPresetTitle(preset)} title={getPresetTitle(preset)}
onClick={() => onSelectPreset(preset)} onClick={() => onSelectPreset(preset)}
icon={ icon={
Icon && Icon != null && (
Icon({ <Icon
context: 'menu-item', context="menu-item"
iconURL: getEndpointField(endpointsConfig, preset.endpoint, 'iconURL'), iconURL={getEndpointField(endpointsConfig, preset.endpoint, 'iconURL')}
className: 'icon-md mr-1 dark:text-white', className="icon-md mr-1 dark:text-white"
endpoint: preset.endpoint, endpoint={preset.endpoint}
}) />
)
} }
selected={false} selected={false}
data-testid={`preset-item-${preset}`} data-testid={`preset-item-${preset}`}
@ -146,17 +153,17 @@ const PresetItems: FC<{
<button <button
className={cn( className={cn(
'm-0 h-full rounded-md bg-transparent p-2 text-gray-400 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200', 'm-0 h-full rounded-md bg-transparent p-2 text-gray-400 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200',
defaultPreset?.presetId === preset.presetId defaultPreset?.presetId === presetId
? '' ? ''
: 'sm:invisible sm:group-hover:visible', : 'sm:invisible sm:group-hover:visible',
)} )}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
onSetDefaultPreset(preset, defaultPreset?.presetId === preset.presetId); onSetDefaultPreset(preset, defaultPreset?.presetId === presetId);
}} }}
> >
<PinIcon unpin={defaultPreset?.presetId === preset.presetId} /> <PinIcon unpin={defaultPreset?.presetId === presetId} />
</button> </button>
<button <button
className="m-0 h-full rounded-md p-2 text-gray-400 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible" className="m-0 h-full rounded-md p-2 text-gray-400 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 sm:invisible sm:group-hover:visible"

View file

@ -20,8 +20,6 @@ const PresetsMenu: FC = () => {
exportPreset, exportPreset,
} = usePresets(); } = usePresets();
const { preset } = useChatContext(); const { preset } = useChatContext();
const presets = presetsQuery.data || [];
return ( return (
<Root> <Root>
<Trigger asChild> <Trigger asChild>
@ -54,7 +52,7 @@ const PresetsMenu: FC = () => {
className="mt-2 max-h-[495px] overflow-x-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white md:min-w-[400px]" className="mt-2 max-h-[495px] overflow-x-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white md:min-w-[400px]"
> >
<PresetItems <PresetItems
presets={presets} presets={presetsQuery.data}
onSetDefaultPreset={onSetDefaultPreset} onSetDefaultPreset={onSetDefaultPreset}
onSelectPreset={onSelectPreset} onSelectPreset={onSelectPreset}
onChangePreset={onChangePreset} onChangePreset={onChangePreset}

View file

@ -7,7 +7,7 @@ import {
isAssistantsEndpoint, isAssistantsEndpoint,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type * as t from 'librechat-data-provider'; import type * as t from 'librechat-data-provider';
import type { LocalizeFunction } from '~/common'; import type { LocalizeFunction, IconsRecord } from '~/common';
export const getEntityName = ({ export const getEntityName = ({
name = '', name = '',
@ -222,7 +222,7 @@ export function getIconKey({
endpointsConfig?: t.TEndpointsConfig; endpointsConfig?: t.TEndpointsConfig;
endpointType?: string | null; endpointType?: string | null;
endpointIconURL?: string; endpointIconURL?: string;
}) { }): keyof IconsRecord {
const endpointType = _eType ?? getEndpointField(endpointsConfig, endpoint, 'type') ?? ''; const endpointType = _eType ?? getEndpointField(endpointsConfig, endpoint, 'type') ?? '';
const endpointIconURL = iconURL ?? getEndpointField(endpointsConfig, endpoint, 'iconURL') ?? ''; const endpointIconURL = iconURL ?? getEndpointField(endpointsConfig, endpoint, 'iconURL') ?? '';
if (endpointIconURL && EModelEndpoint[endpointIconURL] != null) { if (endpointIconURL && EModelEndpoint[endpointIconURL] != null) {

View file

@ -1,17 +1,6 @@
import type { TPreset, TPlugin } from 'librechat-data-provider'; import type { TPreset, TPlugin } from 'librechat-data-provider';
import { EModelEndpoint } from 'librechat-data-provider'; import { EModelEndpoint } from 'librechat-data-provider';
export const getPresetIcon = (preset: TPreset, Icon) => {
return Icon({
size: 20,
endpoint: preset?.endpoint,
model: preset?.model,
error: false,
className: 'icon-md',
isCreatedByUser: false,
});
};
type TEndpoints = Array<string | EModelEndpoint>; type TEndpoints = Array<string | EModelEndpoint>;
export const getPresetTitle = (preset: TPreset, mention?: boolean) => { export const getPresetTitle = (preset: TPreset, mention?: boolean) => {
@ -27,7 +16,7 @@ export const getPresetTitle = (preset: TPreset, mention?: boolean) => {
toneStyle, toneStyle,
} = preset; } = preset;
let title = ''; let title = '';
let modelInfo = model || ''; let modelInfo = model ?? '';
let label = ''; let label = '';
const usesChatGPTLabel: TEndpoints = [ const usesChatGPTLabel: TEndpoints = [
@ -37,24 +26,31 @@ export const getPresetTitle = (preset: TPreset, mention?: boolean) => {
]; ];
const usesModelLabel: TEndpoints = [EModelEndpoint.google, EModelEndpoint.anthropic]; const usesModelLabel: TEndpoints = [EModelEndpoint.google, EModelEndpoint.anthropic];
if (endpoint && usesChatGPTLabel.includes(endpoint)) { if (endpoint != null && endpoint && usesChatGPTLabel.includes(endpoint)) {
label = chatGptLabel || ''; label = chatGptLabel ?? '';
} else if (endpoint && usesModelLabel.includes(endpoint)) { } else if (endpoint != null && endpoint && usesModelLabel.includes(endpoint)) {
label = modelLabel || ''; label = modelLabel ?? '';
} else if (endpoint === EModelEndpoint.bingAI) { } else if (endpoint === EModelEndpoint.bingAI) {
modelInfo = jailbreak ? 'Sydney' : modelInfo; modelInfo = jailbreak === true ? 'Sydney' : modelInfo;
label = toneStyle ? `: ${toneStyle}` : ''; label = toneStyle != null && toneStyle ? `: ${toneStyle}` : '';
} }
if (label && presetTitle && label.toLowerCase().includes(presetTitle.toLowerCase())) { if (
label &&
presetTitle != null &&
presetTitle &&
label.toLowerCase().includes(presetTitle.toLowerCase())
) {
title = label + ': '; title = label + ': ';
label = ''; label = '';
} else if (presetTitle && presetTitle.trim() !== 'New Chat') { } else if (presetTitle != null && presetTitle && presetTitle.trim() !== 'New Chat') {
title = presetTitle + ': '; title = presetTitle + ': ';
} }
if (mention) { if (mention === true) {
return `${modelInfo}${label ? ` | ${label}` : ''}${promptPrefix ? ` | ${promptPrefix}` : ''}${ return `${modelInfo}${label ? ` | ${label}` : ''}${
promptPrefix != null && promptPrefix ? ` | ${promptPrefix}` : ''
}${
tools tools
? ` | ${tools ? ` | ${tools
.map((tool: TPlugin | string) => { .map((tool: TPlugin | string) => {
@ -74,7 +70,7 @@ export const getPresetTitle = (preset: TPreset, mention?: boolean) => {
/** Remove unavailable tools from the preset */ /** Remove unavailable tools from the preset */
export const removeUnavailableTools = ( export const removeUnavailableTools = (
preset: TPreset, preset: TPreset,
availableTools: Record<string, TPlugin>, availableTools: Record<string, TPlugin | undefined>,
) => { ) => {
const newPreset = { ...preset }; const newPreset = { ...preset };