import { useEffect, useRef, useState } from "octane"; import { CaretUpDown, Check, MagnifyingGlass } from "@octanejs/phosphor-icons"; import { SearchField, Input as AriaInput } from "@octanejs/aria/components"; import { inputVariants } from "octane-kumo/components/input"; import { selectVariants } from "octane-kumo/components/select"; import { CommandPalette } from "octane-kumo/components/command-palette"; import { Popover } from "octane-kumo/components/popover"; import { MODEL_CATALOG, MODEL_PROVIDERS, modelLabel, type ModelProvider, type ModelConfiguration, } from "../../shared/model-providers"; const modelChoices = Object.entries(MODEL_CATALOG).flatMap( ([provider, models]) => models.map((model) => ({ value: `${provider}:${model}`, label: modelLabel(provider as ModelProvider, model), configuration: { provider, model } as ModelConfiguration, })), ); function choiceLabel(value: string) { return value === "default" ? "Installation default" : (modelChoices.find((choice) => choice.value === value)?.label ?? value); } export function ChatModelPicker({ value, disabled, providerReady, onChange, }: { value: ModelConfiguration | null; disabled: boolean; providerReady: (provider: ModelProvider) => boolean; onChange: (value: ModelConfiguration | null) => void; }) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const selected = value ? `${value.provider}:${value.model}` : "default"; const select = (configuration: ModelConfiguration | null) => { onChange(configuration); setOpen(false); }; return ( { setOpen(next); setQuery(""); }} > {value ? value.model.split("/").at(-1) : "Installation default"} choice.value)]} value={query} onValueChange={setQuery} itemToStringValue={choiceLabel} filter={(value, query) => choiceLabel(value) .toLowerCase() .includes(query.trim().toLowerCase()) } > setOpen(false)} /> select(null)}> Installation default {selected === "default" && ( {modelChoices.map((choice) => ( select(choice.configuration)} >
{choice.configuration.model} {!providerReady(choice.configuration.provider) && ( {MODEL_PROVIDERS[choice.configuration.provider].name}{" "} · Set up in Settings )}
{selected === choice.value && (
))}
No models found.
); } function ModelSearchInput({ onClose }: { onClose: () => void }) { const input = useRef(null); useEffect(() => { const field = input.current; const dialog = field?.closest(".chat-model-dropdown"); if (!field || !dialog) return; // The pinned ARIA adapter loses the active descendant when filtering replaces rows. const syncFocus = () => { const list = dialog.querySelector('[role="listbox"]'); const option = dialog.querySelector( '[role="option"][data-focused="true"]', ); if (list?.id) field.setAttribute("aria-controls", list.id); if (option?.id) field.setAttribute("aria-activedescendant", option.id); else field.removeAttribute("aria-activedescendant"); }; const observer = new MutationObserver(syncFocus); observer.observe(dialog, { subtree: true, childList: true, attributes: true, attributeFilter: ["data-focused"], }); syncFocus(); return () => observer.disconnect(); }, []); return ( ); }