'use client'
import React, { useState, useRef, useEffect } from 'react'
import { ChevronDownIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
import { MODELS, ModelConfig } from '@/lib/config/models'
interface UsageInfo {
smartUsage: number
fastUsage: number
smartLimit: number
fastLimit: number
}
interface ModelSelectorProps {
selectedModel: string
onModelChange: (modelName: string) => void
disabled?: boolean
className?: string
variant?: 'default' | 'compact' | 'demo' | 'minimal'
usageInfo?: UsageInfo | null
models?: ModelConfig[]
}
export default function ModelSelector({
selectedModel,
onModelChange,
disabled = false,
className = '',
variant = 'default',
usageInfo = null,
models = MODELS
}: ModelSelectorProps) {
const [isOpen, setIsOpen] = useState(false)
const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>('bottom')
const [showInfoFor, setShowInfoFor] = useState<string | null>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
setShowInfoFor(null)
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [isOpen])
// Determine dropdown position
useEffect(() => {
if (dropdownRef.current && isOpen) {
const rect = dropdownRef.current.getBoundingClientRect()
const spaceBelow = window.innerHeight - rect.bottom
const spaceAbove = rect.top
if (spaceBelow < 200 && spaceAbove > spaceBelow) {
setDropdownPosition('top')
} else {
setDropdownPosition('bottom')
}
}
}, [isOpen])
const handleModelSelect = (model: ModelConfig) => {
onModelChange(model.name)
setIsOpen(false)
setShowInfoFor(null)
}
const handleInfoClick = (e: React.MouseEvent, modelId: string) => {
e.stopPropagation()
setShowInfoFor(showInfoFor === modelId ? null : modelId)
}
const selectedModelConfig = models.find(model => model.name === selectedModel) || models[0]
// Get usage info for a specific model tier
const getUsageInfo = (modelTier: 'smart' | 'fast') => {
if (!usageInfo) return null
const usage = modelTier === 'smart' ? usageInfo.smartUsage : usageInfo.fastUsage
const limit = modelTier === 'smart' ? usageInfo.smartLimit : usageInfo.fastLimit
const percentage = (usage / limit) * 100
let warningLevel: 'none' | 'warning' | 'critical' = 'none'
if (percentage >= 90) {
warningLevel = 'critical'
} else if (percentage >= 75) {
warningLevel = 'warning'
}
return { usage, limit, percentage, warningLevel }
}
// Get provider icon
const getProviderIcon = (provider: string) => {
switch (provider.toLowerCase()) {
case 'anthropic':
return '๐ค'
case 'openai':
return 'โก'
case 'google':
return '๐'
case 'moonshotai':
return '๐'
case 'mistral':
return '๐ช๏ธ'
default:
return '๐ค'
}
}
// Get provider color
const getProviderColor = (provider: string) => {
switch (provider.toLowerCase()) {
case 'anthropic':
return 'text-orange-400'
case 'openai':
return 'text-green-400'
case 'google':
return 'text-blue-400'
case 'moonshotai':
return 'text-purple-400'
case 'mistral':
return 'text-cyan-400'
default:
return 'text-slate-400'
}
}
// Get tier badge styles and text
const getTierBadge = (tier: 'smart' | 'fast', size: 'sm' | 'xs' = 'xs') => {
const isSmart = tier === 'smart'
const sizeClasses = size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-1.5 py-0.5 text-[10px]'
return {
text: isSmart ? '๐ง Smart' : 'โก Fast',
className: `
${sizeClasses} font-medium rounded-full
${isSmart
? 'bg-purple-500/20 text-purple-400 border border-purple-500/30'
: 'bg-blue-500/20 text-blue-400 border border-blue-500/30'
}
`
}
}
// Get special badge styles and text
const getSpecialBadge = (badge: 'nsfw' | 'experimental' | 'beta', size: 'sm' | 'xs' = 'xs') => {
const sizeClasses = size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-1.5 py-0.5 text-[10px]'
switch (badge) {
case 'nsfw':
return {
text: '18+',
className: `
${sizeClasses} font-medium rounded-full
bg-red-500/20 text-red-400 border border-red-500/30
`
}
case 'experimental':
return {
text: 'EXP',
className: `
${sizeClasses} font-medium rounded-full
bg-purple-500/20 text-purple-400 border border-purple-500/30
`
}
case 'beta':
return {
text: 'BETA',
className: `
${sizeClasses} font-medium rounded-full
bg-orange-500/20 text-orange-400 border border-orange-500/30
`
}
default:
return null
}
}
// Variant-specific styles
const getVariantStyles = () => {
switch (variant) {
case 'compact':
return {
button: 'px-2 py-1 text-xs bg-slate-600/50 rounded-md',
dropdown: 'mt-1 w-64',
option: 'px-3 py-2 text-sm'
}
case 'demo':
return {
button: 'px-2 py-1 text-xs bg-black/20 rounded',
dropdown: 'mt-1 w-72',
option: 'px-3 py-2 text-sm'
}
case 'minimal':
return {
button: 'text-xs bg-transparent hover:text-white focus:outline-none focus:ring-0 border-0 cursor-pointer transition-colors',
dropdown: 'mt-1 w-64',
option: 'px-3 py-2 text-sm'
}
default:
return {
button: 'px-3 py-2 text-sm bg-slate-700/50 rounded-lg',
dropdown: 'mt-2 w-80',
option: 'px-4 py-3 text-sm'
}
}
}
const styles = getVariantStyles()
return (
<div className={`relative ${className}`} ref={dropdownRef}>
{/* Selected Model Button */}
<button
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
className={`
${styles.button}
${variant === 'minimal'
? 'text-slate-300 hover:text-white flex items-center gap-1.5'
: 'flex items-center justify-between w-full text-slate-200 hover:bg-slate-600/70 border border-slate-600/50 hover:border-slate-500/50'
}
transition-all duration-200
disabled:opacity-50 disabled:cursor-not-allowed
${variant !== 'minimal' && isOpen ? 'ring-1 ring-teal-500/30' : ''}
`}
>
{variant === 'minimal' ? (
<>
<span className="truncate text-xs">{selectedModelConfig.name}</span>
<ChevronDownIcon
className={`w-3 h-3 transition-transform duration-200 flex-shrink-0 ${isOpen ? 'rotate-180' : ''}`}
/>
</>
) : (
<>
<div className="flex items-center gap-2 min-w-0">
<span className={getProviderColor(selectedModelConfig.provider)}>
{getProviderIcon(selectedModelConfig.provider)}
</span>
<span className="truncate">{selectedModelConfig.name}</span>
{/* Special badge (NSFW, etc.) */}
{selectedModelConfig.badge && (
<span className={getSpecialBadge(selectedModelConfig.badge, variant === 'compact' ? 'xs' : 'xs')?.className}>
{getSpecialBadge(selectedModelConfig.badge, variant === 'compact' ? 'xs' : 'xs')?.text}
</span>
)}
{/* Tier badge for selected model */}
<span className={getTierBadge(selectedModelConfig.tier, variant === 'compact' ? 'xs' : 'xs').className}>
{getTierBadge(selectedModelConfig.tier, variant === 'compact' ? 'xs' : 'xs').text}
</span>
</div>
<ChevronDownIcon
className={`w-4 h-4 transition-transform duration-200 flex-shrink-0 ${
isOpen ? 'rotate-180' : ''
}`}
/>
</>
)}
</button>
{/* Dropdown */}
{isOpen && (
<div className={`
${styles.dropdown}
absolute z-50 bg-slate-800/95 backdrop-blur-sm border border-slate-600/50 rounded-lg shadow-xl
overflow-hidden animate-in fade-in-0 zoom-in-95 duration-200
${dropdownPosition === 'top' ? 'bottom-full mb-2' : 'top-full mt-2'}
`}>
<div className="max-h-48 overflow-y-auto">
{models.map((model) => {
const modelUsage = getUsageInfo(model.tier)
const isLimitReached = modelUsage?.warningLevel === 'critical'
return (
<div
key={model.id}
className={`
${styles.option}
transition-colors duration-150
border-b border-slate-700/50 last:border-b-0
${model.name === selectedModel ? 'bg-teal-600/10 border-l-2 border-l-teal-500' : ''}
${isLimitReached
? 'opacity-50 cursor-not-allowed bg-red-900/10'
: 'cursor-pointer hover:bg-slate-700/50'
}
`}
onClick={() => !isLimitReached && handleModelSelect(model)}
>
<div className="flex items-center gap-3">
<span className={`${getProviderColor(model.provider)} text-lg flex-shrink-0`}>
{getProviderIcon(model.provider)}
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="font-medium text-slate-200 truncate">
{model.name}
</span>
<div className="flex items-center gap-1">
{/* Info button */}
<button
onClick={(e) => handleInfoClick(e, model.id)}
className="p-0.5 text-slate-400 hover:text-slate-200 hover:bg-slate-600/50 rounded transition-colors"
title="Model info"
>
<QuestionMarkCircleIcon className="w-3 h-3" />
</button>
{/* Special badge (NSFW, etc.) */}
{model.badge && (
<span className={getSpecialBadge(model.badge, 'xs')?.className}>
{getSpecialBadge(model.badge, 'xs')?.text}
</span>
)}
{/* Tier badge for dropdown options */}
<span className={getTierBadge(model.tier, 'xs').className}>
{getTierBadge(model.tier, 'xs').text}
</span>
</div>
</div>
<div className="text-xs text-slate-400 capitalize mt-0.5">
{model.provider}
</div>
</div>
</div>
{/* Model description popup */}
{showInfoFor === model.id && (
<div className="mt-2 p-3 bg-slate-900/80 border border-slate-600/50 rounded-lg">
<div className="text-xs text-slate-300 leading-relaxed whitespace-pre-line">
{model.description}
</div>
</div>
)}
</div>
)
})}
</div>
</div>
)}
</div>
)
}