Add experiment logs to the monitoring page #48

Merged
0950726 merged 122 commits from feat/experiment-logs into dev 2026-01-28 10:16:00 +00:00
2 changed files with 76 additions and 35 deletions
Showing only changes of commit 108fdeeedc - Show all commits

View File

@@ -2,10 +2,37 @@ import React from 'react';
import styles from './MonitoringPage.module.css'; import styles from './MonitoringPage.module.css';
import useProgramStore from "../../utils/programStore.ts"; import useProgramStore from "../../utils/programStore.ts";
import { GestureControls, SpeechPresets, DirectSpeechInput, StatusList, RobotConnected } from './Components'; import { GestureControls, SpeechPresets, DirectSpeechInput, StatusList, RobotConnected } from './Components';
import { nextPhase, useExperimentLogger, pauseExperiment, playExperiment, resetExperiment, resetPhase } from ".//MonitoringPageAPI.ts" import { nextPhase, useExperimentLogger, pauseExperiment, playExperiment, resetExperiment, resetPhase, type ExperimentStreamData, type GoalUpdate, type TriggerUpdate, type CondNormsStateUpdate, type PhaseUpdate } from ".//MonitoringPageAPI.ts"
import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode.tsx'; import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode.tsx';
// Stream message types are defined in MonitoringPageAPI as `ExperimentStreamData`.
// Types for reduced program items (output from node reducers):
export type ReducedPlanStep = {
id: string;
text?: string;
gesture?: { type: string; name?: string };
goal?: string;
} & Record<string, unknown>;
export type ReducedPlan = { id: string; steps: ReducedPlanStep[] } | "";
export type ReducedGoal = { id: string; name: string; description?: string; can_fail?: boolean; plan?: ReducedPlan };
export type ReducedCondition = {
id: string;
keyword?: string;
emotion?: string;
object?: string;
name?: string;
description?: string;
} & Record<string, unknown>;
export type ReducedTrigger = { id: string; name: string; condition?: ReducedCondition | ""; plan?: ReducedPlan };
export type ReducedNorm = { id: string; label?: string; norm?: string; condition?: ReducedCondition | "" };
const MonitoringPage: React.FC = () => { const MonitoringPage: React.FC = () => {
const getPhaseIds = useProgramStore((s) => s.getPhaseIds); const getPhaseIds = useProgramStore((s) => s.getPhaseIds);
const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase); const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase);
@@ -25,25 +52,27 @@ const MonitoringPage: React.FC = () => {
//see if we reached end node //see if we reached end node
const [isFinished, setIsFinished] = React.useState(false); const [isFinished, setIsFinished] = React.useState(false);
const handleStreamUpdate = React.useCallback((data: any) => { const handleStreamUpdate = React.useCallback((data: ExperimentStreamData) => {
// Check for phase updates // Check for phase updates
if (data.type === 'phase_update' && data.phase_id) { if (data.type === 'phase_update' && data.id) {
if (data.phase_id === "end") { const payload = data as PhaseUpdate;
if (payload.id === "end") {
setIsFinished(true); setIsFinished(true);
} else { } else {
setIsFinished(false); setIsFinished(false);
const allIds = getPhaseIds(); const allIds = getPhaseIds();
const newIndex = allIds.indexOf(data.phase_id); const newIndex = allIds.indexOf(payload.id);
if (newIndex !== -1) { if (newIndex !== -1) {
setPhaseIndex(newIndex); setPhaseIndex(newIndex);
setGoalIndex(0); setGoalIndex(0);
} }
} }
} }
else if (data.type === 'goal_update') { else if (data.type === 'goal_update') {
const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as any[]; const payload = data as GoalUpdate;
const gIndex = currentPhaseGoals.findIndex((g: any) => g.id === data.id); const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as ReducedGoal[];
const gIndex = currentPhaseGoals.findIndex((g: ReducedGoal) => g.id === payload.id);
if (gIndex !== -1) { if (gIndex !== -1) {
//set current goal to the goal that is just started //set current goal to the goal that is just started
@@ -62,30 +91,32 @@ const MonitoringPage: React.FC = () => {
return nextState; return nextState;
}); });
console.log(`Now pursuing goal: ${data.id}. Previous goals marked achieved.`); console.log(`Now pursuing goal: ${payload.id}. Previous goals marked achieved.`);
} }
} }
else if (data.type === 'trigger_update') { else if (data.type === 'trigger_update') {
const payload = data as TriggerUpdate;
setActiveIds((prev) => ({ setActiveIds((prev) => ({
...prev, ...prev,
[data.id]: data.achieved // data.id is de key, achieved is true/false [payload.id]: payload.achieved
})); }));
} }
else if (data.type === 'cond_norms_state_update') { else if (data.type === 'cond_norms_state_update') {
const payload = data as CondNormsStateUpdate;
setActiveIds((prev) => { setActiveIds((prev) => {
const nextState = { ...prev }; const nextState = { ...prev };
// payload.norms is typed on the union, so safe to use directly
data.norms.forEach((normUpdate: { id: string; active: boolean }) => { payload.norms.forEach((normUpdate) => {
nextState[normUpdate.id] = normUpdate.active; nextState[normUpdate.id] = normUpdate.active;
console.log(`Conditional norm ${normUpdate.id} set to active: ${normUpdate.active}`); console.log(`Conditional norm ${normUpdate.id} set to active: ${normUpdate.active}`);
}); });
return nextState; return nextState;
}); });
console.log("Updated conditional norms state:", data.norms); console.log("Updated conditional norms state:", payload.norms);
} }
}, [getPhaseIds]); }, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex]);
useExperimentLogger(handleStreamUpdate); useExperimentLogger(handleStreamUpdate);
@@ -95,35 +126,36 @@ const MonitoringPage: React.FC = () => {
const phaseId = phaseIds[phaseIndex]; const phaseId = phaseIds[phaseIndex];
const goals = (getGoalsInPhase(phaseId) as any[]).map(g => ({ const goals = (getGoalsInPhase(phaseId) as ReducedGoal[]).map(g => ({
...g, ...g,
label: g.name, label: g.name,
achieved: activeIds[g.id] ?? false achieved: activeIds[g.id] ?? false,
})); }));
const triggers = (getTriggersInPhase(phaseId) as any[]).map(t => ({ const triggers = (getTriggersInPhase(phaseId) as ReducedTrigger[]).map(t => ({
...t, ...t,
label: (() => { label: (() => {
let prefix = ""; let prefix = "";
if (t.condition?.keyword) { if (t.condition && typeof t.condition !== "string" && "keyword" in t.condition && typeof t.condition.keyword === "string") {
prefix = `if keywords said: "${t.condition.keyword}"`; prefix = `if keywords said: "${t.condition.keyword}"`;
} else if (t.condition?.name) { } else if (t.condition && typeof t.condition !== "string" && "name" in t.condition && typeof t.condition.name === "string") {
prefix = `if LLM belief: ${t.condition.name}`; prefix = `if LLM belief: ${t.condition.name}`;
} else { //fallback } else { //fallback
prefix = t.label || "Trigger"; prefix = t.name || "Trigger"; // use typed `name` as a reliable fallback
} }
const stepLabels = t.plan?.steps?.map((step: any) => { const stepLabels = (t.plan && typeof t.plan !== "string" ? t.plan.steps : []).map((step: ReducedPlanStep) => {
if (step.text) { if ("text" in step && typeof step.text === "string") {
return `say: "${step.text}"`; return `say: "${step.text}"`;
} }
if (step.gesture) { if ("gesture" in step && step.gesture) {
return `perform gesture: ${step.gesture.name || step.gesture.type}`; const g = step.gesture;
} return `perform gesture: ${g.name || g.type}`;
if (step.goal) { }
if ("goal" in step && typeof step.goal === "string") {
return `perform LLM: ${step.goal}`; return `perform LLM: ${step.goal}`;
} }
return "Action"; // Fallback return "Action"; // Fallback
@@ -144,15 +176,15 @@ const MonitoringPage: React.FC = () => {
...n, ...n,
label: n.norm, label: n.norm,
})); }));
const conditionalNorms = (getNormsInPhase(phaseId) as any[]) const conditionalNorms = (getNormsInPhase(phaseId) as ReducedNorm[])
.filter(n => !!n.condition) // Only items with a condition .filter(n => !!n.condition) // Only items with a condition
.map(n => ({ .map(n => ({
...n, ...n,
label: (() => { label: (() => {
let prefix = ""; let prefix = "";
if (n.condition?.keyword) { if (n.condition && typeof n.condition !== "string" && "keyword" in n.condition && typeof n.condition.keyword === "string") {
prefix = `if keywords said: "${n.condition.keyword}"`; prefix = `if keywords said: "${n.condition.keyword}"`;
} else if (n.condition?.name) { } else if (n.condition && typeof n.condition !== "string" && "name" in n.condition && typeof n.condition.name === "string") {
prefix = `if LLM belief: ${n.condition.name}`; prefix = `if LLM belief: ${n.condition.name}`;
} }

View File

@@ -66,17 +66,26 @@ export async function playExperiment(): Promise<void> {
} }
/**
* Types for the experiment stream messages
*/
export type PhaseUpdate = { type: 'phase_update'; id: string };
export type GoalUpdate = { type: 'goal_update'; id: string };
export type TriggerUpdate = { type: 'trigger_update'; id: string; achieved: boolean };
export type CondNormsStateUpdate = { type: 'cond_norms_state_update'; norms: { id: string; active: boolean }[] };
export type ExperimentStreamData = PhaseUpdate | GoalUpdate | TriggerUpdate | CondNormsStateUpdate | Record<string, unknown>;
/** /**
* A hook that listens to the experiment stream and logs data to the console. * A hook that listens to the experiment stream and logs data to the console.
* It does not render anything. * It does not render anything.
*/ */
export function useExperimentLogger(onUpdate?: (data: any) => void) { export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => void) {
useEffect(() => { useEffect(() => {
const eventSource = new EventSource(`${API_BASE}/experiment_stream`); const eventSource = new EventSource(`${API_BASE}/experiment_stream`);
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
try { try {
const parsedData = JSON.parse(event.data); const parsedData = JSON.parse(event.data) as ExperimentStreamData;
if (onUpdate) { if (onUpdate) {
console.log(event.data); console.log(event.data);
onUpdate(parsedData); onUpdate(parsedData);