Files
agentic/frontend/js/doc-viewer.js
T
2026-06-10 08:20:27 +02:00

299 lines
11 KiB
JavaScript

// Doc viewer with polygon visualization
const DOC_VIEWER = {
svgEl: null,
canvasEl: null,
init() {
this.svgEl = $("#polygon-svg");
this.canvasEl = $("#polygon-canvas");
$("#viewer-prev").addEventListener("click", () => this.goPage(-1));
$("#viewer-next").addEventListener("click", () => this.goPage(1));
$("#show-polygons").addEventListener("change", () => this.redraw());
$("#show-text-layers").addEventListener("change", () => this.redraw());
$("#show-chunk-labels").addEventListener("change", () => this.redraw());
$("input[id='viewer-page-input']").addEventListener("change", (e) => {
const page = parseInt(e.target.value, 10);
if (page >= 0 && page <= STORE.totalPages) {
this.currentPage = page;
this.loadPage(page);
}
});
// Click on polygon to show chunk info
this.svgEl.addEventListener("click", (e) => {
const target = e.target;
if (target.dataset.chunkId) {
this.showChunkInfo(target.dataset.chunkId);
}
});
},
async loadDocument(docId) {
STORE.currentDocId = docId;
const res = await API.get(`/api/documents/${docId}/chunks`);
STORE.viewerDoc = res.chunks || [];
// Compute page range
const pages = new Set(STORE.viewerDoc.map(c => c.page_num));
STORE.totalPages = Math.max(...pages) + 1;
STORE.currentPage = 0;
$("#viewer-doc-name").textContent = STORE.documents.find(d => d.id === docId)?.filename || "Unknown";
$("#viewer-page-input").max = STORE.totalPages - 1;
this.updatePageInfo();
await this.loadPage(0);
},
async loadPage(page) {
STORE.currentPage = page;
this.updatePageInfo();
// Get chunks for this page
const chunks = STORE.viewerDoc.filter(c => c.page_num === page);
if (!chunks.length) {
$("#viewer-toolbar > span:nth-child(2)").textContent = "No content";
this.clearSvg();
return;
}
// Get polygon data
const polyRes = await API.get(`/api/documents/${STORE.currentDocId}/polygon-view?page=${page}`);
STORE.polygons = polyRes.blocks || chunks.map((c, i) => ({
...c,
polygon: c.polygon || { x_min: 50, y_min: 50 + (i * 30), width: 900, height: 25 },
}));
this.redraw();
},
updatePageInfo() {
$("#viewer-page-input").value = STORE.currentPage;
$("#viewer-page-info").textContent = `Page ${STORE.currentPage + 1} / ${STORE.totalPages}`;
},
goPage(dir) {
const newPage = STORE.currentPage + dir;
if (newPage >= 0 && newPage < STORE.totalPages) {
this.loadPage(newPage);
}
},
clearSvg() {
if (this.svgEl) {
this.svgEl.innerHTML = "";
}
},
redraw() {
const chunks = STORE.polygons.filter(c => c.polygon);
if (!chunks.length || !this.svgEl) return;
this.clearSvg();
// Calculate bounding box from all polygons
const bbox = chunks.reduce((acc, c) => {
const p = c.polygon;
if (!p || !p.bbox) return acc;
const [x1, y1, x2, y2] = p.bbox;
acc.x1 = Math.min(acc.x1, x1);
acc.y1 = Math.min(acc.y1, y1);
acc.x2 = Math.max(acc.x2, x2);
acc.y2 = Math.max(acc.y2, y2);
return acc;
}, { x1: Infinity, y1: Infinity, x2: -Infinity, y2: -Infinity });
const padding = 20;
const svgWidth = 1000;
const svgHeight = 1200;
const scaleX = svgWidth / Math.max(bbox.x2 - bbox.x1, 100);
const scaleY = svgHeight / Math.max(bbox.y2 - bbox.y1, 100);
const scale = Math.min(scaleX, scaleY);
this.svgEl.setAttribute("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
// Page background
const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
bg.setAttribute("x", padding);
bg.setAttribute("y", padding);
bg.setAttribute("width", svgWidth - padding * 2);
bg.setAttribute("height", svgHeight - padding * 2);
bg.setAttribute("fill", "#1e1e2e");
bg.setAttribute("rx", "4");
this.svgEl.appendChild(bg);
const showPolys = $("#show-polygons").checked;
const showText = $("#show-text-layers").checked;
const showLabels = $("#show-chunk-labels").checked;
// Draw each chunk as a polygon/rect
chunks.forEach((chunk, idx) => {
let rect;
let x, y, w, h;
// Handle both bbox format [x1, y1, x2, y2] and explicit polygon format
let coords;
if (chunk.polygon.bbox && Array.isArray(chunk.polygon.bbox)) {
const [x1, y1, x2, y2] = chunk.polygon.bbox;
x = padding + (x1 - bbox.x1) * scale;
y = padding + (y1 - bbox.y1) * scale;
w = Math.max((x2 - x1) * scale, 10);
h = Math.max((y2 - y1) * scale, 5);
} else if (chunk.polygon.polygon && Array.isArray(chunk.polygon.polygon)) {
const pol = chunk.polygon.polygon;
const minX = Math.min(...pol.map(p => p[0]));
const minY = Math.min(...pol.map(p => p[1]));
const maxX = Math.max(...pol.map(p => p[0]));
const maxY = Math.max(...pol.map(p => p[1]));
x = padding + (minX - bbox.x1) * scale;
y = padding + (minY - bbox.y1) * scale;
w = Math.max((maxX - minX) * scale, 10);
h = Math.max((maxY - minY) * scale, 5);
} else if (chunk.polygon.x_min != null) {
x = padding + (chunk.polygon.x_min - bbox.x1) * scale;
y = padding + (chunk.polygon.y_min - bbox.y1) * scale;
w = Math.max((chunk.polygon.width || 100) * scale, 50);
h = Math.max((chunk.polygon.height || 20) * scale, 5);
} else {
return; // Skip if no polygon data
}
// Polygon shape
if (showPolys) {
rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", x);
rect.setAttribute("y", y);
rect.setAttribute("width", w);
rect.setAttribute("height", h);
rect.setAttribute("rx", "2");
// Color by chunk type
const colors = {
"text": "#58a6ff",
"figure": "#3fb950",
"table": "#a371f7",
"footnote": "#f0883e",
"equation": "#f85149",
"list": "#d29922",
"caption": "#39d2c0",
"title": "#e6edf3",
};
const color = colors[chunk.type] || colors["text"];
rect.setAttribute("fill", `${color}15`);
rect.setAttribute("stroke", `${color}80`);
rect.setAttribute("stroke-width", "1");
rect.setAttribute("stroke-dasharray", "3,2");
rect.dataset.chunkId = chunk.id;
rect.style.cursor = "pointer";
rect.addEventListener("mouseenter", () => {
rect.setAttribute("stroke-width", "2");
rect.setAttribute("fill", `${color}30`);
});
rect.addEventListener("mouseleave", () => {
rect.setAttribute("stroke-width", "1");
rect.setAttribute("fill", `${color}15`);
});
// Store text content as tooltip
const title = document.createElementNS("http://www.w3.org/2000/svg", "title");
title.textContent = chunk.content ? chunk.content.substring(0, 100) : "";
rect.appendChild(title);
this.svgEl.appendChild(rect);
}
// Text label overlay
if (showText && chunk.content) {
const fontSize = Math.max(6, Math.min(12, h * 0.6));
if (h > 12) {
const textEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
textEl.setAttribute("x", x + 3);
textEl.setAttribute("y", y + fontSize + 1);
textEl.setAttribute("fill", "#e6edf3");
textEl.setAttribute("font-size", fontSize);
textEl.setAttribute("font-family", "monospace");
// Truncate text to fit
const maxChars = Math.max(2, Math.floor(w / (fontSize * 0.55)));
let displayText = chunk.content.substring(0, maxChars);
if (chunk.content.length > maxChars) displayText += "...";
textEl.textContent = displayText;
this.svgEl.appendChild(textEl);
}
}
// Chunk index label
if (showLabels) {
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
label.setAttribute("x", x);
label.setAttribute("y", Math.max(y - 4, 12));
label.setAttribute("fill", `${color || "#58a6ff"}aa`);
label.setAttribute("font-size", "8");
label.setAttribute("font-family", "monospace");
label.textContent = `${idx}`;
this.svgEl.appendChild(label);
}
});
// Legend
const legendY = svgHeight - 30;
const legendBg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
legendBg.setAttribute("x", padding);
legendBg.setAttribute("y", legendY);
legendBg.setAttribute("width", "180");
legendBg.setAttribute("height", "22");
legendBg.setAttribute("fill", "var(--bg-primary)");
legendBg.setAttribute("rx", "3");
this.svgEl.appendChild(legendBg);
const legendText = document.createElementNS("http://www.w3.org/2000/svg", "text");
legendText.setAttribute("x", padding + 8);
legendText.setAttribute("y", legendY + 14);
legendText.setAttribute("fill", "#8b949e");
legendText.setAttribute("font-size", "9");
legendText.textContent = "Click a region to view content";
this.svgEl.appendChild(legendText);
},
showChunkInfo(chunkId) {
const chunk = STORE.viewerDoc.find(c => c.id === chunkId) ||
STORE.polygons.find(c => c.id === chunkId);
if (!chunk) return;
// Switch to analysis tab and show chunk
const content = chunk.content || "(no text content)";
const page = chunk.page_num ?? "?";
const div = document.createElement("div");
div.className = "block";
div.style.cursor = "pointer";
div.innerHTML = `<div style="font-size:10px;color:var(--accent-cyan);margin-bottom:4px">[p${page}]${chunk.polygon ? ' (polygon)' : ''}</div><div style="font-size:12px;color:var(--text-primary)">${content.substring(0, 300)}</div>`;
div.addEventListener("click", () => {
// Open in research tab
const output = $("#research-output");
const section = document.createElement("div");
section.className = "research-section";
section.innerHTML = `<h3>Chunk Viewer - Page ${page}</h3>
<div style="background:var(--bg-tertiary);padding:10px;border-radius:6px;margin:8px 0;font-family:monospace;font-size:12px;color:var(--text-secondary);white-space:pre-wrap;max-height:300px;overflow-y:auto">${content}</div>`;
if (output.querySelector(".empty-state")) {
output.innerHTML = "";
}
output.appendChild(section);
// Switch to analysis tab
$$(".tab").forEach(t => t.classList.remove("active"));
$$(".tab-pane").forEach(p => p.classList.remove("active"));
document.querySelector('[data-tab="analysis"]').classList.add("active");
$("#tab-analysis").classList.add("active");
});
showToast(`Chunk p${page}: ${content.substring(0, 50)}...`, "info");
},
};