Spaces:
Running
Running
"use client"; | |
import { MODELS, modelDetails, type modelID, defaultModel } from "@/ai/providers"; | |
import { | |
Select, | |
SelectContent, | |
SelectGroup, | |
SelectItem, | |
SelectTrigger, | |
SelectValue, | |
} from "./ui/select"; | |
import { cn } from "@/lib/utils"; | |
import { Sparkles, Zap, Info, Bolt, Code, Brain, Lightbulb, Image, Gauge, Rocket, Bot } from "lucide-react"; | |
import { useState, useEffect } from "react"; | |
import { TextMorph } from "./ui/text-morph"; | |
interface ModelPickerProps { | |
selectedModel: modelID; | |
setSelectedModel: (model: modelID) => void; | |
} | |
export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProps) => { | |
const [hoveredModel, setHoveredModel] = useState<modelID | null>(null); | |
// Ensure we always have a valid model ID | |
const validModelId = MODELS.includes(selectedModel) ? selectedModel : defaultModel; | |
// If the selected model is invalid, update it to the default | |
useEffect(() => { | |
if (selectedModel !== validModelId) { | |
setSelectedModel(validModelId as modelID); | |
} | |
}, [selectedModel, validModelId, setSelectedModel]); | |
// Function to get the appropriate icon for each provider | |
const getProviderIcon = (provider: string) => { | |
switch (provider.toLowerCase()) { | |
case 'xai': | |
return <Sparkles className="h-3 w-3 text-yellow-500" />; | |
case 'openai': | |
return <Zap className="h-3 w-3 text-green-500" />; | |
default: | |
return <Info className="h-3 w-3 text-blue-500" />; | |
} | |
}; | |
// Function to get capability icon | |
const getCapabilityIcon = (capability: string) => { | |
switch (capability.toLowerCase()) { | |
case 'code': | |
return <Code className="h-2.5 w-2.5" />; | |
case 'reasoning': | |
return <Brain className="h-2.5 w-2.5" />; | |
case 'research': | |
return <Lightbulb className="h-2.5 w-2.5" />; | |
case 'vision': | |
return <Image className="h-2.5 w-2.5" />; | |
case 'fast': | |
case 'rapid': | |
return <Bolt className="h-2.5 w-2.5" />; | |
case 'efficient': | |
case 'compact': | |
return <Gauge className="h-2.5 w-2.5" />; | |
case 'creative': | |
case 'balance': | |
return <Rocket className="h-2.5 w-2.5" />; | |
case 'agentic': | |
return <Bot className="h-2.5 w-2.5" />; | |
default: | |
return <Info className="h-2.5 w-2.5" />; | |
} | |
}; | |
// Get capability badge color | |
const getCapabilityColor = (capability: string) => { | |
switch (capability.toLowerCase()) { | |
case 'code': | |
return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"; | |
case 'reasoning': | |
case 'research': | |
return "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300"; | |
case 'vision': | |
return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300"; | |
case 'fast': | |
case 'rapid': | |
return "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300"; | |
case 'efficient': | |
case 'compact': | |
return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300"; | |
case 'creative': | |
case 'balance': | |
return "bg-rose-100 text-rose-800 dark:bg-rose-900/30 dark:text-rose-300"; | |
case 'agentic': | |
return "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300"; | |
default: | |
return "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300"; | |
} | |
}; | |
// Get current model details to display | |
const displayModelId = hoveredModel || validModelId; | |
const currentModelDetails = modelDetails[displayModelId]; | |
// Handle model change | |
const handleModelChange = (modelId: string) => { | |
if (MODELS.includes(modelId)) { | |
const typedModelId = modelId as modelID; | |
setSelectedModel(typedModelId); | |
} | |
}; | |
return ( | |
<div className="absolute bottom-2 left-2 z-10"> | |
<Select | |
value={validModelId} | |
onValueChange={handleModelChange} | |
defaultValue={validModelId} | |
> | |
<SelectTrigger | |
className="max-w-[150px] sm:max-w-none sm:w-48 px-2 sm:px-3 h-8 sm:h-9 rounded-full group border-border/80 bg-background/80 backdrop-blur-sm hover:bg-muted/30 transition-all duration-200" | |
> | |
<SelectValue | |
placeholder="Select model" | |
className="text-xs font-medium flex items-center gap-1 sm:gap-2" | |
> | |
<div className="flex items-center gap-1 sm:gap-2"> | |
{getProviderIcon(modelDetails[validModelId].provider)} | |
<TextMorph>{modelDetails[validModelId].name}</TextMorph> | |
</div> | |
</SelectValue> | |
</SelectTrigger> | |
<SelectContent | |
align="start" | |
className="bg-background/95 dark:bg-muted/95 backdrop-blur-sm border-border/80 rounded-lg overflow-hidden p-0 w-[280px] sm:w-[350px] md:w-[485px]" | |
> | |
<div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] md:grid-cols-[170px_1fr] items-start"> | |
{/* Model selector column */} | |
<div className="sm:border-r border-border/40 bg-muted/20 p-2"> | |
<SelectGroup className="space-y-1"> | |
{MODELS.map((id) => { | |
const modelId = id as modelID; | |
return ( | |
<SelectItem | |
key={id} | |
value={id} | |
onMouseEnter={() => setHoveredModel(modelId)} | |
onMouseLeave={() => setHoveredModel(null)} | |
className={cn( | |
"!px-2 sm:!px-3 py-1.5 sm:py-2 cursor-pointer rounded-md text-xs transition-colors duration-150", | |
"hover:bg-primary/5 hover:text-primary-foreground", | |
"focus:bg-primary/10 focus:text-primary focus:outline-none", | |
"data-[highlighted]:bg-primary/10 data-[highlighted]:text-primary", | |
validModelId === id && "!bg-primary/15 !text-primary font-medium" | |
)} | |
> | |
<div className="flex flex-col gap-0.5"> | |
<div className="flex items-center gap-1.5"> | |
{getProviderIcon(modelDetails[modelId].provider)} | |
<span className="font-medium truncate">{modelDetails[modelId].name}</span> | |
</div> | |
<span className="text-[10px] sm:text-xs text-muted-foreground"> | |
{modelDetails[modelId].provider} | |
</span> | |
</div> | |
</SelectItem> | |
); | |
})} | |
</SelectGroup> | |
</div> | |
{/* Model details column - hidden on smallest screens, visible on sm+ */} | |
<div className="p-2 sm:p-3 md:p-4 flex flex-col sm:block hidden"> | |
<div> | |
<div className="flex items-center gap-2 mb-1"> | |
{getProviderIcon(currentModelDetails.provider)} | |
<h3 className="text-sm font-semibold">{currentModelDetails.name}</h3> | |
</div> | |
<div className="text-xs text-muted-foreground mb-1"> | |
Provider: <span className="font-medium">{currentModelDetails.provider}</span> | |
</div> | |
{/* Capability badges */} | |
<div className="flex flex-wrap gap-1 mt-2 mb-3"> | |
{currentModelDetails.capabilities.map((capability) => ( | |
<span | |
key={capability} | |
className={cn( | |
"inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded-full font-medium", | |
getCapabilityColor(capability) | |
)} | |
> | |
{getCapabilityIcon(capability)} | |
<span>{capability}</span> | |
</span> | |
))} | |
</div> | |
<div className="text-xs text-foreground/90 leading-relaxed mb-3 hidden md:block"> | |
{currentModelDetails.description} | |
</div> | |
</div> | |
<div className="bg-muted/40 rounded-md p-2 hidden md:block"> | |
<div className="text-[10px] text-muted-foreground flex justify-between items-center"> | |
<span>API Version:</span> | |
<code className="bg-background/80 px-2 py-0.5 rounded text-[10px] font-mono"> | |
{currentModelDetails.apiVersion} | |
</code> | |
</div> | |
</div> | |
</div> | |
{/* Condensed model details for mobile only */} | |
<div className="p-3 sm:hidden border-t border-border/30"> | |
<div className="flex flex-wrap gap-1 mb-2"> | |
{currentModelDetails.capabilities.slice(0, 4).map((capability) => ( | |
<span | |
key={capability} | |
className={cn( | |
"inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded-full font-medium", | |
getCapabilityColor(capability) | |
)} | |
> | |
{getCapabilityIcon(capability)} | |
<span>{capability}</span> | |
</span> | |
))} | |
{currentModelDetails.capabilities.length > 4 && ( | |
<span className="text-[10px] text-muted-foreground">+{currentModelDetails.capabilities.length - 4} more</span> | |
)} | |
</div> | |
</div> | |
</div> | |
</SelectContent> | |
</Select> | |
</div> | |
); | |
}; | |