Adds ability to manually enter model name

This commit is contained in:
2026-04-01 18:13:04 -07:00
parent be6641de24
commit a31c053298

View File

@@ -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<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(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 (
<div className="relative" ref={rootRef}>
<button
type="button"
className="flex h-9 min-w-56 items-center justify-between rounded-md border border-input bg-background px-2 text-sm"
onClick={() => {
if (disabled) return;
setOpen((current) => !current);
}}
disabled={disabled}
>
<span className="truncate text-left">{value || "Select model"}</span>
<ChevronDown className="ml-2 h-4 w-4 shrink-0 text-muted-foreground" />
</button>
<div className="flex h-9 min-w-56 items-center rounded-md border border-input bg-background px-2 text-sm">
<input
ref={inputRef}
value={draftValue}
onFocus={() => {
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}
/>
<button
type="button"
className="ml-2 shrink-0 text-muted-foreground disabled:opacity-50"
onClick={() => {
if (disabled) return;
if (open) {
commitDraftValue();
return;
}
setDraftValue(value);
setOpen(true);
}}
disabled={disabled}
aria-label="Toggle model options"
>
<ChevronDown className="h-4 w-4" />
</button>
</div>
{open ? (
<div className="absolute right-0 z-50 mt-1 w-full rounded-md border border-border bg-background p-1 shadow-md">
<input
ref={inputRef}
value={query}
onInput={(event) => 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"
/>
<div className="max-h-64 overflow-y-auto">
{normalizedDraftValue && !hasExactOption ? (
<button
type="button"
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-muted"
onClick={commitDraftValue}
>
<Check className={cn("h-4 w-4", normalizedDraftValue === value ? "opacity-100" : "opacity-0")} />
<span className="truncate">Use "{normalizedDraftValue}"</span>
</button>
) : null}
{filteredOptions.length ? (
filteredOptions.map((option) => (
<button
@@ -268,16 +310,16 @@ function ModelCombobox({ options, value, onChange, disabled = false }: ModelComb
onClick={() => {
onChange(option);
setOpen(false);
setQuery("");
setDraftValue(option);
}}
>
<Check className={cn("h-4 w-4", option === value ? "opacity-100" : "opacity-0")} />
<span className="truncate">{option}</span>
</button>
))
) : (
) : !normalizedDraftValue ? (
<p className="px-2 py-2 text-sm text-muted-foreground">No models found</p>
)}
) : null}
</div>
</div>
) : 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() {
<ModelCombobox
options={providerModelOptions}
value={model}
disabled={isSending || providerModelOptions.length === 0}
disabled={isSending}
onChange={(nextModel) => {
setModel(nextModel);
const normalizedModel = nextModel.trim();
setModel(normalizedModel);
setProviderModelPreferences((current) => ({
...current,
[provider]: nextModel,
[provider]: normalizedModel || null,
}));
}}
/>