683 lines
18 KiB
HTML
683 lines
18 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>Hermes Agent - Session Viewer</title>
|
||
|
|
<style>
|
||
|
|
:root {
|
||
|
|
--bg: #0d1117;
|
||
|
|
--surface: #161b22;
|
||
|
|
--surface2: #1c2333;
|
||
|
|
--border: #30363d;
|
||
|
|
--text: #e6edf3;
|
||
|
|
--text-muted: #8b949e;
|
||
|
|
--accent: #58a6ff;
|
||
|
|
--accent-dim: #1f3a5f;
|
||
|
|
--user: #da8ee7;
|
||
|
|
--user-bg: #2d1b3d;
|
||
|
|
--assistant: #58a6ff;
|
||
|
|
--assistant-bg: #152238;
|
||
|
|
--tool: #3fb950;
|
||
|
|
--tool-bg: #12261e;
|
||
|
|
--system: #d29922;
|
||
|
|
--system-bg: #2a2000;
|
||
|
|
--error: #f85149;
|
||
|
|
--meta: #768390;
|
||
|
|
--radius: 10px;
|
||
|
|
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
|
||
|
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||
|
|
}
|
||
|
|
|
||
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
|
|
||
|
|
body {
|
||
|
|
background: var(--bg);
|
||
|
|
color: var(--text);
|
||
|
|
font-family: var(--font-sans);
|
||
|
|
font-size: 14px;
|
||
|
|
line-height: 1.6;
|
||
|
|
display: flex;
|
||
|
|
height: 100vh;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Sidebar */
|
||
|
|
#sidebar {
|
||
|
|
width: 340px;
|
||
|
|
min-width: 340px;
|
||
|
|
background: var(--surface);
|
||
|
|
border-right: 1px solid var(--border);
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
#sidebar-header {
|
||
|
|
padding: 20px;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
#sidebar-header h1 {
|
||
|
|
font-size: 18px;
|
||
|
|
font-weight: 700;
|
||
|
|
color: var(--accent);
|
||
|
|
margin-bottom: 4px;
|
||
|
|
letter-spacing: -0.3px;
|
||
|
|
}
|
||
|
|
|
||
|
|
#sidebar-header p {
|
||
|
|
color: var(--text-muted);
|
||
|
|
font-size: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
#file-picker {
|
||
|
|
padding: 12px 20px;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
#file-picker label {
|
||
|
|
display: block;
|
||
|
|
padding: 10px 16px;
|
||
|
|
background: var(--accent-dim);
|
||
|
|
border: 1px dashed var(--accent);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
text-align: center;
|
||
|
|
cursor: pointer;
|
||
|
|
color: var(--accent);
|
||
|
|
font-size: 13px;
|
||
|
|
font-weight: 500;
|
||
|
|
transition: all 0.15s;
|
||
|
|
}
|
||
|
|
|
||
|
|
#file-picker label:hover {
|
||
|
|
background: #1a4478;
|
||
|
|
}
|
||
|
|
|
||
|
|
#file-picker input { display: none; }
|
||
|
|
|
||
|
|
#session-list {
|
||
|
|
flex: 1;
|
||
|
|
overflow-y: auto;
|
||
|
|
padding: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.session-item {
|
||
|
|
padding: 12px 14px;
|
||
|
|
border-radius: 8px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: background 0.12s;
|
||
|
|
margin-bottom: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.session-item:hover { background: var(--surface2); }
|
||
|
|
.session-item.active { background: var(--accent-dim); border: 1px solid var(--accent); }
|
||
|
|
|
||
|
|
.session-item .session-title {
|
||
|
|
font-weight: 600;
|
||
|
|
font-size: 13px;
|
||
|
|
color: var(--text);
|
||
|
|
margin-bottom: 3px;
|
||
|
|
white-space: nowrap;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
}
|
||
|
|
|
||
|
|
.session-item .session-meta {
|
||
|
|
display: flex;
|
||
|
|
gap: 10px;
|
||
|
|
font-size: 11px;
|
||
|
|
color: var(--text-muted);
|
||
|
|
}
|
||
|
|
|
||
|
|
.session-item .session-meta .badge {
|
||
|
|
display: inline-block;
|
||
|
|
padding: 1px 6px;
|
||
|
|
border-radius: 4px;
|
||
|
|
font-size: 10px;
|
||
|
|
font-weight: 600;
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
.badge-cli { background: #1a3a2a; color: #3fb950; }
|
||
|
|
.badge-telegram { background: #1a2a3a; color: #58a6ff; }
|
||
|
|
.badge-discord { background: #2a1a3a; color: #bc8cff; }
|
||
|
|
|
||
|
|
/* Main area */
|
||
|
|
#main {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
#session-header {
|
||
|
|
padding: 16px 24px;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
background: var(--surface);
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
#session-header h2 {
|
||
|
|
font-size: 16px;
|
||
|
|
font-weight: 600;
|
||
|
|
margin-bottom: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
#session-header .meta-row {
|
||
|
|
display: flex;
|
||
|
|
gap: 20px;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
font-size: 12px;
|
||
|
|
color: var(--text-muted);
|
||
|
|
}
|
||
|
|
|
||
|
|
#session-header .meta-row span { display: flex; align-items: center; gap: 4px; }
|
||
|
|
|
||
|
|
#messages-container {
|
||
|
|
flex: 1;
|
||
|
|
overflow-y: auto;
|
||
|
|
padding: 20px 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Welcome state */
|
||
|
|
#welcome {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
height: 100%;
|
||
|
|
color: var(--text-muted);
|
||
|
|
text-align: center;
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
#welcome .icon { font-size: 48px; opacity: 0.3; }
|
||
|
|
#welcome h3 { font-size: 18px; color: var(--text); font-weight: 600; }
|
||
|
|
|
||
|
|
/* Messages */
|
||
|
|
.message {
|
||
|
|
margin-bottom: 16px;
|
||
|
|
border-radius: var(--radius);
|
||
|
|
overflow: hidden;
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.message-header {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 8px 14px;
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 600;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.5px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.message-body {
|
||
|
|
padding: 12px 16px;
|
||
|
|
white-space: pre-wrap;
|
||
|
|
word-wrap: break-word;
|
||
|
|
font-size: 13.5px;
|
||
|
|
line-height: 1.65;
|
||
|
|
}
|
||
|
|
|
||
|
|
.msg-user .message-header { background: var(--user-bg); color: var(--user); }
|
||
|
|
.msg-user .message-body { background: #1e1228; }
|
||
|
|
.msg-user { border-color: #3d2650; }
|
||
|
|
|
||
|
|
.msg-assistant .message-header { background: var(--assistant-bg); color: var(--assistant); }
|
||
|
|
.msg-assistant .message-body { background: #0f1a2e; }
|
||
|
|
.msg-assistant { border-color: #1e3a5f; }
|
||
|
|
|
||
|
|
.msg-tool .message-header { background: var(--tool-bg); color: var(--tool); }
|
||
|
|
.msg-tool .message-body { background: #0c1a14; font-family: var(--font-mono); font-size: 12px; }
|
||
|
|
.msg-tool { border-color: #1a3525; }
|
||
|
|
|
||
|
|
.msg-session_meta .message-header { background: var(--system-bg); color: var(--system); }
|
||
|
|
.msg-session_meta .message-body { background: #1a1800; }
|
||
|
|
.msg-session_meta { border-color: #3a3000; }
|
||
|
|
|
||
|
|
.msg-system .message-header { background: var(--system-bg); color: var(--system); }
|
||
|
|
.msg-system .message-body { background: #1a1800; }
|
||
|
|
.msg-system { border-color: #3a3000; }
|
||
|
|
|
||
|
|
.tool-calls-section {
|
||
|
|
margin-top: 8px;
|
||
|
|
border-top: 1px solid var(--border);
|
||
|
|
padding-top: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tool-call-item {
|
||
|
|
background: var(--surface2);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: 6px;
|
||
|
|
margin-bottom: 6px;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tool-call-name {
|
||
|
|
padding: 6px 10px;
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--tool);
|
||
|
|
background: var(--tool-bg);
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.tool-call-args {
|
||
|
|
padding: 8px 10px;
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 11px;
|
||
|
|
white-space: pre-wrap;
|
||
|
|
word-break: break-all;
|
||
|
|
color: var(--text-muted);
|
||
|
|
max-height: 300px;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* System prompt collapsible */
|
||
|
|
.system-prompt-toggle {
|
||
|
|
padding: 10px 16px;
|
||
|
|
background: var(--surface2);
|
||
|
|
border: 1px solid var(--border);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
margin-bottom: 16px;
|
||
|
|
cursor: pointer;
|
||
|
|
user-select: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.system-prompt-toggle summary {
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--system);
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.5px;
|
||
|
|
list-style: none;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.system-prompt-toggle summary::before {
|
||
|
|
content: '\25B6';
|
||
|
|
font-size: 10px;
|
||
|
|
transition: transform 0.15s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.system-prompt-toggle[open] summary::before {
|
||
|
|
transform: rotate(90deg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.system-prompt-content {
|
||
|
|
margin-top: 10px;
|
||
|
|
padding: 12px;
|
||
|
|
background: var(--bg);
|
||
|
|
border-radius: 6px;
|
||
|
|
font-size: 12px;
|
||
|
|
white-space: pre-wrap;
|
||
|
|
word-wrap: break-word;
|
||
|
|
color: var(--text-muted);
|
||
|
|
max-height: 400px;
|
||
|
|
overflow-y: auto;
|
||
|
|
line-height: 1.5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.timestamp {
|
||
|
|
font-size: 11px;
|
||
|
|
color: var(--meta);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
}
|
||
|
|
|
||
|
|
.tool-result-truncated {
|
||
|
|
max-height: 400px;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Scrollbar styling */
|
||
|
|
::-webkit-scrollbar { width: 8px; }
|
||
|
|
::-webkit-scrollbar-track { background: transparent; }
|
||
|
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||
|
|
::-webkit-scrollbar-thumb:hover { background: #484f58; }
|
||
|
|
|
||
|
|
.no-content { color: var(--text-muted); font-style: italic; font-size: 12px; }
|
||
|
|
|
||
|
|
.reasoning-block {
|
||
|
|
margin-top: 8px;
|
||
|
|
padding: 8px 12px;
|
||
|
|
background: #1a1a2e;
|
||
|
|
border: 1px solid #2a2a4e;
|
||
|
|
border-radius: 6px;
|
||
|
|
font-size: 12px;
|
||
|
|
color: #a0a0d0;
|
||
|
|
white-space: pre-wrap;
|
||
|
|
max-height: 200px;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.reasoning-label {
|
||
|
|
font-size: 10px;
|
||
|
|
font-weight: 700;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.5px;
|
||
|
|
color: #7070b0;
|
||
|
|
margin-bottom: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.session-divider {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
margin: 24px 0;
|
||
|
|
color: var(--text-muted);
|
||
|
|
font-size: 11px;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 1px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.session-divider::before, .session-divider::after {
|
||
|
|
content: '';
|
||
|
|
flex: 1;
|
||
|
|
height: 1px;
|
||
|
|
background: var(--border);
|
||
|
|
}
|
||
|
|
|
||
|
|
.stats-bar {
|
||
|
|
display: flex;
|
||
|
|
gap: 16px;
|
||
|
|
padding: 8px 14px;
|
||
|
|
background: var(--surface2);
|
||
|
|
border-radius: 6px;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
font-size: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stats-bar .stat { display: flex; align-items: center; gap: 4px; }
|
||
|
|
.stats-bar .stat-label { color: var(--text-muted); }
|
||
|
|
.stats-bar .stat-value { color: var(--text); font-weight: 600; font-family: var(--font-mono); }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
|
||
|
|
<div id="sidebar">
|
||
|
|
<div id="sidebar-header">
|
||
|
|
<h1>Hermes Agent</h1>
|
||
|
|
<p>Session Transcript Viewer</p>
|
||
|
|
</div>
|
||
|
|
<div id="file-picker">
|
||
|
|
<label for="jsonl-input">Load .jsonl file</label>
|
||
|
|
<input type="file" id="jsonl-input" accept=".jsonl,.json,.txt">
|
||
|
|
</div>
|
||
|
|
<div id="session-list"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="main">
|
||
|
|
<div id="session-header" style="display:none"></div>
|
||
|
|
<div id="messages-container">
|
||
|
|
<div id="welcome">
|
||
|
|
<div class="icon">⚙</div>
|
||
|
|
<h3>Load a session file</h3>
|
||
|
|
<p>Select a .jsonl file from the sidebar to view exported Hermes Agent sessions.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const sessions = [];
|
||
|
|
let activeIdx = -1;
|
||
|
|
|
||
|
|
document.getElementById('jsonl-input').addEventListener('change', e => {
|
||
|
|
const file = e.target.files[0];
|
||
|
|
if (!file) return;
|
||
|
|
const reader = new FileReader();
|
||
|
|
reader.onload = ev => {
|
||
|
|
sessions.length = 0;
|
||
|
|
const lines = ev.target.result.split('\n').filter(l => l.trim());
|
||
|
|
for (const line of lines) {
|
||
|
|
try { sessions.push(JSON.parse(line)); } catch {}
|
||
|
|
}
|
||
|
|
renderSessionList();
|
||
|
|
if (sessions.length > 0) selectSession(0);
|
||
|
|
document.querySelector('#sidebar-header p').textContent = `${sessions.length} sessions loaded from ${file.name}`;
|
||
|
|
};
|
||
|
|
reader.readAsText(file);
|
||
|
|
});
|
||
|
|
|
||
|
|
function renderSessionList() {
|
||
|
|
const list = document.getElementById('session-list');
|
||
|
|
list.innerHTML = '';
|
||
|
|
sessions.forEach((s, i) => {
|
||
|
|
const firstUserMsg = (s.messages || []).find(m => m.role === 'user');
|
||
|
|
const preview = firstUserMsg
|
||
|
|
? firstUserMsg.content.substring(0, 80).replace(/\n/g, ' ')
|
||
|
|
: '(no messages)';
|
||
|
|
|
||
|
|
const dt = s.started_at ? new Date(s.started_at * 1000) : null;
|
||
|
|
const dateStr = dt ? dt.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
|
||
|
|
|
||
|
|
const div = document.createElement('div');
|
||
|
|
div.className = 'session-item' + (i === activeIdx ? ' active' : '');
|
||
|
|
div.onclick = () => selectSession(i);
|
||
|
|
div.innerHTML = `
|
||
|
|
<div class="session-title">${esc(preview)}</div>
|
||
|
|
<div class="session-meta">
|
||
|
|
<span class="badge badge-${s.source || 'cli'}">${s.source || 'cli'}</span>
|
||
|
|
<span>${dateStr}</span>
|
||
|
|
<span>${s.message_count || 0} msgs</span>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
list.appendChild(div);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectSession(idx) {
|
||
|
|
activeIdx = idx;
|
||
|
|
const s = sessions[idx];
|
||
|
|
|
||
|
|
document.querySelectorAll('.session-item').forEach((el, i) => {
|
||
|
|
el.classList.toggle('active', i === idx);
|
||
|
|
});
|
||
|
|
|
||
|
|
const header = document.getElementById('session-header');
|
||
|
|
header.style.display = 'block';
|
||
|
|
|
||
|
|
const dt = s.started_at ? new Date(s.started_at * 1000) : null;
|
||
|
|
const endDt = s.ended_at ? new Date(s.ended_at * 1000) : null;
|
||
|
|
const duration = s.started_at && s.ended_at
|
||
|
|
? formatDuration(s.ended_at - s.started_at)
|
||
|
|
: 'unknown';
|
||
|
|
|
||
|
|
header.innerHTML = `
|
||
|
|
<h2>Session ${esc(s.id)}</h2>
|
||
|
|
<div class="meta-row">
|
||
|
|
<span>📡 ${esc(s.source || 'cli')}</span>
|
||
|
|
<span>🤖 ${esc(s.model || 'unknown')}</span>
|
||
|
|
<span>💬 ${s.message_count || 0} messages</span>
|
||
|
|
<span>🔧 ${s.tool_call_count || 0} tool calls</span>
|
||
|
|
<span>⏱ ${duration}</span>
|
||
|
|
${s.end_reason ? `<span>🏁 ${esc(s.end_reason)}</span>` : ''}
|
||
|
|
${dt ? `<span>📅 ${dt.toLocaleString()}</span>` : ''}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
renderMessages(s);
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderMessages(session) {
|
||
|
|
const container = document.getElementById('messages-container');
|
||
|
|
container.innerHTML = '';
|
||
|
|
|
||
|
|
// System prompt (collapsible)
|
||
|
|
if (session.system_prompt) {
|
||
|
|
const details = document.createElement('details');
|
||
|
|
details.className = 'system-prompt-toggle';
|
||
|
|
details.innerHTML = `
|
||
|
|
<summary>System Prompt (${(session.system_prompt.length / 1024).toFixed(1)}KB)</summary>
|
||
|
|
<div class="system-prompt-content">${esc(session.system_prompt)}</div>
|
||
|
|
`;
|
||
|
|
container.appendChild(details);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Stats bar
|
||
|
|
const stats = document.createElement('div');
|
||
|
|
stats.className = 'stats-bar';
|
||
|
|
stats.innerHTML = `
|
||
|
|
<div class="stat"><span class="stat-label">Messages:</span><span class="stat-value">${session.message_count || 0}</span></div>
|
||
|
|
<div class="stat"><span class="stat-label">Tool Calls:</span><span class="stat-value">${session.tool_call_count || 0}</span></div>
|
||
|
|
<div class="stat"><span class="stat-label">Source:</span><span class="stat-value">${esc(session.source || 'cli')}</span></div>
|
||
|
|
${session.user_id ? `<div class="stat"><span class="stat-label">User ID:</span><span class="stat-value">${esc(session.user_id)}</span></div>` : ''}
|
||
|
|
`;
|
||
|
|
container.appendChild(stats);
|
||
|
|
|
||
|
|
const messages = session.messages || [];
|
||
|
|
for (const msg of messages) {
|
||
|
|
const el = renderMessage(msg);
|
||
|
|
container.appendChild(el);
|
||
|
|
}
|
||
|
|
|
||
|
|
container.scrollTop = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderMessage(msg) {
|
||
|
|
const div = document.createElement('div');
|
||
|
|
const role = msg.role || 'unknown';
|
||
|
|
div.className = `message msg-${role}`;
|
||
|
|
|
||
|
|
const roleIcon = {
|
||
|
|
user: '👤',
|
||
|
|
assistant: '🤖',
|
||
|
|
tool: '🔧',
|
||
|
|
session_meta: '⚙',
|
||
|
|
system: '📋'
|
||
|
|
}[role] || '❓';
|
||
|
|
|
||
|
|
const ts = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleTimeString() : '';
|
||
|
|
const toolName = msg.tool_name ? ` (${msg.tool_name})` : '';
|
||
|
|
|
||
|
|
let headerExtra = '';
|
||
|
|
if (msg.tool_call_id && role === 'tool') {
|
||
|
|
headerExtra = ` — <span style="opacity:0.7;font-size:10px;text-transform:none;letter-spacing:0">${esc(msg.tool_call_id.substring(0, 24))}...</span>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
div.innerHTML = `<div class="message-header">
|
||
|
|
<span>${roleIcon}</span>
|
||
|
|
<span>${role}${toolName}</span>
|
||
|
|
${headerExtra}
|
||
|
|
<span style="margin-left:auto" class="timestamp">${ts}</span>
|
||
|
|
</div>`;
|
||
|
|
|
||
|
|
const body = document.createElement('div');
|
||
|
|
body.className = 'message-body';
|
||
|
|
|
||
|
|
// Content
|
||
|
|
if (msg.content) {
|
||
|
|
let text = msg.content;
|
||
|
|
// Try to detect if content is a JSON string and pretty-print it
|
||
|
|
if (role === 'tool' && text.startsWith('{')) {
|
||
|
|
try {
|
||
|
|
const parsed = JSON.parse(text);
|
||
|
|
text = JSON.stringify(parsed, null, 2);
|
||
|
|
} catch {}
|
||
|
|
}
|
||
|
|
const contentDiv = document.createElement('div');
|
||
|
|
if (role === 'tool') {
|
||
|
|
contentDiv.className = 'tool-result-truncated';
|
||
|
|
}
|
||
|
|
contentDiv.textContent = text;
|
||
|
|
body.appendChild(contentDiv);
|
||
|
|
} else if (role !== 'session_meta' && !msg.tool_calls) {
|
||
|
|
const empty = document.createElement('span');
|
||
|
|
empty.className = 'no-content';
|
||
|
|
empty.textContent = '(no text content)';
|
||
|
|
body.appendChild(empty);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reasoning
|
||
|
|
if (msg.reasoning) {
|
||
|
|
const rBlock = document.createElement('div');
|
||
|
|
rBlock.innerHTML = `<div class="reasoning-label">Reasoning</div>`;
|
||
|
|
const rContent = document.createElement('div');
|
||
|
|
rContent.className = 'reasoning-block';
|
||
|
|
rContent.textContent = msg.reasoning;
|
||
|
|
rBlock.appendChild(rContent);
|
||
|
|
body.appendChild(rBlock);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Tool calls
|
||
|
|
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
||
|
|
const tcSection = document.createElement('div');
|
||
|
|
tcSection.className = 'tool-calls-section';
|
||
|
|
const label = document.createElement('div');
|
||
|
|
label.style.cssText = 'font-size:11px;font-weight:700;color:var(--tool);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;';
|
||
|
|
label.textContent = `Tool Calls (${msg.tool_calls.length})`;
|
||
|
|
tcSection.appendChild(label);
|
||
|
|
|
||
|
|
for (const tc of msg.tool_calls) {
|
||
|
|
const fn = tc.function || {};
|
||
|
|
const tcItem = document.createElement('div');
|
||
|
|
tcItem.className = 'tool-call-item';
|
||
|
|
|
||
|
|
const nameDiv = document.createElement('div');
|
||
|
|
nameDiv.className = 'tool-call-name';
|
||
|
|
nameDiv.textContent = fn.name || 'unknown';
|
||
|
|
tcItem.appendChild(nameDiv);
|
||
|
|
|
||
|
|
if (fn.arguments) {
|
||
|
|
const argsDiv = document.createElement('div');
|
||
|
|
argsDiv.className = 'tool-call-args';
|
||
|
|
let argsText = fn.arguments;
|
||
|
|
try {
|
||
|
|
argsText = JSON.stringify(JSON.parse(fn.arguments), null, 2);
|
||
|
|
} catch {}
|
||
|
|
argsDiv.textContent = argsText;
|
||
|
|
tcItem.appendChild(argsDiv);
|
||
|
|
}
|
||
|
|
|
||
|
|
tcSection.appendChild(tcItem);
|
||
|
|
}
|
||
|
|
body.appendChild(tcSection);
|
||
|
|
}
|
||
|
|
|
||
|
|
div.appendChild(body);
|
||
|
|
return div;
|
||
|
|
}
|
||
|
|
|
||
|
|
function esc(str) {
|
||
|
|
if (!str) return '';
|
||
|
|
const d = document.createElement('div');
|
||
|
|
d.textContent = str;
|
||
|
|
return d.innerHTML;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatDuration(seconds) {
|
||
|
|
if (seconds < 60) return `${Math.round(seconds)}s`;
|
||
|
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
|
||
|
|
const h = Math.floor(seconds / 3600);
|
||
|
|
const m = Math.floor((seconds % 3600) / 60);
|
||
|
|
return `${h}h ${m}m`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auto-load if file is in same directory (for local dev)
|
||
|
|
window.addEventListener('DOMContentLoaded', () => {
|
||
|
|
fetch('exprted.jsonl')
|
||
|
|
.then(r => { if (!r.ok) throw new Error(); return r.text(); })
|
||
|
|
.then(text => {
|
||
|
|
const lines = text.split('\n').filter(l => l.trim());
|
||
|
|
for (const line of lines) {
|
||
|
|
try { sessions.push(JSON.parse(line)); } catch {}
|
||
|
|
}
|
||
|
|
if (sessions.length) {
|
||
|
|
renderSessionList();
|
||
|
|
selectSession(sessions.length - 1);
|
||
|
|
document.querySelector('#sidebar-header p').textContent = `${sessions.length} sessions loaded`;
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.catch(() => {});
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|