Add searchable model picker

This commit is contained in:
武阳
2026-05-08 11:56:30 +08:00
parent e4c3b973d4
commit c8ea2bad6d
+51 -17
View File
@@ -27,6 +27,7 @@ import {
MoreHorizontalIcon, MoreHorizontalIcon,
PencilIcon, PencilIcon,
RefreshCwIcon, RefreshCwIcon,
SearchIcon,
SquareIcon, SquareIcon,
WrenchIcon, WrenchIcon,
} from "lucide-react"; } from "lucide-react";
@@ -92,6 +93,8 @@ type ClawStoredSession = {
type ModelOption = { type ModelOption = {
id: string; id: string;
provider?: string;
model_type?: string;
}; };
type ContextBudget = { type ContextBudget = {
@@ -589,6 +592,19 @@ function ModelPickerButton({
setModel: (model: string) => Promise<void>; setModel: (model: string) => Promise<void>;
}; };
}) { }) {
const [query, setQuery] = useState("");
const normalizedQuery = query.trim().toLowerCase();
const filteredModels = useMemo(() => {
if (!normalizedQuery) return status.models;
return status.models.filter((model) => {
const provider = model.provider ?? modelProvider(model.id);
return (
model.id.toLowerCase().includes(normalizedQuery) ||
provider.toLowerCase().includes(normalizedQuery)
);
});
}, [normalizedQuery, status.models]);
return ( return (
<PopoverPrimitive.Root> <PopoverPrimitive.Root>
<PopoverPrimitive.Trigger asChild> <PopoverPrimitive.Trigger asChild>
@@ -608,28 +624,46 @@ function ModelPickerButton({
side="top" side="top"
align="end" align="end"
sideOffset={10} sideOffset={10}
className="z-50 w-72 rounded-2xl border bg-popover p-2 text-popover-foreground shadow-lg outline-none" className="z-50 w-[min(36rem,calc(100vw-2rem))] rounded-2xl border bg-popover p-2 text-popover-foreground shadow-lg outline-none"
> >
<div className="px-2 py-1.5 font-medium text-muted-foreground text-xs"> <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">
</div> </div>
<div className="relative mb-2">
<SearchIcon className="-translate-y-1/2 pointer-events-none absolute top-1/2 left-2.5 size-3.5 text-muted-foreground" />
<input
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="搜索模型或来源"
className="h-8 w-full rounded-lg border bg-background pr-2.5 pl-8 text-sm outline-none transition-colors placeholder:text-muted-foreground focus:border-ring"
/>
</div>
{status.models.length ? ( {status.models.length ? (
status.models.map((model) => ( <div className="h-[min(24rem,calc(100vh-12rem))] overflow-y-auto pr-1">
<button {filteredModels.length ? (
key={model.id} filteredModels.map((model) => (
type="button" <button
className="flex h-8 w-full items-center justify-between gap-2 rounded-lg px-2 text-left text-sm hover:bg-muted" key={model.id}
onClick={() => status.setModel(model.id)} type="button"
> className="flex h-8 w-full items-center justify-between gap-2 rounded-lg px-2 text-left text-sm hover:bg-muted"
<span className="min-w-0 flex-1 truncate">{model.id}</span> onClick={() => status.setModel(model.id)}
<span className="shrink-0 text-muted-foreground text-xs"> >
{modelProvider(model.id)} <span className="min-w-0 flex-1 truncate">{model.id}</span>
</span> <span className="shrink-0 text-muted-foreground text-xs">
{model.id === status.model ? ( {model.provider ?? modelProvider(model.id)}
<CheckIcon className="size-3.5 shrink-0" /> </span>
) : null} {model.id === status.model ? (
</button> <CheckIcon className="size-3.5 shrink-0" />
)) ) : null}
</button>
))
) : (
<div className="px-2 py-8 text-center text-muted-foreground text-sm">
</div>
)}
</div>
) : ( ) : (
<div className="px-2 py-2 text-muted-foreground text-sm"> <div className="px-2 py-2 text-muted-foreground text-sm">