mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-05 17:21:50 +01:00
✨ feat: Add subfolder support for model specs (#9165)
- Add optional `folder` field to TModelSpec type and schema - Create ModelSpecFolder component for hierarchical display - Implement expand/collapse functionality for folders - Update search results to show folder context - Sort folders alphabetically and specs by order/label - Maintain backward compatibility (specs without folders appear at root) This enhancement allows organizing model specs into categories/folders for better organization and improved user experience when dealing with many model configurations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
543b617e1c
commit
4fe600990c
7 changed files with 439 additions and 3 deletions
|
|
@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
|
|||
import type { ModelSelectorProps } from '~/common';
|
||||
import { ModelSelectorProvider, useModelSelectorContext } from './ModelSelectorContext';
|
||||
import { ModelSelectorChatProvider } from './ModelSelectorChatContext';
|
||||
import { renderModelSpecs, renderEndpoints, renderSearchResults } from './components';
|
||||
import { renderModelSpecsWithFolders, renderEndpoints, renderSearchResults } from './components';
|
||||
import { getSelectedIcon, getDisplayValue } from './utils';
|
||||
import { CustomMenu as Menu } from './CustomMenu';
|
||||
import DialogManager from './DialogManager';
|
||||
|
|
@ -86,7 +86,7 @@ function ModelSelectorContent() {
|
|||
renderSearchResults(searchResults, localize, searchValue)
|
||||
) : (
|
||||
<>
|
||||
{renderModelSpecs(modelSpecs, selectedValues.modelSpec || '')}
|
||||
{renderModelSpecsWithFolders(modelSpecs, selectedValues.modelSpec || '')}
|
||||
{renderEndpoints(mappedEndpoints ?? [])}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
import React, { useState } from 'react';
|
||||
import { ChevronDown, ChevronRight, Folder, FolderOpen } from 'lucide-react';
|
||||
import type { TModelSpec } from 'librechat-data-provider';
|
||||
import { ModelSpecItem } from './ModelSpecItem';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface ModelSpecFolderProps {
|
||||
folderName: string;
|
||||
specs: TModelSpec[];
|
||||
selectedSpec: string;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export function ModelSpecFolder({
|
||||
folderName,
|
||||
specs,
|
||||
selectedSpec,
|
||||
level = 0
|
||||
}: ModelSpecFolderProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
const handleToggle = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
const indent = level * 16;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-1 rounded-lg px-2 py-1.5 text-sm hover:bg-surface-hover',
|
||||
'text-text-secondary transition-colors'
|
||||
)}
|
||||
style={{ paddingLeft: `${8 + indent}px` }}
|
||||
>
|
||||
<span className="flex-shrink-0">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
)}
|
||||
</span>
|
||||
<span className="flex-shrink-0">
|
||||
{isExpanded ? (
|
||||
<FolderOpen className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Folder className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate text-left font-medium">{folderName}</span>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="mt-0.5">
|
||||
{specs.map((spec) => (
|
||||
<div key={spec.name} style={{ paddingLeft: `${indent}px` }}>
|
||||
<ModelSpecItem spec={spec} isSelected={selectedSpec === spec.name} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface GroupedSpecs {
|
||||
[folder: string]: TModelSpec[];
|
||||
}
|
||||
|
||||
export function renderModelSpecsWithFolders(specs: TModelSpec[], selectedSpec: string) {
|
||||
if (!specs || specs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Group specs by folder
|
||||
const grouped: GroupedSpecs = {};
|
||||
const rootSpecs: TModelSpec[] = [];
|
||||
|
||||
specs.forEach((spec) => {
|
||||
if (spec.folder) {
|
||||
if (!grouped[spec.folder]) {
|
||||
grouped[spec.folder] = [];
|
||||
}
|
||||
grouped[spec.folder].push(spec);
|
||||
} else {
|
||||
rootSpecs.push(spec);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort folders alphabetically
|
||||
const sortedFolders = Object.keys(grouped).sort((a, b) =>
|
||||
a.toLowerCase().localeCompare(b.toLowerCase())
|
||||
);
|
||||
|
||||
// Sort specs within each folder by order or label
|
||||
sortedFolders.forEach(folder => {
|
||||
grouped[folder].sort((a, b) => {
|
||||
if (a.order !== undefined && b.order !== undefined) {
|
||||
return a.order - b.order;
|
||||
}
|
||||
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
||||
});
|
||||
});
|
||||
|
||||
// Sort root specs
|
||||
rootSpecs.sort((a, b) => {
|
||||
if (a.order !== undefined && b.order !== undefined) {
|
||||
return a.order - b.order;
|
||||
}
|
||||
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Render folders first */}
|
||||
{sortedFolders.map((folder) => (
|
||||
<ModelSpecFolder
|
||||
key={folder}
|
||||
folderName={folder}
|
||||
specs={grouped[folder]}
|
||||
selectedSpec={selectedSpec}
|
||||
/>
|
||||
))}
|
||||
{/* Render root level specs */}
|
||||
{rootSpecs.map((spec) => (
|
||||
<ModelSpecItem key={spec.name} spec={spec} isSelected={selectedSpec === spec.name} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -67,7 +67,12 @@ export function SearchResults({ results, localize, searchValue }: SearchResultsP
|
|||
</div>
|
||||
)}
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<span className="truncate text-left">{spec.label}</span>
|
||||
<span className="truncate text-left">
|
||||
{spec.folder && (
|
||||
<span className="text-xs text-text-tertiary">{spec.folder} / </span>
|
||||
)}
|
||||
{spec.label}
|
||||
</span>
|
||||
{spec.description && (
|
||||
<span className="break-words text-xs font-normal">{spec.description}</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from './ModelSpecItem';
|
||||
export * from './ModelSpecFolder';
|
||||
export * from './EndpointModelItem';
|
||||
export * from './EndpointItem';
|
||||
export * from './SearchResults';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue