import React, { useEffect, useMemo, useState, useCallback } from 'react'
import { Download, RefreshCw, Copy, CheckCircle2, AlertTriangle, XCircle, Settings2, Key, X, Trash2, Shield, ExternalLink } from 'lucide-react'

// -----------------------------------------------------------------------------
// Types
// -----------------------------------------------------------------------------

type Provider = {
  name: string
  label: string
  kind?: 'chat' | 'image' | 'video' | 'edit' | 'multi'
}

type ModelCatalogEntry = {
  id: string
  label?: string
  recommended?: boolean
  recommended_nsfw?: boolean
  protected?: boolean
  nsfw?: boolean
  description?: string
  size_gb?: number
  resolution?: string
  frames?: number
  // Civitai-specific fields
  civitai_url?: string
  civitai_version_id?: string
  // Addon dependencies (for video models)
  requires_addons?: string[]
  recommends_addons?: string[]
  // Addon-specific fields
  provides_nodes?: string[]
  // Optional install metadata (backend-supported)
  install?: {
    type: 'ollama_pull' | 'http_files' | 'hf_files' | 'hf_snapshot' | 'script' | 'git_repo'
    hint?: string
    requires_custom_nodes?: string[]
    repo_url?: string
    files?: Array<{
      url?: string
      repo_id?: string
      filename?: string
      dest: string
      sha256?: string
    }>
    repo_id?: string
    dest_dir?: string
  }
}

type ModelCatalogResponse = {
  providers?: Record<string, Record<string, ModelCatalogEntry[]>> // providers[providerName][modelType] = list
}

type ModelsListResponse = {
  provider: string
  model_type?: string
  base_url?: string
  models: string[]
  error?: string | null
}

type InstallRequest = {
  provider: string
  model_type: string
  model_id: string
  base_url?: string
  options?: Record<string, any>
}

type InstallResponse = {
  ok?: boolean
  job_id?: string
  message?: string
}

export type ModelsParams = {
  backendUrl: string
  apiKey?: string

  // Defaults from Enterprise Settings
  providerChat?: string
  providerImages?: string
  providerVideo?: string

  baseUrlChat?: string
  baseUrlImages?: string
  baseUrlVideo?: string

  // Experimental features
  experimentalCivitai?: boolean
  civitaiApiKey?: string  // Optional API key for Civitai NSFW content

  // NSFW/Spice Mode - shows additional adult content models
  nsfwMode?: boolean
}

// Avatar model types (from /v1/avatar-models endpoint)
type AvatarModelUsedBy = {
  feature: string
  role: 'required' | 'recommended'
}

type AvatarModelInfo = {
  id: string
  name: string
  description: string
  filename: string
  subdir: string
  installed: boolean
  license: string | null
  commercial_use_ok: boolean | null
  homepage: string | null
  download_url: string | null
  sha256: string | null
  requires: string[] | null
  is_default: boolean
  used_by: AvatarModelUsedBy[]
}

type AvatarFeatureStatus = {
  label: string
  description: string
  ready: boolean
  required_missing: string[]
  recommended_installed: boolean
  recommended_note: string | null
}

type AvatarModelsResponse = {
  category: string
  installed: string[]
  available: AvatarModelInfo[]
  defaults: string[]
  features: Record<string, AvatarFeatureStatus>
}

type AvatarDownloadResult = {
  id: string
  name?: string
  status: 'installed' | 'already_installed' | 'failed' | 'timeout' | 'error' | 'not_registered' | 'no_download_url'
  error?: string
  size?: number
  elapsed?: number
}

type AvatarDownloadStartResponse = {
  ok: boolean
  message?: string
  error?: string
  preset?: string
  total_models?: number
  model_ids?: string[]
}

type AvatarDownloadStatus = {
  running: boolean
  preset: string | null
  current_model: string | null
  current_index: number
  total_models: number
  results: AvatarDownloadResult[]
  finished: boolean
  error: string | null
  elapsed: number
  installed_count: number
  failed_count: number
  downloaded_bytes: number
}

// Civitai search result types
type CivitaiSearchVersion = {
  id: string
  name: string
  downloadUrl?: string
  sizeKB: number
  trainedWords: string[]
}

type CivitaiSearchResult = {
  id: string
  name: string
  type: string
  creator: string
  downloads: number
  rating: number
  ratingCount: number
  link: string | null
  thumbnail: string | null
  nsfw: boolean
  description: string
  tags: string[]
  versions: CivitaiSearchVersion[]
}

type CivitaiSearchResponse = {
  ok: boolean
  query: string
  model_type: string
  nsfw: boolean
  items: CivitaiSearchResult[]
  metadata: {
    currentPage: number
    pageSize: number
    totalItems: number
    totalPages: number
  }
}

// -----------------------------------------------------------------------------
// Fallback Model Catalogs (when backend /model-catalog is not available)
// -----------------------------------------------------------------------------

const FALLBACK_CATALOGS: Record<string, Record<string, ModelCatalogEntry[]>> = {
  ollama: {
    chat: [
      // Standard Chat
      { id: 'llama3:8b', label: 'Llama 3 8B', recommended: true },
      { id: 'llama3:70b', label: 'Llama 3 70B' },
      { id: 'llama3.1', label: 'Llama 3.1 (8B)', recommended: true },
      { id: 'llama3.1:70b', label: 'Llama 3.1 70B' },
      { id: 'llama3.2', label: 'Llama 3.2 (3B)', recommended: true },
      { id: 'mistral:7b', label: 'Mistral 7B' },
      { id: 'mistral-nemo', label: 'Mistral Nemo (12B)', recommended: true },
      { id: 'mixtral:8x7b', label: 'Mixtral 8x7B' },
      { id: 'qwen2.5', label: 'Qwen 2.5 (7B)', recommended: true },
      { id: 'gemma2', label: 'Gemma 2 (9B)' },
      { id: 'phi3:3.8b', label: 'Phi-3 3.8B' },
      { id: 'phi4', label: 'Phi-4 (14B)' },
      { id: 'deepseek-r1:latest', label: 'DeepSeek R1 (7B)' },
      // Uncensored & Roleplay (hidden when Spice Mode off)
      { id: 'dolphin3', label: 'Dolphin 3.0 (8B)', nsfw: true, recommended_nsfw: true },
      { id: 'dolphin-llama3', label: 'Dolphin Llama 3 (8B)', nsfw: true },
      { id: 'dolphin-mistral', label: 'Dolphin Mistral (7B)', nsfw: true, recommended_nsfw: true },
      { id: 'dolphin-mixtral:8x7b', label: 'Dolphin Mixtral (8x7B MoE)', nsfw: true },
      { id: 'hermes3', label: 'Hermes 3 (8B)', nsfw: true, recommended_nsfw: true },
      { id: 'solar', label: 'Solar (10.7B)', nsfw: true },
      { id: 'wizardlm2', label: 'WizardLM2 (7B)', nsfw: true },
      // Legacy Uncensored
      { id: 'llama2-uncensored', label: 'Llama 2 Uncensored (7B)', nsfw: true },
      { id: 'wizardlm-uncensored', label: 'WizardLM Uncensored (13B)', nsfw: true },
      { id: 'wizard-vicuna-uncensored', label: 'Wizard Vicuna Uncensored (7B)', nsfw: true },
      // Abliterated - Mannix
      { id: 'mannix/llama3.1-8b-abliterated', label: 'Llama 3.1 Abliterated (8B)', nsfw: true },
      { id: 'mannix/dolphin-2.9-llama3-8b', label: 'Dolphin 2.9 Llama 3 (8B)', nsfw: true },
      // Abliterated - Huihui.ai
      { id: 'huihui_ai/qwen3-abliterated', label: 'Qwen3 Abliterated', nsfw: true, recommended_nsfw: true },
      { id: 'huihui_ai/qwen3-abliterated:8b', label: 'Qwen3 Abliterated (8B)', nsfw: true, recommended_nsfw: true },
      { id: 'huihui_ai/qwen3-abliterated:4b', label: 'Qwen3 Abliterated (4B)', nsfw: true, recommended_nsfw: true },
      { id: 'huihui_ai/qwen3-coder-abliterated', label: 'Qwen3 Coder Abliterated', nsfw: true },
      { id: 'huihui_ai/qwen3-next-abliterated', label: 'Qwen3-Next Abliterated', nsfw: true },
      { id: 'huihui_ai/dolphin3-abliterated', label: 'Dolphin 3 Abliterated (8B)', nsfw: true, recommended_nsfw: true },
      { id: 'huihui_ai/huihui-moe-abliterated', label: 'Huihui MoE Abliterated', nsfw: true },
      { id: 'huihui_ai/gpt-oss-abliterated', label: 'GPT-OSS Abliterated', nsfw: true },
      // Abliterated - JOSIEFIED
      { id: 'goekdenizguelmez/JOSIEFIED-Qwen3', label: 'JOSIEFIED Qwen3', nsfw: true, recommended_nsfw: true },
      { id: 'goekdenizguelmez/JOSIEFIED-Qwen3:8b', label: 'JOSIEFIED Qwen3 (8B)', nsfw: true, recommended_nsfw: true },
      { id: 'goekdenizguelmez/JOSIEFIED-Qwen2.5', label: 'JOSIEFIED Qwen2.5', nsfw: true },
      { id: 'goekdenizguelmez/JOSIEFIED-Qwen2.5:7b', label: 'JOSIEFIED Qwen2.5 (7B)', nsfw: true, recommended_nsfw: true },
      { id: 'goekdenizguelmez/JOSIEFIED-Qwen2.5:14b', label: 'JOSIEFIED Qwen2.5 (14B)', nsfw: true },
      { id: 'goekdenizguelmez/JOSIEFIED-Qwen2.5:3b', label: 'JOSIEFIED Qwen2.5 (3B)', nsfw: true, recommended_nsfw: true },
      // Vision
      { id: 'huihui_ai/qwen3-vl-abliterated:8b-instruct', label: 'Qwen3 Vision Abliterated (8B)', nsfw: true },
      // Niche Uncensored
      { id: 'yarn-mistral', label: 'Yarn Mistral (7B)', nsfw: true },
      { id: 'openhermes', label: 'OpenHermes (7B)', nsfw: true },
      { id: 'neural-chat', label: 'Neural Chat (7B)', nsfw: true },
      // Additional Abliterated
      { id: 'huihui_ai/llama3.2-abliterate:3b', label: 'Llama 3.2 Abliterated (3B)', nsfw: true },
      { id: 'huihui_ai/gemma3-abliterated', label: 'Gemma 3 Abliterated', nsfw: true },
      { id: 'huihui_ai/deepseek-r1-abliterated:8b', label: 'DeepSeek R1 Abliterated (8B)', nsfw: true },
      { id: 'huihui_ai/deepseek-r1-abliterated:14b', label: 'DeepSeek R1 Abliterated (14B)', nsfw: true },
      { id: 'huihui_ai/deepseek-r1-abliterated:1.5b', label: 'DeepSeek R1 Abliterated (1.5B)', nsfw: true },
      { id: 'dolphincoder', label: 'Dolphin Coder (7B)', nsfw: true },
      { id: 'goekdenizguelmez/JOSIEFIED-Llama', label: 'JOSIEFIED Llama', nsfw: true, recommended_nsfw: true },
      { id: 'samantha-mistral', label: 'Samantha Mistral (7B)', nsfw: true, recommended_nsfw: true },
    ],
    multimodal: [
      // SFW Vision Models
      { id: 'moondream', label: 'Moondream (1.6 GB)', recommended: true, description: 'Ultra-light vision captioning + OCR.' },
      { id: 'gemma3:4b', label: 'Gemma 3 Vision 4B (3 GB)', recommended: true, description: 'Best overall edge multimodal model.' },
      { id: 'llava:7b', label: 'LLaVA 1.6 7B (4.7 GB)', recommended: true, description: 'Strong general-purpose vision model.' },
      { id: 'minicpm-v:latest', label: 'MiniCPM-V 2.6 (5 GB)', description: 'Strong multi-image reasoning.' },
      { id: 'llama3.2-vision:11b', label: 'Llama 3.2 Vision 11B (7 GB)', description: 'Best reasoning near RAM limit.' },
      // NSFW Vision Models
      { id: 'huihui_ai/qwen3-vl-abliterated:8b-instruct', label: 'Qwen3-VL Abliterated 8B (5 GB)', nsfw: true, recommended_nsfw: true, description: 'Unfiltered image descriptions.' },
      { id: 'internvl3:8b', label: 'InternVL3 8B (7 GB)', nsfw: true, recommended_nsfw: true, description: 'Detailed scene analysis.' },
      { id: 'smolvlm2:latest', label: 'SmolVLM2 2.2B (2 GB)', nsfw: true, recommended_nsfw: true, description: 'Fast unrestricted captioning.' },
    ],
  },
  comfyui: {
    image: [
      // Standard SFW models
      { id: 'sd_xl_base_1.0.safetensors', label: 'SDXL Base 1.0 (7GB)', recommended: true, nsfw: false },
      { id: 'flux1-schnell.safetensors', label: 'Flux.1 Schnell (23GB)', nsfw: false },
      { id: 'flux1-dev.safetensors', label: 'Flux.1 Dev (23GB)', nsfw: false },
      { id: 'sd15.safetensors', label: 'Stable Diffusion 1.5 (4GB)', nsfw: false },
      { id: 'realisticVisionV51.safetensors', label: 'Realistic Vision v5.1 (2GB)', nsfw: false },
      // NSFW models (shown when Spice Mode enabled)
      { id: 'ponyDiffusionV6XL.safetensors', label: 'Pony Diffusion v6 XL (7GB)', nsfw: true },
      { id: 'dreamshaper_8.safetensors', label: 'DreamShaper 8 (2GB)', nsfw: true, recommended_nsfw: true },
      { id: 'deliberate_v3.safetensors', label: 'Deliberate v3 (2GB)', nsfw: true },
      { id: 'epicrealism_pureEvolution.safetensors', label: 'epiCRealism Pure Evolution (2GB)', nsfw: true, recommended_nsfw: true },
      { id: 'cyberrealistic_v42.safetensors', label: 'CyberRealistic v4.2 (2GB)', nsfw: true },
      { id: 'absolutereality_v181.safetensors', label: 'AbsoluteReality v1.8.1 (2GB)', nsfw: true },
      { id: 'aZovyaRPGArtist_v5.safetensors', label: 'aZovya RPG Artist v5 (2GB)', nsfw: true },
      { id: 'unstableDiffusion.safetensors', label: 'Unstable Diffusion (4GB)', nsfw: true },
      { id: 'majicmixRealistic_v7.safetensors', label: 'MajicMix Realistic v7 (2GB)', nsfw: true },
      { id: 'bbmix_v4.safetensors', label: 'BBMix v4 (2GB)', nsfw: true },
      { id: 'realisian_v50.safetensors', label: 'Realisian v5.0 (2GB)', nsfw: true },
    ],
    video: [
      { id: 'svd_xt_1_1.safetensors', label: 'Stable Video Diffusion XT 1.1 (10GB)', recommended: true, nsfw: false },
      { id: 'svd_xt.safetensors', label: 'Stable Video Diffusion XT (10GB)', nsfw: false },
      { id: 'svd.safetensors', label: 'Stable Video Diffusion (10GB)', nsfw: false },
      { id: 'ltx-video-2b-v0.9.1.safetensors', label: 'LTX-Video 2B v0.9.1 (6GB)', recommended: true, nsfw: false, description: 'Best for RTX 4080. Fast, lightweight video model.' },
      { id: 'hunyuanvideo_t2v_720p_gguf_q4_k_m_pack', label: 'HunyuanVideo GGUF Q4_K_M Pack (10GB)', recommended: true, nsfw: false, description: 'GGUF pack for 16GB cards. Requires ComfyUI-GGUF.' },
      { id: 'wan2.2_5b_fp16_pack', label: 'Wan 2.2 5B FP16 Pack (22GB)', recommended: true, nsfw: false, description: 'Strong motion + modern video. Official Comfy-Org repack.' },
      { id: 'mochi_preview_fp8_pack', label: 'Mochi 1 Preview FP8 Pack (28GB)', nsfw: false, description: 'Heavier model - may push VRAM limits on 16GB.' },
      { id: 'cogvideox1.5_5b_i2v_snapshot', label: 'CogVideoX 1.5 5B I2V (20GB)', nsfw: false, description: 'Diffusers-style repo. Requires CogVideoX wrapper.' },
    ],
    edit: [
      { id: 'sd_xl_base_1.0_inpainting_0.1.safetensors', label: 'SDXL Inpainting 0.1 (7GB)', recommended: true, nsfw: false },
      { id: 'sd-v1-5-inpainting.ckpt', label: 'SD 1.5 Inpainting (4GB)', recommended: true, nsfw: false },
      { id: 'control_v11p_sd15_inpaint.safetensors', label: 'ControlNet Inpaint (1.5GB)', recommended: true, nsfw: false },
      { id: 'sam_vit_h_4b8939.pth', label: 'SAM ViT-H (2.5GB)', nsfw: false },
      { id: 'u2net.onnx', label: 'Background Remove U2Net (170MB)', nsfw: false },
    ],
    enhance: [
      { id: '4x-UltraSharp.pth', label: '4x UltraSharp (Upscale)', recommended: true, nsfw: false, description: 'Sharp, clean 4x upscaler for general photos.' },
      { id: 'RealESRGAN_x4plus.pth', label: 'RealESRGAN x4+ (Photo)', recommended: true, nsfw: false, description: 'Excellent photo upscaling with natural texture recovery.' },
      { id: 'realesr-general-x4v3.pth', label: 'Real-ESRGAN General x4v3', nsfw: false, description: 'General-purpose Real-ESRGAN model, good for mixed content.' },
      { id: 'SwinIR_4x.pth', label: 'SwinIR 4x (Restore)', nsfw: false, description: 'Restoration upscaler for compression and mild blur cleanup.' },
      { id: 'GFPGANv1.4.pth', label: 'GFPGAN v1.4 (Face Restore)', nsfw: false, description: 'Optional face restoration after heavy edits or upscaling.' },
      { id: 'u2net.onnx', label: 'U2Net (Background Remove)', recommended: true, nsfw: false, description: 'Background removal for Edit mode. Downloads to ~/.u2net or models/comfy/rembg.' },
    ],
    addons: [
      // Text Encoders (required for video models)
      {
        id: 't5xxl_fp8_e4m3fn.safetensors',
        label: 'T5-XXL FP8 Text Encoder (5GB)',
        recommended: true,
        nsfw: false,
        description: 'For 12-16GB VRAM (RTX 4080, 3080). Uses ~5GB vs ~10GB for FP16. Required for LTX-Video on limited VRAM.',
        install: {
          type: 'hf_files',
          files: [{
            repo_id: 'comfyanonymous/flux_text_encoders',
            filename: 't5xxl_fp8_e4m3fn.safetensors',
            dest: 'models/clip/t5xxl_fp8_e4m3fn.safetensors'
          }],
          hint: 'Download to ComfyUI/models/clip folder'
        }
      },
      {
        id: 't5xxl_fp16.safetensors',
        label: 'T5-XXL FP16 Text Encoder (10GB)',
        recommended: true,
        nsfw: false,
        description: 'For 24GB+ VRAM (RTX 4090, A5000). Full precision for best quality. Baseline ~20GB + sampling ~6-10GB peak.',
        install: {
          type: 'hf_files',
          files: [{
            repo_id: 'comfyanonymous/flux_text_encoders',
            filename: 't5xxl_fp16.safetensors',
            dest: 'models/clip/t5xxl_fp16.safetensors'
          }],
          hint: 'Download to ComfyUI/models/clip folder'
        }
      },
      // VAE Models
      {
        id: 'mochi_vae.safetensors',
        label: 'Mochi VAE (400MB)',
        nsfw: false,
        description: 'Required VAE for Mochi video model.',
        install: {
          type: 'hf_files',
          files: [{
            repo_id: 'Comfy-Org/mochi_preview_repackaged',
            filename: 'split_files/vae/mochi_vae.safetensors',
            dest: 'models/vae/mochi_vae.safetensors'
          }],
          hint: 'Download to ComfyUI/models/vae folder'
        }
      },
      // CLIP Models
      {
        id: 'clip_l.safetensors',
        label: 'CLIP-L Text Encoder (250MB)',
        nsfw: false,
        description: 'CLIP-L text encoder for SDXL and video models.',
        install: {
          type: 'hf_files',
          files: [{
            repo_id: 'comfyanonymous/flux_text_encoders',
            filename: 'clip_l.safetensors',
            dest: 'models/clip/clip_l.safetensors'
          }],
          hint: 'Download to ComfyUI/models/clip folder'
        }
      },
    ],
  },
  openai_compat: {
    chat: [
      { id: 'local-model', label: 'Local Model (auto-detect)', recommended: true },
    ],
  },
  civitai: {
    image: [
      // Recommended Civitai models for image generation
      {
        id: 'pony_diffusion_v6_xl',
        label: 'Pony Diffusion V6 XL',
        recommended: true,
        nsfw: true,
        description: 'Base model for character consistency and prompt adherence',
        civitai_url: 'https://civitai.com/models/257749/pony-diffusion-v6-xl',
        civitai_version_id: '290640'
      },
      {
        id: 'cyberrealistic_pony',
        label: 'CyberRealistic Pony',
        recommended: true,
        nsfw: true,
        description: 'Best blend of Pony prompt understanding with photorealism',
        civitai_url: 'https://civitai.com/models/443821/cyberrealistic-pony',
        civitai_version_id: '544666'
      },
      {
        id: 'realvisxl_v50',
        label: 'RealVisXL V5.0',
        recommended: true,
        nsfw: false,
        description: 'Gold standard for photorealistic skin texture and lighting',
        civitai_url: 'https://civitai.com/models/139562/realvisxl-v50',
        civitai_version_id: '361593'
      },
      {
        id: 'juggernaut_xl',
        label: 'Juggernaut XL',
        recommended: true,
        nsfw: false,
        description: 'Cinematic and moody photorealism',
        civitai_url: 'https://civitai.com/models/133005/juggernaut-xl',
        civitai_version_id: '471120'
      },
      {
        id: 'flux1_checkpoint',
        label: 'Flux.1 Checkpoint (Easy)',
        nsfw: false,
        description: 'High-quality Flux checkpoint for ComfyUI',
        civitai_url: 'https://civitai.com/models/628682/flux-1-checkpoint-easy-to-use',
        civitai_version_id: '704954'
      },
    ],
    video: [
      // Recommended Civitai models for video generation
      {
        id: 'ltx_video_workflow',
        label: 'LTX Video (I2V)',
        recommended: true,
        nsfw: false,
        description: 'Fast, lightweight video generation for RTX cards',
        civitai_url: 'https://civitai.com/models/995093/ltx-image-to-video-with-stg-caption-and-clip-extend-workflow',
        civitai_version_id: '1119428'
      },
      {
        id: 'mochi_1_pack',
        label: 'Mochi 1 Video Pack',
        recommended: true,
        nsfw: false,
        description: 'High motion fidelity video model',
        civitai_url: 'https://civitai.com/models/886896/donut-mochi-pack-video-generation',
        civitai_version_id: '992820'
      },
      {
        id: 'animatediff_sdxl',
        label: 'AnimateDiff SDXL',
        nsfw: false,
        description: 'Animate SDXL images using AnimateDiff',
        civitai_url: 'https://civitai.com/models/331700/odinson-sdxl-animatediff',
        civitai_version_id: '373089'
      },
      {
        id: 'animatediff_lightning',
        label: 'AnimateDiff Lightning',
        nsfw: false,
        description: 'Fast 4-step AnimateDiff model',
        civitai_url: 'https://civitai.com/models/500187/animatediff-lightning',
        civitai_version_id: '554533'
      },
    ],
  },
}

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

function cleanBase(url: string) {
  return (url || '').trim().replace(/\/+$/, '')
}

async function getJson<T>(baseUrl: string, path: string, apiKey?: string): Promise<T> {
  const url = `${cleanBase(baseUrl)}${path.startsWith('/') ? path : `/${path}`}`
  const res = await fetch(url, {
    method: 'GET',
    headers: {
      ...(apiKey ? { 'x-api-key': apiKey } : {}),
    },
  })
  if (!res.ok) {
    const text = await res.text().catch(() => '')
    throw new Error(`HTTP ${res.status} ${res.statusText}${text ? `: ${text}` : ''}`)
  }
  return (await res.json()) as T
}

async function postJson<T>(baseUrl: string, path: string, body: any, apiKey?: string): Promise<T> {
  const url = `${cleanBase(baseUrl)}${path.startsWith('/') ? path : `/${path}`}`
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(apiKey ? { 'x-api-key': apiKey } : {}),
    },
    credentials: 'include',
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const text = await res.text().catch(() => '')
    throw new Error(`HTTP ${res.status} ${res.statusText}${text ? `: ${text}` : ''}`)
  }
  return (await res.json()) as T
}

function chipClass(kind: 'ok' | 'warn' | 'bad' | 'muted') {
  switch (kind) {
    case 'ok':
      return 'bg-emerald-500/10 text-emerald-200 border-emerald-500/20'
    case 'warn':
      return 'bg-amber-500/10 text-amber-200 border-amber-500/20'
    case 'bad':
      return 'bg-rose-500/10 text-rose-200 border-rose-500/20'
    default:
      return 'bg-white/5 text-white/60 border-white/10'
  }
}

function IconStatus({ kind }: { kind: 'ok' | 'warn' | 'bad' | 'muted' }) {
  if (kind === 'ok') return <CheckCircle2 size={14} className="text-emerald-300" />
  if (kind === 'warn') return <AlertTriangle size={14} className="text-amber-300" />
  if (kind === 'bad') return <XCircle size={14} className="text-rose-300" />
  return <Settings2 size={14} className="text-white/50" />
}

function safeLabel(id: string, label?: string) {
  return label?.trim() ? label.trim() : id
}

function formatBytes(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
}

// -----------------------------------------------------------------------------
// Component
// -----------------------------------------------------------------------------

export default function ModelsView(props: ModelsParams) {
  const authKey = (props.apiKey || '').trim()
  const backendUrl = cleanBase(props.backendUrl)

  const [providers, setProviders] = useState<Provider[]>([])
  const [providersError, setProvidersError] = useState<string | null>(null)

  const [modelType, setModelType] = useState<'chat' | 'multimodal' | 'image' | 'video' | 'edit' | 'enhance' | 'addons' | 'lora'>('chat')
  const [provider, setProvider] = useState<string>(props.providerChat || 'ollama')

  const defaultBaseUrl = useMemo(() => {
    if (modelType === 'chat') return props.baseUrlChat || ''
    if (modelType === 'multimodal') return props.baseUrlChat || ''
    if (modelType === 'image') return props.baseUrlImages || ''
    if (modelType === 'edit') return props.baseUrlImages || ''
    if (modelType === 'enhance') return props.baseUrlImages || ''
    if (modelType === 'addons') return props.baseUrlImages || ''
    if (modelType === 'lora') return props.baseUrlImages || ''
    return props.baseUrlVideo || ''
  }, [modelType, props.baseUrlChat, props.baseUrlImages, props.baseUrlVideo])

  const [baseUrl, setBaseUrl] = useState<string>(defaultBaseUrl)

  // Installed models (dynamic)
  const [installed, setInstalled] = useState<string[]>([])
  const [installedError, setInstalledError] = useState<string | null>(null)
  const [installedLoading, setInstalledLoading] = useState(false)

  // Supported models (curated) — optional endpoint
  const [catalog, setCatalog] = useState<ModelCatalogResponse | null>(null)
  const [catalogError, setCatalogError] = useState<string | null>(null)
  const [catalogLoading, setCatalogLoading] = useState(false)

  // Install/delete jobs — optional endpoint (we degrade if not present)
  const [installBusy, setInstallBusy] = useState<string | null>(null)
  const [deleteBusy, setDeleteBusy] = useState<string | null>(null)
  const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null)
  const [toast, setToast] = useState<string | null>(null)

  // Civitai-specific state
  const [civitaiVersionId, setCivitaiVersionId] = useState<string>('')

  // Civitai search state
  const [civitaiQuery, setCivitaiQuery] = useState<string>('')
  const [civitaiResults, setCivitaiResults] = useState<CivitaiSearchResult[]>([])
  const [civitaiSearchLoading, setCivitaiSearchLoading] = useState(false)
  const [civitaiSearchError, setCivitaiSearchError] = useState<string | null>(null)
  const [civitaiPage, setCivitaiPage] = useState(1)
  const [civitaiTotalPages, setCivitaiTotalPages] = useState(1)

  // API Keys state (optional - for gated models)
  const [apiKeysExpanded, setApiKeysExpanded] = useState(false)
  const [apiKeysStatus, setApiKeysStatus] = useState<Record<string, { configured: boolean; source: string; masked: string | null }>>({})
  const [apiKeyInput, setApiKeyInput] = useState<{ huggingface: string; civitai: string }>({ huggingface: '', civitai: '' })
  const [apiKeyTesting, setApiKeyTesting] = useState<string | null>(null)
  const [apiKeySaving, setApiKeySaving] = useState<string | null>(null)

  // Avatar & Identity models state (shown in Add-ons tab)
  const [avatarModels, setAvatarModels] = useState<AvatarModelInfo[]>([])
  const [avatarFeatures, setAvatarFeatures] = useState<Record<string, AvatarFeatureStatus>>({})
  const [avatarModelsLoading, setAvatarModelsLoading] = useState(false)
  const [avatarModelsError, setAvatarModelsError] = useState<string | null>(null)
  const [avatarDownloadBusy, setAvatarDownloadBusy] = useState<string | null>(null) // 'basic' | 'full' | null
  const [avatarDownloadStatus, setAvatarDownloadStatus] = useState<AvatarDownloadStatus | null>(null)
  const [avatarDeleteConfirm, setAvatarDeleteConfirm] = useState<string | null>(null)
  const [avatarDeleteBusy, setAvatarDeleteBusy] = useState<string | null>(null)
  const [avatarInstallBusy, setAvatarInstallBusy] = useState<string | null>(null) // model ID being installed

  // LoRA registry state (additive — Golden Rule 1.0)
  const [loraRegistry, setLoraRegistry] = useState<any[]>([])
  const [loraInstalled, setLoraInstalled] = useState<any[]>([])
  const [loraLoading, setLoraLoading] = useState(false)
  const [loraError, setLoraError] = useState<string | null>(null)
  const [loraInstallBusy, setLoraInstallBusy] = useState<string | null>(null)
  const [loraDownloadBusy, setLoraDownloadBusy] = useState<string | null>(null)
  const [loraDownloadStatus, setLoraDownloadStatus] = useState<AvatarDownloadStatus | null>(null)
  const [loraDeleteConfirm, setLoraDeleteConfirm] = useState<string | null>(null)
  const [loraDeleteBusy, setLoraDeleteBusy] = useState<string | null>(null)

  // Filter providers based on model type
  const availableProviders = useMemo(() => {
    if (modelType === 'chat') {
      // For chat: all providers EXCEPT comfyui and civitai
      return providers.filter(p => p.name !== 'comfyui' && p.name !== 'civitai')
    } else if (modelType === 'multimodal') {
      // Multimodal: only Ollama (vision models run locally)
      return providers.filter(p => p.name === 'ollama')
    } else {
      // For image/video/edit/enhance: comfyui + civitai (if experimental enabled)
      const filtered = providers.filter(p => p.name === 'comfyui')

      // Add Civitai only for image/video (Civitai API only supports these types)
      if (props.experimentalCivitai && (modelType === 'image' || modelType === 'video')) {
        filtered.push({
          name: 'civitai',
          label: '🧪 Civitai (Experimental)',
          kind: modelType as any,
        })
      }

      return filtered
    }
  }, [providers, modelType, props.experimentalCivitai])

  useEffect(() => {
    // When modelType changes, update provider + baseUrl defaults
    if (modelType === 'chat') {
      // Switch to a chat provider (prefer the one from settings, or default to ollama)
      const newProvider = props.providerChat || 'ollama'
      setProvider(newProvider)
      setBaseUrl(props.baseUrlChat || '')
    } else if (modelType === 'multimodal') {
      // Multimodal models are served via Ollama (vision-capable models)
      setProvider('ollama')
      setBaseUrl(props.baseUrlChat || '')
    } else if (modelType === 'image') {
      // Switch to comfyui for images
      setProvider('comfyui')
      setBaseUrl(props.baseUrlImages || '')
    } else if (modelType === 'addons') {
      // Switch to comfyui for addons (extensions)
      setProvider('comfyui')
      setBaseUrl(props.baseUrlImages || '')
    } else if (modelType === 'lora') {
      // LoRA tab — uses own registry, no provider needed
      setProvider('comfyui')
      setBaseUrl(props.baseUrlImages || '')
    } else {
      // Switch to comfyui for videos
      setProvider('comfyui')
      setBaseUrl(props.baseUrlVideo || '')
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [modelType])

  // Load providers list from backend
  useEffect(() => {
    let mounted = true
    setProvidersError(null)

    ;(async () => {
      try {
        const res = await getJson<{ ok: boolean; providers: Record<string, any> }>(backendUrl, '/providers', authKey)
        if (!mounted) return
        const providerList: Provider[] = Object.entries(res.providers || {}).map(([name, info]: [string, any]) => ({
          name,
          label: info.label || name,
          kind: 'multi',
        }))
        setProviders(providerList)
      } catch (e: any) {
        if (!mounted) return
        setProviders([])
        setProvidersError(e?.message || String(e))
      }
    })()

    return () => {
      mounted = false
    }
  }, [backendUrl, authKey])

  // Load supported catalog (optional)
  const refreshCatalog = async () => {
    setCatalogLoading(true)
    setCatalogError(null)
    try {
      const data = await getJson<ModelCatalogResponse>(backendUrl, '/model-catalog', authKey)
      setCatalog(data)
    } catch (e: any) {
      // Degrade gracefully if endpoint doesn't exist
      setCatalog(null)
      setCatalogError(e?.message || String(e))
    } finally {
      setCatalogLoading(false)
    }
  }

  // Load installed models (only for local providers)
  const refreshInstalled = async () => {
    // Skip fetching installed models for remote API providers and Civitai
    // Remote providers: they're cloud services, not local installations
    // Civitai: download-only provider, models get installed to ComfyUI after download
    const skipProviders = ['openai', 'claude', 'watsonx', 'civitai']

    if (skipProviders.includes(provider)) {
      setInstalled([])
      setInstalledError(null)
      setInstalledLoading(false)
      return
    }

    setInstalledLoading(true)
    setInstalledError(null)
    setInstalled([])  // Clear stale list immediately so old type models don't flash
    try {
      const q = new URLSearchParams()
      q.set('provider', provider)
      if (baseUrl.trim()) q.set('base_url', baseUrl.trim())
      // Pass model_type so backend returns correct installed models for this type
      if (modelType && provider === 'comfyui') q.set('model_type', modelType)
      // For Ollama multimodal, filter to vision-capable models only
      if (modelType === 'multimodal' && provider === 'ollama') q.set('model_type', 'multimodal')

      const data = await getJson<ModelsListResponse>(backendUrl, `/models?${q.toString()}`, authKey)
      setInstalled(Array.isArray(data.models) ? data.models : [])
      if (data.error) setInstalledError(String(data.error))
    } catch (e: any) {
      setInstalled([])
      setInstalledError(e?.message || String(e))
    } finally {
      setInstalledLoading(false)
    }
  }

  useEffect(() => {
    refreshInstalled()
    // Load catalog once initially (optional)
    if (catalog === null && catalogError === null && !catalogLoading) {
      refreshCatalog()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [provider, modelType, baseUrl, backendUrl])

  const supportedForSelection: ModelCatalogEntry[] = useMemo(() => {
    let list: ModelCatalogEntry[] = []

    // Try backend catalog first
    const p = catalog?.providers?.[provider]
    if (p) {
      const catalogList = p?.[modelType] || []
      if (Array.isArray(catalogList) && catalogList.length > 0) {
        list = catalogList
      }
    }

    // Fallback to hardcoded catalogs if backend catalog not available
    if (list.length === 0) {
      const fallback = FALLBACK_CATALOGS[provider]?.[modelType]
      list = Array.isArray(fallback) ? fallback : []
    }

    // Filter based on NSFW mode:
    // - When nsfwMode is OFF: show only non-NSFW models (nsfw !== true)
    // - When nsfwMode is ON: show ALL models (both SFW and NSFW)
    if (!props.nsfwMode) {
      list = list.filter((m) => m.nsfw !== true)
    }

    return list
  }, [catalog, provider, modelType, props.nsfwMode])

  const installedSet = useMemo(() => new Set(installed), [installed])

  // For Ollama: also match "model" with "model:latest" (Ollama convention)
  const isInstalled = useCallback((id: string): boolean => {
    if (installedSet.has(id)) return true
    // Check if installed list contains id:latest (e.g. catalog has "moondream", Ollama reports "moondream:latest")
    if (!id.includes(':') && installedSet.has(`${id}:latest`)) return true
    // Check if catalog has "model:tag" and installed has exactly that
    return false
  }, [installedSet])

  // Merge supported + installed
  const merged = useMemo(() => {
    const supportedMap = new Map<string, ModelCatalogEntry>()
    for (const s of supportedForSelection) supportedMap.set(s.id, s)

    const rows: Array<{
      id: string
      label: string
      recommended?: boolean
      recommended_nsfw?: boolean
      protected?: boolean
      nsfw?: boolean
      description?: string
      civitai_url?: string
      civitai_version_id?: string
      status: 'installed' | 'missing' | 'installed_unsupported'
      install?: ModelCatalogEntry['install']
    }> = []

    // Supported first
    for (const s of supportedForSelection) {
      rows.push({
        id: s.id,
        label: safeLabel(s.id, s.label),
        recommended: s.recommended,
        recommended_nsfw: s.recommended_nsfw,
        protected: s.protected,
        nsfw: s.nsfw,
        description: s.description,
        civitai_url: s.civitai_url,
        civitai_version_id: s.civitai_version_id,
        status: isInstalled(s.id) ? 'installed' : 'missing',
        install: s.install,
      })
    }

    // Then show installed but not in supported catalog
    // Use the catalog as source of truth to prevent type mixing:
    // e.g. video checkpoints (svd_xt_1_1, ltx-video) must NOT appear
    // in the Image tab just because they share the checkpoints/ directory.
    const otherTypeIds = new Set<string>()
    if (provider === 'comfyui' && catalog?.providers?.comfyui) {
      const comfyui = catalog.providers.comfyui as Record<string, ModelCatalogEntry[]>
      for (const [t, entries] of Object.entries(comfyui)) {
        if (t !== modelType && Array.isArray(entries)) {
          for (const entry of entries) {
            if (entry.id) otherTypeIds.add(entry.id)
          }
        }
      }
    }

    // Heuristic: filter out "custom" models that clearly belong to the wrong type.
    // Checkpoint files (.safetensors, .ckpt) should never appear in the chat tab;
    // Ollama-style models (name:tag) should never appear in image/video/edit tabs.
    const isCheckpointFile = (id: string) =>
      /\.(safetensors|ckpt|pth|onnx|gguf|pt|bin)$/i.test(id)
    const isLlmModel = (id: string) =>
      id.includes(':') || /^(llama|mistral|gemma|qwen|phi|deepseek|codellama|vicuna|samantha)/i.test(id)

    // Check if an installed model name matches any supported catalog entry
    // e.g. "moondream:latest" matches catalog "moondream"
    const matchesSupportedEntry = (modelName: string): boolean => {
      if (supportedMap.has(modelName)) return true
      // "model:latest" → try "model" (Ollama convention)
      if (modelName.endsWith(':latest')) {
        const base = modelName.slice(0, -':latest'.length)
        if (supportedMap.has(base)) return true
      }
      return false
    }

    for (const m of installed) {
      if (!matchesSupportedEntry(m)) {
        // Skip models that belong to another type's catalog
        if (otherTypeIds.has(m)) continue
        // Skip checkpoint files on the chat tab
        if (modelType === 'chat' && isCheckpointFile(m)) continue
        // Skip LLM models on ComfyUI tabs (image/video/edit/enhance)
        if (modelType !== 'chat' && provider === 'comfyui' && isLlmModel(m)) continue
        rows.push({
          id: m,
          label: m,
          status: 'installed_unsupported',
        })
      }
    }

    // Sorting: recommended → recommended_nsfw → installed → missing → unsupported
    rows.sort((a, b) => {
      // Priority: recommended (or recommended_nsfw in NSFW mode) first
      const isRecommendedA = a.recommended || (props.nsfwMode && a.recommended_nsfw)
      const isRecommendedB = b.recommended || (props.nsfwMode && b.recommended_nsfw)
      const ar = isRecommendedA ? 0 : 1
      const br = isRecommendedB ? 0 : 1
      if (ar !== br) return ar - br
      const order = (s: string) =>
        s === 'installed' ? 0 : s === 'missing' ? 1 : 2
      return order(a.status) - order(b.status)
    })

    return rows
  }, [supportedForSelection, installed, installedSet, isInstalled, props.nsfwMode, modelType, provider])

  const tryInstall = async (modelId: string, install?: ModelCatalogEntry['install']) => {
    setInstallBusy(modelId)

    try {
      const body: InstallRequest = {
        provider,
        model_type: modelType,
        model_id: modelId,
        base_url: baseUrl.trim() || undefined,
        options: {},
      }

      // Add civitai_version_id if provider is civitai
      if (provider === 'civitai') {
        if (!civitaiVersionId.trim()) {
          setToast('Please enter a Civitai version ID')
          setInstallBusy(null)
          return
        }
        (body as any).civitai_version_id = civitaiVersionId.trim()
        setToast(`Starting Civitai download for version ${civitaiVersionId}...`)
      } else {
        setToast(`Starting download for ${modelId}...`)
      }

      const res = await postJson<InstallResponse>(backendUrl, '/models/install', body, authKey)

      if (res?.ok) {
        setToast(res.message || `Successfully installed ${modelId}`)
        // Refresh installed list after successful installation
        setTimeout(() => {
          refreshInstalled()
        }, 2000)
      } else {
        setToast(res?.message || 'Installation request sent.')
      }
    } catch (e: any) {
      // Fallback to copy-paste instructions if API fails
      if (provider === 'ollama') {
        const cmd = `ollama pull ${modelId}`
        navigator.clipboard?.writeText(cmd).catch(() => {})
        setToast(`API failed. Command copied: ${cmd}`)
      } else if (provider === 'comfyui') {
        navigator.clipboard?.writeText(`Run: python scripts/download.py --model ${modelId}`).catch(() => {})
        setToast('API failed. Download command copied to clipboard.')
      } else if (provider === 'civitai') {
        setToast(`Installation failed: ${e?.message || String(e)}`)
      } else {
        setToast(`Installation failed: ${e?.message || String(e)}`)
      }
    } finally {
      setInstallBusy(null)
    }
  }

  const tryDelete = async (modelId: string) => {
    setDeleteBusy(modelId)
    setDeleteConfirm(null)
    try {
      const body = {
        provider,
        model_type: modelType,
        model_id: modelId,
      }
      setToast(`Deleting ${modelId}...`)
      const res = await postJson<{ ok?: boolean; message?: string }>(backendUrl, '/models/delete', body, authKey)
      if (res?.ok) {
        setToast(res.message || `Deleted ${modelId}`)
        setTimeout(() => refreshInstalled(), 500)
      } else {
        setToast(res?.message || 'Delete failed.')
      }
    } catch (e: any) {
      setToast(`Delete failed: ${e?.message || String(e)}`)
    } finally {
      setDeleteBusy(null)
    }
  }

  // Civitai search function
  const searchCivitai = async (page = 1) => {
    if (!civitaiQuery.trim()) {
      setToast('Please enter a search query')
      return
    }

    setCivitaiSearchLoading(true)
    setCivitaiSearchError(null)

    try {
      const headers: Record<string, string> = {
        'Content-Type': 'application/json',
        ...(authKey ? { 'x-api-key': authKey } : {}),
      }

      // Pass Civitai API key if available and NSFW mode is enabled
      if (props.civitaiApiKey && props.nsfwMode) {
        headers['X-Civitai-Api-Key'] = props.civitaiApiKey
      }

      const res = await fetch(`${backendUrl}/civitai/search`, {
        method: 'POST',
        headers,
        body: JSON.stringify({
          query: civitaiQuery.trim(),
          // Civitai only supports 'image' or 'video' - map other types appropriately
          model_type: modelType === 'video' ? 'video' : 'image',
          nsfw: props.nsfwMode || false,
          limit: 20,
          page,
          sort: 'Highest Rated',
        }),
      })

      if (!res.ok) {
        const text = await res.text().catch(() => '')
        throw new Error(`HTTP ${res.status}: ${text}`)
      }

      const data: CivitaiSearchResponse = await res.json()

      if (data.ok) {
        setCivitaiResults(data.items || [])
        setCivitaiPage(data.metadata?.currentPage || page)
        setCivitaiTotalPages(data.metadata?.totalPages || 1)

        if (data.items.length === 0) {
          setToast('No models found. Try a different search term.')
        }
      } else {
        throw new Error('Search failed')
      }
    } catch (e: any) {
      console.error('[Civitai Search Error]', e)
      setCivitaiSearchError(e?.message || String(e))
      setToast(`Search failed: ${e?.message || 'Unknown error'}`)
    } finally {
      setCivitaiSearchLoading(false)
    }
  }

  // Install from Civitai search result
  const installFromCivitaiResult = async (model: CivitaiSearchResult, versionId: string) => {
    setInstallBusy(model.id)

    try {
      const body = {
        provider: 'civitai',
        model_type: modelType === 'edit' ? 'image' : modelType, // Civitai uses 'image' for edit models
        model_id: model.id,
        civitai_version_id: versionId,
        civitai_api_key: props.nsfwMode ? props.civitaiApiKey : undefined,
      }

      setToast(`Starting download for ${model.name}...`)

      const res = await postJson<InstallResponse>(backendUrl, '/models/install', body, authKey)

      if (res?.ok) {
        setToast(res.message || `Successfully installed ${model.name}`)
        // Refresh installed list after successful installation
        setTimeout(() => {
          refreshInstalled()
        }, 2000)
      } else {
        setToast(res?.message || 'Installation request sent.')
      }
    } catch (e: any) {
      setToast(`Installation failed: ${e?.message || String(e)}`)
    } finally {
      setInstallBusy(null)
    }
  }

  // API Keys management functions
  const loadApiKeysStatus = async () => {
    try {
      const data = await getJson<{ ok: boolean; keys: Record<string, any> }>(backendUrl, '/settings/api-keys', authKey)
      if (data.keys) {
        setApiKeysStatus(data.keys)
      }
    } catch (e) {
      // API keys endpoint not available - that's OK, it's optional
      console.debug('[API Keys] Endpoint not available:', e)
    }
  }

  const saveApiKey = async (provider: 'huggingface' | 'civitai') => {
    const key = apiKeyInput[provider].trim()
    if (!key) {
      setToast(`Please enter a ${provider === 'huggingface' ? 'HuggingFace' : 'Civitai'} API key`)
      return
    }

    setApiKeySaving(provider)
    try {
      const res = await postJson<{ ok: boolean; message?: string }>(
        backendUrl,
        '/settings/api-keys',
        { provider, key },
        authKey
      )
      if (res.ok) {
        setToast(res.message || `${provider} API key saved successfully`)
        setApiKeyInput(prev => ({ ...prev, [provider]: '' }))
        await loadApiKeysStatus()
      } else {
        setToast(`Failed to save ${provider} API key`)
      }
    } catch (e: any) {
      setToast(`Error saving API key: ${e?.message || String(e)}`)
    } finally {
      setApiKeySaving(null)
    }
  }

  const testApiKey = async (provider: 'huggingface' | 'civitai') => {
    setApiKeyTesting(provider)
    try {
      const keyToTest = apiKeyInput[provider].trim() || undefined
      const res = await postJson<{ ok: boolean; valid: boolean; message: string; username?: string }>(
        backendUrl,
        '/settings/api-keys/test',
        { provider, key: keyToTest },
        authKey
      )
      if (res.valid) {
        setToast(`${provider} key valid: ${res.message}`)
      } else {
        setToast(`${provider} key invalid: ${res.message}`)
      }
    } catch (e: any) {
      setToast(`Error testing API key: ${e?.message || String(e)}`)
    } finally {
      setApiKeyTesting(null)
    }
  }

  const deleteApiKey = async (provider: 'huggingface' | 'civitai') => {
    try {
      const res = await fetch(`${backendUrl}/settings/api-keys/${provider}`, {
        method: 'DELETE',
        headers: authKey ? { 'x-api-key': authKey } : {},
      })
      const data = await res.json()
      if (data.ok) {
        setToast(data.message || `${provider} API key removed`)
        await loadApiKeysStatus()
      }
    } catch (e: any) {
      setToast(`Error removing API key: ${e?.message || String(e)}`)
    }
  }

  // Load API keys status when expanded
  useEffect(() => {
    if (apiKeysExpanded) {
      loadApiKeysStatus()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [apiKeysExpanded])

  // Fetch avatar models when Add-ons tab is active
  const refreshAvatarModels = useCallback(async () => {
    setAvatarModelsLoading(true)
    setAvatarModelsError(null)
    try {
      const data = await getJson<AvatarModelsResponse>(backendUrl, '/v1/avatar-models', authKey)
      setAvatarModels(data.available || [])
      setAvatarFeatures(data.features || {})
    } catch (e: any) {
      setAvatarModelsError(e?.message || String(e))
      setAvatarModels([])
      setAvatarFeatures({})
    } finally {
      setAvatarModelsLoading(false)
    }
  }, [backendUrl, authKey])

  useEffect(() => {
    if (modelType === 'addons') {
      refreshAvatarModels()
    }
  }, [modelType, refreshAvatarModels])

  // Fetch LoRA registry + installed when LoRA tab is active (additive)
  const refreshLoraModels = useCallback(async () => {
    setLoraLoading(true)
    setLoraError(null)
    try {
      const [registryRes, installedRes] = await Promise.all([
        getJson<{ ok: boolean; loras: any[] }>(backendUrl, `/v1/lora/registry?spicy=${props.nsfwMode ? 'true' : 'false'}`, authKey),
        getJson<{ ok: boolean; loras: any[] }>(backendUrl, '/v1/lora/installed', authKey),
      ])
      setLoraRegistry(registryRes.loras || [])
      setLoraInstalled(installedRes.loras || [])
    } catch (e: any) {
      setLoraError(e?.message || String(e))
      setLoraRegistry([])
      setLoraInstalled([])
    } finally {
      setLoraLoading(false)
    }
  }, [backendUrl, authKey, props.nsfwMode])

  useEffect(() => {
    if (modelType === 'lora') {
      refreshLoraModels()
    } else {
      // Clear stale registry when leaving LoRA tab so NSFW entries
      // don't linger in memory after spicy mode is toggled off.
      setLoraRegistry([])
      setLoraInstalled([])
    }
  }, [modelType, refreshLoraModels])

  // LoRA install — mirrors installSingleAvatarModel pattern
  const installLora = async (loraId: string) => {
    setLoraInstallBusy(loraId)
    const entry = loraRegistry.find((l: any) => l.id === loraId)
    setToast(`Installing ${entry?.name || loraId}...`)
    try {
      const res = await fetch(`${backendUrl}/v1/lora/${loraId}/install`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(authKey ? { 'x-api-key': authKey } : {}),
        },
      })
      const data = await res.json()
      if (data.ok) {
        if (data.already_installed) {
          setToast(`${entry?.name || loraId} is already installed`)
          setLoraInstallBusy(null)
          return
        }
        setLoraDownloadBusy(`single:${loraId}`)
      } else {
        setToast(`Install failed: ${data.error || 'unknown error'}`)
      }
    } catch (e: any) {
      setToast(`Install error: ${e?.message || String(e)}`)
    } finally {
      setLoraInstallBusy(null)
    }
  }

  // LoRA delete — mirrors deleteAvatarModel pattern
  const deleteLora = async (loraId: string) => {
    setLoraDeleteBusy(loraId)
    setLoraDeleteConfirm(null)
    try {
      const res = await fetch(`${backendUrl}/v1/lora/${loraId}`, {
        method: 'DELETE',
        headers: authKey ? { 'x-api-key': authKey } : {},
      })
      const data = await res.json()
      if (data.ok) {
        setToast(`Deleted ${data.name || loraId} — freed ${data.freed_human || ''}`)
        setTimeout(() => refreshLoraModels(), 500)
      } else {
        setToast(`Delete failed: ${data.error || 'unknown error'}`)
      }
    } catch (e: any) {
      setToast(`Delete error: ${e?.message || String(e)}`)
    } finally {
      setLoraDeleteBusy(null)
    }
  }

  // Poll LoRA download status — mirrors avatar download polling
  useEffect(() => {
    if (!loraDownloadBusy) return

    let cancelled = false
    const poll = async () => {
      while (!cancelled) {
        await new Promise((r) => setTimeout(r, 4000))
        if (cancelled) break
        try {
          const res = await fetch(`${backendUrl}/v1/lora/download/status`, {
            headers: authKey ? { 'x-api-key': authKey } : {},
          })
          if (!res.ok) continue
          const status: AvatarDownloadStatus = await res.json()
          if (cancelled) break

          setLoraDownloadStatus(status)

          if (status.finished || !status.running) {
            const msg = `Install complete: ${status.installed_count}/${status.total_models} installed`
              + (status.failed_count > 0 ? ` (${status.failed_count} failed)` : '')
              + (status.downloaded_bytes > 0 ? ` — ${formatBytes(status.downloaded_bytes)} in ${Math.round(status.elapsed)}s` : '')
            setToast(msg)
            setLoraDownloadBusy(null)
            setTimeout(() => refreshLoraModels(), 500)
            break
          }
        } catch {
          // Network hiccup — keep polling
        }
      }
    }
    poll()
    return () => { cancelled = true }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [loraDownloadBusy, backendUrl, authKey])

  const downloadAvatarPreset = async (preset: 'basic' | 'full') => {
    setAvatarDownloadBusy(preset)
    setAvatarDownloadStatus(null)
    setToast(`Starting ${preset === 'basic' ? 'Basic' : 'Full'} avatar model download...`)
    try {
      const res = await fetch(`${backendUrl}/v1/avatar-models/download?preset=${preset}`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(authKey ? { 'x-api-key': authKey } : {}),
        },
      })
      if (!res.ok) {
        const text = await res.text().catch(() => '')
        throw new Error(`HTTP ${res.status}${text ? `: ${text}` : ''}`)
      }
      const data: AvatarDownloadStartResponse = await res.json()
      if (!data.ok) {
        setToast(data.error || 'Download failed to start.')
        setAvatarDownloadBusy(null)
        return
      }
      setToast(data.message || 'Download started — monitoring progress...')
      // Start polling
    } catch (e: any) {
      setToast(`Download error: ${e?.message || String(e)}`)
      setAvatarDownloadBusy(null)
    }
  }

  // Poll download status while a download is active
  useEffect(() => {
    if (!avatarDownloadBusy) return

    let cancelled = false
    const poll = async () => {
      while (!cancelled) {
        await new Promise((r) => setTimeout(r, 4000))
        if (cancelled) break
        try {
          const res = await fetch(`${backendUrl}/v1/avatar-models/download/status`, {
            headers: authKey ? { 'x-api-key': authKey } : {},
          })
          if (!res.ok) continue
          const status: AvatarDownloadStatus = await res.json()
          if (cancelled) break

          setAvatarDownloadStatus(status)

          if (status.finished || !status.running) {
            // Done — show final summary toast only once
            const msg = `Download complete: ${status.installed_count}/${status.total_models} installed`
              + (status.failed_count > 0 ? ` (${status.failed_count} failed)` : '')
              + ` — ${formatBytes(status.downloaded_bytes)} in ${Math.round(status.elapsed)}s`
            setToast(msg)
            setAvatarDownloadBusy(null)
            // Refresh model list
            setTimeout(() => refreshAvatarModels(), 500)
            break
          }
        } catch {
          // Network hiccup — keep polling
        }
      }
    }
    poll()
    return () => { cancelled = true }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [avatarDownloadBusy, backendUrl, authKey])

  const deleteAvatarModel = async (modelId: string) => {
    setAvatarDeleteBusy(modelId)
    setAvatarDeleteConfirm(null)
    try {
      const res = await fetch(`${backendUrl}/v1/avatar-models/${modelId}`, {
        method: 'DELETE',
        headers: authKey ? { 'x-api-key': authKey } : {},
      })
      const data = await res.json()
      if (data.ok) {
        setToast(`Uninstalled ${data.name || modelId} — freed ${data.freed_human || ''}`)
        setTimeout(() => refreshAvatarModels(), 500)
      } else {
        setToast(`Delete failed: ${data.error || 'unknown error'}`)
      }
    } catch (e: any) {
      setToast(`Delete error: ${e?.message || String(e)}`)
    } finally {
      setAvatarDeleteBusy(null)
    }
  }

  const installSingleAvatarModel = async (modelId: string) => {
    setAvatarInstallBusy(modelId)
    setToast(`Starting download for ${modelId}...`)
    try {
      const res = await fetch(`${backendUrl}/v1/avatar-models/${modelId}/install`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(authKey ? { 'x-api-key': authKey } : {}),
        },
      })
      const data = await res.json()
      if (data.ok) {
        if (data.already_installed) {
          setToast(`${modelId} is already installed`)
          setAvatarInstallBusy(null)
          return
        }
        // Start polling for progress
        setAvatarDownloadBusy(`single:${modelId}`)
      } else {
        setToast(`Install failed: ${data.error || 'unknown error'}`)
      }
    } catch (e: any) {
      setToast(`Install error: ${e?.message || String(e)}`)
    } finally {
      setAvatarInstallBusy(null)
    }
  }

  // Auto-dismiss toast
  useEffect(() => {
    if (!toast) return
    const t = setTimeout(() => setToast(null), 3500)
    return () => clearTimeout(t)
  }, [toast])

  return (
    <div className="h-full w-full bg-black text-white overflow-hidden flex flex-col">
      {/* Header */}
      <div className="px-8 py-6 border-b border-white/10 flex items-center justify-between bg-gradient-to-b from-white/[0.02] to-transparent">
        <div>
          <div className="text-2xl font-bold text-white tracking-tight">Model Management</div>
          <div className="text-sm text-white/40 mt-1">Configure and deploy AI models across providers</div>
        </div>

        <div className="flex items-center gap-3">
          <button
            type="button"
            onClick={() => refreshCatalog()}
            disabled={catalogLoading}
            className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-xs font-semibold flex items-center gap-2.5 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
          >
            <RefreshCw size={15} className={catalogLoading ? 'animate-spin' : ''} />
            Refresh Catalog
          </button>

          <button
            type="button"
            onClick={() => refreshInstalled()}
            disabled={installedLoading}
            className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-xs font-semibold flex items-center gap-2.5 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
          >
            <RefreshCw size={15} className={installedLoading ? 'animate-spin' : ''} />
            Refresh Installed
          </button>

          {/* API Keys Settings Button */}
          <button
            type="button"
            onClick={() => setApiKeysExpanded(true)}
            className="p-2.5 rounded-xl bg-transparent hover:bg-white/5 border border-transparent hover:border-white/10 text-white/40 hover:text-white/70 transition-all"
            title="API Keys (for gated models)"
          >
            <Key size={16} />
          </button>
        </div>
      </div>

      {/* API Keys Modal */}
      {apiKeysExpanded && (
        <div className="fixed inset-0 z-50 flex items-center justify-center">
          {/* Backdrop */}
          <div
            className="absolute inset-0 bg-black/70 backdrop-blur-sm"
            onClick={() => setApiKeysExpanded(false)}
          />

          {/* Modal */}
          <div className="relative bg-zinc-900 border border-white/10 rounded-2xl shadow-2xl w-full max-w-2xl mx-4 overflow-hidden">
            {/* Modal Header */}
            <div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/[0.02]">
              <div className="flex items-center gap-3">
                <Key size={18} className="text-white/50" />
                <div>
                  <h2 className="text-lg font-bold text-white">API Keys</h2>
                  <p className="text-xs text-white/40">Optional - for gated HuggingFace and Civitai models</p>
                </div>
              </div>
              <button
                onClick={() => setApiKeysExpanded(false)}
                className="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white/70 transition-all"
              >
                <X size={18} />
              </button>
            </div>

            {/* Modal Content */}
            <div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
              {/* HuggingFace Token */}
              <div className="bg-white/5 border border-white/10 rounded-xl p-4">
                <div className="flex items-center justify-between mb-3">
                  <div>
                    <div className="text-sm font-bold text-white">HuggingFace Token</div>
                    <div className="text-[10px] text-white/40 mt-0.5">For FLUX, SVD XT 1.1, gated models</div>
                  </div>
                  {apiKeysStatus.huggingface?.configured && (
                    <span className={`px-2 py-1 rounded text-[10px] font-bold uppercase ${
                      apiKeysStatus.huggingface.source === 'environment'
                        ? 'bg-blue-500/20 text-blue-300'
                        : 'bg-emerald-500/20 text-emerald-300'
                    }`}>
                      {apiKeysStatus.huggingface.source === 'environment' ? 'ENV' : 'Stored'}
                    </span>
                  )}
                </div>

                {apiKeysStatus.huggingface?.configured ? (
                  <div className="space-y-2">
                    <div className="flex items-center gap-2 bg-white/5 rounded-lg px-3 py-2">
                      <span className="text-emerald-400 text-sm">✓</span>
                      <span className="text-xs text-white/60 font-mono">{apiKeysStatus.huggingface.masked}</span>
                    </div>
                    <div className="flex gap-2">
                      <button
                        onClick={() => testApiKey('huggingface')}
                        disabled={apiKeyTesting === 'huggingface'}
                        className="flex-1 px-3 py-2 text-xs font-semibold rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-all disabled:opacity-50"
                      >
                        {apiKeyTesting === 'huggingface' ? 'Testing...' : 'Test'}
                      </button>
                      {apiKeysStatus.huggingface.source !== 'environment' && (
                        <button
                          onClick={() => deleteApiKey('huggingface')}
                          className="px-3 py-2 text-xs font-semibold rounded-lg bg-red-500/10 hover:bg-red-500/20 text-red-300 border border-red-500/20 transition-all"
                        >
                          Remove
                        </button>
                      )}
                    </div>
                  </div>
                ) : (
                  <div className="space-y-2">
                    <input
                      type="password"
                      value={apiKeyInput.huggingface}
                      onChange={(e) => setApiKeyInput(prev => ({ ...prev, huggingface: e.target.value }))}
                      placeholder="hf_xxxxxxxxxxxxxxxxxx"
                      className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-white/30 transition-all placeholder:text-white/20"
                    />
                    <div className="flex gap-2">
                      <button
                        onClick={() => testApiKey('huggingface')}
                        disabled={!apiKeyInput.huggingface.trim() || apiKeyTesting === 'huggingface'}
                        className="flex-1 px-3 py-2 text-xs font-semibold rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-all disabled:opacity-50"
                      >
                        {apiKeyTesting === 'huggingface' ? 'Testing...' : 'Test'}
                      </button>
                      <button
                        onClick={() => saveApiKey('huggingface')}
                        disabled={!apiKeyInput.huggingface.trim() || apiKeySaving === 'huggingface'}
                        className="flex-1 px-3 py-2 text-xs font-semibold rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition-all disabled:opacity-50"
                      >
                        {apiKeySaving === 'huggingface' ? 'Saving...' : 'Save'}
                      </button>
                    </div>
                    <a
                      href="https://huggingface.co/settings/tokens"
                      target="_blank"
                      rel="noopener noreferrer"
                      className="text-[10px] text-blue-400 hover:text-blue-300"
                    >
                      Get token from huggingface.co →
                    </a>
                  </div>
                )}
              </div>

              {/* Civitai API Key */}
              <div className="bg-white/5 border border-white/10 rounded-xl p-4">
                <div className="flex items-center justify-between mb-3">
                  <div>
                    <div className="text-sm font-bold text-white">Civitai API Key</div>
                    <div className="text-[10px] text-white/40 mt-0.5">For NSFW and restricted downloads</div>
                  </div>
                  {apiKeysStatus.civitai?.configured && (
                    <span className={`px-2 py-1 rounded text-[10px] font-bold uppercase ${
                      apiKeysStatus.civitai.source === 'environment'
                        ? 'bg-blue-500/20 text-blue-300'
                        : 'bg-emerald-500/20 text-emerald-300'
                    }`}>
                      {apiKeysStatus.civitai.source === 'environment' ? 'ENV' : 'Stored'}
                    </span>
                  )}
                </div>

                {apiKeysStatus.civitai?.configured ? (
                  <div className="space-y-2">
                    <div className="flex items-center gap-2 bg-white/5 rounded-lg px-3 py-2">
                      <span className="text-emerald-400 text-sm">✓</span>
                      <span className="text-xs text-white/60 font-mono">{apiKeysStatus.civitai.masked}</span>
                    </div>
                    <div className="flex gap-2">
                      <button
                        onClick={() => testApiKey('civitai')}
                        disabled={apiKeyTesting === 'civitai'}
                        className="flex-1 px-3 py-2 text-xs font-semibold rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-all disabled:opacity-50"
                      >
                        {apiKeyTesting === 'civitai' ? 'Testing...' : 'Test'}
                      </button>
                      {apiKeysStatus.civitai.source !== 'environment' && (
                        <button
                          onClick={() => deleteApiKey('civitai')}
                          className="px-3 py-2 text-xs font-semibold rounded-lg bg-red-500/10 hover:bg-red-500/20 text-red-300 border border-red-500/20 transition-all"
                        >
                          Remove
                        </button>
                      )}
                    </div>
                  </div>
                ) : (
                  <div className="space-y-2">
                    <input
                      type="password"
                      value={apiKeyInput.civitai}
                      onChange={(e) => setApiKeyInput(prev => ({ ...prev, civitai: e.target.value }))}
                      placeholder="xxxxxxxxxxxxxxxxxxxxxxxx"
                      className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-sm font-mono outline-none focus:border-white/30 transition-all placeholder:text-white/20"
                    />
                    <div className="flex gap-2">
                      <button
                        onClick={() => testApiKey('civitai')}
                        disabled={!apiKeyInput.civitai.trim() || apiKeyTesting === 'civitai'}
                        className="flex-1 px-3 py-2 text-xs font-semibold rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-all disabled:opacity-50"
                      >
                        {apiKeyTesting === 'civitai' ? 'Testing...' : 'Test'}
                      </button>
                      <button
                        onClick={() => saveApiKey('civitai')}
                        disabled={!apiKeyInput.civitai.trim() || apiKeySaving === 'civitai'}
                        className="flex-1 px-3 py-2 text-xs font-semibold rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition-all disabled:opacity-50"
                      >
                        {apiKeySaving === 'civitai' ? 'Saving...' : 'Save'}
                      </button>
                    </div>
                    <a
                      href="https://civitai.com/user/account"
                      target="_blank"
                      rel="noopener noreferrer"
                      className="text-[10px] text-blue-400 hover:text-blue-300"
                    >
                      Get API key from civitai.com →
                    </a>
                  </div>
                )}
              </div>
            </div>

            {/* Modal Footer */}
            <div className="px-6 py-4 border-t border-white/10 bg-white/[0.02]">
              <p className="text-[11px] text-white/30">
                Keys are stored locally and never sent to external servers except for authentication.
                Environment variables (HF_TOKEN, CIVITAI_API_KEY) take precedence over stored keys.
              </p>
            </div>
          </div>
        </div>
      )}

      {/* Controls */}
      <div className="px-8 py-5 border-b border-white/10 bg-white/[0.01]">
        <div className="grid grid-cols-1 lg:grid-cols-[200px_1fr_1fr] gap-4 max-w-7xl">
          <div>
            <label className="text-[10px] text-white/40 font-bold uppercase tracking-wider block mb-2.5">Model Type</label>
            <div className="flex flex-col gap-1.5">
              {(['chat', 'multimodal', 'image', 'edit', 'video', 'enhance', 'lora', 'addons'] as const).map((t) => (
                <button
                  key={t}
                  type="button"
                  onClick={() => setModelType(t)}
                  className={`px-4 py-2.5 rounded-lg border text-xs font-bold uppercase tracking-wide transition-all ${
                    modelType === t
                      ? 'bg-white text-black border-white shadow-lg shadow-white/20'
                      : 'bg-transparent border-white/10 text-white/60 hover:bg-white/5 hover:border-white/20 hover:text-white/80'
                  }`}
                >
                  {t === 'addons' ? '🧩 Add-ons' : t === 'lora' ? 'LoRA' : t}
                </button>
              ))}
            </div>
          </div>

          <div>
            <label className="text-[10px] text-white/40 font-bold uppercase tracking-wider block mb-2.5">Provider</label>
            <select
              value={provider}
              onChange={(e) => setProvider(e.target.value)}
              className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm font-medium outline-none focus:border-white/30 focus:bg-white/10 transition-all"
            >
              {availableProviders.length > 0 ? (
                availableProviders.map((p) => (
                  <option key={p.name} value={p.name}>
                    {p.label || p.name}
                  </option>
                ))
              ) : (
                // Fallback options when providers haven't loaded
                modelType === 'chat' ? (
                  <>
                    <option value="ollama">Ollama</option>
                    <option value="openai_compat">OpenAI-compatible (vLLM)</option>
                    <option value="openai">OpenAI</option>
                    <option value="claude">Claude</option>
                    <option value="watsonx">Watsonx</option>
                  </>
                ) : (
                  <option value="comfyui">ComfyUI</option>
                )
              )}
            </select>
            {providersError ? <div className="mt-2 text-xs text-rose-400/80">{providersError}</div> : null}
          </div>

          <div>
            <label className="text-[10px] text-white/40 font-bold uppercase tracking-wider block mb-2.5">Base URL Override</label>
            <input
              value={baseUrl}
              onChange={(e) => setBaseUrl(e.target.value)}
              placeholder={modelType === 'chat' ? 'http://localhost:11434' : 'http://localhost:8188'}
              className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm font-medium outline-none focus:border-white/30 focus:bg-white/10 transition-all placeholder:text-white/30"
            />
            <div className="mt-2 text-[10px] text-white/30 font-medium">
              Optional. Leave empty to use default configuration.
            </div>
          </div>
        </div>
      </div>

      {/* Civitai Input Section (only when provider is civitai) */}
      {provider === 'civitai' && (
        <div className="px-8 py-4 border-b border-white/10 bg-gradient-to-br from-blue-500/5 to-blue-500/0">
          <div className="max-w-7xl">
            {/* Search Bar */}
            <div className="mb-6">
              <label className="text-[10px] text-cyan-400 font-bold uppercase tracking-wider block mb-2.5">
                🔍 Search Civitai Models
              </label>
              <div className="flex items-center gap-3">
                <input
                  value={civitaiQuery}
                  onChange={(e) => setCivitaiQuery(e.target.value)}
                  onKeyDown={(e) => e.key === 'Enter' && searchCivitai()}
                  placeholder={`Search ${modelType === 'video' ? 'video' : 'image'} models on Civitai...`}
                  className="flex-1 bg-white/5 border border-cyan-500/20 rounded-xl px-4 py-3 text-sm font-medium outline-none focus:border-cyan-500/40 focus:bg-white/10 transition-all placeholder:text-white/30"
                />
                <button
                  type="button"
                  onClick={() => searchCivitai()}
                  disabled={civitaiSearchLoading || !civitaiQuery.trim()}
                  className={[
                    "px-6 py-3 rounded-xl text-white text-sm font-bold uppercase tracking-wide transition-all shadow-lg",
                    civitaiSearchLoading
                      ? "bg-cyan-700 cursor-wait shadow-cyan-700/30"
                      : !civitaiQuery.trim()
                      ? "bg-cyan-600/50 cursor-not-allowed shadow-cyan-600/10"
                      : "bg-cyan-600 hover:bg-cyan-500 shadow-cyan-600/20 hover:shadow-cyan-600/30 hover:scale-105 active:scale-95"
                  ].join(" ")}
                >
                  {civitaiSearchLoading ? (
                    <svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                      <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                      <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                    </svg>
                  ) : (
                    'Search'
                  )}
                </button>
              </div>
              {civitaiSearchError && (
                <div className="mt-2 text-[10px] text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
                  ⚠️ {civitaiSearchError}
                </div>
              )}
            </div>

            {/* Search Results */}
            {civitaiResults.length > 0 && (
              <div className="mb-6">
                <div className="flex items-center justify-between mb-3">
                  <div className="text-[10px] text-cyan-400 font-bold uppercase tracking-wider">
                    Search Results ({civitaiResults.length} models)
                  </div>
                  {civitaiTotalPages > 1 && (
                    <div className="flex items-center gap-2">
                      <button
                        onClick={() => searchCivitai(civitaiPage - 1)}
                        disabled={civitaiPage <= 1 || civitaiSearchLoading}
                        className="px-3 py-1.5 text-xs font-semibold rounded-lg bg-white/5 hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
                      >
                        ← Prev
                      </button>
                      <span className="text-xs text-white/50">
                        Page {civitaiPage} of {civitaiTotalPages}
                      </span>
                      <button
                        onClick={() => searchCivitai(civitaiPage + 1)}
                        disabled={civitaiPage >= civitaiTotalPages || civitaiSearchLoading}
                        className="px-3 py-1.5 text-xs font-semibold rounded-lg bg-white/5 hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
                      >
                        Next →
                      </button>
                    </div>
                  )}
                </div>

                <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[400px] overflow-y-auto pr-2 scrollbar-hide">
                  {civitaiResults.map((model) => (
                    <div
                      key={model.id}
                      className="bg-white/5 border border-white/10 rounded-xl overflow-hidden hover:border-cyan-500/30 transition-all group"
                    >
                      {/* Thumbnail */}
                      <div className="relative h-32 bg-black/50">
                        {model.thumbnail ? (
                          <img
                            src={model.thumbnail}
                            alt={model.name}
                            className="w-full h-full object-cover"
                            loading="lazy"
                          />
                        ) : (
                          <div className="w-full h-full flex items-center justify-center text-white/20">
                            <span className="text-4xl">🖼️</span>
                          </div>
                        )}
                        {model.nsfw && (
                          <span className="absolute top-2 right-2 px-2 py-1 text-[9px] font-bold uppercase bg-red-600 text-white rounded">
                            NSFW
                          </span>
                        )}
                      </div>

                      {/* Info */}
                      <div className="p-3">
                        <div className="font-semibold text-sm text-white truncate mb-1">{model.name}</div>
                        <div className="text-[10px] text-white/40 mb-2">by {model.creator}</div>

                        <div className="flex items-center gap-3 text-[10px] text-white/50 mb-3">
                          <span>⬇️ {model.downloads >= 1000 ? `${(model.downloads / 1000).toFixed(1)}K` : model.downloads}</span>
                          <span>⭐ {model.rating.toFixed(1)}</span>
                          <span>({model.ratingCount})</span>
                        </div>

                        {model.tags.length > 0 && (
                          <div className="flex flex-wrap gap-1 mb-3">
                            {model.tags.slice(0, 3).map((tag) => (
                              <span key={tag} className="px-2 py-0.5 text-[9px] bg-white/5 rounded text-white/40">
                                {tag}
                              </span>
                            ))}
                          </div>
                        )}

                        {/* Version selector and install */}
                        {model.versions.length > 0 && (
                          <div className="flex items-center gap-2">
                            <select
                              className="flex-1 bg-white/5 border border-white/10 rounded-lg px-2 py-1.5 text-xs outline-none focus:border-cyan-500/30"
                              defaultValue={model.versions[0]?.id}
                              id={`version-${model.id}`}
                            >
                              {model.versions.map((v) => (
                                <option key={v.id} value={v.id}>
                                  {v.name} ({(v.sizeKB / 1024 / 1024).toFixed(1)}GB)
                                </option>
                              ))}
                            </select>
                            <button
                              type="button"
                              onClick={() => {
                                const select = document.getElementById(`version-${model.id}`) as HTMLSelectElement
                                const versionId = select?.value || model.versions[0]?.id
                                installFromCivitaiResult(model, versionId)
                              }}
                              disabled={installBusy === model.id}
                              className={[
                                "px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all",
                                installBusy === model.id
                                  ? "bg-cyan-700 text-white cursor-wait"
                                  : "bg-cyan-600 text-white hover:bg-cyan-500"
                              ].join(" ")}
                            >
                              {installBusy === model.id ? (
                                <svg className="animate-spin h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                                  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                                  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                                </svg>
                              ) : (
                                <Download size={12} />
                              )}
                              Install
                            </button>
                          </div>
                        )}

                        {model.link && (
                          <a
                            href={model.link}
                            target="_blank"
                            rel="noopener noreferrer"
                            className="block mt-2 text-[10px] text-cyan-400 hover:text-cyan-300"
                          >
                            View on Civitai →
                          </a>
                        )}
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            )}

            {/* Manual Version ID Input */}
            <div className="flex items-start gap-4 mb-6 border-t border-white/10 pt-4">
              <div className="flex-1">
                <label className="text-[10px] text-blue-400 font-bold uppercase tracking-wider block mb-2.5">
                  📋 Direct Version ID (Advanced)
                </label>
                <input
                  value={civitaiVersionId}
                  onChange={(e) => setCivitaiVersionId(e.target.value)}
                  placeholder="e.g., 128713 (from Civitai model URL)"
                  className="w-full bg-white/5 border border-blue-500/20 rounded-xl px-4 py-3 text-sm font-medium outline-none focus:border-blue-500/40 focus:bg-white/10 transition-all placeholder:text-white/30"
                />
                <div className="mt-2 text-[10px] text-blue-300/60 font-medium">
                  Enter a version ID from any Civitai model URL (e.g., civitai.com/models/<strong>128713</strong>)
                </div>
              </div>
              <div className="flex-shrink-0 pt-7">
                <button
                  type="button"
                  onClick={() => {
                    if (!civitaiVersionId.trim()) {
                      setToast('Please enter a Civitai version ID first')
                      return
                    }
                    tryInstall(civitaiVersionId, undefined)
                  }}
                  disabled={!civitaiVersionId.trim() || installBusy !== null}
                  className={[
                    "px-6 py-3 rounded-xl text-white text-sm font-bold uppercase tracking-wide transition-all shadow-lg relative overflow-hidden",
                    installBusy !== null
                      ? "bg-blue-700 cursor-wait shadow-blue-700/30"
                      : !civitaiVersionId.trim()
                      ? "bg-blue-600/50 cursor-not-allowed shadow-blue-600/10"
                      : "bg-blue-600 hover:bg-blue-500 shadow-blue-600/20 hover:shadow-blue-600/30 hover:scale-105 active:scale-95"
                  ].join(" ")}
                >
                  {installBusy !== null && (
                    <span className="absolute inset-0 bg-gradient-to-r from-blue-700 via-blue-600 to-blue-700 animate-shimmer" style={{
                      backgroundSize: '200% 100%',
                      animation: 'shimmer 2s infinite linear'
                    }} />
                  )}
                  <span className="relative z-10 flex items-center gap-2.5 justify-center">
                    {installBusy !== null ? (
                      <>
                        <svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                          <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                          <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                        </svg>
                        <span>Downloading...</span>
                      </>
                    ) : (
                      <>
                        <Download size={16} strokeWidth={2.5} />
                        <span>Download</span>
                      </>
                    )}
                  </span>
                </button>
              </div>
            </div>

            {/* Recommended Models Info */}
            <div className="border-t border-blue-500/20 pt-4">
              <div className="text-[10px] text-blue-400 font-bold uppercase tracking-wider mb-3">
                ⭐ Recommended {modelType === 'image' ? 'Image' : 'Video'} Models from Civitai
              </div>
              <div className="text-[10px] text-blue-300/60 font-medium">
                Click "Install" on any model below to download directly from Civitai. Models are installed to your ComfyUI models folder.
              </div>
            </div>
          </div>
        </div>
      )}

      {/* Body */}
      <div className="flex-1 overflow-y-auto px-8 py-6 scrollbar-hide">
        <div className="flex flex-col gap-4 max-w-7xl mx-auto">
          {/* Error messages - hide for Civitai since it's download-only */}
          {installedError && provider !== 'civitai' ? (
            <div className={`rounded-xl border p-5 ${
              installedError.includes('Ollama') || installedError.includes('11434')
                ? 'border-white/10 bg-white/[0.03] text-white/60'
                : 'border-amber-500/30 bg-gradient-to-br from-amber-500/10 to-amber-500/5 text-amber-200'
            }`}>
              <div className={`font-bold text-sm ${
                installedError.includes('Ollama') || installedError.includes('11434')
                  ? 'text-white/70'
                  : 'text-amber-100'
              }`}>
                {installedError.includes('Ollama') || installedError.includes('11434')
                  ? 'Ollama Not Running'
                  : 'Configuration Required'}
              </div>
              <div className="text-xs mt-2 font-medium opacity-70">
                {installedError.includes('Ollama') || installedError.includes('11434')
                  ? 'Ollama is not reachable. Start Ollama or switch to a different provider tab. ComfyUI models and Add-ons still work normally.'
                  : installedError.includes('LLM_BASE_URL')
                    ? 'Configure LLM_BASE_URL environment variable to use OpenAI-compatible (vLLM) provider. Or switch to a different provider.'
                    : installedError}
              </div>
            </div>
          ) : null}

          {catalogError && !supportedForSelection.length ? (
            <div className="rounded-xl border border-blue-500/20 bg-gradient-to-br from-blue-500/10 to-blue-500/5 p-5 text-blue-200">
              <div className="font-bold text-sm text-blue-100">Backend catalog unavailable</div>
              <div className="mt-2 text-xs text-blue-200/70 font-medium">
                Using fallback model list. Configure <span className="font-mono bg-blue-500/20 px-1.5 py-0.5 rounded text-blue-100">/model-catalog</span> endpoint for enhanced functionality.
              </div>
            </div>
          ) : null}

          {/* Models table */}
          <div className="rounded-2xl border border-white/10 overflow-hidden bg-gradient-to-b from-white/[0.02] to-transparent">
            <div className="bg-white/5 px-6 py-4 flex items-center justify-between border-b border-white/10">
              <div className="text-xs font-bold text-white uppercase tracking-wider">Available Models</div>
              <div className="text-xs text-white/50 font-semibold">
                {(() => {
                  const isRemoteProvider = ['openai', 'claude', 'watsonx'].includes(provider)
                  if (installedLoading) return 'Loading…'
                  if (provider === 'civitai') {
                    return `🧪 ${supportedForSelection.length} Recommended Models`
                  }
                  if (isRemoteProvider) {
                    return `${supportedForSelection.length} API Models`
                  }
                  return `${installed.length} Installed${supportedForSelection.length ? ` · ${supportedForSelection.length} Available` : ''}`
                })()}
              </div>
            </div>

            <div className="divide-y divide-white/5">
              {merged.length === 0 ? (
                <div className="p-12 text-white/50 text-sm text-center font-medium">
                  {provider === 'civitai' ? (
                    <div className="space-y-2">
                      <div className="text-blue-400 font-bold">🧪 Civitai Download</div>
                      <div>Enter a Civitai version ID above to download models.</div>
                      <div className="text-xs text-white/40">
                        Find models at <a href="https://civitai.com" target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:text-blue-300 underline">civitai.com</a>
                      </div>
                    </div>
                  ) : (
                    'No models found. Try changing provider/base URL and refresh.'
                  )}
                </div>
              ) : (
                merged.map((row) => {
                  const statusKind =
                    row.status === 'installed'
                      ? 'ok'
                      : row.status === 'missing'
                      ? 'warn'
                      : 'muted'

                  const statusLabel =
                    row.status === 'installed'
                      ? 'Installed'
                      : row.status === 'missing'
                      ? 'Available'
                      : 'Custom'

                  // Only allow downloads for local providers (Ollama, ComfyUI, Civitai)
                  // Remote API providers (OpenAI, Claude, Watsonx, openai_compat) don't support local installation
                  const isLocalProvider = provider === 'ollama' || provider === 'comfyui' || provider === 'civitai'
                  const canDownload = row.status === 'missing' && isLocalProvider
                  const canDelete = (row.status === 'installed' || row.status === 'installed_unsupported') && isLocalProvider && !row.protected
                  const isCivitai = provider === 'civitai'

                  return (
                    <div key={row.id} className="p-5 flex items-center justify-between gap-4 hover:bg-white/[0.02] transition-all group">
                      <div className="min-w-0 flex-1">
                        <div className="flex items-center gap-2.5 mb-3">
                          <div className={`px-3 py-1.5 rounded-lg border text-[10px] font-bold uppercase tracking-wider flex items-center gap-1.5 ${chipClass(statusKind)}`}>
                            <IconStatus kind={statusKind} />
                            <span>{statusLabel}</span>
                          </div>
                          {row.recommended ? (
                            <div className="px-3 py-1.5 rounded-lg border border-blue-500/30 bg-blue-500/10 text-[10px] font-bold uppercase tracking-wider text-blue-200">
                              ⭐ Recommended
                            </div>
                          ) : null}
                          {row.recommended_nsfw && props.nsfwMode ? (
                            <div className="px-3 py-1.5 rounded-lg border border-pink-500/30 bg-pink-500/10 text-[10px] font-bold uppercase tracking-wider text-pink-200">
                              🔥 NSFW Pick
                            </div>
                          ) : null}
                          {row.nsfw ? (
                            <div className="px-3 py-1.5 rounded-lg border border-red-500/30 bg-red-500/10 text-[10px] font-bold uppercase tracking-wider text-red-200">
                              🌶️ Adult
                            </div>
                          ) : null}
                          {isCivitai && row.civitai_version_id ? (
                            <div className="px-3 py-1.5 rounded-lg border border-cyan-500/30 bg-cyan-500/10 text-[10px] font-bold uppercase tracking-wider text-cyan-200">
                              v{row.civitai_version_id}
                            </div>
                          ) : null}
                          {row.protected && (row.status === 'installed' || row.status === 'installed_unsupported') ? (
                            <div className="px-3 py-1.5 rounded-lg border border-white/20 bg-white/5 text-[10px] font-bold uppercase tracking-wider text-white/50">
                              🔒 Default
                            </div>
                          ) : null}
                        </div>

                        <div className="font-bold text-base text-white truncate group-hover:text-white transition-colors">{row.label}</div>
                        <div className="text-[11px] text-white/40 font-mono truncate mt-1">{row.id}</div>
                        {row.description ? (
                          <div className="text-[11px] text-white/30 truncate mt-1">{row.description}</div>
                        ) : null}

                        {/* Pack metadata - shows file count, required nodes, and hints */}
                        {row.install?.files && row.install.files.length > 1 ? (
                          <div className="text-[11px] text-purple-400/80 mt-1">
                            Pack: <span className="font-semibold">{row.install.files.length}</span> files
                          </div>
                        ) : null}

                        {row.install?.requires_custom_nodes && row.install.requires_custom_nodes.length > 0 ? (
                          <div className="text-[11px] text-amber-400/70 mt-1 truncate">
                            Requires:{" "}
                            <span className="font-semibold">
                              {row.install.requires_custom_nodes.join(", ")}
                            </span>
                          </div>
                        ) : null}

                        {row.install?.hint ? (
                          <div className="text-[11px] text-white/25 mt-1 truncate">
                            {row.install.hint}
                          </div>
                        ) : null}

                        {isCivitai && row.civitai_url ? (
                          <a
                            href={row.civitai_url}
                            target="_blank"
                            rel="noopener noreferrer"
                            className="text-[11px] text-blue-400 hover:text-blue-300 underline mt-1 inline-block"
                          >
                            View on Civitai →
                          </a>
                        ) : null}
                      </div>

                      <div className="flex items-center gap-3 flex-shrink-0">
                        <button
                          type="button"
                          className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-xs font-bold flex items-center gap-2 transition-all hover:scale-105 active:scale-95"
                          onClick={() => {
                            navigator.clipboard?.writeText(row.id).catch(() => {})
                            setToast('Model ID copied to clipboard')
                          }}
                          title="Copy model ID"
                        >
                          <Copy size={14} />
                          <span>Copy</span>
                        </button>

                        {canDownload ? (
                          <button
                            type="button"
                            className={[
                              "px-5 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wide flex items-center gap-2.5 transition-all shadow-lg relative overflow-hidden",
                              installBusy === row.id
                                ? "bg-blue-600 text-white cursor-wait shadow-blue-600/30"
                                : isCivitai
                                ? "bg-cyan-600 text-white hover:bg-cyan-500 hover:shadow-cyan-600/30 hover:scale-105 active:scale-95 shadow-cyan-600/20"
                                : "bg-white text-black hover:bg-gray-100 hover:shadow-white/30 hover:scale-105 active:scale-95 shadow-white/20"
                            ].join(" ")}
                            disabled={installBusy === row.id}
                            onClick={() => {
                              // For Civitai, use the version ID if available
                              if (isCivitai && row.civitai_version_id) {
                                setCivitaiVersionId(row.civitai_version_id)
                                void tryInstall(row.civitai_version_id, row.install)
                              } else {
                                void tryInstall(row.id, row.install)
                              }
                            }}
                            title={installBusy === row.id ? "Downloading model... Please wait" : isCivitai ? "Download from Civitai" : "Download and install model"}
                          >
                            {installBusy === row.id && (
                              <span className="absolute inset-0 bg-gradient-to-r from-blue-600 via-blue-500 to-blue-600 animate-shimmer" style={{
                                backgroundSize: '200% 100%',
                                animation: 'shimmer 2s infinite linear'
                              }} />
                            )}
                            <span className="relative z-10 flex items-center gap-2.5">
                              {installBusy === row.id ? (
                                <>
                                  <svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                                    <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                                    <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                                  </svg>
                                  <span>Downloading...</span>
                                </>
                              ) : (
                                <>
                                  <Download size={15} strokeWidth={2.5} />
                                  <span>Install</span>
                                </>
                              )}
                            </span>
                          </button>
                        ) : null}

                        {canDelete ? (
                          deleteConfirm === row.id ? (
                            <div className="flex items-center gap-2">
                              <button
                                type="button"
                                className="px-4 py-2.5 rounded-xl bg-red-600 hover:bg-red-500 text-xs font-bold text-white flex items-center gap-2 transition-all hover:scale-105 active:scale-95"
                                disabled={deleteBusy === row.id}
                                onClick={() => void tryDelete(row.id)}
                              >
                                {deleteBusy === row.id ? (
                                  <>
                                    <svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                                      <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                                      <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                                    </svg>
                                    <span>Deleting...</span>
                                  </>
                                ) : (
                                  <span>Confirm</span>
                                )}
                              </button>
                              <button
                                type="button"
                                className="px-3 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-xs font-bold transition-all"
                                onClick={() => setDeleteConfirm(null)}
                              >
                                Cancel
                              </button>
                            </div>
                          ) : (
                            <button
                              type="button"
                              className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-red-600/20 border border-white/10 hover:border-red-500/30 text-xs font-bold flex items-center gap-2 transition-all hover:scale-105 active:scale-95 text-white/60 hover:text-red-400"
                              onClick={() => setDeleteConfirm(row.id)}
                              title="Delete this model from disk"
                            >
                              <Trash2 size={14} />
                              <span>Delete</span>
                            </button>
                          )
                        ) : null}
                      </div>
                    </div>
                  )
                })
              )}
            </div>
          </div>

          {/* Avatar & Identity Section (Add-ons tab only) */}
          {modelType === 'addons' && (
            <div className="rounded-2xl border border-purple-500/20 overflow-hidden bg-gradient-to-b from-purple-500/[0.03] to-transparent">
              <div className="bg-purple-500/5 px-6 py-4 flex items-center justify-between border-b border-purple-500/20">
                <div className="flex items-center gap-3">
                  <div className="text-xs font-bold text-purple-200 uppercase tracking-wider">Avatar & Identity Models</div>
                  <div className="px-2.5 py-1 rounded-md bg-purple-500/10 border border-purple-500/20 text-[10px] font-bold text-purple-300 uppercase tracking-wider">
                    Persona
                  </div>
                </div>
                <div className="flex items-center gap-3">
                  {avatarModelsLoading ? (
                    <div className="text-xs text-purple-300/50 font-semibold">Loading...</div>
                  ) : (
                    <div className="text-xs text-purple-300/50 font-semibold">
                      {avatarModels.filter(m => m.installed).length} / {avatarModels.length} Installed
                    </div>
                  )}
                  <button
                    type="button"
                    onClick={() => refreshAvatarModels()}
                    disabled={avatarModelsLoading}
                    className="p-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-all disabled:opacity-50"
                    title="Refresh avatar models"
                  >
                    <RefreshCw size={13} className={avatarModelsLoading ? 'animate-spin' : ''} />
                  </button>
                </div>
              </div>

              {avatarModelsError ? (
                <div className="p-5 text-xs text-purple-300/60">
                  Could not load avatar models: {avatarModelsError}
                </div>
              ) : (
                <>
                  {/* Feature readiness dashboard */}
                  {Object.keys(avatarFeatures).length > 0 && (
                    <div className="px-6 py-4 border-b border-purple-500/10">
                      <div className="text-[10px] text-white/40 font-bold uppercase tracking-wider mb-3">Feature Readiness</div>
                      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
                        {Object.entries(avatarFeatures).map(([featId, feat]) => (
                          <div
                            key={featId}
                            className={`rounded-xl border p-3 transition-all ${
                              feat.ready
                                ? 'border-emerald-500/20 bg-emerald-500/5'
                                : 'border-white/10 bg-white/[0.02]'
                            }`}
                          >
                            <div className="flex items-center gap-2 mb-1.5">
                              {feat.ready ? (
                                <CheckCircle2 size={13} className="text-emerald-400 flex-shrink-0" />
                              ) : (
                                <XCircle size={13} className="text-white/30 flex-shrink-0" />
                              )}
                              <div className={`text-[11px] font-bold ${feat.ready ? 'text-emerald-200' : 'text-white/50'}`}>
                                {feat.label}
                              </div>
                            </div>
                            <div className="text-[10px] text-white/30 leading-relaxed">
                              {feat.description}
                            </div>
                            {!feat.ready && feat.required_missing.length > 0 && (
                              <div className="mt-2 text-[10px] text-amber-400/70">
                                Missing: {feat.required_missing.join(', ')}
                              </div>
                            )}
                            {feat.ready && !feat.recommended_installed && feat.recommended_note && (
                              <div className="mt-2 text-[10px] text-blue-300/60">
                                Tip: {feat.recommended_note}
                              </div>
                            )}
                          </div>
                        ))}
                      </div>
                      <div className="mt-3 text-[10px] text-white/20">
                        Existing personas and avatars work without any of these models. These enable optional enhanced features only.
                      </div>
                    </div>
                  )}

                  {/* Quick-install preset buttons */}
                  <div className="px-6 py-4 border-b border-purple-500/10 flex flex-wrap items-center gap-3">
                    <span className="text-[10px] text-white/40 font-bold uppercase tracking-wider mr-1">Quick Install:</span>
                    <button
                      type="button"
                      onClick={() => downloadAvatarPreset('basic')}
                      disabled={avatarDownloadBusy !== null}
                      className={[
                        "px-5 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wide flex items-center gap-2.5 transition-all shadow-lg relative overflow-hidden",
                        avatarDownloadBusy === 'basic'
                          ? "bg-purple-700 text-white cursor-wait shadow-purple-700/30"
                          : "bg-purple-600 text-white hover:bg-purple-500 shadow-purple-600/20 hover:shadow-purple-600/30 hover:scale-105 active:scale-95"
                      ].join(" ")}
                    >
                      {avatarDownloadBusy === 'basic' ? (
                        <>
                          <svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                            <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                            <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                          </svg>
                          <span>Downloading...</span>
                        </>
                      ) : (
                        <>
                          <Download size={14} strokeWidth={2.5} />
                          <span>Basic Pack (~4.3 GB)</span>
                        </>
                      )}
                    </button>
                    <button
                      type="button"
                      onClick={() => downloadAvatarPreset('full')}
                      disabled={avatarDownloadBusy !== null}
                      className={[
                        "px-5 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wide flex items-center gap-2.5 transition-all shadow-lg relative overflow-hidden",
                        avatarDownloadBusy === 'full'
                          ? "bg-purple-700 text-white cursor-wait shadow-purple-700/30"
                          : "bg-white/5 text-white/70 hover:bg-white/10 border border-white/10 hover:border-purple-500/30 hover:text-white shadow-none hover:shadow-purple-600/10 hover:scale-105 active:scale-95"
                      ].join(" ")}
                    >
                      {avatarDownloadBusy === 'full' ? (
                        <>
                          <svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                            <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                            <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                          </svg>
                          <span>Downloading...</span>
                        </>
                      ) : (
                        <>
                          <Download size={14} strokeWidth={2.5} />
                          <span>Full Pack (~9.5 GB)</span>
                        </>
                      )}
                    </button>
                    <span className="text-[10px] text-white/25 ml-2">
                      Basic = InsightFace + InstantID (portraits + outfits, ~4.3 GB). Full = all 9 models (~9.5 GB, + face swap, random faces).
                    </span>
                  </div>

                  {/* Download progress panel (visible during active download) */}
                  {avatarDownloadStatus && (avatarDownloadStatus.running || avatarDownloadStatus.finished) && (
                    <div className="px-6 py-4 border-b border-purple-500/10 bg-gradient-to-r from-purple-500/[0.03] to-transparent">
                      <div className="flex items-center justify-between mb-3">
                        <div className="flex items-center gap-2.5">
                          {avatarDownloadStatus.running && (
                            <svg className="animate-spin h-4 w-4 text-purple-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                            </svg>
                          )}
                          {avatarDownloadStatus.finished && (
                            <CheckCircle2 size={16} className="text-emerald-400" />
                          )}
                          <span className="text-xs font-bold text-white">
                            {avatarDownloadStatus.finished
                              ? `Download Complete — ${avatarDownloadStatus.installed_count}/${avatarDownloadStatus.total_models} installed`
                              : `Downloading ${avatarDownloadStatus.preset?.toUpperCase()} preset — ${avatarDownloadStatus.current_index}/${avatarDownloadStatus.total_models}`
                            }
                          </span>
                        </div>
                        <div className="text-[10px] text-white/40 font-mono">
                          {Math.round(avatarDownloadStatus.elapsed)}s
                          {avatarDownloadStatus.downloaded_bytes > 0 && (
                            <> | {formatBytes(avatarDownloadStatus.downloaded_bytes)}</>
                          )}
                        </div>
                      </div>

                      {/* Progress bar */}
                      {avatarDownloadStatus.total_models > 0 && (
                        <div className="h-2 bg-white/10 rounded-full overflow-hidden mb-3">
                          <div
                            className={`h-full rounded-full transition-all duration-700 ${
                              avatarDownloadStatus.finished ? 'bg-emerald-500' : 'bg-purple-500'
                            }`}
                            style={{
                              width: `${Math.round(
                                ((avatarDownloadStatus.results?.length || 0) / avatarDownloadStatus.total_models) * 100
                              )}%`,
                            }}
                          />
                        </div>
                      )}

                      {/* Per-model status rows */}
                      <div className="space-y-1">
                        {(avatarDownloadStatus.results || []).map((r) => (
                          <div key={r.id} className="flex items-center gap-2 text-[11px]">
                            {r.status === 'installed' ? (
                              <CheckCircle2 size={11} className="text-emerald-400 shrink-0" />
                            ) : r.status === 'already_installed' ? (
                              <CheckCircle2 size={11} className="text-white/30 shrink-0" />
                            ) : (
                              <XCircle size={11} className="text-red-400 shrink-0" />
                            )}
                            <span className={r.status === 'already_installed' ? 'text-white/30' : 'text-white/60'}>
                              {r.name || r.id}
                            </span>
                            <span className="text-white/20 ml-auto">
                              {r.status === 'installed' && r.size ? formatBytes(r.size) : ''}
                              {r.status === 'installed' && r.elapsed ? ` (${r.elapsed}s)` : ''}
                              {r.status === 'already_installed' ? 'already installed' : ''}
                              {r.status === 'failed' ? 'failed' : ''}
                              {r.status === 'timeout' ? 'timeout' : ''}
                              {r.status === 'error' ? r.error || 'error' : ''}
                            </span>
                          </div>
                        ))}

                        {/* Currently downloading indicator */}
                        {avatarDownloadStatus.running && avatarDownloadStatus.current_model && (
                          <div className="flex items-center gap-2 text-[11px]">
                            <svg className="animate-spin h-3 w-3 text-purple-400 shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                            </svg>
                            <span className="text-purple-300">
                              {avatarModels.find(m => m.id === avatarDownloadStatus.current_model)?.name || avatarDownloadStatus.current_model}
                            </span>
                            <span className="text-white/20 ml-auto">downloading...</span>
                          </div>
                        )}
                      </div>

                      {/* Dismiss when finished */}
                      {avatarDownloadStatus.finished && (
                        <button
                          type="button"
                          onClick={() => setAvatarDownloadStatus(null)}
                          className="mt-3 text-[10px] text-white/30 hover:text-white/50 transition-colors"
                        >
                          Dismiss
                        </button>
                      )}
                    </div>
                  )}

                  {/* Model list */}
                  <div className="divide-y divide-purple-500/10">
                    {avatarModels.map((m) => {
                      const statusKind = m.installed ? 'ok' : 'warn'
                      const statusLabel = m.installed ? 'Installed' : 'Not Installed'

                      // Basic pack models are tagged as "Recommended" — minimum needed for Avatar to work
                      const BASIC_PACK_IDS = new Set(['insightface-antelopev2', 'instantid-ip-adapter', 'instantid-controlnet'])
                      const isRecommended = BASIC_PACK_IDS.has(m.id)

                      // Feature labels from used_by
                      const FEATURE_LABELS: Record<string, { label: string; color: string }> = {
                        photo_variations: { label: 'Portraits', color: 'purple' },
                        outfit_generation: { label: 'Outfits', color: 'indigo' },
                        face_swap: { label: 'Face Swap', color: 'cyan' },
                        random_faces: { label: 'Random Faces', color: 'pink' },
                      }

                      return (
                        <div key={m.id} className="p-5 flex items-center justify-between gap-4 hover:bg-white/[0.02] transition-all group">
                          <div className="min-w-0 flex-1">
                            <div className="flex items-center gap-2.5 mb-2 flex-wrap">
                              <div className={`px-3 py-1.5 rounded-lg border text-[10px] font-bold uppercase tracking-wider flex items-center gap-1.5 ${chipClass(statusKind)}`}>
                                <IconStatus kind={statusKind} />
                                <span>{statusLabel}</span>
                              </div>
                              {m.is_default && (
                                <div className="px-3 py-1.5 rounded-lg border border-blue-500/30 bg-blue-500/10 text-[10px] font-bold uppercase tracking-wider text-blue-200">
                                  Core
                                </div>
                              )}
                              {isRecommended && (
                                <div className="px-3 py-1.5 rounded-lg border border-purple-500/30 bg-purple-500/10 text-[10px] font-bold uppercase tracking-wider text-purple-200">
                                  Recommended
                                </div>
                              )}
                              {m.license && (
                                <div className="px-2.5 py-1 rounded-lg border border-white/10 bg-white/5 text-[10px] font-medium text-white/40 flex items-center gap-1">
                                  <Shield size={10} />
                                  {m.license}
                                </div>
                              )}
                              {m.commercial_use_ok === true && (
                                <div className="px-2.5 py-1 rounded-lg border border-emerald-500/20 bg-emerald-500/5 text-[10px] font-medium text-emerald-300/70">
                                  Commercial OK
                                </div>
                              )}
                              {m.commercial_use_ok === false && (
                                <div className="px-2.5 py-1 rounded-lg border border-amber-500/20 bg-amber-500/5 text-[10px] font-medium text-amber-300/70">
                                  Non-Commercial
                                </div>
                              )}
                            </div>

                            <div className="font-bold text-base text-white truncate group-hover:text-white transition-colors">{m.name}</div>
                            <div className="text-[11px] text-white/40 font-mono truncate mt-1">{m.id}</div>
                            {m.description && (
                              <div className="text-[11px] text-white/30 mt-1">{m.description}</div>
                            )}
                            {m.requires && m.requires.length > 0 && (
                              <div className="text-[11px] text-amber-400/70 mt-1 truncate">
                                Requires: <span className="font-semibold">{m.requires.join(', ')}</span>
                              </div>
                            )}

                            {/* Feature tags: which features this model enables */}
                            {m.used_by && m.used_by.length > 0 && (
                              <div className="flex items-center gap-1.5 mt-2 flex-wrap">
                                <span className="text-[10px] text-white/25">Enables:</span>
                                {m.used_by.map((ub) => {
                                  const fl = FEATURE_LABELS[ub.feature]
                                  if (!fl) return null
                                  const isRequired = ub.role === 'required'
                                  return (
                                    <span
                                      key={`${ub.feature}-${ub.role}`}
                                      className={`px-2 py-0.5 rounded text-[9px] font-semibold uppercase tracking-wider border ${
                                        isRequired
                                          ? `border-${fl.color}-500/30 bg-${fl.color}-500/10 text-${fl.color}-300`
                                          : 'border-white/10 bg-white/5 text-white/40'
                                      }`}
                                      title={isRequired ? `Required for ${fl.label}` : `Recommended for ${fl.label}`}
                                    >
                                      {fl.label}{!isRequired ? ' (opt)' : ''}
                                    </span>
                                  )
                                })}
                              </div>
                            )}
                          </div>

                          <div className="flex items-center gap-3 flex-shrink-0">
                            {m.homepage && (
                              <a
                                href={m.homepage}
                                target="_blank"
                                rel="noopener noreferrer"
                                className="px-3 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-xs font-bold flex items-center gap-2 transition-all text-white/50 hover:text-white/70"
                                title="View project page"
                              >
                                <ExternalLink size={13} />
                                <span>Info</span>
                              </a>
                            )}

                            {/* Install button — only for not-installed models with a download URL */}
                            {!m.installed && m.download_url && (
                              <button
                                type="button"
                                className="px-4 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-xs font-bold text-white flex items-center gap-2 transition-all hover:scale-105 active:scale-95 shadow-lg shadow-purple-600/20 disabled:opacity-50 disabled:cursor-wait"
                                disabled={avatarInstallBusy === m.id || avatarDownloadBusy !== null}
                                onClick={() => installSingleAvatarModel(m.id)}
                                title={`Download and install ${m.name}`}
                              >
                                {avatarInstallBusy === m.id ? (
                                  <>
                                    <svg className="animate-spin h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                                      <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                                      <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                                    </svg>
                                    <span>Installing...</span>
                                  </>
                                ) : (
                                  <>
                                    <Download size={13} />
                                    <span>Install</span>
                                  </>
                                )}
                              </button>
                            )}

                            {/* Uninstall button — only for installed models */}
                            {m.installed && (
                              avatarDeleteConfirm === m.id ? (
                                <div className="flex items-center gap-2">
                                  <button
                                    type="button"
                                    className="px-4 py-2.5 rounded-xl bg-red-600 hover:bg-red-500 text-xs font-bold text-white flex items-center gap-2 transition-all hover:scale-105 active:scale-95"
                                    disabled={avatarDeleteBusy === m.id}
                                    onClick={() => deleteAvatarModel(m.id)}
                                  >
                                    {avatarDeleteBusy === m.id ? (
                                      <>
                                        <svg className="animate-spin h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                                          <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                                          <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                                        </svg>
                                        <span>Deleting...</span>
                                      </>
                                    ) : (
                                      <span>Confirm</span>
                                    )}
                                  </button>
                                  <button
                                    type="button"
                                    className="px-3 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-xs font-bold transition-all"
                                    onClick={() => setAvatarDeleteConfirm(null)}
                                  >
                                    Cancel
                                  </button>
                                </div>
                              ) : (
                                <button
                                  type="button"
                                  className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-red-600/20 border border-white/10 hover:border-red-500/30 text-xs font-bold flex items-center gap-2 transition-all hover:scale-105 active:scale-95 text-white/60 hover:text-red-400"
                                  onClick={() => setAvatarDeleteConfirm(m.id)}
                                  title="Uninstall this model to free disk space"
                                >
                                  <Trash2 size={13} />
                                  <span>Uninstall</span>
                                </button>
                              )
                            )}
                          </div>
                        </div>
                      )
                    })}

                    {avatarModels.length === 0 && !avatarModelsLoading && (
                      <div className="p-8 text-center text-white/40 text-sm">
                        No avatar models registered. Check backend configuration.
                      </div>
                    )}
                  </div>
                </>
              )}
            </div>
          )}

          {/* LoRA Models Section (LoRA tab only — additive, Golden Rule 1.0) */}
          {modelType === 'lora' && (
            <div className="rounded-2xl border border-cyan-500/20 overflow-hidden bg-gradient-to-b from-cyan-500/[0.03] to-transparent">
              <div className="bg-cyan-500/5 px-6 py-4 flex items-center justify-between border-b border-cyan-500/20">
                <div className="flex items-center gap-3">
                  <div className="text-xs font-bold text-cyan-200 uppercase tracking-wider">LoRA Models</div>
                  <div className="px-2.5 py-1 rounded-md bg-cyan-500/10 border border-cyan-500/20 text-[10px] font-bold text-cyan-300 uppercase tracking-wider">
                    Lightweight Adapters
                  </div>
                </div>
                <div className="flex items-center gap-3">
                  {loraLoading ? (
                    <div className="text-xs text-cyan-300/50 font-semibold">Loading...</div>
                  ) : (
                    <div className="text-xs text-cyan-300/50 font-semibold">
                      {loraInstalled.length} Installed / {loraRegistry.length} Available
                    </div>
                  )}
                  <button
                    type="button"
                    onClick={() => refreshLoraModels()}
                    disabled={loraLoading}
                    className="p-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-all disabled:opacity-50"
                    title="Refresh LoRA models"
                  >
                    <RefreshCw size={13} className={loraLoading ? 'animate-spin' : ''} />
                  </button>
                </div>
              </div>

              {loraError ? (
                <div className="p-5 text-xs text-cyan-300/60">
                  Could not load LoRA models: {loraError}
                </div>
              ) : (
                <>
                  {/* Info banner */}
                  <div className="px-6 py-4 border-b border-cyan-500/10">
                    <div className="text-[11px] text-white/40 leading-relaxed">
                      LoRA (Low-Rank Adaptation) models are lightweight adapters that modify checkpoint behavior without replacing the base model.
                      Perfect for GPUs with less than 12 GB VRAM. Click Install to add a LoRA to your <span className="font-mono bg-white/10 px-1.5 py-0.5 rounded text-white/60">models/comfy/loras/</span> directory.
                    </div>
                    {!props.nsfwMode && (
                      <div className="mt-2 text-[10px] text-white/25 flex items-center gap-1.5">
                        <Shield size={10} />
                        Some models are hidden. Enable Spice Mode in Settings to see all available LoRAs.
                      </div>
                    )}
                  </div>

                  {/* Install progress (mirrors Add-ons progress bar) */}
                  {loraDownloadStatus && (loraDownloadStatus.running || loraDownloadStatus.finished) && (
                    <div className="px-6 py-4 border-b border-cyan-500/10">
                      <div className="flex items-center gap-3 mb-2">
                        {loraDownloadStatus.running && (
                          <svg className="animate-spin h-4 w-4 text-cyan-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                            <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                            <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                          </svg>
                        )}
                        {loraDownloadStatus.finished && <CheckCircle2 size={16} className="text-emerald-400" />}
                        <span className="text-xs font-bold text-white/70">
                          {loraDownloadStatus.finished
                            ? `Install Complete — ${loraDownloadStatus.installed_count}/${loraDownloadStatus.total_models} installed`
                            : `Installing LoRA — ${loraDownloadStatus.current_index}/${loraDownloadStatus.total_models}`
                          }
                        </span>
                        <span className="text-[10px] text-white/30 ml-auto">
                          {Math.round(loraDownloadStatus.elapsed)}s
                          {loraDownloadStatus.downloaded_bytes > 0 && (
                            <> | {formatBytes(loraDownloadStatus.downloaded_bytes)}</>
                          )}
                        </span>
                      </div>
                      {loraDownloadStatus.total_models > 0 && (
                        <div className="w-full h-1.5 bg-white/10 rounded-full overflow-hidden">
                          <div
                            className={`h-full rounded-full transition-all duration-500 ${
                              loraDownloadStatus.finished ? 'bg-emerald-500' : 'bg-cyan-500'
                            }`}
                            style={{
                              width: `${loraDownloadStatus.finished ? 100 :
                                ((loraDownloadStatus.results?.length || 0) / loraDownloadStatus.total_models) * 100
                              }%`,
                            }}
                          />
                        </div>
                      )}
                      {loraDownloadStatus.running && loraDownloadStatus.current_model && (
                        <div className="mt-2 flex items-center gap-2">
                          <div className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse" />
                          <span className="text-[10px] text-white/40">
                            Installing: {loraRegistry.find((l: any) => l.id === loraDownloadStatus.current_model)?.name || loraDownloadStatus.current_model}
                          </span>
                        </div>
                      )}
                      {loraDownloadStatus.finished && (
                        <button
                          type="button"
                          onClick={() => setLoraDownloadStatus(null)}
                          className="mt-3 text-[10px] text-white/30 hover:text-white/50 transition-colors"
                        >
                          Dismiss
                        </button>
                      )}
                    </div>
                  )}

                  {/* Installed LoRAs */}
                  {loraInstalled.length > 0 && (
                    <div className="px-6 py-4 border-b border-cyan-500/10">
                      <div className="text-[10px] text-white/40 font-bold uppercase tracking-wider mb-3">Installed LoRAs</div>
                      <div className="flex flex-wrap gap-2">
                        {loraInstalled.map((l: any) => (
                          <div
                            key={l.id}
                            className="px-3 py-2 rounded-xl border border-emerald-500/20 bg-emerald-500/5 text-[11px] font-semibold text-emerald-200 flex items-center gap-2"
                          >
                            <CheckCircle2 size={12} className="text-emerald-400" />
                            {l.id}
                          </div>
                        ))}
                      </div>
                    </div>
                  )}

                  {/* Registry LoRAs */}
                  <div className="divide-y divide-white/5">
                    {loraRegistry.map((lora: any) => {
                      const isInstalled = loraInstalled.some((i: any) => i.id === lora.id)
                      return (
                        <div key={lora.id} className="p-5 flex items-center justify-between gap-4 hover:bg-white/[0.02] transition-all group">
                          <div className="min-w-0 flex-1">
                            <div className="flex items-center gap-2.5 mb-2 flex-wrap">
                              <div className={`px-3 py-1.5 rounded-lg border text-[10px] font-bold uppercase tracking-wider flex items-center gap-1.5 ${
                                isInstalled
                                  ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200'
                                  : 'border-white/10 bg-white/5 text-white/40'
                              }`}>
                                {isInstalled ? (
                                  <><CheckCircle2 size={10} /> Installed</>
                                ) : (
                                  <><Download size={10} /> Available</>
                                )}
                              </div>
                              <div className="px-2.5 py-1 rounded-lg border border-white/10 bg-white/5 text-[10px] font-medium text-white/40">
                                {lora.base}
                              </div>
                              {lora.recommended ? (
                                <div className="px-3 py-1.5 rounded-lg border border-blue-500/30 bg-blue-500/10 text-[10px] font-bold uppercase tracking-wider text-blue-200">
                                  ⭐ Recommended
                                </div>
                              ) : null}
                              {lora.recommended_nsfw && props.nsfwMode ? (
                                <div className="px-3 py-1.5 rounded-lg border border-pink-500/30 bg-pink-500/10 text-[10px] font-bold uppercase tracking-wider text-pink-200">
                                  🔥 NSFW Pick
                                </div>
                              ) : null}
                              {lora.gated ? (
                                <div className="px-2.5 py-1 rounded-lg border border-red-500/30 bg-red-500/10 text-[10px] font-medium text-red-200">
                                  🌶️ Adult
                                </div>
                              ) : null}
                              {lora.size_mb > 0 && (
                                <div className="text-[10px] text-white/25">{lora.size_mb} MB</div>
                              )}
                            </div>
                            <div className="font-bold text-base text-white truncate group-hover:text-white transition-colors">{lora.name}</div>
                            <div className="text-[11px] text-white/40 font-mono truncate mt-1">{lora.filename}</div>
                            {lora.description && (
                              <div className="text-[11px] text-white/30 mt-1">{lora.description}</div>
                            )}
                            {lora.trigger_words && lora.trigger_words.length > 0 && (
                              <div className="flex items-center gap-1.5 mt-2 flex-wrap">
                                <span className="text-[10px] text-white/25">Trigger words:</span>
                                {lora.trigger_words.map((tw: string) => (
                                  <span
                                    key={tw}
                                    className="px-2 py-0.5 rounded text-[9px] font-semibold uppercase tracking-wider border border-cyan-500/20 bg-cyan-500/5 text-cyan-300/70"
                                  >
                                    {tw}
                                  </span>
                                ))}
                              </div>
                            )}
                          </div>

                          <div className="flex items-center gap-2 shrink-0">
                            {lora.model_url && (
                              <a
                                href={lora.model_url}
                                target="_blank"
                                rel="noopener noreferrer"
                                className="px-3 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-xs font-bold flex items-center gap-2 transition-all text-white/50 hover:text-white/70"
                                title="View model page"
                              >
                                <ExternalLink size={13} />
                                <span>View</span>
                              </a>
                            )}

                            {/* Install button — not installed, has download URL */}
                            {!isInstalled && lora.download_url && (
                              <button
                                type="button"
                                className="px-4 py-2.5 rounded-xl bg-cyan-600 hover:bg-cyan-500 text-xs font-bold text-white flex items-center gap-2 transition-all hover:scale-105 active:scale-95 shadow-lg shadow-cyan-600/20 disabled:opacity-50 disabled:cursor-wait"
                                disabled={loraInstallBusy === lora.id || loraDownloadBusy !== null}
                                onClick={() => installLora(lora.id)}
                                title={`Install ${lora.name}`}
                              >
                                {loraInstallBusy === lora.id ? (
                                  <>
                                    <svg className="animate-spin h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                                      <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                                      <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                                    </svg>
                                    <span>Installing...</span>
                                  </>
                                ) : (
                                  <>
                                    <Download size={13} />
                                    <span>Install</span>
                                  </>
                                )}
                              </button>
                            )}

                            {/* Delete button — installed models */}
                            {isInstalled && (
                              loraDeleteConfirm === lora.id ? (
                                <div className="flex items-center gap-2">
                                  <button
                                    type="button"
                                    className="px-4 py-2.5 rounded-xl bg-red-600 hover:bg-red-500 text-xs font-bold text-white flex items-center gap-2 transition-all hover:scale-105 active:scale-95"
                                    disabled={loraDeleteBusy === lora.id}
                                    onClick={() => deleteLora(lora.id)}
                                  >
                                    {loraDeleteBusy === lora.id ? (
                                      <>
                                        <svg className="animate-spin h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                                          <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                                          <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                                        </svg>
                                        <span>Deleting...</span>
                                      </>
                                    ) : (
                                      <span>Confirm</span>
                                    )}
                                  </button>
                                  <button
                                    type="button"
                                    className="px-3 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-xs font-bold transition-all"
                                    onClick={() => setLoraDeleteConfirm(null)}
                                  >
                                    Cancel
                                  </button>
                                </div>
                              ) : (
                                <button
                                  type="button"
                                  className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-red-600/20 border border-white/10 hover:border-red-500/30 text-xs font-bold flex items-center gap-2 transition-all hover:scale-105 active:scale-95 text-white/60 hover:text-red-400"
                                  onClick={() => setLoraDeleteConfirm(lora.id)}
                                  title="Delete this LoRA to free disk space"
                                >
                                  <Trash2 size={13} />
                                  <span>Delete</span>
                                </button>
                              )
                            )}
                          </div>
                        </div>
                      )
                    })}
                    {loraRegistry.length === 0 && !loraLoading && (
                      <div className="p-8 text-center text-white/40 text-sm">
                        No LoRA models available. Check backend configuration.
                      </div>
                    )}
                  </div>
                </>
              )}
            </div>
          )}
        </div>
      </div>

      {/* Toast notification */}
      {toast ? (
        <div className="fixed bottom-8 right-8 z-50 bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-xl border border-white/20 rounded-2xl px-6 py-4 text-sm text-white shadow-2xl shadow-black/40 animate-in slide-in-from-bottom-4 flex items-center gap-3 min-w-[320px]">
          {toast.includes('Successfully') || toast.includes('installed') ? (
            <div className="flex-shrink-0">
              <CheckCircle2 size={18} className="text-emerald-400" />
            </div>
          ) : toast.includes('failed') || toast.includes('error') || toast.includes('Error') ? (
            <div className="flex-shrink-0">
              <XCircle size={18} className="text-red-400" />
            </div>
          ) : (
            <div className="w-2 h-2 rounded-full bg-blue-400 animate-pulse flex-shrink-0" />
          )}
          <div className="flex-1 min-w-0">
            <div className="font-semibold text-white/90 truncate">{toast}</div>
            {installBusy && (
              <div className="text-[10px] text-white/50 mt-1 font-medium">
                Large models may take several minutes...
              </div>
            )}
          </div>
        </div>
      ) : null}

      <style>{`
        .scrollbar-hide::-webkit-scrollbar { display: none; }
        .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }

        @keyframes shimmer {
          0% { background-position: -200% 0; }
          100% { background-position: 200% 0; }
        }

        .animate-shimmer {
          animation: shimmer 2s infinite linear;
        }
      `}</style>
    </div>
  )
}
