bookwiz.io / components / ModelSelector.tsx
ModelSelector.tsx
Raw
'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>
  )
}