299 lines
11 KiB
JavaScript
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");
|
|
},
|
|
};
|