diff --git a/packages/ui/src/components/model-selector.tsx b/packages/ui/src/components/model-selector.tsx index 6f5c5955..363bfe79 100644 --- a/packages/ui/src/components/model-selector.tsx +++ b/packages/ui/src/components/model-selector.tsx @@ -8,7 +8,6 @@ import { getLogger } from "../lib/logger" import { uiState, toggleFavoriteModelPreference } from "../stores/preferences" const log = getLogger("session") - interface ModelSelectorProps { instanceId: string sessionId: string @@ -22,6 +21,26 @@ interface FlatModel extends Model { searchText: string } +interface ModelGroup { + providerId: string + providerName: string + models: FlatModel[] +} + +interface ProviderHeaderOption { + type: "header" + key: string + providerId: string + providerName: string + searchText: string +} + +type PickerOption = FlatModel | ProviderHeaderOption + +const compareIds = (left: string, right: string) => left.localeCompare(right, undefined, { sensitivity: "base" }) + +const isProviderHeaderOption = (option: PickerOption): option is ProviderHeaderOption => "type" in option && option.type === "header" + export default function ModelSelector(props: ModelSelectorProps) { const { t } = useI18n() const instanceProviders = () => providers().get(props.instanceId) || [] @@ -57,6 +76,16 @@ export default function ModelSelector(props: ModelSelectorProps) { ), ) + const sortedModels = createMemo(() => + [...allModels()].sort((left, right) => { + const providerComparison = compareIds(left.providerId, right.providerId) + if (providerComparison !== 0) return providerComparison + const nameComparison = compareIds(left.name, right.name) + if (nameComparison !== 0) return nameComparison + return compareIds(left.id, right.id) + }), + ) + const favoriteKeySet = createMemo(() => { const result = new Set() for (const item of uiState().models.favorites ?? []) { @@ -70,7 +99,7 @@ export default function ModelSelector(props: ModelSelectorProps) { const favoriteModels = createMemo(() => { const keys = favoriteKeySet() if (keys.size === 0) return [] - return allModels().filter((m) => keys.has(m.key)) + return sortedModels().filter((m) => keys.has(m.key)) }) const hasFavorites = createMemo(() => favoriteModels().length > 0) @@ -104,20 +133,46 @@ export default function ModelSelector(props: ModelSelectorProps) { const visibleOptions = createMemo(() => { if (!favoritesOnlyEnabled()) { - return allModels() + return sortedModels() } return favoriteModels() }) - const handleChange = async (value: FlatModel | null) => { - if (!value) return + const groupedVisibleOptions = createMemo(() => { + const query = searchActive() ? inputValue().trim().toLowerCase() : "" + const groups = new Map() + for (const model of visibleOptions()) { + if (query && !model.searchText.toLowerCase().includes(query)) continue + const existing = groups.get(model.providerId) + if (existing) { + existing.models.push(model) + } else { + groups.set(model.providerId, { providerId: model.providerId, providerName: model.providerName, models: [model] }) + } + } + + return Array.from(groups.values()) + }) + + const pickerOptions = createMemo(() => + groupedVisibleOptions().flatMap((group) => [ + { + type: "header" as const, + key: `provider:${group.providerId}`, + providerId: group.providerId, + providerName: group.providerName, + searchText: `${group.providerName} ${group.providerId}`, + }, + ...group.models, + ]), + ) + + const handleChange = async (value: PickerOption | null) => { + if (!value || isProviderHeaderOption(value)) return await props.onModelChange({ providerId: value.providerId, modelId: value.id }) } - const customFilter = (option: FlatModel, rawInput: string) => { - if (!searchDirty()) return true - return option.searchText.toLowerCase().includes(rawInput.toLowerCase()) - } + const customFilter = () => true createEffect(() => { if (isOpen()) { @@ -209,7 +264,7 @@ export default function ModelSelector(props: ModelSelectorProps) { return (