// ── AI Backends section: copy, API keys, local service connect ─────────────

(function initLlmsSection() {

  // Copy buttons
  document.querySelectorAll('.llm-copy-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const text = btn.dataset.copy || '';
      const orig = btn.textContent;
      const done = () => { btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = orig; }, 1500); };
      if (navigator.clipboard) {
        navigator.clipboard.writeText(text).then(done).catch(done);
      } else {
        const ta = document.createElement('textarea');
        ta.value = text; ta.style.cssText = 'position:fixed;opacity:0';
        document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove();
        done();
      }
    });
  });

  // API key inputs — persist to localStorage, eye toggle, saved badge
  document.querySelectorAll('.llm-input[data-llm-key]').forEach(inp => {
    const key   = inp.dataset.llmKey;
    const saved = localStorage.getItem('llm-key-' + key);
    if (saved) inp.value = saved;

    const eye = document.createElement('button');
    eye.type = 'button'; eye.className = 'llm-eye-btn'; eye.title = 'Show / hide';
    eye.textContent = '👁';
    inp.insertAdjacentElement('afterend', eye);

    const badge = document.createElement('span');
    badge.className = 'llm-saved-badge'; badge.textContent = 'Saved'; badge.hidden = true;
    eye.insertAdjacentElement('afterend', badge);

    eye.addEventListener('click', () => {
      inp.type = inp.type === 'password' ? 'text' : 'password';
      eye.classList.toggle('active', inp.type === 'text');
    });

    let t;
    inp.addEventListener('input', () => {
      clearTimeout(t);
      t = setTimeout(() => {
        if (inp.value) localStorage.setItem('llm-key-' + key, inp.value);
        else localStorage.removeItem('llm-key-' + key);
        badge.hidden = false;
        setTimeout(() => { badge.hidden = true; }, 1800);
      }, 600);
    });
  });

  // Local service URL inputs + Connect / Disconnect
  function normalizeProbeUrl(raw) {
    // 0.0.0.0 is a bind address, not routable; from inside Docker use host.docker.internal
    return raw.replace(/^(https?:\/\/)0\.0\.0\.0([\/:])/, '$1host.docker.internal$2');
  }

  async function probeUrl(rawUrl, type = '', apiKey = '') {
    const url = normalizeProbeUrl(rawUrl);
    const params = { url };
    if (type) params.type = type;
    if (apiKey) params.api_key = apiKey;
    const r = await fetch('/api/probe-url?' + new URLSearchParams(params));
    return r.json();
  }

  function applyCardState(card, key, connected, failed) {
    card.classList.toggle('llm-local-card-online',  connected);
    card.classList.toggle('llm-local-card-offline', !connected && !!failed);
    localStorage.setItem('llm-local-con-' + key, connected ? '1' : '0');
    const btn = card.querySelector('.llm-local-ping');
    if (!btn) return;
    if (connected) {
      btn.innerHTML = '<span class="mdi mdi-check"></span> Disconnect'; btn.dataset.action = 'disconnect';
      btn.className = 'llm-local-ping ok';
    } else {
      btn.textContent = 'Connect'; btn.dataset.action = 'connect';
      btn.className = 'llm-local-ping';
    }
  }

  document.querySelectorAll('[data-llm-local-key]').forEach(inp => {
    const key  = inp.dataset.llmLocalKey;
    const card = inp.closest('.llm-local-card');
    if (!card) return;
    const btn  = card.querySelector('.llm-local-ping');

    const savedUrl = (_appSettings && _appSettings.engine_local_urls && _appSettings.engine_local_urls[key]) || localStorage.getItem('llm-local-url-' + key);
    if (savedUrl) inp.value = savedUrl;
    inp.addEventListener('input', () => { localStorage.setItem('llm-local-url-' + key, inp.value); _saveEngineLocalUrls(); });

    if (localStorage.getItem('llm-local-con-' + key) === '1') applyCardState(card, key, true, false);

    if (!btn) return;
    btn.addEventListener('click', async () => {
      const action = btn.dataset.action || 'connect';
      if (action === 'disconnect') { applyCardState(card, key, false, false); return; }

      const rawUrl = inp.value.trim() || inp.placeholder;
      if (!rawUrl) return;
      btn.disabled = true;
      btn.textContent = 'Connecting…';
      try {
        const type = cardType(card);
        const d = await probeUrl(rawUrl, type);
        applyCardState(card, key, d.ok, !d.ok);
        if (d.ok) {
          toast(`✓ ${type.toUpperCase() || 'Service'} reachable — ${d.endpoint}`, 'success');
        } else {
          toast('Cannot reach ' + normalizeProbeUrl(rawUrl) + ': ' + (d.error || 'No response'), 'error');
        }
      } catch (e) {
        applyCardState(card, key, false, true);
        toast('Probe failed: ' + e.message, 'error');
      } finally {
        btn.disabled = false;
      }
    });
  });

  // ── "Use as STT / TTS" quick-apply buttons in AI Backends section ──────────
  async function applyAndSaveSettings(patch) {
    try {
      const resp = await fetch('/api/settings').then(r => r.json());
      const updated = { ...resp, ...patch };
      await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch) });
      Object.assign(_appSettings || {}, patch);
      // refresh visible inputs in Settings if open
      for (const [id, val] of Object.entries(patch)) {
        const inp = $('s-' + id.replace(/_/g, '-'));
        if (inp && inp.value !== undefined) inp.value = val;
      }
      await refreshTtsBackendAvailability();
      await refreshSttBackends();
    } catch (e) { toast('Apply failed: ' + e.message, 'error'); }
  }
  window.applyAndSaveSettings = applyAndSaveSettings;
  window.probeUrl = probeUrl;

  $('llm-use-faster-whisper-stt')?.addEventListener('click', () => {
    const url = document.querySelector('[data-llm-local-key="faster-whisper"]')?.value.trim()
      || 'http://host.docker.internal:8000';
    applyAndSaveSettings({ faster_whisper_url: url });
    toast('faster-whisper-server URL saved → Settings. Use the "faster-whisper" engine in the STT dropdown.', 'success');
  });

  $('llm-use-whisper-cpp-stt')?.addEventListener('click', () => {
    const url = document.querySelector('[data-llm-local-key="whisper-cpp"]')?.value.trim()
      || 'http://host.docker.internal:8080';
    applyAndSaveSettings({ whisper_cpp_url: url });
    toast('whisper.cpp URL saved → Settings. Use the "whisper.cpp" engine in the STT dropdown.', 'success');
  });

  $('llm-use-kokoro-tts')?.addEventListener('click', () => {
    const url = document.querySelector('[data-llm-local-key="kokoro"]')?.value.trim()
      || 'http://host.docker.internal:8880/v1';
    applyAndSaveSettings({ kokoro_url: url });
    toast('Kokoro FastAPI URL saved → Settings. It now appears as "Kokoro FastAPI (82M)" in the TTS backend dropdown.', 'success');
  });

  $('llm-use-fishspeech-tts')?.addEventListener('click', () => {
    const url = document.querySelector('[data-llm-local-key="fishspeech"]')?.value.trim()
      || 'http://host.docker.internal:38080';
    applyAndSaveSettings({ fishspeech_url: url });
    toast('Fish-Speech URL saved → Settings. Pick "Fish-Speech (Clone + Emotion)" in Try It Out → Backend or the Rehearser.', 'success');
  });

  $('llm-use-vibevoice-tts')?.addEventListener('click', () => {
    const url = document.querySelector('[data-llm-local-key="vibevoice"]')?.value.trim()
      || 'http://192.168.178.8:8027';
    applyAndSaveSettings({ vibevoice_url: url });
    toast('VibeVoice URL saved → Settings. Pick "VibeVoice" in Try It Out → Backend or a routing rule.', 'success');
  });

  $('llm-use-piper-tts')?.addEventListener('click', () => {
    const url = document.querySelector('[data-llm-local-key="piper"]')?.value.trim()
      || 'localhost:10200';
    applyAndSaveSettings({ tts_url: url });
    toast('Piper URL saved → tts_url. Note: Piper uses Wyoming protocol — pair with an OpenAI-compatible wrapper for full support.', 'success');
  });

  $('llm-use-xtts-tts')?.addEventListener('click', () => {
    const url = document.querySelector('[data-llm-local-key="xtts"]')?.value.trim()
      || 'http://localhost:8024';
    applyAndSaveSettings({ xtts_url: url });
    toast('XTTS v2 URL saved → xtts_url. It now appears as "XTTS v2" in the TTS backend dropdown.', 'success');
  });

  const LLM_USE_MAP = {
    'llm-use-ollama-llm':   { key: 'ollama',   fallback: 'http://localhost:11434/v1' },
    'llm-use-vllm-llm':     { key: 'vllm',     fallback: 'http://localhost:8000/v1'  },
    'llm-use-lmstudio-llm': { key: 'lmstudio', fallback: 'http://localhost:1234/v1'  },
    'llm-use-llamacpp-llm': { key: 'llamacpp', fallback: 'http://localhost:8080/v1'  },
    'llm-use-llama-swap-llm': { key: 'llama-swap', fallback: 'http://host.docker.internal:28080/v1' },
    'llm-use-litellm-llm':  { key: 'litellm',  fallback: 'http://host.docker.internal:14000/v1' },
  };
  Object.entries(LLM_USE_MAP).forEach(([id, { key, fallback }]) => {
    $(id)?.addEventListener('click', () => {
      const url = document.querySelector(`[data-llm-local-key="${key}"]`)?.value.trim() || fallback;
      applyAndSaveSettings({ llm_url: url });
      // Keep the global Active-LLM panel in sync with the card the user applied
      window.syncActiveLlmPanel?.(url);
      toast(`Active LLM endpoint set to ${url}. Pick a model in "Active Language Model" above.`, 'success');
    });
  });

  // ── Active Language Model (global picker) ───────────────────────────────
  (function initActiveLlmPanel() {
    const urlInp     = $('llm-active-url');
    const modelSel   = $('llm-active-model');
    const refreshBtn = $('llm-active-refresh');
    const statusEl   = $('llm-active-status');
    if (!urlInp || !modelSel) return;

    function setStatus(msg, cls) {
      if (!statusEl) return;
      statusEl.textContent = msg || '';
      statusEl.className = 'llm-active-status' + (cls ? ' ' + cls : '');
    }

    async function fetchModels(url, apiKey) {
      url = (url || urlInp.value.trim());
      if (!url) { modelSel.innerHTML = '<option value="">— set an endpoint —</option>'; setStatus(''); return; }
      const want = (_appSettings && _appSettings.llm_model) || '';
      setStatus('Loading models…');
      if (refreshBtn) refreshBtn.disabled = true;
      try {
        let fetchUrl = '/api/conversation/llm-models?url=' + encodeURIComponent(url);
        if (apiKey) fetchUrl += '&api_key=' + encodeURIComponent(apiKey);
        const d = await fetch(fetchUrl).then(r => r.json());
        if (d.error) {
           let msg = 'Failed';
           try { 
             const j = JSON.parse(d.error); 
             msg = j.error?.message || j.detail || d.error; 
           } catch(e) { msg = d.error; }
           
           // truncate long error messages
           if (msg.length > 50) msg = msg.substring(0, 47) + '...';
           
           modelSel.innerHTML = '<option value="">Auth / Error: ' + escHtml(msg) + '</option>';
           setStatus(msg, 'err');
           return;
        }
        
        const models = d.models || [];
        modelSel.innerHTML = models.length
          ? models.map(m => `<option value="${escHtml(m)}">${escHtml(m)}</option>`).join('')
          : '<option value="">No models found</option>';
        if (want && models.includes(want)) modelSel.value = want;
        setStatus(models.length ? `${models.length} model${models.length === 1 ? '' : 's'}` : 'No models found',
                  models.length ? 'ok' : 'err');
      } catch (e) {
        modelSel.innerHTML = '<option value="">Fetch failed</option>';
        setStatus('Could not reach endpoint', 'err');
      } finally {
        if (refreshBtn) refreshBtn.disabled = false;
      }
    }

    // Commit URL on blur or Enter — saves to settings and refetches models
    function commitUrl() {
      const url = urlInp.value.trim();
      if (!url) return;
      applyAndSaveSettings({ llm_url: url });
      if (_appSettings) _appSettings.llm_url = url;
      fetchModels(url);
    }
    urlInp.addEventListener('blur', commitUrl);
    urlInp.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); commitUrl(); } });
    // Also react when user picks a datalist suggestion (fires 'change')
    urlInp.addEventListener('change', commitUrl);

    // Helper to refresh the dropdown options based on visible cards
    function refreshEndpointOptions(currentUrl) {
      if (!urlInp || urlInp.tagName !== 'SELECT') return;
      let html = '<option value="">— Select an endpoint —</option>';
      const added = new Set();
      document.querySelectorAll('.llm-local-card, .llm-card').forEach(card => {
         const isLlm = card.closest('#dc-grid-llm, #llm-cloud-grid, .llm-service-grid');
         if (!isLlm) return;
         
         const uInp = card.querySelector('.dc-url-inp');
         if (uInp) {
            const u = uInp.value.trim() || uInp.placeholder;
            const name = card.querySelector('.llm-local-name')?.textContent.trim() || 'Local Engine';
            if (u && !added.has(u)) { added.add(u); html += `<option value="${escHtml(u)}">${escHtml(name)}</option>`; }
         }
         const epCode = card.querySelector('.llm-endpoint code');
         if (epCode) {
            const u = epCode.textContent.trim();
            const name = card.querySelector('.llm-card-name')?.textContent.trim() || 'Cloud API';
            if (u && !added.has(u)) { added.add(u); html += `<option value="${escHtml(u)}">${escHtml(name)}</option>`; }
         }
      });
      // If the current URL isn't in the options, add it as a custom entry
      const activeUrl = currentUrl || urlInp.value;
      if (activeUrl && !added.has(activeUrl)) {
         html += `<option value="${escHtml(activeUrl)}">Custom: ${escHtml(activeUrl)}</option>`;
      }
      urlInp.innerHTML = html;
      if (activeUrl) urlInp.value = activeUrl;
    }

    // Populate the dropdown when the user opens it
    urlInp.addEventListener('focus', () => refreshEndpointOptions());

    modelSel.addEventListener('change', () => {
      applyAndSaveSettings({ llm_model: modelSel.value });
      if (_appSettings) _appSettings.llm_model = modelSel.value;
      if (modelSel.value) setStatus('Active model: ' + modelSel.value, 'ok');
    });

    refreshBtn?.addEventListener('click', () => fetchModels());

    // Exposed so the "Use as LLM" card buttons can sync this panel
    window.syncActiveLlmPanel = function (url, apiKey) {
      if (typeof refreshEndpointOptions === 'function') refreshEndpointOptions(url);
      urlInp.value = url || '';
      fetchModels(url, apiKey);
    };

    // Initial state — restore saved endpoint + fetch its models
    (async function () {
      let url = (_appSettings && _appSettings.llm_url) || '';
      if (!url) {
        try {
          const s = await fetch('/api/settings').then(r => r.json());
          url = s.llm_url || '';
          if (!_appSettings) _appSettings = s;
          else { _appSettings.llm_url = s.llm_url || _appSettings.llm_url; _appSettings.llm_model = s.llm_model || _appSettings.llm_model; }
        } catch (_) {}
      }
      
      // wait a tick for docker cards to render so options are available
      setTimeout(() => {
        if (typeof refreshEndpointOptions === 'function') refreshEndpointOptions(url);
        urlInp.value = url || '';
        if (url) fetchModels(url);
      }, 100);
    })();
  })();

  // ── Inline STT quick-test panel ─────────────────────────────────────────
  (async function initSttTestPanel() {
    const panel    = $('stt-test-panel');
    const sel      = $('stt-test-backend');
    const micBtn   = $('stt-test-mic-btn');
    const statusEl = $('stt-test-status');
    const resultEl = $('stt-test-result');
    if (!panel || !micBtn) return;

    // Populate backend dropdown
    async function refreshSttTestBackends() {
      try {
        const d = await fetch('/api/stt-backends').then(r => r.json());
        const prev = sel.value;
        sel.innerHTML = (d.backends || []).map(b =>
          `<option value="${escHtml(b.id)}"${!b.available ? ' disabled' : ''}>${b.available ? '✓' : '✗'} ${escHtml(b.label)}</option>`
        ).join('');
        if (prev && sel.querySelector(`option[value="${CSS.escape(prev)}"]`)) sel.value = prev;
      } catch(_) {}
    }
    refreshSttTestBackends();

    if (!navigator.mediaDevices?.getUserMedia) {
      micBtn.disabled = true;
      micBtn.title = 'Microphone unavailable (requires HTTPS or localhost)';
      return;
    }

    let mediaRecorder = null;
    let chunks = [];

    micBtn.addEventListener('click', async () => {
      if (mediaRecorder && mediaRecorder.state === 'recording') {
        mediaRecorder.stop();
        return;
      }
      if (micBtn.classList.contains('busy')) return;
      chunks = [];
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        mediaRecorder = new MediaRecorder(stream);
        mediaRecorder.ondataavailable = e => { if (e.data.size) chunks.push(e.data); };
        mediaRecorder.onstop = async () => {
          stream.getTracks().forEach(t => t.stop());
          micBtn.className = 'stt-test-mic busy';
          micBtn.innerHTML = '<span class="mdi mdi-loading mdi-spin"></span>';
          statusEl.textContent = 'Transcribing…';
          resultEl.style.display = 'none';
          const blob = new Blob(chunks, { type: 'audio/webm' });
          const form = new FormData();
          form.append('file', blob, 'audio.webm');
          form.append('backend', sel.value || 'configured');
          try {
            const r = await fetch('/api/transcribe-bytes', { method: 'POST', body: form });
            const d = await r.json();
            if (d.text !== undefined) {
              resultEl.textContent = d.text || '(no speech detected)';
              statusEl.textContent = 'Done';
            } else {
              resultEl.textContent = '⚠ ' + (d.detail || JSON.stringify(d));
              statusEl.textContent = 'Error';
            }
          } catch(e) {
            resultEl.textContent = '⚠ ' + e.message;
            statusEl.textContent = 'Error';
          }
          resultEl.style.display = '';
          micBtn.className = 'stt-test-mic';
          micBtn.innerHTML = '<span class="mdi mdi-microphone"></span>';
        };
        mediaRecorder.start();
        micBtn.className = 'stt-test-mic recording';
        micBtn.innerHTML = '<span class="mdi mdi-stop"></span>';
        statusEl.textContent = 'Recording…';
        resultEl.style.display = 'none';
      } catch(e) {
        statusEl.textContent = 'Mic error';
        toast('Microphone error: ' + e.message, 'error');
      }
    });
  })();

})();

// ── LLM snippet collapse ───────────────────────────────────────────────────

function initLlmSnippets(root) {
  (root || document).querySelectorAll('.llm-local-snippet:not([data-snip-init])').forEach(snip => {
    snip.dataset.snipInit = '1';
    const bar     = snip.querySelector('.llm-snippet-bar');
    const pre     = snip.querySelector('pre');
    const copyBtn = snip.querySelector('.llm-copy-btn');
    if (!bar || !pre) return;

    const labelEl  = bar.querySelector('span:not(.llm-snip-chev)');
    const origText = labelEl ? labelEl.textContent.trim() : 'snippet';
    const cardName = snip.closest('.llm-local-card')?.querySelector('.llm-local-name')?.textContent?.trim() || '';
    const key = 'snip-' + (cardName + '-' + origText).toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 52);

    const chev = document.createElement('span');
    chev.className = 'llm-snip-chev';
    if (labelEl) bar.insertBefore(chev, labelEl);
    else bar.prepend(chev);

    const apply = (open) => {
      pre.hidden        = !open;
      if (copyBtn) copyBtn.hidden = !open;
      if (labelEl) labelEl.textContent = open ? origText : 'Show code snippet';
      chev.textContent = open ? '▾' : '▸';
    };

    const saved = localStorage.getItem(key);
    apply(saved === '1'); // default: collapsed

    bar.addEventListener('click', e => {
      if (copyBtn && (e.target === copyBtn || copyBtn.contains(e.target))) return;
      const nowOpen = pre.hidden;
      apply(nowOpen);
      try { localStorage.setItem(key, nowOpen ? '1' : ''); } catch {}
    });
  });
}

initLlmSnippets(document);

// ── Docker management for static llm-local-cards ──────────────────────────
// Injects a unified .dc-controls-row into every static engine card that has a
// [data-llm-local-key] URL input.  The row contains:
//   [Connect / Disconnect]  [Stop | Start | Restart]  [Use as TTS/STT →]
// The Connect button is moved here from inside the URL row.
// Connecting auto-applies the URL to settings so it appears in dropdowns.
(function initStaticDockerManagement() {
  document.querySelectorAll('.llm-local-card:not([data-docker-init])').forEach(card => {
    const urlInp = card.querySelector('[data-llm-local-key]');
    if (!urlInp) return;
    card.dataset.dockerInit = '1';

    const key    = urlInp.dataset.llmLocalKey;
    const lsKey  = 'llm-docker-name-' + key;

    // ── 1. Container name row (always injected, right after URL row) ──────
    const urlRow = urlInp.closest('.llm-local-url');
    const savedName = (_appSettings?.engine_container_names?.[key])
      || localStorage.getItem(lsKey) || '';
    const nameRow = document.createElement('div');
    nameRow.className = 'llm-local-url';
    nameRow.innerHTML =
      `<span class="llm-local-url-label" title="Docker container name — enables Stop / Start / Restart"><span class="mdi mdi-docker"></span></span>` +
      `<input class="llm-local-url-inp" type="text" placeholder="container name (optional)" spellcheck="false" data-cn-key="${key}">`;
    const nameInp = nameRow.querySelector('input');
    nameInp.value = savedName;
    if (urlRow) urlRow.after(nameRow);

    // ── 2. Unified controls row ───────────────────────────────────────────
    const controlsRow = document.createElement('div');
    controlsRow.className = 'dc-controls-row';

    // Move the existing .llm-local-ping Connect button from URL row → controls row
    const connectBtn = urlRow?.querySelector('.llm-local-ping');
    if (connectBtn) {
      connectBtn.innerHTML = '<span class="mdi mdi-lan-connect"></span> Connect';
      connectBtn.title = 'Test connection to this service';
      urlRow.removeChild(connectBtn);
      controlsRow.appendChild(connectBtn);
    }

    // Docker action buttons group
    const dockerGroup = document.createElement('span');
    dockerGroup.className = 'dc-ctrl-group';
    controlsRow.appendChild(dockerGroup);

    // Move the existing .llm-use-btn to controls row (gets dc-use-btn class + right-align)
    const actionsEl = card.querySelector('.llm-local-actions');
    const useBtnEl  = actionsEl?.querySelector('.llm-use-btn');
    if (useBtnEl) {
      useBtnEl.classList.add('dc-use-btn');
      useBtnEl.title = 'Saves this URL to Settings — makes it available in the backend dropdown. Also applied automatically when you Connect.';
      const icon = document.createElement('span');
      icon.className = 'mdi mdi-arrow-right-circle-outline';
      useBtnEl.prepend(icon);
      controlsRow.appendChild(useBtnEl);
    }
    if (actionsEl && actionsEl.children.length === 0) actionsEl.remove();

    // Insert controls row right after the name row
    nameRow.after(controlsRow);

    // ── 3. Set connected / disconnected state ─────────────────────────────
    function setConnected(connected) {
      if (!connectBtn) return;
      if (connected) {
        connectBtn.innerHTML = '<span class="mdi mdi-check-network"></span> Connected';
        connectBtn.className = 'llm-local-ping ok';
        connectBtn.dataset.action = 'disconnect';
        connectBtn.title = 'Reachable — click to disconnect';
        card.classList.add('llm-local-card-online');
        if (useBtnEl) useBtnEl.classList.add('active');
      } else {
        connectBtn.innerHTML = '<span class="mdi mdi-lan-connect"></span> Connect';
        connectBtn.className = 'llm-local-ping';
        connectBtn.dataset.action = 'connect';
        connectBtn.title = 'Test connection to this service';
        card.classList.remove('llm-local-card-online');
        if (useBtnEl) useBtnEl.classList.remove('active');
      }
    }
    if (localStorage.getItem('llm-local-con-' + key) === '1') setConnected(true);

    // ── 4. Connect / Disconnect click ─────────────────────────────────────
    connectBtn?.addEventListener('click', async () => {
      if (connectBtn.dataset.action === 'disconnect') {
        setConnected(false);
        localStorage.setItem('llm-local-con-' + key, '0');
        return;
      }
      const rawUrl = urlInp.value.trim() || urlInp.placeholder;
      if (!rawUrl) return;
      connectBtn.disabled = true;
      connectBtn.innerHTML = '<span class="mdi mdi-loading mdi-spin"></span> Connecting…';
      try {
        const type = cardType(card);
        const d    = await probeUrl(rawUrl, type);
        setConnected(d.ok);
        localStorage.setItem('llm-local-con-' + key, d.ok ? '1' : '0');
        if (d.ok) {
          // Auto-apply URL to settings so it appears in dropdowns immediately
          if (useBtnEl && window.applyAndSaveSettings) {
            useBtnEl.click(); // triggers the existing handler which calls applyAndSaveSettings
            toast(`✓ Connected and set as active ${type.toUpperCase() || 'backend'}`, 'success');
          } else {
            toast(`✓ ${type.toUpperCase() || 'Service'} reachable`, 'success');
          }
        } else {
          toast('Cannot reach ' + rawUrl + ': ' + (d.error || 'No response'), 'error');
        }
      } catch (e) {
        setConnected(false);
        toast('Probe failed: ' + e.message, 'error');
      } finally {
        connectBtn.disabled = false;
      }
    });

    // ── 5. Docker action buttons (Start / Stop / Restart) ─────────────────
    function renderDockerBtns() {
      const name = nameInp.value.trim();
      if (!name) { dockerGroup.innerHTML = ''; return; }
      const esc = name.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
      dockerGroup.innerHTML =
        `<button class="llm-use-btn dc-btn" data-dc-action="start"   data-dc-name="${esc}"><span class="mdi mdi-play"></span> Start</button>` +
        `<button class="llm-use-btn dc-btn" data-dc-action="stop"    data-dc-name="${esc}"><span class="mdi mdi-stop"></span> Stop</button>` +
        `<button class="llm-use-btn dc-btn" data-dc-action="restart" data-dc-name="${esc}"><span class="mdi mdi-restart"></span> Restart</button>`;
      dockerGroup.querySelectorAll('.dc-btn').forEach(btn => {
        btn.addEventListener('click', async () => {
          const action = btn.dataset.dcAction;
          const cname  = btn.dataset.dcName;
          const orig   = btn.innerHTML;
          btn.disabled = true;
          btn.textContent = action === 'start' ? 'Starting…' : action === 'stop' ? 'Stopping…' : 'Restarting…';
          try {
            const r = await fetch(`/api/local-containers/${encodeURIComponent(cname)}/${action}`, { method: 'POST' });
            const d = await r.json();
            if (d.ok) toast(`${cname}: ${action} OK`, 'success');
            else toast(d.error || `${action} failed`, 'error');
          } catch (e) { toast(`${action} failed: ${e.message}`, 'error'); }
          btn.disabled = false; btn.innerHTML = orig;
        });
      });
    }
    nameInp.addEventListener('input', () => {
      localStorage.setItem(lsKey, nameInp.value);
      if (window._saveEngineContainerNames) window._saveEngineContainerNames();
      renderDockerBtns();
    });
    renderDockerBtns();
  });
})();

// ── Collapsible cards ──────────────────────────────────────────────────────

window.initCollapsibleCards = function initCollapsibleCards() {
  const LS_KEY = 'card-collapse-v1';
  const load  = () => { try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); } catch { return {}; } };
  const save  = (k, v) => { const s = load(); s[k] = v; try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch {} };
  const slug  = t => t.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 48);

  // Sync existing <details> elements inside .card to localStorage
  document.querySelectorAll('.card > details').forEach(det => {
    const sum = det.querySelector('summary');
    if (!sum) return;
    const key = 'det-' + slug(sum.textContent);
    const st = load();
    if (st[key] !== undefined) det.open = st[key];
    else st[key] = det.open; // persist current default
    det.addEventListener('toggle', () => save(key, det.open));
  });

  // Add collapse toggle to every .card that has a direct h2 child
  document.querySelectorAll('.card > h2').forEach(h2 => {
    const card = h2.parentElement;
    if (!card || card.dataset.colInit) return;
    card.dataset.colInit = '1';

    const key  = 'card-' + slug(h2.textContent);
    const open = load()[key] !== false; // default: open

    // Prepend a rotating chevron inside the h2
    const chev = document.createElement('span');
    chev.className = 'card-chev mdi mdi-chevron-down';
    chev.setAttribute('aria-hidden', 'true');
    h2.prepend(chev);
    h2.classList.add('card-collapse-h2');

    // Wrap every element after h2 (skipping .card-subtitle which stays visible) in body
    const body = document.createElement('div');
    body.className = 'card-col-body';
    let sib = h2.nextElementSibling;
    while (sib && sib.classList.contains('card-subtitle')) sib = sib.nextElementSibling;
    while (sib) { const nx = sib.nextElementSibling; body.appendChild(sib); sib = nx; }
    card.appendChild(body);

    const apply = (isOpen) => {
      body.hidden = !isOpen;
      card.classList.toggle('card-col-closed', !isOpen);
    };

    apply(open);

    h2.addEventListener('click', () => {
      const nowOpen = body.hidden; // hidden → about to open
      apply(nowOpen);
      save(key, nowOpen);
    });
  });
};
window.initCollapsibleCards();

// ── Collapsible integration cards (collapsed by default) ───────────────────

(function initIntegrationCards() {
  const LS_KEY = 'icard-collapse-v1';
  const load = () => { try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); } catch { return {}; } };
  const save = (k, v) => { const s = load(); s[k] = v; try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch {} };
  const slug = t => t.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 48);

  document.querySelectorAll('.integration-card').forEach(card => {
    const h3 = card.querySelector('h3');
    if (!h3 || card.dataset.icardInit) return;
    card.dataset.icardInit = '1';

    const key  = 'icard-' + slug(h3.textContent);
    const open = load()[key] === true; // default: closed

    // Prepend logo — favicon img or MDI icon (skip if h3 already has a leading .mdi span)
    const hasIcon = h3.firstElementChild && h3.firstElementChild.classList.contains('mdi');
    const faviconUrl = card.dataset.favicon;
    const iconClass  = card.dataset.icon;
    if (!hasIcon) {
      if (faviconUrl) {
        const img = document.createElement('img');
        img.className = 'icard-logo';
        img.src = faviconUrl;
        img.alt = '';
        img.onerror = () => { img.style.display = 'none'; };
        h3.prepend(img);
      } else if (iconClass) {
        const span = document.createElement('span');
        span.className = 'icard-icon ' + iconClass;
        h3.prepend(span);
      }
    } else if (faviconUrl) {
      // Has existing .mdi span but also has a favicon — add favicon before the mdi span
      const img = document.createElement('img');
      img.className = 'icard-logo';
      img.src = faviconUrl;
      img.alt = '';
      img.onerror = () => { img.style.display = 'none'; };
      h3.prepend(img);
    }

    // Append chevron
    const chev = document.createElement('span');
    chev.className = 'icard-chev';
    chev.setAttribute('aria-hidden', 'true');
    h3.appendChild(chev);

    // Wrap all siblings after h3 in a body div
    const body = document.createElement('div');
    body.className = 'icard-body';
    let sib = h3.nextElementSibling;
    while (sib) { const nx = sib.nextElementSibling; body.appendChild(sib); sib = nx; }
    card.appendChild(body);

    const apply = (isOpen) => {
      body.hidden = !isOpen;
      card.classList.toggle('icard-closed', !isOpen);
    };

    apply(open);

    h3.addEventListener('click', () => {
      const nowOpen = body.hidden;
      apply(nowOpen);
      save(key, nowOpen);
    });
  });
})();

