Newer
Older
navi-1 / client / debug.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Navi — Context Debug</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      font-family: ui-monospace, "Cascadia Code", "Fira Code", monospace;
      font-size: 13px;
      background: #0e0e0e;
      color: #d4d4d4;
      min-height: 100vh;
    }

    /* ── Header ── */
    header {
      position: sticky;
      top: 0;
      z-index: 10;
      background: #161616;
      border-bottom: 1px solid #2a2a2a;
      padding: 10px 16px;
      display: flex;
      align-items: center;
      gap: 12px;
      flex-wrap: wrap;
    }

    header h1 { font-size: 13px; color: #888; font-weight: 400; white-space: nowrap; }

    #input-wrap { display: flex; gap: 6px; flex: 1; min-width: 260px; }

    #session-input {
      flex: 1;
      background: #1e1e1e;
      border: 1px solid #333;
      border-radius: 4px;
      color: #d4d4d4;
      font-family: inherit;
      font-size: 12px;
      padding: 4px 8px;
      outline: none;
    }
    #session-input:focus { border-color: #555; }

    button {
      background: #2a2a2a;
      border: 1px solid #444;
      border-radius: 4px;
      color: #d4d4d4;
      cursor: pointer;
      font-family: inherit;
      font-size: 12px;
      padding: 4px 10px;
    }
    button:hover { background: #333; }

    #stats {
      font-size: 11px;
      color: #666;
      white-space: nowrap;
    }
    #stats span { color: #999; }

    /* ── Main ── */
    main { max-width: 900px; margin: 0 auto; padding: 16px; }

    #placeholder {
      text-align: center;
      color: #444;
      padding: 60px 0;
    }

    /* ── Message card ── */
    .msg {
      display: grid;
      grid-template-columns: 90px 1fr;
      gap: 0 12px;
      margin-bottom: 12px;
      border: 1px solid #222;
      border-radius: 6px;
      overflow: hidden;
    }

    .msg-role {
      background: #1a1a1a;
      border-right: 1px solid #222;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: flex-start;
      padding: 10px 6px;
      gap: 4px;
      text-align: center;
    }

    .role-badge {
      font-size: 10px;
      font-weight: 700;
      letter-spacing: .04em;
      text-transform: uppercase;
      padding: 2px 6px;
      border-radius: 3px;
    }
    .role-system   { background: #1e3a2f; color: #4ec9b0; }
    .role-user     { background: #1e2d40; color: #569cd6; }
    .role-assistant{ background: #2d1e3a; color: #c586c0; }
    .role-tool     { background: #3a2d1e; color: #ce9178; }

    .badge-summary { background: #3a3a1e; color: #dcdcaa; font-size: 9px; padding: 1px 5px; border-radius: 3px; }
    .badge-images  { background: #1e3a3a; color: #4ec9b0; font-size: 9px; padding: 1px 5px; border-radius: 3px; }
    .badge-tools   { background: #3a1e2d; color: #f48771; font-size: 9px; padding: 1px 5px; border-radius: 3px; }

    .msg-idx {
      font-size: 10px;
      color: #444;
    }

    .msg-body { padding: 10px 12px; overflow: hidden; }

    .msg-content {
      white-space: pre-wrap;
      word-break: break-word;
      line-height: 1.55;
      color: #d4d4d4;
    }

    .msg-content.dim { color: #888; }

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

    .tool-calls { margin-top: 8px; }

    .tool-call-block {
      background: #1a1a1a;
      border: 1px solid #2a2a2a;
      border-radius: 4px;
      margin-bottom: 6px;
      overflow: hidden;
    }
    .tool-call-name {
      background: #222;
      color: #ce9178;
      padding: 3px 8px;
      font-size: 11px;
      font-weight: 600;
    }
    .tool-call-args {
      padding: 6px 8px;
      white-space: pre-wrap;
      word-break: break-word;
      color: #9cdcfe;
      font-size: 11px;
    }

    .tool-id { font-size: 10px; color: #555; margin-top: 4px; }

    .img-strip { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
    .img-thumb { width: 80px; height: 80px; object-fit: cover; border-radius: 3px; border: 1px solid #333; }

    .char-count { font-size: 10px; color: #444; margin-top: 6px; }

    #error { color: #f48771; padding: 20px; text-align: center; }
  </style>
</head>
<body>

<header>
  <h1>context debug</h1>
  <div id="input-wrap">
    <input id="session-input" type="text" placeholder="session id…" spellcheck="false">
    <button id="btn-load">Load</button>
  </div>
  <div id="stats"></div>
</header>

<main>
  <div id="placeholder">Enter a session ID to inspect what the model sees.</div>
  <div id="error" hidden></div>
  <div id="messages"></div>
</main>

<script>
const input   = document.getElementById('session-input');
const btnLoad = document.getElementById('btn-load');
const statsEl = document.getElementById('stats');
const msgsEl  = document.getElementById('messages');
const errEl   = document.getElementById('error');
const phEl    = document.getElementById('placeholder');

// Pre-fill from URL hash
if (location.hash) {
  input.value = location.hash.slice(1);
}

btnLoad.addEventListener('click', load);
input.addEventListener('keydown', e => { if (e.key === 'Enter') load(); });

async function load() {
  const id = input.value.trim();
  if (!id) return;
  location.hash = id;

  msgsEl.innerHTML = '';
  errEl.hidden = true;
  phEl.hidden = true;
  statsEl.textContent = 'loading…';

  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 = '';
  }
}

function renderContext(data) {
  const estTokens = Math.round(data.total_chars / 4);
  statsEl.innerHTML =
    `<span>${data.profile_id}</span> · ` +
    `<span>${data.message_count}</span> messages · ` +
    `<span>${data.total_chars.toLocaleString()}</span> chars · ` +
    `~<span>${estTokens.toLocaleString()}</span> tokens`;

  data.context.forEach((msg, idx) => {
    msgsEl.appendChild(renderMessage(msg, idx));
  });
}

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

  // ── Left column: role + badges ──
  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);

  if (msg.is_summary) {
    const b = document.createElement('span');
    b.className = 'badge-summary';
    b.textContent = 'summary';
    roleCol.appendChild(b);
  }
  if (msg.images?.length) {
    const b = document.createElement('span');
    b.className = 'badge-images';
    b.textContent = `${msg.images.length} img`;
    roleCol.appendChild(b);
  }
  if (msg.tool_calls?.length) {
    const b = document.createElement('span');
    b.className = 'badge-tools';
    b.textContent = `${msg.tool_calls.length} call${msg.tool_calls.length > 1 ? 's' : ''}`;
    roleCol.appendChild(b);
  }

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

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

  // Content
  if (msg.content) {
    const pre = document.createElement('pre');
    pre.className = 'msg-content' + (msg.role === 'tool' ? ' dim' : '');
    pre.textContent = msg.content;
    body.appendChild(pre);
  }

  // Tool calls (assistant planning)
  if (msg.tool_calls?.length) {
    const wrap = document.createElement('div');
    wrap.className = 'tool-calls';
    const lbl = document.createElement('div');
    lbl.className = 'section-label';
    lbl.textContent = 'tool calls';
    wrap.appendChild(lbl);
    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 || '?';
      block.appendChild(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(args);
      wrap.appendChild(block);
    }
    body.appendChild(wrap);
  }

  // Tool call ID (for 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}`;
    body.appendChild(tid);
  }

  // Images
  if (msg.images?.length) {
    const lbl = document.createElement('div');
    lbl.className = 'section-label';
    lbl.textContent = 'images';
    body.appendChild(lbl);
    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}`;
      strip.appendChild(img);
    }
    body.appendChild(strip);
  }

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

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

// Auto-load if hash present
if (input.value) load();
</script>
</body>
</html>