mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
newstufff
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
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-CS__Wyws.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C_i0uH-D.css">
|
||||
<script type="module" crossorigin src="/assets/index-DKDSGdkZ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DzeZhrlZ.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -22,6 +22,7 @@ const ACTIVITY_WINDOW_MS = 30000;
|
||||
const ACTIVITY_BUCKET_MS = 1000;
|
||||
const SELF_TALK_WINDOW_MS = 30 * 60 * 1000;
|
||||
const MAX_CONTEXT_EVENTS = 10;
|
||||
const MAX_RUN_HISTORY = 30;
|
||||
|
||||
const config = loadConfig();
|
||||
const commentaryConfig = config.llmCommentary || {};
|
||||
@@ -40,6 +41,8 @@ let generationCount = 0;
|
||||
let generationTotalMs = 0;
|
||||
let contextResetAt = Date.now();
|
||||
let clearCount = 0;
|
||||
let runHistory = [];
|
||||
let currentRun = null;
|
||||
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
|
||||
|
||||
@@ -65,14 +68,22 @@ let status = {
|
||||
lastTickAt: null,
|
||||
lastOutcome: null,
|
||||
lastReason: null,
|
||||
phase: 'idle',
|
||||
phaseAt: Date.now(),
|
||||
currentRunId: null,
|
||||
lastError: null,
|
||||
lastErrorDetails: null,
|
||||
lastFailedAt: null,
|
||||
lastPromptReadAt: null,
|
||||
lastPromptChars: 0,
|
||||
lastSystemPrompt: null,
|
||||
lastInfoSnapshot: null,
|
||||
lastModelMessages: null,
|
||||
lastModelInputAt: null,
|
||||
lastModelInputTickId: null,
|
||||
lastModelRawOutput: null,
|
||||
lastModelOutputAt: null,
|
||||
lastModelOutputTickId: null,
|
||||
lastSnapshotSummary: null,
|
||||
lastClearedAt: contextResetAt,
|
||||
clearCount,
|
||||
@@ -86,6 +97,120 @@ let status = {
|
||||
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) {
|
||||
if (!socket) return false;
|
||||
const role = getRole(socket);
|
||||
@@ -94,13 +219,14 @@ function isAdminSocket(socket) {
|
||||
|
||||
function emitStatusToSocket(socket) {
|
||||
if (!socket || !isAdminSocket(socket)) return;
|
||||
socket.emit('llmCommentary:status', status);
|
||||
socket.emit('llm:state', buildAdminState());
|
||||
}
|
||||
|
||||
function emitStatusToAdmins() {
|
||||
const payload = buildAdminState();
|
||||
io.sockets.sockets.forEach((socket) => {
|
||||
if (!isAdminSocket(socket)) return;
|
||||
socket.emit('llmCommentary:status', status);
|
||||
socket.emit('llm:state', payload);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -189,24 +315,34 @@ function clearRuntimeHistory() {
|
||||
skipStreak = 0;
|
||||
generationCount = 0;
|
||||
generationTotalMs = 0;
|
||||
runHistory = [];
|
||||
currentRun = null;
|
||||
roverActivity.clear();
|
||||
lastRoverStateById.clear();
|
||||
updateStatus({
|
||||
lastClearedAt: contextResetAt,
|
||||
clearCount,
|
||||
skipStreak,
|
||||
phase: 'idle',
|
||||
phaseAt: Date.now(),
|
||||
currentRunId: null,
|
||||
lastGenerationMs: null,
|
||||
avgGenerationMs: null,
|
||||
generationCount,
|
||||
lastInfoSnapshot: null,
|
||||
lastModelMessages: null,
|
||||
lastModelInputAt: null,
|
||||
lastModelInputTickId: null,
|
||||
lastModelRawOutput: null,
|
||||
lastModelOutputAt: null,
|
||||
lastModelOutputTickId: null,
|
||||
lastSnapshotSummary: null,
|
||||
lastGeneratedText: null,
|
||||
lastPostedText: null,
|
||||
lastPostedAt: null,
|
||||
lastError: null,
|
||||
lastErrorDetails: null,
|
||||
lastFailedAt: null,
|
||||
lastOutcome: 'cleared',
|
||||
lastReason: 'admin requested clear history',
|
||||
});
|
||||
@@ -569,10 +705,7 @@ async function readSystemPrompt() {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
async function generateCommentary(systemPrompt, snapshot) {
|
||||
if (!ollamaClient) {
|
||||
throw new Error('Ollama client unavailable');
|
||||
}
|
||||
function buildModelMessages(systemPrompt, snapshot) {
|
||||
const messages = [];
|
||||
messages.push({ role: 'system', content: systemPrompt });
|
||||
messages.push({
|
||||
@@ -608,10 +741,13 @@ async function generateCommentary(systemPrompt, snapshot) {
|
||||
role: 'user',
|
||||
content: formatSnapshotFinalMessage(snapshot?.current_snapshot || {}),
|
||||
});
|
||||
updateStatus({
|
||||
lastModelMessages: messages,
|
||||
});
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function generateCommentary(messages) {
|
||||
if (!ollamaClient) {
|
||||
throw new Error('Ollama client unavailable');
|
||||
}
|
||||
const payload = await ollamaClient.chat({
|
||||
model,
|
||||
stream: false,
|
||||
@@ -649,16 +785,19 @@ async function runTick() {
|
||||
let nextDelayMs = defaultTickDelayMs();
|
||||
tickCount += 1;
|
||||
const tickId = tickCount;
|
||||
updateStatus({
|
||||
updatePhase('tick_started', {
|
||||
tickCount,
|
||||
inFlight: true,
|
||||
currentRunId: tickId,
|
||||
lastTickAt: Date.now(),
|
||||
lastError: null,
|
||||
lastErrorDetails: null,
|
||||
});
|
||||
if (inFlight) {
|
||||
logger.info('Commentary tick skipped; previous tick still running', { tickId });
|
||||
updateStatus({
|
||||
updatePhase('idle', {
|
||||
inFlight: false,
|
||||
currentRunId: null,
|
||||
lastOutcome: 'skipped',
|
||||
lastReason: 'previous tick still running',
|
||||
});
|
||||
@@ -679,19 +818,61 @@ async function runTick() {
|
||||
tickId,
|
||||
...snapshotSummary,
|
||||
});
|
||||
updateStatus({
|
||||
startRunRecord(tickId, snapshotSummary);
|
||||
patchCurrentRun({
|
||||
phase: 'snapshot_ready',
|
||||
summary: snapshotSummary,
|
||||
input: {
|
||||
...(currentRun?.input || {}),
|
||||
infoSnapshot: snapshot,
|
||||
},
|
||||
});
|
||||
updatePhase('snapshot_ready', {
|
||||
lastSnapshotSummary: snapshotSummary,
|
||||
lastInfoSnapshot: snapshot,
|
||||
});
|
||||
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 modelResult = await generateCommentary(systemPrompt, snapshot);
|
||||
const modelResult = await generateCommentary(modelMessages);
|
||||
const generationMs = Math.max(0, Date.now() - generationStartMs);
|
||||
generationCount += 1;
|
||||
generationTotalMs += generationMs;
|
||||
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 || '',
|
||||
lastModelOutputAt: modelOutputAt,
|
||||
lastModelOutputTickId: tickId,
|
||||
lastGenerationMs: generationMs,
|
||||
avgGenerationMs,
|
||||
generationCount,
|
||||
@@ -700,17 +881,26 @@ async function runTick() {
|
||||
if (!text) {
|
||||
logger.info('Commentary tick produced SKIP/empty output', { tickId });
|
||||
skipStreak += 1;
|
||||
updateStatus({
|
||||
patchCurrentRun({
|
||||
phase: 'decision_skip',
|
||||
outcome: 'skipped',
|
||||
reason: modelResult?.raw?.trim() ? 'model returned SKIP' : 'model returned empty',
|
||||
});
|
||||
updatePhase('decision_skip', {
|
||||
lastOutcome: 'skipped',
|
||||
lastReason: modelResult?.raw?.trim() ? 'model returned SKIP' : 'model returned empty',
|
||||
skipStreak,
|
||||
lastGeneratedText: null,
|
||||
});
|
||||
finalizeRunRecord({
|
||||
outcome: 'skipped',
|
||||
reason: modelResult?.raw?.trim() ? 'model returned SKIP' : 'model returned empty',
|
||||
});
|
||||
// Immediate retry after model skip.
|
||||
nextDelayMs = 0;
|
||||
return;
|
||||
}
|
||||
updateStatus({
|
||||
updatePhase('decision_post', {
|
||||
lastGeneratedText: text,
|
||||
});
|
||||
const recentBotMessages = getRecentMessages(80, { includeSystem: true })
|
||||
@@ -723,11 +913,20 @@ async function runTick() {
|
||||
if (duplicate) {
|
||||
logger.info('Commentary tick skipped duplicate output', { tickId, text });
|
||||
skipStreak += 1;
|
||||
updateStatus({
|
||||
patchCurrentRun({
|
||||
phase: 'decision_skip',
|
||||
outcome: 'skipped',
|
||||
reason: 'duplicate text',
|
||||
});
|
||||
updatePhase('decision_skip', {
|
||||
lastOutcome: 'skipped',
|
||||
lastReason: 'duplicate text',
|
||||
skipStreak,
|
||||
});
|
||||
finalizeRunRecord({
|
||||
outcome: 'skipped',
|
||||
reason: 'duplicate text',
|
||||
});
|
||||
// Immediate retry after a skip outcome.
|
||||
nextDelayMs = 0;
|
||||
return;
|
||||
@@ -735,13 +934,27 @@ async function runTick() {
|
||||
sendSystemMessage(text);
|
||||
logger.info('Commentary message posted', { tickId, text });
|
||||
skipStreak = 0;
|
||||
updateStatus({
|
||||
patchCurrentRun({
|
||||
phase: 'posted',
|
||||
outcome: 'posted',
|
||||
reason: null,
|
||||
output: {
|
||||
...(currentRun?.output || {}),
|
||||
posted: text,
|
||||
postedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
updatePhase('posted', {
|
||||
lastOutcome: 'posted',
|
||||
lastReason: null,
|
||||
skipStreak,
|
||||
lastPostedText: text,
|
||||
lastPostedAt: Date.now(),
|
||||
});
|
||||
finalizeRunRecord({
|
||||
outcome: 'posted',
|
||||
reason: null,
|
||||
});
|
||||
} catch (err) {
|
||||
const failure = buildFailureInfo(err);
|
||||
logger.warn('Commentary tick failed', {
|
||||
@@ -750,16 +963,35 @@ async function runTick() {
|
||||
error: failure.message,
|
||||
details: failure.details,
|
||||
});
|
||||
updateStatus({
|
||||
patchCurrentRun({
|
||||
phase: 'failed',
|
||||
outcome: 'failed',
|
||||
reason: failure.reason,
|
||||
errors: {
|
||||
message: failure.message,
|
||||
details: failure.details,
|
||||
},
|
||||
});
|
||||
updatePhase('failed', {
|
||||
lastOutcome: 'failed',
|
||||
lastReason: failure.reason,
|
||||
lastError: failure.message,
|
||||
lastErrorDetails: failure.details,
|
||||
lastFailedAt: Date.now(),
|
||||
});
|
||||
finalizeRunRecord({
|
||||
outcome: 'failed',
|
||||
reason: failure.reason,
|
||||
errors: {
|
||||
message: failure.message,
|
||||
details: failure.details,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
inFlight = false;
|
||||
updateStatus({
|
||||
updatePhase('idle', {
|
||||
inFlight: false,
|
||||
currentRunId: null,
|
||||
});
|
||||
scheduleNextTick(nextDelayMs);
|
||||
}
|
||||
@@ -768,7 +1000,7 @@ async function runTick() {
|
||||
function start() {
|
||||
if (!enabled) {
|
||||
logger.info('LLM commentary disabled');
|
||||
updateStatus({
|
||||
updatePhase('disabled', {
|
||||
running: false,
|
||||
lastOutcome: 'disabled',
|
||||
lastReason: 'llmCommentary.enabled is false',
|
||||
@@ -777,7 +1009,7 @@ function start() {
|
||||
}
|
||||
if (!model || !ollamaUrl) {
|
||||
logger.warn('LLM commentary disabled; model or ollamaUrl missing');
|
||||
updateStatus({
|
||||
updatePhase('disabled', {
|
||||
running: false,
|
||||
lastOutcome: 'disabled',
|
||||
lastReason: 'model or ollama server missing',
|
||||
@@ -785,7 +1017,7 @@ function start() {
|
||||
return;
|
||||
}
|
||||
logger.info('LLM commentary enabled', { model, ollamaUrl, frequencyMs, promptPath: PROMPT_PATH });
|
||||
updateStatus({
|
||||
updatePhase('idle', {
|
||||
running: true,
|
||||
lastOutcome: 'running',
|
||||
lastReason: null,
|
||||
@@ -795,14 +1027,15 @@ function start() {
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
emitStatusToSocket(socket);
|
||||
socket.on('llm:control', ({ action } = {}, cb = () => {}) => {
|
||||
socket.on('llm:control', ({ controls } = {}, cb = () => {}) => {
|
||||
if (!isAdminSocket(socket)) {
|
||||
cb({ error: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
if (action === 'clearHistory') {
|
||||
const command = controls?.action || null;
|
||||
if (command === 'clearHistory') {
|
||||
clearRuntimeHistory();
|
||||
cb({ success: true, status });
|
||||
cb({ success: true, state: buildAdminState() });
|
||||
return;
|
||||
}
|
||||
cb({ error: 'Unknown llm control action' });
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function AdminPanel() {
|
||||
rebootServer,
|
||||
llmControl,
|
||||
adminLogs,
|
||||
llmCommentaryStatus,
|
||||
llmCommentaryState,
|
||||
} = useSession();
|
||||
const roster = useMemo(() => session?.roster ?? [], [session?.roster]);
|
||||
const [lockStates, setLockStates] = useState({});
|
||||
@@ -256,7 +256,7 @@ export default function AdminPanel() {
|
||||
/>
|
||||
<ReplaySnapshotHealth health={health} />
|
||||
<LlmCommentaryPanel
|
||||
status={llmCommentaryStatus}
|
||||
state={llmCommentaryState}
|
||||
onClearHistory={handleClearLlmHistory}
|
||||
clearingHistory={clearingLlmHistory}
|
||||
/>
|
||||
@@ -265,8 +265,9 @@ export default function AdminPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
function LlmCommentaryPanel({ status, onClearHistory, clearingHistory }) {
|
||||
if (!status) {
|
||||
function LlmCommentaryPanel({ state, onClearHistory, clearingHistory }) {
|
||||
const [selectedRunId, setSelectedRunId] = useState(null);
|
||||
if (!state) {
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<div className="panel-muted text-xs uppercase">LLM Commentary</div>
|
||||
@@ -274,45 +275,42 @@ function LlmCommentaryPanel({ status, onClearHistory, clearingHistory }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const lastTickAt = status.lastTickAt ? new Date(status.lastTickAt).toLocaleString() : 'never';
|
||||
const nextRunAt = status.nextRunAt ? new Date(status.nextRunAt).toLocaleString() : 'n/a';
|
||||
const lastPostedAt = status.lastPostedAt ? new Date(status.lastPostedAt).toLocaleString() : 'never';
|
||||
const summary = status.lastSnapshotSummary || {};
|
||||
const statusColor =
|
||||
status.lastOutcome === 'failed'
|
||||
? 'text-red-300'
|
||||
: status.lastOutcome === 'posted'
|
||||
? 'text-emerald-300'
|
||||
: 'text-slate-300';
|
||||
const largeIndicator = buildLlmLargeIndicator(status);
|
||||
const conversationRows = buildLlmConversationRows(status);
|
||||
const runtime = state.runtime || {};
|
||||
const counters = state.counters || {};
|
||||
const timings = state.timings || {};
|
||||
const input = state.input || {};
|
||||
const output = state.output || {};
|
||||
const errors = state.errors || {};
|
||||
const history = Array.isArray(state.history) ? state.history : [];
|
||||
const selectedRun =
|
||||
history.find((run) => run.runId === selectedRunId) || (history.length ? history[history.length - 1] : null);
|
||||
const largeIndicator = buildLlmLargeIndicatorFromState(state);
|
||||
const conversationRows = buildLlmConversationRowsFromMessages(input.modelMessages, output.raw);
|
||||
const statPills = [
|
||||
{ label: 'enabled', value: status.enabled ? 'yes' : 'no' },
|
||||
{ label: 'running', value: status.running ? 'yes' : 'no' },
|
||||
{ label: 'in flight', value: status.inFlight ? 'yes' : 'no' },
|
||||
{ label: 'model', value: status.model || '--' },
|
||||
{ label: 'server', value: status.ollamaUrl || '--' },
|
||||
{ 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: 'running', value: runtime.running ? 'yes' : 'no' },
|
||||
{ label: 'in flight', value: runtime.inFlight ? 'yes' : 'no' },
|
||||
{ label: 'phase', value: runtime.phase || '--' },
|
||||
{ label: 'run id', value: runtime.currentRunId ?? '--' },
|
||||
{ label: 'tick count', value: runtime.tickCount ?? 0 },
|
||||
{
|
||||
label: 'last cleared',
|
||||
value: status.lastClearedAt ? new Date(status.lastClearedAt).toLocaleString() : 'never',
|
||||
label: 'last tick',
|
||||
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: 'snapshot chat msgs', value: summary.chatMessages ?? 0 },
|
||||
{
|
||||
label: 'next run',
|
||||
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 (
|
||||
@@ -336,107 +334,151 @@ function LlmCommentaryPanel({ status, onClearHistory, clearingHistory }) {
|
||||
{statPills.map((pill) => (
|
||||
<span
|
||||
key={pill.label}
|
||||
className={`rounded border px-0.5 py-0.25 text-[0.72rem] leading-tight ${
|
||||
pill.label === 'last outcome'
|
||||
? `${statusColor} border-slate-500/40 bg-slate-800/70`
|
||||
: 'border-slate-600/60 bg-slate-800/70 text-slate-200'
|
||||
}`}
|
||||
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}: {pill.value}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{status.lastError ? (
|
||||
{errors.message ? (
|
||||
<div className="surface text-xs text-red-300 break-words">
|
||||
Error: {status.lastError}
|
||||
Error: {errors.message}
|
||||
</div>
|
||||
) : null}
|
||||
{status.lastErrorDetails ? (
|
||||
{errors.details ? (
|
||||
<details className="surface text-xs text-red-200">
|
||||
<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">
|
||||
{JSON.stringify(status.lastErrorDetails, null, 2)}
|
||||
{JSON.stringify(errors.details, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
) : null}
|
||||
{status.lastGeneratedText ? (
|
||||
{output.generated ? (
|
||||
<div className="surface text-xs text-slate-200 break-words">
|
||||
Generated: {status.lastGeneratedText}
|
||||
Generated: {output.generated}
|
||||
</div>
|
||||
) : null}
|
||||
{status.lastPostedText ? (
|
||||
{output.posted ? (
|
||||
<div className="surface text-xs text-emerald-200 break-words">
|
||||
Posted: {status.lastPostedText}
|
||||
Posted: {output.posted}
|
||||
</div>
|
||||
) : 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">
|
||||
<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">
|
||||
{status.lastSystemPrompt || 'No prompt read yet.'}
|
||||
</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.'}
|
||||
{JSON.stringify(state, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
<div className="space-y-0.5">
|
||||
<div className="panel-muted text-xs uppercase">Most recent LLM 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="panel-muted text-xs uppercase">Recent Runs</div>
|
||||
<div className="surface max-h-52 space-y-0.5 overflow-y-auto text-xs">
|
||||
{history.length ? (
|
||||
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>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
function buildLlmLargeIndicator(status) {
|
||||
if (status?.inFlight) {
|
||||
function buildLlmLargeIndicatorFromState(state) {
|
||||
const runtime = state?.runtime || {};
|
||||
const output = state?.output || {};
|
||||
const errors = state?.errors || {};
|
||||
if (runtime.inFlight) {
|
||||
return {
|
||||
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',
|
||||
};
|
||||
}
|
||||
if (status?.lastOutcome === 'posted') {
|
||||
if (runtime.outcome === 'posted') {
|
||||
return {
|
||||
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',
|
||||
};
|
||||
}
|
||||
if (status?.lastOutcome === 'skipped') {
|
||||
if (runtime.outcome === 'skipped') {
|
||||
return {
|
||||
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',
|
||||
};
|
||||
}
|
||||
if (status?.lastOutcome === 'failed') {
|
||||
if (runtime.outcome === 'failed') {
|
||||
return {
|
||||
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',
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: status?.running ? 'IDLE' : 'STOPPED',
|
||||
detail: status?.lastReason || 'Waiting for next tick',
|
||||
label: runtime.running ? 'IDLE' : 'STOPPED',
|
||||
detail: runtime.reason || 'Waiting for next tick',
|
||||
className: 'border-sky-400/50 bg-sky-700/20 text-sky-200',
|
||||
};
|
||||
}
|
||||
|
||||
function buildLlmConversationRows(status) {
|
||||
function buildLlmConversationRowsFromMessages(modelMessages, rawOutput) {
|
||||
const now = Date.now();
|
||||
const modelMessages = Array.isArray(status?.lastModelMessages) ? status.lastModelMessages : [];
|
||||
const rows = modelMessages.map((entry, index) => {
|
||||
const messages = Array.isArray(modelMessages) ? modelMessages : [];
|
||||
const rows = messages.map((entry, index) => {
|
||||
const role = String(entry?.role || '').toLowerCase();
|
||||
const content =
|
||||
typeof entry?.content === 'string' ? entry.content : JSON.stringify(entry?.content ?? null, null, 2);
|
||||
@@ -459,8 +501,8 @@ function buildLlmConversationRows(status) {
|
||||
},
|
||||
};
|
||||
});
|
||||
if (status?.lastModelRawOutput != null) {
|
||||
const raw = String(status.lastModelRawOutput);
|
||||
if (rawOutput != null) {
|
||||
const raw = String(rawOutput);
|
||||
rows.push({
|
||||
id: 'llm-output',
|
||||
message: {
|
||||
|
||||
@@ -8,6 +8,7 @@ const SessionContext = createContext({
|
||||
session: null,
|
||||
logs: [],
|
||||
adminLogs: [],
|
||||
llmCommentaryState: null,
|
||||
llmCommentaryStatus: null,
|
||||
login: async () => {},
|
||||
setRole: async () => {},
|
||||
@@ -48,6 +49,7 @@ export function SessionProvider({ children }) {
|
||||
const [session, setSession] = useState(null);
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [adminLogs, setAdminLogs] = useState([]);
|
||||
const [llmCommentaryState, setLlmCommentaryState] = useState(null);
|
||||
const [llmCommentaryStatus, setLlmCommentaryStatus] = useState(null);
|
||||
const [alerts, setAlerts] = useState([]);
|
||||
const [connected, setConnected] = useState(socket.connected);
|
||||
@@ -79,15 +81,19 @@ export function SessionProvider({ children }) {
|
||||
function handleAdminLogEntry(entry) {
|
||||
setAdminLogs((prev) => [...prev.slice(-199), entry]);
|
||||
}
|
||||
function handleLlmCommentaryStatus(payload = null) {
|
||||
setLlmCommentaryStatus(payload && typeof payload === 'object' ? payload : null);
|
||||
function handleLlmState(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('log:init', handleLogInit);
|
||||
socket.on('log:entry', handleLogEntry);
|
||||
socket.on('adminlog:init', handleAdminLogInit);
|
||||
socket.on('adminlog:entry', handleAdminLogEntry);
|
||||
socket.on('llmCommentary:status', handleLlmCommentaryStatus);
|
||||
socket.on('llm:state', handleLlmState);
|
||||
socket.on('alert:new', (payload = {}) => {
|
||||
setAlerts((prev) => [
|
||||
...prev.slice(-49),
|
||||
@@ -103,7 +109,7 @@ export function SessionProvider({ children }) {
|
||||
socket.off('log:entry', handleLogEntry);
|
||||
socket.off('adminlog:init', handleAdminLogInit);
|
||||
socket.off('adminlog:entry', handleAdminLogEntry);
|
||||
socket.off('llmCommentary:status', handleLlmCommentaryStatus);
|
||||
socket.off('llm:state', handleLlmState);
|
||||
socket.off('alert:new');
|
||||
};
|
||||
}, [socket]);
|
||||
@@ -130,7 +136,8 @@ export function SessionProvider({ children }) {
|
||||
rebootRover: (roverId) =>
|
||||
emitWithAck('command', { roverId, type: 'reboot', data: { reboot: {} } }),
|
||||
rebootServer: () => emitWithAck('server:reboot'),
|
||||
llmControl: (action, payload = {}) => emitWithAck('llm:control', { action, ...payload }),
|
||||
llmControl: (action, controls = {}) =>
|
||||
emitWithAck('llm:control', { controls: { action, ...controls } }),
|
||||
pushAlert: (alert) =>
|
||||
setAlerts((prev) => [
|
||||
...prev.slice(-49),
|
||||
@@ -146,11 +153,12 @@ export function SessionProvider({ children }) {
|
||||
session,
|
||||
logs,
|
||||
adminLogs,
|
||||
llmCommentaryState,
|
||||
llmCommentaryStatus,
|
||||
alerts,
|
||||
...actions,
|
||||
}),
|
||||
[actions, adminLogs, alerts, connected, llmCommentaryStatus, logs, session],
|
||||
[actions, adminLogs, alerts, connected, llmCommentaryState, llmCommentaryStatus, logs, session],
|
||||
);
|
||||
|
||||
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
|
||||
|
||||
Reference in New Issue
Block a user