AgentOp
Agent Template

Data Analysis Agent Template

The Data Analysis Agent Template turns any CSV file into an interactive AI-powered dashboard that runs entirely in your browser. Powered by Pyodide (Python compiled to WebAssembly), it loads pandas and numpy locally — no data ever leaves your machine. Upload your CSV by dragging and dropping it onto the interface. The agent parses it instantly and displays a preview table of the first 10 rows. From there, ask questions in plain English: "show me a summary of the data", "plot a histogram of the sales column", "what are the correlations between numeric variables?", or "show value counts for the region column." Under the hood, the agent uses GPT-4o, Claude 3.5 Sonnet, or a fully local WebLLM model (your choice) to interpret your query and call the correct Python analysis function. It can generate pandas .describe() statistics, detect missing values per column, compute a full Pearson correlation matrix with highlighted strong correlations, and create matplotlib bar charts or histograms rendered as inline images — all without sending your data to any external server. This template is ideal for data analysts, researchers, and developers who need quick exploratory data analysis (EDA) on sensitive datasets, offline environments, or anywhere a traditional server-side tool is not appropriate. The exported HTML file is fully self-contained — open it in any modern browser (Chrome 113+, Edge 113+) with WebGPU support and it works immediately.

data-analysis pandas visualization csv statistics eda exploratory-data-analysis matplotlib numpy browser-python pyodide no-server offline
ozzo Oct 27, 2025 16 uses

Preview Mode

This is a preview with sample data. The template uses placeholders like which will be replaced with actual agent data.

About This Template

Data Analysis Agent Template is a browser-executable AI agent template built on AgentOp. It runs entirely in the browser using Python (via Pyodide) and can be deployed without a server — just download the generated HTML file and open it locally or host it anywhere.

Topics data-analysis pandas visualization csv statistics eda exploratory-data-analysis matplotlib numpy browser-python pyodide no-server offline
Template Preview

Template Metadata

Slug
data-analysis-agent-template
Created By
ozzo
Created
Oct 27, 2025
Usage Count
16

Tags

data-analysis pandas visualization csv statistics eda exploratory-data-analysis matplotlib numpy browser-python pyodide no-server offline

Code Statistics

HTML Lines
140
CSS Lines
481
JS Lines
322
Python Lines
199

Source Code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>{{ agent_name }} — AI-Powered Data Analysis</title>
  
  <style>
    {{ css_code }}
  </style>

  <!-- Conditional Script Imports -->
  {% if needs_pyodide %}
  <script src="https://cdn.jsdelivr.net/pyodide/v{{ pyodide_version }}/full/pyodide.js"></script>
  {% endif %}
  
  <script>
    const PROVIDER = "{{ embedded_provider }}";
    const API_KEY = "{{ embedded_api_key }}";
    const AGENT_CONFIG = {{ default_config|safe }};
    const NEEDS_PYODIDE = {{ needs_pyodide|lower }};
    const PYODIDE_VERSION = "{{ pyodide_version }}";
  </script>
</head>
<body>
  <div class="dashboard">
    <div class="sidebar">
      <div class="agent-header">
        <div class="agent-icon">📊</div>
        <div>
          <h2 style="margin: 0; font-size: 1.1rem;">Data Analysis Agent</h2>
          <p style="margin: 0; color: #64748b; font-size: 0.875rem;">Data Analysis</p>
        </div>
      </div>
      
      <h3>Data Upload</h3>
      <div class="input-section" style="margin-bottom: 2rem;">
        <div class="upload-area" id="file-upload-area" 
             ondragenter="handleDragOver(event)"
             ondragover="handleDragOver(event)" 
             ondragleave="handleDragLeave(event)" 
             ondrop="handleDrop(event)"
             onclick="document.getElementById('csv-file-input').click()">
          <div style="color: var(--text-muted);">
            <div style="font-size: 2rem; margin-bottom: 0.5rem;">📁</div>
            <p style="margin: 0; font-weight: 500;">Drop CSV file here</p>
            <p style="margin: 0.25rem 0 0 0; font-size: 0.75rem;">or click to browse</p>
          </div>
          <input type="file" id="csv-file-input" accept=".csv,.txt" style="display: none;" onchange="handleFileSelect(event)">
        </div>
        
        <div id="file-info" class="file-info" style="display: none;">
          <div style="display: flex; justify-content: space-between; align-items: center;">
            <div>
              <div style="font-weight: 500;" id="fileName">data.csv</div>
              <div style="font-size: 0.75rem; color: #6b7280;" id="fileSize">1.2 KB</div>
            </div>
            <button onclick="clearFile()" class="btn" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;">Clear</button>
          </div>
        </div>
      </div>

      <!-- Data Preview Section -->
      <div id="data-preview" style="display: none; margin-top: 1.5rem;">
        <h3>Data Preview</h3>
        <div id="preview-content" style="
            border: 1px solid var(--surface-border);
            border-radius: 8px;
            padding: 1rem;
            background: var(--surface-bg);
            overflow-x: auto;
            max-height: 300px;
            overflow-y: auto;
        ">
          <div id="preview-table"></div>
        </div>
      </div>

      <h3>Analysis Query</h3>
      <div class="input-section">
        <textarea id="queryInput" 
                 class="query-input"
                 placeholder="Examples:
• Plot a histogram of sales values
• Show correlation between variables
• What are the summary statistics?
• Create a chart for the region column"
          rows="4">
        </textarea>
        
        <button class="analyze-button" id="analyzeBtn" onclick="processQuery()">
          <span>🔍</span>
          <span>Analyze Data</span>
        </button>
      </div>
      
      <div class="sample-queries">
        <h4 style="margin-bottom: 0.75rem; font-size: 0.9rem; color: var(--text-heading);">Sample Queries</h4>
        <div class="sample-query" onclick="fillSampleQuery('Show me a summary of the data')">📊 Show me a summary of the data</div>
        <div class="sample-query" onclick="fillSampleQuery('Create a histogram of sales')">📈 Create a histogram of sales</div>
        <div class="sample-query" onclick="fillSampleQuery('What are the correlations between numeric columns?')">📋 What are the correlations between numeric columns?</div>
        <div class="sample-query" onclick="fillSampleQuery('Show value counts for the region column')">🔗 Show value counts for the region column</div>
      </div>
    </div>
    
    <div class="main-content">
      <div class="agent-header">
        <h1 style="margin: 0; font-size: 1.5rem; color: var(--text-heading);">Data Analysis Results</h1>
        <div style="margin-left: auto; font-size: 0.875rem; color: #64748b;">Powered by AI</div>
      </div>
      
      <div class="result-area">
        <div id="results-container" class="result-content">
          <div style="text-align: center; color: var(--text-muted); padding: 3rem 0;">
            <div style="font-size: 3rem; margin-bottom: 1rem;">📊</div>
            <h3 style="margin: 0 0 0.5rem 0;">Ready for Analysis</h3>
            <p style="margin: 0;">Upload a CSV file and ask questions about your data</p>
          </div>
        </div>
        
        <div class="loading-indicator" id="loading-indicator">
          <div class="spinner"></div>
          <p>Analyzing your data...</p>
        </div>
      </div>
    </div>
  </div>

<script>
{{ js_code|safe }}
</script>

<!-- Hidden Python code -->
<script type="text/python" id="python-code">
{{ python_code|safe }}
</script>

</body>
</html>
/* ═══════════════════════════════════════════
   AgentOp Design System — Contract Explainer
   Base: data-analysis-agent (known working)
   Additions: Contract Explainer only classes
═══════════════════════════════════════════ */

:root {
  --primary: #059669;
  --primary-hover: #047857;
  --secondary: #0ea5e9;
  --surface-bg: #f8fafc;
  --surface-border: #e2e8f0;
  --text-heading: #1e293b;
  --text-body: #475569;
  --text-muted: #64748b;
  --radius-lg: 12px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, #f0f9ff 0%, #f8fafc 100%);
  color: var(--text-body);
  line-height: 1.6;
}

/* ── Layout ── */
.dashboard {
  max-width: 1400px;
  margin: 0 auto;
  padding: 1.5rem;
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 1.5rem;
  min-height: 100vh;
}

.sidebar {
  background: white;
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  height: fit-content;
  border: 1px solid var(--surface-border);
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

.main-content {
  background: white;
  border-radius: var(--radius-lg);
  padding: 2rem;
  border: 1px solid var(--surface-border);
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  position: relative;
}

/* ── Agent header ── */
.agent-header {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 2rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--surface-border);
}

.agent-icon {
  width: 48px;
  height: 48px;
  background: linear-gradient(135deg, var(--primary), var(--secondary));
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
  font-size: 1.25rem;
}

.sidebar h3 {
  margin: 0 0 1rem 0;
  color: var(--text-heading);
  font-size: 1.1rem;
}

/* ── Upload area (kept from base — used by other templates) ── */
.upload-area {
  border: 2px dashed var(--surface-border);
  border-radius: 8px;
  padding: 2rem;
  text-align: center;
  cursor: pointer;
  transition: all 0.3s;
  margin-bottom: 1rem;
}

.upload-area:hover, .upload-area.drag-over {
  border-color: var(--primary);
  background: rgba(5, 150, 105, 0.05);
}

.file-info {
  display: none;
  padding: 0.75rem;
  background: #f0f9ff;
  border: 1px solid #bfdbfe;
  border-radius: 6px;
  margin-bottom: 0.75rem;
}

/* ── Form elements ── */
.form-group { margin-bottom: 0.9rem; }

.form-label {
  display: block;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-heading);
  margin-bottom: 0.35rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.form-select,
.form-input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--surface-border);
  border-radius: 8px;
  font-family: inherit;
  font-size: 0.875rem;
  background: white;
  color: var(--text-body);
  outline: none;
  transition: border-color 0.2s, box-shadow 0.2s;
  appearance: none;
}

.form-select:focus,
.form-input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.1);
}

.query-input {
  width: 100%;
  min-height: 100px;
  max-height: 200px;
  padding: 0.75rem;
  border: 1px solid var(--surface-border);
  border-radius: 8px;
  font-family: inherit;
  font-size: 0.875rem;
  resize: vertical;
  margin-bottom: 0.75rem;
  outline: none;
  transition: border-color 0.2s;
  box-sizing: border-box;
  color: var(--text-body);
  line-height: 1.5;
}

.query-input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.1);
}

/* ── Buttons ── */
.analyze-button {
  width: 100%;
  background: var(--primary);
  color: white;
  padding: 0.75rem 1rem;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 500;
  transition: background-color 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: inherit;
}

.analyze-button:hover:not(:disabled) { background: var(--primary-hover); }
.analyze-button:disabled { opacity: 0.5; cursor: not-allowed; }

.btn {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 500;
  transition: background-color 0.2s;
  background: var(--primary);
  color: white;
  font-family: inherit;
}

.btn:hover { background: var(--primary-hover); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* ── Sample queries ── */
.sample-queries { margin-top: 1.5rem; }

.sample-query {
  background: #f8fafc;
  border: 1px solid var(--surface-border);
  border-radius: 6px;
  padding: 0.5rem 0.75rem;
  margin-bottom: 0.5rem;
  font-size: 0.875rem;
  cursor: pointer;
  transition: background-color 0.2s;
  color: var(--text-body);
}

.sample-query:hover { background: #e2e8f0; }

/* ── Result area ── */
.result-area {
  min-height: 500px;
  background: var(--surface-bg);
  border-radius: 8px;
  padding: 1.5rem;
  margin-top: 1rem;
  border: 1px solid var(--surface-border);
  position: relative;
}

.result-content { line-height: 1.6; }

.result-content h1, .result-content h2, .result-content h3 {
  color: var(--text-heading);
  margin-top: 1.5rem;
  margin-bottom: 0.75rem;
}

.result-content img {
  max-width: 100%;
  height: auto;
  margin: 1rem 0;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

/* ── Message bubbles ── */
.message {
  display: flex;
  gap: 1rem;
  margin-bottom: 1.5rem;
  padding: 1rem;
  border-radius: 8px;
  background: white;
  border: 1px solid var(--surface-border);
}

.message-user    { background: #eff6ff; border-color: #bfdbfe; }
.message-assistant { background: #f0fdf4; border-color: #bbf7d0; }
.message-error   { background: #fef2f2; border-color: #fecaca; }
.message-system,
.message-info    { background: #fefce8; border-color: #fde047; }

.message-icon    { font-size: 1.5rem; flex-shrink: 0; }
.message-content { flex: 1; overflow-wrap: break-word; }
.message-content p { margin: 0 0 0.6rem 0; }
.message-content p:last-child { margin-bottom: 0; }

.message-content h2,
.message-content h3 {
  color: var(--text-heading);
  margin: 1.1rem 0 0.4rem 0;
  font-size: 1rem;
}
.message-content h2:first-child,
.message-content h3:first-child { margin-top: 0; }

.message-content ul {
  margin: 0.4rem 0 0.6rem 0;
  padding-left: 1.4rem;
}
.message-content li { margin-bottom: 0.25rem; }

/* ── Loading indicator ── */
.loading-indicator {
  display: none;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  color: var(--text-muted);
}

.loading-indicator.show { display: block; }

.spinner {
  width: 40px;
  height: 40px;
  border: 3px solid var(--surface-border);
  border-top: 3px solid var(--primary);
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto 1rem;
}

@keyframes spin {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* ── Tables ── */
table {
  border-collapse: collapse;
  width: 100%;
  margin: 1rem 0;
  font-family: monospace;
  font-size: 0.9rem;
  background: white;
  border-radius: 8px;
  overflow: hidden;
}

th, td {
  border: 1px solid var(--surface-border);
  padding: 0.5rem;
  text-align: left;
}

th {
  background: var(--surface-bg);
  font-weight: 600;
  color: var(--text-heading);
}

tr:nth-child(even) { background: #f8fafc; }

/* ── Responsive ── */
@media (max-width: 768px) {
  .dashboard {
    grid-template-columns: 1fr;
    gap: 1rem;
    padding: 1rem;
  }
}


/* ════════════════════════════════════════════
   Contract Explainer — additions only
   These classes do NOT exist in the base CSS
════════════════════════════════════════════ */

/* ── Privacy badge ── */
.privacy-badge {
  background: linear-gradient(135deg, #f0fdf4, #dcfce7);
  border: 1px solid #bbf7d0;
  border-radius: 8px;
  padding: 0.5rem 0.75rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: #16a34a;
  text-align: center;
  margin-bottom: 1.25rem;
}

/* ── Char counter ── */
.char-counter {
  font-size: 0.72rem;
  color: var(--text-muted);
  text-align: right;
  margin-top: 0.25rem;
  margin-bottom: 0.75rem;
}

/* ── Clear button ── */
.btn-clear {
  background: transparent;
  border: 1px solid var(--surface-border);
  border-radius: 6px;
  padding: 0.4rem 0.8rem;
  font-size: 0.8rem;
  color: var(--text-muted);
  cursor: pointer;
  transition: all 0.15s;
  font-family: inherit;
}

.btn-clear:hover {
  border-color: #fca5a5;
  color: #dc2626;
  background: #fef2f2;
}

/* ── Pyodide / WebLLM status badges ── */
.status-badge {
  margin-top: 1rem;
  padding: 0.4rem 0.75rem;
  border-radius: 6px;
  font-size: 0.75rem;
  font-weight: 500;
  text-align: center;
}

.status-loading {
  background: #fffbeb;
  border: 1px solid #fde68a;
  color: #92400e;
}

.status-ready {
  background: #f0fdf4;
  border: 1px solid #bbf7d0;
  color: #16a34a;
}

.status-error {
  background: #fef2f2;
  border: 1px solid #fecaca;
  color: #dc2626;
}

/* ── Feature pills ── */
.feature-pill {
  background: var(--surface-bg);
  border: 1px solid var(--surface-border);
  border-radius: 20px;
  padding: 0.35rem 0.75rem;
  font-size: 0.78rem;
  color: var(--text-muted);
}

/* ── Risk badges ── */
.risk-high {
  display: inline-block;
  background: #fef2f2;
  color: #dc2626;
  border: 1px solid #fca5a5;
  border-radius: 4px;
  padding: 0.05rem 0.45rem;
  font-size: 0.78rem;
  font-weight: 700;
  vertical-align: middle;
}

.risk-med {
  display: inline-block;
  background: #fffbeb;
  color: #d97706;
  border: 1px solid #fcd34d;
  border-radius: 4px;
  padding: 0.05rem 0.45rem;
  font-size: 0.78rem;
  font-weight: 700;
  vertical-align: middle;
}

.risk-low {
  display: inline-block;
  background: #f0fdf4;
  color: #16a34a;
  border: 1px solid #86efac;
  border-radius: 4px;
  padding: 0.05rem 0.45rem;
  font-size: 0.78rem;
  font-weight: 700;
  vertical-align: middle;
}

/* ── Disclaimer bar ── */
.disclaimer-bar {
  margin-top: 1.25rem;
  padding: 0.75rem 1rem;
  background: #fffbeb;
  border: 1px solid #fde68a;
  border-radius: 8px;
  font-size: 0.8rem;
  color: #78350f;
  line-height: 1.5;
}
// File Upload Handlers
function handleDragOver(event) {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'copy';
    document.getElementById('file-upload-area').classList.add('drag-over');
}

function handleDragLeave(event) {
    event.preventDefault();
    document.getElementById('file-upload-area').classList.remove('drag-over');
}

function handleDrop(event) {
    event.preventDefault();
    document.getElementById('file-upload-area').classList.remove('drag-over');
    
    const files = event.dataTransfer.files;
    if (files.length > 0) {
        handleFile(files[0]);
    }
}

function handleFileSelect(event) {
    const file = event.target.files[0];
    if (file) {
        handleFile(file);
    }
}

async function handleFile(file) {
    const fileInfo = document.getElementById('file-info');
    const fileName = document.getElementById('fileName') || document.querySelector('#file-info .file-name');
    const fileSize = document.getElementById('fileSize') || document.querySelector('#file-info .file-size');
    const dataPreview = document.getElementById('data-preview');
    
    if (fileName) fileName.textContent = file.name;
    if (fileSize) fileSize.textContent = formatFileSize(file.size);
    if (fileInfo) fileInfo.style.display = 'block';
    
    // Wait for Pyodide to be ready
    let attempts = 0;
    const maxAttempts = 150; // 30 seconds (150 * 200ms)
    while (!window.pyodide && attempts < maxAttempts) {
        await new Promise(resolve => setTimeout(resolve, 200));
        attempts++;
    }
    
    if (!window.pyodide) {
        console.error('Pyodide not initialized after 30 seconds');
        return;
    }
    
    // Read and preview the file
    const reader = new FileReader();
    reader.onload = async function(e) {
        const csvContent = e.target.result;
        displayDataPreview(csvContent);
        if (dataPreview) dataPreview.style.display = 'block';
        
        // Load data into Python for analysis
        try {
            window.pyodide.globals.set('csv_content', csvContent);
            window.pyodide.globals.set('filename', file.name);
            const result = await window.pyodide.runPythonAsync('load_csv_data(csv_content, filename)');
            console.log('✅ CSV data loaded into Python:', result);
        } catch (error) {
            console.error('❌ Failed to load CSV into Python:', error);
        }
    };
    reader.readAsText(file);
}

function displayDataPreview(csvContent) {
    const lines = csvContent.split('\n').filter(line => line.trim());
    const previewTable = document.getElementById('preview-table');
    
    if (!previewTable || lines.length === 0) return;
    
    const maxRows = Math.min(lines.length, 10); // Show first 10 rows
    let tableHTML = '<table style="width: 100%; border-collapse: collapse; font-size: 0.875rem;">';
    
    for (let i = 0; i < maxRows; i++) {
        const cells = lines[i].split(',');
        const isHeader = i === 0;
        const tag = isHeader ? 'th' : 'td';
        const style = isHeader ? 
            'border: 1px solid var(--surface-border); padding: 0.5rem; background: var(--surface-bg); font-weight: 600;' :
            'border: 1px solid var(--surface-border); padding: 0.5rem;';
            
        tableHTML += '<tr>';
        cells.forEach(cell => {
            tableHTML += `<${tag} style="${style}">${cell.trim()}</${tag}>`;
        });
        tableHTML += '</tr>';
    }
    
    if (lines.length > 10) {
        tableHTML += '<tr><td colspan="100%" style="border: 1px solid var(--surface-border); padding: 0.5rem; text-align: center; font-style: italic; color: var(--text-muted);">... and ' + (lines.length - 10) + ' more rows</td></tr>';
    }
    
    tableHTML += '</table>';
    previewTable.innerHTML = tableHTML;
}

function formatFileSize(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

function clearFile() {
    const fileInput = document.getElementById('csv-file-input');
    const fileInfo = document.getElementById('file-info');
    const dataPreview = document.getElementById('data-preview');
    
    if (fileInput) fileInput.value = '';
    if (fileInfo) fileInfo.style.display = 'none';
    if (dataPreview) dataPreview.style.display = 'none';
}

// Global variables
let pyodide = null;
let current_api_key = '';

// Load dataset into Python
async function loadDataset(csvContent, filename) {
    if (!window.pyodide) {
        addMessage('error', 'Python environment not ready. Please wait...');
        return;
    }
    
    try {
        addMessage('system', `Loading ${filename}...`);
        
        // Call Python function to load data
        window.pyodide.globals.set('csv_content', csvContent);
        window.pyodide.globals.set('filename', filename);
        
        const result = await window.pyodide.runPythonAsync(`
            load_csv_data(csv_content, filename)
        `);
        
        addMessage('system', result);
        
        // Show quick info about the dataset
        const info = await window.pyodide.runPython(`
            f"Dataset shape: {current_data.shape if current_data is not None else 'No data'}"
        `);
        addMessage('info', info);
        
    } catch (error) {
        addMessage('error', `Error loading dataset: ${error.message}`);
    }
}

// Query processing
async function processQuery() {
    const input = document.getElementById('queryInput');
    const query = input.value.trim();
    
    if (!query) return;
    
    console.log('[QUERY START]', query, new Date().toLocaleTimeString());
    const startTime = Date.now();
    
    if (!window.pyodide) {
        addMessage('error', 'Python environment not ready. Please wait...');
        return;
    }
    
    // Clear input and add user message
    input.value = '';
    addMessage('user', query);
    
    // Show loading indicator and disable button
    const loadingEl = document.getElementById('loading-indicator');
    const analyzeBtn = document.getElementById('analyzeBtn');
    
    if (loadingEl) loadingEl.classList.add('show');
    if (analyzeBtn) {
        analyzeBtn.disabled = true;
        analyzeBtn.innerHTML = '<span>⏳</span><span>Processing...</span>';
    }
    
    try {
        console.log('[PYODIDE] Setting up query execution');
        
        // Set API key for this query
        if (window.API_KEY) {
            window.pyodide.globals.set('current_api_key', window.API_KEY);
            console.log('[PYODIDE] API key set, length:', window.API_KEY.length);
        }
        
        // Process query in Python
        window.pyodide.globals.set('user_query', query);
        console.log('[PYODIDE] Calling process_user_query()');
        
        const result = await window.pyodide.runPythonAsync(`process_user_query(user_query)`);
        
        console.log('[PYODIDE] Query completed, result length:', result ? result.length : 0);
        addMessage('assistant', result);
        
    } catch (error) {
        console.error('[ERROR] Query processing failed:', error);
        addMessage('error', `Error processing query: ${error.message}`);
    } finally {
        const duration = Date.now() - startTime;
        console.log('[QUERY END] Duration:', duration + 'ms');
        
        if (loadingEl) loadingEl.classList.remove('show');
        if (analyzeBtn) {
            analyzeBtn.disabled = false;
            analyzeBtn.innerHTML = '<span>🔍</span><span>Analyze Data</span>';
        }
    }
}

// Sample query functions
function fillSampleQuery(query) {
    const textarea = document.getElementById('queryInput');
    textarea.value = query;
    textarea.focus();
    // Don't auto-submit - let user review and click button
}

// Message display
function addMessage(type, content) {
    const resultsContainer = document.getElementById('results-container');
    const messageEl = document.createElement('div');
    messageEl.className = `message message-${type}`;
    
    // Preserve image tags by replacing them with placeholders
    const imgPlaceholders = [];
    let contentWithPlaceholders = content.replace(/<img[^>]*>/g, (match) => {
        const placeholder = `___IMG_PLACEHOLDER_${imgPlaceholders.length}___`;
        imgPlaceholders.push(match);
        return placeholder;
    });
    
    // Handle markdown-like formatting
    let formattedContent = contentWithPlaceholders
        .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
        .replace(/\*(.*?)\*/g, '<em>$1</em>')
        .replace(/### (.*?)(\n|$)/g, '<h3>$1</h3>')
        .replace(/## (.*?)(\n|$)/g, '<h2>$1</h2>')
        .replace(/# (.*?)(\n|$)/g, '<h1>$1</h1>')
        .replace(/^- (.*?)$/gm, '<li>$1</li>')
        .replace(/^• (.*?)$/gm, '<li>$1</li>')
        .replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
    
    // Convert markdown tables to HTML
    formattedContent = formattedContent.replace(
        /(\|[^\n]+\|\n)(\|[-:\s|]+\|\n)((?:\|[^\n]+\|\n?)+)/g,
        (match, headerRow, separatorRow, bodyRows) => {
            // Parse header
            const headers = headerRow.split('|').filter(h => h.trim()).map(h => h.trim());
            
            // Parse body rows
            const rows = bodyRows.trim().split('\n').map(row => 
                row.split('|').filter(c => c.trim()).map(c => c.trim())
            );
            
            // Build HTML table
            let tableHtml = '<table style="width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.875rem;">';
            
            // Header
            tableHtml += '<thead><tr>';
            headers.forEach(h => {
                tableHtml += `<th style="border: 1px solid var(--surface-border); padding: 0.5rem; background: var(--surface-bg); font-weight: 600; text-align: left;">${h}</th>`;
            });
            tableHtml += '</tr></thead>';
            
            // Body
            tableHtml += '<tbody>';
            rows.forEach((row, idx) => {
                const bgStyle = idx % 2 === 0 ? 'background: white;' : 'background: #f8fafc;';
                tableHtml += `<tr style="${bgStyle}">`;
                row.forEach(cell => {
                    tableHtml += `<td style="border: 1px solid var(--surface-border); padding: 0.5rem;">${cell}</td>`;
                });
                tableHtml += '</tr>';
            });
            tableHtml += '</tbody></table>';
            
            return tableHtml;
        }
    );
    
    // Replace newlines with <br> (after table processing)
    formattedContent = formattedContent.replace(/\n/g, '<br>');
    
    // Restore image tags
    imgPlaceholders.forEach((img, index) => {
        formattedContent = formattedContent.replace(`___IMG_PLACEHOLDER_${index}___`, img);
    });
    
    messageEl.innerHTML = `
        <div class="message-icon">
            ${type === 'user' ? '👤' : type === 'assistant' ? '🤖' : type === 'error' ? '❌' : type === 'system' ? '🔧' : 'ℹ️'}
        </div>
        <div class="message-content">${formattedContent}</div>
    `;
    
    resultsContainer.appendChild(messageEl);
    resultsContainer.scrollTop = resultsContainer.scrollHeight;
}

// Enter key handling
document.addEventListener('DOMContentLoaded', function() {
    // Enter key for query
    const queryInput = document.getElementById('queryInput');
    if (queryInput) {
        queryInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                processQuery();
            }
        });
    }
});
import pandas as pd
import numpy as np

# Template Variables - Users can customize these
max_rows = [[[MAX_ROWS|1000]]]
precision = [[[PRECISION|2]]]
chart_type = "[[[CHART_TYPE|bar]]]"
include_summary = [[[INCLUDE_SUMMARY|True]]]

# Global variables to store loaded data
current_data = None
current_filename = None

def dataframe_to_markdown(df, max_rows=10):
    """Convert pandas DataFrame to markdown table format."""
    if len(df) > max_rows:
        df = df.head(max_rows)
        truncated = True
    else:
        truncated = False
    lines = []
    headers = [''] + list(df.columns)
    lines.append('| ' + ' | '.join(str(h) for h in headers) + ' |')
    lines.append('|' + '|'.join([' --- ' for _ in range(len(headers))]) + '|')
    for idx, row in df.iterrows():
        row_values = [str(idx)] + [str(v) for v in row]
        lines.append('| ' + ' | '.join(row_values) + ' |')
    if truncated:
        lines.append(f'\n*Showing first {max_rows} rows of {len(df)} total*')
    return '\n'.join(lines)

def load_csv_data(csv_content: str, filename: str = "data.csv"):
    """Load CSV data into global variable."""
    global current_data, current_filename
    try:
        from io import StringIO
        current_data = pd.read_csv(StringIO(csv_content))
        current_filename = filename
        return f"✅ Loaded {current_data.shape[0]} rows and {current_data.shape[1]} columns from {filename}"
    except Exception as e:
        return f"❌ Error loading CSV: {str(e)}"

def get_data_summary() -> str:
    """Get dataset summary: shape, columns, data types, statistics."""
    global current_data
    if current_data is None:
        return "❌ No data loaded. Please upload a CSV file first."
    result = []
    result.append(f"## Dataset Overview")
    result.append(f"Shape: {current_data.shape[0]} rows × {current_data.shape[1]} columns")
    result.append(f"\nColumns: {', '.join(current_data.columns.tolist())}")
    result.append("\n### Data Types:")
    for col in current_data.columns:
        dtype = str(current_data[col].dtype)
        result.append(f"- **{col}**: {dtype}")
    result.append("\n### Missing Values:")
    missing = current_data.isnull().sum()
    has_missing = False
    for col in current_data.columns:
        if missing[col] > 0:
            pct = (missing[col] / len(current_data)) * 100
            result.append(f"- **{col}**: {missing[col]} missing ({pct:.1f}%)")
            has_missing = True
    if not has_missing:
        result.append("- ✅ No missing values found")
    if current_data.select_dtypes(include=[np.number]).shape[1] > 0:
        result.append("\n### Summary Statistics:")
        stats_df = current_data.describe()
        result.append(dataframe_to_markdown(stats_df))
    return "\n".join(result)

def get_column_info() -> str:
    """Get column info: data types and missing values."""
    global current_data
    if current_data is None:
        return "❌ No data loaded. Please upload a CSV file first."
    result = [f"## Column Information\n"]
    result.append(f"Dataset has **{len(current_data.columns)} columns** and **{len(current_data)} rows**:\n")
    result.append("| Column | Type | Non-Null | Missing | % Missing |")
    result.append("| --- | --- | --- | --- | --- |")
    for col in current_data.columns:
        dtype = str(current_data[col].dtype)
        non_null = current_data[col].count()
        total = len(current_data)
        missing = total - non_null
        pct_missing = (missing / total) * 100
        result.append(f"| {col} | {dtype} | {non_null} | {missing} | {pct_missing:.1f}% |")
    return "\n".join(result)

def get_value_counts(column: str) -> str:
    """Get value counts for a specific column."""
    global current_data
    if current_data is None:
        return "❌ No data loaded. Please upload a CSV file first."
    if column not in current_data.columns:
        return f"❌ Column '{column}' not found. Available columns: {', '.join(current_data.columns)}"
    result = [f"## Value counts for '{column}'\n"]
    value_counts = current_data[column].value_counts().head(15)
    total = len(current_data)
    result.append("| Value | Count | Percentage |")
    result.append("| --- | --- | --- |")
    for value, count in value_counts.items():
        pct = (count / total) * 100
        result.append(f"| {value} | {count} | {pct:.1f}% |")
    unique_count = current_data[column].nunique()
    if unique_count > 15:
        result.append(f"\n*Showing top 15 of {unique_count} unique values*")
    else:
        result.append(f"\n*Total unique values: {unique_count}*")
    return "\n".join(result)

def create_chart(column: str, chart_type: str = "histogram") -> str:
    """Create a chart for a specific column."""
    global current_data
    if current_data is None:
        return "❌ No data loaded. Please upload a CSV file first."
    if column not in current_data.columns:
        return f"❌ Column '{column}' not found. Available columns: {', '.join(current_data.columns)}"
    try:
        try:
            import matplotlib
            matplotlib.use('Agg')  # CRITICAL: Use Agg backend for Pyodide
            import matplotlib.pyplot as plt
            import base64
            from io import BytesIO
            plt.ioff()
        except ImportError as e:
            return f"❌ Chart creation unavailable: {str(e)}"
        
        fig, ax = plt.subplots(figsize=(10, 6))
        
        if chart_type.lower() == "bar":
            value_counts = current_data[column].value_counts().head(10)
            ax.bar(range(len(value_counts)), value_counts.values, color='#059669')
            ax.set_xticks(range(len(value_counts)))
            ax.set_xticklabels(value_counts.index, rotation=45, ha='right')
            ax.set_ylabel('Count')
            ax.set_title(f'Bar Chart: {column}', fontsize=14, fontweight='bold')
            ax.grid(axis='y', alpha=0.3)
        elif chart_type.lower() == "histogram":
            if pd.api.types.is_numeric_dtype(current_data[column]):
                ax.hist(current_data[column].dropna(), bins=20, alpha=0.7, color='#059669', edgecolor='white')
                ax.set_xlabel(column)
                ax.set_ylabel('Frequency')
                ax.set_title(f'Histogram: {column}', fontsize=14, fontweight='bold')
                ax.grid(axis='y', alpha=0.3)
            else:
                plt.close(fig)
                return f"❌ Cannot create histogram for non-numeric column '{column}'. Try 'bar' chart instead."
        else:
            plt.close(fig)
            return f"❌ Unsupported chart type '{chart_type}'. Use: bar or histogram"
        
        plt.tight_layout()
        
        # Save to BytesIO buffer and encode to base64
        buffer = BytesIO()
        plt.savefig(buffer, format='png', dpi=100, bbox_inches='tight')
        buffer.seek(0)
        image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
        plt.close(fig)
        
        # Return HTML with embedded base64 image
        return f"""✅ Chart created successfully for '{column}' ({chart_type} chart).

<img src="data:image/png;base64,{image_base64}" alt="{chart_type.title()} chart for {column}" style="max-width: 100%; height: auto; margin: 10px 0; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">

Chart shows the distribution of values in the '{column}' column."""
    except Exception as e:
        import traceback
        error_details = traceback.format_exc()
        return f"❌ Error creating chart: {str(e)}\n\nDetails:\n{error_details}"

def get_correlation_analysis() -> str:
    """Get correlation analysis for numeric columns."""
    global current_data
    if current_data is None:
        return "❌ No data loaded. Please upload a CSV file first."
    numeric_cols = current_data.select_dtypes(include=[np.number]).columns
    if len(numeric_cols) < 2:
        return "❌ Need at least 2 numerical columns to calculate correlations."
    corr_matrix = current_data[numeric_cols].corr()
    result = ["## Correlation Analysis\n"]
    result.append("### Correlation Matrix:\n")
    result.append(dataframe_to_markdown(corr_matrix, max_rows=20))
    result.append("\n### Key Insights:")
    strong_corr = []
    for i in range(len(numeric_cols)):
        for j in range(i+1, len(numeric_cols)):
            corr_val = corr_matrix.iloc[i, j]
            if abs(corr_val) > 0.7:
                strength = "strong positive" if corr_val > 0 else "strong negative"
                emoji = "📈" if corr_val > 0 else "📉"
                strong_corr.append(f"- {emoji} **{numeric_cols[i]}** and **{numeric_cols[j]}**: {strength} correlation ({corr_val:.3f})")
    if strong_corr:
        result.extend(strong_corr)
    else:
        result.append("- ℹ️ No strong correlations found (|r| > 0.7)")
    return "\n".join(result)