diff --git a/src/common/stores/llms/components/LLMSearchFilterInput.tsx b/src/common/stores/llms/components/LLMSearchFilterInput.tsx new file mode 100644 index 000000000..c9864dc6c --- /dev/null +++ b/src/common/stores/llms/components/LLMSearchFilterInput.tsx @@ -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; + + +/** + * 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 ( + + + {/**/} + + ) : undefined} + sx={_styles.filterBox} + /> + ); +}); diff --git a/src/common/stores/llms/components/llms.dropdown.utils.ts b/src/common/stores/llms/components/llms.dropdown.utils.ts new file mode 100644 index 000000000..eec24b738 --- /dev/null +++ b/src/common/stores/llms/components/llms.dropdown.utils.ts @@ -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, + 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): 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; +}