Chat Dropdown: reusable parts. #955

This commit is contained in:
Enrico Ros
2026-02-03 17:30:34 -08:00
parent 94b68ebefa
commit f32d991413
2 changed files with 113 additions and 0 deletions
@@ -0,0 +1,51 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { IconButton } from '@mui/joy';
import { DebouncedInputMemo } from '~/common/components/DebouncedInput';
import { StarredState } from '~/common/components/StarIcons';
const _styles = {
filterBox: {
m: 1.5,
mb: 1,
backgroundColor: 'background.level1',
'&:focus-within': { backgroundColor: 'background.popup' },
},
} as const satisfies Record<string, SxProps>;
/**
* Model Selection Dropdowns: Shared search input with starred filter toggle
*/
export const LLMSearchFilterInput = React.memo(function LLMSearchFilterInput(props: {
size?: 'sm' | 'md' | 'lg',
llmsCount: number,
onSearch: (search: string | null) => void,
onStarredToggle?: () => void, // if provided, shows the starred filter button
showStarredOnly?: boolean,
}) {
return (
<DebouncedInputMemo
size={props.size}
aggressiveRefocus
debounceTimeout={300}
onDebounce={props.onSearch}
placeholder={`Search ${props.llmsCount} models...`}
startDecorator={props.onStarredToggle ? (
<IconButton
size='sm'
variant='plain'
aria-label='Filter starred models'
onClick={props.onStarredToggle}
>
<StarredState isStarred={!!props.showStarredOnly} />
{/*<StarIconUnstyled isStarred={showStarredOnly} />*/}
</IconButton>
) : undefined}
sx={_styles.filterBox}
/>
);
});
@@ -0,0 +1,62 @@
import { findModelVendor } from '~/modules/llms/vendors/vendors.registry';
import type { DLLM, DLLMId } from '../llms.types';
import type { DModelsServiceId } from '../llms.service.types';
import { findModelsServiceOrNull } from '../store-llms';
import { isLLMVisible } from '../llms.types';
/**
* Filter LLMs for dropdown display.
* Always includes the current model, respects starred/search/visibility filters.
*/
export function filterLLMsForDropdown(
llms: ReadonlyArray<DLLM>,
options: {
currentModelId?: DLLMId | null,
searchString?: string | null,
starredOnly?: boolean,
},
): DLLM[] {
const lcSearch = options.searchString?.toLowerCase();
return llms.filter(llm => {
// Always include the currently selected model
if (options.currentModelId && llm.id === options.currentModelId) return true;
// Filter by starred status
if (options.starredOnly && !llm.userStarred) return false;
// Filter by search string
if (lcSearch && !llm.label.toLowerCase().includes(lcSearch)) return false;
// Show visible models, or all if actively searching
return lcSearch ? true : isLLMVisible(llm);
});
}
export interface LLMServiceGroup {
serviceId: DModelsServiceId;
serviceLabel: string;
models: DLLM[];
}
/**
* Group LLMs by service, resolving service display labels.
*/
export function groupLLMsByService(llms: ReadonlyArray<DLLM>): LLMServiceGroup[] {
const groups: LLMServiceGroup[] = [];
let currentGroup: LLMServiceGroup | null = null;
for (const llm of llms) {
if (!currentGroup || currentGroup.serviceId !== llm.sId) {
const vendor = findModelVendor(llm.vId);
const serviceLabel = findModelsServiceOrNull(llm.sId)?.label || vendor?.name || llm.sId;
currentGroup = { serviceId: llm.sId, serviceLabel, models: [] };
groups.push(currentGroup);
}
currentGroup.models.push(llm);
}
return groups;
}