[claude] Matrix chat history persistence — localStorage per-agent, 100-msg cap, Clear button (#43) (#63)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local> Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
This commit was merged in pull request #63.
This commit is contained in:
@@ -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 @@
|
||||
|
||||
<div id="connection-status">OFFLINE</div>
|
||||
<div id="event-log"></div>
|
||||
<button id="clear-history-btn">CLEAR HISTORY</button>
|
||||
|
||||
<!-- ── Timmy identity card ────────────────────────────────────────── -->
|
||||
<div id="timmy-id-card">
|
||||
|
||||
@@ -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';
|
||||
import qrcode from 'qrcode-generator';
|
||||
@@ -111,7 +111,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');
|
||||
@@ -159,7 +159,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 => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user