diff --git a/web/src/App.tsx b/web/src/App.tsx index 5c288fc..863c20f 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -119,9 +119,8 @@ function loadStoredModelPreferences() { } } -function pickProviderModel(options: string[], preferred: string | null, fallback: string | null = null) { - if (fallback && options.includes(fallback)) return fallback; - if (preferred && options.includes(preferred)) return preferred; +function pickProviderModel(options: string[], preferred: string | null) { + if (preferred?.trim()) return preferred.trim(); return options[0] ?? ""; } @@ -197,34 +196,46 @@ type ModelComboboxProps = { function ModelCombobox({ options, value, onChange, disabled = false }: ModelComboboxProps) { const [open, setOpen] = useState(false); - const [query, setQuery] = useState(""); + const [draftValue, setDraftValue] = useState(value); const rootRef = useRef(null); const inputRef = useRef(null); + const normalizedDraftValue = draftValue.trim(); const filteredOptions = useMemo(() => { - const needle = query.trim().toLowerCase(); + const needle = normalizedDraftValue.toLowerCase(); if (!needle) return options; return options.filter((option) => option.toLowerCase().includes(needle)); - }, [options, query]); + }, [normalizedDraftValue, options]); + const hasExactOption = options.includes(normalizedDraftValue); + + useEffect(() => { + if (open) return; + setDraftValue(value); + }, [open, value]); useEffect(() => { if (!open) return; inputRef.current?.focus(); }, [open]); + const commitDraftValue = () => { + onChange(normalizedDraftValue); + setDraftValue(normalizedDraftValue); + setOpen(false); + }; + useEffect(() => { if (!open) return; const handlePointerDown = (event: PointerEvent) => { if (rootRef.current?.contains(event.target as Node)) return; - setOpen(false); - setQuery(""); + commitDraftValue(); }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key !== "Escape") return; setOpen(false); - setQuery(""); + setDraftValue(value); }; window.addEventListener("pointerdown", handlePointerDown); @@ -233,32 +244,63 @@ function ModelCombobox({ options, value, onChange, disabled = false }: ModelComb window.removeEventListener("pointerdown", handlePointerDown); window.removeEventListener("keydown", handleKeyDown); }; - }, [open]); + }, [commitDraftValue, open, value]); return (
- +
+ { + if (disabled) return; + setDraftValue(value); + setOpen(true); + }} + onInput={(event) => { + setDraftValue(event.currentTarget.value); + setOpen(true); + }} + onKeyDown={(event) => { + if (event.key !== "Enter") return; + event.preventDefault(); + commitDraftValue(); + }} + className="h-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground" + placeholder="Select or type model" + disabled={disabled} + /> + +
{open ? (
- setQuery(event.currentTarget.value)} - className="mb-1 h-8 w-full rounded-sm border border-input bg-background px-2 text-sm outline-none" - placeholder="Filter models" - />
+ {normalizedDraftValue && !hasExactOption ? ( + + ) : null} {filteredOptions.length ? ( filteredOptions.map((option) => ( )) - ) : ( + ) : !normalizedDraftValue ? (

No models found

- )} + ) : null}
) : null} @@ -545,10 +587,11 @@ export default function App() { const providerModelOptions = useMemo(() => getModelOptions(modelCatalog, provider), [modelCatalog, provider]); useEffect(() => { + if (model.trim()) return; setModel((current) => { - return pickProviderModel(providerModelOptions, providerModelPreferences[provider], current); + return current.trim() || pickProviderModel(providerModelOptions, providerModelPreferences[provider]); }); - }, [provider, providerModelOptions, providerModelPreferences]); + }, [model, provider, providerModelOptions, providerModelPreferences]); useEffect(() => { if (typeof window === "undefined") return; @@ -1233,7 +1276,7 @@ export default function App() { const nextProvider = event.currentTarget.value as Provider; setProvider(nextProvider); const options = getModelOptions(modelCatalog, nextProvider); - setModel((current) => pickProviderModel(options, providerModelPreferences[nextProvider], current)); + setModel(pickProviderModel(options, providerModelPreferences[nextProvider])); }} disabled={isSending} > @@ -1244,12 +1287,13 @@ export default function App() { { - setModel(nextModel); + const normalizedModel = nextModel.trim(); + setModel(normalizedModel); setProviderModelPreferences((current) => ({ ...current, - [provider]: nextModel, + [provider]: normalizedModel || null, })); }} />