oxios 1.5.2

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { useEffect, 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 { Select } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'

// ─── Provider-specific option schemas ────────────────────────

interface OptionDef {
  key: string
  labelKey: string
  descriptionKey: string
  type: 'select' | 'number'
  options?: { value: string; labelKey: string }[]
  placeholderKey?: string
}

const PROVIDER_OPTION_SCHEMAS: Record<string, OptionDef[]> = {
  anthropic: [
    {
      key: 'thinking_type',
      labelKey: 'engine.thinkingType',
      descriptionKey: 'engine.thinkingTypeDescription',
      type: 'select',
      options: [
        { value: 'enabled', labelKey: 'engine.enabled' },
        { value: 'disabled', labelKey: 'engine.disabled' },
      ],
    },
    {
      key: 'thinking_budget_tokens',
      labelKey: 'engine.thinkingBudget',
      descriptionKey: 'engine.thinkingBudgetDescription',
      type: 'number',
      placeholderKey: 'engine.thinkingBudgetPlaceholder',
    },
  ],
  openai: [
    {
      key: 'reasoning_effort',
      labelKey: 'engine.reasoningEffort',
      descriptionKey: 'engine.reasoningEffortDescription',
      type: 'select',
      options: [
        { value: 'low', labelKey: 'engine.low' },
        { value: 'medium', labelKey: 'engine.medium' },
        { value: 'high', labelKey: 'engine.high' },
      ],
    },
    {
      key: 'text_verbosity',
      labelKey: 'engine.textVerbosity',
      descriptionKey: 'engine.textVerbosityDescription',
      type: 'select',
      options: [
        { value: 'low', labelKey: 'engine.verbosityLow' },
        { value: 'medium', labelKey: 'engine.verbosityMedium' },
        { value: 'high', labelKey: 'engine.verbosityHigh' },
      ],
    },
  ],
  google: [
    {
      key: 'thinking_level',
      labelKey: 'engine.thinkingLevel',
      descriptionKey: 'engine.thinkingLevelDescription',
      type: 'select',
      options: [
        { value: 'none', labelKey: 'engine.thinkingNone' },
        { value: 'light', labelKey: 'engine.thinkingLight' },
        { value: 'medium', labelKey: 'engine.thinkingMedium' },
        { value: 'heavy', labelKey: 'engine.thinkingHeavy' },
      ],
    },
    {
      key: 'thinking_budget',
      labelKey: 'engine.thinkingBudget',
      descriptionKey: 'engine.thinkingBudgetDescription',
      type: 'number',
      placeholderKey: 'engine.thinkingBudgetPlaceholder',
    },
  ],
}

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

interface ProviderOptionsProps {
  provider: string
  /** Current option values */
  values: Record<string, unknown>
  /** Called when an option changes */
  onChange: (key: string, value: string | number) => void
  className?: string
}

export function ProviderOptions({ provider, values, onChange, className }: ProviderOptionsProps) {
  const { t } = useTranslation()
  const schema = PROVIDER_OPTION_SCHEMAS[provider]

  if (!schema || schema.length === 0) {
    return (
      <div className={className}>
        <p className="text-sm text-muted-foreground">
          {t('engine.noAdvancedOptionsFor', { provider })}
        </p>
      </div>
    )
  }

  return (
    <div className={className}>
      <div className="space-y-4">
        {schema.map((opt, i) => {
          // Compute disabled state for thinking-budget fields
          // that depend on their parent thinking-type/thinking-level select.
          const isDisabled =
            (opt.key === 'thinking_budget_tokens' && values['thinking_type'] === 'disabled') ||
            (opt.key === 'thinking_budget' && values['thinking_level'] === 'none')

          return (
            <div key={opt.key}>
              {i > 0 && <Separator className="mb-4" />}
              <div className="flex items-start justify-between gap-6">
                <div className="flex-1 min-w-0 pt-0.5">
                  <Label
                    className={`text-sm font-medium ${isDisabled ? 'text-muted-foreground/50' : ''}`}
                  >
                    {t(opt.labelKey)}
                  </Label>
                  <p className="text-xs text-muted-foreground mt-0.5">{t(opt.descriptionKey)}</p>
                </div>
                <div className="shrink-0 w-56">
                  <OptionControl
                    option={opt}
                    value={values[opt.key]}
                    onChange={(val) => onChange(opt.key, val)}
                    disabled={isDisabled}
                  />
                </div>
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}

// ─── Option control ──────────────────────────────────────────

function OptionControl({
  option,
  value,
  onChange,
  disabled,
}: {
  option: OptionDef
  value: unknown
  onChange: (val: string | number) => void
  disabled?: boolean
}) {
  const { t } = useTranslation()

  if (option.type === 'select' && option.options) {
    return (
      <Select
        value={String(value ?? '')}
        onValueChange={(val) => onChange(val)}
        options={option.options.map((o) => ({ value: o.value, label: t(o.labelKey) }))}
        placeholder={
          option.placeholderKey ? t(option.placeholderKey) : t('common.selectPlaceholder')
        }
        disabled={disabled}
      />
    )
  }

  // Number input
  return (
    <Input
      type="number"
      value={value !== undefined && value !== null ? String(value) : ''}
      onChange={(e) => {
        const num = Number(e.target.value)
        if (!Number.isNaN(num) && e.target.value !== '') {
          onChange(num)
        }
      }}
      placeholder={option.placeholderKey ? t(option.placeholderKey) : undefined}
      disabled={disabled}
    />
  )
}

// ─── Wrapper with local state management ─────────────────────

interface ProviderOptionsPanelProps {
  provider: string
  /** Initial option values */
  initialValues?: Record<string, unknown>
  /** Called when user wants to save options */
  onSave: (options: Record<string, unknown>) => void
  isPending?: boolean
  className?: string
}

/**
 * Full provider options panel with local state and save button.
 * Handles the edit → save lifecycle internally.
 */
export function ProviderOptionsPanel({
  provider,
  initialValues = {},
  onSave,
  isPending,
  className,
}: ProviderOptionsPanelProps) {
  const { t } = useTranslation()
  const [localValues, setLocalValues] = useState<Record<string, unknown>>(initialValues)

  // Reset when provider changes
  useEffect(() => {
    setLocalValues(initialValues)
  }, [provider, initialValues])

  const handleChange = (key: string, value: string | number) => {
    setLocalValues((prev) => ({ ...prev, [key]: value }))
  }

  const handleSave = () => {
    onSave(localValues)
  }

  return (
    <div className={className}>
      <ProviderOptions provider={provider} values={localValues} onChange={handleChange} />
      <div className="mt-4 flex justify-end">
        <Button onClick={handleSave} disabled={isPending}>
          {isPending ? t('engine.saving') : t('engine.saveOptions')}
        </Button>
      </div>
    </div>
  )
}