{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-settings-panel",
  "title": "AI Settings Panel",
  "description": "Configure AI model parameters and behavior settings.",
  "registryDependencies": [
    "alert-dialog",
    "badge",
    "button",
    "card",
    "field",
    "input-group",
    "select",
    "separator",
    "slider",
    "textarea"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/ai/ai-settings-panel.tsx",
      "content": "\"use client\";\n\nimport { Loader2, RotateCcw, Save, Settings, Trash2 } from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@/registry/new-york/ui/alert-dialog\";\nimport { Badge } from \"@/registry/new-york/ui/badge\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/new-york/ui/card\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldLabel,\n} from \"@/registry/new-york/ui/field\";\nimport {\n  InputGroup,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/new-york/ui/select\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\nimport { Slider } from \"@/registry/new-york/ui/slider\";\nimport { Textarea } from \"@/registry/new-york/ui/textarea\";\n\nexport interface AISettings {\n  temperature?: number;\n  maxTokens?: number;\n  topP?: number;\n  topK?: number;\n  frequencyPenalty?: number;\n  presencePenalty?: number;\n  systemPrompt?: string;\n  model?: string;\n}\n\nexport interface AISettingsPreset {\n  id: string;\n  name: string;\n  description?: string;\n  settings: AISettings;\n}\n\nexport interface AISettingsPanelProps {\n  settings?: AISettings;\n  presets?: AISettingsPreset[];\n  onSettingsChange?: (settings: AISettings) => void;\n  onSave?: (settings: AISettings) => Promise<void>;\n  onReset?: () => void;\n  onLoadPreset?: (presetId: string) => void;\n  onSavePreset?: (name: string, settings: AISettings) => Promise<void>;\n  onDeletePreset?: (presetId: string) => Promise<void>;\n  className?: string;\n  showAdvanced?: boolean;\n  availableModels?: string[];\n}\n\nconst defaultSettings: AISettings = {\n  temperature: 0.7,\n  maxTokens: 2000,\n  topP: 1.0,\n  topK: 40,\n  frequencyPenalty: 0.0,\n  presencePenalty: 0.0,\n  systemPrompt: \"\",\n  model: \"gpt-4\",\n};\n\nfunction getTemperatureLabel(temperature: number): string {\n  if (temperature === 0) return \"Deterministic\";\n  if (temperature < 0.5) return \"Focused\";\n  if (temperature < 1.0) return \"Balanced\";\n  return \"Creative\";\n}\n\ninterface SettingsHeaderProps {\n  hasChanges: boolean;\n  isSaving: boolean;\n  onReset?: () => void;\n  onSave?: () => void;\n}\n\nfunction SettingsHeader({\n  hasChanges,\n  isSaving,\n  onReset,\n  onSave,\n}: SettingsHeaderProps) {\n  return (\n    <CardHeader>\n      <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n        <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n          <CardTitle className=\"wrap-break-word\">AI Settings</CardTitle>\n          <CardDescription className=\"wrap-break-word\">\n            Configure AI model parameters and behavior\n          </CardDescription>\n        </div>\n        <div className=\"flex flex-col gap-2 sm:shrink-0 sm:flex-row\">\n          {onReset && (\n            <Button\n              aria-label=\"Reset settings to defaults\"\n              className=\"min-h-[44px] w-full min-w-[44px] sm:min-h-[32px] sm:w-auto sm:min-w-[32px]\"\n              onClick={onReset}\n              type=\"button\"\n              variant=\"outline\"\n            >\n              <RotateCcw aria-hidden=\"true\" className=\"size-4\" />\n              <span className=\"whitespace-nowrap\">Reset</span>\n            </Button>\n          )}\n          {onSave && (\n            <Button\n              aria-busy={isSaving}\n              aria-label={isSaving ? \"Saving settings\" : \"Save settings\"}\n              className=\"min-h-[44px] w-full min-w-[44px] sm:min-h-[32px] sm:w-auto sm:min-w-[32px]\"\n              data-loading={isSaving}\n              disabled={!hasChanges || isSaving}\n              onClick={onSave}\n              type=\"button\"\n            >\n              {isSaving ? (\n                <>\n                  <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n                  <span className=\"whitespace-nowrap\">Saving…</span>\n                </>\n              ) : (\n                <>\n                  <Save aria-hidden=\"true\" className=\"size-4\" />\n                  <span className=\"whitespace-nowrap\">Save</span>\n                </>\n              )}\n            </Button>\n          )}\n        </div>\n      </div>\n    </CardHeader>\n  );\n}\n\ninterface PresetItemProps {\n  preset: AISettingsPreset;\n  onLoad: (presetId: string) => void;\n  onDelete?: (presetId: string) => Promise<void>;\n  isFocused: boolean;\n  presetRef: (node: HTMLButtonElement | null) => void;\n}\n\nfunction PresetItem({\n  preset,\n  onLoad,\n  onDelete,\n  isFocused,\n  presetRef,\n}: PresetItemProps) {\n  const [showDeleteDialog, setShowDeleteDialog] = useState(false);\n  const [isDeleting, setIsDeleting] = useState(false);\n\n  const handleDelete = useCallback(\n    async (e: React.MouseEvent) => {\n      e.stopPropagation();\n      if (!onDelete) return;\n      setShowDeleteDialog(true);\n    },\n    [onDelete]\n  );\n\n  const confirmDelete = useCallback(async () => {\n    if (!onDelete) return;\n    setIsDeleting(true);\n    try {\n      await onDelete(preset.id);\n      setShowDeleteDialog(false);\n    } catch (error) {\n      console.error(\"Failed to delete preset:\", error);\n    } finally {\n      setIsDeleting(false);\n    }\n  }, [onDelete, preset.id]);\n\n  return (\n    <div role=\"listitem\">\n      <Badge\n        aria-label={`Load preset ${preset.name}`}\n        className={cn(\n          \"min-h-[44px] w-fit min-w-[44px] cursor-pointer gap-2 px-0.5 pl-3.5 sm:min-h-[32px] sm:min-w-[32px]\",\n          isFocused && \"ring-2 ring-ring ring-offset-2\"\n        )}\n        onClick={() => onLoad(preset.id)}\n        onKeyDown={(e) => {\n          if (e.key === \"Enter\" || e.key === \" \") {\n            e.preventDefault();\n            onLoad(preset.id);\n          }\n        }}\n        ref={presetRef}\n        role=\"button\"\n        tabIndex={0}\n        variant=\"outline\"\n      >\n        <span>{preset.name}</span>\n        {onDelete && (\n          <Button\n            aria-label={`Delete preset ${preset.name}`}\n            className=\"min-h-[44px] min-w-[44px] rounded-full sm:min-h-[32px] sm:min-w-[32px]\"\n            onClick={handleDelete}\n            size=\"icon-sm\"\n            type=\"button\"\n          >\n            <Trash2 aria-hidden=\"true\" className=\"size-4\" />\n          </Button>\n        )}\n      </Badge>\n      <AlertDialog onOpenChange={setShowDeleteDialog} open={showDeleteDialog}>\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>Delete preset?</AlertDialogTitle>\n            <AlertDialogDescription>\n              Are you sure you want to delete &quot;{preset.name}&quot;? This\n              action cannot be undone.\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>\n            <AlertDialogAction\n              className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20\"\n              disabled={isDeleting}\n              onClick={confirmDelete}\n            >\n              {isDeleting ? (\n                <>\n                  <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n                  Deleting…\n                </>\n              ) : (\n                \"Delete\"\n              )}\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </div>\n  );\n}\n\ninterface PresetSelectorProps {\n  presets: AISettingsPreset[];\n  onLoadPreset: (presetId: string) => void;\n  onDeletePreset?: (presetId: string) => Promise<void>;\n}\n\nfunction PresetSelector({\n  presets,\n  onLoadPreset,\n  onDeletePreset,\n}: PresetSelectorProps) {\n  const [focusedIndex, setFocusedIndex] = useState<number | null>(null);\n  const presetRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n  const setPresetRef = useCallback(\n    (index: number) => (node: HTMLButtonElement | null) => {\n      presetRefs.current[index] = node;\n    },\n    []\n  );\n\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (\n        e.target instanceof HTMLInputElement ||\n        e.target instanceof HTMLTextAreaElement ||\n        e.target instanceof HTMLButtonElement\n      ) {\n        return;\n      }\n\n      if (presets.length === 0) return;\n\n      switch (e.key) {\n        case \"ArrowDown\": {\n          e.preventDefault();\n          const nextIndex =\n            focusedIndex === null\n              ? 0\n              : focusedIndex < presets.length - 1\n                ? focusedIndex + 1\n                : 0;\n          setFocusedIndex(nextIndex);\n          presetRefs.current[nextIndex]?.focus();\n          break;\n        }\n        case \"ArrowUp\": {\n          e.preventDefault();\n          const prevIndex =\n            focusedIndex === null\n              ? presets.length - 1\n              : focusedIndex > 0\n                ? focusedIndex - 1\n                : presets.length - 1;\n          setFocusedIndex(prevIndex);\n          presetRefs.current[prevIndex]?.focus();\n          break;\n        }\n        case \"Home\": {\n          e.preventDefault();\n          setFocusedIndex(0);\n          presetRefs.current[0]?.focus();\n          break;\n        }\n        case \"End\": {\n          e.preventDefault();\n          const lastIndex = presets.length - 1;\n          setFocusedIndex(lastIndex);\n          presetRefs.current[lastIndex]?.focus();\n          break;\n        }\n        case \"Escape\": {\n          if (focusedIndex !== null) {\n            e.preventDefault();\n            setFocusedIndex(null);\n            presetRefs.current[focusedIndex]?.blur();\n          }\n          break;\n        }\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [presets.length, focusedIndex]);\n\n  if (presets.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <FieldLabel id=\"presets-label\">Presets</FieldLabel>\n      <div\n        aria-labelledby=\"presets-label\"\n        className=\"flex flex-wrap gap-2\"\n        role=\"list\"\n      >\n        {presets.map((preset, index) => (\n          <PresetItem\n            isFocused={focusedIndex === index}\n            key={index}\n            onDelete={onDeletePreset}\n            onLoad={onLoadPreset}\n            preset={preset}\n            presetRef={setPresetRef(index)}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\ninterface TemperatureFieldProps {\n  value: number;\n  onChange: (value: number) => void;\n}\n\nfunction TemperatureField({ value, onChange }: TemperatureFieldProps) {\n  const label = useMemo(() => getTemperatureLabel(value), [value]);\n\n  return (\n    <Field>\n      <div className=\"flex items-center justify-between\">\n        <FieldLabel htmlFor=\"temperature\">\n          Temperature: {value.toFixed(1)}\n        </FieldLabel>\n        <Badge\n          aria-label={`Temperature mode: ${label}`}\n          className=\"text-xs\"\n          variant=\"secondary\"\n        >\n          {label}\n        </Badge>\n      </div>\n      <FieldContent>\n        <Slider\n          aria-label=\"Temperature\"\n          id=\"temperature\"\n          max={2}\n          min={0}\n          onValueChange={(values) => onChange(values[0])}\n          step={0.1}\n          value={[value]}\n        />\n        <FieldDescription>\n          Controls randomness. Lower values make responses more focused and\n          deterministic.\n        </FieldDescription>\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface MaxTokensFieldProps {\n  value: number;\n  onChange: (value: number) => void;\n}\n\nfunction MaxTokensField({ value, onChange }: MaxTokensFieldProps) {\n  return (\n    <Field>\n      <FieldLabel htmlFor=\"max-tokens\">Max Tokens</FieldLabel>\n      <FieldContent>\n        <InputGroup>\n          <InputGroupInput\n            aria-label=\"Maximum tokens\"\n            id=\"max-tokens\"\n            max={32_000}\n            min={1}\n            onChange={(e) => {\n              const numValue = Number.parseInt(e.target.value) || 2000;\n              onChange(numValue);\n            }}\n            type=\"number\"\n            value={value}\n          />\n        </InputGroup>\n        <FieldDescription>\n          Maximum number of tokens in the response\n        </FieldDescription>\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface SystemPromptFieldProps {\n  value: string;\n  onChange: (value: string) => void;\n}\n\nfunction SystemPromptField({ value, onChange }: SystemPromptFieldProps) {\n  return (\n    <Field>\n      <FieldLabel htmlFor=\"system-prompt\">System Prompt</FieldLabel>\n      <FieldContent>\n        <Textarea\n          aria-label=\"System prompt\"\n          id=\"system-prompt\"\n          onChange={(e) => onChange(e.target.value)}\n          placeholder=\"Enter system prompt to guide AI behavior…\"\n          rows={4}\n          value={value}\n        />\n        <FieldDescription>\n          Instructions that guide the AI&apos;s behavior and responses\n        </FieldDescription>\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface ModelSelectFieldProps {\n  value: string;\n  availableModels: string[];\n  onChange: (value: string) => void;\n}\n\nfunction ModelSelectField({\n  value,\n  availableModels,\n  onChange,\n}: ModelSelectFieldProps) {\n  if (availableModels.length === 0) {\n    return null;\n  }\n\n  return (\n    <Field>\n      <FieldLabel htmlFor=\"model\">Model</FieldLabel>\n      <FieldContent>\n        <div className=\"[&_button]:min-h-[44px] [&_button]:sm:min-h-[32px]\">\n          <Select\n            onValueChange={onChange}\n            value={value || availableModels[0] || \"\"}\n          >\n            <SelectTrigger id=\"model\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              {availableModels.map((model) => (\n                <SelectItem key={model} value={model}>\n                  {model}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface AdvancedSettingsSectionProps {\n  settings: AISettings;\n  onSettingChange: <K extends keyof AISettings>(\n    key: K,\n    value: AISettings[K]\n  ) => void;\n}\n\nfunction AdvancedSettingsSection({\n  settings,\n  onSettingChange,\n}: AdvancedSettingsSectionProps) {\n  return (\n    <div className=\"flex flex-col gap-4 rounded-lg border bg-muted/30 p-4\">\n      <Field>\n        <div className=\"flex items-center justify-between\">\n          <FieldLabel htmlFor=\"top-p\">\n            Top P: {settings.topP?.toFixed(2) || \"1.00\"}\n          </FieldLabel>\n        </div>\n        <FieldContent>\n          <Slider\n            aria-label=\"Top P\"\n            id=\"top-p\"\n            max={1}\n            min={0}\n            onValueChange={(values) => onSettingChange(\"topP\", values[0])}\n            step={0.01}\n            value={[settings.topP || 1.0]}\n          />\n          <FieldDescription>\n            Nucleus sampling: considers tokens with top-p probability mass\n          </FieldDescription>\n        </FieldContent>\n      </Field>\n\n      <Field>\n        <FieldLabel htmlFor=\"top-k\">Top K: {settings.topK || \"40\"}</FieldLabel>\n        <FieldContent>\n          <Slider\n            aria-label=\"Top K\"\n            id=\"top-k\"\n            max={100}\n            min={1}\n            onValueChange={(values) => onSettingChange(\"topK\", values[0])}\n            step={1}\n            value={[settings.topK || 40]}\n          />\n          <FieldDescription>\n            Limits sampling to top K most likely tokens\n          </FieldDescription>\n        </FieldContent>\n      </Field>\n\n      <Field>\n        <div className=\"flex items-center justify-between\">\n          <FieldLabel htmlFor=\"frequency-penalty\">\n            Frequency Penalty: {settings.frequencyPenalty?.toFixed(1) || \"0.0\"}\n          </FieldLabel>\n        </div>\n        <FieldContent>\n          <Slider\n            aria-label=\"Frequency penalty\"\n            id=\"frequency-penalty\"\n            max={2}\n            min={-2}\n            onValueChange={(values) =>\n              onSettingChange(\"frequencyPenalty\", values[0])\n            }\n            step={0.1}\n            value={[settings.frequencyPenalty || 0.0]}\n          />\n          <FieldDescription>\n            Reduces likelihood of repeating tokens (positive) or increases it\n            (negative)\n          </FieldDescription>\n        </FieldContent>\n      </Field>\n\n      <Field>\n        <div className=\"flex items-center justify-between\">\n          <FieldLabel htmlFor=\"presence-penalty\">\n            Presence Penalty: {settings.presencePenalty?.toFixed(1) || \"0.0\"}\n          </FieldLabel>\n        </div>\n        <FieldContent>\n          <Slider\n            aria-label=\"Presence penalty\"\n            id=\"presence-penalty\"\n            max={2}\n            min={-2}\n            onValueChange={(values) =>\n              onSettingChange(\"presencePenalty\", values[0])\n            }\n            step={0.1}\n            value={[settings.presencePenalty || 0.0]}\n          />\n          <FieldDescription>\n            Reduces likelihood of talking about new topics (positive) or\n            increases it (negative)\n          </FieldDescription>\n        </FieldContent>\n      </Field>\n    </div>\n  );\n}\n\ninterface AdvancedSettingsToggleProps {\n  isOpen: boolean;\n  onToggle: () => void;\n}\n\nfunction AdvancedSettingsToggle({\n  isOpen,\n  onToggle,\n}: AdvancedSettingsToggleProps) {\n  return (\n    <Button\n      aria-expanded={isOpen}\n      aria-label={isOpen ? \"Hide advanced settings\" : \"Show advanced settings\"}\n      className=\"min-h-[44px] w-full min-w-[44px] sm:min-h-[32px] sm:w-auto sm:min-w-[32px]\"\n      onClick={onToggle}\n      type=\"button\"\n      variant=\"ghost\"\n    >\n      <Settings aria-hidden=\"true\" className=\"size-4\" />\n      {isOpen ? \"Hide\" : \"Show\"} Advanced Settings\n    </Button>\n  );\n}\n\nexport default function AISettingsPanel({\n  settings = defaultSettings,\n  presets = [],\n  onSettingsChange,\n  onSave,\n  onReset,\n  onLoadPreset,\n  onDeletePreset,\n  className,\n  showAdvanced = false,\n  availableModels = [\n    \"gpt-4\",\n    \"gpt-3.5-turbo\",\n    \"claude-3-opus\",\n    \"claude-3-sonnet\",\n  ],\n}: AISettingsPanelProps) {\n  const [localSettings, setLocalSettings] = useState<AISettings>(settings);\n  const [isSaving, setIsSaving] = useState(false);\n  const [showAdvancedSettings, setShowAdvancedSettings] =\n    useState(showAdvanced);\n\n  useEffect(() => {\n    setLocalSettings(settings);\n  }, [settings]);\n\n  const updateSetting = useCallback(\n    <K extends keyof AISettings>(key: K, value: AISettings[K]) => {\n      const newSettings = { ...localSettings, [key]: value };\n      setLocalSettings(newSettings);\n      onSettingsChange?.(newSettings);\n    },\n    [localSettings, onSettingsChange]\n  );\n\n  const handleSave = useCallback(async () => {\n    if (!onSave) return;\n\n    setIsSaving(true);\n    try {\n      await onSave(localSettings);\n    } catch (error) {\n      console.error(\"Failed to save settings:\", error);\n    } finally {\n      setIsSaving(false);\n    }\n  }, [onSave, localSettings]);\n\n  const handleReset = useCallback(() => {\n    setLocalSettings(defaultSettings);\n    onReset?.();\n    onSettingsChange?.(defaultSettings);\n  }, [onReset, onSettingsChange]);\n\n  const toggleAdvancedSettings = useCallback(() => {\n    setShowAdvancedSettings((prev) => !prev);\n  }, []);\n\n  const hasChanges = useMemo(\n    () => JSON.stringify(localSettings) !== JSON.stringify(settings),\n    [localSettings, settings]\n  );\n\n  const handleLoadPreset = useCallback(\n    (presetId: string) => {\n      const preset = presets.find((p) => p.id === presetId);\n      if (preset) {\n        setLocalSettings(preset.settings);\n        onLoadPreset?.(presetId);\n        onSettingsChange?.(preset.settings);\n      }\n    },\n    [presets, onLoadPreset, onSettingsChange]\n  );\n\n  return (\n    <Card className={cn(\"w-full shadow-xs\", className)}>\n      <SettingsHeader\n        hasChanges={hasChanges}\n        isSaving={isSaving}\n        onReset={onReset ? handleReset : undefined}\n        onSave={onSave ? handleSave : undefined}\n      />\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {presets.length > 0 && onLoadPreset && (\n            <>\n              <PresetSelector\n                onDeletePreset={onDeletePreset}\n                onLoadPreset={handleLoadPreset}\n                presets={presets}\n              />\n              <Separator />\n            </>\n          )}\n\n          <div className=\"flex flex-col gap-4\">\n            <ModelSelectField\n              availableModels={availableModels}\n              onChange={(value) => updateSetting(\"model\", value)}\n              value={localSettings.model ?? availableModels[0] ?? \"\"}\n            />\n\n            <TemperatureField\n              onChange={(value) => updateSetting(\"temperature\", value)}\n              value={localSettings.temperature || 0.7}\n            />\n\n            <MaxTokensField\n              onChange={(value) => updateSetting(\"maxTokens\", value)}\n              value={localSettings.maxTokens || 2000}\n            />\n\n            <SystemPromptField\n              onChange={(value) => updateSetting(\"systemPrompt\", value)}\n              value={localSettings.systemPrompt || \"\"}\n            />\n          </div>\n\n          <Separator />\n\n          <div className=\"flex flex-col gap-4\">\n            <AdvancedSettingsToggle\n              isOpen={showAdvancedSettings}\n              onToggle={toggleAdvancedSettings}\n            />\n\n            {showAdvancedSettings && (\n              <AdvancedSettingsSection\n                onSettingChange={updateSetting}\n                settings={localSettings}\n              />\n            )}\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "ai"
  ],
  "type": "registry:ui"
}