184 lines
4.9 KiB
JavaScript
184 lines
4.9 KiB
JavaScript
/**
|
|
* 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<TranscriptEntry>} */
|
|
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 =
|
|
`<span class="transcript-label">LOG</span>` +
|
|
`<span id="transcript-badge" class="transcript-badge">${entries.length}</span>` +
|
|
`<button class="transcript-btn" id="transcript-dl-txt" title="Download as text">TXT</button>` +
|
|
`<button class="transcript-btn" id="transcript-dl-json" title="Download as JSON">JSON</button>` +
|
|
`<button class="transcript-btn transcript-btn-clear" id="transcript-clear" title="Clear transcript">✕</button>`;
|
|
|
|
// 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;
|
|
}
|