mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-17 07:55:32 +01:00
feat: add groupIcon property to modelSpecs for custom group icons
Added the ability to define icons for custom model spec groups in the UI selector. Changes: - Added property to TModelSpec type and schema in data-provider - Created GroupIcon component to render URL or built-in endpoint icons - Updated CustomGroup component to display group icons - Added documentation and examples in librechat.example.yaml Usage: The groupIcon can be: - A built-in endpoint key (e.g., "openAI", "anthropic", "groq") - A URL to a custom icon image Only the first spec in a group needs groupIcon - all specs share the same icon.
This commit is contained in:
parent
8bdc808074
commit
308347a7ad
4 changed files with 102 additions and 8 deletions
|
|
@ -3,13 +3,15 @@ import type { TModelSpec } from 'librechat-data-provider';
|
|||
import { CustomMenu as Menu } from '../CustomMenu';
|
||||
import { ModelSpecItem } from './ModelSpecItem';
|
||||
import { useModelSelectorContext } from '../ModelSelectorContext';
|
||||
import GroupIcon from './GroupIcon';
|
||||
|
||||
interface CustomGroupProps {
|
||||
groupName: string;
|
||||
specs: TModelSpec[];
|
||||
groupIcon?: string;
|
||||
}
|
||||
|
||||
export function CustomGroup({ groupName, specs }: CustomGroupProps) {
|
||||
export function CustomGroup({ groupName, specs, groupIcon }: CustomGroupProps) {
|
||||
const { selectedValues } = useModelSelectorContext();
|
||||
const { modelSpec: selectedSpec } = selectedValues;
|
||||
|
||||
|
|
@ -25,6 +27,11 @@ export function CustomGroup({ groupName, specs }: CustomGroupProps) {
|
|||
label={
|
||||
<div className="group flex w-full flex-shrink cursor-pointer items-center justify-between rounded-xl px-1 py-1 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
{groupIcon && (
|
||||
<div className="flex-shrink-0">
|
||||
<GroupIcon iconURL={groupIcon} groupName={groupName} />
|
||||
</div>
|
||||
)}
|
||||
<span className="truncate text-left">{groupName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -45,22 +52,27 @@ export function renderCustomGroups(
|
|||
const endpointValues = new Set(mappedEndpoints.map((ep) => ep.value));
|
||||
|
||||
// Group specs by their group field (excluding endpoint-matched groups and ungrouped)
|
||||
// Also track the groupIcon for each group (first spec with groupIcon wins)
|
||||
const customGroups = modelSpecs.reduce(
|
||||
(acc, spec) => {
|
||||
if (!spec.group || endpointValues.has(spec.group)) {
|
||||
return acc;
|
||||
}
|
||||
if (!acc[spec.group]) {
|
||||
acc[spec.group] = [];
|
||||
acc[spec.group] = { specs: [], groupIcon: undefined };
|
||||
}
|
||||
acc[spec.group].specs.push(spec);
|
||||
// Use the first groupIcon found for the group
|
||||
if (!acc[spec.group].groupIcon && spec.groupIcon) {
|
||||
acc[spec.group].groupIcon = spec.groupIcon;
|
||||
}
|
||||
acc[spec.group].push(spec);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, TModelSpec[]>,
|
||||
{} as Record<string, { specs: TModelSpec[]; groupIcon?: string }>,
|
||||
);
|
||||
|
||||
// Render each custom group
|
||||
return Object.entries(customGroups).map(([groupName, specs]) => (
|
||||
<CustomGroup key={groupName} groupName={groupName} specs={specs} />
|
||||
return Object.entries(customGroups).map(([groupName, { specs, groupIcon }]) => (
|
||||
<CustomGroup key={groupName} groupName={groupName} specs={specs} groupIcon={groupIcon} />
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import React, { memo, useState } from 'react';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import type { IconMapProps } from '~/common';
|
||||
import { icons } from '~/hooks/Endpoint/Icons';
|
||||
|
||||
interface GroupIconProps {
|
||||
iconURL: string;
|
||||
groupName: string;
|
||||
}
|
||||
|
||||
type IconType = (props: IconMapProps) => React.JSX.Element;
|
||||
|
||||
const GroupIcon: React.FC<GroupIconProps> = ({ iconURL, groupName }) => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
const handleImageError = () => {
|
||||
setImageError(true);
|
||||
};
|
||||
|
||||
// Check if the iconURL is a URL or a built-in icon key
|
||||
if (!iconURL.includes('http')) {
|
||||
const Icon: IconType = (icons[iconURL] ?? icons.unknown) as IconType;
|
||||
return <Icon size={20} context="menu-item" className="icon-md shrink-0 text-text-primary" />;
|
||||
}
|
||||
|
||||
if (imageError || !iconURL) {
|
||||
const DefaultIcon: IconType = icons.unknown as IconType;
|
||||
return (
|
||||
<div className="relative" style={{ width: 20, height: 20, margin: '2px' }}>
|
||||
<div className="icon-md shrink-0 overflow-hidden rounded-full">
|
||||
<DefaultIcon context="menu-item" size={20} />
|
||||
</div>
|
||||
{imageError && iconURL && (
|
||||
<div
|
||||
className="absolute flex items-center justify-center rounded-full bg-red-500"
|
||||
style={{ width: '14px', height: '14px', top: 0, right: 0 }}
|
||||
>
|
||||
<AlertCircle size={10} className="text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="icon-md shrink-0 overflow-hidden rounded-full"
|
||||
style={{ width: 20, height: 20 }}
|
||||
>
|
||||
<img
|
||||
src={iconURL}
|
||||
alt={groupName}
|
||||
className="h-full w-full object-cover"
|
||||
onError={handleImageError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GroupIcon);
|
||||
Loading…
Add table
Add a link
Reference in a new issue