oxios 1.5.2

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { CircleX, Plus, RouteIcon, Zap } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { useRoutingConfig, useSetRouting } from '@/hooks/use-engine'
import type { RoutingConfig } from '@/types/routing'

// ─── Translation keys ─────────────────────────────────────────
const tKeys = {
  routingTitle: 'settings.routing.title',
  routingDesc: 'settings.routing.desc',
  autoRouting: 'settings.routing.auto',
  autoRoutingDesc: 'settings.routing.autoDesc',
  costEfficient: 'settings.routing.costEfficient',
  costEfficientDesc: 'settings.routing.costEfficientDesc',
  fallbacks: 'settings.routing.fallbacks',
  fallbacksDesc: 'settings.routing.fallbacksDesc',
  excludedModels: 'settings.routing.excludedModels',
  excludedModelsDesc: 'settings.routing.excludedModelsDesc',
  addModel: 'settings.routing.addModel',
} as const

// ─── RoutingSection ───────────────────────────────────────────

export function RoutingSection() {
  const { t } = useTranslation()
  const { data: routing } = useRoutingConfig()
  const setRouting = useSetRouting()

  const update = (patch: Partial<RoutingConfig>) => setRouting.mutate(patch)

  if (!routing) return null

  return (
    <div className="space-y-6">
      <Separator />

      <div className="space-y-4">
        <div className="flex items-center gap-2">
          <RouteIcon className="h-4 w-4 text-muted-foreground" />
          <h3 className="text-lg font-semibold">{t(tKeys.routingTitle)}</h3>
        </div>
        <p className="text-sm text-muted-foreground">{t(tKeys.routingDesc)}</p>

        {/* Auto routing toggle */}
        <div className="flex items-start justify-between gap-4">
          <div className="space-y-0.5">
            <Label>{t(tKeys.autoRouting)}</Label>
            <p className="text-xs text-muted-foreground">{t(tKeys.autoRoutingDesc)}</p>
          </div>
          <Switch
            checked={routing.routingEnabled}
            onCheckedChange={(v) => update({ routingEnabled: v })}
          />
        </div>

        {/* Cost efficient toggle */}
        <div className="flex items-start justify-between gap-4">
          <div className="space-y-0.5">
            <Label
              className={`flex items-center gap-1 ${!routing.routingEnabled ? 'text-muted-foreground/50' : ''}`}
            >
              <Zap className="h-3.5 w-3.5 text-warning" />
              {t(tKeys.costEfficient)}
            </Label>
            <p className="text-xs text-muted-foreground">{t(tKeys.costEfficientDesc)}</p>
          </div>
          <Switch
            checked={routing.preferCostEfficient}
            onCheckedChange={(v) => update({ preferCostEfficient: v })}
            disabled={!routing.routingEnabled}
          />
        </div>

        <Separator />

        {/* Fallback models */}
        <FallbackModelsEditor
          models={routing.fallbackModels}
          onAdd={(m) => update({ fallbackModels: [...routing.fallbackModels, m] })}
          onRemove={(i) =>
            update({ fallbackModels: routing.fallbackModels.filter((_, idx) => idx !== i) })
          }
          disabled={!routing.routingEnabled}
        />

        {/* Excluded models */}
        <ExcludedModelsEditor
          models={routing.excludedModels}
          onAdd={(m) => update({ excludedModels: [...routing.excludedModels, m] })}
          onRemove={(m) =>
            update({ excludedModels: routing.excludedModels.filter((x) => x !== m) })
          }
          disabled={!routing.routingEnabled}
        />
      </div>
    </div>
  )
}

// ─── FallbackModelsEditor ────────────────────────────────────

function FallbackModelsEditor({
  models,
  onAdd,
  onRemove,
  disabled,
}: {
  models: string[]
  onAdd: (m: string) => void
  onRemove: (i: number) => void
  disabled?: boolean
}) {
  const { t } = useTranslation()
  const [newModel, setNewModel] = useState('')

  const handleAdd = () => {
    const trimmed = newModel.trim()
    if (!trimmed) return
    onAdd(trimmed)
    setNewModel('')
  }

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') {
      e.preventDefault()
      handleAdd()
    }
  }

  return (
    <div className="space-y-2">
      <Label>{t(tKeys.fallbacks)}</Label>
      <p className="text-xs text-muted-foreground">{t(tKeys.fallbacksDesc)}</p>

      <div className="space-y-2">
        {models.map((model, i) => (
          <div key={i} className="flex items-center gap-2">
            <span className="w-6 text-sm text-muted-foreground text-right shrink-0">{i + 1}.</span>
            <div className="flex-1 rounded-md border px-3 py-2 text-sm bg-muted/30">{model}</div>
            <Button
              variant="ghost"
              size="sm"
              className="h-8 w-8 p-0 shrink-0"
              onClick={() => onRemove(i)}
              title="Remove"
              disabled={disabled}
            >
              <CircleX className="h-4 w-4 text-muted-foreground" />
            </Button>
          </div>
        ))}

        <div className="flex items-center gap-2">
          <span className="w-6 shrink-0" />
          <Input
            value={newModel}
            onChange={(e) => setNewModel(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder="provider/model-id"
            className="h-8 text-sm flex-1"
            disabled={disabled}
          />
          <Button
            variant="outline"
            size="sm"
            className="h-8 shrink-0"
            onClick={handleAdd}
            disabled={!newModel.trim() || disabled}
          >
            <Plus className="h-3.5 w-3.5 mr-1" />
            {t(tKeys.addModel)}
          </Button>
        </div>
      </div>
    </div>
  )
}

// ─── ExcludedModelsEditor ─────────────────────────────────────

function ExcludedModelsEditor({
  models,
  onAdd,
  onRemove,
  disabled,
}: {
  models: string[]
  onAdd: (m: string) => void
  onRemove: (m: string) => void
  disabled?: boolean
}) {
  const { t } = useTranslation()
  const [newModel, setNewModel] = useState('')

  const handleAdd = () => {
    const trimmed = newModel.trim()
    if (!trimmed) return
    onAdd(trimmed)
    setNewModel('')
  }

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') {
      e.preventDefault()
      handleAdd()
    }
  }

  return (
    <div className="space-y-2">
      <Label>{t(tKeys.excludedModels)}</Label>
      <p className="text-xs text-muted-foreground">{t(tKeys.excludedModelsDesc)}</p>

      <div className="flex flex-wrap gap-2">
        {models.map((model) => (
          <span
            key={model}
            className="inline-flex items-center gap-1 rounded-full bg-muted px-3 py-1 text-sm"
          >
            <span className="text-xs text-muted-foreground">🚫</span>
            {model}
            <button
              type="button"
              className={`ml-1 ${disabled ? 'text-muted-foreground/30 cursor-not-allowed' : 'text-muted-foreground hover:text-foreground'}`}
              onClick={() => !disabled && onRemove(model)}
              disabled={disabled}
            >
              <CircleX className="h-3 w-3" />
            </button>
          </span>
        ))}

        <div className="flex items-center gap-1">
          <Input
            value={newModel}
            onChange={(e) => setNewModel(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder="provider/model-id"
            className="h-7 w-48 text-xs"
            disabled={disabled}
          />
          <Button
            variant="ghost"
            size="sm"
            className="h-7 px-2"
            onClick={handleAdd}
            disabled={!newModel.trim() || disabled}
          >
            <Plus className="h-3.5 w-3.5" />
          </Button>
        </div>
      </div>
    </div>
  )
}