shoaib4045's picture
Add Conversational Analytics (Chat with Data) feature
b7e1e0c
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 = '<div class="empty-state">No visualizations applicable for this dataset structure.</div>';
}
}
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 = '<i class="fa-solid fa-spinner fa-spin"></i>';
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 <i class="fa-solid fa-paper-plane"></i>';
chatHistory.scrollTop = chatHistory.scrollHeight;
chatInput.focus();
}
}
if (chatBtn && chatInput) {
chatBtn.addEventListener('click', sendChatMessage);
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendChatMessage();
});
}
});