diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx index e66e6a2..cc9026c 100644 --- a/src/pages/MonitoringPage/MonitoringPage.tsx +++ b/src/pages/MonitoringPage/MonitoringPage.tsx @@ -2,10 +2,37 @@ import React from 'react'; import styles from './MonitoringPage.module.css'; import useProgramStore from "../../utils/programStore.ts"; 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'; +// 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; + +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; + +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 getPhaseIds = useProgramStore((s) => s.getPhaseIds); const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase); @@ -25,25 +52,27 @@ const MonitoringPage: React.FC = () => { //see if we reached end node const [isFinished, setIsFinished] = React.useState(false); - const handleStreamUpdate = React.useCallback((data: any) => { + const handleStreamUpdate = React.useCallback((data: ExperimentStreamData) => { // Check for phase updates - if (data.type === 'phase_update' && data.phase_id) { - if (data.phase_id === "end") { + if (data.type === 'phase_update' && data.id) { + const payload = data as PhaseUpdate; + if (payload.id === "end") { setIsFinished(true); } else { - setIsFinished(false); - + setIsFinished(false); + const allIds = getPhaseIds(); - const newIndex = allIds.indexOf(data.phase_id); + const newIndex = allIds.indexOf(payload.id); if (newIndex !== -1) { setPhaseIndex(newIndex); setGoalIndex(0); } - } + } } else if (data.type === 'goal_update') { - const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as any[]; - const gIndex = currentPhaseGoals.findIndex((g: any) => g.id === data.id); + const payload = data as GoalUpdate; + const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as ReducedGoal[]; + const gIndex = currentPhaseGoals.findIndex((g: ReducedGoal) => g.id === payload.id); if (gIndex !== -1) { //set current goal to the goal that is just started @@ -62,30 +91,32 @@ const MonitoringPage: React.FC = () => { 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') { + const payload = data as TriggerUpdate; setActiveIds((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') { + const payload = data as CondNormsStateUpdate; setActiveIds((prev) => { const nextState = { ...prev }; - - data.norms.forEach((normUpdate: { id: string; active: boolean }) => { + // payload.norms is typed on the union, so safe to use directly + payload.norms.forEach((normUpdate) => { nextState[normUpdate.id] = normUpdate.active; console.log(`Conditional norm ${normUpdate.id} set to active: ${normUpdate.active}`); }); 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); @@ -95,35 +126,36 @@ const MonitoringPage: React.FC = () => { const phaseId = phaseIds[phaseIndex]; - const goals = (getGoalsInPhase(phaseId) as any[]).map(g => ({ + const goals = (getGoalsInPhase(phaseId) as ReducedGoal[]).map(g => ({ ...g, - label: g.name, - achieved: activeIds[g.id] ?? false + label: g.name, + achieved: activeIds[g.id] ?? false, })); - const triggers = (getTriggersInPhase(phaseId) as any[]).map(t => ({ + const triggers = (getTriggersInPhase(phaseId) as ReducedTrigger[]).map(t => ({ ...t, label: (() => { 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}"`; - } 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}`; } 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) => { - if (step.text) { + const stepLabels = (t.plan && typeof t.plan !== "string" ? t.plan.steps : []).map((step: ReducedPlanStep) => { + if ("text" in step && typeof step.text === "string") { return `say: "${step.text}"`; - } - if (step.gesture) { - return `perform gesture: ${step.gesture.name || step.gesture.type}`; - } - if (step.goal) { + } + if ("gesture" in step && step.gesture) { + const g = step.gesture; + return `perform gesture: ${g.name || g.type}`; + } + if ("goal" in step && typeof step.goal === "string") { return `perform LLM: ${step.goal}`; } return "Action"; // Fallback @@ -144,15 +176,15 @@ const MonitoringPage: React.FC = () => { ...n, label: n.norm, })); - const conditionalNorms = (getNormsInPhase(phaseId) as any[]) + const conditionalNorms = (getNormsInPhase(phaseId) as ReducedNorm[]) .filter(n => !!n.condition) // Only items with a condition .map(n => ({ ...n, label: (() => { 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}"`; - } 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}`; } diff --git a/src/pages/MonitoringPage/MonitoringPageAPI.ts b/src/pages/MonitoringPage/MonitoringPageAPI.ts index 076d6fa..19e10e4 100644 --- a/src/pages/MonitoringPage/MonitoringPageAPI.ts +++ b/src/pages/MonitoringPage/MonitoringPageAPI.ts @@ -66,17 +66,26 @@ export async function playExperiment(): Promise { } +/** + * 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; + /** * A hook that listens to the experiment stream and logs data to the console. * It does not render anything. */ -export function useExperimentLogger(onUpdate?: (data: any) => void) { +export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => void) { useEffect(() => { const eventSource = new EventSource(`${API_BASE}/experiment_stream`); eventSource.onmessage = (event) => { try { - const parsedData = JSON.parse(event.data); + const parsedData = JSON.parse(event.data) as ExperimentStreamData; if (onUpdate) { console.log(event.data); onUpdate(parsedData);