mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
overser memory viewer!
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-CZrapRE0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-l6FtFm8i.css">
|
||||
<script type="module" crossorigin src="/assets/index-8c6_50As.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CzvRO_Gd.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -156,6 +156,27 @@ function updateStatus(patch = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function buildMemorySnapshot() {
|
||||
// The public UI only needs the same compact text summary that the overseer
|
||||
// already reasons over. Keeping this as text avoids exposing the raw mutable
|
||||
// store shape as a browser-facing contract.
|
||||
return {
|
||||
summary: summarizeMemory(runtime.memoryStore),
|
||||
updatedAt: Number(runtime.memoryStore?.updatedAt) || Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function emitMemoryToSocket(socket) {
|
||||
if (!socket) return;
|
||||
socket.emit('overseer:memory', buildMemorySnapshot());
|
||||
}
|
||||
|
||||
function emitMemoryToAll() {
|
||||
// Memory writes are rare and user-visible, so broadcast immediately instead
|
||||
// of waiting for unrelated session sync events to refresh the popup.
|
||||
io.sockets.sockets.forEach((socket) => emitMemoryToSocket(socket));
|
||||
}
|
||||
|
||||
function pushRun(run = {}) {
|
||||
runtime.runHistory = [...runtime.runHistory.slice(-(MAX_RUN_HISTORY - 1)), run];
|
||||
}
|
||||
@@ -391,6 +412,7 @@ async function runDecision(triggerReason) {
|
||||
});
|
||||
if (result?.memory && typeof result.memory === 'object') {
|
||||
runtime.memoryStore = saveMemory(result.memory);
|
||||
emitMemoryToAll();
|
||||
}
|
||||
pushLiveToolCall({ phase: 'ok', tool: action.tool, result });
|
||||
actionResults.push({ kind: 'tool', tool: action.tool, ok: true, result });
|
||||
@@ -528,6 +550,7 @@ function clearHistory(reason = 'admin requested clear history', options = {}) {
|
||||
runtime.generationTotalMs = 0;
|
||||
if (resetPersistentMemory) {
|
||||
runtime.memoryStore = saveMemory(createDefaultMemory());
|
||||
emitMemoryToAll();
|
||||
}
|
||||
updateStatus({
|
||||
phase: 'idle',
|
||||
@@ -600,6 +623,7 @@ function evaluateSchedulerGate(reason = 'gate reevaluated') {
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
emitStateToSocket(socket);
|
||||
emitMemoryToSocket(socket);
|
||||
evaluateSchedulerGate('online vote update');
|
||||
socket.on('disconnect', () => evaluateSchedulerGate('online vote update'));
|
||||
socket.on('overseer:control', ({ controls } = {}, cb = () => {}) => {
|
||||
|
||||
@@ -1,13 +1,53 @@
|
||||
import { useState } from 'react';
|
||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
|
||||
function formatUpdatedAt(value) {
|
||||
const timestamp = Number(value);
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return null;
|
||||
const date = new Date(timestamp);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
|
||||
function OverseerMemoryPopup({ memory, onClose }) {
|
||||
const summary = typeof memory?.summary === 'string' && memory.summary.trim() ? memory.summary : 'Memory unavailable.';
|
||||
const updatedAt = formatUpdatedAt(memory?.updatedAt);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-40 flex items-center justify-center bg-black/75 p-1"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className="w-full max-w-2xl" onClick={(event) => event.stopPropagation()}>
|
||||
<CardFrame
|
||||
title="Overseer memory"
|
||||
meta={updatedAt ? `Updated ${updatedAt}` : null}
|
||||
actions={
|
||||
<button type="button" onClick={onClose} className="button-dark px-1 py-0.25 text-[0.75rem]">
|
||||
Close
|
||||
</button>
|
||||
}
|
||||
bodyClassName="p-0.5 text-xs"
|
||||
>
|
||||
{/* The memory is rendered as the server's existing plain-text summary so
|
||||
this popup stays consistent with the overseer/admin debug display. */}
|
||||
<pre className="surface max-h-[70vh] overflow-y-auto whitespace-pre-wrap break-words font-mono text-[0.72rem] text-slate-200">{summary}</pre>
|
||||
</CardFrame>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OverseerPreferencePanel() {
|
||||
const { identifySession } = useSessionActions();
|
||||
const vote = useSessionSelector((state) => state.session?.overseerVote || null);
|
||||
const overseerMemory = useSessionSelector((state) => state.overseerMemory || null);
|
||||
const { value: profile } = useSettingsNamespace('profile', { nickname: '' });
|
||||
const { value: identity } = useSettingsNamespace('identity', { cookieUserId: '' });
|
||||
const { value, save } = useSettingsNamespace('overseerPreference', { enabled: false });
|
||||
const [memoryOpen, setMemoryOpen] = useState(false);
|
||||
const enabled = Boolean(value?.enabled);
|
||||
const running = Boolean(vote?.running);
|
||||
|
||||
@@ -21,30 +61,44 @@ export default function OverseerPreferencePanel() {
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<CardFrame title="Overseer vote" bodyClassName="space-y-0.5 text-center text-xs text-slate-200">
|
||||
<label
|
||||
className={`flex items-center justify-center gap-1.5 rounded px-1 py-0.5 ${
|
||||
enabled ? 'bg-emerald-600/80 text-emerald-50' : 'bg-slate-600/70 text-slate-100'
|
||||
}`}
|
||||
>
|
||||
<input type="checkbox" checked={enabled} onChange={onToggle} />
|
||||
<span>Your vote: {enabled ? 'Yes' : 'No'}</span>
|
||||
</label>
|
||||
<span className="text-xs">Enable a local LLM in chat</span>
|
||||
const actions = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMemoryOpen(true)}
|
||||
className="button-dark px-1 py-0.25 text-[0.75rem]"
|
||||
title="View overseer memory"
|
||||
>
|
||||
Memory
|
||||
</button>
|
||||
);
|
||||
|
||||
<div className="mt-0.5 text-slate-400">
|
||||
Yes!: {Number(vote?.yesCount || 0)}, No...: {Number(vote?.noCount || 0)}
|
||||
</div>
|
||||
<div className="mt-0.5 flex justify-center">
|
||||
<span
|
||||
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.7rem] font-medium ${
|
||||
running ? 'bg-emerald-600/80 text-emerald-50' : 'bg-slate-600/70 text-slate-100'
|
||||
return (
|
||||
<>
|
||||
<CardFrame title="Overseer vote" actions={actions} bodyClassName="space-y-0.5 text-center text-xs text-slate-200">
|
||||
<label
|
||||
className={`flex items-center justify-center gap-1.5 rounded px-1 py-0.5 ${
|
||||
enabled ? 'bg-emerald-600/80 text-emerald-50' : 'bg-slate-600/70 text-slate-100'
|
||||
}`}
|
||||
>
|
||||
{running ? 'Running!' : 'Stopped by vote.'}
|
||||
</span>
|
||||
</div>
|
||||
</CardFrame>
|
||||
<input type="checkbox" checked={enabled} onChange={onToggle} />
|
||||
<span>Your vote: {enabled ? 'Yes' : 'No'}</span>
|
||||
</label>
|
||||
<span className="text-xs">Enable a local LLM in chat</span>
|
||||
|
||||
<div className="mt-0.5 text-slate-400">
|
||||
Yes!: {Number(vote?.yesCount || 0)}, No...: {Number(vote?.noCount || 0)}
|
||||
</div>
|
||||
<div className="mt-0.5 flex justify-center">
|
||||
<span
|
||||
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.7rem] font-medium ${
|
||||
running ? 'bg-emerald-600/80 text-emerald-50' : 'bg-slate-600/70 text-slate-100'
|
||||
}`}
|
||||
>
|
||||
{running ? 'Running!' : 'Stopped by vote.'}
|
||||
</span>
|
||||
</div>
|
||||
</CardFrame>
|
||||
{memoryOpen ? <OverseerMemoryPopup memory={overseerMemory} onClose={() => setMemoryOpen(false)} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ const INITIAL_STATE = {
|
||||
llmCommentaryState: null,
|
||||
llmCommentaryStatus: null,
|
||||
overseerControlState: null,
|
||||
overseerMemory: null,
|
||||
alerts: [],
|
||||
};
|
||||
|
||||
@@ -132,6 +133,12 @@ export function SessionProvider({ children }) {
|
||||
const state = payload && typeof payload === 'object' ? payload : null;
|
||||
setState((prev) => ({ ...prev, overseerControlState: state }));
|
||||
}
|
||||
function handleOverseerMemory(payload = null) {
|
||||
// Overseer memory is intentionally stored separately from session sync so
|
||||
// the vote panel can refresh as soon as the overseer writes memory.
|
||||
const memory = payload && typeof payload === 'object' ? payload : null;
|
||||
setState((prev) => ({ ...prev, overseerMemory: memory }));
|
||||
}
|
||||
socket.on('session:sync', handleSession);
|
||||
socket.on('log:init', handleLogInit);
|
||||
socket.on('log:entry', handleLogEntry);
|
||||
@@ -139,6 +146,7 @@ export function SessionProvider({ children }) {
|
||||
socket.on('adminlog:entry', handleAdminLogEntry);
|
||||
socket.on('llm:state', handleLlmState);
|
||||
socket.on('overseer:state', handleOverseerState);
|
||||
socket.on('overseer:memory', handleOverseerMemory);
|
||||
socket.on('alert:new', handleAlertNew);
|
||||
return () => {
|
||||
socket.off('session:sync', handleSession);
|
||||
@@ -148,6 +156,7 @@ export function SessionProvider({ children }) {
|
||||
socket.off('adminlog:entry', handleAdminLogEntry);
|
||||
socket.off('llm:state', handleLlmState);
|
||||
socket.off('overseer:state', handleOverseerState);
|
||||
socket.off('overseer:memory', handleOverseerMemory);
|
||||
socket.off('alert:new', handleAlertNew);
|
||||
};
|
||||
}, [setState, socket]);
|
||||
|
||||
Reference in New Issue
Block a user