diff --git a/src/nodes/shared/model-select.tsx b/src/nodes/shared/model-select.tsx index b0854d7..6f08c82 100644 --- a/src/nodes/shared/model-select.tsx +++ b/src/nodes/shared/model-select.tsx @@ -1,8 +1,7 @@ "use client"; -import { useState, useRef, useEffect } from "react"; -import { ChevronDown, Check, Sparkles, Lock, Loader2 } from "lucide-react"; +import { useState, useRef, useEffect, useMemo } from "react"; +import { ChevronDown, Check, Sparkles, Lock, Loader2, Search } from "lucide-react"; import { cn } from "@/lib/utils"; -import { ScrollArea } from "@/components/ui/scroll-area"; import { SubAgentModel, MODEL_DISPLAY_NAMES, MODEL_COST_MULTIPLIER } from "@/nodes/agent/enums"; import { useModels } from "@/hooks/use-models"; @@ -40,31 +39,13 @@ interface ModelSelectProps { export function ModelSelect({ value, onChange, hideInherit }: ModelSelectProps) { const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const [highlightedIndex, setHighlightedIndex] = useState(-1); const containerRef = useRef(null); + const searchInputRef = useRef(null); + const listRef = useRef(null); const { groups, isLoading, isDisabled } = useModels(); - // Close on outside click - useEffect(() => { - if (!open) return; - function handleClick(e: MouseEvent) { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { - setOpen(false); - } - } - document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, [open]); - - // Close on Escape - useEffect(() => { - if (!open) return; - function handleKey(e: KeyboardEvent) { - if (e.key === "Escape") setOpen(false); - } - document.addEventListener("keydown", handleKey); - return () => document.removeEventListener("keydown", handleKey); - }, [open]); - // Resolve display name — prefer API data, fall back to static map, then raw value const resolveDisplayName = (modelValue: string): string => { if (!modelValue) return MODEL_DISPLAY_NAMES[SubAgentModel.Inherit]; @@ -84,18 +65,115 @@ export function ModelSelect({ value, onChange, hideInherit }: ModelSelectProps) }; const displayName = resolveDisplayName(value); + const normalizedQuery = query.trim().toLowerCase(); + + const filteredGroups = useMemo(() => { + if (!normalizedQuery) return groups; + return groups + .map((group) => ({ + ...group, + models: group.models.filter((m) => { + const haystack = `${group.label} ${m.displayName} ${m.value}`.toLowerCase(); + return haystack.includes(normalizedQuery); + }), + })) + .filter((group) => group.models.length > 0); + }, [groups, normalizedQuery]); + + const getVisibleOptionsForQuery = (nextQuery: string) => { + const normalized = nextQuery.trim().toLowerCase(); + const options: string[] = []; + + const nextShowInherit = !hideInherit && (!normalized || MODEL_DISPLAY_NAMES[SubAgentModel.Inherit].toLowerCase().includes(normalized)); + if (nextShowInherit) options.push(SubAgentModel.Inherit); + + for (const group of groups) { + for (const model of group.models) { + const haystack = `${group.label} ${model.displayName} ${model.value}`.toLowerCase(); + if (!normalized || haystack.includes(normalized)) { + options.push(model.value); + } + } + } + + return options; + }; + + const showInherit = !hideInherit && (!normalizedQuery || MODEL_DISPLAY_NAMES[SubAgentModel.Inherit].toLowerCase().includes(normalizedQuery)); + + const visibleOptions = getVisibleOptionsForQuery(query); + + const closeDropdown = () => { + setOpen(false); + setQuery(""); + setHighlightedIndex(-1); + }; + + const selectModel = (nextValue: string) => { + onChange(nextValue); + closeDropdown(); + }; // Find which group the selected model belongs to const selectedGroup = groups.find((g) => g.models.some((m) => m.value === value) ); + // Close on outside click + useEffect(() => { + if (!open) return; + function handleClick(e: MouseEvent) { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + closeDropdown(); + } + } + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, [open]); + + // Close on Escape + useEffect(() => { + if (!open) return; + function handleKey(e: KeyboardEvent) { + if (e.key === "Escape") { + closeDropdown(); + } + } + document.addEventListener("keydown", handleKey); + return () => document.removeEventListener("keydown", handleKey); + }, [open]); + + useEffect(() => { + if (!open) return; + const id = requestAnimationFrame(() => searchInputRef.current?.focus()); + return () => cancelAnimationFrame(id); + }, [open]); + + useEffect(() => { + if (!open || highlightedIndex < 0) return; + const id = requestAnimationFrame(() => { + const el = listRef.current?.querySelector(`[data-option-index="${highlightedIndex}"]`); + el?.scrollIntoView({ block: "nearest" }); + }); + return () => cancelAnimationFrame(id); + }, [open, highlightedIndex]); + return (
{/* Trigger */}
)} - + + {!isLoading && groups.length > 0 && filteredGroups.length === 0 && normalizedQuery && ( +
+ No models match “{query.trim()}” +
+ )} + )}