/** * transcript.js — Transcript Logger for The Matrix. * * Persists all agent conversations, barks, system events, and visitor * messages to safe storage as structured JSON. Provides download as * plaintext (.txt) or JSON (.json) via the HUD controls. * * Architecture: * - `logEntry()` is called from ui.js on every appendChatMessage * - Entries stored via storage.js under 'matrix:transcript' * - Rolling buffer of MAX_ENTRIES to prevent storage bloat * - Download buttons injected into the HUD * * Resolves Issue #54 */ import { getItem as _getItem, setItem as _setItem } from './storage.js'; const STORAGE_KEY = 'matrix:transcript'; const MAX_ENTRIES = 500; /** @type {Array} */ let entries = []; /** @type {HTMLElement|null} */ let $controls = null; /** * @typedef {Object} TranscriptEntry * @property {number} ts — Unix timestamp (ms) * @property {string} iso — ISO 8601 timestamp * @property {string} agent — Agent label (TIMMY, PERPLEXITY, SYS, YOU, etc.) * @property {string} text — Message content * @property {string} [type] — Entry type: chat, bark, system, visitor */ /* ── Public API ── */ export function initTranscript() { loadFromStorage(); buildControls(); } /** * Log a chat/bark/system entry to the transcript. * Called from ui.js appendChatMessage. * * @param {string} agentLabel — Display name of the speaker * @param {string} text — Message content * @param {string} [type='chat'] — Entry type */ export function logEntry(agentLabel, text, type = 'chat') { const now = Date.now(); const entry = { ts: now, iso: new Date(now).toISOString(), agent: agentLabel, text: text, type: type, }; entries.push(entry); // Trim rolling buffer if (entries.length > MAX_ENTRIES) { entries = entries.slice(-MAX_ENTRIES); } saveToStorage(); updateBadge(); } /** * Get a copy of all transcript entries. * @returns {TranscriptEntry[]} */ export function getTranscript() { return [...entries]; } /** * Clear the transcript. */ export function clearTranscript() { entries = []; saveToStorage(); updateBadge(); } export function disposeTranscript() { // Nothing to dispose — DOM controls persist across context loss } /* ── Storage ── */ function loadFromStorage() { try { const raw = _getItem(STORAGE_KEY); if (!raw) return; const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { entries = parsed.filter(e => e && typeof e.ts === 'number' && typeof e.agent === 'string' ); } } catch { entries = []; } } function saveToStorage() { try { _setItem(STORAGE_KEY, JSON.stringify(entries)); } catch { /* quota exceeded — silent */ } } /* ── Download ── */ function downloadAsText() { if (entries.length === 0) return; const lines = entries.map(e => { const time = new Date(e.ts).toLocaleTimeString('en-US', { hour12: false }); return `[${time}] ${e.agent}: ${e.text}`; }); const header = `THE MATRIX — Transcript\n` + `Exported: ${new Date().toISOString()}\n` + `Entries: ${entries.length}\n` + `${'─'.repeat(50)}\n`; download(header + lines.join('\n'), 'matrix-transcript.txt', 'text/plain'); } function downloadAsJson() { if (entries.length === 0) return; const data = { export_time: new Date().toISOString(), entry_count: entries.length, entries: entries, }; download(JSON.stringify(data, null, 2), 'matrix-transcript.json', 'application/json'); } function download(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /* ── HUD Controls ── */ function buildControls() { $controls = document.getElementById('transcript-controls'); if (!$controls) return; $controls.innerHTML = `LOG` + `${entries.length}` + `` + `` + ``; // Wire up buttons (pointer-events: auto on the container) $controls.querySelector('#transcript-dl-txt').addEventListener('click', downloadAsText); $controls.querySelector('#transcript-dl-json').addEventListener('click', downloadAsJson); $controls.querySelector('#transcript-clear').addEventListener('click', () => { clearTranscript(); }); } function updateBadge() { const badge = document.getElementById('transcript-badge'); if (badge) badge.textContent = entries.length; }