import { Combobox } from "@kobalte/core/combobox" import { createEffect, createMemo, createSignal } from "solid-js" import { providers, fetchProviders } from "../stores/sessions" import { ChevronDown, Star } from "lucide-solid" import type { Model } from "../types/session" import { useI18n } from "../lib/i18n" import { getLogger } from "../lib/logger" import { uiState, toggleFavoriteModelPreference } from "../stores/preferences" const log = getLogger("session") interface ModelSelectorProps { instanceId: string sessionId: string currentModel: { providerId: string; modelId: string } onModelChange: (model: { providerId: string; modelId: string }) => Promise } interface FlatModel extends Model { providerName: string key: string searchText: string } export default function ModelSelector(props: ModelSelectorProps) { const { t } = useI18n() const instanceProviders = () => providers().get(props.instanceId) || [] const [isOpen, setIsOpen] = createSignal(false) const [manualAll, setManualAll] = createSignal(false) const [explicitFavorites, setExplicitFavorites] = createSignal(false) const [autoFavoritesEligibleAtOpen, setAutoFavoritesEligibleAtOpen] = createSignal(false) const [searchDirty, setSearchDirty] = createSignal(false) const [initialQuery, setInitialQuery] = createSignal("") const [initialQueryReady, setInitialQueryReady] = createSignal(false) const [inputValue, setInputValue] = createSignal("") let triggerRef!: HTMLButtonElement let searchInputRef!: HTMLInputElement let listboxRef!: HTMLUListElement let suppressNextClose = false let wasFavoritesOnlyEnabled = false let wasCurrentModelFavorite = false createEffect(() => { if (instanceProviders().length === 0) { fetchProviders(props.instanceId).catch((error) => log.error("Failed to fetch providers", error)) } }) const allModels = createMemo(() => instanceProviders().flatMap((p) => p.models.map((m) => ({ ...m, providerName: p.name, key: `${m.providerId}/${m.id}`, searchText: `${m.name} ${p.name} ${m.providerId} ${m.id} ${m.providerId}/${m.id}`, })), ), ) const favoriteKeySet = createMemo(() => { const result = new Set() for (const item of uiState().models.favorites ?? []) { if (item.providerId && item.modelId) { result.add(`${item.providerId}/${item.modelId}`) } } return result }) const favoriteModels = createMemo(() => { const keys = favoriteKeySet() if (keys.size === 0) return [] return allModels().filter((m) => keys.has(m.key)) }) const hasFavorites = createMemo(() => favoriteModels().length > 0) const currentModelValue = createMemo(() => allModels().find((m) => m.providerId === props.currentModel.providerId && m.id === props.currentModel.modelId), ) const currentModelIsFavorite = createMemo(() => { const current = props.currentModel return favoriteKeySet().has(`${current.providerId}/${current.modelId}`) }) const currentModelKey = createMemo(() => { const current = props.currentModel return `${current.providerId}/${current.modelId}` }) const searchActive = createMemo(() => { if (!searchDirty()) return false const next = inputValue().trim() return next.length > 0 }) const favoritesOnlyEnabled = createMemo(() => { if (searchActive()) return false if (manualAll()) return false if (!hasFavorites()) return false return explicitFavorites() || autoFavoritesEligibleAtOpen() }) const visibleOptions = createMemo(() => { if (!favoritesOnlyEnabled()) { return allModels() } return favoriteModels() }) const handleChange = async (value: FlatModel | null) => { if (!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()) } createEffect(() => { if (isOpen()) { setManualAll(false) setExplicitFavorites(false) setAutoFavoritesEligibleAtOpen(hasFavorites() && currentModelIsFavorite()) setSearchDirty(false) setInitialQuery("") setInputValue("") setInitialQueryReady(false) setTimeout(() => { const seeded = searchInputRef?.value ?? "" setInitialQuery(seeded) setInputValue(seeded) setInitialQueryReady(true) searchInputRef?.focus() searchInputRef?.select() }, 100) } else { setInitialQueryReady(false) setSearchDirty(false) setAutoFavoritesEligibleAtOpen(false) } }) createEffect(() => { if (!isOpen()) { wasFavoritesOnlyEnabled = favoritesOnlyEnabled() wasCurrentModelFavorite = currentModelIsFavorite() return } const nowFavoritesOnlyEnabled = favoritesOnlyEnabled() const nowCurrentModelFavorite = currentModelIsFavorite() if (wasFavoritesOnlyEnabled && !nowFavoritesOnlyEnabled && wasCurrentModelFavorite && !nowCurrentModelFavorite) { setTimeout(() => { const key = currentModelKey() const target = listboxRef?.querySelector(`[data-key="${key}"]`) as HTMLElement | null target?.scrollIntoView({ block: "nearest" }) }, 0) } wasFavoritesOnlyEnabled = nowFavoritesOnlyEnabled wasCurrentModelFavorite = nowCurrentModelFavorite }) const handleSearchInput = (event: InputEvent & { currentTarget: HTMLInputElement }) => { const next = event.currentTarget.value setInputValue(next) if (!initialQueryReady()) return if (searchDirty()) return if (next !== initialQuery()) { setSearchDirty(true) } } const preventListboxPress = (event: PointerEvent | MouseEvent) => { event.preventDefault() event.stopImmediatePropagation?.() event.stopPropagation() suppressNextClose = true setTimeout(() => { suppressNextClose = false }, 0) } const toggleFavoritesOnly = () => { if (!hasFavorites()) return if (searchActive()) return if (favoritesOnlyEnabled()) { setManualAll(true) setExplicitFavorites(false) setAutoFavoritesEligibleAtOpen(false) return } setExplicitFavorites(true) setManualAll(false) } const showAllModels = () => { setManualAll(true) setExplicitFavorites(false) setAutoFavoritesEligibleAtOpen(false) setTimeout(() => searchInputRef?.focus(), 0) } return ( ) }