Improves the HTML export (#853)

- Added jsonl download button

Jump to last message on click

Fix missing labels
This commit is contained in:
Armin Ronacher 2026-01-19 15:57:44 +01:00 committed by GitHub
parent 3e68744cae
commit 98fb9f378c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 89 additions and 5 deletions

View file

@ -221,6 +221,25 @@
font-size: 11px;
color: var(--warning);
margin-bottom: var(--line-height);
display: flex;
align-items: center;
gap: 12px;
}
.download-json-btn {
font-size: 10px;
padding: 2px 8px;
background: var(--container-bg);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text);
cursor: pointer;
font-family: inherit;
}
.download-json-btn:hover {
background: var(--hover);
border-color: var(--borderAccent);
}
/* Header */

View file

@ -54,11 +54,11 @@
}
// Label lookup (entryId -> label string)
// Labels are stored in 'label' entries that reference their target via parentId
// Labels are stored in 'label' entries that reference their target via targetId
const labelMap = new Map();
for (const entry of entries) {
if (entry.type === 'label' && entry.parentId && entry.label) {
labelMap.set(entry.parentId, entry.label);
if (entry.type === 'label' && entry.targetId && entry.label) {
labelMap.set(entry.targetId, entry.label);
}
}
@ -144,6 +144,37 @@
return path;
}
// Tree node lookup for finding leaves
let treeNodeMap = null;
/**
* Find the newest leaf node reachable from a given node.
* This allows clicking any node in a branch to show the full branch.
* Children are sorted by timestamp, so the newest is always last.
*/
function findNewestLeaf(nodeId) {
// Build tree node map lazily
if (!treeNodeMap) {
treeNodeMap = new Map();
const tree = buildTree();
function mapNodes(node) {
treeNodeMap.set(node.entry.id, node);
node.children.forEach(mapNodes);
}
tree.forEach(mapNodes);
}
const node = treeNodeMap.get(nodeId);
if (!node) return nodeId;
// Follow the newest (last) child at each level
let current = node;
while (current.children.length > 0) {
current = current.children[current.children.length - 1];
}
return current.entry.id;
}
/**
* Flatten tree into list with indentation and connector info.
* Returns array of { node, indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots }.
@ -674,7 +705,11 @@
div.appendChild(prefixSpan);
div.appendChild(marker);
div.appendChild(content);
div.addEventListener('click', () => navigateTo(entry.id));
// Navigate to the newest leaf through this node, but scroll to the clicked node
div.addEventListener('click', () => {
const leafId = findNewestLeaf(entry.id);
navigateTo(leafId, 'target', entry.id);
});
container.appendChild(div);
}
@ -954,6 +989,33 @@
return html;
}
/**
* Download the session data as a JSONL file.
* Reconstructs the original format: header line + entry lines.
*/
window.downloadSessionJson = function() {
// Build JSONL content: header first, then all entries
const lines = [];
if (header) {
lines.push(JSON.stringify({ type: 'header', ...header }));
}
for (const entry of entries) {
lines.push(JSON.stringify(entry));
}
const jsonlContent = lines.join('\n');
// Create download
const blob = new Blob([jsonlContent], { type: 'application/x-ndjson' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${header?.id || 'session'}.jsonl`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Build a shareable URL for a specific message.
* URL format: base?gistId&leafId=<leafId>&targetId=<entryId>
@ -1212,7 +1274,10 @@
let html = `
<div class="header">
<h1>Session: ${escapeHtml(header?.id || 'unknown')}</h1>
<div class="help-bar">Ctrl+T toggle thinking · Ctrl+O toggle tools</div>
<div class="help-bar">
<span>Ctrl+T toggle thinking · Ctrl+O toggle tools</span>
<button class="download-json-btn" onclick="downloadSessionJson()" title="Download session as JSONL"> JSONL</button>
</div>
<div class="header-info">
<div class="info-item"><span class="info-label">Date:</span><span class="info-value">${header?.timestamp ? new Date(header.timestamp).toLocaleString() : 'unknown'}</span></div>
<div class="info-item"><span class="info-label">Models:</span><span class="info-value">${globalStats.models.join(', ') || 'unknown'}</span></div>