"use client"; import { useAtom, useAtomValue } from "jotai"; import { Check, ChevronDown, Cpu, Search, Settings2, Zap } from "lucide-react"; import { useRouter } from "next/navigation"; import type { UIEvent } from "react"; import { useCallback, useMemo, useState } from "react"; import { updateModelRolesMutationAtom } from "@/atoms/model-connections/model-connections-mutation.atoms"; import { globalModelConnectionsAtom, modelConnectionsAtom, modelRolesAtom, } from "@/atoms/model-connections/model-connections-query.atoms"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Drawer, DrawerContent, DrawerHandle, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Spinner } from "@/components/ui/spinner"; import type { ConnectionRead, ModelRead } from "@/contracts/types/model-connections.types"; import { useIsMobile } from "@/hooks/use-mobile"; import { getProviderIcon } from "@/lib/provider-icons"; import { cn } from "@/lib/utils"; import { providerDisplay } from "../settings/model-connections/provider-metadata"; interface ModelSelectorProps { searchSpaceId: number; className?: string; } type ChatModel = ModelRead & { connectionId: number; connectionLabel: string; connectionScope: string; provider: string; }; function modelName(model: ModelRead) { return (model.display_name || model.model_id).replace(/\s+\(free\)$/i, ""); } function connectionLabel(connection: ConnectionRead) { if (connection.scope === "GLOBAL") return "Global"; return providerDisplay(connection.provider).name; } function flattenChatModels(connections: ConnectionRead[]) { return connections.flatMap((connection) => connection.models .filter((model) => model.enabled && Boolean(model.supports_chat)) .map((model) => ({ ...model, connectionId: connection.id, connectionLabel: connectionLabel(connection), connectionScope: connection.scope, provider: connection.provider, })) ); } function isFreeGlobalModel(model: ChatModel) { return model.connectionScope === "GLOBAL" && model.billing_tier?.toLowerCase() === "free"; } function groupedModels(models: ChatModel[]) { return models.reduce>((groups, model) => { const key = model.connectionLabel; if (!groups[key]) groups[key] = []; groups[key].push(model); return groups; }, {}); } export function ModelSelector({ searchSpaceId, className, }: ModelSelectorProps) { const router = useRouter(); const isMobile = useIsMobile(); const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const [scrollPos, setScrollPos] = useState<"top" | "middle" | "bottom">("top"); const [{ data: globalConnections = [], isLoading: globalLoading }] = useAtom( globalModelConnectionsAtom ); const [{ data: connections = [], isLoading: connectionsLoading }] = useAtom(modelConnectionsAtom); const [{ data: roles }] = useAtom(modelRolesAtom); const updateRoles = useAtomValue(updateModelRolesMutationAtom); const chatModels = useMemo(() => { const normalized = search.trim().toLowerCase(); const models = flattenChatModels([...globalConnections, ...connections]); if (!normalized) return models; return models.filter((model) => [modelName(model), model.model_id, model.connectionLabel] .join(" ") .toLowerCase() .includes(normalized) ); }, [globalConnections, connections, search]); const selected = chatModels.find((model) => model.id === roles?.chat_model_id); const groups = groupedModels(chatModels); const loading = globalLoading || connectionsLoading; function selectModel(modelId: number) { updateRoles.mutate({ chat_model_id: modelId }); setOpen(false); } function manageModelConnections() { setOpen(false); router.push(`/dashboard/${searchSpaceId}/search-space-settings/models`); } const handleScroll = useCallback((event: UIEvent) => { const el = event.currentTarget; const atTop = el.scrollTop <= 2; const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2; setScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle"); }, []); const content = (
setSearch(event.target.value)} placeholder="Search chat models" className="h-8 border-0 bg-transparent pl-6 text-sm shadow-none" />
{loading ? (
) : Object.keys(groups).length === 0 ? (
No enabled chat models. Add or enable models in Settings.
) : ( Object.entries(groups).map(([connection, models]) => (
{connection}
{models.map((model) => ( ))}
)) )}
); const trigger = ( ); if (isMobile) { return ( {trigger} Select Chat Model {content} ); } return ( {trigger} {content} ); }