newstufff

This commit is contained in:
legop3
2026-02-26 02:58:45 -05:00
parent 4d953f3cad
commit 5a575aeab4
7 changed files with 413 additions and 130 deletions
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
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-CS__Wyws.js"></script> <script type="module" crossorigin src="/assets/index-DKDSGdkZ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C_i0uH-D.css"> <link rel="stylesheet" crossorigin href="/assets/index-DzeZhrlZ.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+259 -26
View File
@@ -22,6 +22,7 @@ const ACTIVITY_WINDOW_MS = 30000;
const ACTIVITY_BUCKET_MS = 1000; const ACTIVITY_BUCKET_MS = 1000;
const SELF_TALK_WINDOW_MS = 30 * 60 * 1000; const SELF_TALK_WINDOW_MS = 30 * 60 * 1000;
const MAX_CONTEXT_EVENTS = 10; const MAX_CONTEXT_EVENTS = 10;
const MAX_RUN_HISTORY = 30;
const config = loadConfig(); const config = loadConfig();
const commentaryConfig = config.llmCommentary || {}; const commentaryConfig = config.llmCommentary || {};
@@ -40,6 +41,8 @@ let generationCount = 0;
let generationTotalMs = 0; let generationTotalMs = 0;
let contextResetAt = Date.now(); let contextResetAt = Date.now();
let clearCount = 0; let clearCount = 0;
let runHistory = [];
let currentRun = null;
const roverActivity = new Map(); // roverId -> { buckets: Map(bucketTs -> { distanceMm, turnDeg, bumps }), bumpLeftActive, bumpRightActive } const roverActivity = new Map(); // roverId -> { buckets: Map(bucketTs -> { distanceMm, turnDeg, bumps }), bumpLeftActive, bumpRightActive }
const lastRoverStateById = new Map(); // roverId -> compact rover state used as prev_state const lastRoverStateById = new Map(); // roverId -> compact rover state used as prev_state
@@ -65,14 +68,22 @@ let status = {
lastTickAt: null, lastTickAt: null,
lastOutcome: null, lastOutcome: null,
lastReason: null, lastReason: null,
phase: 'idle',
phaseAt: Date.now(),
currentRunId: null,
lastError: null, lastError: null,
lastErrorDetails: null, lastErrorDetails: null,
lastFailedAt: null,
lastPromptReadAt: null, lastPromptReadAt: null,
lastPromptChars: 0, lastPromptChars: 0,
lastSystemPrompt: null, lastSystemPrompt: null,
lastInfoSnapshot: null, lastInfoSnapshot: null,
lastModelMessages: null, lastModelMessages: null,
lastModelInputAt: null,
lastModelInputTickId: null,
lastModelRawOutput: null, lastModelRawOutput: null,
lastModelOutputAt: null,
lastModelOutputTickId: null,
lastSnapshotSummary: null, lastSnapshotSummary: null,
lastClearedAt: contextResetAt, lastClearedAt: contextResetAt,
clearCount, clearCount,
@@ -86,6 +97,120 @@ let status = {
updatedAt: Date.now(), updatedAt: Date.now(),
}; };
function buildAdminState() {
return {
runtime: {
running: status.running,
inFlight: status.inFlight,
phase: status.phase,
phaseAt: status.phaseAt,
currentRunId: status.currentRunId,
tickCount: status.tickCount,
lastTickAt: status.lastTickAt,
nextRunAt: status.nextRunAt,
outcome: status.lastOutcome,
reason: status.lastReason,
},
counters: {
clearCount: status.clearCount,
skipStreak: status.skipStreak,
promptChars: status.lastPromptChars,
snapshotSummary: status.lastSnapshotSummary,
},
timings: {
lastGenerationMs: status.lastGenerationMs,
avgGenerationMs: status.avgGenerationMs,
generationCount: status.generationCount,
},
input: {
promptPath: status.promptPath,
systemPrompt: status.lastSystemPrompt,
infoSnapshot: status.lastInfoSnapshot,
modelMessages: status.lastModelMessages,
modelInputAt: status.lastModelInputAt,
modelInputTickId: status.lastModelInputTickId,
},
output: {
raw: status.lastModelRawOutput,
generated: status.lastGeneratedText,
posted: status.lastPostedText,
postedAt: status.lastPostedAt,
modelOutputAt: status.lastModelOutputAt,
modelOutputTickId: status.lastModelOutputTickId,
},
errors: {
message: status.lastError,
details: status.lastErrorDetails,
failedAt: status.lastFailedAt,
},
history: runHistory,
debug: {
status,
},
controls: {
supportedActions: ['clearHistory'],
},
};
}
function updatePhase(phase, patch = {}) {
updateStatus({
phase,
phaseAt: Date.now(),
...patch,
});
}
function startRunRecord(tickId, snapshotSummary) {
currentRun = {
runId: tickId,
startedAt: Date.now(),
endedAt: null,
phase: 'tick_started',
outcome: null,
reason: null,
durationMs: null,
summary: snapshotSummary || {},
input: {
systemPrompt: null,
infoSnapshot: null,
modelMessages: null,
modelInputAt: null,
},
output: {
raw: null,
normalized: null,
posted: null,
postedAt: null,
modelOutputAt: null,
},
errors: null,
};
}
function patchCurrentRun(patch = {}) {
if (!currentRun) return;
currentRun = {
...currentRun,
...patch,
};
}
function finalizeRunRecord({ outcome, reason, errors } = {}) {
if (!currentRun) return;
const endedAt = Date.now();
const finalized = {
...currentRun,
outcome: outcome ?? currentRun.outcome,
reason: reason ?? currentRun.reason,
errors: errors ?? currentRun.errors ?? null,
endedAt,
durationMs: Math.max(0, endedAt - Number(currentRun.startedAt || endedAt)),
};
runHistory = [...runHistory.slice(-(MAX_RUN_HISTORY - 1)), finalized];
currentRun = null;
}
function isAdminSocket(socket) { function isAdminSocket(socket) {
if (!socket) return false; if (!socket) return false;
const role = getRole(socket); const role = getRole(socket);
@@ -94,13 +219,14 @@ function isAdminSocket(socket) {
function emitStatusToSocket(socket) { function emitStatusToSocket(socket) {
if (!socket || !isAdminSocket(socket)) return; if (!socket || !isAdminSocket(socket)) return;
socket.emit('llmCommentary:status', status); socket.emit('llm:state', buildAdminState());
} }
function emitStatusToAdmins() { function emitStatusToAdmins() {
const payload = buildAdminState();
io.sockets.sockets.forEach((socket) => { io.sockets.sockets.forEach((socket) => {
if (!isAdminSocket(socket)) return; if (!isAdminSocket(socket)) return;
socket.emit('llmCommentary:status', status); socket.emit('llm:state', payload);
}); });
} }
@@ -189,24 +315,34 @@ function clearRuntimeHistory() {
skipStreak = 0; skipStreak = 0;
generationCount = 0; generationCount = 0;
generationTotalMs = 0; generationTotalMs = 0;
runHistory = [];
currentRun = null;
roverActivity.clear(); roverActivity.clear();
lastRoverStateById.clear(); lastRoverStateById.clear();
updateStatus({ updateStatus({
lastClearedAt: contextResetAt, lastClearedAt: contextResetAt,
clearCount, clearCount,
skipStreak, skipStreak,
phase: 'idle',
phaseAt: Date.now(),
currentRunId: null,
lastGenerationMs: null, lastGenerationMs: null,
avgGenerationMs: null, avgGenerationMs: null,
generationCount, generationCount,
lastInfoSnapshot: null, lastInfoSnapshot: null,
lastModelMessages: null, lastModelMessages: null,
lastModelInputAt: null,
lastModelInputTickId: null,
lastModelRawOutput: null, lastModelRawOutput: null,
lastModelOutputAt: null,
lastModelOutputTickId: null,
lastSnapshotSummary: null, lastSnapshotSummary: null,
lastGeneratedText: null, lastGeneratedText: null,
lastPostedText: null, lastPostedText: null,
lastPostedAt: null, lastPostedAt: null,
lastError: null, lastError: null,
lastErrorDetails: null, lastErrorDetails: null,
lastFailedAt: null,
lastOutcome: 'cleared', lastOutcome: 'cleared',
lastReason: 'admin requested clear history', lastReason: 'admin requested clear history',
}); });
@@ -569,10 +705,7 @@ async function readSystemPrompt() {
return trimmed; return trimmed;
} }
async function generateCommentary(systemPrompt, snapshot) { function buildModelMessages(systemPrompt, snapshot) {
if (!ollamaClient) {
throw new Error('Ollama client unavailable');
}
const messages = []; const messages = [];
messages.push({ role: 'system', content: systemPrompt }); messages.push({ role: 'system', content: systemPrompt });
messages.push({ messages.push({
@@ -608,10 +741,13 @@ async function generateCommentary(systemPrompt, snapshot) {
role: 'user', role: 'user',
content: formatSnapshotFinalMessage(snapshot?.current_snapshot || {}), content: formatSnapshotFinalMessage(snapshot?.current_snapshot || {}),
}); });
updateStatus({ return messages;
lastModelMessages: messages, }
});
async function generateCommentary(messages) {
if (!ollamaClient) {
throw new Error('Ollama client unavailable');
}
const payload = await ollamaClient.chat({ const payload = await ollamaClient.chat({
model, model,
stream: false, stream: false,
@@ -649,16 +785,19 @@ async function runTick() {
let nextDelayMs = defaultTickDelayMs(); let nextDelayMs = defaultTickDelayMs();
tickCount += 1; tickCount += 1;
const tickId = tickCount; const tickId = tickCount;
updateStatus({ updatePhase('tick_started', {
tickCount, tickCount,
inFlight: true, inFlight: true,
currentRunId: tickId,
lastTickAt: Date.now(), lastTickAt: Date.now(),
lastError: null, lastError: null,
lastErrorDetails: null,
}); });
if (inFlight) { if (inFlight) {
logger.info('Commentary tick skipped; previous tick still running', { tickId }); logger.info('Commentary tick skipped; previous tick still running', { tickId });
updateStatus({ updatePhase('idle', {
inFlight: false, inFlight: false,
currentRunId: null,
lastOutcome: 'skipped', lastOutcome: 'skipped',
lastReason: 'previous tick still running', lastReason: 'previous tick still running',
}); });
@@ -679,19 +818,61 @@ async function runTick() {
tickId, tickId,
...snapshotSummary, ...snapshotSummary,
}); });
updateStatus({ startRunRecord(tickId, snapshotSummary);
patchCurrentRun({
phase: 'snapshot_ready',
summary: snapshotSummary,
input: {
...(currentRun?.input || {}),
infoSnapshot: snapshot,
},
});
updatePhase('snapshot_ready', {
lastSnapshotSummary: snapshotSummary, lastSnapshotSummary: snapshotSummary,
lastInfoSnapshot: snapshot, lastInfoSnapshot: snapshot,
}); });
const systemPrompt = await readSystemPrompt(); const systemPrompt = await readSystemPrompt();
const modelMessages = buildModelMessages(systemPrompt, snapshot);
const modelInputAt = Date.now();
patchCurrentRun({
phase: 'input_ready',
input: {
...(currentRun?.input || {}),
systemPrompt,
infoSnapshot: snapshot,
modelMessages,
modelInputAt,
},
});
updatePhase('awaiting_model_output', {
lastModelMessages: modelMessages,
lastModelInputAt: modelInputAt,
lastModelInputTickId: tickId,
lastModelRawOutput: null,
lastModelOutputAt: null,
lastModelOutputTickId: null,
lastReason: 'awaiting model output',
});
const generationStartMs = Date.now(); const generationStartMs = Date.now();
const modelResult = await generateCommentary(systemPrompt, snapshot); const modelResult = await generateCommentary(modelMessages);
const generationMs = Math.max(0, Date.now() - generationStartMs); const generationMs = Math.max(0, Date.now() - generationStartMs);
generationCount += 1; generationCount += 1;
generationTotalMs += generationMs; generationTotalMs += generationMs;
const avgGenerationMs = Math.round(generationTotalMs / generationCount); const avgGenerationMs = Math.round(generationTotalMs / generationCount);
updateStatus({ const modelOutputAt = Date.now();
patchCurrentRun({
phase: 'output_received',
output: {
...(currentRun?.output || {}),
raw: modelResult?.raw || '',
normalized: modelResult?.normalized || null,
modelOutputAt,
},
});
updatePhase('output_received', {
lastModelRawOutput: modelResult?.raw || '', lastModelRawOutput: modelResult?.raw || '',
lastModelOutputAt: modelOutputAt,
lastModelOutputTickId: tickId,
lastGenerationMs: generationMs, lastGenerationMs: generationMs,
avgGenerationMs, avgGenerationMs,
generationCount, generationCount,
@@ -700,17 +881,26 @@ async function runTick() {
if (!text) { if (!text) {
logger.info('Commentary tick produced SKIP/empty output', { tickId }); logger.info('Commentary tick produced SKIP/empty output', { tickId });
skipStreak += 1; skipStreak += 1;
updateStatus({ patchCurrentRun({
phase: 'decision_skip',
outcome: 'skipped',
reason: modelResult?.raw?.trim() ? 'model returned SKIP' : 'model returned empty',
});
updatePhase('decision_skip', {
lastOutcome: 'skipped', lastOutcome: 'skipped',
lastReason: modelResult?.raw?.trim() ? 'model returned SKIP' : 'model returned empty', lastReason: modelResult?.raw?.trim() ? 'model returned SKIP' : 'model returned empty',
skipStreak, skipStreak,
lastGeneratedText: null, lastGeneratedText: null,
}); });
finalizeRunRecord({
outcome: 'skipped',
reason: modelResult?.raw?.trim() ? 'model returned SKIP' : 'model returned empty',
});
// Immediate retry after model skip. // Immediate retry after model skip.
nextDelayMs = 0; nextDelayMs = 0;
return; return;
} }
updateStatus({ updatePhase('decision_post', {
lastGeneratedText: text, lastGeneratedText: text,
}); });
const recentBotMessages = getRecentMessages(80, { includeSystem: true }) const recentBotMessages = getRecentMessages(80, { includeSystem: true })
@@ -723,11 +913,20 @@ async function runTick() {
if (duplicate) { if (duplicate) {
logger.info('Commentary tick skipped duplicate output', { tickId, text }); logger.info('Commentary tick skipped duplicate output', { tickId, text });
skipStreak += 1; skipStreak += 1;
updateStatus({ patchCurrentRun({
phase: 'decision_skip',
outcome: 'skipped',
reason: 'duplicate text',
});
updatePhase('decision_skip', {
lastOutcome: 'skipped', lastOutcome: 'skipped',
lastReason: 'duplicate text', lastReason: 'duplicate text',
skipStreak, skipStreak,
}); });
finalizeRunRecord({
outcome: 'skipped',
reason: 'duplicate text',
});
// Immediate retry after a skip outcome. // Immediate retry after a skip outcome.
nextDelayMs = 0; nextDelayMs = 0;
return; return;
@@ -735,13 +934,27 @@ async function runTick() {
sendSystemMessage(text); sendSystemMessage(text);
logger.info('Commentary message posted', { tickId, text }); logger.info('Commentary message posted', { tickId, text });
skipStreak = 0; skipStreak = 0;
updateStatus({ patchCurrentRun({
phase: 'posted',
outcome: 'posted',
reason: null,
output: {
...(currentRun?.output || {}),
posted: text,
postedAt: Date.now(),
},
});
updatePhase('posted', {
lastOutcome: 'posted', lastOutcome: 'posted',
lastReason: null, lastReason: null,
skipStreak, skipStreak,
lastPostedText: text, lastPostedText: text,
lastPostedAt: Date.now(), lastPostedAt: Date.now(),
}); });
finalizeRunRecord({
outcome: 'posted',
reason: null,
});
} catch (err) { } catch (err) {
const failure = buildFailureInfo(err); const failure = buildFailureInfo(err);
logger.warn('Commentary tick failed', { logger.warn('Commentary tick failed', {
@@ -750,16 +963,35 @@ async function runTick() {
error: failure.message, error: failure.message,
details: failure.details, details: failure.details,
}); });
updateStatus({ patchCurrentRun({
phase: 'failed',
outcome: 'failed',
reason: failure.reason,
errors: {
message: failure.message,
details: failure.details,
},
});
updatePhase('failed', {
lastOutcome: 'failed', lastOutcome: 'failed',
lastReason: failure.reason, lastReason: failure.reason,
lastError: failure.message, lastError: failure.message,
lastErrorDetails: failure.details, lastErrorDetails: failure.details,
lastFailedAt: Date.now(),
});
finalizeRunRecord({
outcome: 'failed',
reason: failure.reason,
errors: {
message: failure.message,
details: failure.details,
},
}); });
} finally { } finally {
inFlight = false; inFlight = false;
updateStatus({ updatePhase('idle', {
inFlight: false, inFlight: false,
currentRunId: null,
}); });
scheduleNextTick(nextDelayMs); scheduleNextTick(nextDelayMs);
} }
@@ -768,7 +1000,7 @@ async function runTick() {
function start() { function start() {
if (!enabled) { if (!enabled) {
logger.info('LLM commentary disabled'); logger.info('LLM commentary disabled');
updateStatus({ updatePhase('disabled', {
running: false, running: false,
lastOutcome: 'disabled', lastOutcome: 'disabled',
lastReason: 'llmCommentary.enabled is false', lastReason: 'llmCommentary.enabled is false',
@@ -777,7 +1009,7 @@ function start() {
} }
if (!model || !ollamaUrl) { if (!model || !ollamaUrl) {
logger.warn('LLM commentary disabled; model or ollamaUrl missing'); logger.warn('LLM commentary disabled; model or ollamaUrl missing');
updateStatus({ updatePhase('disabled', {
running: false, running: false,
lastOutcome: 'disabled', lastOutcome: 'disabled',
lastReason: 'model or ollama server missing', lastReason: 'model or ollama server missing',
@@ -785,7 +1017,7 @@ function start() {
return; return;
} }
logger.info('LLM commentary enabled', { model, ollamaUrl, frequencyMs, promptPath: PROMPT_PATH }); logger.info('LLM commentary enabled', { model, ollamaUrl, frequencyMs, promptPath: PROMPT_PATH });
updateStatus({ updatePhase('idle', {
running: true, running: true,
lastOutcome: 'running', lastOutcome: 'running',
lastReason: null, lastReason: null,
@@ -795,14 +1027,15 @@ function start() {
io.on('connection', (socket) => { io.on('connection', (socket) => {
emitStatusToSocket(socket); emitStatusToSocket(socket);
socket.on('llm:control', ({ action } = {}, cb = () => {}) => { socket.on('llm:control', ({ controls } = {}, cb = () => {}) => {
if (!isAdminSocket(socket)) { if (!isAdminSocket(socket)) {
cb({ error: 'Not authorized' }); cb({ error: 'Not authorized' });
return; return;
} }
if (action === 'clearHistory') { const command = controls?.action || null;
if (command === 'clearHistory') {
clearRuntimeHistory(); clearRuntimeHistory();
cb({ success: true, status }); cb({ success: true, state: buildAdminState() });
return; return;
} }
cb({ error: 'Unknown llm control action' }); cb({ error: 'Unknown llm control action' });
+126 -84
View File
@@ -22,7 +22,7 @@ export default function AdminPanel() {
rebootServer, rebootServer,
llmControl, llmControl,
adminLogs, adminLogs,
llmCommentaryStatus, llmCommentaryState,
} = useSession(); } = useSession();
const roster = useMemo(() => session?.roster ?? [], [session?.roster]); const roster = useMemo(() => session?.roster ?? [], [session?.roster]);
const [lockStates, setLockStates] = useState({}); const [lockStates, setLockStates] = useState({});
@@ -256,7 +256,7 @@ export default function AdminPanel() {
/> />
<ReplaySnapshotHealth health={health} /> <ReplaySnapshotHealth health={health} />
<LlmCommentaryPanel <LlmCommentaryPanel
status={llmCommentaryStatus} state={llmCommentaryState}
onClearHistory={handleClearLlmHistory} onClearHistory={handleClearLlmHistory}
clearingHistory={clearingLlmHistory} clearingHistory={clearingLlmHistory}
/> />
@@ -265,8 +265,9 @@ export default function AdminPanel() {
); );
} }
function LlmCommentaryPanel({ status, onClearHistory, clearingHistory }) { function LlmCommentaryPanel({ state, onClearHistory, clearingHistory }) {
if (!status) { const [selectedRunId, setSelectedRunId] = useState(null);
if (!state) {
return ( return (
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="panel-muted text-xs uppercase">LLM Commentary</div> <div className="panel-muted text-xs uppercase">LLM Commentary</div>
@@ -274,45 +275,42 @@ function LlmCommentaryPanel({ status, onClearHistory, clearingHistory }) {
</div> </div>
); );
} }
const runtime = state.runtime || {};
const lastTickAt = status.lastTickAt ? new Date(status.lastTickAt).toLocaleString() : 'never'; const counters = state.counters || {};
const nextRunAt = status.nextRunAt ? new Date(status.nextRunAt).toLocaleString() : 'n/a'; const timings = state.timings || {};
const lastPostedAt = status.lastPostedAt ? new Date(status.lastPostedAt).toLocaleString() : 'never'; const input = state.input || {};
const summary = status.lastSnapshotSummary || {}; const output = state.output || {};
const statusColor = const errors = state.errors || {};
status.lastOutcome === 'failed' const history = Array.isArray(state.history) ? state.history : [];
? 'text-red-300' const selectedRun =
: status.lastOutcome === 'posted' history.find((run) => run.runId === selectedRunId) || (history.length ? history[history.length - 1] : null);
? 'text-emerald-300' const largeIndicator = buildLlmLargeIndicatorFromState(state);
: 'text-slate-300'; const conversationRows = buildLlmConversationRowsFromMessages(input.modelMessages, output.raw);
const largeIndicator = buildLlmLargeIndicator(status);
const conversationRows = buildLlmConversationRows(status);
const statPills = [ const statPills = [
{ label: 'enabled', value: status.enabled ? 'yes' : 'no' }, { label: 'running', value: runtime.running ? 'yes' : 'no' },
{ label: 'running', value: status.running ? 'yes' : 'no' }, { label: 'in flight', value: runtime.inFlight ? 'yes' : 'no' },
{ label: 'in flight', value: status.inFlight ? 'yes' : 'no' }, { label: 'phase', value: runtime.phase || '--' },
{ label: 'model', value: status.model || '--' }, { label: 'run id', value: runtime.currentRunId ?? '--' },
{ label: 'server', value: status.ollamaUrl || '--' }, { label: 'tick count', value: runtime.tickCount ?? 0 },
{ label: 'frequency', value: `${status.frequencyMs} ms` },
{ label: 'tick count', value: status.tickCount ?? 0 },
{ label: 'skip streak', value: status.skipStreak ?? 0 },
{ label: 'last gen', value: status.lastGenerationMs != null ? `${status.lastGenerationMs} ms` : '--' },
{ label: 'avg gen', value: status.avgGenerationMs != null ? `${status.avgGenerationMs} ms` : '--' },
{ label: 'gen count', value: status.generationCount ?? 0 },
{ label: 'last outcome', value: status.lastOutcome || '--' },
{ label: 'last reason', value: status.lastReason || '--' },
{ label: 'last tick', value: lastTickAt },
{ label: 'next run', value: nextRunAt },
{ label: 'last posted', value: lastPostedAt },
{ label: 'prompt chars', value: status.lastPromptChars ?? 0 },
{ label: 'cleared count', value: status.clearCount ?? 0 },
{ {
label: 'last cleared', label: 'last tick',
value: status.lastClearedAt ? new Date(status.lastClearedAt).toLocaleString() : 'never', value: runtime.lastTickAt ? new Date(runtime.lastTickAt).toLocaleString() : 'never',
}, },
{ label: 'snapshot active drivers', value: summary.activeDrivers ?? 0 }, {
{ label: 'snapshot rovers', value: summary.rovers ?? 0 }, label: 'next run',
{ label: 'snapshot chat msgs', value: summary.chatMessages ?? 0 }, value: runtime.nextRunAt ? new Date(runtime.nextRunAt).toLocaleString() : 'n/a',
},
{ label: 'outcome', value: runtime.outcome || '--' },
{ label: 'reason', value: runtime.reason || '--' },
{ label: 'skip streak', value: counters.skipStreak ?? 0 },
{ label: 'clear count', value: counters.clearCount ?? 0 },
{ label: 'last gen', value: timings.lastGenerationMs != null ? `${timings.lastGenerationMs} ms` : '--' },
{ label: 'avg gen', value: timings.avgGenerationMs != null ? `${timings.avgGenerationMs} ms` : '--' },
{ label: 'gen count', value: timings.generationCount ?? 0 },
{ label: 'prompt chars', value: counters.promptChars ?? 0 },
{ label: 'active drivers', value: counters.snapshotSummary?.activeDrivers ?? 0 },
{ label: 'snapshot rovers', value: counters.snapshotSummary?.rovers ?? 0 },
{ label: 'snapshot chat', value: counters.snapshotSummary?.chatMessages ?? 0 },
]; ];
return ( return (
@@ -336,107 +334,151 @@ function LlmCommentaryPanel({ status, onClearHistory, clearingHistory }) {
{statPills.map((pill) => ( {statPills.map((pill) => (
<span <span
key={pill.label} key={pill.label}
className={`rounded border px-0.5 py-0.25 text-[0.72rem] leading-tight ${ className="rounded border border-slate-600/60 bg-slate-800/70 px-0.5 py-0.25 text-[0.72rem] leading-tight text-slate-200"
pill.label === 'last outcome'
? `${statusColor} border-slate-500/40 bg-slate-800/70`
: 'border-slate-600/60 bg-slate-800/70 text-slate-200'
}`}
> >
{pill.label}: {pill.value} {pill.label}: {pill.value}
</span> </span>
))} ))}
</div> </div>
{status.lastError ? ( {errors.message ? (
<div className="surface text-xs text-red-300 break-words"> <div className="surface text-xs text-red-300 break-words">
Error: {status.lastError} Error: {errors.message}
</div> </div>
) : null} ) : null}
{status.lastErrorDetails ? ( {errors.details ? (
<details className="surface text-xs text-red-200"> <details className="surface text-xs text-red-200">
<summary className="cursor-pointer select-none text-red-300">Failure details</summary> <summary className="cursor-pointer select-none text-red-300">Failure details</summary>
<pre className="mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-red-200"> <pre className="mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-red-200">
{JSON.stringify(status.lastErrorDetails, null, 2)} {JSON.stringify(errors.details, null, 2)}
</pre> </pre>
</details> </details>
) : null} ) : null}
{status.lastGeneratedText ? ( {output.generated ? (
<div className="surface text-xs text-slate-200 break-words"> <div className="surface text-xs text-slate-200 break-words">
Generated: {status.lastGeneratedText} Generated: {output.generated}
</div> </div>
) : null} ) : null}
{status.lastPostedText ? ( {output.posted ? (
<div className="surface text-xs text-emerald-200 break-words"> <div className="surface text-xs text-emerald-200 break-words">
Posted: {status.lastPostedText} Posted: {output.posted}
</div> </div>
) : null} ) : null}
<div className="grid gap-0.5 md:grid-cols-2">
<div className="space-y-0.5">
<div className="panel-muted text-xs uppercase">Live Input Conversation</div>
<div className="surface max-h-72 space-y-0.5 overflow-y-auto">
{conversationRows.length ? (
conversationRows.map((row) => <ChatMessageRow key={row.id} message={row.message} />)
) : (
<div className="text-xs text-slate-300">No model conversation captured yet.</div>
)}
</div>
</div>
<div className="space-y-0.5">
<div className="panel-muted text-xs uppercase">Output + Error</div>
<div className="surface space-y-0.5 text-xs text-slate-200">
<div>Raw output: {output.raw?.trim() ? output.raw : '<none>'}</div>
<div>Posted: {output.posted || '<none>'}</div>
<div>
Output at:{' '}
{output.modelOutputAt ? new Date(output.modelOutputAt).toLocaleString() : 'n/a'}
</div>
<div>
Failed at: {errors.failedAt ? new Date(errors.failedAt).toLocaleString() : 'n/a'}
</div>
</div>
</div>
</div>
<details className="surface text-xs text-slate-200"> <details className="surface text-xs text-slate-200">
<summary className="cursor-pointer select-none text-slate-300">Most recent system prompt</summary> <summary className="cursor-pointer select-none text-slate-300">Full Monitor Payload</summary>
<pre className="mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-slate-200"> <pre className="mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-slate-200">
{status.lastSystemPrompt || 'No prompt read yet.'} {JSON.stringify(state, null, 2)}
</pre>
</details>
<details className="surface text-xs text-slate-200">
<summary className="cursor-pointer select-none text-slate-300">Most recent info snapshot</summary>
<pre className="mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-slate-200">
{status.lastInfoSnapshot
? JSON.stringify(status.lastInfoSnapshot, null, 2)
: 'No snapshot captured yet.'}
</pre> </pre>
</details> </details>
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="panel-muted text-xs uppercase">Most recent LLM conversation</div> <div className="panel-muted text-xs uppercase">Recent Runs</div>
<div className="surface max-h-72 space-y-0.5 overflow-y-auto"> <div className="surface max-h-52 space-y-0.5 overflow-y-auto text-xs">
{conversationRows.length ? ( {history.length ? (
conversationRows.map((row) => <ChatMessageRow key={row.id} message={row.message} />) history
.slice()
.reverse()
.map((run) => (
<button
key={run.runId}
type="button"
onClick={() => setSelectedRunId(run.runId)}
className={`w-full text-left surface ${
selectedRun?.runId === run.runId ? 'border border-sky-400/50' : ''
}`}
>
<span className="text-slate-300">#{run.runId}</span>{' '}
<span className="text-slate-200">{run.outcome || run.phase || '--'}</span>{' '}
<span className="text-slate-400">{run.durationMs != null ? `${run.durationMs}ms` : '--'}</span>{' '}
<span className="text-slate-500">{run.reason || ''}</span>
</button>
))
) : ( ) : (
<div className="text-xs text-slate-300">No model conversation captured yet.</div> <div className="text-slate-300">No runs recorded yet.</div>
)} )}
</div> </div>
</div> </div>
{selectedRun ? (
<details className="surface text-xs text-slate-200" open>
<summary className="cursor-pointer select-none text-slate-300">
Run #{selectedRun.runId} details
</summary>
<pre className="mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-slate-200">
{JSON.stringify(selectedRun, null, 2)}
</pre>
</details>
) : null}
</div> </div>
); );
} }
function buildLlmLargeIndicator(status) { function buildLlmLargeIndicatorFromState(state) {
if (status?.inFlight) { const runtime = state?.runtime || {};
const output = state?.output || {};
const errors = state?.errors || {};
if (runtime.inFlight) {
return { return {
label: 'IN FLIGHT', label: 'IN FLIGHT',
detail: 'Generating commentary now', detail: runtime.reason || 'Generating commentary now',
className: 'border-amber-400/60 bg-amber-700/20 text-amber-200', className: 'border-amber-400/60 bg-amber-700/20 text-amber-200',
}; };
} }
if (status?.lastOutcome === 'posted') { if (runtime.outcome === 'posted') {
return { return {
label: 'POSTED', label: 'POSTED',
detail: status?.lastPostedText ? `Last: ${status.lastPostedText}` : 'Commentary posted', detail: output.posted ? `Last: ${output.posted}` : 'Commentary posted',
className: 'border-emerald-400/60 bg-emerald-700/20 text-emerald-200', className: 'border-emerald-400/60 bg-emerald-700/20 text-emerald-200',
}; };
} }
if (status?.lastOutcome === 'skipped') { if (runtime.outcome === 'skipped') {
return { return {
label: 'SKIPPED', label: 'SKIPPED',
detail: status?.lastReason || 'Model chose to skip', detail: runtime.reason || 'Model chose to skip',
className: 'border-slate-400/60 bg-slate-700/30 text-slate-200', className: 'border-slate-400/60 bg-slate-700/30 text-slate-200',
}; };
} }
if (status?.lastOutcome === 'failed') { if (runtime.outcome === 'failed') {
return { return {
label: 'FAILED', label: 'FAILED',
detail: status?.lastError || status?.lastReason || 'Tick failed', detail: errors.message || runtime.reason || 'Tick failed',
className: 'border-red-400/60 bg-red-700/20 text-red-200', className: 'border-red-400/60 bg-red-700/20 text-red-200',
}; };
} }
return { return {
label: status?.running ? 'IDLE' : 'STOPPED', label: runtime.running ? 'IDLE' : 'STOPPED',
detail: status?.lastReason || 'Waiting for next tick', detail: runtime.reason || 'Waiting for next tick',
className: 'border-sky-400/50 bg-sky-700/20 text-sky-200', className: 'border-sky-400/50 bg-sky-700/20 text-sky-200',
}; };
} }
function buildLlmConversationRows(status) { function buildLlmConversationRowsFromMessages(modelMessages, rawOutput) {
const now = Date.now(); const now = Date.now();
const modelMessages = Array.isArray(status?.lastModelMessages) ? status.lastModelMessages : []; const messages = Array.isArray(modelMessages) ? modelMessages : [];
const rows = modelMessages.map((entry, index) => { const rows = messages.map((entry, index) => {
const role = String(entry?.role || '').toLowerCase(); const role = String(entry?.role || '').toLowerCase();
const content = const content =
typeof entry?.content === 'string' ? entry.content : JSON.stringify(entry?.content ?? null, null, 2); typeof entry?.content === 'string' ? entry.content : JSON.stringify(entry?.content ?? null, null, 2);
@@ -459,8 +501,8 @@ function buildLlmConversationRows(status) {
}, },
}; };
}); });
if (status?.lastModelRawOutput != null) { if (rawOutput != null) {
const raw = String(status.lastModelRawOutput); const raw = String(rawOutput);
rows.push({ rows.push({
id: 'llm-output', id: 'llm-output',
message: { message: {
+14 -6
View File
@@ -8,6 +8,7 @@ const SessionContext = createContext({
session: null, session: null,
logs: [], logs: [],
adminLogs: [], adminLogs: [],
llmCommentaryState: null,
llmCommentaryStatus: null, llmCommentaryStatus: null,
login: async () => {}, login: async () => {},
setRole: async () => {}, setRole: async () => {},
@@ -48,6 +49,7 @@ export function SessionProvider({ children }) {
const [session, setSession] = useState(null); const [session, setSession] = useState(null);
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [adminLogs, setAdminLogs] = useState([]); const [adminLogs, setAdminLogs] = useState([]);
const [llmCommentaryState, setLlmCommentaryState] = useState(null);
const [llmCommentaryStatus, setLlmCommentaryStatus] = useState(null); const [llmCommentaryStatus, setLlmCommentaryStatus] = useState(null);
const [alerts, setAlerts] = useState([]); const [alerts, setAlerts] = useState([]);
const [connected, setConnected] = useState(socket.connected); const [connected, setConnected] = useState(socket.connected);
@@ -79,15 +81,19 @@ export function SessionProvider({ children }) {
function handleAdminLogEntry(entry) { function handleAdminLogEntry(entry) {
setAdminLogs((prev) => [...prev.slice(-199), entry]); setAdminLogs((prev) => [...prev.slice(-199), entry]);
} }
function handleLlmCommentaryStatus(payload = null) { function handleLlmState(payload = null) {
setLlmCommentaryStatus(payload && typeof payload === 'object' ? payload : null); const state = payload && typeof payload === 'object' ? payload : null;
const nextStatus =
state?.debug?.status && typeof state.debug.status === 'object' ? state.debug.status : null;
setLlmCommentaryState(state);
setLlmCommentaryStatus(nextStatus);
} }
socket.on('session:sync', handleSession); socket.on('session:sync', handleSession);
socket.on('log:init', handleLogInit); socket.on('log:init', handleLogInit);
socket.on('log:entry', handleLogEntry); socket.on('log:entry', handleLogEntry);
socket.on('adminlog:init', handleAdminLogInit); socket.on('adminlog:init', handleAdminLogInit);
socket.on('adminlog:entry', handleAdminLogEntry); socket.on('adminlog:entry', handleAdminLogEntry);
socket.on('llmCommentary:status', handleLlmCommentaryStatus); socket.on('llm:state', handleLlmState);
socket.on('alert:new', (payload = {}) => { socket.on('alert:new', (payload = {}) => {
setAlerts((prev) => [ setAlerts((prev) => [
...prev.slice(-49), ...prev.slice(-49),
@@ -103,7 +109,7 @@ export function SessionProvider({ children }) {
socket.off('log:entry', handleLogEntry); socket.off('log:entry', handleLogEntry);
socket.off('adminlog:init', handleAdminLogInit); socket.off('adminlog:init', handleAdminLogInit);
socket.off('adminlog:entry', handleAdminLogEntry); socket.off('adminlog:entry', handleAdminLogEntry);
socket.off('llmCommentary:status', handleLlmCommentaryStatus); socket.off('llm:state', handleLlmState);
socket.off('alert:new'); socket.off('alert:new');
}; };
}, [socket]); }, [socket]);
@@ -130,7 +136,8 @@ export function SessionProvider({ children }) {
rebootRover: (roverId) => rebootRover: (roverId) =>
emitWithAck('command', { roverId, type: 'reboot', data: { reboot: {} } }), emitWithAck('command', { roverId, type: 'reboot', data: { reboot: {} } }),
rebootServer: () => emitWithAck('server:reboot'), rebootServer: () => emitWithAck('server:reboot'),
llmControl: (action, payload = {}) => emitWithAck('llm:control', { action, ...payload }), llmControl: (action, controls = {}) =>
emitWithAck('llm:control', { controls: { action, ...controls } }),
pushAlert: (alert) => pushAlert: (alert) =>
setAlerts((prev) => [ setAlerts((prev) => [
...prev.slice(-49), ...prev.slice(-49),
@@ -146,11 +153,12 @@ export function SessionProvider({ children }) {
session, session,
logs, logs,
adminLogs, adminLogs,
llmCommentaryState,
llmCommentaryStatus, llmCommentaryStatus,
alerts, alerts,
...actions, ...actions,
}), }),
[actions, adminLogs, alerts, connected, llmCommentaryStatus, logs, session], [actions, adminLogs, alerts, connected, llmCommentaryState, llmCommentaryStatus, logs, session],
); );
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>; return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;