chore: remove deprecated session viewer and exported data files
- Deleted the session_viewer.html file, which was no longer in use. - Removed the exprted.jsonl file, as it contained outdated exported data that is no longer relevant to the current project structure.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,682 +0,0 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user