"use client"; import { useAtomValue } from "jotai"; import { Bot, Check, ChevronDown, Edit3, ImageIcon, Plus, Zap } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; import { globalImageGenConfigsAtom, imageGenConfigsAtom, } from "@/atoms/image-gen-config/image-gen-config-query.atoms"; import { updateLLMPreferencesMutationAtom } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms"; import { globalNewLLMConfigsAtom, llmPreferencesAtom, newLLMConfigsAtom, } from "@/atoms/new-llm-config/new-llm-config-query.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { GlobalImageGenConfig, GlobalNewLLMConfig, ImageGenerationConfig, NewLLMConfigPublic, } from "@/contracts/types/new-llm-config.types"; import { getProviderIcon } from "@/lib/provider-icons"; import { cn } from "@/lib/utils"; interface ModelSelectorProps { onEditLLM: (config: NewLLMConfigPublic | GlobalNewLLMConfig, isGlobal: boolean) => void; onAddNewLLM: () => void; onEditImage?: (config: ImageGenerationConfig | GlobalImageGenConfig, isGlobal: boolean) => void; onAddNewImage?: () => void; className?: string; } export function ModelSelector({ onEditLLM, onAddNewLLM, onEditImage, onAddNewImage, className, }: ModelSelectorProps) { const [open, setOpen] = useState(false); const [activeTab, setActiveTab] = useState<"llm" | "image">("llm"); const [llmSearchQuery, setLlmSearchQuery] = useState(""); const [imageSearchQuery, setImageSearchQuery] = useState(""); // LLM data const { data: llmUserConfigs, isLoading: llmUserLoading } = useAtomValue(newLLMConfigsAtom); const { data: llmGlobalConfigs, isLoading: llmGlobalLoading } = useAtomValue(globalNewLLMConfigsAtom); const { data: preferences, isLoading: prefsLoading } = useAtomValue(llmPreferencesAtom); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom); // Image data const { data: imageGlobalConfigs, isLoading: imageGlobalLoading } = useAtomValue(globalImageGenConfigsAtom); const { data: imageUserConfigs, isLoading: imageUserLoading } = useAtomValue(imageGenConfigsAtom); const isLoading = llmUserLoading || llmGlobalLoading || prefsLoading || imageGlobalLoading || imageUserLoading; // ─── LLM current config ─── const currentLLMConfig = useMemo(() => { if (!preferences) return null; const agentLlmId = preferences.agent_llm_id; if (agentLlmId === null || agentLlmId === undefined) return null; if (agentLlmId <= 0) { return llmGlobalConfigs?.find((c) => c.id === agentLlmId) ?? null; } return llmUserConfigs?.find((c) => c.id === agentLlmId) ?? null; }, [preferences, llmGlobalConfigs, llmUserConfigs]); const isLLMAutoMode = useMemo(() => { return currentLLMConfig && "is_auto_mode" in currentLLMConfig && currentLLMConfig.is_auto_mode; }, [currentLLMConfig]); // ─── Image current config ─── const currentImageConfig = useMemo(() => { if (!preferences) return null; const id = preferences.image_generation_config_id; if (id === null || id === undefined) return null; const globalMatch = imageGlobalConfigs?.find((c) => c.id === id); if (globalMatch) return globalMatch; return imageUserConfigs?.find((c) => c.id === id) ?? null; }, [preferences, imageGlobalConfigs, imageUserConfigs]); const isImageAutoMode = useMemo(() => { return ( currentImageConfig && "is_auto_mode" in currentImageConfig && currentImageConfig.is_auto_mode ); }, [currentImageConfig]); // ─── LLM filtering ─── const filteredLLMGlobal = useMemo(() => { if (!llmGlobalConfigs) return []; if (!llmSearchQuery) return llmGlobalConfigs; const q = llmSearchQuery.toLowerCase(); return llmGlobalConfigs.filter( (c) => c.name.toLowerCase().includes(q) || c.model_name.toLowerCase().includes(q) || c.provider.toLowerCase().includes(q) ); }, [llmGlobalConfigs, llmSearchQuery]); const filteredLLMUser = useMemo(() => { if (!llmUserConfigs) return []; if (!llmSearchQuery) return llmUserConfigs; const q = llmSearchQuery.toLowerCase(); return llmUserConfigs.filter( (c) => c.name.toLowerCase().includes(q) || c.model_name.toLowerCase().includes(q) || c.provider.toLowerCase().includes(q) ); }, [llmUserConfigs, llmSearchQuery]); const totalLLMModels = (llmGlobalConfigs?.length ?? 0) + (llmUserConfigs?.length ?? 0); // ─── Image filtering ─── const filteredImageGlobal = useMemo(() => { if (!imageGlobalConfigs) return []; if (!imageSearchQuery) return imageGlobalConfigs; const q = imageSearchQuery.toLowerCase(); return imageGlobalConfigs.filter( (c) => c.name.toLowerCase().includes(q) || c.model_name.toLowerCase().includes(q) || c.provider.toLowerCase().includes(q) ); }, [imageGlobalConfigs, imageSearchQuery]); const filteredImageUser = useMemo(() => { if (!imageUserConfigs) return []; if (!imageSearchQuery) return imageUserConfigs; const q = imageSearchQuery.toLowerCase(); return imageUserConfigs.filter( (c) => c.name.toLowerCase().includes(q) || c.model_name.toLowerCase().includes(q) || c.provider.toLowerCase().includes(q) ); }, [imageUserConfigs, imageSearchQuery]); const totalImageModels = (imageGlobalConfigs?.length ?? 0) + (imageUserConfigs?.length ?? 0); // ─── Handlers ─── const handleSelectLLM = useCallback( async (config: NewLLMConfigPublic | GlobalNewLLMConfig) => { if (currentLLMConfig?.id === config.id) { setOpen(false); return; } if (!searchSpaceId) { toast.error("No search space selected"); return; } try { await updatePreferences({ search_space_id: Number(searchSpaceId), data: { agent_llm_id: config.id }, }); toast.success(`Switched to ${config.name}`); setOpen(false); } catch (error) { console.error("Failed to switch model:", error); toast.error("Failed to switch model"); } }, [currentLLMConfig, searchSpaceId, updatePreferences] ); const handleEditLLMConfig = useCallback( (e: React.MouseEvent, config: NewLLMConfigPublic | GlobalNewLLMConfig, isGlobal: boolean) => { e.stopPropagation(); onEditLLM(config, isGlobal); setOpen(false); }, [onEditLLM] ); const handleSelectImage = useCallback( async (configId: number) => { if (currentImageConfig?.id === configId) { setOpen(false); return; } if (!searchSpaceId) { toast.error("No search space selected"); return; } try { await updatePreferences({ search_space_id: Number(searchSpaceId), data: { image_generation_config_id: configId }, }); toast.success("Image model updated"); setOpen(false); } catch { toast.error("Failed to switch image model"); } }, [currentImageConfig, searchSpaceId, updatePreferences] ); return ( setActiveTab(v as "llm" | "image")} className="w-full" >
LLM Image
{/* ─── LLM Tab ─── */} {totalLLMModels > 3 && (
)}

No models found

Try a different search term

{/* Global LLM Configs */} {filteredLLMGlobal.length > 0 && (
Global Models
{filteredLLMGlobal.map((config) => { const isSelected = currentLLMConfig?.id === config.id; const isAutoMode = "is_auto_mode" in config && config.is_auto_mode; return ( handleSelectLLM(config)} className={cn( "mx-2 rounded-lg mb-1 cursor-pointer group transition-all", "hover:bg-accent/50 dark:hover:bg-white/10", isSelected && "bg-accent/80 dark:bg-white/10", isAutoMode && "" )} >
{getProviderIcon(config.provider, { isAutoMode })}
{config.name} {isAutoMode && ( Recommended )} {isSelected && ( )}
{isAutoMode ? "Auto Mode" : config.model_name} {!isAutoMode && config.citations_enabled && ( Citations )}
{!isAutoMode && ( )}
); })}
)} {filteredLLMGlobal.length > 0 && filteredLLMUser.length > 0 && ( )} {/* User LLM Configs */} {filteredLLMUser.length > 0 && (
Your Configurations
{filteredLLMUser.map((config) => { const isSelected = currentLLMConfig?.id === config.id; return ( handleSelectLLM(config)} className={cn( "mx-2 rounded-lg mb-1 cursor-pointer group transition-all", "hover:bg-accent/50 dark:hover:bg-white/10", isSelected && "bg-accent/80 dark:bg-white/10" )} >
{getProviderIcon(config.provider)}
{config.name} {isSelected && ( )}
{config.model_name} {config.citations_enabled && ( Citations )}
); })}
)} {/* Add New LLM Config */}
{/* ─── Image Tab ─── */} {totalImageModels > 3 && (
)}

No image models found

{/* Global Image Configs */} {filteredImageGlobal.length > 0 && (
Global Image Models
{filteredImageGlobal.map((config) => { const isSelected = currentImageConfig?.id === config.id; const isAuto = "is_auto_mode" in config && config.is_auto_mode; return ( handleSelectImage(config.id)} className={cn( "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50 dark:hover:bg-white/10", isSelected && "bg-accent/80 dark:bg-white/10", isAuto && "" )} >
{getProviderIcon(config.provider, { isAutoMode: isAuto })}
{config.name} {isAuto && ( Recommended )} {isSelected && }
{isAuto ? "Auto Mode" : config.model_name}
{onEditImage && !isAuto && ( )}
); })}
)} {/* User Image Configs */} {filteredImageUser.length > 0 && ( <> {filteredImageGlobal.length > 0 && ( )}
Your Image Models
{filteredImageUser.map((config) => { const isSelected = currentImageConfig?.id === config.id; return ( handleSelectImage(config.id)} className={cn( "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50 dark:hover:bg-white/10", isSelected && "bg-accent/80 dark:bg-white/10" )} >
{getProviderIcon(config.provider)}
{config.name} {isSelected && ( )}
{config.model_name}
{onEditImage && ( )}
); })}
)} {/* Add New Image Config */} {onAddNewImage && (
)}
); }