(() => { const dropZone = document.getElementById("drop-zone"); const fileInput = document.getElementById("file-input"); const fileList = document.getElementById("file-list"); const uploadBtn = document.getElementById("upload-btn"); const uploadSection = document.getElementById("upload-section"); const statusSection = document.getElementById("status-section"); const statusText = document.getElementById("status-text"); const resultsSection = document.getElementById("results-section"); const resultsContainer = document.getElementById("results-container"); let selectedFiles = []; // Drag & drop 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"); addFiles(e.dataTransfer.files); }); fileInput.addEventListener("change", () => addFiles(fileInput.files)); function addFiles(files) { for (const f of files) { if (!selectedFiles.some((s) => s.name === f.name && s.size === f.size)) { selectedFiles.push(f); } } renderFileList(); } function renderFileList() { fileList.innerHTML = ""; selectedFiles.forEach((f, i) => { const tag = document.createElement("span"); tag.className = "file-tag"; tag.innerHTML = `${f.name} ×`; fileList.appendChild(tag); }); fileList.querySelectorAll(".remove").forEach((btn) => btn.addEventListener("click", (e) => { selectedFiles.splice(+e.target.dataset.idx, 1); renderFileList(); }) ); uploadBtn.disabled = selectedFiles.length === 0; } // Upload uploadBtn.addEventListener("click", async () => { if (!selectedFiles.length) return; uploadSection.hidden = true; statusSection.hidden = false; resultsSection.hidden = true; statusText.textContent = `Uploading ${selectedFiles.length} file(s)...`; const form = new FormData(); selectedFiles.forEach((f) => form.append("files", f)); try { const resp = await fetch("/api/upload", { method: "POST", body: form }); if (!resp.ok) { const err = await resp.json().catch(() => ({ detail: resp.statusText })); throw new Error(err.detail || "Upload failed"); } const { batch_id } = await resp.json(); statusText.textContent = "Analyzing..."; await pollResults(batch_id); } catch (err) { statusSection.hidden = true; uploadSection.hidden = false; alert("Error: " + err.message); } }); async function pollResults(batchId) { const maxAttempts = 60; for (let i = 0; i < maxAttempts; i++) { const resp = await fetch(`/api/results/${batchId}`); if (!resp.ok) { await sleep(1000); continue; } const data = await resp.json(); if (data.status === "complete") { renderResults(data); return; } statusText.textContent = `Analyzing... (${i + 1}s)`; await sleep(1000); } statusSection.hidden = true; uploadSection.hidden = false; alert("Timed out waiting for results"); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } // Render function renderResults(data) { statusSection.hidden = true; resultsSection.hidden = false; resultsContainer.innerHTML = ""; for (const file of data.files) { const div = document.createElement("div"); div.className = "file-result"; if (file.error) { div.innerHTML = `

${esc(file.filename)}

Error: ${esc(file.error)}

`; resultsContainer.appendChild(div); continue; } let html = `

${esc(file.filename)}

`; // AI Summary html += `
${esc(file.summary)}
`; // SPC table if (file.spc.length) { html += ``; for (const s of file.spc) { const cpkClass = s.cpk === null ? "" : s.cpk >= 1.33 ? "cpk-good" : s.cpk >= 1.0 ? "cpk-warn" : "cpk-bad"; html += ``; } html += `
FeaturenMeanStd CpCpkPpPpk USLLSLOOS
${esc(s.feature_name)}${s.n} ${s.mean}${s.std} ${fmtIdx(s.cp)}${fmtIdx(s.cpk)} ${fmtIdx(s.pp)}${fmtIdx(s.ppk)} ${s.usl}${s.lsl}${s.out_of_spec_count}
`; } // Charts html += `
`; const histDivs = (file.charts.histograms || []).map((_, i) => `hist-${data.batch_id}-${file.filename}-${i}`); const ctrlDivs = (file.charts.control_charts || []).map((_, i) => `ctrl-${data.batch_id}-${file.filename}-${i}`); const capDiv = file.charts.capability_bar ? `cap-${data.batch_id}-${file.filename}` : null; histDivs.forEach((id) => { html += `
`; }); ctrlDivs.forEach((id) => { html += `
`; }); if (capDiv) html += `
`; html += `
`; // Measurements toggle if (file.report.measurements && file.report.measurements.length) { const tableId = `meas-${data.batch_id}-${file.filename}`; html += ``; html += ``; } div.innerHTML = html; resultsContainer.appendChild(div); // Render Plotly charts after DOM insertion requestAnimationFrame(() => { (file.charts.histograms || []).forEach((chart, i) => { Plotly.newPlot(histDivs[i], chart.data, { ...chart.layout, autosize: true }, { responsive: true }); }); (file.charts.control_charts || []).forEach((chart, i) => { Plotly.newPlot(ctrlDivs[i], chart.data, { ...chart.layout, autosize: true }, { responsive: true }); }); if (capDiv && file.charts.capability_bar) { const cap = file.charts.capability_bar; Plotly.newPlot(capDiv, cap.data, { ...cap.layout, autosize: true }, { responsive: true }); } }); } // Reset for new uploads selectedFiles = []; renderFileList(); uploadSection.hidden = false; } function esc(s) { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; } function fmtIdx(v) { return v === null || v === undefined ? "N/A" : v.toFixed(3); } })();