2025-12-23 01:16:25 -08:00
"use client" ;
import { useAtomValue } from "jotai" ;
2026-04-13 16:10:29 -07:00
import {
Bot ,
Check ,
ChevronDown ,
2026-04-14 20:35:16 +05:30
ChevronLeft ,
ChevronRight ,
ChevronUp ,
2026-04-13 16:10:29 -07:00
Edit3 ,
ImageIcon ,
Layers ,
Plus ,
2026-04-14 21:26:00 -07:00
ScanEye ,
2026-04-13 16:10:29 -07:00
Search ,
Zap ,
} from "lucide-react" ;
2026-04-14 21:26:00 -07:00
import type React from "react" ;
2026-04-13 16:10:29 -07:00
import { Fragment , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
2025-12-23 01:16:25 -08:00
import { toast } from "sonner" ;
2026-02-10 17:20:42 +05:30
import {
globalImageGenConfigsAtom ,
imageGenConfigsAtom ,
} from "@/atoms/image-gen-config/image-gen-config-query.atoms" ;
2025-12-23 01:16:25 -08:00
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" ;
2026-04-07 20:47:17 +02:00
import {
globalVisionLLMConfigsAtom ,
visionLLMConfigsAtom ,
} from "@/atoms/vision-llm-config/vision-llm-config-query.atoms" ;
2025-12-23 01:16:25 -08:00
import { Badge } from "@/components/ui/badge" ;
import { Button } from "@/components/ui/button" ;
import {
2026-04-13 16:10:29 -07:00
Drawer ,
DrawerContent ,
DrawerHandle ,
DrawerHeader ,
DrawerTitle ,
DrawerTrigger ,
} from "@/components/ui/drawer" ;
2025-12-23 01:16:25 -08:00
import { Popover , PopoverContent , PopoverTrigger } from "@/components/ui/popover" ;
2026-01-25 15:23:45 +05:30
import { Spinner } from "@/components/ui/spinner" ;
2026-04-13 16:10:29 -07:00
import { Tooltip , TooltipContent , TooltipTrigger } from "@/components/ui/tooltip" ;
2025-12-23 01:16:25 -08:00
import type {
2026-02-10 17:20:42 +05:30
GlobalImageGenConfig ,
2025-12-23 01:16:25 -08:00
GlobalNewLLMConfig ,
2026-04-07 20:47:17 +02:00
GlobalVisionLLMConfig ,
2026-02-10 17:20:42 +05:30
ImageGenerationConfig ,
2025-12-23 01:16:25 -08:00
NewLLMConfigPublic ,
2026-04-07 20:47:17 +02:00
VisionLLMConfig ,
2025-12-23 01:16:25 -08:00
} from "@/contracts/types/new-llm-config.types" ;
2026-04-13 16:10:29 -07:00
import { useIsMobile } from "@/hooks/use-mobile" ;
2026-02-09 20:48:42 +05:30
import { getProviderIcon } from "@/lib/provider-icons" ;
2026-02-10 17:20:42 +05:30
import { cn } from "@/lib/utils" ;
2025-12-23 01:16:25 -08:00
2026-04-13 16:10:29 -07:00
// ─── Helpers ────────────────────────────────────────────────────────
const PROVIDER_NAMES : Record < string , string > = {
OPENAI : "OpenAI" ,
ANTHROPIC : "Anthropic" ,
GOOGLE : "Google" ,
AZURE : "Azure" ,
AZURE_OPENAI : "Azure OpenAI" ,
AWS_BEDROCK : "AWS Bedrock" ,
BEDROCK : "Bedrock" ,
DEEPSEEK : "DeepSeek" ,
MISTRAL : "Mistral" ,
COHERE : "Cohere" ,
2026-04-14 20:35:16 +05:30
GITHUB_MODELS : "GitHub Models" ,
2026-04-13 16:10:29 -07:00
GROQ : "Groq" ,
OLLAMA : "Ollama" ,
TOGETHER_AI : "Together AI" ,
FIREWORKS_AI : "Fireworks AI" ,
REPLICATE : "Replicate" ,
HUGGINGFACE : "HuggingFace" ,
PERPLEXITY : "Perplexity" ,
XAI : "xAI" ,
OPENROUTER : "OpenRouter" ,
CEREBRAS : "Cerebras" ,
SAMBANOVA : "SambaNova" ,
VERTEX_AI : "Vertex AI" ,
MINIMAX : "MiniMax" ,
MOONSHOT : "Moonshot" ,
ZHIPU : "Zhipu" ,
DEEPINFRA : "DeepInfra" ,
CLOUDFLARE : "Cloudflare" ,
DATABRICKS : "Databricks" ,
NSCALE : "NScale" ,
RECRAFT : "Recraft" ,
XINFERENCE : "XInference" ,
CUSTOM : "Custom" ,
AI21 : "AI21" ,
ALIBABA_QWEN : "Qwen" ,
ANYSCALE : "Anyscale" ,
COMETAPI : "CometAPI" ,
} ;
// Provider keys valid per model type, matching backend enums
// (LiteLLMProvider, ImageGenProvider, VisionProvider in db.py)
const LLM_PROVIDER_KEYS : string [ ] = [
"OPENAI" ,
"ANTHROPIC" ,
"GOOGLE" ,
"AZURE_OPENAI" ,
"BEDROCK" ,
"VERTEX_AI" ,
"GROQ" ,
"DEEPSEEK" ,
"XAI" ,
"MISTRAL" ,
"COHERE" ,
"OPENROUTER" ,
"TOGETHER_AI" ,
"FIREWORKS_AI" ,
"REPLICATE" ,
"PERPLEXITY" ,
"OLLAMA" ,
"CEREBRAS" ,
"SAMBANOVA" ,
"DEEPINFRA" ,
"AI21" ,
"ALIBABA_QWEN" ,
"MOONSHOT" ,
"ZHIPU" ,
"MINIMAX" ,
"HUGGINGFACE" ,
"CLOUDFLARE" ,
"DATABRICKS" ,
"ANYSCALE" ,
"COMETAPI" ,
"GITHUB_MODELS" ,
"CUSTOM" ,
] ;
const IMAGE_PROVIDER_KEYS : string [ ] = [
"OPENAI" ,
"AZURE_OPENAI" ,
"GOOGLE" ,
"VERTEX_AI" ,
"BEDROCK" ,
"RECRAFT" ,
"OPENROUTER" ,
"XINFERENCE" ,
"NSCALE" ,
] ;
const VISION_PROVIDER_KEYS : string [ ] = [
"OPENAI" ,
"ANTHROPIC" ,
"GOOGLE" ,
"AZURE_OPENAI" ,
"VERTEX_AI" ,
"BEDROCK" ,
"XAI" ,
"OPENROUTER" ,
"OLLAMA" ,
"GROQ" ,
"TOGETHER_AI" ,
"FIREWORKS_AI" ,
"DEEPSEEK" ,
"MISTRAL" ,
"CUSTOM" ,
] ;
const PROVIDER_KEYS_BY_TAB : Record < string , string [ ] > = {
llm : LLM_PROVIDER_KEYS ,
image : IMAGE_PROVIDER_KEYS ,
vision : VISION_PROVIDER_KEYS ,
} ;
function formatProviderName ( provider : string ) : string {
const key = provider . toUpperCase ( ) ;
return (
PROVIDER_NAMES [ key ] ? ?
2026-04-14 21:26:00 -07:00
provider . charAt ( 0 ) . toUpperCase ( ) + provider . slice ( 1 ) . toLowerCase ( ) . replace ( /_/g , " " )
2026-04-13 16:10:29 -07:00
) ;
}
function normalizeText ( input : string ) : string {
return input
. normalize ( "NFD" )
. replace ( / \ p { D i a c r i t i c } / g u , " " )
. toLowerCase ( )
. replace ( /[^a-z0-9]+/g , " " )
. trim ( ) ;
}
interface ConfigBase {
id : number ;
name : string ;
model_name : string ;
provider : string ;
}
function filterAndScore < T extends ConfigBase > (
configs : T [ ] ,
selectedProvider : string ,
2026-04-14 21:26:00 -07:00
searchQuery : string
2026-04-13 16:10:29 -07:00
) : T [ ] {
let result = configs ;
if ( selectedProvider !== "all" ) {
2026-04-14 21:26:00 -07:00
result = result . filter ( ( c ) = > c . provider . toUpperCase ( ) === selectedProvider ) ;
2026-04-13 16:10:29 -07:00
}
if ( ! searchQuery . trim ( ) ) return result ;
const normalized = normalizeText ( searchQuery ) ;
const tokens = normalized . split ( /\s+/ ) . filter ( Boolean ) ;
const scored = result . map ( ( c ) = > {
2026-04-14 21:26:00 -07:00
const aggregate = normalizeText ( [ c . name , c . model_name , c . provider ] . join ( " " ) ) ;
2026-04-13 16:10:29 -07:00
let score = 0 ;
if ( aggregate . includes ( normalized ) ) score += 5 ;
for ( const token of tokens ) {
if ( aggregate . includes ( token ) ) score += 1 ;
}
return { config : c , score } ;
} ) ;
return scored
. filter ( ( s ) = > s . score > 0 )
. sort ( ( a , b ) = > b . score - a . score )
. map ( ( s ) = > s . config ) ;
}
interface DisplayItem {
config : ConfigBase & Record < string , unknown > ;
isGlobal : boolean ;
isAutoMode : boolean ;
}
// ─── Component ──────────────────────────────────────────────────────
2025-12-23 01:16:25 -08:00
interface ModelSelectorProps {
2026-04-14 21:26:00 -07:00
onEditLLM : ( config : NewLLMConfigPublic | GlobalNewLLMConfig , isGlobal : boolean ) = > void ;
2026-04-13 16:10:29 -07:00
onAddNewLLM : ( provider? : string ) = > void ;
2026-04-14 21:26:00 -07:00
onEditImage ? : ( config : ImageGenerationConfig | GlobalImageGenConfig , isGlobal : boolean ) = > void ;
2026-04-13 16:10:29 -07:00
onAddNewImage ? : ( provider? : string ) = > void ;
2026-04-14 21:26:00 -07:00
onEditVision ? : ( config : VisionLLMConfig | GlobalVisionLLMConfig , isGlobal : boolean ) = > void ;
2026-04-13 16:10:29 -07:00
onAddNewVision ? : ( provider? : string ) = > void ;
2025-12-23 01:16:25 -08:00
className? : string ;
}
2026-02-10 17:20:42 +05:30
export function ModelSelector ( {
onEditLLM ,
onAddNewLLM ,
onEditImage ,
onAddNewImage ,
2026-04-07 20:47:17 +02:00
onEditVision ,
onAddNewVision ,
2026-02-10 17:20:42 +05:30
className ,
} : ModelSelectorProps ) {
2025-12-23 01:16:25 -08:00
const [ open , setOpen ] = useState ( false ) ;
2026-04-14 21:26:00 -07:00
const [ activeTab , setActiveTab ] = useState < "llm" | "image" | "vision" > ( "llm" ) ;
2026-04-13 16:10:29 -07:00
const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
const [ selectedProvider , setSelectedProvider ] = useState < string > ( "all" ) ;
const [ focusedIndex , setFocusedIndex ] = useState ( - 1 ) ;
2026-04-14 20:35:16 +05:30
const [ modelScrollPos , setModelScrollPos ] = useState < "top" | "middle" | "bottom" > ( "top" ) ;
const [ sidebarScrollPos , setSidebarScrollPos ] = useState < "top" | "middle" | "bottom" > ( "top" ) ;
2026-04-13 16:10:29 -07:00
const providerSidebarRef = useRef < HTMLDivElement > ( null ) ;
const modelListRef = useRef < HTMLDivElement > ( null ) ;
const searchInputRef = useRef < HTMLInputElement > ( null ) ;
const isMobile = useIsMobile ( ) ;
2026-04-14 20:35:16 +05:30
const handleModelListScroll = useCallback ( ( e : React.UIEvent < HTMLDivElement > ) = > {
const el = e . currentTarget ;
const atTop = el . scrollTop <= 2 ;
const atBottom = el . scrollHeight - el . scrollTop - el . clientHeight <= 2 ;
setModelScrollPos ( atTop ? "top" : atBottom ? "bottom" : "middle" ) ;
} , [ ] ) ;
2026-04-14 21:26:00 -07:00
const handleSidebarScroll = useCallback (
( e : React.UIEvent < HTMLDivElement > ) = > {
const el = e . currentTarget ;
if ( isMobile ) {
const atStart = el . scrollLeft <= 2 ;
const atEnd = el . scrollWidth - el . scrollLeft - el . clientWidth <= 2 ;
setSidebarScrollPos ( atStart ? "top" : atEnd ? "bottom" : "middle" ) ;
} else {
const atTop = el . scrollTop <= 2 ;
const atBottom = el . scrollHeight - el . scrollTop - el . clientHeight <= 2 ;
setSidebarScrollPos ( atTop ? "top" : atBottom ? "bottom" : "middle" ) ;
}
} ,
[ isMobile ]
) ;
2026-04-14 20:35:16 +05:30
2026-04-13 16:10:29 -07:00
// Reset search + provider when tab changes
2026-04-14 20:35:16 +05:30
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTab is intentionally used as a trigger
2026-04-13 16:10:29 -07:00
useEffect ( ( ) = > {
setSelectedProvider ( "all" ) ;
setSearchQuery ( "" ) ;
setFocusedIndex ( - 1 ) ;
2026-04-14 20:35:16 +05:30
setModelScrollPos ( "top" ) ;
2026-04-13 16:10:29 -07:00
} , [ activeTab ] ) ;
2025-12-23 01:16:25 -08:00
2026-04-13 16:10:29 -07:00
// Reset on open
useEffect ( ( ) = > {
if ( open ) {
setSearchQuery ( "" ) ;
setSelectedProvider ( "all" ) ;
}
} , [ open ] ) ;
2026-04-14 20:35:16 +05:30
// Cmd/Ctrl+M shortcut (desktop only)
2026-04-13 16:10:29 -07:00
useEffect ( ( ) = > {
2026-04-14 20:35:16 +05:30
if ( isMobile ) return ;
2026-04-13 16:10:29 -07:00
const handler = ( e : KeyboardEvent ) = > {
if ( ( e . metaKey || e . ctrlKey ) && e . key === "m" ) {
e . preventDefault ( ) ;
setOpen ( ( prev ) = > ! prev ) ;
}
} ;
document . addEventListener ( "keydown" , handler ) ;
return ( ) = > document . removeEventListener ( "keydown" , handler ) ;
2026-04-14 20:35:16 +05:30
} , [ isMobile ] ) ;
2026-04-13 16:10:29 -07:00
// Focus search input on open
2026-04-14 20:35:16 +05:30
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTab is intentionally used as a trigger to re-focus on tab switch
2026-04-13 16:10:29 -07:00
useEffect ( ( ) = > {
if ( open && ! isMobile ) {
requestAnimationFrame ( ( ) = > searchInputRef . current ? . focus ( ) ) ;
}
} , [ open , isMobile , activeTab ] ) ;
// ─── Data ───
2026-04-14 21:26:00 -07:00
const { data : llmUserConfigs , isLoading : llmUserLoading } = useAtomValue ( newLLMConfigsAtom ) ;
2026-02-10 17:20:42 +05:30
const { data : llmGlobalConfigs , isLoading : llmGlobalLoading } =
2025-12-23 01:16:25 -08:00
useAtomValue ( globalNewLLMConfigsAtom ) ;
2026-04-14 21:26:00 -07:00
const { data : preferences , isLoading : prefsLoading } = useAtomValue ( llmPreferencesAtom ) ;
2025-12-23 01:16:25 -08:00
const searchSpaceId = useAtomValue ( activeSearchSpaceIdAtom ) ;
2026-04-14 21:26:00 -07:00
const { mutateAsync : updatePreferences } = useAtomValue ( updateLLMPreferencesMutationAtom ) ;
2026-02-10 17:20:42 +05:30
const { data : imageGlobalConfigs , isLoading : imageGlobalLoading } =
useAtomValue ( globalImageGenConfigsAtom ) ;
2026-04-14 21:26:00 -07:00
const { data : imageUserConfigs , isLoading : imageUserLoading } = useAtomValue ( imageGenConfigsAtom ) ;
const { data : visionGlobalConfigs , isLoading : visionGlobalLoading } = useAtomValue (
globalVisionLLMConfigsAtom
) ;
2026-04-07 20:47:17 +02:00
const { data : visionUserConfigs , isLoading : visionUserLoading } =
useAtomValue ( visionLLMConfigsAtom ) ;
2026-02-10 19:06:21 +05:30
const isLoading =
2026-04-07 20:47:17 +02:00
llmUserLoading ||
llmGlobalLoading ||
prefsLoading ||
imageGlobalLoading ||
imageUserLoading ||
visionGlobalLoading ||
visionUserLoading ;
2025-12-23 01:16:25 -08:00
2026-04-13 16:10:29 -07:00
// ─── Current selected configs ───
2026-02-10 17:20:42 +05:30
const currentLLMConfig = useMemo ( ( ) = > {
if ( ! preferences ) return null ;
2026-04-13 16:10:29 -07:00
const id = preferences . agent_llm_id ;
if ( id === null || id === undefined ) return null ;
if ( id <= 0 ) return llmGlobalConfigs ? . find ( ( c ) = > c . id === id ) ? ? null ;
return llmUserConfigs ? . find ( ( c ) = > c . id === id ) ? ? null ;
2026-02-10 17:20:42 +05:30
} , [ preferences , llmGlobalConfigs , llmUserConfigs ] ) ;
2026-04-08 01:36:24 -07:00
const isLLMAutoMode =
2026-04-14 21:26:00 -07:00
currentLLMConfig && "is_auto_mode" in currentLLMConfig && currentLLMConfig . is_auto_mode ;
2026-02-10 17:20:42 +05:30
const currentImageConfig = useMemo ( ( ) = > {
if ( ! preferences ) return null ;
const id = preferences . image_generation_config_id ;
if ( id === null || id === undefined ) return null ;
2026-04-13 16:10:29 -07:00
return (
imageGlobalConfigs ? . find ( ( c ) = > c . id === id ) ? ?
imageUserConfigs ? . find ( ( c ) = > c . id === id ) ? ?
null
) ;
2026-02-10 17:20:42 +05:30
} , [ preferences , imageGlobalConfigs , imageUserConfigs ] ) ;
2026-04-08 01:36:24 -07:00
const isImageAutoMode =
2026-04-14 21:26:00 -07:00
currentImageConfig && "is_auto_mode" in currentImageConfig && currentImageConfig . is_auto_mode ;
2026-02-10 17:20:42 +05:30
2026-04-07 20:47:17 +02:00
const currentVisionConfig = useMemo ( ( ) = > {
if ( ! preferences ) return null ;
const id = preferences . vision_llm_config_id ;
if ( id === null || id === undefined ) return null ;
return (
2026-04-13 16:10:29 -07:00
visionGlobalConfigs ? . find ( ( c ) = > c . id === id ) ? ?
visionUserConfigs ? . find ( ( c ) = > c . id === id ) ? ?
null
2026-04-07 20:47:17 +02:00
) ;
2026-04-13 16:10:29 -07:00
} , [ preferences , visionGlobalConfigs , visionUserConfigs ] ) ;
const isVisionAutoMode =
currentVisionConfig &&
"is_auto_mode" in currentVisionConfig &&
currentVisionConfig . is_auto_mode ;
// ─── Filtered configs (separate global / user for section headers) ───
const filteredLLMGlobal = useMemo (
2026-04-14 21:26:00 -07:00
( ) = > filterAndScore ( llmGlobalConfigs ? ? [ ] , selectedProvider , searchQuery ) ,
[ llmGlobalConfigs , selectedProvider , searchQuery ]
2026-04-13 16:10:29 -07:00
) ;
const filteredLLMUser = useMemo (
2026-04-14 21:26:00 -07:00
( ) = > filterAndScore ( llmUserConfigs ? ? [ ] , selectedProvider , searchQuery ) ,
[ llmUserConfigs , selectedProvider , searchQuery ]
2026-04-13 16:10:29 -07:00
) ;
const filteredImageGlobal = useMemo (
2026-04-14 21:26:00 -07:00
( ) = > filterAndScore ( imageGlobalConfigs ? ? [ ] , selectedProvider , searchQuery ) ,
[ imageGlobalConfigs , selectedProvider , searchQuery ]
2026-04-13 16:10:29 -07:00
) ;
const filteredImageUser = useMemo (
2026-04-14 21:26:00 -07:00
( ) = > filterAndScore ( imageUserConfigs ? ? [ ] , selectedProvider , searchQuery ) ,
[ imageUserConfigs , selectedProvider , searchQuery ]
2026-04-13 16:10:29 -07:00
) ;
const filteredVisionGlobal = useMemo (
2026-04-14 21:26:00 -07:00
( ) = > filterAndScore ( visionGlobalConfigs ? ? [ ] , selectedProvider , searchQuery ) ,
[ visionGlobalConfigs , selectedProvider , searchQuery ]
2026-04-13 16:10:29 -07:00
) ;
const filteredVisionUser = useMemo (
2026-04-14 21:26:00 -07:00
( ) = > filterAndScore ( visionUserConfigs ? ? [ ] , selectedProvider , searchQuery ) ,
[ visionUserConfigs , selectedProvider , searchQuery ]
2026-04-13 16:10:29 -07:00
) ;
// Combined display list for keyboard navigation
const currentDisplayItems : DisplayItem [ ] = useMemo ( ( ) = > {
2026-04-14 21:26:00 -07:00
const toItems = ( configs : ConfigBase [ ] , isGlobal : boolean ) : DisplayItem [ ] = >
2026-04-13 16:10:29 -07:00
configs . map ( ( c ) = > ( {
config : c as ConfigBase & Record < string , unknown > ,
isGlobal ,
isAutoMode :
2026-04-14 21:26:00 -07:00
isGlobal && "is_auto_mode" in c && ! ! ( c as Record < string , unknown > ) . is_auto_mode ,
2026-04-13 16:10:29 -07:00
} ) ) ;
2026-04-15 23:46:29 -07:00
const sortGlobalItems = ( items : DisplayItem [ ] ) : DisplayItem [ ] = >
[ . . . items ] . sort ( ( a , b ) = > {
if ( a . isAutoMode !== b . isAutoMode ) return a . isAutoMode ? - 1 : 1 ;
const aPremium = ! ! ( a . config as Record < string , unknown > ) . is_premium ;
const bPremium = ! ! ( b . config as Record < string , unknown > ) . is_premium ;
if ( aPremium !== bPremium ) return aPremium ? 1 : - 1 ;
return 0 ;
} ) ;
2026-04-13 16:10:29 -07:00
switch ( activeTab ) {
case "llm" :
2026-04-15 23:46:29 -07:00
return [
. . . sortGlobalItems ( toItems ( filteredLLMGlobal , true ) ) ,
. . . toItems ( filteredLLMUser , false ) ,
] ;
2026-04-13 16:10:29 -07:00
case "image" :
2026-04-15 23:46:29 -07:00
return [
. . . sortGlobalItems ( toItems ( filteredImageGlobal , true ) ) ,
. . . toItems ( filteredImageUser , false ) ,
] ;
2026-04-13 16:10:29 -07:00
case "vision" :
2026-04-15 23:46:29 -07:00
return [
. . . sortGlobalItems ( toItems ( filteredVisionGlobal , true ) ) ,
. . . toItems ( filteredVisionUser , false ) ,
] ;
2026-04-13 16:10:29 -07:00
}
} , [
activeTab ,
filteredLLMGlobal ,
filteredLLMUser ,
filteredImageGlobal ,
filteredImageUser ,
filteredVisionGlobal ,
filteredVisionUser ,
] ) ;
// ─── Provider sidebar data ───
// Collect which providers actually have configured models for the active tab
const configuredProviderSet = useMemo ( ( ) = > {
const configs =
activeTab === "llm"
2026-04-14 21:26:00 -07:00
? [ . . . ( llmGlobalConfigs ? ? [ ] ) , . . . ( llmUserConfigs ? ? [ ] ) ]
2026-04-13 16:10:29 -07:00
: activeTab === "image"
2026-04-14 21:26:00 -07:00
? [ . . . ( imageGlobalConfigs ? ? [ ] ) , . . . ( imageUserConfigs ? ? [ ] ) ]
: [ . . . ( visionGlobalConfigs ? ? [ ] ) , . . . ( visionUserConfigs ? ? [ ] ) ] ;
2026-04-13 16:10:29 -07:00
const set = new Set < string > ( ) ;
for ( const c of configs ) {
if ( c . provider ) set . add ( c . provider . toUpperCase ( ) ) ;
}
return set ;
} , [
activeTab ,
llmGlobalConfigs ,
llmUserConfigs ,
imageGlobalConfigs ,
imageUserConfigs ,
visionGlobalConfigs ,
visionUserConfigs ,
] ) ;
// Show only providers valid for the active tab; configured ones first
const activeProviders = useMemo ( ( ) = > {
const tabKeys = PROVIDER_KEYS_BY_TAB [ activeTab ] ? ? LLM_PROVIDER_KEYS ;
2026-04-14 21:26:00 -07:00
const configured = tabKeys . filter ( ( p ) = > configuredProviderSet . has ( p ) ) ;
const unconfigured = tabKeys . filter ( ( p ) = > ! configuredProviderSet . has ( p ) ) ;
2026-04-13 16:10:29 -07:00
return [ "all" , . . . configured , . . . unconfigured ] ;
} , [ activeTab , configuredProviderSet ] ) ;
2026-04-07 20:47:17 +02:00
2026-04-13 16:10:29 -07:00
const providerModelCounts = useMemo ( ( ) = > {
const allConfigs =
activeTab === "llm"
2026-04-14 21:26:00 -07:00
? [ . . . ( llmGlobalConfigs ? ? [ ] ) , . . . ( llmUserConfigs ? ? [ ] ) ]
2026-04-13 16:10:29 -07:00
: activeTab === "image"
2026-04-14 21:26:00 -07:00
? [ . . . ( imageGlobalConfigs ? ? [ ] ) , . . . ( imageUserConfigs ? ? [ ] ) ]
: [ . . . ( visionGlobalConfigs ? ? [ ] ) , . . . ( visionUserConfigs ? ? [ ] ) ] ;
2026-04-13 16:10:29 -07:00
const counts : Record < string , number > = { all : allConfigs.length } ;
for ( const c of allConfigs ) {
const p = c . provider . toUpperCase ( ) ;
counts [ p ] = ( counts [ p ] || 0 ) + 1 ;
}
return counts ;
} , [
activeTab ,
llmGlobalConfigs ,
llmUserConfigs ,
imageGlobalConfigs ,
imageUserConfigs ,
visionGlobalConfigs ,
visionUserConfigs ,
] ) ;
2026-04-07 20:47:17 +02:00
2026-04-13 16:10:29 -07:00
// ─── Selection handlers ───
2026-02-10 17:20:42 +05:30
const handleSelectLLM = useCallback (
2025-12-23 01:16:25 -08:00
async ( config : NewLLMConfigPublic | GlobalNewLLMConfig ) = > {
2026-02-10 17:20:42 +05:30
if ( currentLLMConfig ? . id === config . id ) {
2025-12-23 01:16:25 -08:00
setOpen ( false ) ;
return ;
}
if ( ! searchSpaceId ) {
toast . error ( "No search space selected" ) ;
return ;
}
try {
await updatePreferences ( {
search_space_id : Number ( searchSpaceId ) ,
2026-02-10 17:20:42 +05:30
data : { agent_llm_id : config.id } ,
2025-12-23 01:16:25 -08:00
} ) ;
toast . success ( ` Switched to ${ config . name } ` ) ;
setOpen ( false ) ;
2026-04-13 16:10:29 -07:00
} catch {
2025-12-23 01:16:25 -08:00
toast . error ( "Failed to switch model" ) ;
}
} ,
2026-04-14 21:26:00 -07:00
[ currentLLMConfig , searchSpaceId , updatePreferences ]
2026-02-10 17:20:42 +05:30
) ;
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" ) ;
}
} ,
2026-04-14 21:26:00 -07:00
[ currentImageConfig , searchSpaceId , updatePreferences ]
2025-12-23 01:16:25 -08:00
) ;
2026-04-07 20:47:17 +02:00
const handleSelectVision = useCallback (
async ( configId : number ) = > {
if ( currentVisionConfig ? . id === configId ) {
setOpen ( false ) ;
return ;
}
if ( ! searchSpaceId ) {
toast . error ( "No search space selected" ) ;
return ;
}
try {
await updatePreferences ( {
search_space_id : Number ( searchSpaceId ) ,
data : { vision_llm_config_id : configId } ,
} ) ;
toast . success ( "Vision model updated" ) ;
setOpen ( false ) ;
} catch {
toast . error ( "Failed to switch vision model" ) ;
}
} ,
2026-04-14 21:26:00 -07:00
[ currentVisionConfig , searchSpaceId , updatePreferences ]
2026-04-07 20:47:17 +02:00
) ;
2026-04-13 16:10:29 -07:00
const handleSelectItem = useCallback (
( item : DisplayItem ) = > {
switch ( activeTab ) {
case "llm" :
2026-04-14 21:26:00 -07:00
handleSelectLLM ( item . config as NewLLMConfigPublic | GlobalNewLLMConfig ) ;
2026-04-13 16:10:29 -07:00
break ;
case "image" :
handleSelectImage ( item . config . id ) ;
break ;
case "vision" :
handleSelectVision ( item . config . id ) ;
break ;
}
} ,
2026-04-14 21:26:00 -07:00
[ activeTab , handleSelectLLM , handleSelectImage , handleSelectVision ]
2026-04-13 16:10:29 -07:00
) ;
const handleEditItem = useCallback (
( e : React.MouseEvent , item : DisplayItem ) = > {
e . stopPropagation ( ) ;
setOpen ( false ) ;
switch ( activeTab ) {
case "llm" :
2026-04-14 21:26:00 -07:00
onEditLLM ( item . config as NewLLMConfigPublic | GlobalNewLLMConfig , item . isGlobal ) ;
2026-04-13 16:10:29 -07:00
break ;
case "image" :
2026-04-14 21:26:00 -07:00
onEditImage ? . ( item . config as ImageGenerationConfig | GlobalImageGenConfig , item . isGlobal ) ;
2026-04-13 16:10:29 -07:00
break ;
case "vision" :
2026-04-14 21:26:00 -07:00
onEditVision ? . ( item . config as VisionLLMConfig | GlobalVisionLLMConfig , item . isGlobal ) ;
2026-04-13 16:10:29 -07:00
break ;
}
} ,
2026-04-14 21:26:00 -07:00
[ activeTab , onEditLLM , onEditImage , onEditVision ]
2026-04-13 16:10:29 -07:00
) ;
// ─── Keyboard navigation ───
2026-04-14 20:35:16 +05:30
// biome-ignore lint/correctness/useExhaustiveDependencies: searchQuery and selectedProvider are intentional triggers to reset focus
2026-04-13 16:10:29 -07:00
useEffect ( ( ) = > {
setFocusedIndex ( - 1 ) ;
} , [ searchQuery , selectedProvider ] ) ;
useEffect ( ( ) = > {
if ( focusedIndex < 0 || ! modelListRef . current ) return ;
2026-04-14 21:26:00 -07:00
const items = modelListRef . current . querySelectorAll ( "[data-model-index]" ) ;
2026-04-13 16:10:29 -07:00
items [ focusedIndex ] ? . scrollIntoView ( {
block : "nearest" ,
behavior : "smooth" ,
} ) ;
} , [ focusedIndex ] ) ;
const handleKeyDown = useCallback (
( e : React.KeyboardEvent < HTMLInputElement > ) = > {
const count = currentDisplayItems . length ;
// Arrow Left/Right cycle provider filters
if ( e . key === "ArrowLeft" || e . key === "ArrowRight" ) {
e . preventDefault ( ) ;
const providers = activeProviders ;
const idx = providers . indexOf ( selectedProvider ) ;
let next : number ;
if ( e . key === "ArrowLeft" ) {
next = idx > 0 ? idx - 1 : providers.length - 1 ;
} else {
2026-04-14 21:26:00 -07:00
next = idx < providers . length - 1 ? idx + 1 : 0 ;
2026-04-13 16:10:29 -07:00
}
setSelectedProvider ( providers [ next ] ) ;
if ( providerSidebarRef . current ) {
2026-04-14 21:26:00 -07:00
const buttons = providerSidebarRef . current . querySelectorAll ( "button" ) ;
2026-04-13 16:10:29 -07:00
buttons [ next ] ? . scrollIntoView ( {
block : "nearest" ,
inline : "nearest" ,
behavior : "smooth" ,
} ) ;
}
return ;
}
if ( count === 0 ) return ;
switch ( e . key ) {
case "ArrowDown" :
e . preventDefault ( ) ;
2026-04-14 21:26:00 -07:00
setFocusedIndex ( ( prev ) = > ( prev < count - 1 ? prev + 1 : 0 ) ) ;
2026-04-13 16:10:29 -07:00
break ;
case "ArrowUp" :
e . preventDefault ( ) ;
2026-04-14 21:26:00 -07:00
setFocusedIndex ( ( prev ) = > ( prev > 0 ? prev - 1 : count - 1 ) ) ;
2026-04-13 16:10:29 -07:00
break ;
case "Enter" :
e . preventDefault ( ) ;
if ( focusedIndex >= 0 && focusedIndex < count ) {
handleSelectItem ( currentDisplayItems [ focusedIndex ] ) ;
}
break ;
case "Home" :
e . preventDefault ( ) ;
setFocusedIndex ( 0 ) ;
break ;
case "End" :
e . preventDefault ( ) ;
setFocusedIndex ( count - 1 ) ;
break ;
}
} ,
2026-04-14 21:26:00 -07:00
[ currentDisplayItems , focusedIndex , activeProviders , selectedProvider , handleSelectItem ]
2026-04-13 16:10:29 -07:00
) ;
// ─── Render: Provider sidebar ───
const renderProviderSidebar = ( ) = > {
const configuredCount = configuredProviderSet . size ;
return (
< div
className = { cn (
2026-04-14 20:35:16 +05:30
"shrink-0 border-border/50 flex" ,
2026-04-14 21:26:00 -07:00
isMobile ? "flex-row items-center border-b border-border/40" : "flex-col w-10 border-r"
2026-04-13 16:10:29 -07:00
) }
>
2026-04-14 20:35:16 +05:30
{ ! isMobile && sidebarScrollPos !== "top" && (
< div className = "flex items-center justify-center py-0.5 pointer-events-none" >
< ChevronUp className = "size-3 text-muted-foreground" / >
< / div >
) }
{ isMobile && sidebarScrollPos !== "top" && (
< div className = "flex items-center justify-center px-0.5 shrink-0 pointer-events-none" >
< ChevronLeft className = "size-3 text-muted-foreground" / >
< / div >
) }
2026-04-13 16:10:29 -07:00
< div
ref = { providerSidebarRef }
2026-04-14 20:35:16 +05:30
onScroll = { handleSidebarScroll }
2026-03-17 04:40:46 +05:30
className = { cn (
2026-04-13 16:10:29 -07:00
isMobile
2026-04-14 20:35:16 +05:30
? "flex flex-row gap-0.5 px-1 py-1.5 overflow-x-auto [&::-webkit-scrollbar]:h-0 [&::-webkit-scrollbar-track]:bg-transparent"
2026-04-14 21:26:00 -07:00
: "flex flex-col gap-0.5 p-1 overflow-y-auto flex-1 [&::-webkit-scrollbar]:w-0 [&::-webkit-scrollbar-track]:bg-transparent"
2026-03-17 04:40:46 +05:30
) }
2026-04-14 21:26:00 -07:00
style = {
isMobile
? {
maskImage : ` linear-gradient(to right, ${ sidebarScrollPos === "top" ? "black" : "transparent" } , black 24px, black calc(100% - 24px), ${ sidebarScrollPos === "bottom" ? "black" : "transparent" } ) ` ,
WebkitMaskImage : ` linear-gradient(to right, ${ sidebarScrollPos === "top" ? "black" : "transparent" } , black 24px, black calc(100% - 24px), ${ sidebarScrollPos === "bottom" ? "black" : "transparent" } ) ` ,
}
: {
maskImage : ` linear-gradient(to bottom, ${ sidebarScrollPos === "top" ? "black" : "transparent" } , black 32px, black calc(100% - 32px), ${ sidebarScrollPos === "bottom" ? "black" : "transparent" } ) ` ,
WebkitMaskImage : ` linear-gradient(to bottom, ${ sidebarScrollPos === "top" ? "black" : "transparent" } , black 32px, black calc(100% - 32px), ${ sidebarScrollPos === "bottom" ? "black" : "transparent" } ) ` ,
}
}
2025-12-23 01:16:25 -08:00
>
2026-04-13 16:10:29 -07:00
{ activeProviders . map ( ( provider , idx ) = > {
const isAll = provider === "all" ;
const isActive = selectedProvider === provider ;
const count = providerModelCounts [ provider ] || 0 ;
2026-04-14 21:26:00 -07:00
const isConfigured = isAll || configuredProviderSet . has ( provider ) ;
2026-02-10 17:20:42 +05:30
2026-04-13 16:10:29 -07:00
// Separator between configured and unconfigured providers
// idx 0 is "all", configured run from 1..configuredCount, unconfigured start at configuredCount+1
2026-04-14 21:26:00 -07:00
const showSeparator = ! isAll && idx === configuredCount + 1 && configuredCount > 0 ;
2026-02-10 17:20:42 +05:30
2026-04-13 16:10:29 -07:00
return (
< Fragment key = { provider } >
{ showSeparator &&
( isMobile ? (
< div className = "w-px h-5 bg-border/60 shrink-0 self-center mx-0.5" / >
) : (
< div className = "h-px w-5 bg-border/60 mx-auto my-0.5" / >
) ) }
< Tooltip >
< TooltipTrigger asChild >
< button
type = "button"
2026-04-14 21:26:00 -07:00
onClick = { ( ) = > setSelectedProvider ( provider ) }
2026-04-13 16:10:29 -07:00
tabIndex = { - 1 }
className = { cn (
"relative flex items-center justify-center rounded-md transition-all duration-150" ,
2026-04-14 21:26:00 -07:00
isMobile ? "p-2 shrink-0" : "p-1.5 w-full" ,
2026-04-13 16:10:29 -07:00
isActive
? "bg-primary/10 text-primary"
: isConfigured
? "hover:bg-accent/60 text-muted-foreground hover:text-foreground"
2026-04-14 21:26:00 -07:00
: "opacity-50 hover:opacity-80 hover:bg-accent/40 text-muted-foreground"
2026-04-13 16:10:29 -07:00
) }
>
{ isAll ? (
< Layers className = "size-4" / >
) : (
getProviderIcon ( provider , {
className : "size-4" ,
} )
) }
< / button >
< / TooltipTrigger >
2026-04-14 21:26:00 -07:00
< TooltipContent side = { isMobile ? "bottom" : "right" } >
{ isAll ? "All Models" : formatProviderName ( provider ) }
{ isConfigured ? ` ( ${ count } ) ` : " (not configured)" }
2026-04-13 16:10:29 -07:00
< / TooltipContent >
< / Tooltip >
< / Fragment >
) ;
} ) }
< / div >
2026-04-14 20:35:16 +05:30
{ ! isMobile && sidebarScrollPos !== "bottom" && (
< div className = "flex items-center justify-center py-0.5 pointer-events-none" >
2026-04-13 16:10:29 -07:00
< ChevronDown className = "size-3 text-muted-foreground" / >
< / div >
) }
2026-04-14 20:35:16 +05:30
{ isMobile && sidebarScrollPos !== "bottom" && (
< div className = "flex items-center justify-center px-0.5 shrink-0 pointer-events-none" >
< ChevronRight className = "size-3 text-muted-foreground" / >
< / div >
) }
2026-04-13 16:10:29 -07:00
< / div >
) ;
} ;
2026-04-07 20:47:17 +02:00
2026-04-13 16:10:29 -07:00
// ─── Render: Model card ───
const getSelectedId = ( ) = > {
switch ( activeTab ) {
case "llm" :
return currentLLMConfig ? . id ;
case "image" :
return currentImageConfig ? . id ;
case "vision" :
return currentVisionConfig ? . id ;
}
} ;
2026-04-07 20:47:17 +02:00
2026-04-13 16:10:29 -07:00
const renderModelCard = ( item : DisplayItem , index : number ) = > {
const { config , isAutoMode } = item ;
const isSelected = getSelectedId ( ) === config . id ;
const isFocused = focusedIndex === index ;
2026-04-14 21:26:00 -07:00
const hasCitations = "citations_enabled" in config && ! ! config . citations_enabled ;
2025-12-23 01:16:25 -08:00
2026-04-13 16:10:29 -07:00
return (
< div
key = { ` ${ activeTab } - ${ item . isGlobal ? "g" : "u" } - ${ config . id } ` }
data - model - index = { index }
role = "option"
2026-04-14 20:35:16 +05:30
tabIndex = { isMobile ? - 1 : 0 }
2026-04-13 16:10:29 -07:00
aria - selected = { isSelected }
onClick = { ( ) = > handleSelectItem ( item ) }
2026-04-14 21:26:00 -07:00
onKeyDown = {
isMobile
? undefined
: ( e ) = > {
if ( e . key === "Enter" || e . key === " " ) {
e . preventDefault ( ) ;
handleSelectItem ( item ) ;
}
}
}
2026-04-13 16:10:29 -07:00
onMouseEnter = { ( ) = > setFocusedIndex ( index ) }
className = { cn (
2026-04-14 20:35:16 +05:30
"group flex items-center gap-2.5 px-3 py-2 rounded-xl cursor-pointer" ,
"transition-all duration-150 mx-2" ,
"hover:bg-accent/40" ,
2026-04-13 16:10:29 -07:00
isSelected && "bg-primary/6 dark:bg-primary/8" ,
2026-04-14 21:26:00 -07:00
isFocused && "bg-accent/50"
2026-04-13 16:10:29 -07:00
) }
2025-12-23 01:16:25 -08:00
>
2026-04-13 16:10:29 -07:00
{ /* Provider icon */ }
2026-04-14 20:35:16 +05:30
< div className = "shrink-0" >
2026-04-13 16:10:29 -07:00
{ getProviderIcon ( config . provider as string , {
isAutoMode ,
className : "size-5" ,
} ) }
< / div >
{ /* Model info */ }
< div className = "flex-1 min-w-0" >
< div className = "flex items-center gap-1.5" >
2026-04-14 21:26:00 -07:00
< span className = "font-medium text-sm truncate" > { config . name } < / span >
2026-04-13 16:10:29 -07:00
{ isAutoMode && (
< Badge
variant = "secondary"
2026-04-14 21:50:34 +05:30
className = "text-[9px] px-1 py-0 h-3.5 bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300 border-0"
2026-02-10 17:20:42 +05:30
>
2026-04-13 16:10:29 -07:00
Recommended
< / Badge >
) }
2026-04-15 23:46:29 -07:00
{ "is_premium" in config && ( config as Record < string , unknown > ) . is_premium ? (
2026-04-15 17:02:00 -07:00
< Badge
variant = "secondary"
className = "text-[9px] px-1 py-0 h-3.5 bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300 border-0"
>
Premium
< / Badge >
2026-04-15 23:46:29 -07:00
) : "is_premium" in config &&
! ( config as Record < string , unknown > ) . is_premium &&
! isAutoMode ? (
< Badge
variant = "secondary"
className = "text-[9px] px-1 py-0 h-3.5 bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300 border-0"
>
Free
< / Badge >
) : null }
2026-04-13 16:10:29 -07:00
< / div >
< div className = "flex items-center gap-1.5 mt-0.5" >
< span className = "text-xs text-muted-foreground truncate" >
2026-04-14 21:26:00 -07:00
{ isAutoMode ? "Auto Mode" : ( config . model_name as string ) }
2026-04-13 16:10:29 -07:00
< / span >
{ ! isAutoMode && hasCitations && (
< Badge
2026-04-14 20:35:16 +05:30
variant = "secondary"
className = "text-[10px] px-1.5 py-0.5 border-0 text-muted-foreground bg-muted"
2026-04-07 20:47:17 +02:00
>
2026-04-13 16:10:29 -07:00
Citations
< / Badge >
) }
2026-02-10 17:20:42 +05:30
< / div >
2026-04-13 16:10:29 -07:00
< / div >
2025-12-23 01:16:25 -08:00
2026-04-13 16:10:29 -07:00
{ /* Actions */ }
< div className = "flex items-center gap-1 shrink-0" >
{ ! isAutoMode && (
< Button
variant = "ghost"
size = "icon"
className = "size-7 rounded-md hover:bg-muted opacity-0 group-hover:opacity-100 transition-opacity"
onClick = { ( e ) = > handleEditItem ( e , item ) }
2026-02-10 17:20:42 +05:30
>
2026-04-13 16:10:29 -07:00
< Edit3 className = "size-3.5 text-muted-foreground" / >
< / Button >
) }
2026-04-14 21:26:00 -07:00
{ isSelected && < Check className = "size-4 text-primary shrink-0" / > }
2026-04-13 16:10:29 -07:00
< / div >
< / div >
) ;
} ;
2026-02-10 17:20:42 +05:30
2026-04-13 16:10:29 -07:00
// ─── Render: Full content ───
const renderContent = ( ) = > {
const globalItems = currentDisplayItems . filter ( ( i ) = > i . isGlobal ) ;
const userItems = currentDisplayItems . filter ( ( i ) = > ! i . isGlobal ) ;
const globalStartIdx = 0 ;
const userStartIdx = globalItems . length ;
2026-02-10 17:20:42 +05:30
2026-04-13 16:10:29 -07:00
const addHandler =
2026-04-14 21:26:00 -07:00
activeTab === "llm" ? onAddNewLLM : activeTab === "image" ? onAddNewImage : onAddNewVision ;
2026-04-13 16:10:29 -07:00
const addLabel =
activeTab === "llm"
? "Add Model"
: activeTab === "image"
? "Add Image Model"
: "Add Vision Model" ;
2026-02-10 17:20:42 +05:30
2026-04-13 16:10:29 -07:00
return (
2026-04-14 20:35:16 +05:30
< div className = "flex flex-col w-full overflow-hidden" >
2026-04-13 16:10:29 -07:00
{ /* Tab header */ }
< div className = "border-b border-border/80 dark:border-neutral-800" >
< div className = "w-full grid grid-cols-3 h-11" >
{ (
[
{
value : "llm" as const ,
icon : Zap ,
label : "LLM" ,
} ,
{
value : "image" as const ,
icon : ImageIcon ,
label : "Image" ,
} ,
{
value : "vision" as const ,
2026-04-14 20:35:16 +05:30
icon : ScanEye ,
2026-04-13 16:10:29 -07:00
label : "Vision" ,
} ,
] as const
) . map ( ( { value , icon : Icon , label } ) = > (
< button
key = { value }
type = "button"
onClick = { ( ) = > setActiveTab ( value ) }
className = { cn (
"flex items-center justify-center gap-1.5 text-sm font-medium transition-all duration-200 border-b-[1.5px]" ,
activeTab === value
? "border-foreground dark:border-white text-foreground"
2026-04-14 21:26:00 -07:00
: "border-transparent text-muted-foreground hover:text-foreground/70"
2026-02-10 17:20:42 +05:30
) }
2026-03-06 22:22:28 +05:30
>
2026-04-13 16:10:29 -07:00
< Icon className = "size-3.5" / >
{ label }
< / button >
) ) }
< / div >
< / div >
2026-02-10 17:20:42 +05:30
2026-04-13 16:10:29 -07:00
{ /* Two-pane layout */ }
2026-04-14 21:26:00 -07:00
< div className = { cn ( "flex" , isMobile ? "flex-col h-[60vh]" : "flex-row h-[380px]" ) } >
2026-04-13 16:10:29 -07:00
{ /* Provider sidebar */ }
{ renderProviderSidebar ( ) }
2025-12-23 01:16:25 -08:00
2026-04-13 16:10:29 -07:00
{ /* Main content */ }
< div className = "flex flex-col min-w-0 min-h-0 flex-1 overflow-hidden" >
{ /* Search */ }
2026-04-14 20:35:16 +05:30
< div className = "relative" >
< Search className = "absolute left-3 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground/100 pointer-events-none" / >
2026-04-13 16:10:29 -07:00
< input
ref = { searchInputRef }
2026-04-14 20:35:16 +05:30
placeholder = "Search models"
2026-04-13 16:10:29 -07:00
value = { searchQuery }
2026-04-14 21:26:00 -07:00
onChange = { ( e ) = > setSearchQuery ( e . target . value ) }
2026-04-14 20:35:16 +05:30
onKeyDown = { isMobile ? undefined : handleKeyDown }
2026-04-13 16:10:29 -07:00
role = "combobox"
aria - expanded = { true }
aria - controls = "model-selector-list"
className = { cn (
2026-04-14 20:35:16 +05:30
"w-full pl-8 pr-3 py-2.5 text-sm bg-transparent" ,
"focus:outline-none" ,
2026-04-14 21:26:00 -07:00
"placeholder:text-muted-foreground"
2026-02-10 17:20:42 +05:30
) }
2026-04-13 16:10:29 -07:00
/ >
< / div >
{ /* Provider header when filtered */ }
{ selectedProvider !== "all" && (
2026-04-14 20:35:16 +05:30
< div className = "flex items-center gap-2 px-3 py-1.5" >
2026-04-13 16:10:29 -07:00
{ getProviderIcon ( selectedProvider , {
className : "size-4" ,
} ) }
2026-04-14 21:26:00 -07:00
< span className = "text-sm font-medium" > { formatProviderName ( selectedProvider ) } < / span >
2026-04-13 16:10:29 -07:00
< span className = "text-xs text-muted-foreground ml-auto" >
2026-04-14 21:26:00 -07:00
{ configuredProviderSet . has ( selectedProvider )
2026-04-13 16:10:29 -07:00
? ` ${ providerModelCounts [ selectedProvider ] || 0 } models `
: "Not configured" }
< / span >
< / div >
) }
{ /* Model list */ }
< div
id = "model-selector-list"
ref = { modelListRef }
role = "listbox"
2026-04-14 20:35:16 +05:30
className = "overflow-y-auto flex-1 py-1 space-y-1 flex flex-col"
onScroll = { handleModelListScroll }
style = { {
maskImage : ` linear-gradient(to bottom, ${ modelScrollPos === "top" ? "black" : "transparent" } , black 16px, black calc(100% - 16px), ${ modelScrollPos === "bottom" ? "black" : "transparent" } ) ` ,
WebkitMaskImage : ` linear-gradient(to bottom, ${ modelScrollPos === "top" ? "black" : "transparent" } , black 16px, black calc(100% - 16px), ${ modelScrollPos === "bottom" ? "black" : "transparent" } ) ` ,
} }
2026-04-07 20:47:17 +02:00
>
2026-04-13 16:10:29 -07:00
{ currentDisplayItems . length === 0 ? (
2026-04-14 20:35:16 +05:30
< div className = "flex-1 flex flex-col items-center justify-center gap-3 px-4" >
2026-04-14 21:26:00 -07:00
{ selectedProvider !== "all" && ! configuredProviderSet . has ( selectedProvider ) ? (
2026-04-13 16:10:29 -07:00
< >
< div className = "opacity-40" >
2026-04-14 21:26:00 -07:00
{ getProviderIcon ( selectedProvider , {
className : "size-10" ,
} ) }
2026-04-13 16:10:29 -07:00
< / div >
< p className = "text-sm font-medium text-muted-foreground" >
2026-04-14 21:26:00 -07:00
No { formatProviderName ( selectedProvider ) } models configured
2026-04-13 16:10:29 -07:00
< / p >
< p className = "text-xs text-muted-foreground/60 text-center" >
2026-04-14 21:26:00 -07:00
Add a model with this provider to get started
2026-04-13 16:10:29 -07:00
< / p >
{ addHandler && (
< Button
2026-04-14 20:35:16 +05:30
variant = "secondary"
2026-04-13 16:10:29 -07:00
size = "sm"
2026-04-14 20:35:16 +05:30
className = "mt-1"
2026-04-13 16:10:29 -07:00
onClick = { ( ) = > {
setOpen ( false ) ;
addHandler ( selectedProvider !== "all" ? selectedProvider : undefined ) ;
} }
2026-04-07 20:47:17 +02:00
>
2026-04-13 16:10:29 -07:00
{ addLabel }
< / Button >
) }
< / >
2026-04-14 20:35:16 +05:30
) : searchQuery ? (
2026-04-13 16:10:29 -07:00
< >
2026-04-14 20:35:16 +05:30
< Search className = "size-8 text-muted-foreground" / >
2026-04-14 21:26:00 -07:00
< p className = "text-sm text-muted-foreground" > No models found < / p >
2026-04-13 16:10:29 -07:00
< p className = "text-xs text-muted-foreground/60" >
2026-04-14 21:26:00 -07:00
Try a different search term
2026-04-13 16:10:29 -07:00
< / p >
< / >
2026-04-14 20:35:16 +05:30
) : (
< >
< p className = "text-sm font-medium text-muted-foreground" >
No models configured
< / p >
< p className = "text-xs text-muted-foreground/60 text-center" >
Configure models in your search space settings
< / p >
< / >
2026-04-13 16:10:29 -07:00
) }
< / div >
) : (
< >
{ globalItems . length > 0 && (
< >
2026-04-14 20:35:16 +05:30
< div className = "flex items-center gap-2 px-3 py-1.5 text-[12px] font-semibold text-muted-foreground tracking-wider" >
2026-04-13 16:10:29 -07:00
Global Models
< / div >
2026-04-14 21:26:00 -07:00
{ globalItems . map ( ( item , i ) = > renderModelCard ( item , globalStartIdx + i ) ) }
2026-04-13 16:10:29 -07:00
< / >
) }
2026-04-14 21:26:00 -07:00
{ globalItems . length > 0 && userItems . length > 0 && (
< div className = "my-1.5 mx-4 h-px bg-border/60" / >
) }
2026-04-13 16:10:29 -07:00
{ userItems . length > 0 && (
< >
2026-04-14 20:35:16 +05:30
< div className = "flex items-center gap-2 px-3 py-1.5 text-[12px] font-semibold text-muted-foreground tracking-wider" >
2026-04-13 16:10:29 -07:00
Your Configurations
2026-04-07 20:47:17 +02:00
< / div >
2026-04-14 21:26:00 -07:00
{ userItems . map ( ( item , i ) = > renderModelCard ( item , userStartIdx + i ) ) }
2026-04-13 16:10:29 -07:00
< / >
) }
< / >
) }
< / div >
2026-04-07 20:47:17 +02:00
2026-04-13 16:10:29 -07:00
{ /* Add model button */ }
{ addHandler && (
2026-04-14 20:35:16 +05:30
< div className = "p-2" >
2026-04-13 16:10:29 -07:00
< Button
variant = "ghost"
size = "sm"
className = "w-full justify-start gap-2 h-9 rounded-lg hover:bg-accent/50 dark:hover:bg-white/[0.06]"
onClick = { ( ) = > {
setOpen ( false ) ;
addHandler ( selectedProvider !== "all" ? selectedProvider : undefined ) ;
} }
>
< Plus className = "size-4 text-primary" / >
2026-04-14 21:26:00 -07:00
< span className = "text-sm font-medium" > { addLabel } < / span >
2026-04-13 16:10:29 -07:00
< / Button >
< / div >
) }
< / div >
< / div >
< / div >
) ;
} ;
// ─── Trigger button ───
const triggerButton = (
< Button
variant = "ghost"
size = "sm"
role = "combobox"
aria - expanded = { open }
className = { cn (
"h-8 gap-2 px-3 text-sm bg-main-panel hover:bg-accent/50 dark:hover:bg-white/[0.06] border border-border/40 select-none" ,
2026-04-14 21:26:00 -07:00
className
2026-04-13 16:10:29 -07:00
) }
>
{ isLoading ? (
< >
2026-04-14 21:26:00 -07:00
< Spinner size = "sm" className = "text-muted-foreground" / >
< span className = "text-muted-foreground hidden md:inline" > Loading < / span >
2026-04-13 16:10:29 -07:00
< / >
) : (
< >
{ /* LLM */ }
{ currentLLMConfig ? (
< >
{ getProviderIcon ( currentLLMConfig . provider , {
isAutoMode : isLLMAutoMode ? ? false ,
} ) }
< span className = "max-w-[100px] md:max-w-[120px] truncate hidden md:inline" >
{ currentLLMConfig . name }
< / span >
< / >
) : (
< >
< Bot className = "size-4 text-muted-foreground" / >
2026-04-14 21:26:00 -07:00
< span className = "text-muted-foreground hidden md:inline" > Select Model < / span >
2026-04-13 16:10:29 -07:00
< / >
) }
< div className = "h-4 w-px bg-border/60 dark:bg-white/10 mx-0.5" / >
{ /* Image */ }
{ currentImageConfig ? (
< >
{ getProviderIcon ( currentImageConfig . provider , {
isAutoMode : isImageAutoMode ? ? false ,
} ) }
< span className = "max-w-[80px] md:max-w-[100px] truncate hidden md:inline" >
{ currentImageConfig . name }
< / span >
< / >
) : (
< ImageIcon className = "size-4 text-muted-foreground" / >
) }
< div className = "h-4 w-px bg-border/60 dark:bg-white/10 mx-0.5" / >
{ /* Vision */ }
{ currentVisionConfig ? (
< >
{ getProviderIcon ( currentVisionConfig . provider , {
isAutoMode : isVisionAutoMode ? ? false ,
} ) }
< span className = "max-w-[80px] md:max-w-[100px] truncate hidden md:inline" >
{ currentVisionConfig . name }
< / span >
< / >
) : (
2026-04-14 20:35:16 +05:30
< ScanEye className = "size-4 text-muted-foreground" / >
2026-04-13 16:10:29 -07:00
) }
< / >
) }
< ChevronDown className = "h-3.5 w-3.5 text-muted-foreground ml-1 shrink-0" / >
< / Button >
) ;
// ─── Shell: Drawer on mobile, Popover on desktop ───
if ( isMobile ) {
return (
< Drawer open = { open } onOpenChange = { setOpen } >
< DrawerTrigger asChild > { triggerButton } < / DrawerTrigger >
< DrawerContent className = "max-h-[85vh]" >
< DrawerHandle / >
< DrawerHeader className = "pb-0" >
< DrawerTitle > Select Model < / DrawerTitle >
< / DrawerHeader >
2026-04-14 21:26:00 -07:00
< div className = "flex-1 overflow-hidden" > { renderContent ( ) } < / div >
2026-04-13 16:10:29 -07:00
< / DrawerContent >
< / Drawer >
) ;
}
return (
< Popover open = { open } onOpenChange = { setOpen } >
< PopoverTrigger asChild > { triggerButton } < / PopoverTrigger >
< PopoverContent
2026-04-14 20:35:16 +05:30
className = "w-[300px] md:w-[380px] p-0 rounded-lg shadow-lg overflow-hidden bg-white border-border/60 dark:bg-neutral-900 dark:border dark:border-white/5 select-none"
2026-04-13 16:10:29 -07:00
align = "start"
sideOffset = { 8 }
onCloseAutoFocus = { ( e ) = > e . preventDefault ( ) }
>
{ renderContent ( ) }
2025-12-23 01:16:25 -08:00
< / PopoverContent >
< / Popover >
) ;
}