document.addEventListener('DOMContentLoaded', () => { const dropZone = document.getElementById('drop-zone'); const fileInput = document.getElementById('file-input'); const welcomeState = document.getElementById('welcome-state'); const analysisState = document.getElementById('analysis-state'); const terminalLogs = document.getElementById('terminal-logs'); const resultsContainer = document.getElementById('results-container'); const aiInsightsContent = document.getElementById('ai-insights-content'); const profileStats = document.getElementById('profile-stats'); const planList = document.getElementById('plan-list'); const questionsList = document.getElementById('questions-list'); const chartContainer = document.getElementById('chart-container'); const jobStatusTitle = document.getElementById('job-status-title'); const downloadBtn = document.getElementById('download-btn'); const downloadJsonBtn = document.getElementById('download-json-btn'); const downloadHtmlBtn = document.getElementById('download-html-btn'); const downloadPdfBtn = document.getElementById('download-pdf-btn'); const sqlDbUrlInput = document.getElementById('sql-db-url'); const sqlTableNameInput = document.getElementById('sql-table-name'); const sqlUploadBtn = document.getElementById('sql-upload-btn'); const cleaningModal = document.getElementById('cleaning-modal'); const plannerModal = document.getElementById('planner-modal'); const cleaningPlanContainer = document.getElementById('cleaning-plan-container'); const modalQuestionsList = document.getElementById('modal-questions-list'); const modalPlanList = document.getElementById('modal-plan-list'); const approveCleaningBtn = document.getElementById('approve-cleaning-btn'); const approvePlanBtn = document.getElementById('approve-plan-btn'); let currentCleaningPlan = null; let currentAnalysisPlan = null; let currentAnalyticalQuestions = null; let currentJobId = null; let pollingInterval = null; let pollFailureCount = 0; const maxPollFailures = 5; const apiKey = localStorage.getItem('API_KEY'); const requireApiKey = window.APP_CONFIG && window.APP_CONFIG.require_api_key; const backendBase = (window.APP_CONFIG && window.APP_CONFIG.backend_url) || ''; function buildHeaders(extraHeaders = {}) { const headers = { ...extraHeaders }; if (apiKey) headers['X-API-Key'] = apiKey; return headers; } function buildUrl(path) { if (!backendBase) return path; const base = backendBase.replace(/\/+$/, ''); const normalized = path.startsWith('/') ? path : `/${path}`; return `${base}${normalized}`; } async function readError(res) { try { const data = await res.json(); return data.detail || data.error || 'Unknown error'; } catch (e) { return 'Unknown error'; } } function stopPolling() { if (pollingInterval) { clearInterval(pollingInterval); pollingInterval = null; } } function authorizationHint(statusCode) { if (statusCode === 401) { return ' Unauthorized request. Set localStorage API_KEY to match API_KEY.'; } return ''; } function setStatusWithLoader(message) { jobStatusTitle.textContent = message; const loader = document.createElement('span'); loader.className = 'loader'; jobStatusTitle.appendChild(document.createTextNode(' ')); jobStatusTitle.appendChild(loader); } function setStatusWithIcon(message, iconClass, color) { jobStatusTitle.textContent = ''; const icon = document.createElement('i'); icon.className = iconClass; if (color) icon.style.color = color; jobStatusTitle.appendChild(icon); jobStatusTitle.appendChild(document.createTextNode(` ${message}`)); } // File Upload Handlers dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); }); fileInput.addEventListener('change', (e) => { if (e.target.files.length) handleFile(e.target.files[0]); }); sqlUploadBtn.addEventListener('click', () => { const dbUrl = (sqlDbUrlInput.value || '').trim(); const tableName = (sqlTableNameInput.value || '').trim(); if (!dbUrl || !tableName) { alert('Enter both database URL and table name.'); return; } handleSqlUpload(dbUrl, tableName); }); async function handleFile(file) { if (!file.name.toLowerCase().endsWith('.csv')) { alert('Please upload a CSV file.'); return; } // Switch UI state stopPolling(); pollFailureCount = 0; welcomeState.classList.add('hidden'); analysisState.classList.remove('hidden'); resultsContainer.classList.add('hidden'); terminalLogs.textContent = ''; downloadBtn.classList.add('hidden'); setStatusWithLoader('Uploading Dataset...'); const formData = new FormData(); formData.append('file', file); try { logTerminal(`[System]: Initiating upload for ${file.name}...`); const res = await fetch(buildUrl('/upload_dataset'), { method: 'POST', headers: buildHeaders(), body: formData }); if (res.ok) { const data = await res.json(); currentJobId = data.job_id; logTerminal(`[System]: Upload successful. Job ID: ${currentJobId}`); startAnalysis(currentJobId); } else { const errorMsg = await readError(res); logTerminal(`[Error]: Upload failed - ${errorMsg}${authorizationHint(res.status)}`, true); } } catch (error) { console.error('Upload Error:', error); logTerminal(`[Error]: Upload failed - Network Error`, true); } } async function handleSqlUpload(databaseUrl, tableName) { stopPolling(); pollFailureCount = 0; welcomeState.classList.add('hidden'); analysisState.classList.remove('hidden'); resultsContainer.classList.add('hidden'); terminalLogs.textContent = ''; downloadBtn.classList.add('hidden'); setStatusWithLoader('Loading SQL Table...'); try { logTerminal(`[System]: Loading SQL table ${tableName}...`); const res = await fetch(buildUrl('/upload_sql_table'), { method: 'POST', headers: buildHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ database_url: databaseUrl, table_name: tableName, limit: 30000 }) }); if (res.ok) { const data = await res.json(); currentJobId = data.job_id; logTerminal(`[System]: SQL table upload successful. Job ID: ${currentJobId}`); startAnalysis(currentJobId); } else { const errorMsg = await readError(res); logTerminal(`[Error]: SQL upload failed - ${errorMsg}${authorizationHint(res.status)}`, true); } } catch (error) { console.error('SQL Upload Error:', error); logTerminal('[Error]: SQL upload failed - Network Error', true); } } async function startAnalysis(jobId) { try { logTerminal('[System]: Requesting analysis start...'); setStatusWithLoader('Analysis in Progress...'); const res = await fetch(buildUrl('/start_analysis'), { method: 'POST', headers: buildHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ job_id: jobId }) }); if (res.ok) { stopPolling(); // Start polling status pollingInterval = setInterval(() => pollStatus(jobId), 1500); } else { const errorMsg = await readError(res); logTerminal(`[Error]: Start analysis failed - ${errorMsg}${authorizationHint(res.status)}`, true); } } catch (error) { console.error('Start Error:', error); logTerminal(`[Error]: Start analysis failed - Network Error`, true); } } async function pollStatus(jobId) { try { const res = await fetch(buildUrl(`/analysis_status/${jobId}`), { headers: buildHeaders() }); if (!res.ok) { pollFailureCount += 1; if (pollFailureCount >= maxPollFailures) { stopPolling(); setStatusWithIcon('Analysis status unavailable', 'fa-solid fa-triangle-exclamation text-error', '#ef4444'); const apiHint = requireApiKey ? ' If API key auth is enabled, ensure localStorage API_KEY is set.' : ''; logTerminal(`[Error]: Status polling failed repeatedly. Please retry.${apiHint}`, true); } return; } const data = await res.json(); pollFailureCount = 0; // Sync logs if (data.progress_logs) { terminalLogs.textContent = data.progress_logs; terminalLogs.scrollTop = terminalLogs.scrollHeight; } if (data.status === 'pending_cleaning') { stopPolling(); setStatusWithIcon('Review Cleaning Proposals', 'fa-solid fa-broom text-yellow', '#f59e0b'); logTerminal('[System]: Analysis paused. Waiting for human approval of data cleaning proposals.'); fetchAndShowCleaningModal(jobId); } else if (data.status === 'pending_approval') { stopPolling(); setStatusWithIcon('Review Analysis Plan', 'fa-solid fa-sitemap text-indigo', '#818cf8'); logTerminal('[System]: Analysis paused. Waiting for human approval of execution plan.'); fetchAndShowPlannerModal(jobId); } else if (data.status === 'completed') { stopPolling(); logTerminal('[System]: Workflow execution successful.'); setStatusWithIcon('Analysis Complete', 'fa-solid fa-check-circle text-success', '#10b981'); downloadBtn.classList.remove('hidden'); downloadBtn.onclick = () => downloadReport(jobId, 'pdf'); downloadJsonBtn.onclick = () => downloadReport(jobId, 'json'); downloadHtmlBtn.onclick = () => downloadReport(jobId, 'html'); downloadPdfBtn.onclick = () => downloadReport(jobId, 'pdf'); fetchAndRenderReport(jobId); } else if (data.status === 'error') { stopPolling(); setStatusWithIcon('Analysis Failed', 'fa-solid fa-times-circle text-error', '#ef4444'); logTerminal(`[CRITICAL ERROR]: ${data.error_message}`, true); } } catch (error) { console.error('Poll Error:', error); pollFailureCount += 1; if (pollFailureCount >= maxPollFailures) { stopPolling(); setStatusWithIcon('Analysis status unavailable', 'fa-solid fa-triangle-exclamation text-error', '#ef4444'); logTerminal('[Error]: Network polling failed repeatedly. Please retry.', true); } } } async function fetchAndRenderReport(jobId) { try { logTerminal('[System]: Fetching report data...'); const res = await fetch(buildUrl(`/download_report/${jobId}?format=json`), { headers: buildHeaders() }); if (!res.ok) throw new Error("Report not generated"); const report = await res.json(); renderReport(report); resultsContainer.classList.remove('hidden'); logTerminal('[System]: UI Results Rendered.'); } catch (error) { console.error('Fetch Report Error:', error); logTerminal(`[System]: Failed to fetch UI JSON report - ${error.message}`, true); } } function renderReport(report) { // 1. Render Insights if(report.insights) { const rendered = marked.parse(report.insights); aiInsightsContent.innerHTML = DOMPurify.sanitize(rendered); } // 2. Render Profile Stats const profile = report.profile; if(profile) { profileStats.textContent = ''; const stats = [ { label: 'Total Rows', value: profile.num_rows }, { label: 'Total Columns', value: profile.num_cols } ]; stats.forEach((stat) => { const item = document.createElement('div'); item.className = 'stat-item'; const value = document.createElement('div'); value.className = 'stat-value'; value.textContent = String(stat.value ?? 'N/A'); const label = document.createElement('div'); label.className = 'stat-label'; label.textContent = stat.label; item.appendChild(value); item.appendChild(label); profileStats.appendChild(item); }); } // 3. Render Plan const plan = report.plan; if(plan && Array.isArray(plan)) { planList.textContent = ''; plan.forEach((step, index) => { const li = document.createElement('li'); li.textContent = step && step.task ? String(step.task) : `Task ${index + 1}`; planList.appendChild(li); }); } // 4. Render Ranked Analytical Questions const questions = report.analytical_questions; if (questions && Array.isArray(questions)) { questionsList.textContent = ''; questions.forEach((q, index) => { const li = document.createElement('li'); const rank = q && q.rank ? `#${q.rank}` : `#${index + 1}`; const question = q && q.question ? String(q.question) : 'Question unavailable'; li.textContent = `${rank} ${question}`; questionsList.appendChild(li); }); } // 5. Render Visualizations (multiple chart specs) chartContainer.textContent = ''; const chartSpecs = report.visualizations && Array.isArray(report.visualizations.chart_specs) ? report.visualizations.chart_specs : []; if (chartSpecs.length > 0) { chartSpecs.forEach((spec, idx) => { const chartDiv = document.createElement('div'); chartDiv.id = `chart-${idx}`; chartDiv.style.minHeight = '320px'; chartDiv.style.marginBottom = '1rem'; chartContainer.appendChild(chartDiv); const layout = { ...(spec.layout || {}), title: spec.title || 'Chart', paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', font: { color: '#94a3b8' } }; Plotly.newPlot(chartDiv, spec.data || [], layout, { responsive: true }); }); } else { chartContainer.innerHTML = '
No visualizations applicable for this dataset structure.
'; } } function logTerminal(msg, isError = false) { const span = document.createElement('div'); span.textContent = msg; if (isError) span.style.color = '#ef4444'; terminalLogs.appendChild(span); terminalLogs.scrollTop = terminalLogs.scrollHeight; } async function fetchAndShowCleaningModal(jobId) { try { const res = await fetch(buildUrl(`/job_cleaning/${jobId}`), { headers: buildHeaders() }); const data = await res.json(); if (res.ok) { currentCleaningPlan = data.cleaning_plan || []; cleaningPlanContainer.textContent = ''; if (currentCleaningPlan.length === 0) { cleaningPlanContainer.textContent = 'No specific cleaning required.'; } else { const pre = document.createElement('pre'); pre.className = 'json-view'; pre.textContent = JSON.stringify(currentCleaningPlan, null, 2); cleaningPlanContainer.appendChild(pre); } cleaningModal.classList.remove('hidden'); } else { logTerminal('[Error]: Failed to fetch cleaning plan', true); } } catch (e) { logTerminal('[Error]: Network error fetching cleaning plan', true); } } async function fetchAndShowPlannerModal(jobId) { try { const res = await fetch(buildUrl(`/job_plan/${jobId}`), { headers: buildHeaders() }); const data = await res.json(); if (res.ok) { currentAnalyticalQuestions = data.analytical_questions || []; currentAnalysisPlan = data.analysis_plan || []; modalQuestionsList.textContent = ''; currentAnalyticalQuestions.forEach(q => { const li = document.createElement('li'); li.textContent = q.question || JSON.stringify(q); modalQuestionsList.appendChild(li); }); modalPlanList.textContent = ''; currentAnalysisPlan.forEach(p => { const li = document.createElement('li'); li.textContent = p.task || JSON.stringify(p); modalPlanList.appendChild(li); }); plannerModal.classList.remove('hidden'); } else { logTerminal('[Error]: Failed to fetch analysis plan', true); } } catch (e) { logTerminal('[Error]: Network error fetching analysis plan', true); } } approveCleaningBtn.addEventListener('click', async () => { cleaningModal.classList.add('hidden'); logTerminal('[System]: Sending approved cleaning plan to engine...'); setStatusWithLoader('Executing Cleaning...'); try { const res = await fetch(buildUrl('/approve_cleaning'), { method: 'POST', headers: buildHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ job_id: currentJobId, cleaning_plan: currentCleaningPlan }) }); if (res.ok) { logTerminal('[System]: Cleaning approved. Resuming background workflows...'); pollingInterval = setInterval(() => pollStatus(currentJobId), 1500); } else { logTerminal('[Error]: Failed to approve cleaning', true); } } catch (e) { logTerminal('[Error]: Network error during approval', true); } }); approvePlanBtn.addEventListener('click', async () => { plannerModal.classList.add('hidden'); logTerminal('[System]: Sending approved execution plan to engine...'); setStatusWithLoader('Executing Analysis...'); try { const res = await fetch(buildUrl('/approve_plan'), { method: 'POST', headers: buildHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ job_id: currentJobId, analysis_plan: currentAnalysisPlan }) }); if (res.ok) { logTerminal('[System]: Execution plan approved. Engine starting execution phase...'); pollingInterval = setInterval(() => pollStatus(currentJobId), 1500); } else { logTerminal('[Error]: Failed to approve plan', true); } } catch (e) { logTerminal('[Error]: Network error during approval', true); } }); window.addEventListener('beforeunload', () => { stopPolling(); }); async function downloadReport(jobId, format) { try { const res = await fetch(buildUrl(`/download_report/${jobId}?format=${format}`), { headers: buildHeaders() }); if (!res.ok) { const errorMsg = await readError(res); logTerminal(`[Error]: Download failed - ${errorMsg}${authorizationHint(res.status)}`, true); return; } const blob = await res.blob(); const fileExt = format === 'pdf' ? 'pdf' : (format === 'html' ? 'html' : 'json'); const filename = `report_${jobId}.${fileExt}`; const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } catch (error) { logTerminal('[Error]: Download failed - Network Error', true); } } // --- Chat with Data Logic --- const chatBtn = document.getElementById('chat-btn'); const chatInput = document.getElementById('chat-input'); const chatHistory = document.getElementById('chat-history'); async function sendChatMessage() { if (!currentJobId) return; const query = chatInput.value.trim(); if (!query) return; // Add user message const userMsg = document.createElement('div'); userMsg.style = "align-self: flex-end; background: #3b82f6; color: white; padding: 8px 12px; border-radius: 8px; max-width: 80%; word-wrap: break-word;"; userMsg.innerText = query; chatHistory.appendChild(userMsg); chatInput.value = ''; chatInput.disabled = true; chatBtn.disabled = true; chatBtn.innerHTML = ''; chatHistory.scrollTop = chatHistory.scrollHeight; try { const res = await fetch(buildUrl(`/chat/${currentJobId}`), { method: 'POST', headers: buildHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ query: query }) }); const data = await res.json(); const aiMsg = document.createElement('div'); aiMsg.style = "align-self: flex-start; background: #334155; color: #f8fafc; padding: 8px 12px; border-radius: 8px; max-width: 80%; word-wrap: break-word; border:1px solid #475569;"; if (res.ok) { aiMsg.innerText = data.response; } else { aiMsg.innerText = `Error: ${data.detail || 'Could not process query.'}`; } chatHistory.appendChild(aiMsg); } catch (error) { const aiMsg = document.createElement('div'); aiMsg.style = "align-self: flex-start; background: #7f1d1d; color: #f8fafc; padding: 8px 12px; border-radius: 8px; border:1px solid #991b1b;"; aiMsg.innerText = "Network error. Please try again."; chatHistory.appendChild(aiMsg); } finally { chatInput.disabled = false; chatBtn.disabled = false; chatBtn.innerHTML = 'Send '; chatHistory.scrollTop = chatHistory.scrollHeight; chatInput.focus(); } } if (chatBtn && chatInput) { chatBtn.addEventListener('click', sendChatMessage); chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendChatMessage(); }); } });