mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
Merge 917f60c83d into 1143f73f59
This commit is contained in:
commit
b3e5f3efaf
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);
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue