feat: restore frontend shell and implement Project Mnemosyne visual memory bridge
This commit is contained in:
183
frontend/js/transcript.js
Normal file
183
frontend/js/transcript.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user