pdftract/crates/pdftract-cli/src/inspect/demo-json-tree-navigation.html
jedarden cd1b6377b6 feat(pdftract-saddv): implement inspector JSON-tree click navigation
Add data-span-index attribute to span rectangles for click navigation
between SVG canvas and JSON-tree panel. Updated render_spans() to use
enumerate() for tracking indices. Added unit tests for index assignment.

Created demo HTML file demonstrating the full click navigation feature:
- Click span rect -> scroll JSON tree to matching entry
- Highlight target node with yellow background for 2 seconds
- Auto-open ancestor <details> elements
- Smooth scrollIntoView with center alignment

Acceptance criteria:
- PASS: data-span-index attribute added to all spans
- PASS: Click handler scrolls tree to matching node
- PASS: .highlighted class applied for 2 seconds
- PASS: Ancestor details auto-opened before scroll
- PASS: 9 unit tests pass including new span_index test

Closes: pdftract-saddv

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 03:35:24 -04:00

273 lines
9.2 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inspector JSON-Tree Click Navigation Demo</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
display: flex;
height: 100vh;
overflow: hidden;
}
/* Left panel: SVG canvas */
#canvas {
flex: 1;
background: #f5f5f5;
position: relative;
overflow: auto;
padding: 20px;
}
#canvas svg {
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* Span rectangles - clickable */
.span-rect {
cursor: pointer;
transition: stroke-width 0.1s;
}
.span-rect:hover {
stroke-width: 2 !important;
}
/* Right panel: JSON tree */
#json-tree {
width: 400px;
background: white;
border-left: 1px solid #ddd;
overflow-y: auto;
padding: 16px;
}
#json-tree h2 {
font-size: 16px;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
/* JSON tree styling */
details {
margin-left: 16px;
margin-bottom: 4px;
}
summary {
cursor: pointer;
font-size: 13px;
padding: 2px 4px;
border-radius: 2px;
}
summary:hover {
background: #f0f0f0;
}
.span-entry {
padding: 4px 8px;
margin: 2px 0;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
}
.span-entry:hover {
background: #f0f0f0;
}
/* Highlighted class for click navigation */
.highlighted {
background: #ffff99 !important;
animation: highlight-pulse 0.5s ease-out;
}
@keyframes highlight-pulse {
0% { background: #ffff00; }
100% { background: #ffff99; }
}
/* Collapsed state for tree panel */
.collapsed #json-tree {
display: none;
}
</style>
</head>
<body>
<!-- Left panel: SVG canvas with spans -->
<div id="canvas">
<svg width="600" height="400" viewBox="0 0 600 400">
<!-- Sample spans with data-span-index attributes -->
<rect x="50" y="50" width="100" height="20" fill="none" stroke="#22c55e" stroke-width="1"
class="span-rect" data-text="Hello World" data-confidence="0.95" data-span-index="0" />
<rect x="50" y="80" width="80" height="20" fill="none" stroke="#22c55e" stroke-width="1"
class="span-rect" data-text="Foo Bar" data-confidence="0.88" data-span-index="1" />
<rect x="50" y="110" width="120" height="20" fill="none" stroke="#eab308" stroke-width="1"
class="span-rect" data-text="Low Confidence" data-confidence="0.65" data-span-index="2" />
<rect x="50" y="140" width="90" height="20" fill="none" stroke="#ef4444" stroke-width="1"
class="span-rect" data-text="Very Low" data-confidence="0.3" data-span-index="3" />
<rect x="50" y="170" width="110" height="20" fill="none" stroke="#94a3b8" stroke-width="1"
class="span-rect" data-text="Direct Extract" data-confidence="" data-span-index="4" />
</svg>
</div>
<!-- Right panel: JSON tree -->
<div id="json-tree">
<h2>Page JSON Tree</h2>
<div id="tree-content"></div>
</div>
<script>
// Sample page data (normally from /api/page/{i})
const pageData = {
page_index: 0,
width: 600,
height: 400,
spans: [
{ text: "Hello World", bbox: [50, 50, 150, 70], confidence: 0.95, font: "Arial", size: 12 },
{ text: "Foo Bar", bbox: [50, 80, 130, 100], confidence: 0.88, font: "Arial", size: 12 },
{ text: "Low Confidence", bbox: [50, 110, 170, 130], confidence: 0.65, font: "Arial", size: 12 },
{ text: "Very Low", bbox: [50, 140, 140, 160], confidence: 0.3, font: "Arial", size: 12 },
{ text: "Direct Extract", bbox: [50, 170, 160, 190], confidence: null, font: "Arial", size: 12 }
]
};
// Build JSON tree once on page load
function buildJsonTree(data, container) {
const root = document.createElement('div');
// Page metadata
const pageDetails = document.createElement('details');
pageDetails.open = true;
pageDetails.innerHTML = `
<summary>page (index: ${data.page_index})</summary>
`;
root.appendChild(pageDetails);
const pageContent = document.createElement('div');
pageDetails.appendChild(pageContent);
// Width/height
pageContent.innerHTML += `
<details style="margin-left: 16px;">
<summary>width: ${data.width}</summary>
</details>
<details style="margin-left: 16px;">
<summary>height: ${data.height}</summary>
</details>
`;
// Spans array
const spansDetails = document.createElement('details');
spansDetails.open = true;
spansDetails.innerHTML = `<summary>spans (${data.spans.length} items)</summary>`;
pageContent.appendChild(spansDetails);
const spansContent = document.createElement('div');
spansDetails.appendChild(spansContent);
// Each span entry
data.spans.forEach((span, index) => {
const spanEntry = document.createElement('div');
spanEntry.className = 'span-entry';
spanEntry.id = `span-${index}`;
spanEntry.setAttribute('data-span-index', index);
const confDisplay = span.confidence !== null
? `confidence: ${span.confidence.toFixed(2)}`
: 'confidence: null';
spanEntry.innerHTML = `
<span style="color: #666;">[${index}]</span>
<strong>"${escapeHtml(span.text)}"</strong>
<span style="color: #888; font-size: 11px;">${confDisplay}</span>
`;
spansContent.appendChild(spanEntry);
});
container.appendChild(root);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Click handler for span rectangles
function handleSpanClick(event) {
const rect = event.target;
const spanIndex = rect.getAttribute('data-span-index');
if (spanIndex === null) return;
// Locate the matching element in the JSON tree
const treeEntry = document.getElementById(`span-${spanIndex}`);
if (!treeEntry) return;
// Open all ancestor <details> elements
let parent = treeEntry.parentElement;
while (parent) {
if (parent.tagName === 'DETAILS') {
parent.open = true;
}
parent = parent.parentElement;
}
// Scroll to the element
treeEntry.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
// Add highlighted class
treeEntry.classList.add('highlighted');
// Remove after 2 seconds
setTimeout(() => {
treeEntry.classList.remove('highlighted');
}, 2000);
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
// Build JSON tree
const treeContainer = document.getElementById('tree-content');
buildJsonTree(pageData, treeContainer);
// Attach click handlers to all span rects
const spanRects = document.querySelectorAll('.span-rect');
spanRects.forEach(rect => {
rect.addEventListener('click', handleSpanClick);
});
// Make span entries also clickable (reverse navigation)
const spanEntries = document.querySelectorAll('.span-entry');
spanEntries.forEach((entry, index) => {
entry.addEventListener('click', () => {
const rect = document.querySelector(`.span-rect[data-span-index="${index}"]`);
if (rect) {
rect.scrollIntoView({ behavior: 'smooth', block: 'center' });
rect.style.strokeWidth = '3';
setTimeout(() => {
rect.style.strokeWidth = '1';
}, 1000);
}
});
});
});
</script>
</body>
</html>