/**
* 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();
});