This commit is contained in:
Odrec 2025-12-14 03:48:51 +01:00 committed by GitHub
commit b3e5f3efaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 102 additions and 8 deletions

View file

@ -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} />
));
}

View file

@ -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);

View file

@ -339,6 +339,10 @@ endpoints:
# - If 'group' matches an endpoint name (e.g., "openAI", "groq"), the spec appears nested under that endpoint
# - If 'group' is a custom name (doesn't match any endpoint), it creates a separate collapsible section
# - If 'group' is omitted, the spec appears as a standalone item at the top level
#
# The 'groupIcon' field sets an icon for custom groups:
# - Only needs to be set on one spec per group (first one is used)
# - Can be a URL or a built-in endpoint key (e.g., "openAI", "anthropic", "groq")
# modelSpecs:
# list:
# # Example 1: Nested under an endpoint (grouped with openAI endpoint)
@ -359,11 +363,12 @@ endpoints:
# endpoint: "groq"
# model: "llama3-70b-8192"
#
# # Example 3: Custom group (creates a separate collapsible section)
# # Example 3: Custom group with icon (creates a separate collapsible section)
# - name: "coding-assistant"
# label: "Coding Assistant"
# description: "Specialized for coding tasks"
# group: "my-assistants" # Custom string - doesn't match any endpoint, so creates its own group
# groupIcon: "https://example.com/icons/assistants.png" # Icon URL for the group
# preset:
# endpoint: "openAI"
# model: "gpt-4o"
@ -374,12 +379,22 @@ endpoints:
# label: "Writing Assistant"
# description: "Specialized for creative writing"
# group: "my-assistants" # Same custom group name - both specs appear in same section
# # No need to set groupIcon again - the first spec's icon is used
# preset:
# endpoint: "anthropic"
# model: "claude-sonnet-4"
# instructions: "You are a creative writing expert..."
#
# # Example 4: Standalone (no group - appears at top level)
# # Example 4: Custom group using built-in icon key
# - name: "fast-models"
# label: "Fast Response Model"
# group: "Fast Models"
# groupIcon: "groq" # Uses the built-in Groq icon
# preset:
# endpoint: "groq"
# model: "llama3-8b-8192"
#
# # Example 5: Standalone (no group - appears at top level)
# - name: "general-assistant"
# label: "General Assistant"
# description: "General purpose assistant"

View file

@ -22,6 +22,12 @@ export type TModelSpec = {
* - If omitted, the spec appears as a standalone item at the top level
*/
group?: string;
/**
* Optional icon URL for the group this spec belongs to.
* Only needs to be set on one spec per group - the first one found with a groupIcon will be used.
* Can be a URL or an endpoint name to use its icon.
*/
groupIcon?: string | EModelEndpoint;
showIconInMenu?: boolean;
showIconInHeader?: boolean;
iconURL?: string | EModelEndpoint; // Allow using project-included icons
@ -40,6 +46,7 @@ export const tModelSpecSchema = z.object({
default: z.boolean().optional(),
description: z.string().optional(),
group: z.string().optional(),
groupIcon: z.union([z.string(), eModelEndpointSchema]).optional(),
showIconInMenu: z.boolean().optional(),
showIconInHeader: z.boolean().optional(),
iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),