// 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 = `