OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / opencog-kb / language-learning / mvp / dashboard / dashboard.js

381 lines · javascript ref:
/**
 * Language Learning Dashboard
 * Connects to CogServer via WebSocket and displays real-time stats
 */

class LearningDashboard {
  constructor() {
    this.socket = null;
    this.serverUrl = '';
    this.charts = {};
    this.statsHistory = [];
    this.maxHistory = 60; // Keep 60 data points
    this.pendingCommand = null;
    this.pollInterval = null;

    this.initCharts();
    this.bindEvents();
  }

  initCharts() {
    // Pairs over time chart
    const pairsCtx = document.getElementById('pairs-chart').getContext('2d');
    this.charts.pairs = new Chart(pairsCtx, {
      type: 'line',
      data: {
        labels: [],
        datasets: [{
          label: 'Pairs',
          data: [],
          borderColor: '#4ecca3',
          backgroundColor: 'rgba(78, 204, 163, 0.1)',
          fill: true,
          tension: 0.3
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          x: { display: false },
          y: {
            beginAtZero: false,
            grid: { color: '#0f3460' },
            ticks: { color: '#888' }
          }
        },
        plugins: {
          legend: { display: false }
        }
      }
    });

    // Memory over time chart
    const memCtx = document.getElementById('memory-chart').getContext('2d');
    this.charts.memory = new Chart(memCtx, {
      type: 'line',
      data: {
        labels: [],
        datasets: [{
          label: 'Memory (MB)',
          data: [],
          borderColor: '#e94560',
          backgroundColor: 'rgba(233, 69, 96, 0.1)',
          fill: true,
          tension: 0.3
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          x: { display: false },
          y: {
            beginAtZero: false,
            grid: { color: '#0f3460' },
            ticks: { color: '#888' }
          }
        },
        plugins: {
          legend: { display: false }
        }
      }
    });
  }

  bindEvents() {
    document.getElementById('connect-btn').addEventListener('click', () => {
      if (this.socket && this.socket.readyState === WebSocket.OPEN) {
        this.disconnect();
      } else {
        const server = document.getElementById('server-input').value;
        this.connect(server);
      }
    });

    document.getElementById('parse-btn').addEventListener('click', () => {
      const text = document.getElementById('parse-input').value;
      if (text) {
        this.sendCommand(`(parse-text "${this.escapeString(text)}")`);
      }
    });

    document.getElementById('learn-btn').addEventListener('click', () => {
      const text = document.getElementById('learn-input').value;
      if (text) {
        this.sendCommand(`(learn-text "${this.escapeString(text)}")`);
      }
    });

    document.getElementById('command-btn').addEventListener('click', () => {
      const cmd = document.getElementById('command-input').value;
      if (cmd) {
        this.sendCommand(cmd);
      }
    });

    // Enter key handlers
    document.getElementById('parse-input').addEventListener('keypress', (e) => {
      if (e.key === 'Enter') document.getElementById('parse-btn').click();
    });
    document.getElementById('learn-input').addEventListener('keypress', (e) => {
      if (e.key === 'Enter') document.getElementById('learn-btn').click();
    });
    document.getElementById('command-input').addEventListener('keypress', (e) => {
      if (e.key === 'Enter') document.getElementById('command-btn').click();
    });
  }

  escapeString(str) {
    return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  }

  connect(server) {
    this.serverUrl = server;
    const wsUrl = `ws://${server}/scm`;

    console.log('Connecting to:', wsUrl);
    this.socket = new WebSocket(wsUrl);

    this.socket.onopen = () => {
      console.log('Connected to CogServer');
      this.updateConnectionStatus(true);
      this.startPolling();
    };

    this.socket.onmessage = (event) => {
      this.handleResponse(event.data);
    };

    this.socket.onclose = () => {
      console.log('Disconnected from CogServer');
      this.updateConnectionStatus(false);
      this.stopPolling();
    };

    this.socket.onerror = (error) => {
      console.error('WebSocket error:', error);
      this.updateConnectionStatus(false);
    };
  }

  disconnect() {
    if (this.socket) {
      this.socket.close();
      this.socket = null;
    }
    this.stopPolling();
  }

  updateConnectionStatus(connected) {
    const status = document.getElementById('connection-status');
    const btn = document.getElementById('connect-btn');

    if (connected) {
      status.textContent = 'Connected';
      status.className = 'connected';
      btn.textContent = 'Disconnect';
    } else {
      status.textContent = 'Disconnected';
      status.className = 'disconnected';
      btn.textContent = 'Connect';
    }
  }

  sendCommand(cmd) {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      this.pendingCommand = cmd;
      this.socket.send(cmd);
    }
  }

  startPolling() {
    // Poll stats every second
    this.pollInterval = setInterval(() => {
      this.sendCommand('(learning-stats)');
    }, 1000);

    // Initial poll
    this.sendCommand('(learning-stats)');

    // Poll activity every 3 seconds
    setInterval(() => {
      this.sendCommand('(recent-activity 10)');
    }, 3000);
  }

  stopPolling() {
    if (this.pollInterval) {
      clearInterval(this.pollInterval);
      this.pollInterval = null;
    }
  }

  handleResponse(data) {
    // Try to parse as stats alist
    if (data.includes('timestamp') && data.includes('pairs')) {
      const stats = this.parseSchemeAlist(data);
      if (stats) {
        this.updateStats(stats);
      }
    }
    // Try to parse as activity list
    else if (data.startsWith('((') && data.includes('learn') || data.includes('parse')) {
      const activities = this.parseActivityList(data);
      if (activities) {
        this.updateActivityFeed(activities);
      }
    }
    // Display as command output
    else {
      this.updateOutput(data);
    }
  }

  parseSchemeAlist(data) {
    // Parse Scheme alist like ((timestamp . 123) (pairs . 456) ...)
    try {
      const stats = {};
      // Match (key . value) pairs
      const regex = /\((\w+(?:-\w+)*)\s+\.\s+([^)]+)\)/g;
      let match;
      while ((match = regex.exec(data)) !== null) {
        const key = match[1];
        let value = match[2].trim();
        // Parse numbers
        if (!isNaN(value)) {
          value = parseFloat(value);
        }
        stats[key] = value;
      }
      return Object.keys(stats).length > 0 ? stats : null;
    } catch (e) {
      console.error('Parse error:', e);
      return null;
    }
  }

  parseActivityList(data) {
    // Parse activity list ((time type data) ...)
    try {
      const activities = [];
      const regex = /\((\d+)\s+(\w+)\s+"([^"]+)"\)/g;
      let match;
      while ((match = regex.exec(data)) !== null) {
        activities.push({
          time: parseInt(match[1]),
          type: match[2],
          data: match[3]
        });
      }
      return activities.length > 0 ? activities : null;
    } catch (e) {
      return null;
    }
  }

  updateStats(stats) {
    // Update stat cards
    if (stats.pairs !== undefined) {
      document.getElementById('pairs-count').textContent =
        this.formatNumber(stats.pairs);
    }
    if (stats.words !== undefined) {
      document.getElementById('words-count').textContent =
        this.formatNumber(stats.words);
    }
    if (stats.sections !== undefined) {
      document.getElementById('sections-count').textContent =
        this.formatNumber(stats.sections);
    }
    if (stats['memory-mb'] !== undefined) {
      document.getElementById('memory-usage').textContent =
        stats['memory-mb'].toFixed(1);
    }
    if (stats.uptime !== undefined) {
      document.getElementById('uptime').textContent = stats.uptime;
    }
    if (stats.epoch !== undefined) {
      document.getElementById('epoch').textContent = stats.epoch;
    }

    // Update progress panel
    if (stats.sentences !== undefined) {
      document.getElementById('sentences-count').textContent = stats.sentences;
    }
    if (stats.parses !== undefined) {
      document.getElementById('parses-count').textContent = stats.parses;
    }
    if (stats['n-drift'] !== undefined) {
      const drift = stats['n-drift'] * 100;
      document.getElementById('n-drift').textContent = drift.toFixed(2) + '%';
      document.getElementById('drift-fill').style.width =
        Math.min(drift * 10, 100) + '%'; // Scale: 10% drift = full bar
    }

    // Update history for charts
    this.statsHistory.push({
      time: Date.now(),
      pairs: stats.pairs || 0,
      memory: stats['memory-mb'] || 0
    });

    if (this.statsHistory.length > this.maxHistory) {
      this.statsHistory.shift();
    }

    this.updateCharts();
  }

  updateCharts() {
    const labels = this.statsHistory.map((_, i) => i);
    const pairsData = this.statsHistory.map(s => s.pairs);
    const memoryData = this.statsHistory.map(s => s.memory);

    this.charts.pairs.data.labels = labels;
    this.charts.pairs.data.datasets[0].data = pairsData;
    this.charts.pairs.update('none');

    this.charts.memory.data.labels = labels;
    this.charts.memory.data.datasets[0].data = memoryData;
    this.charts.memory.update('none');
  }

  updateActivityFeed(activities) {
    const list = document.getElementById('activity-list');

    if (activities.length === 0) {
      list.innerHTML = '<li class="activity-empty">No activity yet</li>';
      return;
    }

    list.innerHTML = activities.map(a => {
      const time = new Date(a.time * 1000).toLocaleTimeString();
      return `<li>
        <span class="activity-time">${time}</span>
        <span class="activity-type">${a.type}</span>
        <span class="activity-data">${a.data}</span>
      </li>`;
    }).join('');
  }

  updateOutput(data) {
    const output = document.getElementById('output-content');
    output.textContent = data;
  }

  formatNumber(num) {
    if (num >= 1000000) {
      return (num / 1000000).toFixed(2) + 'M';
    } else if (num >= 1000) {
      return (num / 1000).toFixed(1) + 'K';
    }
    return num.toString();
  }
}

// Initialize dashboard on page load
document.addEventListener('DOMContentLoaded', () => {
  window.dashboard = new LearningDashboard();
});