/* DataMind AI — Frontend Application Logic */ const API = ''; let chatOpen = false; // ===== WAKE-UP CHECK ===== (async function doWakeup() { const overlay = document.getElementById('wakeup-overlay'); const text = document.getElementById('wakeup-text'); if (!overlay) return; let attempts = 0; while (attempts < 10) { try { const res = await fetch(`${API}/api/health`); if (res.ok) { overlay.style.display = 'none'; return; } } catch (e) {} attempts++; await new Promise(r => setTimeout(r, 3000)); } if (text) { text.textContent = "Unable to connect. Please refresh."; text.style.color = "#ff6b6b"; } })(); // ===== UTILITIES ===== const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); const show = (el) => el?.classList.remove('hidden'); const hide = (el) => el?.classList.add('hidden'); // ===== SHADER ANIMATION ===== let shaderAnimationId = null; let shaderRenderer = null; let shaderUniforms = null; let shaderScene = null; let shaderCamera = null; function initShader() { const container = document.getElementById('shader-bg'); if (!container || shaderRenderer || !window.THREE) return; const vertexShader = `void main() { gl_Position = vec4(position, 1.0); }`; const fragmentShader = ` precision highp float; uniform vec2 resolution; uniform float time; void main(void) { vec2 uv = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y); float t = time * 0.05; float lineWidth = 0.002; vec3 color = vec3(0.0); for(int j = 0; j < 3; j++){ for(int i=0; i < 5; i++){ color[j] += lineWidth*float(i*i) / abs(fract(t - 0.01*float(j)+float(i)*0.01)*5.0 - length(uv) + mod(uv.x+uv.y, 0.2)); } } gl_FragColor = vec4(color[0], color[1], color[2], 1.0); } `; shaderCamera = new THREE.Camera(); shaderCamera.position.z = 1; shaderScene = new THREE.Scene(); const geometry = new THREE.PlaneGeometry(2, 2); shaderUniforms = { time: { type: "f", value: 1.0 }, resolution: { type: "v2", value: new THREE.Vector2() } }; const material = new THREE.ShaderMaterial({ uniforms: shaderUniforms, vertexShader: vertexShader, fragmentShader: fragmentShader, transparent: true }); const mesh = new THREE.Mesh(geometry, material); shaderScene.add(mesh); shaderRenderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); shaderRenderer.setPixelRatio(window.devicePixelRatio); container.appendChild(shaderRenderer.domElement); const onWindowResize = () => { if (!shaderRenderer) return; const width = container.clientWidth || window.innerWidth; const height = container.clientHeight || window.innerHeight; shaderRenderer.setSize(width, height); shaderUniforms.resolution.value.x = shaderRenderer.domElement.width; shaderUniforms.resolution.value.y = shaderRenderer.domElement.height; }; onWindowResize(); window.addEventListener("resize", onWindowResize, false); } function startShader() { if (!window.THREE) return; if (!shaderRenderer) initShader(); if (shaderAnimationId) cancelAnimationFrame(shaderAnimationId); const animate = () => { shaderAnimationId = requestAnimationFrame(animate); if (shaderUniforms) shaderUniforms.time.value += 0.05; if (shaderRenderer && shaderScene && shaderCamera) shaderRenderer.render(shaderScene, shaderCamera); }; animate(); } function stopShader() { if (shaderAnimationId) { cancelAnimationFrame(shaderAnimationId); shaderAnimationId = null; } } function showSpinner(text = 'Processing...', progress = null) { $('#spinner-text').textContent = text; $('#spinner-overlay').classList.add('active'); const bar = $('#spinner-progress-bar'); const track = document.querySelector('.spinner-progress-track'); if (bar && track) { if (progress !== null) { track.style.display = 'block'; bar.style.width = progress + '%'; } else { track.style.display = 'none'; bar.style.width = '0%'; } } // Small delay to ensure container is visible before starting shader setTimeout(startShader, 50); } function hideSpinner() { $('#spinner-overlay').classList.remove('active'); const bar = $('#spinner-progress-bar'); if (bar) bar.style.width = '0%'; stopShader(); } function showToast(msg, type = 'info') { const t = document.createElement('div'); t.style.cssText = `position:fixed;bottom:24px;left:50%;transform:translateX(-50%);padding:12px 24px;border-radius:12px;font-size:0.85rem;z-index:300;animation:msgIn 0.3s ease;font-family:'DM Sans',sans-serif;`; t.style.background = type === 'error' ? '#ff6b6b' : type === 'success' ? '#6bcb77' : '#00e5ff'; t.style.color = '#000'; t.style.fontWeight = '600'; t.textContent = msg; document.body.appendChild(t); setTimeout(() => { t.style.opacity = '0'; t.style.transition = 'opacity 0.3s'; setTimeout(() => t.remove(), 300); }, 3000); } // ===== UPLOAD HANDLING ===== function initUpload() { const zone = $('#upload-zone'); const input = $('#file-input'); zone.addEventListener('click', () => input.click()); zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragover'); }); zone.addEventListener('dragleave', () => zone.classList.remove('dragover')); zone.addEventListener('drop', (e) => { e.preventDefault(); zone.classList.remove('dragover'); if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0]); }); input.addEventListener('change', () => { if (input.files.length) uploadFile(input.files[0]); }); } // ===== DATA PREVIEW MODAL ===== function showPreviewModal(data, proceedCallback) { const overlay = $('#preview-modal-overlay'); const table = $('#preview-table'); const stats = $('#preview-stats'); if (!overlay || !table) { proceedCallback(); return; } stats.innerHTML = `

Shape: ${data.rows.toLocaleString()} rows × ${data.columns} columns

`; let thead = ''; data.column_names.forEach(c => { let dtype = data.dtypes ? data.dtypes[c] : 'unknown'; thead += `${c}
${dtype}`; }); thead += ''; let tbody = ''; if (data.sample && data.sample.length > 0) { data.sample.forEach(row => { tbody += ''; data.column_names.forEach(c => { let val = row[c] !== null ? row[c] : ''; tbody += `${val}`; }); tbody += ''; }); } table.innerHTML = `${thead}${tbody}`; overlay.style.display = 'flex'; window.confirmPreview = () => { overlay.style.display = 'none'; proceedCallback(); }; window.cancelPreview = () => { overlay.style.display = 'none'; }; } async function uploadFile(file) { if (!file.name.toLowerCase().endsWith('.csv')) { showToast('Please upload a CSV file', 'error'); return; } showSpinner('Uploading & parsing CSV...'); const form = new FormData(); form.append('file', file); try { const res = await fetch(`${API}/api/upload`, { method: 'POST', body: form }); // Check if the response is JSON (not an HTML error page) const contentType = res.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { if (res.status === 413) { showToast('File is too large. Maximum upload size is 500MB.', 'error'); } else { showToast(`Server error (${res.status}). Please try a smaller file.`, 'error'); } hideSpinner(); return; } const data = await res.json(); if (data.success) { hideSpinner(); showPreviewModal(data, async () => { showSpinner('Cleaning data & running EDA...'); await startAnalysis(data); }); } else { showToast(data.error || 'Upload failed', 'error'); hideSpinner(); } } catch (e) { showToast('Upload failed. The file may be too large or in an unsupported format.', 'error'); hideSpinner(); } } async function generateDataset(type) { showSpinner('Generating your dataset...'); try { const res = await fetch(`${API}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type }) }); const data = await res.json(); if (data.success) { hideSpinner(); showPreviewModal(data, async () => { showSpinner('Cleaning data & running EDA...'); await startAnalysis(data); }); } else { showToast(data.error || 'Generation failed', 'error'); hideSpinner(); } } catch (e) { showToast('Generation failed: ' + e.message, 'error'); hideSpinner(); } } // ===== ANALYSIS PIPELINE ===== async function startAnalysis(datasetInfo) { // Update UI $('#topbar-dataset').textContent = datasetInfo.name; $('#topbar-dataset').classList.add('active'); $('#export-dropdown').style.display = 'block'; hide($('#landing')); $('#dashboard').classList.add('active'); $('#app-layout').classList.remove('sidebar-hidden'); // Update sidebar basic info $('#sidebar-rows').textContent = datasetInfo.rows?.toLocaleString() || '—'; $('#sidebar-cols').textContent = datasetInfo.columns || '—'; $('#sidebar-name').textContent = datasetInfo.name || '—'; // Show skeletons showChartSkeletons(); // ── PHASE 1: EDA (must complete first — produces df_clean) ── showSpinner('Step 1/4 — Cleaning & analyzing data...', 15); await runEDA(); // ── PHASE 2: Charts + Forecast + KPIs in PARALLEL ── // Update spinner text so user knows work is happening showSpinner('STEP 2/4 — GENERATING AI CHARTS & FORECAST...', 40); await Promise.all([ loadKPIs().catch(e => console.warn('KPIs:', e)), loadCharts().catch(e => console.warn('Charts:', e)), loadForecast().catch(e => console.warn('Forecast:', e)), ]); // ── PHASE 3: AI Insights + Recommendations + What-If in PARALLEL ── showSpinner('STEP 3/4 — GENERATING AI INSIGHTS...', 75); await Promise.all([ loadInsights().catch(e => console.warn('Insights:', e)), loadRecommendations().catch(e => console.warn('Recommendations:', e)), setupWhatIf().catch(e => console.warn('What-If:', e)), ]); // ── PHASE 4: Done ── showSpinner('FINALIZING DASHBOARD...', 100); await new Promise(r => setTimeout(r, 600)); // Brief moment so user sees the final step hideSpinner(); // Check if we have date columns to show date filters if (datasetInfo.dtypes) { const hasDate = Object.values(datasetInfo.dtypes).some(t => String(t).includes('datetime')); if (hasDate) { const filters = $('#date-filters-container'); if (filters) filters.style.display = 'flex'; } } showToast('✓ Dataset cleaned & charts ready', 'success'); } // ===== DATE FILTERS ===== window.applyDateFilters = async () => { const dFrom = $('#date-from').value; const dTo = $('#date-to').value; const chartsGrid = $('#charts-grid'); if (chartsGrid) chartsGrid.innerHTML = '
Re-generating charts...
'; try { let url = `${API}/api/charts`; let params = []; if (dFrom) params.push(`date_from=${encodeURIComponent(dFrom)}`); if (dTo) params.push(`date_to=${encodeURIComponent(dTo)}`); if (params.length > 0) url += '?' + params.join('&'); const res = await fetch(url); const data = await res.json(); if (data.success) { chartsGrid.innerHTML = ''; if (!data.charts || data.charts.length === 0) { chartsGrid.innerHTML = '
No charts to display for this date range.
'; } else { data.charts.forEach(chartHtml => { const card = document.createElement('div'); card.innerHTML = chartHtml; chartsGrid.appendChild(card.firstElementChild); }); } } else { showToast('Failed to apply filters: ' + data.error, 'error'); } } catch (e) { showToast('Failed to fetch filtered charts', 'error'); } }; async function runEDA() { $('#sidebar-eda').innerHTML = '
'; try { const res = await fetch(`${API}/api/eda`); const data = await res.json(); if (data.success) renderEDA(data.results); } catch (e) { console.error('EDA error:', e); } } function renderEDA(r) { let html = ''; // Shape html += `
Rows (cleaned)${r.duplicates?.rows_after?.toLocaleString() || r.shape?.rows}
`; html += `
Columns${r.shape?.columns}
`; html += `
Memory${r.shape?.memory_usage_mb} MB
`; // Missing values const mb = r.missing_values?.total_before || 0; const ma = r.missing_values?.total_after || 0; html += `
Missing (before)${mb}
`; html += `
Missing (after)${ma}
`; // Duplicates html += `
Duplicates removed${r.duplicates?.removed || 0}
`; // Outliers const totalOutliers = Object.values(r.outliers || {}).reduce((s, o) => s + (o.count || 0), 0); html += `
Outliers flagged${totalOutliers}
`; // Type fixes if (r.type_fixes?.length) { html += `
Type fixes${r.type_fixes.length}
`; } // Normalised columns if (r.capitalisation?.normalised_columns?.length) { html += `
Text normalised${r.capitalisation.normalised_columns.length} cols
`; } $('#sidebar-eda').innerHTML = html; // Summary stats in sidebar let statsHtml = ''; const stats = r.summary_stats || {}; for (const [col, s] of Object.entries(stats).slice(0, 6)) { statsHtml += `
${col}${s.mean?.toLocaleString(undefined,{maximumFractionDigits:1})} avg
`; } $('#sidebar-stats').innerHTML = statsHtml || '

No numeric columns

'; // Missing value strategies const strategies = r.missing_values?.strategies || {}; let stratHtml = ''; for (const [col, strat] of Object.entries(strategies).slice(0, 8)) { stratHtml += `
${col}${strat}
`; } if (stratHtml) { const stratSection = document.createElement('div'); stratSection.className = 'sidebar-section'; stratSection.innerHTML = `

Fill Strategies

${stratHtml}`; const existing = $('#sidebar-strategies'); if (existing) existing.innerHTML = stratHtml; } } // ===== CHARTS ===== function showChartSkeletons() { const grid = $('#charts-grid'); grid.innerHTML = ''; for (let i = 0; i < 6; i++) { grid.innerHTML += `
`; } } async function loadCharts() { try { const res = await fetch(`${API}/api/charts`); const data = await res.json(); if (data.success) renderCharts(data.charts); else showToast('Chart generation had issues', 'error'); } catch (e) { console.error('Charts error:', e); showToast('Charts failed to load', 'error'); } } function renderCharts(charts) { const grid = $('#charts-grid'); grid.innerHTML = ''; if (!charts?.length) { grid.innerHTML = '

No charts could be generated.

'; return; } // Remove any existing search bar (prevents duplicates on re-render) const existingSearch = document.querySelector('.chart-search-bar'); if (existingSearch) existingSearch.remove(); // Add search/filter bar above charts const searchBar = document.createElement('div'); searchBar.className = 'chart-search-bar'; searchBar.innerHTML = ` ${charts.length} charts`; grid.parentNode.insertBefore(searchBar, grid); window._allCharts = charts; // Store for filtering charts.forEach((chart, i) => { const card = document.createElement('div'); card.className = 'chart-card'; card.style.animation = `fadeSlideUp 0.5s ${0.08 * i}s both`; card.dataset.title = (chart.title || '').toLowerCase(); card.dataset.type = (chart.chart_type || '').toLowerCase(); const chartDivId = `chart-${i}`; card.innerHTML = `
${chart.title}

${chart.caption || chart.description || ''}

`; grid.appendChild(card); if (chart.plotly_json) { const config = { responsive: true, displayModeBar: true, modeBarButtonsToRemove: ['lasso2d', 'select2d', 'autoScale2d'], displaylogo: false, toImageButtonOptions: { format: 'png', filename: chart.title || 'datamind_chart', height: null, width: null, scale: 2 }, modeBarButtonsToAdd: [{ name: 'Fullscreen', icon: { width: 24, height: 24, path: 'M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3', transform: 'matrix(1 0 0 1 0 0)' }, click: function(gd) { expandPlotlyChart(gd.id); } }] }; Plotly.newPlot(chartDivId, chart.plotly_json.data, chart.plotly_json.layout, config); } }); } function downloadChartPNG(divId, title) { const gd = document.getElementById(divId); if (!gd) return; Plotly.downloadImage(gd, { format: 'png', width: 1200, height: 700, scale: 2, filename: title || 'chart' }); showToast('Downloading chart as PNG...', 'success'); } function filterCharts(query) { const q = query.toLowerCase().trim(); const cards = document.querySelectorAll('.chart-card'); let visible = 0; cards.forEach(card => { const match = !q || card.dataset.title?.includes(q) || card.dataset.type?.includes(q); card.style.display = match ? '' : 'none'; if (match) visible++; }); const counter = document.getElementById('chart-count'); if (counter) counter.textContent = `${visible} of ${cards.length} charts`; } function expandPlotlyChart(divId) { const sourceDiv = document.getElementById(divId); if (!sourceDiv) return; $('#modal-img').style.display = 'none'; let modalPlotDiv = document.getElementById('modal-plot-div'); if (!modalPlotDiv) { modalPlotDiv = document.createElement('div'); modalPlotDiv.id = 'modal-plot-div'; modalPlotDiv.style.cssText = 'width:90vw;height:80vh;'; $('#modal-overlay .modal-content').appendChild(modalPlotDiv); } modalPlotDiv.style.display = 'block'; const data = sourceDiv.data; const layout = Object.assign({}, sourceDiv.layout, { width: window.innerWidth * 0.88, height: window.innerHeight * 0.78 }); Plotly.newPlot('modal-plot-div', data, layout, { responsive: true }); $('#modal-overlay').classList.add('active'); } function closeModal() { $('#modal-overlay').classList.remove('active'); const mp = document.getElementById('modal-plot-div'); if (mp) mp.style.display = 'none'; $('#modal-img').style.display = ''; } function openImageModal(src) { const mp = document.getElementById('modal-plot-div'); if (mp) mp.style.display = 'none'; const img = $('#modal-img'); img.src = src; img.style.display = 'block'; $('#modal-overlay').classList.add('active'); } // Close modal on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { const overlay = $('#modal-overlay'); if (overlay && overlay.classList.contains('active')) { closeModal(); } } }); // Close modal when clicking outside document.addEventListener('DOMContentLoaded', () => { const overlay = $('#modal-overlay'); if (overlay) { overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeModal(); } }); } }); // ===== FORECAST ===== async function loadForecast() { const panel = $('#forecast-panel'); panel.innerHTML = `

Forecast

Predicting future trends...

`; try { const res = await fetch(`${API}/api/forecast`); const data = await res.json(); if (data.success || data.chart_json) { const forecastDivId = 'forecast-plotly-chart'; panel.innerHTML = `

Forecast — ${data.title || 'Prediction'}

Next 3 months ${data.growth_pct ? `(${data.growth_pct > 0 ? '+' : ''}${data.growth_pct}% projected)` : ''}

${data.commentary || data.summary || ''}
`; if (data.chart_json) { Plotly.newPlot(forecastDivId, data.chart_json.data, data.chart_json.layout, { responsive: true, displaylogo: false }); } } else { panel.innerHTML = `

Forecast

${data.error || 'Insufficient data.'}

`; } } catch(e) { panel.innerHTML = `

Forecast

Forecast unavailable.

`; } } // ===== INSIGHTS ===== async function loadInsights() { const panel = $('#insights-panel'); panel.innerHTML = `

Key Insights

`; try { const res = await fetch(`${API}/api/insights`); const data = await res.json(); if (data.success && data.insights) { let html = `

Key Insights

AI-generated actionable findings

`; data.insights.forEach(insight => { html += `
${insight}
`; }); panel.innerHTML = html; } } catch (e) { console.error('Insights error:', e); } } // ===== WHAT-IF (first definition removed — FIX 2) ===== // ── Chat Widget ────────────────────────────────────────── (function () { const fab = document.getElementById('dm-chat-fab'); const popup = document.getElementById('dm-chat-popup'); const closeBtn = document.getElementById('dm-close-btn'); const input = document.getElementById('dm-input'); const sendBtn = document.getElementById('dm-send-btn'); const msgBox = document.getElementById('dm-messages'); const sugsEl = document.getElementById('dm-suggestions'); if(!fab || !popup) return; let isOpen = false; // Initial greeting addMsg("Hi! I'm your AI analyst. Load a dataset and ask me anything — trends, anomalies, insights.", 'bot'); function toggleChat() { isOpen = !isOpen; popup.classList.toggle('open', isOpen); fab.querySelector('i').className = isOpen ? 'ti ti-x' : 'ti ti-message-chatbot'; if (isOpen) input.focus(); } fab.addEventListener('click', toggleChat); closeBtn.addEventListener('click', toggleChat); function addMsg(text, role) { const wrap = document.createElement('div'); wrap.className = 'dm-msg ' + role; // Convert newlines to breaks const sanitised = text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/\n/g, '
'); if (role === 'bot') { wrap.innerHTML = `
${sanitised}
`; } else { wrap.innerHTML = `
${sanitised}
`; } msgBox.appendChild(wrap); msgBox.scrollTop = msgBox.scrollHeight; } function addBotChart(chartImg) { const wrap = document.createElement('div'); wrap.className = 'dm-msg bot'; wrap.innerHTML = `
Chart
`; msgBox.appendChild(wrap); msgBox.scrollTop = msgBox.scrollHeight; } function addBotPlotly(plotlyData) { const wrap = document.createElement('div'); wrap.className = 'dm-msg bot'; const chartId = 'chat-chart-' + Date.now(); wrap.innerHTML = `
`; msgBox.appendChild(wrap); setTimeout(() => { Plotly.newPlot(chartId, plotlyData.data, Object.assign({}, plotlyData.layout, {height: 200, width: 200, margin: {l:20,r:10,t:20,b:20}}), {responsive: true, displaylogo: false, displayModeBar: false}); msgBox.scrollTop = msgBox.scrollHeight; }, 50); } function showTyping() { const t = document.createElement('div'); t.className = 'dm-msg bot'; t.id = 'dm-typing'; t.innerHTML = `
`; msgBox.appendChild(t); msgBox.scrollTop = msgBox.scrollHeight; } function removeTyping() { const t = document.getElementById('dm-typing'); if (t) t.remove(); } async function sendMessage(text) { if (!text.trim()) return; addMsg(text, 'user'); input.value = ''; sugsEl.style.display = 'none'; showTyping(); try { const res = await fetch(`${API}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: text }) }); const data = await res.json(); removeTyping(); if (data.success) { addMsg(data.answer, 'bot'); if (data.chart?.plotly_json) { addBotPlotly(data.chart.plotly_json); } else if (data.chart?.image_base64) { addBotChart(data.chart.image_base64); } } else { addMsg(data.error || 'Sorry, I could not process that question.', 'bot'); } } catch (err) { removeTyping(); addMsg('Connection error. Please try again.', 'bot'); } } sendBtn.addEventListener('click', () => sendMessage(input.value)); input.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(input.value); }); document.querySelectorAll('.dm-sug').forEach(btn => { btn.addEventListener('click', () => sendMessage(btn.dataset.msg)); }); })(); // ===== WHAT-IF SCENARIO ===== async function setupWhatIf() { try { const res = await fetch(`${API}/api/dataset-info`); const data = await res.json(); if (!data.success) return; const numCols = data.numeric_columns || []; if (numCols.length < 2) return; // Need at least 2 numeric cols const targetSelect = $('#whatif-target'); const adjustSelect = $('#whatif-adjust'); targetSelect.innerHTML = ''; adjustSelect.innerHTML = ''; numCols.forEach(col => { targetSelect.innerHTML += ``; adjustSelect.innerHTML += ``; }); // Set different defaults for target and adjust if (numCols.length >= 2) { adjustSelect.selectedIndex = 1; } // Show the what-if section const section = $('#whatif-section'); if (section) section.classList.remove('hidden'); } catch (e) { console.warn('What-If setup failed:', e); } } let _whatIfTimer = null; function updateSliderLabel(val) { const label = $('#slider-label'); if (label) label.textContent = `${val > 0 ? '+' : ''}${val}%`; // Auto-run What-If with debounce clearTimeout(_whatIfTimer); _whatIfTimer = setTimeout(() => { runWhatIf(); }, 400); } async function runWhatIf() { const targetCol = $('#whatif-target')?.value; const adjustCol = $('#whatif-adjust')?.value; const adjustPct = $('#whatif-slider')?.value || 0; if (!targetCol || !adjustCol) { showToast('Please select both target and adjust columns', 'error'); return; } const resultDiv = $('#whatif-result'); const chartImg = $('#whatif-chart'); resultDiv.textContent = 'Running scenario...'; try { const res = await fetch(`${API}/api/whatif`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target_col: targetCol, adjust_col: adjustCol, adjust_pct: parseFloat(adjustPct) }) }); const data = await res.json(); if (data.success) { const orig = data.original?.toLocaleString(undefined, { maximumFractionDigits: 0 }) || '—'; const proj = data.projected?.toLocaleString(undefined, { maximumFractionDigits: 0 }) || '—'; const diff = data.projected - data.original; const diffPct = data.original ? ((diff / data.original) * 100).toFixed(1) : 0; const arrow = diff >= 0 ? '↑' : '↓'; const color = diff >= 0 ? 'var(--success)' : 'var(--danger)'; resultDiv.innerHTML = ` ${targetCol}: ${orig} → ${proj} ${arrow} ${diffPct}% (when ${adjustCol} changes by ${adjustPct > 0 ? '+' : ''}${adjustPct}%) `; if (data.chart_json) { let wiDiv = document.getElementById('whatif-plotly'); if (!wiDiv) { wiDiv = document.createElement('div'); wiDiv.id = 'whatif-plotly'; wiDiv.style.cssText = 'width:100%;height:380px;margin-top:16px;'; chartImg.parentNode.insertBefore(wiDiv, chartImg); } Plotly.react('whatif-plotly', data.chart_json.data, data.chart_json.layout, { responsive: true, displaylogo: false }); chartImg.classList.add('hidden'); } } else { resultDiv.textContent = data.error || 'Scenario failed'; } } catch (e) { resultDiv.textContent = 'Error running scenario'; } } // ===== EXPORT ===== function toggleExportMenu() { const menu = document.getElementById('export-menu'); if (menu) menu.classList.toggle('active'); // Close on outside click setTimeout(() => { document.addEventListener('click', function closeExport(e) { if (!e.target.closest('.export-dropdown')) { menu?.classList.remove('active'); document.removeEventListener('click', closeExport); } }); }, 10); } async function exportData(format = 'csv') { const menu = document.getElementById('export-menu'); if (menu) menu.classList.remove('active'); try { if (format === 'csv' || format === 'excel') { // Use hidden iframe — most reliable cross-browser download method const endpoint = format === 'excel' ? '/api/export/excel' : '/api/export/csv'; let iframe = document.getElementById('download-iframe'); if (!iframe) { iframe = document.createElement('iframe'); iframe.id = 'download-iframe'; iframe.style.display = 'none'; document.body.appendChild(iframe); } iframe.src = `${API}${endpoint}`; showToast(`${format.toUpperCase()} download started!`, 'success'); } else { // JSON — build from API response const res = await fetch(`${API}/api/export`); if (!res.ok) { showToast('No dataset loaded', 'error'); return; } const data = await res.json(); if (!data.success) { showToast(data.error || 'Export failed', 'error'); return; } const safeName = (data.report.dataset_name || 'export').replace(/\s+/g, '_'); const jsonStr = JSON.stringify(data.report, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `datamind_report_${safeName}.json`; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 2000); showToast('JSON report exported!', 'success'); } } catch (e) { showToast('Export failed: ' + e.message, 'error'); } } // ===== INIT ===== document.addEventListener('DOMContentLoaded', () => { initUpload(); initChat(); initResize(); // Modal close $('#modal-overlay').addEventListener('click', (e) => { if (e.target === $('#modal-overlay')) closeModal(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); }); }); // ===== RESIZABLE PANELS ===== function initResize() { const layout = $('#app-layout'); const sidebar = $('#sidebar-panel'); const chatPanel = $('#chat-panel'); const leftHandle = $('#resize-left'); const rightHandle = $('#resize-right'); if (!layout || !sidebar || !chatPanel) return; let isResizing = false; let currentHandle = null; function onMouseDown(handle) { return (e) => { e.preventDefault(); isResizing = true; currentHandle = handle; handle.classList.add('active'); document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }; } function onMouseMove(e) { if (!isResizing) return; const layoutRect = layout.getBoundingClientRect(); if (currentHandle === leftHandle) { let newWidth = e.clientX - layoutRect.left; newWidth = Math.max(200, Math.min(450, newWidth)); layout.style.gridTemplateColumns = `${newWidth}px 4px 1fr 4px ${chatPanel.offsetWidth}px`; } else if (currentHandle === rightHandle) { let newWidth = layoutRect.right - e.clientX; newWidth = Math.max(280, Math.min(550, newWidth)); layout.style.gridTemplateColumns = `${sidebar.offsetWidth}px 4px 1fr 4px ${newWidth}px`; } } function onMouseUp() { if (!isResizing) return; isResizing = false; if (currentHandle) currentHandle.classList.remove('active'); currentHandle = null; document.body.style.cursor = ''; document.body.style.userSelect = ''; } leftHandle.addEventListener('mousedown', onMouseDown(leftHandle)); rightHandle.addEventListener('mousedown', onMouseDown(rightHandle)); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); } // ===== KPI CARDS ===== async function loadKPIs() { try { const res = await fetch(`${API}/api/kpis`); const data = await res.json(); if (!data.success || !data.kpis) return; const row = $('#kpi-row'); row.innerHTML = ''; data.kpis.forEach((kpi, i) => { // Skip the quality KPI from the KPI row — it goes in the sidebar gauge if (kpi.label === 'Data Quality') { updateQualityGauge(kpi.value, kpi.quality_breakdown); return; } const card = document.createElement('div'); card.className = 'kpi-card'; card.style.animationDelay = `${i * 0.1}s`; const formatted = formatKPIValue(kpi.value, kpi.format); const trendClass = kpi.trend > 0 ? 'up' : kpi.trend < 0 ? 'down' : 'neutral'; const trendIcon = kpi.trend > 0 ? '↑' : kpi.trend < 0 ? '↓' : '—'; card.innerHTML = `
${kpi.label}
${formatted}
${trendIcon} ${kpi.trend_label || ''}
`; row.appendChild(card); }); // Animate counting animateKPICounters(); } catch (e) { console.warn('KPI load failed:', e); } } function formatKPIValue(value, format) { if (format === 'currency') { if (value >= 1000000) return `$${(value/1000000).toFixed(1)}M`; if (value >= 1000) return `$${(value/1000).toFixed(1)}K`; return `$${value.toFixed(2)}`; } if (format === 'percent') return `${value}%`; if (format === 'days') return `${value} days`; if (format === 'number') return value.toLocaleString(); return String(value); } function animateKPICounters() { document.querySelectorAll('.kpi-value').forEach(el => { const target = parseFloat(el.dataset.target); const format = el.dataset.format; if (isNaN(target)) return; let current = 0; const increment = target / 40; const timer = setInterval(() => { current += increment; if (current >= target) { current = target; clearInterval(timer); } el.textContent = formatKPIValue(current, format); }, 30); }); } // ===== DATA QUALITY GAUGE ===== function updateQualityGauge(score, breakdown) { const section = $('#quality-section'); if (!section) return; section.style.display = 'block'; const fill = $('#gauge-fill'); const text = $('#gauge-text'); const details = $('#quality-details'); // Circumference = 2 * PI * r = 2 * 3.1416 * 52 ≈ 326.7 const circumference = 326.7; const offset = circumference - (score / 100) * circumference; // Animate after a short delay setTimeout(() => { fill.style.strokeDashoffset = offset; fill.classList.remove('good', 'warn', 'bad'); if (score >= 80) fill.classList.add('good'); else if (score >= 60) fill.classList.add('warn'); else fill.classList.add('bad'); }, 200); text.textContent = `${Math.round(score)}%`; if (breakdown) { details.innerHTML = `
Completeness ${breakdown.completeness}%
Uniqueness ${breakdown.uniqueness}%
Outlier Health ${breakdown.outlier_health}%
`; } } // ===== BUSINESS RECOMMENDATIONS ===== async function loadRecommendations() { const panel = $('#recommendations-panel'); if (!panel) return; try { const res = await fetch(`${API}/api/recommendations`); const data = await res.json(); if (!data.success || !data.recommendations) return; const recs = data.recommendations; const icons = { critical: '●', opportunity: '●', strength: '●' }; let html = `

Business Recommendations

${recs.length} actionable strategies identified

`; recs.forEach((rec, i) => { const severity = rec.severity || 'opportunity'; html += `
${icons[severity] || '●'}
${rec.title}
${rec.description}
`; }); panel.innerHTML = html; } catch (e) { console.warn('Recommendations load failed:', e); } }