import { getAgentDefs } from './agents.js'; import { AGENT_DEFS, colorToCss } from './agent-defs.js'; const $agentCount = document.getElementById('agent-count'); const $activeJobs = document.getElementById('active-jobs'); const $fps = document.getElementById('fps'); const $agentList = document.getElementById('agent-list'); const $connStatus = document.getElementById('connection-status'); const $chatPanel = document.getElementById('chat-panel'); const $clearBtn = document.getElementById('chat-clear-btn'); const MAX_CHAT_ENTRIES = 12; const MAX_STORED = 100; const STORAGE_PREFIX = 'matrix:chat:'; const chatEntries = []; const chatHistory = {}; const IDLE_COLOR = '#005500'; const ACTIVE_COLOR = '#00ff41'; /* ── localStorage chat history ────────────────────────── */ function storageKey(agentId) { return STORAGE_PREFIX + agentId; } export function loadChatHistory(agentId) { try { const raw = localStorage.getItem(storageKey(agentId)); if (!raw) return []; const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return []; return parsed.filter(m => m && typeof m.agentLabel === 'string' && typeof m.text === 'string' ); } catch { return []; } } export function saveChatHistory(agentId, messages) { try { localStorage.setItem(storageKey(agentId), JSON.stringify(messages.slice(-MAX_STORED))); } catch { /* quota exceeded or private mode */ } } function formatTimestamp(ts) { const d = new Date(ts); const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); return `${hh}:${mm}`; } function loadAllHistories() { const all = []; const agentIds = [...AGENT_DEFS.map(d => d.id), 'sys']; for (const id of agentIds) { const msgs = loadChatHistory(id); chatHistory[id] = msgs; all.push(...msgs); } all.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)); for (const msg of all.slice(-MAX_CHAT_ENTRIES)) { const entry = buildChatEntry(msg.agentLabel, msg.text, msg.cssColor, msg.timestamp); chatEntries.push(entry); $chatPanel.appendChild(entry); } $chatPanel.scrollTop = $chatPanel.scrollHeight; } function clearAllHistories() { const agentIds = [...AGENT_DEFS.map(d => d.id), 'sys']; for (const id of agentIds) { localStorage.removeItem(storageKey(id)); chatHistory[id] = []; } while ($chatPanel.firstChild) $chatPanel.removeChild($chatPanel.firstChild); chatEntries.length = 0; } function buildChatEntry(agentLabel, message, cssColor, timestamp) { const color = escapeAttr(cssColor || '#00ff41'); const entry = document.createElement('div'); entry.className = 'chat-entry'; const ts = timestamp ? `[${formatTimestamp(timestamp)}] ` : ''; entry.innerHTML = `${ts}${escapeHtml(agentLabel)}: ${escapeHtml(message)}`; return entry; } export function initUI() { renderAgentList(); loadAllHistories(); if ($clearBtn) $clearBtn.addEventListener('click', clearAllHistories); } function renderAgentList() { const defs = getAgentDefs(); $agentList.innerHTML = defs.map(a => { const css = escapeAttr(colorToCss(a.color)); const safeLabel = escapeHtml(a.label); const safeId = escapeAttr(a.id); return `
[ ${safeLabel} ] IDLE
`; }).join(''); } export function updateUI({ fps, agentCount, jobCount, connectionState }) { $fps.textContent = `FPS: ${fps}`; $agentCount.textContent = `AGENTS: ${agentCount}`; $activeJobs.textContent = `JOBS: ${jobCount}`; if (connectionState === 'connected') { $connStatus.textContent = '● CONNECTED'; $connStatus.className = 'connected'; } else if (connectionState === 'connecting') { $connStatus.textContent = '◌ CONNECTING...'; $connStatus.className = ''; } else { $connStatus.textContent = '○ OFFLINE'; $connStatus.className = ''; } const defs = getAgentDefs(); defs.forEach(a => { const el = document.getElementById(`agent-state-${a.id}`); if (el) { el.textContent = ` ${a.state.toUpperCase()}`; el.style.color = a.state === 'active' ? ACTIVE_COLOR : IDLE_COLOR; } }); } /** * Append a line to the chat panel. * @param {string} agentLabel — display name * @param {string} message — message text (HTML-escaped before insertion) * @param {string} cssColor — CSS color string, e.g. '#00ff88' */ export function appendChatMessage(agentLabel, message, cssColor, extraClass) { const now = Date.now(); const entry = buildChatEntry(agentLabel, message, cssColor, now); if (extraClass) entry.className += ' ' + extraClass; chatEntries.push(entry); while (chatEntries.length > MAX_CHAT_ENTRIES) { const removed = chatEntries.shift(); try { $chatPanel.removeChild(removed); } catch { /* already removed */ } } $chatPanel.appendChild(entry); $chatPanel.scrollTop = $chatPanel.scrollHeight; /* persist per-agent history */ const agentId = AGENT_DEFS.find(d => d.label === agentLabel)?.id || 'sys'; if (!chatHistory[agentId]) chatHistory[agentId] = []; chatHistory[agentId].push({ agentLabel, text: message, cssColor, timestamp: now }); saveChatHistory(agentId, chatHistory[agentId]); } /** * Escape HTML text content — prevents tag injection. */ function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Escape a value for use inside an HTML attribute (style="...", id="..."). */ function escapeAttr(str) { return String(str) .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); }