oxios 1.5.2

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { Check, ChevronDown } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import type { ModelInfo } from '@/types/engine'

// ─── Helpers ─────────────────────────────────────────────────

function formatContextWindow(tokens: number): string {
  if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`
  if (tokens >= 1000) return `${Math.round(tokens / 1000)}K`
  return String(tokens)
}

function formatCost(cost: number): string {
  if (cost === 0) return 'Free'
  if (cost < 0.01) return '<$0.01'
  return `$${cost.toFixed(2)}`
}

// ─── Component ───────────────────────────────────────────────

interface ModelSelectProps {
  models: ModelInfo[]
  value: string | null
  onValueChange: (modelId: string) => void
  className?: string
}

export function ModelSelect({ models, value, onValueChange, className }: ModelSelectProps) {
  const { t } = useTranslation()
  const [open, setOpen] = useState(false)
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    function handleClickOutside(e: MouseEvent) {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
    }
    if (open) document.addEventListener('mousedown', handleClickOutside)
    return () => document.removeEventListener('mousedown', handleClickOutside)
  }, [open])

  const selected = models.find((m) => m.id === value)

  // Separate reasoning and non-reasoning models
  const reasoningModels = models.filter((m) => m.reasoning)
  const standardModels = models.filter((m) => !m.reasoning)

  return (
    <div className={cn('relative', className)} ref={ref}>
      <button
        type="button"
        onClick={() => setOpen(!open)}
        disabled={models.length === 0}
        className="flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background hover:bg-accent/50 focus:outline-none focus:ring-1 focus:ring-ring disabled:opacity-50 disabled:cursor-not-allowed"
      >
        <span className={selected ? '' : 'text-muted-foreground'}>
          {selected ? (
            <span className="flex items-center gap-1.5">
              {selected.reasoning && <span title={t('engine.supportsReasoning')}></span>}
              {selected.input.includes('image') && (
                <span title={t('engine.supportsVision')}>👁</span>
              )}
              {selected.name}
            </span>
          ) : models.length === 0 ? (
            t('engine.noModelsAvailable')
          ) : (
            t('engine.selectModel')
          )}
        </span>
        <ChevronDown className="h-4 w-4 opacity-50" />
      </button>

      {open && models.length > 0 && (
        <div className="absolute z-50 mt-1 w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md max-h-96 overflow-y-auto">
          {/* Reasoning models */}
          {reasoningModels.length > 0 && (
            <div>
              <div className="px-3 py-1.5 text-xs font-semibold text-muted-foreground bg-muted/50 flex items-center gap-1">
                <span></span> {t('engine.reasoningModels')}
              </div>
              {reasoningModels.map((m) => (
                <ModelRow
                  key={m.id}
                  model={m}
                  selected={value === m.id}
                  onSelect={onValueChange}
                  onClose={() => setOpen(false)}
                />
              ))}
            </div>
          )}

          {/* Standard models */}
          {standardModels.length > 0 && (
            <div>
              {reasoningModels.length > 0 && (
                <div className="px-3 py-1.5 text-xs font-semibold text-muted-foreground bg-muted/50 flex items-center gap-1">
                  {t('engine.standardModels')}
                </div>
              )}
              {standardModels.map((m) => (
                <ModelRow
                  key={m.id}
                  model={m}
                  selected={value === m.id}
                  onSelect={onValueChange}
                  onClose={() => setOpen(false)}
                />
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  )
}

// ─── Model row ───────────────────────────────────────────────

function ModelRow({
  model,
  selected,
  onSelect,
  onClose,
}: {
  model: ModelInfo
  selected: boolean
  onSelect: (id: string) => void
  onClose: () => void
}) {
  const { t } = useTranslation()

  return (
    <button
      type="button"
      className={cn(
        'relative flex w-full cursor-pointer items-start gap-2 px-3 py-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground',
        selected && 'bg-accent text-accent-foreground',
      )}
      onClick={() => {
        onSelect(model.id)
        onClose()
      }}
    >
      {/* Selection indicator */}
      <span className="w-4 shrink-0 mt-0.5">{selected && <Check className="h-4 w-4" />}</span>

      {/* Model info */}
      <div className="flex-1 min-w-0 text-left">
        <div className="flex items-center gap-1.5">
          {model.reasoning && (
            <span className="text-warning text-xs" title={t('engine.supportsReasoning')}></span>
          )}
          {model.input.includes('image') && (
            <span className="text-info text-xs" title={t('engine.supportsVision')}>
              👁
            </span>
          )}
          <span className="font-medium truncate">{model.name}</span>
        </div>
        <div className="flex items-center gap-3 mt-0.5 text-xs text-muted-foreground">
          <span>
            {formatContextWindow(model.contextWindow)} {t('engine.ctx')}
          </span>
          <span>
            {t('engine.input')} {formatCost(model.costInput)}/M
          </span>
          <span>
            {t('engine.output')} {formatCost(model.costOutput)}/M
          </span>
        </div>
      </div>
    </button>
  )
}