- Server now returns metadata-only tree on initial load (no file content
in the JSON payload); content is served on-demand via the new
GET /api/repos/{key}/file?path=... endpoint
- Cache still stores full content; strip_content() runs in-memory before
the response is sent
- Frontend fetches file content lazily in _fetchContent() when a tile
enters the LOD view, preventing a massive upfront JSON download for
large repos (e.g. claude code)
- computeColorRanges() is now deferred to first _showCode() call instead
of running synchronously for every file during load()
- Cap label fontSize at 5 world units to prevent giant text on large tiles
183 lines
4.8 KiB
JavaScript
183 lines
4.8 KiB
JavaScript
import { computeLayout } from "./layout.js";
|
|
import { RepoRenderer } from "./renderer.js";
|
|
|
|
const landing = document.getElementById("landing");
|
|
const loading = document.getElementById("loading");
|
|
const loadingText = document.getElementById("loading-text");
|
|
const viewport = document.getElementById("viewport");
|
|
const controlsHint = document.getElementById("controls-hint");
|
|
const gitUrlInput = document.getElementById("git-url");
|
|
const btnClone = document.getElementById("btn-clone");
|
|
const dropZone = document.getElementById("drop-zone");
|
|
const fileInput = document.getElementById("file-input");
|
|
const historyEl = document.getElementById("history");
|
|
const historyList = document.getElementById("history-list");
|
|
|
|
function showLoading(msg) {
|
|
landing.style.display = "none";
|
|
loading.classList.add("active");
|
|
loadingText.textContent = msg;
|
|
}
|
|
|
|
function showVisualization() {
|
|
loading.classList.remove("active");
|
|
viewport.classList.add("active");
|
|
controlsHint.classList.add("active");
|
|
}
|
|
|
|
function showError(msg) {
|
|
loading.classList.remove("active");
|
|
landing.style.display = "";
|
|
alert(msg);
|
|
}
|
|
|
|
async function visualize(tree, repoName, cacheKey) {
|
|
showLoading("Building layout...");
|
|
|
|
// Wait for fonts to load so canvas renders them correctly
|
|
await document.fonts.ready;
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
|
|
const { leaves, totalWidth, totalHeight } = computeLayout(tree);
|
|
|
|
if (leaves.length === 0) {
|
|
showError("No source files found in repository.");
|
|
return;
|
|
}
|
|
|
|
showLoading(`Rendering ${leaves.length} files...`);
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
|
|
showVisualization();
|
|
document.getElementById("osd-info").classList.add("active");
|
|
const renderer = new RepoRenderer(viewport, repoName || tree.name, cacheKey);
|
|
await renderer.load(leaves, totalWidth, totalHeight);
|
|
}
|
|
|
|
// --- History ---
|
|
async function loadHistory() {
|
|
try {
|
|
const res = await fetch("/api/repos");
|
|
if (!res.ok) return;
|
|
const repos = await res.json();
|
|
if (repos.length === 0) return;
|
|
|
|
historyEl.classList.add("has-items");
|
|
historyList.innerHTML = "";
|
|
|
|
for (const repo of repos) {
|
|
const item = document.createElement("div");
|
|
item.className = "history-item";
|
|
item.innerHTML = `
|
|
<span class="name">${escapeHtml(repo.name)}</span>
|
|
<span class="meta">${repo.file_count} files</span>
|
|
`;
|
|
item.addEventListener("click", () => loadCachedRepo(repo.cache_key, repo.name));
|
|
historyList.appendChild(item);
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
async function loadCachedRepo(key, name) {
|
|
showLoading(`Loading ${name}...`);
|
|
try {
|
|
const res = await fetch(`/api/repos/${key}`);
|
|
if (!res.ok) throw new Error("Cache expired");
|
|
const { cache_key, tree } = await res.json();
|
|
await visualize(tree, name, cache_key);
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
const div = document.createElement("div");
|
|
div.textContent = s;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Load history on page load
|
|
loadHistory();
|
|
|
|
// --- Git clone ---
|
|
btnClone.addEventListener("click", async () => {
|
|
const url = gitUrlInput.value.trim();
|
|
if (!url) return;
|
|
|
|
btnClone.disabled = true;
|
|
showLoading("Cloning repository...");
|
|
|
|
try {
|
|
const res = await fetch("/api/scan-git", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ url }),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
throw new Error(err.error || "Clone failed");
|
|
}
|
|
|
|
const { cache_key, tree } = await res.json();
|
|
await visualize(tree, undefined, cache_key);
|
|
} catch (err) {
|
|
showError(err.message);
|
|
} finally {
|
|
btnClone.disabled = false;
|
|
}
|
|
});
|
|
|
|
gitUrlInput.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") btnClone.click();
|
|
});
|
|
|
|
// --- Zip upload ---
|
|
dropZone.addEventListener("click", () => fileInput.click());
|
|
|
|
dropZone.addEventListener("dragover", (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.add("dragover");
|
|
});
|
|
|
|
dropZone.addEventListener("dragleave", () => {
|
|
dropZone.classList.remove("dragover");
|
|
});
|
|
|
|
dropZone.addEventListener("drop", (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove("dragover");
|
|
const file = e.dataTransfer.files[0];
|
|
if (file) uploadZip(file);
|
|
});
|
|
|
|
fileInput.addEventListener("change", () => {
|
|
if (fileInput.files[0]) uploadZip(fileInput.files[0]);
|
|
});
|
|
|
|
async function uploadZip(file) {
|
|
showLoading("Uploading and scanning zip...");
|
|
|
|
try {
|
|
const form = new FormData();
|
|
form.append("file", file);
|
|
|
|
const res = await fetch("/api/scan-zip", {
|
|
method: "POST",
|
|
body: form,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
throw new Error(err.error || "Upload failed");
|
|
}
|
|
|
|
const { cache_key, tree } = await res.json();
|
|
await visualize(tree, undefined, cache_key);
|
|
} catch (err) {
|
|
showError(err.message);
|
|
}
|
|
}
|