Files
cmm-report-analyzer/app/static/js/app.js
chrisryn 9abf9b4b58 Initial commit: CMM Report Analyzer
FastAPI app that parses CMM inspection reports (PDF/Excel/CSV),
computes SPC metrics (Cp/Cpk/Pp/Ppk, control limits, Shapiro-Wilk),
generates interactive Plotly charts, and provides AI-powered quality
summaries via Azure OpenAI with graceful fallback.

Includes 21 passing tests covering parsers, SPC calculations, and
API endpoints.
2026-02-19 10:38:51 -06:00

205 lines
7.7 KiB
JavaScript

(() => {
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} <span class="remove" data-idx="${i}">&times;</span>`;
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 = `<h2>${esc(file.filename)}</h2><p class="error">Error: ${esc(file.error)}</p>`;
resultsContainer.appendChild(div);
continue;
}
let html = `<h2>${esc(file.filename)}</h2>`;
// AI Summary
html += `<div class="summary">${esc(file.summary)}</div>`;
// SPC table
if (file.spc.length) {
html += `<table class="spc-table"><thead><tr>
<th>Feature</th><th>n</th><th>Mean</th><th>Std</th>
<th>Cp</th><th>Cpk</th><th>Pp</th><th>Ppk</th>
<th>USL</th><th>LSL</th><th>OOS</th>
</tr></thead><tbody>`;
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 += `<tr>
<td>${esc(s.feature_name)}</td><td>${s.n}</td>
<td>${s.mean}</td><td>${s.std}</td>
<td>${fmtIdx(s.cp)}</td><td class="${cpkClass}">${fmtIdx(s.cpk)}</td>
<td>${fmtIdx(s.pp)}</td><td>${fmtIdx(s.ppk)}</td>
<td>${s.usl}</td><td>${s.lsl}</td><td>${s.out_of_spec_count}</td>
</tr>`;
}
html += `</tbody></table>`;
}
// Charts
html += `<div class="charts-grid">`;
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 += `<div class="chart-container" id="${id}"></div>`; });
ctrlDivs.forEach((id) => { html += `<div class="chart-container" id="${id}"></div>`; });
if (capDiv) html += `<div class="chart-container chart-full" id="${capDiv}"></div>`;
html += `</div>`;
// Measurements toggle
if (file.report.measurements && file.report.measurements.length) {
const tableId = `meas-${data.batch_id}-${file.filename}`;
html += `<button class="meas-toggle" onclick="document.getElementById('${tableId}').hidden = !document.getElementById('${tableId}').hidden">
Show/Hide Measurements (${file.report.measurements.length})
</button>`;
html += `<div id="${tableId}" hidden><table class="meas-table"><thead><tr>
<th>Feature</th><th>Nominal</th><th>Actual</th><th>Dev</th>
<th>Tol+</th><th>Tol-</th><th>USL</th><th>LSL</th><th>Status</th>
</tr></thead><tbody>`;
for (const m of file.report.measurements) {
const cls = m.in_tolerance ? "" : "oot";
html += `<tr class="${cls}">
<td>${esc(m.feature_name)}</td><td>${m.nominal}</td><td>${m.actual}</td>
<td>${m.deviation.toFixed(4)}</td><td>+${m.tolerance_plus}</td><td>${m.tolerance_minus}</td>
<td>${m.usl}</td><td>${m.lsl}</td>
<td>${m.in_tolerance ? "OK" : "OOT"}</td>
</tr>`;
}
html += `</tbody></table></div>`;
}
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); }
})();