[claude] Matrix chat history persistence — localStorage per-agent, 100-msg cap, Clear button (#43) #63
@@ -71,6 +71,15 @@
|
|||||||
pointer-events: none; z-index: 10;
|
pointer-events: none; z-index: 10;
|
||||||
}
|
}
|
||||||
.log-entry { opacity: 0.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.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 button bar ───────────────────────────────────────────────── */
|
||||||
#top-buttons {
|
#top-buttons {
|
||||||
@@ -495,6 +504,7 @@
|
|||||||
|
|
||||||
<div id="connection-status">OFFLINE</div>
|
<div id="connection-status">OFFLINE</div>
|
||||||
<div id="event-log"></div>
|
<div id="event-log"></div>
|
||||||
|
<button id="clear-history-btn">CLEAR HISTORY</button>
|
||||||
|
|
||||||
<!-- ── Timmy identity card ────────────────────────────────────────── -->
|
<!-- ── Timmy identity card ────────────────────────────────────────── -->
|
||||||
<div id="timmy-id-card">
|
<div id="timmy-id-card">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { setSpeechBubble, setMood } from './agents.js';
|
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 { getOrRefreshToken } from './nostr-identity.js';
|
||||||
import { sentiment } from './edge-worker-client.js';
|
import { sentiment } from './edge-worker-client.js';
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ export async function sessionSendHandler(text) {
|
|||||||
|
|
||||||
_inFlight = true;
|
_inFlight = true;
|
||||||
_setSendBusy(true);
|
_setSendBusy(true);
|
||||||
appendSystemMessage(`you: ${text}`);
|
appendChatMessage('you', `you: ${text}`, null, 'timmy');
|
||||||
|
|
||||||
// Attach Nostr token if available
|
// Attach Nostr token if available
|
||||||
const nostrToken = await getOrRefreshToken('/api');
|
const nostrToken = await getOrRefreshToken('/api');
|
||||||
@@ -158,7 +158,7 @@ export async function sessionSendHandler(text) {
|
|||||||
|
|
||||||
const reply = data.result || data.reason || '…';
|
const reply = data.result || data.reason || '…';
|
||||||
setSpeechBubble(reply);
|
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-driven mood on inbound Timmy reply
|
||||||
sentiment(reply).then(s => {
|
sentiment(reply).then(s => {
|
||||||
|
|||||||
@@ -138,6 +138,15 @@ export function initUI() {
|
|||||||
if (uiInitialized) return;
|
if (uiInitialized) return;
|
||||||
uiInitialized = true;
|
uiInitialized = true;
|
||||||
initInputBar();
|
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() {
|
function initInputBar() {
|
||||||
@@ -159,7 +168,8 @@ function initInputBar() {
|
|||||||
|
|
||||||
if (cls.complexity === 'trivial' && cls.localReply) {
|
if (cls.complexity === 'trivial' && cls.localReply) {
|
||||||
// Greeting / small-talk → answer locally, 0 sats, no network call in any mode
|
// 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`);
|
setSpeechBubble(`${cls.localReply} ⚡ local`);
|
||||||
_showCostPreview('answered locally ⚡ 0 sats', '#44dd88');
|
_showCostPreview('answered locally ⚡ 0 sats', '#44dd88');
|
||||||
setTimeout(_hideCostPreview, 3000);
|
setTimeout(_hideCostPreview, 3000);
|
||||||
@@ -184,7 +194,7 @@ function initInputBar() {
|
|||||||
|
|
||||||
// Route to server via WebSocket
|
// Route to server via WebSocket
|
||||||
sendVisitorMessage(text);
|
sendVisitorMessage(text);
|
||||||
appendSystemMessage(`you: ${text}`);
|
appendChatMessage('you', `you: ${text}`, null, 'timmy');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sendBtn.addEventListener('click', send);
|
$sendBtn.addEventListener('click', send);
|
||||||
@@ -226,9 +236,81 @@ export function appendSystemMessage(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function appendChatMessage(agentLabel, message, cssColor, agentId) {
|
export function appendChatMessage(agentLabel, message, cssColor, agentId) {
|
||||||
void agentLabel; void cssColor; void agentId;
|
const id = agentId || 'timmy';
|
||||||
appendSystemMessage(message);
|
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 []; }
|
// ── Chat history persistence (localStorage, 100-msg cap per agent) ───────────
|
||||||
export function saveChatHistory() {}
|
|
||||||
|
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 { 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 { sentiment } from './edge-worker-client.js';
|
||||||
import { setLabelState } from './hud-labels.js';
|
import { setLabelState } from './hud-labels.js';
|
||||||
|
|
||||||
@@ -110,9 +110,9 @@ function handleMessage(msg) {
|
|||||||
|
|
||||||
case 'chat': {
|
case 'chat': {
|
||||||
if (msg.agentId === 'timmy') {
|
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);
|
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
|
// Sentiment-driven facial expression on inbound Timmy messages
|
||||||
if (msg.text) {
|
if (msg.text) {
|
||||||
sentiment(msg.text).then(s => {
|
sentiment(msg.text).then(s => {
|
||||||
@@ -122,7 +122,7 @@ function handleMessage(msg) {
|
|||||||
}
|
}
|
||||||
} else if (msg.agentId === 'visitor') {
|
} else if (msg.agentId === 'visitor') {
|
||||||
// Another visitor's message: event log only (don't hijack the speech bubble)
|
// 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 {
|
} else {
|
||||||
// System agent messages (delta payment confirmations, etc.): speech bubble
|
// System agent messages (delta payment confirmations, etc.): speech bubble
|
||||||
if (msg.text) setSpeechBubble(msg.text);
|
if (msg.text) setSpeechBubble(msg.text);
|
||||||
|
|||||||
Reference in New Issue
Block a user