// ── Theme ──
function toggleTheme() {
  const html = document.documentElement;
  const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
  html.setAttribute('data-theme', next);
  try { localStorage.setItem('index1-theme', next); } catch(e) {}
}
(function() {
  try {
    const saved = localStorage.getItem('index1-theme');
    if (saved) document.documentElement.setAttribute('data-theme', saved);
  } catch(e) {}
})();

// ── State ──
let selIdx = -1, curResults = [], curContent = '', curViewMode = 'list', curBrowseDocs = [];

// ── Tabs ──
function switchTab(btn) {
  document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
  document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
  btn.classList.add('active');
  document.getElementById(btn.dataset.panel).classList.add('active');
  if (btn.dataset.panel === 'status-panel') loadStatus();
  if (btn.dataset.panel === 'memory-panel') { loadMemStats(); loadFacts(); }
  if (btn.dataset.panel === 'events-panel') { loadEvents(); startEvtAutoRefresh(); } else { stopEvtAutoRefresh(); }
}

// ── Search ──
let timer = null;
const sInput = document.getElementById('search-input');
const sClear = document.getElementById('s-clear');
const sKbd = document.getElementById('kbd-slash');

sInput.addEventListener('input', () => {
  clearTimeout(timer);
  sClear.classList.toggle('show', sInput.value.length > 0);
  sKbd.style.display = sInput.value.length > 0 ? 'none' : '';
  timer = setTimeout(doSearch, 300);
});
sInput.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    if (selIdx >= 0 && curResults[selIdx]) showChunk(curResults[selIdx].chunk_id);
    else { clearTimeout(timer); doSearch(); }
    return;
  }
  if (e.key === 'ArrowDown') { e.preventDefault(); moveSel(1); }
  if (e.key === 'ArrowUp') { e.preventDefault(); moveSel(-1); }
});

function clearSearch() {
  sInput.value = ''; sInput.focus();
  sClear.classList.remove('show'); sKbd.style.display = '';
  selIdx = -1; curResults = []; curBrowseDocs = []; activateLogo(false);
  document.getElementById('results').innerHTML = '';
  document.getElementById('meta').style.display = 'none';
  document.getElementById('proj-overview').style.display = '';
  document.getElementById('col-filter').value = '';
  document.getElementById('type-filter').value = '';
  // 恢复全局 type filter
  fetch('/api/status').then(r => r.json()).then(d => { if (d.doc_types) updTypes(d.doc_types); }).catch(() => {});
  loadProjectOverview();
}

function moveSel(d) {
  const c = document.querySelectorAll('.result-card');
  if (!c.length) return;
  c.forEach(x => x.classList.remove('selected'));
  selIdx = Math.max(-1, Math.min(c.length - 1, selIdx + d));
  if (selIdx >= 0) { c[selIdx].classList.add('selected'); c[selIdx].scrollIntoView({ block: 'nearest' }); }
}

function hl(text, q) {
  if (!q || !text) return esc(text);
  const terms = q.split(/\s+/).filter(t => t.length > 1);
  if (!terms.length) return esc(text);
  const e = esc(text);
  const p = terms.map(t => esc(t).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
  try { return e.replace(new RegExp('(' + p + ')', 'gi'), '<mark>$1</mark>'); } catch(x) { return e; }
}

async function doSearch() {
  const q = sInput.value.trim();
  const rEl = document.getElementById('results');
  const mEl = document.getElementById('meta');
  const col = document.getElementById('col-filter').value;
  selIdx = -1; curResults = [];

  if (!q) {
    // 搜索框为空但选了集合 → 浏览该集合
    if (col) { browseCollection(col); return; }
    clearSearch(); return;
  }

  document.getElementById('proj-overview').style.display = 'none';
  rEl.innerHTML = '<div class="empty"><span class="spinner"></span> Searching...</div>';
  mEl.style.display = 'none';

  try {
    let url = '/api/search?q=' + encodeURIComponent(q) + '&top_k=10';
    if (col) url += '&collection=' + encodeURIComponent(col);
    const resp = await fetch(url);
    const data = await resp.json();

    if (data.error) { rEl.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(data.error) + '</div>'; return; }
    if (!data.results || !data.results.length) { activateLogo(false); rEl.innerHTML = '<div class="empty">No results for "' + esc(q) + '"</div>'; return; }

    curResults = data.results;
    activateLogo(data.results.length > 0);
    const mc = data.mode === 'hybrid' ? 'badge-hybrid' : 'badge-bm25';
    mEl.innerHTML = '<span>' + data.total + ' results &middot; ' + data.query_ms + 'ms</span><span class="badge ' + mc + '">' + esc(data.mode) + '</span>' + (data.hint ? '<span>' + esc(data.hint) + '</span>' : '');
    mEl.style.display = 'flex';

    rEl.innerHTML = data.results.map((r, i) =>
      '<div class="result-card" style="animation-delay:' + (i*30) + 'ms" onclick="showChunk(' + r.chunk_id + ')" onmouseenter="selIdx=' + i + '">' +
        '<div class="r-top">' +
          '<span class="r-dot ' + r.relevance + '"></span>' +
          '<span class="r-src">' + esc(r.source || '?') + '</span>' +
          '<div class="r-right">' + (r.token_count ? '<span class="r-tok">' + r.token_count + ' tok</span>' : '') +
            '<span class="r-score">' + r.score.toFixed(3) + '</span></div>' +
        '</div>' +
        (r.section ? '<div class="r-sec">' + esc(r.section) + '</div>' : '') +
        '<div class="r-sum">' + hl(r.summary ? r.summary.substring(0,200) : (r.content ? r.content.substring(0,200) : ''), q) + '</div>' +
      '</div>'
    ).join('');
  } catch (e) { rEl.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

// ── Chunk ──
async function showChunk(id) {
  const modal = document.getElementById('chunk-modal');
  const tEl = document.getElementById('m-title');
  const mEl = document.getElementById('m-meta');
  const cEl = document.getElementById('m-content');
  curContent = ''; tEl.textContent = 'Loading...'; mEl.innerHTML = ''; cEl.textContent = '';
  modal.classList.add('open');
  document.getElementById('copy-btn').classList.remove('copied');
  document.getElementById('copy-btn').querySelector('span').textContent = 'Copy';

  try {
    const data = await (await fetch('/api/chunk/' + id)).json();
    if (data.error) { tEl.textContent = 'Error'; cEl.textContent = data.error; return; }
    curContent = data.content || '';
    tEl.textContent = data.source || 'Chunk #' + data.id;
    mEl.innerHTML = [
      { l: 'Chunk', v: '#' + data.id }, { l: 'Section', v: data.section || '-' },
      { l: 'Index', v: data.chunk_index }, { l: 'Tokens', v: data.token_count || '-' },
      { l: 'Tags', v: data.tags || '-' },
    ].map(m => '<div class="m-meta-item"><div class="label">' + m.l + '</div><div class="value">' + esc(String(m.v)) + '</div></div>').join('');
    cEl.textContent = curContent;
    if (data.context && (data.context.prev_section || data.context.next_section)) {
      const d = document.createElement('div'); d.className = 'm-ctx';
      if (data.context.prev_section) { const s = document.createElement('span'); s.textContent = '\u2190 ' + data.context.prev_section; d.appendChild(s); }
      if (data.context.next_section) { const s = document.createElement('span'); s.textContent = data.context.next_section + ' \u2192'; d.appendChild(s); }
      cEl.appendChild(d);
    }
  } catch (e) { tEl.textContent = 'Error'; cEl.textContent = e.message; }
}

async function copyContent() {
  if (!curContent) return;
  try {
    await navigator.clipboard.writeText(curContent);
    const b = document.getElementById('copy-btn'); b.classList.add('copied');
    b.querySelector('span').textContent = 'Copied!';
    setTimeout(() => { b.classList.remove('copied'); b.querySelector('span').textContent = 'Copy'; }, 2000);
  } catch(e) { toast('Copy failed'); }
}

function closeModal() { document.getElementById('chunk-modal').classList.remove('open'); }

// ── Status ──
async function loadStatus() {
  const el = document.getElementById('status-content');
  el.innerHTML = '<div class="empty"><span class="spinner"></span></div>';
  try {
    const [data, obs] = await Promise.all([
      fetch('/api/status').then(r => r.json()),
      fetch('/api/cognition/observer').then(r => r.json()).catch(() => null)
    ]);
    if (data.error) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(data.error) + '</div>'; return; }
    const vc = data.vec_loaded ? 'on' : 'off';
    let html =
      '<button class="s-refresh" onclick="loadStatus()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 102.13-9.36L1 10"/></svg> Refresh</button>';

    // Observer section — 三层显示，谁工作谁绿灯
    if (obs) {
      const h = obs.health || {};
      const ap = obs.active_provider;
      const st = h.status || 'unknown';
      const isCritical = st === 'critical';
      const l2ok = !isCritical && !!ap;
      const l1ok = (obs.providers||[]).some(p => p.name==='ollama' && p.available);
      // 只有当前生效层亮绿灯：L2 > L1 > L0
      const active = l2ok ? 'l2' : l1ok ? 'l1' : 'l0';

      html += '<div class="obs-section">' +
        '<div class="obs-header">' +
          '<div class="obs-title">Observer</div>' +
        '</div>';

      const layers = [
        { id: 'l0', name: 'L0 Rules', desc: 'Pattern extraction' },
        { id: 'l1', name: 'L1 Local', desc: 'Ollama embedding' },
        { id: 'l2', name: 'L2 Cloud', desc: ap ? ap.name + ' · ' + ap.model : 'LLM analysis' }
      ];
      html += '<div class="obs-layers">';
      layers.forEach(l => {
        const isActive = l.id === active;
        const color = isActive ? 'var(--green)' : 'var(--text-3)';
        const label = isActive ? (l.id === 'l0' ? 'Fallback' : 'Active') : 'Standby';
        html += '<div class="obs-layer' + (isActive ? ' on' : '') + '">' +
          '<div class="obs-layer-head">' +
            '<span class="obs-dot" style="background:' + color + '"></span>' +
            '<span class="obs-layer-name">' + l.name + '</span>' +
            '<span class="obs-layer-status" style="color:' + color + '">' + label + '</span>' +
          '</div>' +
          '<span class="obs-layer-desc">' + esc(l.desc) + '</span>' +
        '</div>';
      });
      html += '</div>';

      // Provider chain（L2 可用时展示）
      const provs = obs.providers || [];
      if (provs.length && l2ok) {
        html += '<div class="obs-chain">';
        provs.forEach(pr => {
          const isActive = ap && pr.name === ap.name;
          const cls = isActive ? 'obs-prov active' : pr.available ? 'obs-prov avail' : 'obs-prov off';
          html += '<div class="' + cls + '">' +
            '<span class="obs-prov-name">' + esc(pr.name) + '</span>' +
            '<span class="obs-prov-model">' + esc(pr.model) + '</span>' +
          '</div>';
        });
        html += '</div>';
      }

      html += '</div>';
    }

    // Hooks status
    const lastEvt = data.last_event;
    const evtHour = data.events_last_hour || 0;
    let hookOk = true;
    if (lastEvt) {
      const ago = (Date.now() - new Date(lastEvt + 'Z').getTime()) / 60000;
      hookOk = ago < 30; // 30 分钟内有事件 = 存活
    } else {
      hookOk = false;
    }
    const hookColor = hookOk ? 'var(--green)' : 'var(--red)';
    const hookLabel = hookOk ? 'Connected' : 'No recent events';
    html += '<div class="obs-section" style="padding:16px 20px">' +
      '<div class="obs-header" style="margin-bottom:0">' +
        '<div class="obs-title">Hooks</div>' +
        '<div class="obs-status" style="color:' + hookColor + '"><span class="obs-dot" style="background:' + hookColor + '"></span>' + hookLabel + '</div>' +
      '</div>';
    if (lastEvt) {
      html += '<div style="margin-top:8px;font:11px var(--mono);color:var(--text-3)">Last event: ' + esc(localTime(lastEvt)) + ' · ' + evtHour + ' events/hour</div>';
    }
    if (!hookOk) {
      html += '<div class="obs-hint">No hook events received recently. Check that Claude Code hooks are configured: <code>index1 setup --status</code></div>';
    }
    html += '</div>';

    // Version + stats
    const ver = data.version || '?';
    html += '<div class="status-grid">' +
        sc('Version', ver, '', 'small') +
        sc('Documents', data.doc_count, '') + sc('Chunks', data.chunk_count, '') +
        sc('Collections', data.collections ? data.collections.length : 0, data.collections ? data.collections.join(', ') : '-') +
        sc('Vector', data.vec_loaded ? 'ON' : 'OFF', data.vec_loaded ? 'sqlite-vec loaded' : 'BM25 only', vc) +
      '</div>';

    el.innerHTML = html;
    updCol(data.collections || []);
    updTypes(data.doc_types || []);

    // Async: version check + error logs (non-blocking)
    fetch('/api/version').then(r => r.json()).then(v => {
      if (v.update_available) {
        const card = el.querySelector('.stat-card');
        if (card) {
          card.innerHTML = '<div class="label">Version</div>' +
            '<div class="value small">' + esc(v.current) + '</div>' +
            '<div class="ver-update">New: ' + esc(v.latest) + ' · <code>pip install -U index1</code></div>';
        }
      }
    }).catch(() => {});

    fetch('/api/logs').then(r => r.json()).then(logs => {
      const keys = Object.keys(logs);
      if (!keys.length) return;
      let lhtml = '<div class="logs-section"><div class="obs-title" style="margin-bottom:10px">Recent Logs</div>';
      keys.forEach(name => {
        const lines = logs[name];
        if (!lines.length) return;
        const label = name.replace('.log', '').replace(/_/g, ' ');
        lhtml += '<div class="log-block"><div class="log-name">' + esc(label) + '</div>' +
          '<pre class="log-pre">' + esc(lines.join('\n')) + '</pre></div>';
      });
      lhtml += '</div>';
      el.insertAdjacentHTML('beforeend', lhtml);
    }).catch(() => {});
  } catch(e) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

function sc(l, v, s, c) {
  return '<div class="stat-card"><div class="label">' + l + '</div><div class="value ' + (c||'') + '">' + esc(String(v)) + '</div>' + (s ? '<div class="sub">' + esc(s) + '</div>' : '') + '</div>';
}

function updCol(cols) {
  const sel = document.getElementById('col-filter');
  const cur = sel.value;
  sel.innerHTML = '<option value="">All Projects</option>';
  cols.forEach(c => { const o = document.createElement('option'); o.value = c; o.textContent = c; if (c === cur) o.selected = true; sel.appendChild(o); });
}

const TYPE_LABELS = { markdown: 'Markdown', python: 'Python', rust: 'Rust', javascript: 'JavaScript', text: 'Text' };
function updTypes(types) {
  const sel = document.getElementById('type-filter');
  const cur = sel.value;
  sel.innerHTML = '<option value="">All Types</option>';
  (types || []).forEach(t => { const o = document.createElement('option'); o.value = t; o.textContent = TYPE_LABELS[t] || t; if (t === cur) o.selected = true; sel.appendChild(o); });
}

function applyTypeFilter() {
  const tf = document.getElementById('type-filter').value;
  const col = document.getElementById('col-filter').value;
  // 如果在浏览模式（没有搜索词），重新加载浏览结果
  if (!sInput.value.trim() && col) {
    browseCollection(col);
    return;
  }
  // 搜索模式下，客户端过滤
  if (!tf) { document.querySelectorAll('.result-card').forEach(c => c.style.display = ''); return; }
  document.querySelectorAll('.result-card').forEach((c, i) => {
    if (!curResults[i]) return;
    const src = curResults[i].source || '';
    const ext = src.split('.').pop().toLowerCase();
    const typeMap = { py: 'python', md: 'markdown', rs: 'rust', js: 'javascript', ts: 'javascript', txt: 'text', toml: 'text', cfg: 'text', ini: 'text', yml: 'text', yaml: 'text' };
    const docType = typeMap[ext] || 'text';
    c.style.display = (docType === tf) ? '' : 'none';
  });
}


// ── Keys ──
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') { closeModal(); return; }
  if (['INPUT','TEXTAREA','SELECT'].includes(e.target.tagName)) return;
  if (e.key === '/') { e.preventDefault(); document.querySelectorAll('.nav-tab')[0].click(); sInput.focus(); }
  if (e.key >= '1' && e.key <= '4') { const t = document.querySelectorAll('.nav-tab'); if (t[e.key-1]) switchTab(t[e.key-1]); }
});

// ── Util ──
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function toast(m) { const t = document.getElementById('toast'); t.textContent = m; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 2500); }
function localTime(utcStr) {
  if (!utcStr) return '';
  const d = new Date(utcStr + (utcStr.endsWith('Z') ? '' : 'Z'));
  if (isNaN(d.getTime())) return utcStr.substring(5, 16).replace('T',' ');
  const m = String(d.getMonth()+1).padStart(2,'0');
  const day = String(d.getDate()).padStart(2,'0');
  const h = String(d.getHours()).padStart(2,'0');
  const min = String(d.getMinutes()).padStart(2,'0');
  return m + '-' + day + ' ' + h + ':' + min;
}

// ── Logo activation ──
function activateLogo(on) {
  const el = document.querySelector('.logo-icon');
  if (el) el.classList.toggle('active', !!on);
}

// ── Project Overview ──
async function loadProjectOverview() {
  const el = document.getElementById('proj-overview');
  try {
    const data = await (await fetch('/api/collections')).json();
    if (!data || !data.length) {
      el.innerHTML = '<div class="empty"><svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg><p>No indexed projects yet</p><div class="hint">Run <code>index1 index /path</code> to add projects</div></div>';
      return;
    }
    const totalDocs = data.reduce((s, c) => s + c.doc_count, 0);
    const totalChunks = data.reduce((s, c) => s + c.chunk_count, 0);
    el.innerHTML =
      '<div class="proj-total"><b>' + data.length + '</b> projects &middot; <b>' + totalDocs + '</b> documents &middot; <b>' + totalChunks + '</b> chunks</div>' +
      '<div class="proj-grid">' + data.map((c, i) =>
        '<div class="proj-card" style="animation-delay:' + (i*50) + 'ms" onclick="browseCollection(\'' + esc(c.collection) + '\')">' +
          '<div class="proj-name"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>' + esc(c.collection) + '</div>' +
          '<div class="proj-stats"><span class="proj-stat"><b>' + c.doc_count + '</b> docs</span><span class="proj-stat"><b>' + c.chunk_count + '</b> chunks</span></div>' +
        '</div>'
      ).join('') + '</div>';
  } catch(e) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

async function browseCollection(col) {
  document.getElementById('col-filter').value = col;
  document.getElementById('proj-overview').style.display = 'none';
  const rEl = document.getElementById('results');
  const mEl = document.getElementById('meta');
  rEl.innerHTML = '<div class="empty"><span class="spinner"></span> Loading...</div>';

  const tf = document.getElementById('type-filter').value;
  let url = '/api/browse?collection=' + encodeURIComponent(col) + '&limit=100';
  if (tf) url += '&doc_type=' + encodeURIComponent(tf);

  try {
    const data = await (await fetch(url)).json();
    if (data.error) { rEl.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(data.error) + '</div>'; return; }

    // 动态更新 type filter，只显示该集合存在的类型
    if (data.doc_types) updTypes(data.doc_types);

    if (!data.docs || !data.docs.length) { rEl.innerHTML = '<div class="empty">No documents in ' + esc(col) + (tf ? ' (' + tf + ')' : '') + '</div>'; mEl.innerHTML = '<span>0 documents</span><button class="m-btn" style="margin-left:auto" onclick="clearSearch()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Back</button>'; mEl.style.display = 'flex'; return; }

    curBrowseDocs = data.docs;

    mEl.innerHTML = '<span>' + data.docs.length + ' documents in <b>' + esc(col) + '</b></span>' +
      '<div class="view-modes">' +
        '<button class="view-mode' + (curViewMode==='list'?' active':'') + '" onclick="switchViewMode(\'list\')" title="List view"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="2" width="14" height="2" rx="0.5"/><rect x="1" y="7" width="14" height="2" rx="0.5"/><rect x="1" y="12" width="14" height="2" rx="0.5"/></svg></button>' +
        '<button class="view-mode' + (curViewMode==='table'?' active':'') + '" onclick="switchViewMode(\'table\')" title="Table view"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="6" height="6" rx="0.5" opacity=".7"/><rect x="9" y="1" width="6" height="6" rx="0.5" opacity=".7"/><rect x="1" y="9" width="6" height="6" rx="0.5" opacity=".7"/><rect x="9" y="9" width="6" height="6" rx="0.5" opacity=".7"/></svg></button>' +
        '<button class="view-mode' + (curViewMode==='grid'?' active':'') + '" onclick="switchViewMode(\'grid\')" title="Grid view"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="4" height="4" rx="0.5"/><rect x="6" y="1" width="4" height="4" rx="0.5"/><rect x="11" y="1" width="4" height="4" rx="0.5"/><rect x="1" y="6" width="4" height="4" rx="0.5"/><rect x="6" y="6" width="4" height="4" rx="0.5"/><rect x="11" y="6" width="4" height="4" rx="0.5"/><rect x="1" y="11" width="4" height="4" rx="0.5"/><rect x="6" y="11" width="4" height="4" rx="0.5"/><rect x="11" y="11" width="4" height="4" rx="0.5"/></svg></button>' +
      '</div>' +
      '<button class="m-btn" onclick="clearSearch()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Back</button>';
    mEl.style.display = 'flex';

    renderBrowseView(data.docs);
  } catch(e) { rEl.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

const TYPE_ICONS = {
  python: '\ud83d\udc0d', rust: '\u2699\ufe0f', javascript: '\ud83d\udfe1',
  markdown: '\ud83d\udcdd', text: '\ud83d\udcc4'
};

function switchViewMode(mode) {
  curViewMode = mode;
  document.querySelectorAll('.view-mode').forEach(b => b.classList.remove('active'));
  const btns = document.querySelectorAll('.view-mode');
  const idx = {list:0, table:1, grid:2}[mode] || 0;
  if (btns[idx]) btns[idx].classList.add('active');
  if (curBrowseDocs.length) renderBrowseView(curBrowseDocs);
}

function renderBrowseView(docs) {
  const rEl = document.getElementById('results');
  if (curViewMode === 'table') {
    rEl.innerHTML = '<table class="results-table"><thead><tr><th>File</th><th>Type</th><th style="text-align:right">Chunks</th><th style="text-align:right">Tokens</th></tr></thead><tbody>' +
      docs.map(d =>
        '<tr class="browse-row" data-doc-id="' + d.id + '">' +
          '<td class="t-path">' + esc(d.path) + '</td>' +
          '<td class="t-type">' + (TYPE_ICONS[d.doc_type]||'') + ' ' + (TYPE_LABELS[d.doc_type]||d.doc_type) + '</td>' +
          '<td class="t-num">' + d.chunk_count + '</td>' +
          '<td class="t-num">' + d.total_tokens + '</td>' +
        '</tr>'
      ).join('') + '</tbody></table>';
  } else if (curViewMode === 'grid') {
    rEl.innerHTML = '<div class="results-grid">' + docs.map((d, i) => {
      const fname = d.path.split('/').pop();
      return '<div class="grid-card browse-row" data-doc-id="' + d.id + '" style="animation-delay:' + (i*20) + 'ms">' +
        '<div class="g-icon">' + (TYPE_ICONS[d.doc_type]||'\ud83d\udcc4') + '</div>' +
        '<div class="g-name">' + esc(fname) + '</div>' +
        '<div class="g-meta">' + d.chunk_count + ' chunks &middot; ' + d.total_tokens + ' tok</div>' +
      '</div>';
    }).join('') + '</div>';
  } else {
    rEl.innerHTML = docs.map((d, i) =>
      '<div class="result-card browse-row" data-doc-id="' + d.id + '" style="animation-delay:' + (i*20) + 'ms">' +
        '<div class="r-top">' +
          '<span style="font-size:14px">' + (TYPE_ICONS[d.doc_type]||'\ud83d\udcc4') + '</span>' +
          '<span class="r-src">' + esc(d.path) + '</span>' +
          '<div class="r-right"><span class="r-tok">' + d.chunk_count + ' chunks</span><span class="r-tok">' + d.total_tokens + ' tok</span></div>' +
        '</div>' +
      '</div>'
    ).join('');
  }
  // 事件委托：安全绑定点击事件，避免 XSS
  rEl.querySelectorAll('.browse-row').forEach(el => {
    el.addEventListener('click', () => browseDoc(parseInt(el.dataset.docId)));
  });
}

async function browseDoc(docId) {
  const modal = document.getElementById('chunk-modal');
  const tEl = document.getElementById('m-title');
  const mEl = document.getElementById('m-meta');
  const cEl = document.getElementById('m-content');
  curContent = ''; tEl.textContent = 'Loading...'; mEl.innerHTML = ''; cEl.textContent = '';
  modal.classList.add('open');
  document.getElementById('copy-btn').classList.remove('copied');
  document.getElementById('copy-btn').querySelector('span').textContent = 'Copy';

  try {
    const data = await (await fetch('/api/doc/' + docId + '/chunks')).json();
    if (data.error) { tEl.textContent = 'Error'; cEl.textContent = data.error; return; }
    tEl.textContent = data.source || 'Document #' + docId;
    mEl.innerHTML = '<div class="m-meta-item"><div class="label">Chunks</div><div class="value">' + data.chunks.length + '</div></div>' +
      '<div class="m-meta-item"><div class="label">Tokens</div><div class="value">' + data.chunks.reduce((s,c) => s + (c.token_count||0), 0) + '</div></div>';
    curContent = data.chunks.map(c => (c.section ? '## ' + c.section + '\n\n' : '') + c.content).join('\n\n---\n\n');
    cEl.textContent = curContent;
  } catch(e) { tEl.textContent = 'Error'; cEl.textContent = e.message; }
}

// ── Memory ──
let memPage = 0, memTotal = 0, memLimit = 30;
let memFactsTimer = null, memStatsTimer = null, latestFactId = 0, latestObsId = 0;

async function loadCollections() {
  try {
    const data = await (await fetch('/api/cognition/collections')).json();
    if (!data || data.error) return;
    ['mem-col-filter', 'evt-col-filter'].forEach(id => {
      const sel = document.getElementById(id);
      if (!sel) return;
      const cur = sel.value;
      sel.innerHTML = '<option value="">All Projects</option>';
      data.forEach(c => {
        const o = document.createElement('option');
        o.value = c.name;
        o.textContent = c.name + ' (' + c.count + ')';
        if (c.name === cur) o.selected = true;
        sel.appendChild(o);
      });
    });
  } catch(e) { /* silent */ }
}

async function checkMemAlert() {
  const el = document.getElementById('mem-alert');
  if (!el) return;
  try {
    // 并行检查 Hooks + Observer（#248: 两者都影响记忆质量）
    const [status, obsData] = await Promise.all([
      fetch('/api/status').then(r => r.json()).catch(() => null),
      fetch('/api/cognition/observer').then(r => r.json()).catch(() => null),
    ]);
    const alerts = [];
    // Hooks 检查 — 断连 = 不记忆了
    if (status && status.last_event) {
      const ago = (Date.now() - new Date(status.last_event + 'Z').getTime()) / 60000;
      if (ago > 30) alerts.push('Hooks disconnected \u2014 new memories won\'t be recorded');
    } else if (status && !status.last_event) {
      alerts.push('No hook events found \u2014 run <code>index1 setup</code> to configure');
    }
    // Observer 检查 — critical = 观察能力丢失（#248 宪法第6条）
    if (obsData && obsData.health && obsData.health.status === 'critical') {
      alerts.push('Observer paused \u2014 observations won\'t be recorded. Check Status tab');
    }
    if (alerts.length) {
      el.innerHTML = alerts.join('<br>');
      el.style.display = 'block';
    } else {
      el.style.display = 'none';
    }
  } catch(e) { el.style.display = 'none'; }
}

async function loadMemStats() {
  const el = document.getElementById('mem-stats');
  try {
    const memCol = document.getElementById('mem-col-filter').value;
    let statsUrl = '/api/cognition/stats';
    if (memCol) statsUrl += '?collection=' + encodeURIComponent(memCol);
    const data = await (await fetch(statsUrl)).json();
    if (data.error) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(data.error) + '</div>'; return; }
    const cats = data.facts_by_category || {};
    const topCats = Object.entries(cats).sort((a,b) => b[1]-a[1]).slice(0, 6);
    el.innerHTML =
      '<div class="mem-stat"><div class="label">Total Facts</div><div class="value">' + (data.facts_active||0) + '</div></div>' +
      '<div class="mem-stat"><div class="label">Models</div><div class="value">' + (data.mental_models||0) + '</div></div>' +
      topCats.map(([cat, cnt]) =>
        '<div class="mem-stat"><div class="label">' + esc(cat) + '</div><div class="value">' + cnt + '</div></div>'
      ).join('');
    // session filter removed — replaced by time range filter
  } catch(e) { el.innerHTML = ''; }
}

// ── Memory Search ──
let memSearchTimer = null;
const memSearchInput = document.getElementById('mem-search');
const memSClear = document.getElementById('mem-s-clear');
let memSearchMode = false;

memSearchInput.addEventListener('input', () => {
  clearTimeout(memSearchTimer);
  memSClear.classList.toggle('show', memSearchInput.value.length > 0);
  memSearchTimer = setTimeout(() => {
    if (memSearchInput.value.trim()) searchMemory();
    else { memSearchMode = false; loadFacts(); document.getElementById('mem-search-meta').style.display = 'none'; }
  }, 300);
});
memSearchInput.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') { clearTimeout(memSearchTimer); if (memSearchInput.value.trim()) searchMemory(); }
  if (e.key === 'Escape') clearMemSearch();
});

function clearMemSearch() {
  memSearchInput.value = '';
  memSClear.classList.remove('show');
  memSearchMode = false;
  document.getElementById('mem-search-meta').style.display = 'none';
  loadFacts();
}

function onMemFilterChange() {
  if (memSearchMode) searchMemory();
  else { loadFacts(); loadMemStats(); }
}

async function searchMemory() {
  const q = memSearchInput.value.trim();
  if (!q) { memSearchMode = false; loadFacts(); return; }
  memSearchMode = true;
  const el = document.getElementById('mem-facts');
  const metaEl = document.getElementById('mem-search-meta');
  el.innerHTML = '<div class="empty"><span class="spinner"></span> Searching...</div>';
  metaEl.style.display = 'none';
  document.getElementById('mem-pager').innerHTML = '';

  const memCol = document.getElementById('mem-col-filter').value;
  let url = '/api/cognition/search?q=' + encodeURIComponent(q) + '&top_k=20';
  if (memCol) url += '&collection=' + encodeURIComponent(memCol);

  try {
    const data = await (await fetch(url)).json();
    if (data.error) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(data.error) + '</div>'; return; }
    const facts = data.facts || [];
    if (!facts.length) { el.innerHTML = '<div class="empty">No results for "' + esc(q) + '"</div>'; metaEl.style.display = 'none'; return; }
    const mc = data.mode === 'hybrid' ? 'badge-hybrid' : 'badge-bm25';
    metaEl.innerHTML = '<span>' + data.total + ' results &middot; ' + Math.round(data.query_ms) + 'ms</span><span class="badge ' + mc + '">' + esc(data.mode) + '</span>';
    metaEl.style.display = 'flex';
    el.innerHTML = facts.map((f, i) => renderSearchFactCard(f, i, q)).join('');
    el.querySelectorAll('.fact-card').forEach(c => {
      c.addEventListener('click', () => toggleFact(c));
    });
  } catch(e) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

function renderSearchFactCard(f, i, q) {
  const assertion = f.assertion || '';
  const col = f.collection || '';
  const ago = relTime(f.created_at);
  const cat = f.category || 'general';
  const conf = f.confidence || 0;
  const confCls = conf >= 0.8 ? 'conf-high' : conf >= 0.5 ? 'conf-mid' : 'conf-low';
  const score = f.score != null ? f.score.toFixed(3) : '';
  const edges = f.context_json?.edge_count || 0;
  return '<div class="fact-card" data-fact-id="' + f.id + '" data-category="' + esc(cat) + '" style="animation-delay:' + (i*20) + 'ms">' +
    '<div class="fact-row">' +
      '<span class="fact-dot"></span>' +
      '<span class="fact-title">' + hl(assertion, q) + '</span>' +
      (edges > 0 ? '<span class="fact-links">' + edges + ' links</span>' : '') +
      (score ? '<span class="r-score">' + score + '</span>' : '') +
      '<span class="fact-conf-inline"><span class="conf-fill ' + confCls + '" style="width:' + (conf*100) + '%"></span></span>' +
      (col ? '<span class="fact-col">' + esc(col) + '</span>' : '') +
      '<span class="fact-ago">' + esc(ago) + '</span>' +
    '</div>' +
    '<div class="fact-detail" id="fd-' + f.id + '"></div>' +
  '</div>';
}

async function loadFacts() {
  const el = document.getElementById('mem-facts');
  el.innerHTML = '<div class="empty"><span class="spinner"></span> Loading...</div>';
  const tr = document.getElementById('mem-time-filter').value;
  const memCol = document.getElementById('mem-col-filter').value;
  let url = '/api/cognition/facts?limit=' + memLimit + '&offset=' + (memPage * memLimit);
  if (tr) url += '&time_range=' + encodeURIComponent(tr);
  if (memCol) url += '&collection=' + encodeURIComponent(memCol);
  try {
    // 并行加载 facts + 用户对话 + observations (#241)
    let promptsUrl = '/api/cognition/events?hook_event=UserPromptSubmit&include_payload=1&limit=30';
    if (memCol) promptsUrl += '&collection=' + encodeURIComponent(memCol);
    if (tr) promptsUrl += '&time_range=' + encodeURIComponent(tr);
    let obsUrl = '/api/cognition/observations?limit=50';
    if (memCol) obsUrl += '&collection=' + encodeURIComponent(memCol);
    if (tr) obsUrl += '&time_range=' + encodeURIComponent(tr);
    const [factsData, promptsData, obsData] = await Promise.all([
      fetch(url).then(r => r.json()),
      fetch(promptsUrl).then(r => r.json()).catch(() => ({events:[]})),
      fetch(obsUrl).then(r => r.json()).catch(() => ({observations:[]}))
    ]);
    if (factsData.error) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(factsData.error) + '</div>'; return; }
    memTotal = factsData.total || 0;
    const facts = factsData.facts || [];
    const rawPrompts = (promptsData.events || []).map(e => {
      let text = e.summary || '';
      try {
        if (e.payload) {
          const p = typeof e.payload === 'string' ? JSON.parse(e.payload) : e.payload;
          if (p.prompt) text = p.prompt;
        }
      } catch(_) {}
      return { _type: 'prompt', _sort_time: e.timestamp, event_id: e.event_id, text: text, timestamp: e.timestamp, session_id: e.session_id };
    });
    // deduplicate consecutive identical prompts
    const prompts = [];
    let lastText = '';
    for (const p of rawPrompts) {
      if (p.text === lastText) continue;
      lastText = p.text;
      prompts.push(p);
    }
    // observations (#241) — 收集已关联 observation 的 fact ids，避免重复显示
    const observations = (obsData.observations || []).map(o => ({
      _type: 'observation', _sort_time: o.timestamp || '', ...o
    }));
    const obsFactIds = new Set();
    observations.forEach(o => (o.facts || []).forEach(f => obsFactIds.add(f.id)));
    // 合并 facts（排除已被 observation 关联的）+ prompts + observations 为时间线
    // 统一时间格式：facts 用 "YYYY-MM-DD HH:MM:SS"，events 用 ISO "YYYY-MM-DDTHH:MM:SS"
    // 空格 vs T 会导致字符串排序错误，统一替换为 T 后比较
    const timeline = [
      ...facts.filter(f => !obsFactIds.has(f.id)).map(f => ({ _type: 'fact', _sort_time: (f.created_at||'').replace(' ','T'), ...f })),
      ...prompts,
      ...observations
    ].sort((a, b) => (b._sort_time || '').localeCompare(a._sort_time || ''));
    if (!timeline.length) { el.innerHTML = '<div class="empty">No facts found</div>'; latestFactId = 0; renderMemPager(); return; }
    // render with time group headers
    let lastGroup = '';
    el.innerHTML = timeline.map((item, i) => {
      const g = timeGroup(item._sort_time);
      let hdr = '';
      if (g !== lastGroup) { lastGroup = g; hdr = '<div class="time-group">' + esc(g) + '</div>'; }
      return hdr + (item._type === 'prompt' ? renderPromptBubble(item, i) : item._type === 'observation' ? renderObservationCard(item, i) : renderFactCard(item, i));
    }).join('');
    // track latest ids for incremental polling
    if (facts.length) latestFactId = Math.max(...facts.map(f => f.id));
    if (observations.length) latestObsId = Math.max(...observations.map(o => o.id));
    // bind click events for fact cards (collapsible)
    el.querySelectorAll('.fact-card').forEach(c => {
      c.addEventListener('click', () => toggleFact(c));
    });
    el.querySelectorAll('.obs-card').forEach(c => {
      c.addEventListener('click', () => c.classList.toggle('expanded'));
    });
    el.querySelectorAll('.q-bubble').forEach(b => {
      b.addEventListener('click', () => b.classList.toggle('expanded'));
    });
    renderMemPager();
  } catch(e) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

// #241 — observation type → icon
const _OBS_TYPE_ICON = {bugfix:'\u{1F534}',feature:'\u{1F7E3}',refactor:'\u{1F504}',change:'\u2705',discovery:'\u{1F535}',decision:'\u2696\uFE0F'};
function renderObservationCard(o, i) {
  const ago = relTime(o.timestamp);
  const icon = _OBS_TYPE_ICON[o.obs_type] || '\u{1F7E2}';
  const files = Array.isArray(o.files) ? o.files : [];
  const fileBadge = files.length ? files.map(f => f.split('/').pop()).join(', ') : '';
  const facts = o.facts || [];
  let factsHtml = '';
  if (facts.length) {
    factsHtml = '<div class="obs-facts">' + facts.map(f =>
      '<div class="obs-fact-item"><span class="obs-fact-dot"></span>' + esc(f.assertion || '') + '</div>'
    ).join('') + '</div>';
  }
  return '<div class="obs-card" style="animation-delay:' + (i*20) + 'ms">' +
    '<div class="obs-header">' +
      '<span class="obs-icon">' + icon + '</span>' +
      '<span class="obs-title">' + esc(o.title || '') + '</span>' +
      '<span class="obs-meta">' + esc(o.obs_type || '') +
        (fileBadge ? ' \u00B7 ' + esc(fileBadge) : '') +
        ' \u00B7 ' + esc(ago) +
      '</span>' +
    '</div>' +
    (o.narrative ? '<div class="obs-narrative">' + esc(o.narrative) + '</div>' : '') +
    factsHtml +
  '</div>';
}

function renderPromptBubble(p, i) {
  const ago = relTime(p.timestamp);
  const col = p.collection || '';
  return '<div class="q-bubble" style="animation-delay:' + (i*20) + 'ms">' +
    '<div class="q-label"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg> User' +
      (col ? '<span style="margin-left:auto;font:10px var(--mono);color:var(--text-3);font-weight:400">' + esc(col) + '</span>' : '') +
      '<span style="font:11px var(--mono);color:var(--text-3);font-weight:400">' + esc(ago) + '</span>' +
    '</div>' +
    '<div class="q-text">' + esc(p.text) + '</div>' +
  '</div>';
}

async function pollNewFacts() {
  if (memPage !== 0 || latestFactId === 0) return;  // 只在第一页增量
  const tr = document.getElementById('mem-time-filter').value;
  const memCol = document.getElementById('mem-col-filter').value;
  let url = '/api/cognition/facts?since_id=' + latestFactId + '&limit=50';
  if (tr) url += '&time_range=' + encodeURIComponent(tr);
  if (memCol) url += '&collection=' + encodeURIComponent(memCol);
  // #241: 并行轮询 facts + observations
  let obsUrl = '/api/cognition/observations?since_id=' + latestObsId + '&limit=20';
  if (memCol) obsUrl += '&collection=' + encodeURIComponent(memCol);
  if (tr) obsUrl += '&time_range=' + encodeURIComponent(tr);
  try {
    const [data, obsData] = await Promise.all([
      fetch(url).then(r => r.json()),
      latestObsId > 0
        ? fetch(obsUrl).then(r => r.json()).catch(() => ({observations:[]}))
        : Promise.resolve({observations:[]})
    ]);
    const el = document.getElementById('mem-facts');
    // 新 observations prepend
    const newObs = (obsData.observations || []).sort((a, b) => a.id - b.id);
    if (newObs.length) {
      // 收集新 observation 关联的 fact ids（避免下面 facts 重复显示）
      const newObsFactIds = new Set();
      newObs.forEach(o => (o.facts || []).forEach(f => newObsFactIds.add(f.id)));
      newObs.forEach(o => {
        o._type = 'observation';
        const html = renderObservationCard(o, 0);
        const tmp = document.createElement('div');
        tmp.innerHTML = html;
        const card = tmp.firstElementChild;
        card.classList.add('fact-new');
        card.addEventListener('click', () => card.classList.toggle('expanded'));
        el.prepend(card);
        setTimeout(() => card.classList.remove('fact-new'), 1500);
      });
      latestObsId = Math.max(latestObsId, ...newObs.map(o => o.id));
      // 从 facts 结果中排除已被 observation 关联的
      if (data.facts) {
        data.facts = data.facts.filter(f => !newObsFactIds.has(f.id));
      }
    }
    // 新 facts prepend
    if (data.facts && data.facts.length) {
      const sorted = data.facts.sort((a, b) => a.id - b.id);
      sorted.forEach(f => {
        const html = renderFactCard(f, 0);
        const tmp = document.createElement('div');
        tmp.innerHTML = html;
        const card = tmp.firstElementChild;
        card.classList.add('fact-new');
        card.addEventListener('click', () => toggleFact(card));
        el.prepend(card);
        setTimeout(() => card.classList.remove('fact-new'), 1500);
      });
      latestFactId = Math.max(latestFactId, ...data.facts.map(f => f.id));
    }
    // 有新记忆 → logo 脉冲动画 3s
    if (newObs.length || (data.facts && data.facts.length)) {
      activateLogo(true);
      setTimeout(() => activateLogo(false), 3000);
    }
    // 更新 total
    if (data.total) { memTotal = data.total; renderMemPager(); }
  } catch(e) { /* 静默失败，下次重试 */ }
}

function timeGroup(ts) {
  if (!ts) return '';
  const norm = ts.replace(' ', 'T');
  const d = new Date(norm.endsWith('Z') ? norm : norm + 'Z');
  if (isNaN(d.getTime())) return '';
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1);
  const target = new Date(d.getFullYear(), d.getMonth(), d.getDate());
  if (target.getTime() === today.getTime()) return 'Today';
  if (target.getTime() === yesterday.getTime()) return 'Yesterday';
  return String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
}

function relTime(ts) {
  if (!ts) return '';
  const norm = ts.replace(' ', 'T');
  const d = new Date(norm.endsWith('Z') ? norm : norm + 'Z');
  const sec = Math.floor((Date.now() - d.getTime()) / 1000);
  if (sec < 60) return sec + '秒';
  const min = Math.floor(sec / 60);
  if (min < 60) return min + '分';
  const hr = Math.floor(min / 60);
  if (hr < 24) return hr + '小时';
  const day = Math.floor(hr / 24);
  return day + '天';
}

function renderFactCard(f, i) {
  const assertion = f.assertion || '';
  const col = f.collection || '';
  const ago = relTime(f.created_at);
  const cat = f.category || 'general';
  const conf = f.confidence || 0;
  const confCls = conf >= 0.8 ? 'conf-high' : conf >= 0.5 ? 'conf-mid' : 'conf-low';
  // edge count from context_json if available
  const edges = f.context_json?.edge_count || 0;

  return '<div class="fact-card" data-fact-id="' + f.id + '" data-category="' + esc(cat) + '" style="animation-delay:' + (i*20) + 'ms">' +
    '<div class="fact-row">' +
      '<span class="fact-dot"></span>' +
      '<span class="fact-title">' + esc(assertion) + '</span>' +
      (edges > 0 ? '<span class="fact-links">' + edges + ' links</span>' : '') +
      '<span class="fact-conf-inline"><span class="conf-fill ' + confCls + '" style="width:' + (conf*100) + '%"></span></span>' +
      (col ? '<span class="fact-col">' + esc(col) + '</span>' : '') +
      '<span class="fact-ago">' + esc(ago) + '</span>' +
    '</div>' +
    '<div class="fact-detail" id="fd-' + f.id + '"></div>' +
  '</div>';
}

async function toggleFact(card) {
  if (card.classList.contains('expanded')) {
    card.classList.remove('expanded');
    return;
  }
  // collapse others
  document.querySelectorAll('.fact-card.expanded').forEach(c => c.classList.remove('expanded'));
  card.classList.add('expanded');
  const factId = parseInt(card.dataset.factId);
  const detailEl = card.querySelector('.fact-detail');
  if (detailEl.dataset.loaded) return;
  detailEl.innerHTML = '<span class="spinner"></span>';
  try {
    const f = await (await fetch('/api/cognition/fact/' + factId)).json();
    if (f.error) { detailEl.innerHTML = '<div style="color:var(--red)">' + esc(f.error) + '</div>'; return; }
    let html = '<div class="fact-detail-text">' + esc(f.assertion || '') + '</div>';
    // context_json
    if (f.context_json && typeof f.context_json === 'object' && Object.keys(f.context_json).length) {
      const ctx = f.context_json;
      const tags = [];
      if (ctx.file) tags.push(ctx.file);
      if (ctx.function) tags.push(ctx.function);
      if (ctx.action) tags.push(ctx.action);
      if (ctx.tags && Array.isArray(ctx.tags)) tags.push(...ctx.tags);
      if (tags.length) {
        html += '<div class="fact-detail-meta">' + tags.map(t => '<span class="fact-detail-tag">' + esc(String(t)) + '</span>').join('') + '</div>';
      }
      // show remaining context as text
      const extra = Object.entries(ctx).filter(([k]) => !['file','function','action','tags','previous_assertion'].includes(k));
      if (extra.length) {
        html += '<div class="fact-detail-text" style="font-size:12px;color:var(--text-3)">' + extra.map(([k,v]) => esc(k) + ': ' + esc(JSON.stringify(v))).join('\\n') + '</div>';
      }
    }
    // scope files
    const scopes = [];
    if (f.scope_files && Array.isArray(f.scope_files)) scopes.push(...f.scope_files.map(s => s.split('/').pop()));
    if (f.scope_modules && Array.isArray(f.scope_modules)) scopes.push(...f.scope_modules);
    if (scopes.length) {
      html += '<div class="fact-detail-meta">' + scopes.slice(0,5).map(s => '<span class="fact-detail-tag">' + esc(s) + '</span>').join('') + '</div>';
    }
    // confidence bar
    const conf = f.confidence || 0;
    const confCls = conf >= 0.8 ? 'conf-high' : conf >= 0.5 ? 'conf-mid' : 'conf-low';
    html += '<div class="conf-bar" title="confidence: ' + conf.toFixed(2) + '" style="margin:8px 0"><div class="conf-fill ' + confCls + '" style="width:' + (conf*100) + '%"></div></div>';
    html += '<button class="fact-collapse-btn" onclick="event.stopPropagation();this.closest(\'.fact-card\').classList.remove(\'expanded\')">收起 ▲</button>';
    detailEl.innerHTML = html;
    detailEl.dataset.loaded = '1';
  } catch(e) { detailEl.innerHTML = '<div style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

function renderMemPager() {
  const el = document.getElementById('mem-pager');
  const pages = Math.ceil(memTotal / memLimit);
  if (pages <= 1) { el.innerHTML = '<span class="page-info">' + memTotal + ' facts</span>'; return; }
  el.innerHTML =
    '<button onclick="memPage=Math.max(0,memPage-1);loadFacts()"' + (memPage===0?' disabled':'') + '>&lsaquo; Prev</button>' +
    '<span class="page-info">' + (memPage+1) + ' / ' + pages + ' (' + memTotal + ')</span>' +
    '<button onclick="memPage=Math.min(' + (pages-1) + ',memPage+1);loadFacts()"' + (memPage>=pages-1?' disabled':'') + '>Next &rsaquo;</button>';
}

async function showFact(factId) {
  const modal = document.getElementById('chunk-modal');
  const tEl = document.getElementById('m-title');
  const mEl = document.getElementById('m-meta');
  const cEl = document.getElementById('m-content');
  curContent = ''; tEl.textContent = 'Loading...'; mEl.innerHTML = ''; cEl.textContent = '';
  modal.classList.add('open');
  document.getElementById('copy-btn').classList.remove('copied');
  document.getElementById('copy-btn').querySelector('span').textContent = 'Copy';
  try {
    const f = await (await fetch('/api/cognition/fact/' + factId)).json();
    if (f.error) { tEl.textContent = 'Error'; cEl.textContent = f.error; return; }
    curContent = f.assertion || '';
    tEl.textContent = 'Fact #' + f.id + ' — ' + (f.category || '?');
    mEl.innerHTML = [
      { l: 'Category', v: f.category || '-' },
      { l: 'Confidence', v: (f.confidence || 0).toFixed(2) },
      { l: 'Source', v: f.source || '-' },
      { l: 'Layer', v: f.layer || '-' },
      { l: 'Collection', v: f.collection || '-' },
      { l: 'Access', v: f.access_count || 0 },
    ].map(m => '<div class="m-meta-item"><div class="label">' + m.l + '</div><div class="value">' + esc(String(m.v)) + '</div></div>').join('');

    let content = f.assertion || '';
    if (f.context_json && typeof f.context_json === 'object') {
      content += '\n\n--- context_json ---\n' + JSON.stringify(f.context_json, null, 2);
    }
    if (f.scope_files && f.scope_files.length) content += '\n\n--- scope_files ---\n' + f.scope_files.join('\n');
    if (f.scope_modules && f.scope_modules.length) content += '\n\n--- scope_modules ---\n' + f.scope_modules.join('\n');
    if (f.edges && f.edges.length) {
      content += '\n\n--- fact_edges ---\n' + f.edges.map(e => e.relation + ' → #' + e.target_fact_id + ' (conf: ' + e.confidence + ')').join('\n');
    }
    curContent = content;
    cEl.textContent = content;
  } catch(e) { tEl.textContent = 'Error'; cEl.textContent = e.message; }
}

function startAutoRefresh() {
  stopAutoRefresh();
  memFactsTimer = setInterval(pollNewFacts, 3000);   // 增量 facts 3s
  memStatsTimer = setInterval(() => { loadMemStats(); checkMemAlert(); }, 10000);   // stats+alert 10s
}
function stopAutoRefresh() {
  if (memFactsTimer) { clearInterval(memFactsTimer); memFactsTimer = null; }
  if (memStatsTimer) { clearInterval(memStatsTimer); memStatsTimer = null; }
}
function toggleAutoRefresh() {
  if (document.getElementById('mem-auto-refresh').checked) {
    startAutoRefresh();
  } else {
    stopAutoRefresh();
  }
}

// ── Events ──
let evtTimer = null, latestEvtId = 0;
const TOOL_CLASSES = {Bash:'t-bash',Edit:'t-edit',Write:'t-write',Read:'t-read',Grep:'t-grep',Glob:'t-glob'};

async function loadEvents() {
  const el = document.getElementById('evt-list');
  el.innerHTML = '<div class="empty"><span class="spinner"></span> Loading events...</div>';
  const tr = document.getElementById('evt-time-filter').value;
  const evtCol = document.getElementById('evt-col-filter').value;
  let url = '/api/cognition/events?limit=50';
  if (tr) url += '&time_range=' + encodeURIComponent(tr);
  if (evtCol) url += '&collection=' + encodeURIComponent(evtCol);
  try {
    const data = await (await fetch(url)).json();
    if (data.error) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(data.error) + '</div>'; return; }
    document.getElementById('evt-total').textContent = (data.total||0) + ' events';
    if (!data.events || !data.events.length) { el.innerHTML = '<div class="empty">No events yet</div>'; latestEvtId = 0; return; }
    el.innerHTML = data.events.map((e, i) => renderEvtCard(e, i)).join('');
    latestEvtId = Math.max(...data.events.map(e => e.event_id));
  } catch(e) { el.innerHTML = '<div class="empty" style="color:var(--red)">' + esc(e.message) + '</div>'; }
}

async function pollNewEvents() {
  if (latestEvtId === 0) return;
  const tr = document.getElementById('evt-time-filter').value;
  const evtCol = document.getElementById('evt-col-filter').value;
  let url = '/api/cognition/events?since_id=' + latestEvtId + '&limit=50';
  if (tr) url += '&time_range=' + encodeURIComponent(tr);
  if (evtCol) url += '&collection=' + encodeURIComponent(evtCol);
  try {
    const data = await (await fetch(url)).json();
    if (!data.events || !data.events.length) return;
    const el = document.getElementById('evt-list');
    const sorted = data.events.sort((a, b) => a.event_id - b.event_id);
    sorted.forEach(e => {
      const html = renderEvtCard(e, 0);
      const tmp = document.createElement('div');
      tmp.innerHTML = html;
      const card = tmp.firstElementChild;
      card.classList.add('evt-new');
      el.prepend(card);
      setTimeout(() => card.classList.remove('evt-new'), 1500);
    });
    latestEvtId = Math.max(latestEvtId, ...data.events.map(e => e.event_id));
    if (data.total) document.getElementById('evt-total').textContent = data.total + ' events';
  } catch(e) { /* silent */ }
}

function renderEvtCard(e, i) {
  const isPrompt = e.hook_event === 'UserPromptSubmit' && !e.tool_name;
  const tool = isPrompt ? 'User' : (e.tool_name || e.hook_event || '?');
  const toolCls = isPrompt ? 't-user' : (TOOL_CLASSES[tool] || '');
  const imp = (e.importance||0) >= 0.7 ? 'imp-high' : (e.importance||0) >= 0.3 ? 'imp-mid' : 'imp-low';
  const t = localTime(e.timestamp);
  const summary = e.summary || '';
  const col = e.collection || '';
  return '<div class="evt-card' + (isPrompt ? ' evt-prompt' : '') + '" style="animation-delay:' + (i*15) + 'ms">' +
    '<div class="evt-top">' +
      '<span class="evt-importance ' + imp + '" title="importance: ' + (e.importance||0).toFixed(2) + '"></span>' +
      '<span class="evt-tool ' + toolCls + '">' + esc(tool) + '</span>' +
      (col ? '<span class="evt-collection">' + esc(col) + '</span>' : '') +
      '<span class="evt-time">' + esc(t) + '</span>' +
    '</div>' +
    (summary ? '<div class="evt-summary">' + esc(summary) + '</div>' : '') +
  '</div>';
}

// populateEvtSessions removed — replaced by time range filter

function startEvtAutoRefresh() { stopEvtAutoRefresh(); evtTimer = setInterval(pollNewEvents, 3000); }
function stopEvtAutoRefresh() { if (evtTimer) { clearInterval(evtTimer); evtTimer = null; } }
function toggleEvtAutoRefresh() {
  if (document.getElementById('evt-auto-refresh').checked) startEvtAutoRefresh();
  else stopEvtAutoRefresh();
}

// ── Observer Health Indicator ──
async function loadObserverHealth() {
  const link = document.querySelector('.health-indicator');
  const dot = document.querySelector('.health-dot');
  if (!link || !dot) return;
  try {
    const r = await fetch('/api/cognition/observer');
    if (!r.ok) throw new Error('HTTP ' + r.status);
    const data = await r.json();
    const st = (data.health && data.health.status) || 'unknown';
    // 用户视角：critical = paused（红），其他 = running（绿）
    const isCritical = st === 'critical';
    dot.className = 'health-dot ' + (isCritical ? 'critical' : 'healthy');
    const provider = data.active_provider
      ? data.active_provider.name + ' (' + data.active_provider.model + ')'
      : 'No provider';
    link.title = 'Observer: ' + (isCritical ? 'Paused' : 'Running') + ' — ' + provider;
  } catch (e) {
    link.style.display = 'none';
  }
}

// ── Init ──
fetch('/api/status').then(r => r.json()).then(d => {
  if (d.collections) updCol(d.collections);
  if (d.doc_types) updTypes(d.doc_types);
}).catch(() => {});
loadProjectOverview();
loadObserverHealth();
// Memory is default tab — load on startup + start auto-refresh
loadCollections();  // populate mem-col-filter + evt-col-filter
loadMemStats(); loadFacts(); checkMemAlert();
startAutoRefresh();


