document.addEventListener('DOMContentLoaded', () => { // API base URL const API_BASE = ''; // Non-blocking status signaling (replaces window.alert for routine feedback) const toastStack = document.getElementById('toast-stack'); const TOAST_ICONS = { success: '✓', error: '✕', warning: '!', info: 'ℹ' }; const TOAST_DEFAULT_MS = { success: 3800, info: 4200, warning: 5200, error: 6500 }; function notify(message, type = 'info', options = {}) { if (!toastStack || message == null || message === '') return; const kind = ['success', 'error', 'warning', 'info'].includes(type) ? type : 'info'; const duration = options.duration ?? TOAST_DEFAULT_MS[kind]; const el = document.createElement('div'); el.className = `toast toast-${kind}`; el.setAttribute('role', kind === 'error' ? 'alert' : 'status'); const icon = document.createElement('span'); icon.className = 'toast-icon'; icon.textContent = TOAST_ICONS[kind]; icon.setAttribute('aria-hidden', 'true'); const body = document.createElement('div'); body.className = 'toast-body'; body.textContent = String(message); const close = document.createElement('button'); close.type = 'button'; close.className = 'toast-close'; close.setAttribute('aria-label', 'Dismiss'); close.textContent = '×'; let hideTimer = null; const dismiss = () => { if (hideTimer) clearTimeout(hideTimer); el.classList.add('toast-out'); setTimeout(() => el.remove(), 180); }; close.addEventListener('click', dismiss); el.appendChild(icon); el.appendChild(body); el.appendChild(close); toastStack.appendChild(el); if (duration > 0) { hideTimer = setTimeout(dismiss, duration); } return dismiss; } // Stage Navigation const navButtons = document.querySelectorAll('.nav-btn'); const panels = document.querySelectorAll('.stage-panel'); const stageTitle = document.getElementById('stage-title'); const stageDesc = document.getElementById('stage-desc'); const nextBtn = document.getElementById('btn-global-action'); const STAGE_METADATA = { dataset: { title: 'Dataset Configuration', desc: 'Prepare and validate your image source path and annotation files.', next: 'ingest', nextLabel: 'Proceed to 3D Ingest' }, ingest: { title: '3D Ingest & Fusion', desc: 'Fuse Alpamayo/Waymo/A2D2 stubs and/or local image/video sequences with six-axis taxonomy stratification.', next: 'perception', nextLabel: 'Proceed to 3D Perception', gateKey: 'ingest_complete', completionOnly: true }, perception: { title: '3D Perception', desc: 'SAM-based 2D masks lifted to 3D bounding box proposals.', next: 'tracking', nextLabel: 'Proceed to Tracking', gateKey: 'perception_complete', completionOnly: true }, tracking: { title: 'Temporal Tracking', desc: 'Kalman + Hungarian association for ID-smooth multi-frame tracks.', next: 'quality-gate', nextLabel: 'Proceed to Quality Gate', gateKey: 'tracking_complete', completionOnly: true }, 'quality-gate': { title: 'Quality Gate', desc: 'Benchmark automated tracks against vendor GT with 3D and temporal metrics.', next: 'launch-gate', nextLabel: 'Proceed to Launch Gate', gateKey: 'quality_gate_passed', completionKey: 'benchmark_complete' }, 'launch-gate': { title: 'Launch Gate', desc: 'Validate safety thresholds before export is allowed.', next: 'model', nextLabel: 'Proceed to Model Setup', gateKey: 'launch_gate_passed' }, model: { title: 'Model Setup', desc: 'Choose a YOLOv8 base model and compute device architecture.', next: 'training', nextLabel: 'Proceed to Training' }, training: { title: 'Training Execution', desc: 'Fine-tune the selected YOLO model on your configured dataset.', next: 'inference', nextLabel: 'Proceed to Inference' }, inference: { title: 'Auto-Labeler Inference', desc: 'Run predictions on raw images using your fine-tuned weights.', next: 'grader', nextLabel: 'Proceed to Auto-Grader' }, grader: { title: 'Auto-Grader Quality', desc: 'Scan predictions for anomalies, confidence scores, and label quality.', next: 'mitl', nextLabel: 'Proceed to MITL Review' }, mitl: { title: 'Man-in-the-Loop Review', desc: 'Curate predictions, telemetry, and Chain-of-Causation reasoning traces.', next: 'benchmark', nextLabel: 'Proceed to Benchmarking' }, benchmark: { title: 'Model Benchmarking', desc: 'Compare standard vs. VLM autonomous driving models on safety-critical metrics.', next: 'export', nextLabel: 'Proceed to Export' }, export: { title: 'Export & Deploy', desc: 'Serialize model weights into production formats like ONNX.', next: 'mcp', nextLabel: 'Proceed to MCP Settings' }, mcp: { title: 'MCP Settings', desc: 'View, edit, and toggle Model Context Protocol servers in mcp_config.json.', next: 'ssam', nextLabel: 'Proceed to SSAM Safety' }, ssam: { title: 'SSAM Safety', desc: 'View street conflict metrics and add automated or manual severity annotations.', next: 'dataset', nextLabel: 'Back to Start' } }; let pipelineState = {}; let currentSequenceId = 'seq_001'; async function syncActiveSequence() { try { const cfg = await fetch(`${API_BASE}/api/config`).then((r) => r.json()); if (cfg?.sequence_id) { currentSequenceId = cfg.sequence_id; return; } } catch (e) { /* ignore */ } try { const st = await fetch(`${API_BASE}/api/pipeline/status?sequence_id=${encodeURIComponent(currentSequenceId)}`).then((r) => r.json()); if (st?.active_sequence_id) currentSequenceId = st.active_sequence_id; } catch (e) { /* ignore */ } } async function refreshPipelineStatus() { try { await syncActiveSequence(); const res = await fetch(`${API_BASE}/api/pipeline/status?sequence_id=${encodeURIComponent(currentSequenceId)}`); const data = await res.json(); pipelineState = data; if (data.active_sequence_id) currentSequenceId = data.active_sequence_id; updateGateBadges(); } catch (e) { /* backend offline */ } } function updateGateBadges() { navButtons.forEach(btn => { const stage = btn.dataset.stage; const meta = STAGE_METADATA[stage]; if (!meta || !meta.gateKey) return; let badge = btn.querySelector('.gate-badge'); if (!badge) { badge = document.createElement('span'); badge.className = 'gate-badge'; badge.style.cssText = 'margin-left:6px;font-size:10px;padding:2px 6px;border-radius:4px;'; btn.appendChild(badge); } // Completion-only stages: PASS when done, blank when not run (never FAIL). // Pass/fail gates (quality/launch): PASS/FAIL only after evaluation; blank beforehand. const verdict = pipelineState[meta.gateKey]; const completed = meta.completionKey ? pipelineState[meta.completionKey] : null; if (meta.completionOnly) { if (verdict === true) { badge.textContent = pipelineState.demo_stub ? 'STUB' : 'DONE'; badge.style.background = pipelineState.demo_stub ? '#fbbf2433' : '#00ffaa33'; badge.style.color = pipelineState.demo_stub ? '#fbbf24' : '#00ffaa'; } else { badge.textContent = ''; } return; } if (verdict === true) { badge.textContent = 'PASS'; badge.style.background = '#00ffaa33'; badge.style.color = '#00ffaa'; } else if (verdict === false) { badge.textContent = 'FAIL'; badge.style.background = '#ff444433'; badge.style.color = '#ff4444'; } else if (completed === true) { badge.textContent = 'RAN'; badge.style.background = '#3b82f633'; badge.style.color = '#93c5fd'; } else { badge.textContent = ''; } }); } refreshPipelineStatus(); function switchStage(stageId) { // Update nav buttons navButtons.forEach(btn => { if (btn.dataset.stage === stageId) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); // Update panels panels.forEach(panel => { if (panel.id === `panel-${stageId}`) { panel.classList.add('active'); } else { panel.classList.remove('active'); } }); // Update header metadata const meta = STAGE_METADATA[stageId]; stageTitle.textContent = meta.title; stageDesc.textContent = meta.desc; nextBtn.textContent = meta.nextLabel; nextBtn.dataset.next = meta.next; } navButtons.forEach(btn => { btn.addEventListener('click', () => { switchStage(btn.dataset.stage); }); }); nextBtn.addEventListener('click', () => { const currentStage = document.querySelector('.nav-btn.active')?.dataset.stage; const meta = STAGE_METADATA[currentStage]; if (meta && meta.gateKey && pipelineState[meta.gateKey] === false) { notify('Launch gate not passed. Complete the quality gate first.', 'warning'); return; } switchStage(nextBtn.dataset.next); }); // Model selection const modelCards = document.querySelectorAll('.model-card'); const modelDisplay = document.getElementById('selected-weights-display'); let selectedModel = 'yolov8n.pt'; modelCards.forEach(card => { card.addEventListener('click', () => { modelCards.forEach(c => c.classList.remove('active')); card.classList.add('active'); selectedModel = card.dataset.model; modelDisplay.textContent = selectedModel; }); }); // Subtabs within Training panel const subtabButtons = document.querySelectorAll('.tab-sub-btn'); const subtabContents = document.querySelectorAll('.tab-sub-content'); subtabButtons.forEach(btn => { btn.addEventListener('click', () => { subtabButtons.forEach(b => b.classList.remove('active')); subtabContents.forEach(c => c.classList.remove('active')); btn.classList.add('active'); document.getElementById(`subtab-${btn.dataset.tab}`).classList.add('active'); }); }); // Save Dataset Configuration const btnSaveDataset = document.getElementById('btn-save-dataset'); const dataYamlPath = document.getElementById('data-yaml-path'); const datasetSourcePath = document.getElementById('dataset-source-path'); btnSaveDataset.addEventListener('click', async () => { try { const res = await fetch(`${API_BASE}/api/config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ yaml_path: dataYamlPath.value, source_path: datasetSourcePath.value }) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); notify('Configuration saved (YAML/paths not yet executed).', 'success'); const cfgStatus = document.getElementById('config-exec-status'); if (cfgStatus) { cfgStatus.textContent = 'CONFIGURATION SAVED / not yet used by an execution — run Load & Preprocess.'; cfgStatus.dataset.modified = '1'; } } catch (e) { console.error(e); notify('Failed to save dataset configuration.', 'error'); } }); // Run Pre-Check document.getElementById('btn-validate').addEventListener('click', async () => { try { const res = await fetch(`${API_BASE}/api/precheck`); const data = await res.json(); const precheckType = data.verified ? 'success' : 'warning'; notify(`Pre-check: ${data.status}\n${data.message || ''}`, precheckType); refreshScriptStatus(data.scripts); } catch (e) { notify('Failed to complete pre-check.', 'error'); } }); // Device Selection Change Listener const selectDevice = document.getElementById('select-device'); const dgxSetupInstructions = document.getElementById('dgx-setup-instructions'); selectDevice.addEventListener('change', () => { if (selectDevice.value === 'dgx-spark') { dgxSetupInstructions.classList.remove('hidden'); } else { dgxSetupInstructions.classList.add('hidden'); } }); // Training state and polling let trainInterval = null; const btnStartTrain = document.getElementById('btn-start-train'); const btnStopTrain = document.getElementById('btn-stop-train'); const trainProgressSection = document.getElementById('train-progress-section'); const trainProgressBar = document.getElementById('train-progress-bar'); const progressPercent = document.getElementById('progress-percent'); const terminalLogs = document.getElementById('terminal-logs'); btnStartTrain.addEventListener('click', async () => { const epochs = document.getElementById('train-epochs').value; const batch = document.getElementById('train-batch').value; const device = document.getElementById('select-device').value; terminalLogs.textContent = 'Launching training process...\n'; btnStartTrain.classList.add('hidden'); btnStopTrain.classList.remove('hidden'); trainProgressSection.classList.remove('hidden'); try { const res = await fetch(`${API_BASE}/api/train/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: selectedModel, epochs: parseInt(epochs), batch: parseInt(batch), device: device, data: dataYamlPath.value }) }); const data = await res.json(); if (!res.ok) { terminalLogs.textContent += `Error: ${JSON.stringify(data)}\n`; resetTrainingUI(); notify(data.detail?.message || data.detail || 'Failed to start training', 'error'); return; } renderEvidenceCard('evidence-training', { status: 'RUNNING', execution_id: data.execution_id, process_id: data.process_id, command: data.command, message: 'Training process spawned', }); pollTraining(); trainInterval = setInterval(pollTraining, 1500); } catch (e) { terminalLogs.textContent += `Error starting training: ${e.message}\n`; resetTrainingUI(); } }); btnStopTrain.addEventListener('click', async () => { try { await fetch(`${API_BASE}/api/train/stop`, { method: 'POST' }); terminalLogs.textContent += '\n--- Training terminated by user ---\n'; } catch (e) { console.error(e); } resetTrainingUI(); }); function resetTrainingUI() { if (trainInterval) { clearInterval(trainInterval); trainInterval = null; } btnStartTrain.classList.remove('hidden'); btnStopTrain.classList.add('hidden'); } async function pollTraining() { try { const res = await fetch(`${API_BASE}/api/train/status`); const status = await res.json(); // Append logs if (status.logs) { terminalLogs.textContent = status.logs; terminalLogs.scrollTop = terminalLogs.scrollHeight; } // Update progress const pct = Math.min(100, Math.round(status.progress * 100)); trainProgressBar.style.width = `${pct}%`; progressPercent.textContent = `${pct}%`; // Draw loss curves if we have values if (status.losses && status.losses.length > 0) { drawLossChart(status.losses); } if (!status.running) { const verdict = status.status || (status.exit_code === 0 ? 'SUCCEEDED' : 'FAILED'); terminalLogs.textContent += `\n--- Process finished: ${verdict} exit_code=${status.exit_code} ---\n`; renderEvidenceCard('evidence-training', { status: verdict, execution_id: status.execution_id, exit_code: status.exit_code, process_id: status.process_id, command: status.command, metrics: { epochs_observed: status.epochs_observed, epochs_requested: status.epochs_requested, checkpoint: status.checkpoint, losses_parsed: (status.losses || []).length, }, message: `Training ${verdict}`, }); refreshExecutionConsole(); resetTrainingUI(); } } catch (e) { console.error(e); } } // Canvas Loss Curve drawer (Zero external dependencies) function drawLossChart(losses) { const canvas = document.getElementById('loss-chart'); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); // Background ctx.fillStyle = '#04060b'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Margins const margin = 40; const w = canvas.width - margin * 2; const h = canvas.height - margin * 2; const maxLoss = Math.max(...losses, 1.0); const minLoss = 0; // Draw grid lines ctx.strokeStyle = '#1e293b'; ctx.lineWidth = 1; for (let i = 0; i <= 4; i++) { const y = margin + (h / 4) * i; ctx.beginPath(); ctx.moveTo(margin, y); ctx.lineTo(canvas.width - margin, y); ctx.stroke(); // Y label ctx.fillStyle = '#6b7280'; ctx.font = '10px sans-serif'; ctx.fillText((maxLoss - (maxLoss / 4) * i).toFixed(2), 10, y + 4); } // Draw line if (losses.length < 2) return; ctx.strokeStyle = '#00c896'; ctx.lineWidth = 2.5; ctx.shadowColor = 'rgba(0, 200, 150, 0.4)'; ctx.shadowBlur = 8; ctx.beginPath(); losses.forEach((loss, i) => { const x = margin + (w / (losses.length - 1)) * i; const y = margin + h - (loss / maxLoss) * h; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } }); ctx.stroke(); ctx.shadowBlur = 0; // Reset shadow // Draw points ctx.fillStyle = '#3b82f6'; losses.forEach((loss, i) => { const x = margin + (w / (losses.length - 1)) * i; const y = margin + h - (loss / maxLoss) * h; ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fill(); }); // X labels ctx.fillStyle = '#6b7280'; ctx.fillText('Epochs progress', canvas.width / 2 - 30, canvas.height - 10); } // Auto-Labeler Inference const btnRunInfer = document.getElementById('btn-run-infer'); const selectInferImage = document.getElementById('select-infer-image'); const predictionImg = document.getElementById('prediction-img'); const valInferConf = document.getElementById('val-infer-conf'); const valInferIou = document.getElementById('val-infer-iou'); document.getElementById('infer-conf').addEventListener('input', (e) => { valInferConf.textContent = e.target.value; }); document.getElementById('infer-iou').addEventListener('input', (e) => { valInferIou.textContent = e.target.value; }); btnRunInfer.addEventListener('click', async () => { btnRunInfer.textContent = 'Predicting...'; btnRunInfer.disabled = true; try { const res = await fetch(`${API_BASE}/api/infer/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ weights: document.getElementById('infer-weights').value, conf: parseFloat(document.getElementById('infer-conf').value), iou: parseFloat(document.getElementById('infer-iou').value), source: datasetSourcePath.value }) }); const data = await res.json(); if (!res.ok) { const detail = data.detail?.message || data.detail || JSON.stringify(data); notify(`Inference failed: ${detail}`, 'error'); renderEvidenceCard('evidence-inference', { status: 'FAILED', execution_id: data.detail?.execution_id || data.execution_id, message: detail, }); return; } selectInferImage.innerHTML = ''; if (data.images && data.images.length > 0) { data.images.forEach(img => { const opt = document.createElement('option'); opt.value = img; opt.textContent = img; selectInferImage.appendChild(opt); }); loadImage(data.images[0]); } else { selectInferImage.innerHTML = ''; } renderEvidenceCard('evidence-inference', { status: data.status, execution_id: data.execution_id, duration_ms: data.duration_ms, model: data.model, checkpoint: data.checkpoint, metrics: { discovered: data.records_discovered, processed: data.records_processed, succeeded: data.records_succeeded, failed: data.records_failed, inference_calls: data.inference_calls, predictions: data.predictions_generated, output_dir: data.output_dir, exit_code: data.exit_code, }, message: data.message, }); refreshExecutionConsole(); if (data.status === 'SUCCEEDED' || data.status === 'PARTIAL_SUCCESS') { notify(`Inference ${data.status}: ${data.records_succeeded}/${data.records_discovered} images`, 'success'); } else { notify(`Inference ${data.status}: ${data.message || 'see evidence'}`, 'warning'); } } catch (e) { console.error(e); notify('Inference execution failed.', 'error'); } finally { btnRunInfer.textContent = '⚡ Run Auto-Labeler'; btnRunInfer.disabled = false; } }); selectInferImage.addEventListener('change', (e) => { if (e.target.value) { loadImage(e.target.value); } }); function loadImage(filename) { predictionImg.src = `${API_BASE}/api/images/${filename}?t=${Date.now()}`; } // Auto-Grader Quality Diagnostics const btnRunGrader = document.getElementById('btn-run-grader'); const graderScore = document.getElementById('grader-score'); const statTotalImgs = document.getElementById('stat-total-imgs'); const statTotalPreds = document.getElementById('stat-total-preds'); const graderIssuesList = document.getElementById('grader-issues-list'); btnRunGrader.addEventListener('click', async () => { btnRunGrader.textContent = 'Scanning...'; btnRunGrader.disabled = true; try { const res = await fetch(`${API_BASE}/api/grade`); const data = await res.json(); if (!res.ok) { notify(data.detail?.message || data.detail || 'Grader failed', 'error'); renderEvidenceCard('evidence-grader', { status: 'FAILED', execution_id: data.detail?.execution_id || data.execution_id, message: data.detail?.message || data.detail, }); return; } graderScore.textContent = data.quality_score != null ? Number(data.quality_score).toFixed(1) : '—'; statTotalImgs.textContent = data.total_images ?? '—'; statTotalPreds.textContent = data.total_predictions ?? '—'; graderIssuesList.innerHTML = ''; if (data.status === 'NOT_EXECUTED') { graderIssuesList.innerHTML = '
NOT_EXECUTED — no predictions artifact. This is not a perfect score.
'; } else if (data.issues && data.issues.length > 0) { data.issues.forEach(issue => { const el = document.createElement('div'); el.className = `issue-card ${issue.severity.toLowerCase()}`; el.innerHTML = `
${issue.type} (${issue.severity})

${issue.description}

💡 Recommendation: ${issue.recommendation}
`; graderIssuesList.appendChild(el); }); } else { graderIssuesList.innerHTML = '
No quality anomalies reported by grader output (not a claim of ground-truth perfection).
'; } renderEvidenceCard('evidence-grader', { status: data.status, execution_id: data.execution_id, duration_ms: data.duration_ms, metrics: data.metrics || { total_images: data.total_images, total_predictions: data.total_predictions, quality_score: data.quality_score, }, message: data.message, }); refreshExecutionConsole(); } catch (e) { console.error(e); notify('Failed to run quality diagnostics.', 'error'); } finally { btnRunGrader.textContent = 'Run Quality Diagnostics'; btnRunGrader.disabled = false; } }); // ONNX Weight Export const btnExportWeights = document.getElementById('btn-export-weights'); const exportStatusBox = document.getElementById('export-status-box'); btnExportWeights.addEventListener('click', async () => { const format = document.querySelector('input[name="export-format"]:checked').value; exportStatusBox.classList.remove('hidden'); exportStatusBox.textContent = `Compiling YOLO model weights to ${format.toUpperCase()}...`; btnExportWeights.disabled = true; try { const res = await fetch(`${API_BASE}/api/export`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ weights: document.getElementById('infer-weights').value, format: format }) }); const data = await res.json(); exportStatusBox.textContent = `✓ Export complete! File generated: ${data.exported_file}`; } catch (e) { console.error(e); exportStatusBox.textContent = `Error during compilation: ${e.message}`; } finally { btnExportWeights.disabled = false; } }); // AI Copilot Triage Assistant const btnCopilotAsk = document.getElementById('btn-copilot-ask'); const copilotContainer = document.getElementById('copilot-response-container'); const copilotCard = document.querySelector('.copilot-card'); function parseMarkdown(md) { if (!md) return ''; let html = md .replace(/&/g, '&') .replace(//g, '>'); // Fenced code blocks html = html.replace(/```([\s\S]*?)```/g, (match, code) => { return `
${code.trim()}
`; }); // Headings html = html.replace(/^### (.*?)$/gm, '

$1

'); html = html.replace(/^## (.*?)$/gm, '

$1

'); html = html.replace(/^# (.*?)$/gm, '

$1

'); // Bullet lists html = html.replace(/^\s*[-*+]\s+(.*?)$/gm, '
  • $1
  • '); html = html.replace(/(
  • [\s\S]*?<\/li>)/g, ''); html = html.replace(/<\/ul>\s*