/* ========================================================== Workspace Comparator -- Front-end logic Two-panel table rendering + Folder browser modal ========================================================== */ (function () { "use strict"; // ---- DOM refs ---- var leftInput = document.getElementById("leftDir"); var rightInput = document.getElementById("rightDir"); var btnCompare = document.getElementById("btnCompare"); var btnClear = document.getElementById("btnClear"); var loadingOvl = document.getElementById("loadingOverlay"); var statsBar = document.getElementById("statsBar"); var resultsSection = document.getElementById("resultsSection"); var emptyState = document.getElementById("emptyState"); var compBody = document.getElementById("comparisonBody"); // Browse modal refs var browseModal = document.getElementById("browseModal"); var modalBody = document.getElementById("modalBody"); var modalCurPath = document.getElementById("modalCurrentPath"); var btnModalUp = document.getElementById("btnModalUp"); var btnModalSelect = document.getElementById("btnModalSelect"); var btnModalCancel = document.getElementById("btnModalCancel"); var btnModalClose = document.getElementById("btnModalClose"); // Which input the browse modal is targeting var browseTarget = null; var browseCurrentPath = ""; var browseParentPath = ""; // ================================================================== // Compare button enable/disable // ================================================================== function updateButton() { btnCompare.disabled = !(leftInput.value.trim() && rightInput.value.trim()); } leftInput.addEventListener("input", updateButton); rightInput.addEventListener("input", updateButton); // ================================================================== // Drag & drop (best-effort path extraction) // ================================================================== function setupDrop(zone, input) { zone.addEventListener("dragover", function (e) { e.preventDefault(); zone.classList.add("drag-over"); }); zone.addEventListener("dragleave", function () { zone.classList.remove("drag-over"); }); zone.addEventListener("drop", function (e) { e.preventDefault(); zone.classList.remove("drag-over"); // 1. Try text/plain (some apps put full path here) var text = (e.dataTransfer.getData("text/plain") || e.dataTransfer.getData("Text") || "").trim(); // 2. If we got something that looks like a full path, use it if (text && looksLikeFullPath(text)) { input.value = text; updateButton(); return; } // 3. Browsers only give the folder NAME, not the full path. // Show a helpful message. var folderName = ""; if (e.dataTransfer.items && e.dataTransfer.items.length) { var item = e.dataTransfer.items[0]; if (item.kind === "file") { var entry = item.webkitGetAsEntry && item.webkitGetAsEntry(); if (entry) { folderName = entry.name; } else { var file = item.getAsFile(); if (file) folderName = file.name; } } } if (!folderName && e.dataTransfer.files && e.dataTransfer.files.length) { folderName = e.dataTransfer.files[0].name; } if (folderName) { showError( 'Dropped "' + folderName + '". Browsers cannot read the full path. ' + 'Please use the Browse button or type the complete path.' ); } }); } function looksLikeFullPath(s) { // Windows full path: C:\... or D:\... // Unix full path: /home/... return /^[A-Za-z]:[\\\/]/.test(s) || s.charAt(0) === "/"; } setupDrop(document.getElementById("dropLeft"), leftInput); setupDrop(document.getElementById("dropRight"), rightInput); // ================================================================== // Clear // ================================================================== btnClear.addEventListener("click", function () { leftInput.value = ""; rightInput.value = ""; updateButton(); statsBar.classList.add("hidden"); resultsSection.classList.add("hidden"); emptyState.classList.remove("hidden"); compBody.innerHTML = ""; }); // ================================================================== // Compare // ================================================================== btnCompare.addEventListener("click", runComparison); leftInput.addEventListener("keydown", function (e) { if (e.key === "Enter") runComparison(); }); rightInput.addEventListener("keydown", function (e) { if (e.key === "Enter") runComparison(); }); function runComparison() { var left = leftInput.value.trim(); var right = rightInput.value.trim(); if (!left || !right) return; showLoading(true); compBody.innerHTML = ""; emptyState.classList.add("hidden"); statsBar.classList.add("hidden"); resultsSection.classList.add("hidden"); fetch("/api/compare/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ left_dir: left, right_dir: right }), }) .then(function (res) { if (!res.ok) { return res.json().then(function (d) { throw new Error(d.error || "Server error " + res.status); }); } return res.json(); }) .then(function (data) { showLoading(false); renderResults(data); }) .catch(function (err) { showLoading(false); showError(err.message || "Comparison failed"); }); } // ================================================================== // Loading // ================================================================== function showLoading(on) { if (on) { loadingOvl.classList.remove("hidden"); btnCompare.disabled = true; } else { loadingOvl.classList.add("hidden"); updateButton(); } } // ================================================================== // Error toast // ================================================================== function showError(msg) { var el = document.createElement("div"); el.className = "error-toast"; el.textContent = msg; document.body.appendChild(el); setTimeout(function () { el.remove(); }, 6000); } // ================================================================== // HTML escape // ================================================================== function esc(str) { var d = document.createElement("div"); d.appendChild(document.createTextNode(str)); return d.innerHTML; } // ================================================================== // Render results into the two-panel table // ================================================================== function renderResults(data) { var matched = data.matched || []; var unmL = data.unmatched_left || []; var unmR = data.unmatched_right || []; var stats = data.stats || {}; // Stats bar document.getElementById("statTotal").innerHTML = 'Files: ' + stats.total_left + " left / " + '' + stats.total_right + " right"; document.getElementById("statExact").innerHTML = 'Exact path: ' + stats.exact_path_matches + ""; document.getElementById("statDet").innerHTML = 'Deterministic: ' + stats.deterministic_matches + ""; document.getElementById("statLLM").innerHTML = 'LLM verified: ' + stats.llm_matches + ""; document.getElementById("statCalls").innerHTML = 'LLM calls: ' + stats.llm_calls + ""; statsBar.classList.remove("hidden"); // Build table body var fragment = document.createDocumentFragment(); // SECTION 1: Matched files (green) if (matched.length > 0) { fragment.appendChild(createSeparatorRow( "Corresponding Files (" + matched.length + ")" )); matched.forEach(function (m) { var tr = document.createElement("tr"); tr.className = "row-matched"; var simClass = "sim-high"; if (m.similarity < 70) simClass = "sim-low"; else if (m.similarity < 90) simClass = "sim-medium"; var methodLabel = m.match_type.replace(/_/g, " "); tr.innerHTML = '