Newer
Older
navi-1 / debug / index.html
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 17 Apr 16 KB Add standalone debug page at /debug
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Navi — Debug</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    :root {
      --bg:       #0e0e0e;
      --bg2:      #161616;
      --bg3:      #1e1e1e;
      --border:   #252525;
      --border2:  #2e2e2e;
      --text:     #d4d4d4;
      --text2:    #888;
      --text3:    #555;
      --accent:   #4ec9b0;

      --c-system:    #4ec9b0;
      --c-user:      #569cd6;
      --c-assistant: #c586c0;
      --c-tool:      #ce9178;

      --bg-system:    #1e3a2f;
      --bg-user:      #1e2d40;
      --bg-assistant: #2d1e3a;
      --bg-tool:      #3a2d1e;
    }

    html, body { height: 100%; }

    body {
      font-family: ui-monospace, "Cascadia Code", "Fira Code", "JetBrains Mono", monospace;
      font-size: 13px;
      background: var(--bg);
      color: var(--text);
      display: flex;
      flex-direction: column;
      height: 100vh;
      overflow: hidden;
    }

    /* ── Header ── */
    header {
      flex-shrink: 0;
      background: var(--bg2);
      border-bottom: 1px solid var(--border);
      padding: 8px 14px;
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
    }

    .logo { font-size: 11px; color: var(--text3); letter-spacing: .1em; text-transform: uppercase; white-space: nowrap; }
    .logo b { color: var(--accent); }

    .spacer { flex: 1; }

    select, button, input {
      font-family: inherit;
      font-size: 12px;
      background: var(--bg3);
      border: 1px solid var(--border2);
      color: var(--text);
      border-radius: 4px;
      outline: none;
    }
    select { padding: 4px 8px; cursor: pointer; max-width: 320px; }
    select:focus { border-color: #555; }
    input { padding: 4px 8px; width: 280px; }
    input:focus { border-color: #555; }
    button { padding: 4px 10px; cursor: pointer; }
    button:hover { background: #2a2a2a; border-color: #555; }
    button.active { background: #1e3a2f; border-color: var(--accent); color: var(--accent); }

    #stats {
      font-size: 11px;
      color: var(--text3);
      white-space: nowrap;
    }
    #stats b { color: var(--text2); }

    /* ── Layout ── */
    .layout {
      display: flex;
      flex: 1;
      overflow: hidden;
    }

    /* ── Sidebar ── */
    aside {
      width: 230px;
      flex-shrink: 0;
      background: var(--bg2);
      border-right: 1px solid var(--border);
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }

    .aside-title {
      font-size: 10px;
      color: var(--text3);
      text-transform: uppercase;
      letter-spacing: .08em;
      padding: 8px 10px 6px;
      border-bottom: 1px solid var(--border);
      flex-shrink: 0;
    }

    #session-list {
      flex: 1;
      overflow-y: auto;
    }

    .session-item {
      padding: 7px 10px;
      cursor: pointer;
      border-bottom: 1px solid var(--border);
      transition: background .1s;
    }
    .session-item:hover { background: var(--bg3); }
    .session-item.selected { background: #1a1a2a; border-left: 2px solid var(--c-user); padding-left: 8px; }

    .si-name {
      font-size: 11px;
      color: var(--text);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .si-meta {
      font-size: 10px;
      color: var(--text3);
      margin-top: 2px;
      display: flex;
      gap: 6px;
    }
    .si-profile { color: var(--text2); }
    .si-pin { color: var(--accent); }

    /* ── Main content ── */
    main {
      flex: 1;
      overflow-y: auto;
      padding: 14px 16px;
    }

    #placeholder {
      text-align: center;
      color: var(--text3);
      padding: 80px 0;
      font-size: 12px;
    }

    #error-msg {
      color: #f48771;
      padding: 20px;
      text-align: center;
    }

    /* ── Message card ── */
    .msg {
      display: grid;
      grid-template-columns: 86px 1fr;
      margin-bottom: 10px;
      border: 1px solid var(--border);
      border-radius: 6px;
      overflow: hidden;
    }

    .msg-role {
      background: var(--bg2);
      border-right: 1px solid var(--border);
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 8px 5px;
      gap: 4px;
      text-align: center;
    }

    .role-badge {
      font-size: 9px;
      font-weight: 700;
      letter-spacing: .05em;
      text-transform: uppercase;
      padding: 2px 6px;
      border-radius: 3px;
    }
    .role-system    { background: var(--bg-system);    color: var(--c-system); }
    .role-user      { background: var(--bg-user);      color: var(--c-user); }
    .role-assistant { background: var(--bg-assistant); color: var(--c-assistant); }
    .role-tool      { background: var(--bg-tool);      color: var(--c-tool); }

    .tag {
      font-size: 9px;
      padding: 1px 5px;
      border-radius: 3px;
    }
    .tag-summary { background: #3a3a1e; color: #dcdcaa; }
    .tag-plan    { background: #1e2d1e; color: #6a9955; }
    .tag-images  { background: #1e3a3a; color: var(--c-system); }
    .tag-tools   { background: #3a1e2d; color: #f48771; }
    .tag-think   { background: #2d2d1e; color: #dcdcaa; }

    .msg-idx { font-size: 10px; color: var(--text3); }

    /* ── Message body ── */
    .msg-body { padding: 8px 12px; overflow: hidden; min-width: 0; }

    .section-label {
      font-size: 10px;
      color: var(--text3);
      text-transform: uppercase;
      letter-spacing: .06em;
      margin: 8px 0 4px;
    }

    pre.content {
      white-space: pre-wrap;
      word-break: break-word;
      line-height: 1.55;
      color: var(--text);
      font-family: inherit;
      font-size: 12px;
    }
    pre.content.dim { color: var(--text2); }
    pre.content.thinking-text { color: #dcdcaa; background: #1e1e0e; padding: 6px 8px; border-radius: 4px; border-left: 2px solid #5a5a20; }

    .tool-call-block {
      background: var(--bg2);
      border: 1px solid var(--border2);
      border-radius: 4px;
      margin-bottom: 6px;
      overflow: hidden;
    }
    .tool-call-name {
      background: #222;
      color: var(--c-tool);
      padding: 2px 8px;
      font-size: 11px;
      font-weight: 600;
    }
    .tool-call-args {
      padding: 5px 8px;
      white-space: pre-wrap;
      word-break: break-word;
      color: #9cdcfe;
      font-size: 11px;
      font-family: inherit;
    }

    .tool-id { font-size: 10px; color: var(--text3); margin-top: 4px; }

    .img-strip { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
    .img-thumb { width: 72px; height: 72px; object-fit: cover; border-radius: 3px; border: 1px solid var(--border2); cursor: pointer; }

    .char-count { font-size: 10px; color: var(--text3); margin-top: 6px; }

    /* ── Scrollbar ── */
    ::-webkit-scrollbar { width: 6px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
    ::-webkit-scrollbar-thumb:hover { background: #444; }
  </style>
</head>
<body>

<header>
  <div class="logo"><b>navi</b> debug</div>
  <input id="id-input" type="text" placeholder="session id…" spellcheck="false">
  <button id="btn-load">Load</button>
  <button id="btn-refresh" title="Reload context">↺</button>
  <button id="btn-auto" title="Auto-refresh every 3s">Auto</button>
  <div class="spacer"></div>
  <div id="stats"></div>
</header>

<div class="layout">
  <aside>
    <div class="aside-title">Sessions</div>
    <div id="session-list"><div style="padding:12px;color:#555;font-size:11px;">loading…</div></div>
  </aside>

  <main id="main">
    <div id="placeholder">Select a session or enter an ID above.</div>
    <div id="error-msg" hidden></div>
    <div id="messages"></div>
  </main>
</div>

<script>
// ── State ──────────────────────────────────────────────────────────────────
let currentId = null
let autoTimer = null

const idInput   = document.getElementById('id-input')
const btnLoad   = document.getElementById('btn-load')
const btnRefresh= document.getElementById('btn-refresh')
const btnAuto   = document.getElementById('btn-auto')
const statsEl   = document.getElementById('stats')
const msgsEl    = document.getElementById('messages')
const errEl     = document.getElementById('error-msg')
const phEl      = document.getElementById('placeholder')
const listEl    = document.getElementById('session-list')

// ── Init ───────────────────────────────────────────────────────────────────
if (location.hash) idInput.value = location.hash.slice(1)

loadSessions()
if (idInput.value) loadContext(idInput.value)

btnLoad.addEventListener('click', () => loadContext(idInput.value.trim()))
idInput.addEventListener('keydown', e => { if (e.key === 'Enter') loadContext(idInput.value.trim()) })
btnRefresh.addEventListener('click', () => { if (currentId) loadContext(currentId) })
btnAuto.addEventListener('click', toggleAuto)

// ── Session list ───────────────────────────────────────────────────────────
async function loadSessions() {
  try {
    const res = await fetch('/sessions')
    const sessions = await res.json()
    renderSessionList(sessions)
  } catch {
    listEl.innerHTML = '<div style="padding:10px;color:#555;font-size:11px;">failed to load</div>'
  }
}

function renderSessionList(sessions) {
  listEl.innerHTML = ''
  if (!sessions.length) {
    listEl.innerHTML = '<div style="padding:12px;color:#555;font-size:11px;">no sessions</div>'
    return
  }
  for (const s of sessions) {
    const el = document.createElement('div')
    el.className = 'session-item' + (s.session_id === currentId ? ' selected' : '')
    el.dataset.id = s.session_id

    const name = document.createElement('div')
    name.className = 'si-name'
    name.textContent = s.name || s.preview || s.session_id.slice(0, 12) + '…'
    name.title = s.session_id

    const meta = document.createElement('div')
    meta.className = 'si-meta'
    meta.innerHTML = `<span class="si-profile">${s.profile_id}</span><span>${s.message_count} msg</span>${s.pinned ? '<span class="si-pin">★</span>' : ''}`

    el.appendChild(name)
    el.appendChild(meta)
    el.addEventListener('click', () => loadContext(s.session_id))
    listEl.appendChild(el)
  }
}

// ── Context load ───────────────────────────────────────────────────────────
async function loadContext(id) {
  if (!id) return
  currentId = id
  idInput.value = id
  location.hash = id
  statsEl.textContent = 'loading…'
  msgsEl.innerHTML = ''
  errEl.hidden = true
  phEl.hidden = true

  // Highlight selected item
  document.querySelectorAll('.session-item').forEach(el => {
    el.classList.toggle('selected', el.dataset.id === id)
  })

  try {
    const res = await fetch(`/sessions/${encodeURIComponent(id)}/context`)
    if (!res.ok) {
      const j = await res.json().catch(() => ({}))
      throw new Error(j.detail || `HTTP ${res.status}`)
    }
    const data = await res.json()
    renderContext(data)
  } catch (e) {
    errEl.textContent = e.message
    errEl.hidden = false
    statsEl.textContent = ''
  }
}

// ── Render ─────────────────────────────────────────────────────────────────
function renderContext(data) {
  const estTokens = Math.round(data.total_chars / 4)
  statsEl.innerHTML =
    `<b>${data.profile_id}</b> · ` +
    `<b>${data.message_count}</b> msgs · ` +
    `<b>${data.total_chars.toLocaleString()}</b> chars · ` +
    `~<b>${estTokens.toLocaleString()}</b> tok`

  msgsEl.innerHTML = ''
  data.context.forEach((msg, idx) => msgsEl.appendChild(renderMessage(msg, idx)))
}

function renderMessage(msg, idx) {
  const el = document.createElement('div')
  el.className = 'msg'

  // ── Left: role column ──
  const roleCol = document.createElement('div')
  roleCol.className = 'msg-role'

  const badge = document.createElement('span')
  badge.className = `role-badge role-${msg.role}`
  badge.textContent = msg.role
  roleCol.appendChild(badge)

  const tags = []
  if (msg.is_summary)       tags.push(['summary', 'tag-summary'])
  if (msg.is_plan)          tags.push(['plan',    'tag-plan'])
  if (msg.thinking)         tags.push(['think',   'tag-think'])
  if (msg.images?.length)   tags.push([msg.images.length + ' img', 'tag-images'])
  if (msg.tool_calls?.length) tags.push([msg.tool_calls.length + ' call' + (msg.tool_calls.length > 1 ? 's' : ''), 'tag-tools'])

  for (const [label, cls] of tags) {
    const t = document.createElement('span')
    t.className = `tag ${cls}`
    t.textContent = label
    roleCol.appendChild(t)
  }

  const idxEl = document.createElement('span')
  idxEl.className = 'msg-idx'
  idxEl.textContent = `#${idx}`
  roleCol.appendChild(idxEl)

  // ── Right: body ──
  const body = document.createElement('div')
  body.className = 'msg-body'

  // Thinking block
  if (msg.thinking) {
    const lbl = label('thinking')
    body.appendChild(lbl)
    const pre = document.createElement('pre')
    pre.className = 'content thinking-text'
    pre.textContent = msg.thinking
    body.appendChild(pre)
  }

  // Main content
  if (msg.content) {
    if (msg.thinking) body.appendChild(label('response'))
    const pre = document.createElement('pre')
    pre.className = 'content' + (msg.role === 'tool' ? ' dim' : '')
    pre.textContent = msg.content
    body.appendChild(pre)
  }

  // Tool calls
  if (msg.tool_calls?.length) {
    body.appendChild(label('tool calls'))
    for (const tc of msg.tool_calls) {
      const block = document.createElement('div')
      block.className = 'tool-call-block'
      const name = document.createElement('div')
      name.className = 'tool-call-name'
      name.textContent = tc.name || tc.function?.name || '?'
      const args = document.createElement('pre')
      args.className = 'tool-call-args'
      const rawArgs = tc.arguments ?? tc.function?.arguments ?? {}
      args.textContent = JSON.stringify(rawArgs, null, 2)
      block.appendChild(name)
      block.appendChild(args)
      body.appendChild(block)
    }
  }

  // Tool call ID (tool result messages)
  if (msg.tool_call_id) {
    const tid = document.createElement('div')
    tid.className = 'tool-id'
    tid.textContent = `↳ call_id: ${msg.tool_call_id}${msg.name ? '  tool: ' + msg.name : ''}`
    body.appendChild(tid)
  }

  // Images
  if (msg.images?.length) {
    body.appendChild(label('images'))
    const strip = document.createElement('div')
    strip.className = 'img-strip'
    for (const b64 of msg.images) {
      const img = document.createElement('img')
      img.className = 'img-thumb'
      img.src = b64.startsWith('data:') ? b64 : `data:image/jpeg;base64,${b64}`
      img.addEventListener('click', () => window.open(img.src))
      strip.appendChild(img)
    }
    body.appendChild(strip)
  }

  // Char count
  const chars = (msg.content || '').length + (msg.thinking || '').length
  if (chars > 0) {
    const cc = document.createElement('div')
    cc.className = 'char-count'
    cc.textContent = `${chars.toLocaleString()} chars · ~${Math.round(chars / 4).toLocaleString()} tok`
    body.appendChild(cc)
  }

  el.appendChild(roleCol)
  el.appendChild(body)
  return el
}

function label(text) {
  const el = document.createElement('div')
  el.className = 'section-label'
  el.textContent = text
  return el
}

// ── Auto-refresh ───────────────────────────────────────────────────────────
function toggleAuto() {
  if (autoTimer) {
    clearInterval(autoTimer)
    autoTimer = null
    btnAuto.classList.remove('active')
    btnAuto.textContent = 'Auto'
  } else {
    btnAuto.classList.add('active')
    btnAuto.textContent = 'Auto ●'
    autoTimer = setInterval(() => {
      if (currentId) loadContext(currentId)
      loadSessions()
    }, 3000)
  }
}
</script>
</body>
</html>