/* global JSZip, XLSX, Papa */ // Global variables to store state across the app let lastTrainedModelPath = null; let lastUsedTargetColumn = null; let lastCleanedCsvBlob = null; let lastDatasetName = null; let summaryResults = {}; let dataSetPreview = null; let dataSetStats = null; let dataPreprocessing = {}; let shapPlot = null; let PlotsPredictionResults = null; let trainingId = null; let pollingIntervalTraining = null; let currentShapRequestId = null; let pngResultTrainingForPrediction = null; let appMode = 1; // 1 = train, 2 = predict let trainingCountdownInterval = null; let trainingModelInterval = null; let trainingTotalSeconds = 0; let detectedTimeColumns = []; let wizardCurrentStep = 1; let wizardMaxStep = 1; let datasetLoaded = false; let totalDataRows = 0; // ── Advanced Options ───────────────────────────────────────────────────────── const ADVANCED_METRICS = { classification: [ { value: 'accuracy', label: 'Accuracy' }, { value: 'roc_auc', label: 'ROC AUC' }, { value: 'f1_macro', label: 'F1 Macro' }, ], regression: [ { value: 'root_mean_squared_error', label: 'RMSE' }, { value: 'mean_absolute_error', label: 'MAE' }, { value: 'r2', label: 'R²' }, ], timeseries: [ { value: 'MASE', label: 'MASE (Mean Absolute Scaled Error)' }, { value: 'MAPE', label: 'MAPE (Mean Absolute Percentage Error)' }, { value: 'WQL', label: 'WQL (Weighted Quantile Loss)' }, ], }; const ADVANCED_MODELS = { // Order matches AutoGluon's actual training sequence (KNN first, then boosters, then NNs) tabular: [ { value: 'KNN', label: 'KNeighbors' }, { value: 'GBM', label: 'LightGBM' }, { value: 'XGB', label: 'XGBoost' }, { value: 'CAT', label: 'CatBoost' }, { value: 'RF', label: 'Random Forest' }, { value: 'XT', label: 'Extra Trees' }, { value: 'NN_TORCH', label: 'Neural Net (Torch)' }, { value: 'FASTAI', label: 'Neural Net (FastAI)' }, ], // AutoGluon TimeSeriesPredictor medium_quality preset ("light" hyperparameters) timeseries: [ { value: 'Naive', label: 'Naive' }, { value: 'SeasonalNaive', label: 'Seasonal Naive' }, { value: 'ETS', label: 'ETS' }, { value: 'Theta', label: 'Theta' }, { value: 'RecursiveTabular', label: 'Recursive Tabular' }, { value: 'DirectTabular', label: 'Direct Tabular' }, { value: 'TemporalFusionTransformer',label: 'TFT' }, { value: 'Chronos', label: 'Chronos' }, ], }; function toggleValSplit() { const checked = document.getElementById('adv-use-cv').checked; const container = document.getElementById('adv-val-split-container'); if (container) container.style.display = checked ? 'none' : 'flex'; } function toggleAdvancedOptions() { const body = document.getElementById('advanced-options-body'); const arrow = document.getElementById('advanced-arrow'); const isOpen = body.style.display !== 'none'; body.style.display = isOpen ? 'none' : 'block'; arrow.style.transform = isOpen ? '' : 'rotate(90deg)'; } function openAdvancedOptions() { const body = document.getElementById('advanced-options-body'); const arrow = document.getElementById('advanced-arrow'); if (body.style.display === 'none') { body.style.display = 'block'; arrow.style.transform = 'rotate(90deg)'; } } function updateAdvancedOptions() { const isTimeSeries = detectedTimeColumns.length > 0; const targetCol = document.getElementById('target-column')?.value; const targetStat = (dataSetStats || []).find(s => s.name === targetCol); const isNumericType = targetStat?.type === 'Numeric'; const uniqueCount = targetStat?.unique ?? Infinity; // Numeric columns with few unique values are likely multiclass (mirrors AutoGluon heuristic) const isNumeric = isNumericType && uniqueCount > 20; const taskType = isTimeSeries ? 'timeseries' : (isNumeric ? 'regression' : 'classification'); // Metric dropdown const metricSelect = document.getElementById('adv-eval-metric'); if (metricSelect) { metricSelect.innerHTML = ''; ADVANCED_METRICS[taskType].forEach(m => { const opt = document.createElement('option'); opt.value = m.value; opt.textContent = m.label; metricSelect.appendChild(opt); }); } // Model checkboxes const modelList = isTimeSeries ? ADVANCED_MODELS.timeseries : ADVANCED_MODELS.tabular; const container = document.getElementById('adv-models-container'); if (container) { container.innerHTML = ''; modelList.forEach(m => { const lbl = document.createElement('label'); lbl.style.cssText = 'display:flex;align-items:center;gap:4px;padding:3px 8px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:0.88em;background:#fff;'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.value = m.value; cb.name = 'adv-model'; cb.checked = true; lbl.appendChild(cb); lbl.appendChild(document.createTextNode(m.label)); container.appendChild(lbl); }); } // Cross-validation: only for tabular const cvContainer = document.getElementById('adv-cv-container'); if (cvContainer) cvContainer.style.display = isTimeSeries ? 'none' : 'flex'; // Prediction horizon: only for time series const predLenContainer = document.getElementById('pred-len-container'); if (predLenContainer) predLenContainer.style.display = isTimeSeries ? 'flex' : 'none'; // Auto-open panel when time series detected so user sees prediction horizon if (isTimeSeries) openAdvancedOptions(); } function resetAdvancedOptions() { const body = document.getElementById('advanced-options-body'); const arrow = document.getElementById('advanced-arrow'); if (body) body.style.display = 'none'; if (arrow) arrow.style.transform = ''; } // ───────────────────────────────────────────────────────────────────────────── function setUploadStatus(key) { const el = document.getElementById("upload-status-text"); if (el) { el.textContent = t(key); } } function toggleFormatsIcon(show) { const icon = document.getElementById("upload-formats-icon"); if (icon) { icon.style.display = show ? "inline-block" : "none"; } } function setWizardStep(step) { if (!Number.isFinite(step) || step < 1) return; if (step > wizardMaxStep) return; wizardCurrentStep = step; syncWizardPanels(step); updateStepper(step); } function unlockWizardStep(step) { wizardMaxStep = Math.max(wizardMaxStep, step); } function syncWizardPanels(activeStep) { const panels = document.querySelectorAll(".wizard-panel"); panels.forEach((panel) => { const s = parseInt(panel.dataset.step, 10) || 1; const isUnlocked = s <= wizardMaxStep; const isActive = s === activeStep; panel.classList.toggle("locked", !isUnlocked); panel.classList.toggle("active", isActive && isUnlocked); panel.style.display = isActive && isUnlocked ? "block" : "none"; }); const hero = document.getElementById("training-hero"); if (hero) { hero.style.display = activeStep === 1 ? "block" : "none"; } const uploadSection = document.getElementById("csv-upload-section"); const preview = document.getElementById("csv-preview"); if (uploadSection) uploadSection.style.display = activeStep === 1 ? "block" : "none"; if (preview) preview.style.display = activeStep === 1 && datasetLoaded ? "block" : "none"; } function updateStepper(stepNumber) { const steps = document.querySelectorAll(".stepper .step"); if (!steps.length) return; steps.forEach((step) => { const idx = parseInt(step.dataset.step, 10); step.classList.toggle("active", idx === stepNumber); step.classList.toggle("done", idx < stepNumber && idx <= wizardMaxStep); step.classList.toggle("locked", idx > wizardMaxStep); const nextUnlocked = wizardMaxStep > wizardCurrentStep ? wizardCurrentStep + 1 : null; const shouldHighlight = nextUnlocked && idx === nextUnlocked; step.classList.toggle("highlight", shouldHighlight && !step.classList.contains("active") && !step.classList.contains("done")); }); } function isProbablyDate(val) { if (val === null || val === undefined) return false; const s = `${val}`.trim(); if (s.length < 6) return false; if (!/[\/\-:T]/.test(s)) return false; return !isNaN(Date.parse(s)); } function miniHistogram(values) { if (!values || !values.length) return ""; const bins = 5; const vmin = Math.min(...values); const vmax = Math.max(...values); if (vmin === vmax) return "▇▇▇▇▇"; const width = (vmax - vmin) / bins || 1; const counts = new Array(bins).fill(0); values.forEach(v => { let idx = Math.floor((v - vmin) / width); if (idx >= bins) idx = bins - 1; counts[idx] += 1; }); const maxCount = Math.max(...counts) || 1; const blocks = ['▁','▂','▃','▅','▇']; return counts.map(c => { const level = Math.floor((c / maxCount) * (blocks.length - 1)); return blocks[level]; }).join(''); } // On DOM ready: set language and update time display function toggleDarkMode() { const enabled = document.body.classList.toggle("dark-mode"); localStorage.setItem("darkMode", enabled ? "1" : "0"); document.getElementById("dm-thumb").classList.toggle("dm-on", enabled); } window.addEventListener("DOMContentLoaded", () => { const savedLang = localStorage.getItem("selectedLanguage") || "en"; document.getElementById("language-select").value = savedLang; changeLanguage(savedLang); if (localStorage.getItem("darkMode") === "1") { document.body.classList.add("dark-mode"); const thumb = document.getElementById("dm-thumb"); if (thumb) thumb.classList.add("dm-on"); } updateTimeDisplay(); setWizardStep(1); initCsvDropzone(); const stepper = document.querySelector(".stepper"); if (stepper) { stepper.addEventListener("click", (e) => { const stepEl = e.target.closest(".step"); if (!stepEl) return; const step = parseInt(stepEl.dataset.step, 10); if (step <= wizardMaxStep) { setWizardStep(step); } }); } }); function initCsvDropzone() { const dz = document.getElementById("csv-dropzone"); const input = document.getElementById("upload-csv"); if (!dz || !input) return; const prevent = (e) => { e.preventDefault(); e.stopPropagation(); }; ["dragenter", "dragover"].forEach(evt => { dz.addEventListener(evt, (e) => { prevent(e); dz.classList.add("drag-over"); }); }); ["dragleave", "drop"].forEach(evt => { dz.addEventListener(evt, (e) => { prevent(e); dz.classList.remove("drag-over"); }); }); dz.addEventListener("drop", (e) => { const files = e.dataTransfer?.files; if (!files || !files.length) return; const file = files[0]; const dt = new DataTransfer(); dt.items.add(file); input.files = dt.files; input.dispatchEvent(new Event("change")); }); dz.addEventListener("click", () => { input.click(); }); } // Show or hide the training panel function showTrainingOverlay(show) { const panel = document.getElementById("training-panel"); if (!panel) return; if (show) { panel.classList.add("visible"); } else { panel.classList.remove("visible"); } } function stopTrainingCountdown() { if (trainingCountdownInterval) { clearInterval(trainingCountdownInterval); trainingCountdownInterval = null; } const countdownEl = document.getElementById("training-countdown"); if (countdownEl) countdownEl.textContent = "--:--"; const ringEl = document.getElementById("tp-ring-fill"); if (ringEl) ringEl.style.strokeDashoffset = "238.76"; const fillEl = document.getElementById("tp-progress-fill"); if (fillEl) fillEl.style.width = "0%"; const pctEl = document.getElementById("tp-progress-pct"); if (pctEl) pctEl.textContent = "0%"; // Reset panel title and dot if they were changed to finalizing state const dot = document.querySelector('.tp-pulse-dot'); if (dot) dot.classList.remove('finalizing'); const title = document.querySelector('.tp-title'); if (title) { title.removeAttribute('data-i18n-active'); title.textContent = t('trainingOverlayTitle') || 'Training model…'; } } function showFinalizingState() { stopModelAnimation(); // Amber pulsing dot const dot = document.querySelector('.tp-pulse-dot'); if (dot) dot.classList.add('finalizing'); // Update title const title = document.querySelector('.tp-title'); if (title) title.textContent = t('finalizingTitle') || 'Generating results…'; // Keep timer at 0:00 and bar at 100% const countdownEl = document.getElementById("training-countdown"); if (countdownEl) countdownEl.textContent = "0:00"; const fillEl = document.getElementById("tp-progress-fill"); if (fillEl) fillEl.style.width = "100%"; const pctEl = document.getElementById("tp-progress-pct"); if (pctEl) pctEl.textContent = "100%"; // Append finalizing rows to model list const listEl = document.getElementById("tp-models-list"); if (!listEl) return; const finalizingSteps = [ t('finalizingOOF') || 'Computing OOF predictions (unseen data)', t('finalizingFeatureImportance') || 'Computing feature importance', t('finalizingPlots') || 'Generating validation plots', t('finalizingReport') || 'Preparing leaderboard', ]; const divider = document.createElement('div'); divider.className = 'tp-divider'; divider.id = 'tp-finalizing-divider'; listEl.appendChild(divider); finalizingSteps.forEach((label, i) => { const row = document.createElement('div'); row.className = 'tp-model-row training'; row.id = `tp-finalizing-${i}`; row.innerHTML = ` ${label} `; listEl.appendChild(row); }); listEl.lastElementChild?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } function startTrainingCountdown(totalSeconds) { stopTrainingCountdown(); const countdownEl = document.getElementById("training-countdown"); if (!countdownEl || !Number.isFinite(totalSeconds)) return; trainingTotalSeconds = Math.max(1, totalSeconds); let remaining = Math.floor(totalSeconds); const circumference = 238.76; // 2 * π * 38 const render = () => { const mins = Math.floor(remaining / 60); const secs = remaining % 60; countdownEl.textContent = `${mins}:${secs.toString().padStart(2, "0")}`; const pct = remaining / trainingTotalSeconds; const ringEl = document.getElementById("tp-ring-fill"); if (ringEl) ringEl.style.strokeDashoffset = circumference * (1 - pct); const elapsed = (1 - pct) * 100; const fillEl = document.getElementById("tp-progress-fill"); if (fillEl) fillEl.style.width = `${elapsed.toFixed(1)}%`; const pctEl = document.getElementById("tp-progress-pct"); if (pctEl) pctEl.textContent = `${Math.round(elapsed)}%`; }; render(); trainingCountdownInterval = setInterval(() => { remaining = Math.max(0, remaining - 1); render(); if (remaining <= 0) { clearInterval(trainingCountdownInterval); trainingCountdownInterval = null; showFinalizingState(); } }, 1000); } // ── Training Panel – model queue animation ──────────────────────────────────── function initTrainingPanel(selectedModels, datasetName, targetCol) { const nameEl = document.getElementById("tp-dataset-name"); const targetEl = document.getElementById("tp-target-col"); if (nameEl) nameEl.textContent = datasetName; if (targetEl) targetEl.textContent = `▸ ${targetCol}`; const listEl = document.getElementById("tp-models-list"); if (!listEl) return; listEl.innerHTML = ''; selectedModels.forEach((model, i) => { const row = document.createElement('div'); row.className = 'tp-model-row pending'; row.id = `tp-model-${i}`; row.innerHTML = ` ${model.label} ${t('modelPending') || 'Pending'} `; listEl.appendChild(row); }); } function startModelAnimation(nModels, totalSeconds) { if (trainingModelInterval) clearInterval(trainingModelInterval); if (nModels === 0) return; let currentIdx = 0; const perModel = Math.max(3000, (totalSeconds * 1000) / nModels); _markModelTraining(currentIdx); trainingModelInterval = setInterval(() => { _markModelDone(currentIdx); currentIdx++; if (currentIdx < nModels) { _markModelTraining(currentIdx); } else { clearInterval(trainingModelInterval); trainingModelInterval = null; } }, perModel); } function _markModelTraining(idx) { const row = document.getElementById(`tp-model-${idx}`); if (!row) return; row.className = 'tp-model-row training'; const icon = row.querySelector('.tp-model-icon'); icon.className = 'tp-model-icon spin'; icon.textContent = '↻'; row.querySelector('.tp-model-badge').textContent = t('modelTraining') || 'Training'; row.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } function _markModelDone(idx) { const row = document.getElementById(`tp-model-${idx}`); if (!row) return; row.className = 'tp-model-row done'; const icon = row.querySelector('.tp-model-icon'); icon.className = 'tp-model-icon'; icon.textContent = '✓'; row.querySelector('.tp-model-badge').textContent = t('modelDone') || 'Done'; } function stopModelAnimation() { if (trainingModelInterval) { clearInterval(trainingModelInterval); trainingModelInterval = null; } document.querySelectorAll('.tp-model-row').forEach(row => { if (row.classList.contains('pending') || row.classList.contains('training')) { row.className = 'tp-model-row done'; const icon = row.querySelector('.tp-model-icon'); if (icon) { icon.className = 'tp-model-icon'; icon.textContent = '✓'; } const badge = row.querySelector('.tp-model-badge'); if (badge) badge.textContent = t('modelDone') || 'Done'; } }); } function getGroqKey() { return localStorage.getItem("groq_api_key"); } function showGroqModal() { const modal = document.getElementById("groq-modal"); if (!modal) return; modal.classList.remove("hidden"); const input = document.getElementById("groq-api-key-input"); if (input) { input.value = ""; input.focus(); } } function hideGroqModal() { const modal = document.getElementById("groq-modal"); if (modal) modal.classList.add("hidden"); } // Update the time limit display based on slider value function updateTimeDisplay() { const slider = document.getElementById("training-time-limit"); const display = document.getElementById("time-limit-display"); const minutes = parseInt(slider.value, 10); const hours = Math.floor(minutes / 60); const mins = minutes % 60; let formatted = ""; if (minutes === 0) { formatted = "0 min"; } else if (mins === 0) { formatted = `${hours}h`; } else if (hours === 0) { formatted = `${mins} min`; } else { formatted = `${hours}h${mins}`; } display.textContent = formatted; } function updatePredLenDisplay() { const slider = document.getElementById("prediction-length-percent"); const display = document.getElementById("pred-len-display"); if (!slider || !display) return; display.textContent = `${slider.value}%`; } // Toggle settings menu visibility const settingsToggle = document.getElementById("settings-toggle"); const settingsMenu = document.getElementById("settings-menu"); settingsToggle.addEventListener("click", () => { settingsMenu.classList.toggle("hidden"); }); // Close settings menu if clicking outside document.addEventListener("click", (event) => { if ( !settingsMenu.contains(event.target) && !settingsToggle.contains(event.target) ) { settingsMenu.classList.add("hidden"); } }); // Change language and update UI translations function changeLanguage(lang) { if (!lang) { lang = document.getElementById("language-select").value; } // Save selected language localStorage.setItem("selectedLanguage", lang); // Apply translations to elements const elements = document.querySelectorAll("[data-i18n]"); elements.forEach((el) => { const key = el.getAttribute("data-i18n"); const val = translations[lang] && translations[lang][key]; if (!val) return; // For help icons, keep the "?" and set title only if (el.classList.contains("help-icon")) { el.setAttribute("title", val); } else { el.textContent = val; } }); // Special placeholder for chat input const chatInput = document.getElementById("chat-input"); if (translations[lang]["chatPlaceholder"]) { chatInput.placeholder = translations[lang]["chatPlaceholder"]; } } // Translation helper function t(key) { const lang = localStorage.getItem("selectedLanguage") || "en"; return translations[lang] && translations[lang][key] ? translations[lang][key] : key; } // Switch between train and predict modes function switchMode() { const mode = document.querySelector('input[name="mode"]:checked').value; document.getElementById('training-section').style.display = mode === 'train' ? 'block' : 'none'; document.getElementById('predict-section').style.display = mode === 'predict' ? 'block' : 'none'; appMode = mode === 'train' ? 1 : 2; } // Initial call to update time display on DOM load document.addEventListener("DOMContentLoaded", updateTimeDisplay); // Toggle between dataset and model upload sections function toggleLoadChoice() { const choice = document.querySelector('input[name="load-choice"]:checked').value; document.getElementById('csv-upload-section').style.display = (choice === 'dataset') ? 'block' : 'none'; document.getElementById('model-upload-section').style.display = (choice === 'model') ? 'block' : 'none'; } // Remove uploaded model file and reset UI function removeModelFile() { const input = document.getElementById('upload-model'); input.value = ''; input.style.display = 'inline'; document.getElementById('model-file-info').style.display = 'none'; document.getElementById('model-file-name').textContent = ''; document.getElementById('model-status').style.display = 'none'; } // Parse ARFF file content into a rows array (header row + data rows) function parseArff(content) { const lines = content.split('\n'); const columns = []; const dataLines = []; let inData = false; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('%')) continue; if (inData) { dataLines.push(trimmed); } else if (trimmed.toLowerCase().startsWith('@attribute')) { const match = trimmed.match(/@attribute\s+['"]?([^'"@\s]+)['"]?/i); if (match) columns.push(match[1]); } else if (trimmed.toLowerCase().startsWith('@data')) { inData = true; } } if (columns.length === 0 || dataLines.length === 0) return null; const parsed = Papa.parse(dataLines.join('\n'), { header: false, skipEmptyLines: true }); return [columns, ...parsed.data]; } // Handle CSV upload, preview, and stats document.getElementById('upload-csv').addEventListener('change', function () { const file = this.files[0]; const fileName = file ? file.name : ""; const fileNameLower = fileName.toLowerCase(); const nameWithoutExtension = fileName.replace(/\.[^/.]+$/, ""); // Supprime la dernière extension lastDatasetName = nameWithoutExtension; const acceptedExtensions = ['.csv', '.xls', '.xlsx', '.xlsm', '.arff']; if (file && acceptedExtensions.some(ext => fileNameLower.endsWith(ext))) { setUploadStatus("uploadStatusLoading"); // Hide input and label document.querySelectorAll('#upload-csv-label').forEach(el => el.style.display = 'none'); const fileNameSpan = document.getElementById('csv-file-name'); const fileInfo = document.getElementById('csv-file-info'); const selectedWrapper = document.getElementById('csv-selected'); if (fileNameSpan) fileNameSpan.textContent = file.name; if (fileInfo) fileInfo.style.display = 'inline-flex'; if (selectedWrapper) selectedWrapper.style.display = 'block'; toggleFormatsIcon(false); const dropzone = document.getElementById("csv-dropzone"); if (dropzone) dropzone.style.display = "none"; const previewTable = document.getElementById('preview-table'); previewTable.innerHTML = ''; // Reset table const previewLoading = document.getElementById('preview-loading'); if (previewLoading) previewLoading.style.display = 'inline-block'; document.getElementById('csv-preview').style.display = 'none'; const ext = file.name.split('.').pop().toLowerCase(); const reader = new FileReader(); reader.onerror = function () { const previewLoading = document.getElementById('preview-loading'); if (previewLoading) previewLoading.style.display = 'none'; showAlert(t("pleaseSelectFile"), 'warning'); setUploadStatus("uploadStatusIdle"); }; if (ext === 'csv') { reader.onload = function (e) { const content = e.target.result; const parsed = Papa.parse(content, { header: false, skipEmptyLines: true }); buildPreview(parsed.data); setUploadStatus("uploadStatusLoaded"); toggleFormatsIcon(false); }; reader.readAsText(file); } else if (ext === 'arff') { reader.onload = function (e) { const rows = parseArff(e.target.result); if (rows) { buildPreview(rows); setUploadStatus("uploadStatusLoaded"); toggleFormatsIcon(false); } else { const previewLoading = document.getElementById('preview-loading'); if (previewLoading) previewLoading.style.display = 'none'; showAlert(t("pleaseSelectFile"), 'warning'); setUploadStatus("uploadStatusIdle"); } }; reader.readAsText(file); } else if (['xls', 'xlsx', 'xlsm'].includes(ext)) { reader.onload = function (e) { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; const json = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "" }); buildPreview(json); setUploadStatus("uploadStatusLoaded"); toggleFormatsIcon(false); }; reader.readAsArrayBuffer(file); } } else { const previewLoading = document.getElementById('preview-loading'); if (previewLoading) previewLoading.style.display = 'none'; showAlert(t("pleaseSelectFile"), 'warning'); this.value = ''; setUploadStatus("uploadStatusIdle"); toggleFormatsIcon(true); } }); // Build statistics for each column in the dataset function buildStats(rows) { const header = rows[0]; const dataRows = rows.slice(1); const numRows = dataRows.length || 1; const stats = header.map((col, idx) => { const colData = dataRows.map(row => row[idx]).filter(val => val !== "" && val !== null && val !== undefined); const isDate = colData.every(val => isProbablyDate(val)); const isNumeric = !isDate && colData.every(val => !isNaN(parseFloat(val))); const parsedData = colData.map(val => isNumeric ? parseFloat(val) : val); const missing = dataRows.length - colData.length; const stat = { name: col, type: isDate ? "Date" : (isNumeric ? "Numeric" : "Categorical"), missing: missing, mean: isNumeric ? (parsedData.reduce((a, b) => a + b, 0) / parsedData.length).toFixed(2) : "-", std: isNumeric ? Math.sqrt(parsedData.map(x => (x - parsedData.reduce((a, b) => a + b, 0) / parsedData.length) ** 2).reduce((a, b) => a + b, 0) / parsedData.length).toFixed(2) : "-", unique: colData.length ? new Set(colData).size : 0, min: isNumeric ? Math.min(...parsedData).toFixed(2) : "-", max: isNumeric ? Math.max(...parsedData).toFixed(2) : "-", distribution: isNumeric ? miniHistogram(parsedData) : "", qualityClass: missing / numRows > 0.4 ? "quality-bad" : missing / numRows > 0.15 ? "quality-warn" : "quality-good", }; return stat; }); dataSetStats = stats; displayStatsTable(stats); } // Display the statistics table in the UI function displayStatsTable(stats) { const table = document.getElementById('stats-table'); table.innerHTML = ''; const headerRow = document.createElement('tr'); const headers = [ { key: 'statsColumn' }, { key: 'statsType' }, { key: 'statsMissing' }, { key: 'statsUnique' }, { key: 'statsMinMax' }, { key: 'statsMeanStd' }, { key: 'statsDistribution' } ]; headers.forEach(header => { const th = document.createElement('th'); th.setAttribute('data-i18n', header.key); th.textContent = t(header.key); headerRow.appendChild(th); }); table.appendChild(headerRow); stats.forEach(stat => { const row = document.createElement('tr'); row.classList.add(stat.qualityClass); const missingPct = totalDataRows ? ((stat.missing / totalDataRows) * 100).toFixed(1) : "0.0"; const missingText = `${stat.missing} (${missingPct}%)`; const minMax = (stat.min !== "-" && stat.max !== "-") ? `${stat.min} / ${stat.max}` : "-"; const meanStd = (stat.mean !== "-" && stat.std !== "-") ? `${stat.mean} ± ${stat.std}` : "-"; [stat.name, stat.type, missingText, stat.unique, minMax, meanStd, stat.distribution || "—"].forEach(val => { const td = document.createElement('td'); td.textContent = val; row.appendChild(td); }); table.appendChild(row); }); } // Build a preview of the uploaded dataset (first rows, columns, stats) function buildPreview(rows) { const previewTable = document.getElementById('preview-table'); previewTable.innerHTML = ''; const previewLoading = document.getElementById('preview-loading'); if (previewLoading) previewLoading.style.display = 'none'; detectedTimeColumns = []; datasetLoaded = true; document.getElementById('csv-preview').style.display = 'block'; unlockWizardStep(2); unlockWizardStep(3); updateStepper(wizardCurrentStep); const header = rows[0]; const numColumns = header.length; const numRows = rows.length - 1; totalDataRows = numRows; let nanCount = 0; for (let i = 1; i < rows.length; i++) { nanCount += rows[i].filter(cell => cell === null || cell === undefined || cell.toString().trim() === "" || cell.toString().toLowerCase() === "nan" ).length; } // Update stats display document.getElementById('csv-rows-count').textContent = numRows; document.getElementById('csv-columns-count').textContent = numColumns; document.getElementById('csv-nan-count').textContent = nanCount; // Build table header const thead = document.createElement("thead"); const headRow = document.createElement("tr"); header.forEach(col => { const th = document.createElement("th"); th.textContent = col; headRow.appendChild(th); }); thead.appendChild(headRow); previewTable.appendChild(thead); // Build table body (first 5 rows) const tbody = document.createElement("tbody"); for (let i = 1; i < Math.min(rows.length, 6); i++) { const row = rows[i]; const tr = document.createElement("tr"); row.forEach(cell => { const td = document.createElement("td"); td.textContent = cell; tr.appendChild(td); }); tbody.appendChild(tr); } previewTable.appendChild(tbody); // Update target column select refreshTargetOptions(header); // Detect time-like columns using a stricter heuristic detectedTimeColumns = header.filter((_, idx) => { let parsable = 0; let total = 0; for (let i = 1; i < rows.length; i++) { const v = rows[i][idx]; if (v !== null && v !== undefined && `${v}`.trim() !== "") { total += 1; if (isProbablyDate(v)) { parsable += 1; } } if (total >= 10) break; } return total > 0 && parsable / total >= 0.7; }); const timeSelect = document.getElementById("time-column"); if (timeSelect) { timeSelect.innerHTML = ""; if (detectedTimeColumns.length > 0) { detectedTimeColumns.forEach(col => { const opt = document.createElement("option"); opt.value = col; opt.textContent = col; timeSelect.appendChild(opt); }); document.getElementById("time-column-container").style.display = "block"; const predLenContainer = document.getElementById("pred-len-container"); if (predLenContainer) predLenContainer.style.display = "block"; updatePredLenDisplay(); } else { document.getElementById("time-column-container").style.display = "none"; const predLenContainer = document.getElementById("pred-len-container"); if (predLenContainer) predLenContainer.style.display = "none"; } } buildStats(rows); updateAdvancedOptions(); buildImputationControls(header, rows); document.getElementById("enable-imputation").checked = false; // Build drop columns checklist const dropListContainer = document.getElementById("drop-columns-list"); dropListContainer.innerHTML = ""; header.forEach(col => { const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = col; checkbox.id = `drop-${col}`; const label = document.createElement("label"); label.setAttribute("for", `drop-${col}`); label.textContent = col; label.style.marginRight = "15px"; const container = document.createElement("div"); container.appendChild(checkbox); container.appendChild(label); dropListContainer.appendChild(container); }); document.querySelectorAll('#drop-columns-list input[type="checkbox"]').forEach(cb => { cb.addEventListener('change', () => { buildImputationControls(header, rows); refreshTargetOptions(header); }); }); } // Rebuild target select excluding dropped columns function refreshTargetOptions(header) { const targetSelect = document.getElementById("target-column"); if (!targetSelect) return; targetSelect.onchange = updateAdvancedOptions; const dropped = new Set( Array.from(document.querySelectorAll('#drop-columns-list input[type="checkbox"]:checked')).map(cb => cb.value) ); const current = targetSelect.value; targetSelect.innerHTML = ""; header.forEach(col => { if (dropped.has(col)) return; const option = document.createElement("option"); option.value = col; option.textContent = col; targetSelect.appendChild(option); }); // If previous selection still valid, keep it if (current && !dropped.has(current)) { targetSelect.value = current; } updateAdvancedOptions(); } // Remove uploaded CSV file and reset UI function removeCSVFile() { const input = document.getElementById('upload-csv'); input.value = ''; const dropzone = document.getElementById("csv-dropzone"); if (dropzone) dropzone.style.display = "block"; const selectedWrapper = document.getElementById('csv-selected'); const fileInfo = document.getElementById('csv-file-info'); const fileNameSpan = document.getElementById('csv-file-name'); if (fileInfo) fileInfo.style.display = 'none'; if (fileNameSpan) fileNameSpan.textContent = ''; if (selectedWrapper) selectedWrapper.style.display = 'none'; document.querySelectorAll('#upload-csv-label').forEach(el => el.style.display = 'inline-block'); document.getElementById('csv-preview').style.display = 'none'; const previewTable = document.getElementById('preview-table'); const statsTable = document.getElementById('stats-table'); previewTable.innerHTML = ''; statsTable.innerHTML = ''; document.getElementById('target-column').innerHTML = ''; const previewLoading = document.getElementById('preview-loading'); if (previewLoading) previewLoading.style.display = 'none'; document.getElementById('csv-preview').style.display = 'none'; const timeContainer = document.getElementById('time-column-container'); const predLenContainer = document.getElementById('pred-len-container'); if (timeContainer) timeContainer.style.display = 'none'; if (predLenContainer) predLenContainer.style.display = 'none'; const resultsDiv = document.getElementById('training-results'); resultsDiv.style.display = 'none'; dataSetPreview = null; lastDatasetName = null; datasetLoaded = false; resetAdvancedOptions(); currentShapRequestId = null; const shapPlot = document.getElementById("shap-plots"); if (shapPlot) { shapPlot.innerHTML = ''; } wizardMaxStep = 1; setWizardStep(1); setUploadStatus("uploadStatusIdle"); toggleFormatsIcon(true); } document.getElementById("enable-imputation").addEventListener("change", (e) => { const isChecked = e.target.checked; document.getElementById("imputation-controls").style.display = isChecked ? "block" : "none"; }); // Build imputation controls for missing values function buildImputationControls(header, data) { const enableImputationCheckbox = document.getElementById("enable-imputation"); const container = document.getElementById("imputation-controls"); const imputationControlsEnable = document.getElementById("container-enable-imputation"); container.innerHTML = ""; const droppedColumns = new Set( Array.from(document.querySelectorAll('#drop-columns-list input[type="checkbox"]')) .filter(cb => cb.checked) .map(cb => cb.value) ); let anyMissing = false; header.forEach((colName, colIndex) => { if (droppedColumns.has(colName)) return; const missing = data.some((row, i) => i > 0 && (row[colIndex] === "" || row[colIndex] === null || row[colIndex] === undefined || row[colIndex].toString().toLowerCase() === "nan") ); if (!missing) return; anyMissing = true; const wrapper = document.createElement("div"); wrapper.style.marginBottom = "10px"; const label = document.createElement("label"); label.textContent = colName + ": "; label.style.marginRight = "10px"; wrapper.appendChild(label); const select = document.createElement("select"); select.name = `impute-${colName}`; select.dataset.col = colName; // Only offer mean/median for numeric columns; categorical columns get mode/constant only const colValues = data.slice(1) .map(row => row[colIndex]) .filter(v => v !== "" && v !== null && v !== undefined && v.toString().toLowerCase() !== "nan"); const isNumeric = colValues.length > 0 && colValues.every(v => !isNaN(parseFloat(v))); const methods = isNumeric ? ["mean", "median", "mode", "constant"] : ["mode", "constant"]; methods.forEach(method => { const option = document.createElement("option"); option.value = method; option.textContent = t(method); option.setAttribute("data-i18n", method); select.appendChild(option); }); const input = document.createElement("input"); input.type = "text"; input.placeholder = "Constant value"; input.style.marginLeft = "10px"; input.style.display = "none"; select.addEventListener("change", () => { input.style.display = (select.value === "constant") ? "inline-block" : "none"; }); wrapper.appendChild(select); wrapper.appendChild(input); container.appendChild(wrapper); }); if (!anyMissing) { container.textContent = t("noMissingValues"); container.style.display = "block"; container.setAttribute('data-i18n', 'noMissingValues'); imputationControlsEnable.style.display = "none"; return; } imputationControlsEnable.style.display = "block"; container.style.display = enableImputationCheckbox.checked ? "block" : "none"; } // Stop the training process on the server function stopTraining() { fetch(`/stop_training/${trainingId}`, { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.error) { showAlert(t("stopTrainingError"), 'error'); return; } if (pollingIntervalTraining) { clearInterval(pollingIntervalTraining); pollingIntervalTraining = null; } showAlert(t("trainingStopped"), "info"); resetTrainingButtons(); }) .catch(error => { console.error("Erreur lors de l'arrêt de l'entraînement :", error); showAlert(t("stopTrainingError"), 'error'); resetTrainingButtons(); }); } // Reset training buttons to initial state function resetTrainingButtons() { document.getElementById('start-training-btn').style.display = "inline-block"; document.getElementById('stop-training-btn').style.display = "none"; document.getElementById('training-spinner').style.display = "none"; stopTrainingCountdown(); stopModelAnimation(); showTrainingOverlay(false); } // Poll server for training results until ready function pollTrainingResult(trainingId) { pollingIntervalTraining = setInterval(() => { fetch(`/training_result/${trainingId}`) .then(res => { if (res.status === 202) { // Not ready yet return null; } return res.json(); }) .then(result => { if (!result) return; clearInterval(pollingIntervalTraining); pollingIntervalTraining = null; if (result.error) { showAlert(t("trainingErrorGet"), 'error'); } else { renderTrainingResults(result); } resetTrainingButtons(); }) .catch(_ => { clearInterval(pollingIntervalTraining); showAlert(t("trainingErrorNetwork"), 'error'); resetTrainingButtons(); }); }, 2000); } function invalidateShapResult() { const shapPlot = document.getElementById("shap-plots"); if (shapPlot) { shapPlot.innerHTML = ''; } currentShapRequestId = null; } // Start the training process: clean data, apply imputation, send to server function startTraining() { const fileInput = document.getElementById('upload-csv'); const file = fileInput.files[0]; const targetColumn = document.getElementById('target-column').value; if (!file || !targetColumn) { showAlert(t("selectCSVAndTarget"), 'warning'); return; } const slider = document.getElementById("training-time-limit"); const sliderMinutes = slider ? parseInt(slider.value, 10) : 0; const totalSeconds = Number.isFinite(sliderMinutes) ? sliderMinutes * 60 : 0; invalidateShapResult(); // Columns to drop const checkboxes = document.querySelectorAll('#drop-columns-list input[type="checkbox"]:checked'); const columnsToDrop = Array.from(checkboxes).map(cb => cb.value); if (columnsToDrop.includes(targetColumn)) { showAlert(t("cannotDropTargetColumn"), 'error'); return; } // Collect selected models for the progress panel const selectedModelEls = Array.from(document.querySelectorAll('#adv-models-container input[type="checkbox"]:checked')); const panelModels = selectedModelEls.length > 0 ? selectedModelEls.map(cb => ({ value: cb.value, label: cb.closest('label')?.textContent?.trim() || cb.value })) : (detectedTimeColumns.length > 0 ? ADVANCED_MODELS.timeseries : ADVANCED_MODELS.tabular); document.getElementById('start-training-btn').style.display = "none"; document.getElementById('stop-training-btn').style.display = "inline-block"; document.getElementById('training-spinner').style.display = "inline-block"; initTrainingPanel(panelModels, file.name, targetColumn); startTrainingCountdown(totalSeconds); startModelAnimation(panelModels.length, totalSeconds); showTrainingOverlay(true); unlockWizardStep(3); setWizardStep(3); const ext = file.name.split('.').pop().toLowerCase(); const reader = new FileReader(); reader.onload = function (e) { let data = []; let header = []; if (ext === 'csv') { const parsed = Papa.parse(e.target.result, { header: false, skipEmptyLines: true }); data = parsed.data; } else if (ext === 'arff') { const rows = parseArff(e.target.result); if (!rows) { showAlert(t("unsupportedFormat"), 'warning'); document.getElementById('training-spinner').style.display = "none"; return; } data = rows; } else if (['xls', 'xlsx', 'xlsm'].includes(ext)) { const workbook = XLSX.read(new Uint8Array(e.target.result), { type: 'array' }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; data = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "" }); } else { showAlert(t("unsupportedFormat"), 'warning'); document.getElementById('training-spinner').style.display = "none"; return; } // Drop selected columns header = data[0]; const dropIndexes = header.map((name, idx) => columnsToDrop.includes(name) ? idx : -1).filter(i => i !== -1); const cleanedData = data.map(row => row.filter((_, idx) => !dropIndexes.includes(idx))); // Apply imputation if enabled const imputationEnabled = document.getElementById("enable-imputation").checked; let imputationChoices = {}; if (imputationEnabled) { document.querySelectorAll('#imputation-controls select').forEach(select => { const col = select.dataset.col; const method = select.value; let constant = null; if (method === "constant") { const input = select.nextElementSibling; constant = input.value; } imputationChoices[col] = { method, constant }; }); // Apply imputation to missing values const newHeader = cleanedData[0]; const colIndexes = Object.keys(imputationChoices).map(col => ({ col, idx: newHeader.indexOf(col) }) ); for (let rowIdx = 1; rowIdx < cleanedData.length; rowIdx++) { const row = cleanedData[rowIdx]; for (const { col, idx } of colIndexes) { if (row[idx] === "" || row[idx] === null || row[idx] === undefined) { const { method, constant } = imputationChoices[col]; const colValues = cleanedData .slice(1) .map(r => r[idx]) .filter(v => v !== "" && v !== null && v !== undefined); let fillValue; if (method === "mean") { const nums = colValues.map(Number).filter(n => !isNaN(n)); const mean = nums.reduce((a, b) => a + b, 0) / nums.length; fillValue = mean.toFixed(2); } else if (method === "median") { const nums = colValues.map(Number).filter(n => !isNaN(n)).sort((a, b) => a - b); const mid = Math.floor(nums.length / 2); fillValue = nums.length % 2 === 0 ? ((nums[mid - 1] + nums[mid]) / 2).toFixed(2) : nums[mid].toFixed(2); } else if (method === "mode") { const freq = {}; colValues.forEach(v => { freq[v] = (freq[v] || 0) + 1; }); fillValue = Object.entries(freq).reduce((a, b) => (b[1] > a[1] ? b : a))[0]; } else if (method === "constant") { fillValue = constant; } row[idx] = fillValue; } } } } dataPreprocessing = { data_preprocessing: { columns_drop: columnsToDrop, imputation_missing_value: imputationChoices } }; const csv = Papa.unparse(cleanedData, { quotes: false }); // Prepare cleaned file for upload const formData = new FormData(); const blob = new Blob([csv], { type: 'text/csv' }); formData.append('file', blob, `${file.name}.csv`); formData.append('target_column', targetColumn); const timeSlider = document.getElementById("training-time-limit"); const timeLimitSeconds = timeSlider ? parseInt(timeSlider.value, 10) * 60 : 60; formData.append('time_limit', timeLimitSeconds); const timeColumnSelect = document.getElementById('time-column'); if (timeColumnSelect && detectedTimeColumns.length > 0) { formData.append('time_column', timeColumnSelect.value); const predLenSlider = document.getElementById('prediction-length-percent'); if (predLenSlider) { formData.append('prediction_length_percent', predLenSlider.value); } } // Advanced options const advMetric = document.getElementById('adv-eval-metric'); if (advMetric && advMetric.value) formData.append('eval_metric', advMetric.value); const excludedModels = Array.from(document.querySelectorAll('#adv-models-container input[type="checkbox"]')) .filter(cb => !cb.checked).map(cb => cb.value); if (excludedModels.length > 0) formData.append('excluded_model_types', excludedModels.join(',')); const useCvCb = document.getElementById('adv-use-cv'); const useCv = useCvCb && useCvCb.checked; formData.append('use_cv', useCv ? '1' : '0'); if (!useCv) { const valSize = document.getElementById('adv-val-size'); const parsed = valSize ? parseInt(valSize.value, 10) : NaN; const safeVal = (!isNaN(parsed) && parsed >= 10 && parsed <= 50) ? parsed : 20; formData.append('val_size', (safeVal / 100).toFixed(2)); } lastUsedTargetColumn = targetColumn; lastCleanedCsvBlob = blob; // Send to server for training fetch('/train', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.error) { showAlert(t("beginTrainingError"), 'error'); resetTrainingButtons(); return; } const tempTrainingId = data.training_id; trainingId = tempTrainingId pollTrainingResult(tempTrainingId); }) .catch(_ => { resetTrainingButtons(); document.getElementById('training-spinner').style.display = "none"; showTrainingOverlay(false); stopTrainingCountdown(); showAlert(t("trainingError"), 'error'); }); }; if (ext === 'csv' || ext === 'arff') { reader.readAsText(file); } else { reader.readAsArrayBuffer(file); } } // Render training results and plots in the UI function renderTrainingResults(data) { lastTrainedModelPath = data.model_path; summaryResults["summary"] = data.summary_LLM; summaryResults["feature_importance_plot"] = data.feature_importance_plot; summaryResults["metrics_plot"] = data.metrics; summaryResults["forecast_plot"] = data.forecast_plot || null; summaryResults["task_type"] = data.task_type; summaryResults["best_model"] = data.best_model; summaryResults["prediction_length"] = data.prediction_length || null; summaryResults["train_time"] = data.train_time; unlockWizardStep(4); unlockWizardStep(5); setWizardStep(4); dataSetPreview = data.markdown_preview; const resultsDiv = document.getElementById('training-results'); resultsDiv.innerHTML = `

${t("trainingResults")}

`; // -------- Training Summary Section -------- let summaryHTML = `

${t("trainingSummary")}

${t("generalInfo")}

${t("detectedTask")} ${data.task_type}

${t("selectedModel")} ${data.best_model}

${t("trainingTime")} ${data.train_time.toFixed(2)} seconds

`; const metrics = data.metrics; let metricsHTML = ''; let plotsHTML = `

${t("plots")}

`; for (const metric in metrics) { const value = metrics[metric].value; const plotBase64 = metrics[metric].plot; const plotHist = metrics[metric].plot_hist || null; const metricLabel = metric.toUpperCase(); if (value == null) continue; const formattedValue = value.toFixed(4); metricsHTML += `${metricLabel}${formattedValue}`; if (metricLabel === "RMSE" && plotBase64 && plotHist) { plotsHTML += `
${metricLabel} Plot ${metricLabel} Error Distribution
`; } else if (plotBase64) { plotsHTML += `
${metricLabel} Plot
`; } } if (data.task_type === "timeseries" && data.forecast_plot) { plotsHTML += `
Forecast vs Actual
`; } let metricSectionTitle = "Classification Metrics"; if (data.task_type === "regression") { metricSectionTitle = "Regression Metrics"; } else if (data.task_type === "timeseries") { metricSectionTitle = "Time Series Metrics"; } summaryHTML += `

${metricSectionTitle}

${metricsHTML}
${t("metric")}${t("value")}
`; if (data.leaderboard && Array.isArray(data.leaderboard)) { const leaderboardHTML = `

${t("leaderboard")}

${data.leaderboard.map(row => ` `).join("")}
${t("model")} ${t("scoreVal")} ${t("fitTime")} ${t("predictTime")}
${row.model} ${row.score_val != null ? row.score_val.toFixed(4) : 'N/A'} ${row.fit_time != null ? row.fit_time.toFixed(2) + 's' : 'N/A'} ${row.pred_time_val != null ? row.pred_time_val.toFixed(2) + 's' : 'N/A'}
`; summaryHTML += leaderboardHTML; } plotsHTML += `
`; // End plots summaryHTML += plotsHTML + `
`; // End Training Summary resultsDiv.innerHTML += summaryHTML; // -------- Model Explainability Section -------- const isTimeseries = data.task_type === "timeseries"; const hasExplainability = data.feature_importance_plot || !isTimeseries; if (hasExplainability) { let explainabilityHTML = `

${t("modelExplainability")}

`; if (data.feature_importance_plot) { explainabilityHTML += `
Feature Importance Plot
`; } if (!isTimeseries) { explainabilityHTML += `
i
`; } explainabilityHTML += `
`; // End Model Explainability resultsDiv.innerHTML += explainabilityHTML; } resultsDiv.style.display = 'block'; // Mirror downloads into the dedicated panel (wizard step 5) const downloadsPanel = document.getElementById("downloads-panel"); if (downloadsPanel) { downloadsPanel.innerHTML = `

${t("downloadSection") || "Downloads"}

📥 ${t("downloadModel")}
`; } } function renderShapResult(result){ const tempShapPlot = result.shap_summary_plot shapPlot = tempShapPlot; const plotCard = document.getElementById("shap-plots"); plotCard.innerHTML = ` SHAP Summary `; } function resetShapButtons() { const button_generate = document.getElementById("generate-shap-button"); const spinner = document.getElementById('training-spinner-shap'); button_generate.disabled = false; button_generate.style.display = "inline-block"; if (spinner) spinner.style.display = "none"; } // Generate SHAP plot for model explainability async function generateShapPlot() { if (!lastTrainedModelPath || !lastCleanedCsvBlob || !lastUsedTargetColumn) { showAlert(t("missingSHAPInfo"), 'error'); return; } const shapRequestId = crypto.randomUUID(); currentShapRequestId = shapRequestId; const button_generate = document.getElementById("generate-shap-button"); const spinner = document.getElementById("training-spinner-shap"); button_generate.disabled = true; spinner.style.display = "inline-block"; const formData = new FormData(); formData.append("training_id", trainingId || ''); formData.append("model_path", lastTrainedModelPath || ''); formData.append("target_column", lastUsedTargetColumn); formData.append("dataset", lastCleanedCsvBlob, "cleaned_data.csv"); try { const response = await fetch("/generate_shap_plot", { method: "POST", body: formData }); if (!response.ok) { resetShapButtons(); throw new Error(await response.text()); } const { shap_id, error: initError } = await response.json(); if (initError) throw new Error(initError); // Poll until done await new Promise((resolve, reject) => { const interval = setInterval(async () => { if (currentShapRequestId !== shapRequestId) { clearInterval(interval); return resolve(); } try { const pr = await fetch(`/shap_progress/${shap_id}`); const data = await pr.json(); if (data.done) { clearInterval(interval); if (data.error) return reject(new Error(data.error)); renderShapResult(data.result); button_generate.style.display = "none"; resolve(); } } catch (e) { clearInterval(interval); reject(e); } }, 500); }); } catch (error) { showAlert(t("shapGenerationError") + ": " + error.message, "error"); resetShapButtons(); } finally { spinner.style.display = "none"; } } // Download all plots as a ZIP file function downloadAllPlots() { const zip = new JSZip(); const images = document.querySelectorAll('.plot-card img'); images.forEach((img, index) => { const base64 = img.src.split(',')[1]; // Get only the base64 part const alt = img.alt.replace(/\s+/g, '_').toLowerCase(); // Safe filename zip.file(`${alt || 'plot_' + index}.png`, base64, { base64: true }); }); // Get dataset name const fileInput = document.getElementById('upload-csv'); const fileName = fileInput.files.length > 0 ? fileInput.files[0].name.replace(/\.csv$/, '') : 'dataset'; // Generate readable timestamp const now = new Date(); const timestamp = now.toISOString().replace(/[:\-T]/g, '_').split('.')[0]; // ex: 2025_05_30_14_45_12 // Final ZIP filename const finalFileName = `${fileName}_plots_${timestamp}.zip`; zip.generateAsync({ type: "blob" }) .then(function (content) { const link = document.createElement("a"); link.href = URL.createObjectURL(content); link.download = finalFileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); } // Download training results as a PDF async function downloadPDF() { // Check that all required information is available before generating the PDF if (!dataSetPreview || !dataSetStats || Object.keys(summaryResults).length === 0 || Object.keys(dataPreprocessing).length === 0) { showAlert(t("missingPDFInfo"), 'error'); return; } try { // Prepare the payload to send to the backend for PDF generation const payload = { summary: summaryResults, preview: dataSetPreview, stats: dataSetStats, data_preprocessing: dataPreprocessing, target_column: lastUsedTargetColumn, dataset_name: lastDatasetName }; const groqKey = getGroqKey(); if (groqKey) { payload.groq_api_key = groqKey; } if (shapPlot) { payload.shap_summary_plot = shapPlot; // Include SHAP plot if available } // Send the request to the backend to generate the PDF const response = await fetch('/download_pdf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); // Check if the response is OK if (!response.ok) { showAlert(t("pdfGenerationError"), 'error'); } // Retrieve the PDF file as a blob const blob = await response.blob(); // Generate a readable filename using the dataset name and current timestamp const fileInput = document.getElementById('upload-csv'); const fileName = fileInput.files.length > 0 ? fileInput.files[0].name.replace(/\.csv$/, '') : 'dataset'; const now = new Date(); const timestamp = now.toISOString().replace(/[:\-T]/g, '_').split('.')[0]; const finalFileName = `${fileName}_training_summary_${timestamp}.pdf`; // Create a temporary link to trigger the PDF download const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = finalFileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch (error) { // Show an error if PDF generation fails showAlert(t("trainingErrorNetwork"), 'error'); } } // Handle prediction CSV upload and show next step document.getElementById('predict-csv').addEventListener('change', function () { const file = this.files[0]; const acceptedExtensions = ['.csv', '.xls', '.xlsx', '.xlsm', '.arff']; if (file && acceptedExtensions.some(ext => file.name.endsWith(ext))) { this.style.display = 'none'; document.getElementById('predict-csv-label').style.display = 'none'; document.getElementById('predict-file-name').textContent = file.name; document.getElementById('predict-file-info').style.display = 'inline-block'; // Show next step document.getElementById('step-2-model').style.display = 'block'; } else { showAlert(t("pleaseSelectCFile"), 'warning'); this.value = ''; } }); // Remove prediction file and reset UI function removePredictFile() { const input = document.getElementById('predict-csv'); input.value = ''; document.getElementById('predict-csv-label').style.display = 'inline-block'; document.getElementById('predict-file-info').style.display = 'none'; document.getElementById('predict-file-name').textContent = ''; removePredictModel(); // Hide next steps document.getElementById('step-2-model').style.display = 'none'; document.getElementById('step-3-predict').style.display = 'none'; document.getElementById('prediction-results').style.display = 'none'; } // Handle prediction model ZIP upload and show next step document.getElementById('predict-model-zip').addEventListener('change', function () { const file = this.files[0]; if (file && file.name.endsWith('.zip')) { this.style.display = 'none'; document.getElementById('predict-zip-label').style.display = 'none'; document.getElementById('predict-model-name').textContent = file.name; document.getElementById('predict-model-info').style.display = 'inline-block'; // Show predict button document.getElementById('step-3-predict').style.display = 'block'; } else { showAlert(t("pleaseSelectZIP"), 'warning'); this.value = ''; } }); // Remove prediction model and reset UI function removePredictModel() { const input = document.getElementById('predict-model-zip'); input.value = ''; document.getElementById('predict-zip-label').style.display = 'inline-block'; document.getElementById('predict-model-info').style.display = 'none'; document.getElementById('predict-model-name').textContent = ''; document.getElementById('step-3-predict').style.display = 'none'; } // Run prediction: send dataset and model to server, display results and plots async function runPrediction() { const datasetInput = document.getElementById('predict-csv'); const modelInput = document.getElementById('predict-model-zip'); const dataset = datasetInput.files[0]; const model = modelInput.files[0]; if (!dataset || !model) { showAlert(t("selectDatasetAndModel"), 'warning'); return; } const zip = await JSZip.loadAsync(model); const formData = new FormData(); formData.append('dataset', dataset); formData.append('zip_model', model); const pngFiles = []; zip.forEach((relativePath, zipEntry) => { if (relativePath.startsWith("plot_train_results/") && relativePath.endsWith(".png")) { pngFiles.push(zipEntry); } }); pngResultTrainingForPrediction = pngFiles; fetch('/predict', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.error) { showAlert(t(data.error), 'error'); return; } if (!data.preview || data.preview.length === 0) { showAlert("No prediction available.", 'warning'); return; } const plots = data.plots; PlotsPredictionResults = plots; const predictionResults = document.getElementById('prediction-results'); predictionResults.innerHTML = `

${t("predictionResults")}

${t("preview")}

`; // Build preview table const tableContainer = document.getElementById('prediction-table-container'); const table = document.createElement('table'); table.id = 'prediction-table'; table.className = 'preview-table'; const header = Object.keys(data.preview[0]); const thead = document.createElement('thead'); const headRow = document.createElement('tr'); header.forEach(col => { const th = document.createElement('th'); th.textContent = col; headRow.appendChild(th); }); thead.appendChild(headRow); table.appendChild(thead); const tbody = document.createElement('tbody'); data.preview.forEach(row => { const tr = document.createElement('tr'); header.forEach(col => { const td = document.createElement('td'); td.textContent = row[col]; tr.appendChild(td); }); tbody.appendChild(tr); }); table.appendChild(tbody); tableContainer.appendChild(table); // Display prediction plots if (data.plots && Object.keys(data.plots).length > 0) { let plotsHTML = `

${t("predictionPlots")}

`; for (const [title, base64] of Object.entries(data.plots)) { const formattedTitle = title.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); plotsHTML += `

${formattedTitle}

${formattedTitle}
`; } plotsHTML += `
`; predictionResults.innerHTML += plotsHTML; } // Download predictions button const downloadUrl = data.download_url || '#'; predictionResults.innerHTML += `
📥 ${t("downloadPredictions")}
`; predictionResults.style.display = 'block'; }) .catch(_ => { showAlert(t("predictionError"), 'error'); }); } // Download all prediction plots as a ZIP file function downloadAllPredictionPlots() { const zip = new JSZip(); const images = document.querySelectorAll('#prediction-results .plot-card img'); images.forEach((img, index) => { const base64 = img.src.split(',')[1]; const alt = img.alt.replace(/\s+/g, '_').toLowerCase(); zip.file(`${alt || 'plot_' + index}.png`, base64, { base64: true }); }); const fileInput = document.getElementById('predict-csv'); const fileName = fileInput.files.length > 0 ? fileInput.files[0].name.replace(/\.csv$/, '') : 'dataset'; const now = new Date(); const timestamp = now.toISOString().replace(/[:\-T]/g, '_').split('.')[0]; const finalFileName = `${fileName}_prediction_plots_${timestamp}.zip`; zip.generateAsync({ type: "blob" }).then(function (content) { const link = document.createElement("a"); link.href = URL.createObjectURL(content); link.download = finalFileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); } // Send chat message to backend and display AI response async function sendChat() { const input = document.getElementById('chat-input'); const sendButton = document.querySelector('#chat-footer button'); const chatBox = document.getElementById('chat-box'); const lang = localStorage.getItem("selectedLanguage") || "en"; const message = input.value.trim(); if (!message) return; input.disabled = true; sendButton.disabled = true; const userMsg = document.createElement('div'); userMsg.className = 'chat-message user'; userMsg.textContent = message; chatBox.appendChild(userMsg); chatBox.scrollTop = chatBox.scrollHeight; input.value = ''; try { const payload = { message, lang }; const groqKey = getGroqKey(); if (groqKey) { payload.groq_api_key = groqKey; } if (appMode === 1){ if (Object.keys(summaryResults).length !== 0) { payload.summary = { text: summaryResults.summary, feature_importance_plot: summaryResults.feature_importance_plot, metrics_plot: summaryResults.metrics_plot, forecast_plot: summaryResults.forecast_plot }; if (shapPlot){ payload.shap_summary_plot = shapPlot } payload.model_metadata = { task_type: summaryResults.task_type, best_model: summaryResults.best_model, target_column: lastUsedTargetColumn || null, prediction_length: summaryResults.prediction_length || null }; } if (dataSetStats){ payload.stats = dataSetStats } if (dataSetPreview) { payload.markdown_preview = dataSetPreview; } if (dataPreprocessing) { payload.data_preprocessing = dataPreprocessing; } } if (appMode === 2) { if (PlotsPredictionResults) { payload.plots_prediction_results = PlotsPredictionResults; } if (pngResultTrainingForPrediction) { const entries = await Promise.all( pngResultTrainingForPrediction.map(async entry => ({ name: entry.name.split('/').pop(), base64: await entry.async("base64") })) ); payload.png_result_training_for_prediction = Object.fromEntries( entries.map(e => [e.name, e.base64]) ); } } const response = await fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { const errorData = await response.json(); showAlert(t(errorData.error), 'error'); pngResultTrainingForPrediction = null; return; } const data = await response.json(); const aiReply = data.response; const aiMsg = document.createElement('div'); aiMsg.className = 'chat-message ai'; aiMsg.innerHTML = marked.parse(aiReply); chatBox.appendChild(aiMsg); chatBox.scrollTop = chatBox.scrollHeight; } catch (error) { const errorMsg = document.createElement('div'); errorMsg.className = 'chat-message ai'; errorMsg.textContent = t('error_chat', 'error'); chatBox.appendChild(errorMsg); chatBox.scrollTop = chatBox.scrollHeight; pngResultTrainingForPrediction = null; } finally { input.disabled = false; sendButton.disabled = false; input.focus(); } } // Send chat on Enter (without Shift) document.getElementById('chat-input').addEventListener('keydown', function (event) { // If Enter pressed without Shift if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); // prevent newline sendChat(); } }); // Groq API key modal handlers const groqSaveBtn = document.getElementById("groq-save-btn"); const groqCancelBtn = document.getElementById("groq-cancel-btn"); const groqInput = document.getElementById("groq-api-key-input"); if (groqSaveBtn) { groqSaveBtn.addEventListener("click", () => { const val = (groqInput?.value || "").trim(); if (!val) { showAlert(t("groqMissing"), "warning", 4000); return; } localStorage.setItem("groq_api_key", val); hideGroqModal(); // Open chat if it was requested const sidebar = document.getElementById('chat-sidebar'); if (sidebar && sidebar.classList.contains('collapsed')) { toggleChatSidebar(); } }); } if (groqCancelBtn) { groqCancelBtn.addEventListener("click", hideGroqModal); } if (groqInput) { groqInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); groqSaveBtn?.click(); } }); } // Toggle chat sidebar open/collapsed function toggleChatSidebar() { const sidebar = document.getElementById('chat-sidebar'); const toggleButton = document.getElementById('toggle-chat-btn'); const wrapper = document.querySelector('.page-wrapper'); // Only allow opening if Groq API key is set if (sidebar.classList.contains('collapsed') && !getGroqKey()) { showGroqModal(); return; } const isCollapsed = sidebar.classList.toggle('collapsed'); if (isCollapsed) { toggleButton.classList.remove('hidden'); wrapper.classList.remove('chat-open'); } else { toggleButton.classList.add('hidden'); wrapper.classList.add('chat-open'); } } // Show alert message in the UI function showAlert(message, type = 'error', duration = 10000) { const alertBox = document.getElementById('custom-alert'); alertBox.textContent = message; alertBox.className = 'custom-alert'; // reset if (type === 'success') alertBox.classList.add('success'); else if (type === 'warning') alertBox.classList.add('warning'); else alertBox.classList.add('error'); alertBox.classList.remove('hidden'); setTimeout(() => { alertBox.classList.add('hidden'); }, duration); }