khwajai/spreadsheet
0
1const API_URL = ""; 2let chartCounter = 0;3let appState = {4 gridData: [], columns: [], sheets: [], activeSheet: "Sheet 1",5 selection: { start: null, end: null }, isResizing: false, resizeCol: null, startX: 0, startWidth: 0, colWidths: [],6 activeCellCoord: null, currentView: 'grid',7 savedCharts: [] 8};9 10let latestStats = { revenue: "$0", profit: "$0", margin: "0%", count: "0" };11 12const colorPalettes = {13 corporate: ['#0ea5e9', '#0284c7', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#64748b', '#334155'],14 emerald: ['#14b8a6', '#0d9488', '#10b981', '#059669', '#34d399', '#0f766e', '#06b6d4', '#0891b2'],15 cyberpunk: ['#38bdf8', '#0ea5e9', '#8b5cf6', '#a855f7', '#c084fc', '#e879f9', '#2dd4bf', '#22d3ee']16};17 18const chartResizeObserver = new ResizeObserver(entries => {19 for (let entry of entries) {20 const plotDiv = entry.target.querySelector('.js-plotly-plot');21 if (plotDiv) Plotly.Plots.resize(plotDiv);22 }23});24 25document.addEventListener("DOMContentLoaded", () => { 26 fetchState(); 27 setupGlobalEvents(); 28});29 30function toggleTheme() {31 const body = document.body;32 const current = body.getAttribute('data-theme');33 const target = current === 'light' ? 'dark' : 'light';34 body.setAttribute('data-theme', target);35 document.querySelector('#theme-toggle i').className = target === 'light' ? 'fa-solid fa-sun' : 'fa-solid fa-moon';36 if(appState.currentView === 'visual') document.querySelectorAll('.js-plotly-plot').forEach(p => Plotly.Plots.resize(p));37}38 39function toggleAIPanel() {40 const panel = document.getElementById('ai-panel');41 const icon = document.getElementById('ai-toggle-icon');42 const gridPanel = document.getElementById('main-grid-panel');43 44 panel.classList.toggle('collapsed');45 gridPanel.classList.toggle('shifted');46 47 if(panel.classList.contains('collapsed')) {48 icon.classList.remove('fa-chevron-right'); icon.classList.add('fa-chevron-left');49 } else {50 icon.classList.remove('fa-chevron-left'); icon.classList.add('fa-chevron-right');51 }52 53 setTimeout(() => { document.querySelectorAll('.js-plotly-plot').forEach(p => Plotly.Plots.resize(p)); }, 350);54}55 56async function syncDataToBackend() {57 try {58 await fetch(`${API_URL}/sheet/update`, {59 method: 'POST',60 headers: {'Content-Type': 'application/json'},61 body: JSON.stringify({data: appState.gridData, columns: appState.columns})62 });63 updateBusinessStats();64 } catch(e) { console.error("Sync error:", e); }65}66 67async function fetchState() {68 try {69 const res = await fetch(`${API_URL}/grid`);70 const data = await res.json();71 appState.sheets = data.sheets || ["Sheet 1"];72 appState.activeSheet = data.active || "Sheet 1";73 let cols = data.columns || ["Date","Department","Revenue","Expenses","Status"];74 if (!data.data || data.data.length === 0) data.data = Array.from({length: 30}, () => { let r={}; cols.forEach(c=>r[c]=""); return r; });75 renderTabs(); initGrid(data.data, cols); updateBusinessStats(); 76 } catch(e) { console.error(e); }77}78 79async function promoteHeader() {80 try {81 const res = await fetch(`${API_URL}/promote-header`, { method: "POST" });82 const data = await res.json();83 if(data.status === "success") fetchState();84 } catch(e) {}85}86 87function openDataModeler() {88 const colContainer = document.getElementById('modeler-cols');89 colContainer.innerHTML = '';90 appState.columns.forEach(col => {91 colContainer.innerHTML += `<div style="margin-bottom:6px;"><label style="color:var(--text-main); font-size:13px; cursor:pointer;"><input type="checkbox" class="modeler-checkbox" value="${col}" checked style="margin-right:8px;"> ${col}</label></div>`;92 });93 document.getElementById('data-modeler-modal').classList.remove('hidden');94}95 96async function applyDataModel() {97 const checkboxes = document.querySelectorAll('.modeler-checkbox');98 const selectedCols = Array.from(checkboxes).filter(cb => cb.checked).map(cb => cb.value);99 const filterText = document.getElementById('modeler-filter').value.trim();100 document.getElementById('data-modeler-modal').classList.add('hidden');101 try {102 const payload = { selected_cols: selectedCols, filter_val: filterText };103 const res = await fetch(`${API_URL}/model-data`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });104 const data = await res.json();105 if(data.status === "success") fetchState();106 } catch(e) { alert("Modeler Error"); }107}108 109async function generatePivot() {110 if(appState.columns.length < 2) return;111 const groupCol = prompt(`Enter exact column name to Pivot/Group By:`, appState.columns[0]);112 if(!groupCol) return;113 try {114 const res = await fetch(`${API_URL}/pivot`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({group_col: groupCol}) });115 const data = await res.json();116 if(data.status === "success") fetchState();117 } catch(e) {}118}119 120function exportToCSV() {121 if (appState.gridData.length === 0) return;122 let csvContent = "data:text/csv;charset=utf-8," + appState.columns.join(",") + "\n";123 appState.gridData.forEach(row => {124 let rowArray = appState.columns.map(col => `"${row[col] || ''}"`);125 csvContent += rowArray.join(",") + "\n";126 });127 const link = document.createElement("a"); link.href = encodeURI(csvContent); link.download = `${appState.activeSheet}.csv`;128 document.body.appendChild(link); link.click(); document.body.removeChild(link);129}130 131function initGrid(data = null, columns = null) {132 const container = document.getElementById("spreadsheet"); 133 if(!container) return; container.innerHTML = "";134 appState.columns = columns || ["A","B","C"];135 appState.gridData = data || [];136 if (appState.colWidths.length !== appState.columns.length) appState.colWidths = appState.columns.map(() => 140);137 container.style.gridTemplateColumns = `40px ` + appState.colWidths.map(w => `${w}px`).join(" ");138 139 const fragment = document.createDocumentFragment();140 createCell(fragment, "corner", '<i class="fa-solid fa-th"></i>', selectAll);141 appState.columns.forEach((col, i) => {142 const h = document.createElement("div"); h.className = "header"; h.innerText = col; h.onclick = (e) => selectColumn(i, e); 143 const handle = document.createElement("div"); handle.className = "resizer"; handle.onmousedown = (e) => startResize(e, i);144 h.appendChild(handle); fragment.appendChild(h);145 });146 147 const renderLimit = Math.min(appState.gridData.length, 100); 148 for(let r = 0; r < renderLimit; r++) {149 const row = appState.gridData[r];150 createCell(fragment, "header", r + 1, () => selectRow(r));151 appState.columns.forEach((col, c) => {152 const cell = document.createElement("div");153 cell.className = "cell"; cell.contentEditable = true; cell.innerText = row[col] || ""; cell.dataset.r = r; cell.dataset.c = c;154 if (String(row[col]).includes("TOTAL")) cell.classList.add("total-row-highlight");155 cell.onfocus = () => activateFormulaTracking(cell, r, col);156 cell.onmousedown = (e) => startSelect(e, r, c);157 cell.onmouseenter = (e) => updateSelect(e, r, c);158 cell.onkeydown = (e) => handleKey(e, r, c, cell);159 cell.onblur = () => { evaluateFormula(cell); handleUpdate(cell, r, col); };160 fragment.appendChild(cell);161 });162 }163 container.appendChild(fragment);164}165 166function activateFormulaTracking(cell, r, col) {167 appState.activeCellCoord = {r, col};168 const formulaBar = document.getElementById("grid-formula-bar");169 formulaBar.disabled = false; formulaBar.value = cell.dataset.formula || cell.innerText;170}171 172async function updateBusinessStats() {173 try {174 const res = await fetch(`${API_URL}/business-stats`);175 const stats = await res.json();176 latestStats.revenue = '$' + stats.revenue.toLocaleString('en-US', {minimumFractionDigits: 2});177 latestStats.expenses = '$' + stats.expenses.toLocaleString('en-US', {minimumFractionDigits: 2});178 latestStats.profit = '$' + stats.profit.toLocaleString('en-US', {minimumFractionDigits: 2});179 180 document.getElementById('stat-count').innerText = stats.count;181 document.getElementById('stat-revenue').innerText = latestStats.revenue;182 document.getElementById('stat-expenses').innerText = latestStats.expenses;183 document.getElementById('stat-profit').innerText = latestStats.profit;184 } catch (e) {}185}186 187// --- BI STUDIO (Absolute Canvas Engine) ---188function switchVizMode(mode) {189 document.querySelectorAll('.viz-tab-btn').forEach(b => b.classList.remove('active'));190 document.getElementById(`tab-${mode}`).classList.add('active');191 document.getElementById('viz-builder-mode').classList.add('hidden');192 document.getElementById('viz-canvas-mode').classList.add('hidden');193 document.getElementById(`viz-${mode}-mode`).classList.remove('hidden');194 if(mode === 'canvas') document.querySelectorAll('.js-plotly-plot').forEach(p => Plotly.Plots.resize(p));195}196 197// True Custom Absolute Drag & Resize Logic (Fixes all Flexbox bugs)198function makeAbsoluteDraggableAndResizable(elmnt) {199 let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;200 const header = elmnt.querySelector('.slide-hover-menu');201 const resizer = elmnt.querySelector('.resize-handle');202 203 if (header) { header.onmousedown = dragMouseDown; }204 if (resizer) { resizer.onmousedown = resizeMouseDown; }205 206 function dragMouseDown(e) {207 e.preventDefault(); e.stopPropagation();208 pos3 = e.clientX; pos4 = e.clientY;209 document.onmouseup = closeDragElement;210 document.onmousemove = elementDrag;211 document.querySelectorAll('.dash-card-slide').forEach(c => c.style.zIndex = 1);212 elmnt.style.zIndex = 10;213 }214 215 function elementDrag(e) {216 e.preventDefault();217 pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY;218 pos3 = e.clientX; pos4 = e.clientY;219 220 const parent = elmnt.parentElement;221 let newTop = elmnt.offsetTop - pos2;222 let newLeft = elmnt.offsetLeft - pos1;223 224 // Bounds checking225 if(newTop < 0) newTop = 0;226 if(newLeft < 0) newLeft = 0;227 if(newTop + elmnt.offsetHeight > parent.offsetHeight) newTop = parent.offsetHeight - elmnt.offsetHeight;228 if(newLeft + elmnt.offsetWidth > parent.offsetWidth) newLeft = parent.offsetWidth - elmnt.offsetWidth;229 230 elmnt.style.top = newTop + "px";231 elmnt.style.left = newLeft + "px";232 }233 234 function resizeMouseDown(e) {235 e.preventDefault(); e.stopPropagation();236 pos3 = e.clientX; pos4 = e.clientY;237 document.onmouseup = closeDragElement;238 document.onmousemove = elementResize;239 document.querySelectorAll('.dash-card-slide').forEach(c => c.style.zIndex = 1);240 elmnt.style.zIndex = 10;241 }242 243 function elementResize(e) {244 e.preventDefault();245 let deltaX = e.clientX - pos3;246 let deltaY = e.clientY - pos4;247 pos3 = e.clientX; pos4 = e.clientY;248 249 let newWidth = elmnt.offsetWidth + deltaX;250 let newHeight = elmnt.offsetHeight + deltaY;251 252 // Minimum limits253 if(newWidth < 150) newWidth = 150;254 if(newHeight < 100) newHeight = 100;255 256 // Parent bounds checking257 const parent = elmnt.parentElement;258 if (elmnt.offsetLeft + newWidth > parent.offsetWidth) newWidth = parent.offsetWidth - elmnt.offsetLeft;259 if (elmnt.offsetTop + newHeight > parent.offsetHeight) newHeight = parent.offsetHeight - elmnt.offsetTop;260 261 elmnt.style.width = newWidth + "px";262 elmnt.style.height = newHeight + "px";263 }264 265 function closeDragElement() { 266 document.onmouseup = null; 267 document.onmousemove = null; 268 const plot = elmnt.querySelector('.js-plotly-plot');269 if(plot) Plotly.Plots.resize(plot);270 }271}272 273async function previewChart() { 274 const type = document.getElementById("chart-type").value; 275 const x = document.getElementById("x-axis").value; 276 const y = document.getElementById("y-axis").value;277 const agg = document.getElementById("y-agg").value;278 279 document.getElementById('preview-chart-area').innerHTML = "";280 await renderPlotlyChart(type, x, y, agg, 'preview-chart-area', false);281}282 283async function saveToBoard() {284 const board = document.getElementById('presentation-board');285 if (board.children.length >= 6) {286 alert("Gestalt Principles Alert: A presentation slide should contain a maximum of 6 elements to maintain cognitive clarity and prevent overcrowding.");287 return;288 }289 290 const type = document.getElementById("chart-type").value; 291 const x = document.getElementById("x-axis").value; 292 const y = document.getElementById("y-axis").value;293 const agg = document.getElementById("y-agg").value;294 295 appState.savedCharts.push({type, x, y, agg});296 297 chartCounter++;298 const targetDivId = `board-chart-${chartCounter}`;299 300 const div = document.createElement('div');301 div.className = "dash-card-slide";302 // Initialize default absolute size and position303 div.style.top = "20px"; div.style.left = "20px"; div.style.width = "400px"; div.style.height = "300px";304 div.ondblclick = () => zoomChart(type, x, y, agg);305 306 div.innerHTML = `307 <div class="slide-hover-menu">308 <span><i class="fa-solid fa-arrows-up-down-left-right"></i> Drag</span>309 <button onclick="this.parentElement.parentElement.remove()" class="close-btn"><i class="fa-solid fa-xmark"></i></button>310 </div>311 <div class="chart-wrapper"><div id="${targetDivId}" style="height:100%; width:100%;"></div></div>312 <div class="resize-handle"><i class="fa-solid fa-caret-down" style="transform: rotate(-45deg);"></i></div>313 `;314 board.appendChild(div);315 316 makeAbsoluteDraggableAndResizable(div);317 chartResizeObserver.observe(div); 318 319 switchVizMode('canvas');320 321 requestAnimationFrame(async () => {322 await renderPlotlyChart(type, x, y, agg, targetDivId, false, null, true);323 });324}325 326async function generateKPI() {327 const board = document.getElementById('presentation-board');328 if (board.children.length >= 6) { alert("Slide full (Max 6 elements)."); return; }329 330 const x = document.getElementById("x-axis").value; 331 const y = document.getElementById("y-axis").value;332 const agg = document.getElementById("y-agg").value;333 334 chartCounter++;335 336 const div = document.createElement('div');337 div.className = "dash-card-slide";338 div.style.top = "20px"; div.style.left = "20px"; div.style.width = "250px"; div.style.height = "150px";339 340 div.innerHTML = `341 <div class="slide-hover-menu">342 <span><i class="fa-solid fa-arrows-up-down-left-right"></i> Drag</span>343 <button onclick="this.parentElement.parentElement.remove()" class="close-btn"><i class="fa-solid fa-xmark"></i></button>344 </div>345 <div style="display:flex; flex-direction:column; justify-content:center; align-items:center; height:100%; padding:20px; pointer-events:none;">346 <div style="font-size: 12px; color: var(--text-dim); text-transform: uppercase;">${y} (${agg})</div>347 <div id="kpi-val-${chartCounter}" style="font-size: 40px; font-weight: 700; color: var(--accent);">...</div>348 </div>349 <div class="resize-handle"><i class="fa-solid fa-caret-down" style="transform: rotate(-45deg);"></i></div>350 `;351 board.appendChild(div);352 makeAbsoluteDraggableAndResizable(div);353 chartResizeObserver.observe(div);354 switchVizMode('canvas');355 356 const payload = { type: 'bar', x, y, agg }; 357 const res = await fetch(`${API_URL}/visualize/chart`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); 358 const d = await res.json(); 359 if(d.chart_data && d.chart_data.y.length > 0) {360 let total = d.chart_data.y.reduce((a,b)=>a+b,0);361 document.getElementById(`kpi-val-${chartCounter}`).innerText = total > 1000 ? (total/1000).toFixed(1) + 'k' : total.toFixed(0);362 }363}364 365async function zoomChart(type, x, y, agg) {366 document.getElementById('chart-zoom-modal').classList.remove('hidden');367 document.getElementById('zoomed-chart-area').innerHTML = "";368 await renderPlotlyChart(type, x, y, agg, 'zoomed-chart-area', false);369 setTimeout(() => { Plotly.Plots.resize(document.getElementById('zoomed-chart-area')); }, 100);370}371 372// Master Plotly Mapping Engine373async function renderPlotlyChart(type, x, y, agg, targetId, isPrintLayout=false, kpiId=null, isSlideMode=false) {374 const currentTheme = document.getElementById("chart-theme").value || 'corporate';375 const activePalette = colorPalettes[currentTheme];376 const isLight = document.body.getAttribute('data-theme') === 'light' || isPrintLayout;377 378 const fontColor = isLight ? '#0f172a' : '#cbd5e1';379 const gridColor = isLight ? '#e2e8f0' : '#334155';380 const bgColor = isSlideMode ? 'rgba(0,0,0,0)' : (isLight ? '#ffffff' : 'rgba(0,0,0,0)'); 381 382 const payload = { type, x, y, agg }; 383 const res = await fetch(`${API_URL}/visualize/chart`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); 384 const d = await res.json(); 385 386 if (d.chart_data && document.getElementById(targetId)) { 387 let trace = { x: d.chart_data.x, y: d.chart_data.y, marker: { color: activePalette[0] } }; 388 let layoutParams = {};389 390 if (type.includes('pie') || type.includes('donut')) { trace.labels = d.chart_data.x; trace.values = d.chart_data.y; trace.type = 'pie'; trace.marker = {colors: activePalette}; if(type==='donut') trace.hole = 0.6;391 } else if (type === 'line' || type === 'timeline') { trace.type = 'scatter'; trace.mode = 'lines+markers'; trace.line = { shape: 'spline', color: activePalette[1] };392 } else if (type === 'line-log') { trace.type = 'scatter'; trace.mode = 'lines'; layoutParams.yaxis = { type: 'log', gridcolor: gridColor };393 } else if (type === 'scatter') { trace.type = 'scatter'; trace.mode = 'markers'; trace.marker = { size: 10, color: activePalette[2] };394 } else if (type === 'scatter-connect') { trace.type = 'scatter'; trace.mode = 'lines+markers'; trace.line = { color: activePalette[0] };395 } else if (type === 'bubble' || type === 'circle-pack') { trace.type = 'scatter'; trace.mode = 'markers'; trace.marker = { size: d.chart_data.y, sizeref: Math.max(...d.chart_data.y)/30, sizemode: 'area', color: activePalette[3] };396 } else if (type === 'area') { trace.type = 'scatter'; trace.mode = 'lines'; trace.fill = 'tozeroy'; trace.line = { color: activePalette[0] };397 } else if (type === 'area-stacked') { trace.type = 'scatter'; trace.mode = 'lines'; trace.fill = 'tonexty'; trace.stackgroup = 'one';398 } else if (type === 'bar-stacked') { trace.type = 'bar'; layoutParams.barmode = 'stack';399 } else if (type === 'box') { trace.type = 'box'; trace.name = y; trace.marker = { color: activePalette[4] };400 } else if (type === 'violin' || type === 'density') { trace.type = 'violin'; trace.y = d.chart_data.y; trace.x = d.chart_data.x; trace.box = {visible: true}; trace.line = {color: activePalette[0]};401 } else if (type === 'waterfall') { trace.type = 'waterfall'; trace.measure = d.chart_data.x.map((_, i) => i === d.chart_data.x.length -1 ? 'total' : 'relative');402 } else if (type === 'funnel') { trace.type = 'funnel'; trace.y = d.chart_data.x; trace.x = d.chart_data.y; trace.marker = {color: activePalette};403 } else if (type === 'radar') { trace.type = 'scatterpolar'; trace.r = d.chart_data.y; trace.theta = d.chart_data.x; trace.fill = 'toself'; layoutParams.polar = { radialaxis: { visible: true, gridcolor: gridColor }, angularaxis: { gridcolor: gridColor }, bgcolor: 'transparent' };404 } else if (type === 'heatmap') { trace.type = 'heatmap'; trace.z = [d.chart_data.y]; trace.x = d.chart_data.x; trace.colorscale = 'Blues';405 } else if (type === 'treemap') { trace.type = 'treemap'; trace.labels = d.chart_data.x; trace.parents = Array(d.chart_data.x.length).fill('Dataset'); trace.values = d.chart_data.y; trace.textinfo = "label+value";406 } else if (type === 'sunburst') { trace.type = 'sunburst'; trace.labels = d.chart_data.x; trace.parents = Array(d.chart_data.x.length).fill('Root'); trace.values = d.chart_data.y;407 } else if (type === 'radial-bar') { trace.type = 'barpolar'; trace.r = d.chart_data.y; trace.theta = d.chart_data.x; trace.marker = { color: activePalette }; layoutParams.polar = { bgcolor: 'transparent' };408 } else if (type === 'candlestick' || type === 'ohlc') { trace.type = type; trace.x = d.chart_data.x; trace.close = d.chart_data.y; trace.open = d.chart_data.y.map(v=>v*0.9); trace.high = d.chart_data.y.map(v=>v*1.1); trace.low = d.chart_data.y.map(v=>v*0.8);409 } else if (type === 'sankey') { trace.type = 'sankey'; trace.node = {label: d.chart_data.x}; trace.link = {source: Array.from(d.chart_data.x.keys()), target: Array.from(d.chart_data.x.keys()).map(i=>(i+1)%d.chart_data.x.length), value: d.chart_data.y};410 } else { trace.type = 'bar'; trace.marker = {color: activePalette}; }411 412 const layout = { 413 title: { text: `${y} by ${x}`, font: { size: 14, color: fontColor } }, 414 paper_bgcolor: bgColor, plot_bgcolor: bgColor, font: { color: fontColor }, 415 xaxis: { gridcolor: gridColor, zerolinecolor: gridColor }, yaxis: { gridcolor: gridColor, zerolinecolor: gridColor },416 ...layoutParams, margin: { t: 40, l: 40, r: 20, b: 40 }, autosize: true417 }; 418 Plotly.newPlot(targetId, [trace], layout, {responsive: true, displayModeBar: false}); 419 }420}421 422// Full Slide Export Engine423async function exportSlide(format) {424 const slide = document.getElementById('presentation-board');425 if (slide.children.length === 0) return alert("The slide is empty. Please add charts first.");426 427 // Hide UI controls temporarily428 document.querySelectorAll('.slide-hover-menu').forEach(el => el.style.display = 'none');429 document.querySelectorAll('.resize-handle').forEach(el => el.style.display = 'none');430 431 // Set explicit theme background for export based on active theme432 const isLight = document.body.getAttribute('data-theme') === 'light';433 const bgCol = isLight ? '#ffffff' : '#1e293b';434 435 try {436 const canvas = await html2canvas(slide, { scale: 2, backgroundColor: bgCol, useCORS: true }); 437 const imgData = canvas.toDataURL('image/png');438 439 if (format === 'png' || format === 'jpg') {440 const link = document.createElement('a');441 link.download = `Enterprise_Slide.${format}`;442 link.href = format === 'jpg' ? canvas.toDataURL('image/jpeg', 0.9) : imgData;443 link.click();444 } else if (format === 'pdf') {445 const pdf = new jspdf.jsPDF({ orientation: 'landscape', unit: 'px', format: [canvas.width, canvas.height] });446 pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);447 pdf.save('Enterprise_Slide.pdf');448 }449 } catch(e) {450 console.error(e);451 alert("Export failed. Please check browser permissions.");452 } finally {453 // Restore controls454 document.querySelectorAll('.slide-hover-menu').forEach(el => el.style.display = '');455 document.querySelectorAll('.resize-handle').forEach(el => el.style.display = '');456 }457}458 459// --- PUBLISHER (DOCS/PPT) ---460function formatDoc(cmd, value=null) { document.execCommand(cmd, false, value); document.getElementById('report-pages-container').focus(); }461function insertTable() {462 const html = `<table style="width:100%; border-collapse:collapse; margin: 15px 0;">463 <tr><th style="border:1px solid #cbd5e1; padding:8px;">Header 1</th><th style="border:1px solid #cbd5e1; padding:8px;">Header 2</th></tr>464 <tr><td style="border:1px solid #cbd5e1; padding:8px;">Data</td><td style="border:1px solid #cbd5e1; padding:8px;">Data</td></tr>465 </table><p>​</p>`;466 document.execCommand('insertHTML', false, html);467}468function insertMedia(type) {469 if(type === 'image') {470 const url = prompt("Enter Image URL:");471 if(url) document.execCommand('insertHTML', false, `<img src="${url}" style="max-width:100%; border-radius:8px; margin: 10px 0;" />`);472 }473}474 475async function insertSavedChart() {476 if (appState.savedCharts.length === 0) return alert("No charts saved! Go to BI Studio and click 'Save to Slide'.");477 chartCounter++;478 const targetId = `report-saved-chart-${chartCounter}`;479 480 const htmlStr = `<br><div class="report-chart-wrapper" contenteditable="false" style="resize: both; overflow: hidden; width: 600px; height: 350px; max-width: 100%; margin: 10px auto; border: 1px dashed #cbd5e1; border-radius: 8px; position: relative;">481 <div id="${targetId}" style="width: 100%; height: 100%;"></div>482 </div><br><p>​</p>`;483 484 document.execCommand('insertHTML', false, htmlStr);485 const config = appState.savedCharts[appState.savedCharts.length - 1];486 487 setTimeout(async () => { 488 await renderPlotlyChart(config.type, config.x, config.y, config.agg, targetId, true); 489 const wrapper = document.getElementById(targetId).parentElement;490 chartResizeObserver.observe(wrapper);491 }, 100);492}493 494function adjustPageLayout() {495 const layout = document.getElementById("page-layout-size").value;496 document.querySelectorAll('.page-canvas').forEach(page => { page.className = `page-canvas ${layout}`; });497}498 499function addBlankPage() {500 const container = document.getElementById('report-pages-container');501 const layout = document.getElementById("page-layout-size").value;502 const pageCount = container.children.length + 1;503 const newPage = document.createElement('div');504 newPage.className = `page-canvas ${layout}`;505 newPage.contentEditable = true; newPage.spellcheck = false; newPage.id = `page-${pageCount}`;506 newPage.innerHTML = `<h1>Page ${pageCount}</h1><p>Start typing here...</p>`;507 container.appendChild(newPage); 508 newPage.focus();509 return newPage;510}511 512document.getElementById('report-pages-container').addEventListener('input', function(e) {513 const page = e.target.closest('.page-canvas');514 if(page && page.scrollHeight > page.clientHeight + 2 && !page.classList.contains('ppt-slide')) {515 addBlankPage();516 }517});518 519async function publisherAI(action) {520 const selection = window.getSelection().toString();521 if (!selection) return alert("Highlight text first!");522 document.getElementById("ai-input-container").classList.add("ai-processing");523 try {524 const res = await fetch(`${API_URL}/publisher-ai`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: action, text: selection }) });525 const data = await res.json();526 document.execCommand('insertHTML', false, `<span>${data.result}</span>`);527 document.getElementById("ai-input-container").classList.remove("ai-processing");528 } catch(e) { document.getElementById("ai-input-container").classList.remove("ai-processing"); alert("AI Formatting failed."); }529}530 531async function generateAIReport() {532 const container = document.getElementById('report-pages-container');533 const activePage = container.lastElementChild;534 activePage.innerHTML = `<h1>Automated Report</h1><p><i>Generating AI narrative... Please wait.</i></p><br>`;535 try {536 const payload = { report_type: "Strategic Business Summary", layout_style: document.getElementById('page-layout-size').value };537 const res = await fetch(`${API_URL}/generate-report`, { method: 'POST', headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });538 const data = await res.json();539 activePage.innerHTML = data.narrative;540 } catch (e) { activePage.innerHTML = '<span style="color:red;">Error connecting to AI.</span>'; }541}542 543async function handleUpdate(cell, r, col) { 544 appState.gridData[r][col] = cell.innerText.trim(); 545 await syncDataToBackend(); 546 if(appState.currentView === 'grid') {547 const xSelect = document.getElementById("x-axis"); const ySelect = document.getElementById("y-axis"); 548 xSelect.innerHTML = ""; ySelect.innerHTML = ""; 549 appState.columns.forEach(c => { xSelect.appendChild(new Option(c, c)); ySelect.appendChild(new Option(c, c)); });550 }551}552 553function createCell(p, c, h, click) { const d=document.createElement("div"); d.className=c; d.innerHTML=h; if(click) d.onclick=click; p.appendChild(d); }554function evaluateFormula(cell) { let val = cell.innerText.trim(); if (val.startsWith("=")) { cell.dataset.formula = val; try { const formula = val.substring(1).toUpperCase(); let parsed = "0"; if (!val.toUpperCase().includes("SUM")) { parsed = formula; appState.columns.forEach((col) => { appState.gridData.forEach((row, rIdx) => { const ref = `${col}${rIdx+1}`; if (parsed.includes(ref)) { parsed = parsed.replace(ref, parseFloat(row[col]) || 0); } }); }); } try { cell.innerText = eval(parsed); cell.classList.add("has-formula"); } catch(e) { cell.innerText = "#ERR:CALC"; } } catch(e) { cell.innerText = "#REF!"; } } }555function handleKey(e, r, c, cell) { if (e.key === "Enter") { e.preventDefault(); cell.blur(); const next = document.querySelector(`.cell[data-r='${r+1}'][data-c='${c}']`); if (next) next.focus(); } }556function startResize(e, i) { e.stopPropagation(); appState.isResizing = true; appState.resizeCol = i; appState.startX = e.pageX; appState.startWidth = appState.colWidths[i]; document.body.style.cursor = "col-resize"; document.addEventListener("mousemove", onResize); document.addEventListener("mouseup", endResize); }557function onResize(e) { if (!appState.isResizing) return; appState.colWidths[appState.resizeCol] = Math.max(60, appState.startWidth + (e.pageX - appState.startX)); initGrid(appState.gridData, appState.columns); }558function endResize() { appState.isResizing = false; document.body.style.cursor = "default"; document.removeEventListener("mousemove", onResize); document.removeEventListener("mouseup", endResize); }559function renderTabs() { const b = document.getElementById("sheet-tabs"); b.innerHTML = ""; appState.sheets.forEach(s => { const t = document.createElement("div"); t.className = `sheet-tab ${s === appState.activeSheet ? 'active' : ''}`; const sp = document.createElement("span"); sp.innerText = s; sp.onclick = () => switchSheet(s); const cl = document.createElement("span"); cl.className = "close-tab"; cl.innerHTML = '×'; cl.onclick = (e) => { e.stopPropagation(); const fd = new FormData(); fd.append("name", s); fetch(`${API_URL}/sheet/close`, {method:"POST", body:fd}).then(fetchState); }; t.appendChild(sp); t.appendChild(cl); b.appendChild(t); }); }560async function switchSheet(n) { const fd=new FormData(); fd.append("name",n); const res=await fetch(`${API_URL}/sheet/switch`,{method:"POST",body:fd}); const d=await res.json(); appState.activeSheet=d.active; renderTabs(); initGrid(d.data,d.columns); updateBusinessStats();}561function selectAll() { appState.selection = { start: {r: 0, c: 0}, end: {r: Math.min(appState.gridData.length - 1, 99), c: appState.columns.length - 1} }; renderSelection(); }562function selectColumn(colIndex, e) { if(e.target.classList.contains('resizer')) return; appState.selection = { start: {r: 0, c: colIndex}, end: {r: Math.min(appState.gridData.length - 1, 99), c: colIndex} }; renderSelection(); }563function selectRow(r) { appState.selection = { start: {r: r, c: 0}, end: {r: r, c: appState.columns.length - 1} }; renderSelection(); }564function startSelect(e,r,c){ appState.selection={start:{r,c},end:{r,c}}; renderSelection(); }565function updateSelect(e,r,c){ if(e.buttons===1){ appState.selection.end={r,c}; renderSelection(); } }566function renderSelection(){ document.querySelectorAll(".selected").forEach(e=>e.classList.remove("selected")); const {start,end}=appState.selection; if(!start)return; const rMin=Math.min(start.r,end.r), rMax=Math.max(start.r,end.r), cMin=Math.min(start.c,end.c), cMax=Math.max(start.c,end.c); for(let r=rMin;r<=rMax;r++) for(let c=cMin;c<=cMax;c++) { const el=document.querySelector(`.cell[data-r='${r}'][data-c='${c}']`); if(el) el.classList.add("selected"); } calculateStats(); }567function calculateStats(){ const {start,end}=appState.selection; if(!start)return; let sum=0, count=0; const rMin=Math.min(start.r,end.r), rMax=Math.max(start.r,end.r), cMin=Math.min(start.c,end.c), cMax=Math.max(start.c,end.c); for(let r=rMin;r<=rMax;r++) for(let c=cMin;c<=cMax;c++) { const val=appState.gridData[r]?.[appState.columns[c]]; if(val && !isNaN(parseFloat(val))){ sum+=parseFloat(val); count++; } } if (count > 0) document.getElementById("sel-range").innerText = `SUM: ${sum.toFixed(2)} | COUNT: ${count}`; else document.getElementById("sel-range").innerText = "Ready"; }568 569function toggleView(v) { 570 appState.currentView = v;571 document.querySelectorAll('.view-section').forEach(el => el.classList.add('hidden')); 572 document.getElementById(`view-${v}`).classList.remove('hidden'); 573 document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); 574 document.getElementById(`btn-nav-${v}`).classList.add('active'); 575 576 document.getElementById('main-sidebar').classList.remove('mobile-open');577 578 if(v==='visual') {579 const xSelect = document.getElementById("x-axis"); const ySelect = document.getElementById("y-axis"); 580 xSelect.innerHTML = ""; ySelect.innerHTML = ""; 581 appState.columns.forEach(c => { xSelect.appendChild(new Option(c, c)); ySelect.appendChild(new Option(c, c)); });582 setTimeout(() => document.querySelectorAll('.js-plotly-plot').forEach(p => Plotly.Plots.resize(p)), 100);583 }584}585 586async function sendToAI(isSilent=false, commandText="") {587 const input = document.getElementById("ai-input"); const v = commandText || input.value.trim(); if(!v) return;588 if(!isSilent) { addChat("user", v); input.value = ""; }589 document.getElementById("ai-input-container").classList.add("ai-processing");590 try {591 const payload = { message: v, context: appState.currentView };592 const res = await fetch(`${API_URL}/chat`, { method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify(payload) });593 const d = await res.json(); document.getElementById("ai-input-container").classList.remove("ai-processing");594 if (d.response) {595 let responseText = d.response; let thinkingHTML = ""; 596 const thinkMatch = responseText.match(/THINKING:([\s\S]*?)FINAL ANSWER:([\s\S]*)/);597 if (thinkMatch) {598 thinkingHTML = `<details class="thinking-box"><summary>▶ Telemetry Engine</summary><div class="thinking-content">${thinkMatch[1].trim().replace(/\n/g, '<br>')}</div></details>`;599 responseText = thinkMatch[2].trim();600 }601 602 if (responseText.includes("<<GRID_MERGE:")) {603 const match = responseText.match(/<<GRID_MERGE:([\s\S]*?)>>/);604 if(match && match[1]) {605 const newData = JSON.parse(match[1]);606 const keys = Object.keys(newData[0]);607 keys.forEach(k => { if(!appState.columns.includes(k)) appState.columns.push(k); });608 appState.gridData.forEach((row, i) => { if(newData[i]) Object.assign(row, newData[i]); });609 610 await syncDataToBackend(); 611 initGrid(appState.gridData, appState.columns);612 613 if (appState.currentView === 'visual') {614 const xSelect = document.getElementById("x-axis"); const ySelect = document.getElementById("y-axis");615 xSelect.innerHTML = ""; ySelect.innerHTML = "";616 appState.columns.forEach(c => { xSelect.appendChild(new Option(c, c)); ySelect.appendChild(new Option(c, c)); });617 }618 addChat("ai", `${thinkingHTML}✅ I have merged the new data directly into your active table. You can visualize it immediately.`);619 return;620 }621 }622 623 if (responseText.includes("<<CHART_ACTION:")) {624 const match = responseText.match(/<<CHART_ACTION:(.*?)>>/);625 if(match && match[1]) {626 const config = JSON.parse(match[1]); toggleView('visual'); switchVizMode('preview');627 addChat("ai", `${thinkingHTML}📊 Rendering preview requested.`);628 document.getElementById("chart-type").value = config.type || "bar";629 document.getElementById("x-axis").value = config.x; document.getElementById("y-axis").value = config.y;630 previewChart();631 return;632 }633 } 634 635 addChat("ai", thinkingHTML + responseText);636 }637 } catch(e) { document.getElementById("ai-input-container").classList.remove("ai-processing"); addChat("ai", "⚠️ Connection timeout."); }638}639 640function addChat(role, htmlContent) {641 const box = document.getElementById("chat-box");642 const msg = document.createElement("div"); msg.className = `message ${role === 'ai' || role === 'system' ? 'system-message' : 'user-message'}`;643 const bubble = document.createElement("div"); bubble.className = "bubble";644 if(role === 'user') bubble.innerText = htmlContent;645 else bubble.innerHTML = htmlContent.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br>');646 msg.appendChild(bubble); box.appendChild(msg); box.scrollTop = box.scrollHeight;647}648 649function setupGlobalEvents() {650 document.getElementById("btn-add-sheet").onclick = () => { const fd=new FormData(); fd.append("name",`Worksheet ${appState.sheets.length+1}`); fetch(`${API_URL}/sheet/add`,{method:"POST",body:fd}).then(fetchState); };651 document.getElementById("up").onchange = (e) => { 652 document.getElementById("btn-clean").innerHTML = "<i class='fa-solid fa-spinner fa-spin'></i> Uploading...";653 const fd=new FormData(); fd.append("file",e.target.files[0]); fd.append("mode","replace"); 654 fetch(`${API_URL}/upload`,{method:"POST",body:fd}).then(fetchState).then(()=> { document.getElementById("btn-clean").innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i> AI Deep Clean'; }); 655 };656 document.getElementById("btn-clean").onclick = async () => { document.getElementById("btn-clean").innerHTML = "<i class='fa-solid fa-spinner fa-spin'></i> Normalizing..."; const res=await fetch(`${API_URL}/cleanup`,{method:"POST"}); const d=await res.json(); if(d.status) initGrid(appState.gridData, appState.columns); fetchState(); document.getElementById("btn-clean").innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i> AI Deep Clean'; };657 document.getElementById("send-btn").onclick = () => sendToAI();658 document.getElementById("ai-input").onkeydown = (e) => { if(e.key==="Enter"&&!e.shiftKey){ e.preventDefault(); sendToAI(); }};659 document.getElementById("grid-formula-bar").oninput = (e) => {660 if(!appState.activeCellCoord) return;661 const targetCell = document.querySelector(`.cell[data-r='${appState.activeCellCoord.r}'][data-c='${appState.activeCellCoord.c}']`);662 if(targetCell) { targetCell.innerText = e.target.value; appState.gridData[appState.activeCellCoord.r][appState.activeCellCoord.col] = e.target.value; syncDataToBackend(); }663 };664}