CoolFace
Apppublic

khwajai/spreadsheet

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
script.js664 linesDownload Raw Back to root
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>&#8203;</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>&#8203;</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}