diff --git a/the-matrix/index.html b/the-matrix/index.html index b5fac26..3308a8c 100644 --- a/the-matrix/index.html +++ b/the-matrix/index.html @@ -71,6 +71,15 @@ pointer-events: none; z-index: 10; } .log-entry { opacity: 0.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + #clear-history-btn { + position: fixed; bottom: 80px; left: 300px; + background: transparent; border: 1px solid #1a1a2e; + color: #334466; font-family: 'Courier New', monospace; + font-size: 9px; letter-spacing: 1px; padding: 2px 8px; + cursor: pointer; z-index: 10; + transition: color 0.2s, border-color 0.2s; + } + #clear-history-btn:hover { color: #6688bb; border-color: #4466aa; } /* ── Top button bar ───────────────────────────────────────────────── */ #top-buttons { @@ -495,6 +504,7 @@
OFFLINE
+
diff --git a/the-matrix/js/session.js b/the-matrix/js/session.js index bfaedf7..78b17cb 100644 --- a/the-matrix/js/session.js +++ b/the-matrix/js/session.js @@ -13,7 +13,7 @@ */ import { setSpeechBubble, setMood } from './agents.js'; -import { appendSystemMessage, setSessionSendHandler, setInputBarSessionMode } from './ui.js'; +import { appendSystemMessage, appendChatMessage, setSessionSendHandler, setInputBarSessionMode } from './ui.js'; import { getOrRefreshToken } from './nostr-identity.js'; import { sentiment } from './edge-worker-client.js'; @@ -110,7 +110,7 @@ export async function sessionSendHandler(text) { _inFlight = true; _setSendBusy(true); - appendSystemMessage(`you: ${text}`); + appendChatMessage('you', `you: ${text}`, null, 'timmy'); // Attach Nostr token if available const nostrToken = await getOrRefreshToken('/api'); @@ -158,7 +158,7 @@ export async function sessionSendHandler(text) { const reply = data.result || data.reason || '…'; setSpeechBubble(reply); - appendSystemMessage('Timmy: ' + reply.slice(0, 80)); + appendChatMessage('Timmy', 'Timmy: ' + reply.slice(0, 80), null, 'timmy'); // Sentiment-driven mood on inbound Timmy reply sentiment(reply).then(s => { diff --git a/the-matrix/js/ui.js b/the-matrix/js/ui.js index b646057..fd8ccc3 100644 --- a/the-matrix/js/ui.js +++ b/the-matrix/js/ui.js @@ -138,6 +138,15 @@ export function initUI() { if (uiInitialized) return; uiInitialized = true; initInputBar(); + // Restore chat history from localStorage on load + renderChatHistory('timmy'); + // Wire up Clear History button + const $clearBtn = document.getElementById('clear-history-btn'); + if ($clearBtn) { + $clearBtn.addEventListener('click', () => { + clearChatHistory('timmy'); + }); + } } function initInputBar() { @@ -159,7 +168,8 @@ function initInputBar() { if (cls.complexity === 'trivial' && cls.localReply) { // Greeting / small-talk → answer locally, 0 sats, no network call in any mode - appendSystemMessage(`you: ${text}`); + appendChatMessage('you', `you: ${text}`, null, 'timmy'); + appendChatMessage('timmy', `${cls.localReply} ⚡ local`, null, 'timmy'); setSpeechBubble(`${cls.localReply} ⚡ local`); _showCostPreview('answered locally ⚡ 0 sats', '#44dd88'); setTimeout(_hideCostPreview, 3000); @@ -184,7 +194,7 @@ function initInputBar() { // Route to server via WebSocket sendVisitorMessage(text); - appendSystemMessage(`you: ${text}`); + appendChatMessage('you', `you: ${text}`, null, 'timmy'); } $sendBtn.addEventListener('click', send); @@ -226,9 +236,81 @@ export function appendSystemMessage(text) { } export function appendChatMessage(agentLabel, message, cssColor, agentId) { - void agentLabel; void cssColor; void agentId; - appendSystemMessage(message); + const id = agentId || 'timmy'; + const entry = { agentLabel, message, cssColor, agentId: id, timestamp: Date.now() }; + const history = loadChatHistory(id); + history.push(entry); + saveChatHistory(id, history); + _renderChatEntry(entry); } -export function loadChatHistory() { return []; } -export function saveChatHistory() {} +// ── Chat history persistence (localStorage, 100-msg cap per agent) ─────────── + +const CHAT_MAX = 100; + +function _chatKey(agentId) { + return `matrix:chat:${agentId}`; +} + +export function loadChatHistory(agentId) { + try { + const raw = localStorage.getItem(_chatKey(agentId)); + if (!raw) return []; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +export function saveChatHistory(agentId, messages) { + const capped = messages.length > CHAT_MAX + ? messages.slice(messages.length - CHAT_MAX) + : messages; + try { + localStorage.setItem(_chatKey(agentId), JSON.stringify(capped)); + } catch { /* storage full — silently drop */ } +} + +export function clearChatHistory(agentId) { + localStorage.removeItem(_chatKey(agentId)); + // Clear rendered log entries + if ($log) { + while ($log.firstChild) $log.removeChild($log.firstChild); + logEntries.length = 0; + } +} + +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 _renderChatEntry(entry) { + if (!$log) return; + const el = document.createElement('div'); + el.className = 'log-entry'; + const time = entry.timestamp ? `[${_formatTimestamp(entry.timestamp)}] ` : ''; + el.textContent = `${time}${entry.message}`; + if (entry.cssColor) el.style.color = entry.cssColor; + logEntries.push(el); + if (logEntries.length > MAX_LOG) { + const removed = logEntries.shift(); + $log.removeChild(removed); + } + $log.appendChild(el); + $log.scrollTop = $log.scrollHeight; +} + +/** + * Render stored chat history for an agent into the event log. + * Call on panel open / page load to restore conversation continuity. + */ +export function renderChatHistory(agentId) { + const history = loadChatHistory(agentId); + for (const entry of history) { + _renderChatEntry(entry); + } +} diff --git a/the-matrix/js/websocket.js b/the-matrix/js/websocket.js index 8ef4a04..0cdcb86 100644 --- a/the-matrix/js/websocket.js +++ b/the-matrix/js/websocket.js @@ -1,5 +1,5 @@ import { setAgentState, setSpeechBubble, applyAgentStates, setMood } from './agents.js'; -import { appendSystemMessage } from './ui.js'; +import { appendSystemMessage, appendChatMessage } from './ui.js'; import { sentiment } from './edge-worker-client.js'; import { setLabelState } from './hud-labels.js'; @@ -110,9 +110,9 @@ function handleMessage(msg) { case 'chat': { if (msg.agentId === 'timmy') { - // Timmy's AI reply: show in speech bubble + event log + // Timmy's AI reply: show in speech bubble + persist to chat history if (msg.text) setSpeechBubble(msg.text); - appendSystemMessage('Timmy: ' + (msg.text || '').slice(0, 80)); + appendChatMessage('Timmy', 'Timmy: ' + (msg.text || '').slice(0, 80), null, 'timmy'); // Sentiment-driven facial expression on inbound Timmy messages if (msg.text) { sentiment(msg.text).then(s => { @@ -122,7 +122,7 @@ function handleMessage(msg) { } } else if (msg.agentId === 'visitor') { // Another visitor's message: event log only (don't hijack the speech bubble) - appendSystemMessage((msg.text || '').slice(0, 80)); + appendChatMessage('visitor', (msg.text || '').slice(0, 80), null, 'timmy'); } else { // System agent messages (delta payment confirmations, etc.): speech bubble if (msg.text) setSpeechBubble(msg.text);