import React, { useCallback, useRef, useState } from 'react'; import styles from './MonitoringPage.module.css'; // Store & API import useProgramStore from "../../utils/programStore"; import { nextPhase, useExperimentLogger, useStatusLogger, pauseExperiment, playExperiment, type ExperimentStreamData, type GoalUpdate, type TriggerUpdate, type CondNormsStateUpdate, type PhaseUpdate } from "./MonitoringPageAPI"; import { graphReducer, runProgramm } from '../VisProgPage/VisProgLogic.ts'; // Types import type { NormNodeData } from '../VisProgPage/visualProgrammingUI/nodes/NormNode'; import type { GoalNode } from '../VisProgPage/visualProgrammingUI/nodes/GoalNode'; import type { TriggerNode } from '../VisProgPage/visualProgrammingUI/nodes/TriggerNode'; // Sub-components import { GestureControls, SpeechPresets, DirectSpeechInput, StatusList, RobotConnected } from './MonitoringPageComponents'; // ---------------------------------------------------------------------- // 1. State management // ---------------------------------------------------------------------- /** * Manages the state of the active experiment, including phase progression, * goal tracking, and stream event listeners. */ function useExperimentLogic() { const getPhaseIds = useProgramStore((s) => s.getPhaseIds); const getPhaseNames = useProgramStore((s) => s.getPhaseNames); const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase); const setProgramState = useProgramStore((state) => state.setProgramState); const [loading, setLoading] = useState(false); const [activeIds, setActiveIds] = useState>({}); const [goalIndex, setGoalIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [phaseIndex, setPhaseIndex] = useState(0); const [isFinished, setIsFinished] = useState(false); // Ref to suppress stream updates during the "Reset Phase" fast-forward sequence const suppressUpdates = useRef(false); const phaseIds = getPhaseIds(); const phaseNames = getPhaseNames(); // --- Stream Handlers --- const handleStreamUpdate = useCallback((data: ExperimentStreamData) => { if (suppressUpdates.current) return; if (data.type === 'phase_update' && data.id) { const payload = data as PhaseUpdate; console.log(`${data.type} received, id : ${data.id}`); if (payload.id === "end") { setIsFinished(true); } else { setIsFinished(false); const newIndex = getPhaseIds().indexOf(payload.id); if (newIndex !== -1) { setPhaseIndex(newIndex); setGoalIndex(0); } } } else if (data.type === 'goal_update') { const payload = data as GoalUpdate; const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as GoalNode[]; const gIndex = currentPhaseGoals.findIndex((g) => g.id === payload.id); console.log(`${data.type} received, id : ${data.id}`); if (gIndex === -1) { console.warn(`Goal ${payload.id} not found in phase ${phaseNames[phaseIndex]}`); } else { setGoalIndex(gIndex); // Mark all previous goals as achieved setActiveIds((prev) => { const nextState = { ...prev }; for (let i = 0; i < gIndex; i++) { nextState[currentPhaseGoals[i].id] = true; } return nextState; }); } } else if (data.type === 'trigger_update') { const payload = data as TriggerUpdate; setActiveIds((prev) => ({ ...prev, [payload.id]: payload.achieved })); } }, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]); const handleStatusUpdate = useCallback((data: unknown) => { if (suppressUpdates.current) return; const payload = data as CondNormsStateUpdate; if (payload.type !== 'cond_norms_state_update') return; setActiveIds((prev) => { const hasChanges = payload.norms.some((u) => prev[u.id] !== u.active); if (!hasChanges) return prev; const nextState = { ...prev }; payload.norms.forEach((u) => { nextState[u.id] = u.active; }); return nextState; }); }, []); // Connect listeners useExperimentLogger(handleStreamUpdate); useStatusLogger(handleStatusUpdate); // --- Actions --- const resetExperiment = useCallback(async () => { try { setLoading(true); const phases = graphReducer(); setProgramState({ phases }); setActiveIds({}); setPhaseIndex(0); setGoalIndex(0); setIsFinished(false); await runProgramm(); console.log("Experiment & UI successfully reset."); } catch (err) { console.error("Failed to reset program:", err); } finally { setLoading(false); } }, [setProgramState]); const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "resetPhase") => { try { setLoading(true); switch (action) { case "pause": setIsPlaying(false); await pauseExperiment(); break; case "play": setIsPlaying(true); await playExperiment(); break; case "nextPhase": await nextPhase(); break; case "resetPhase": //make sure you don't see the phases pass to arrive back at current phase suppressUpdates.current = true; const targetIndex = phaseIndex; console.log(`Resetting phase: Restarting and skipping to index ${targetIndex}`); const phases = graphReducer(); setProgramState({ phases }); setActiveIds({}); setPhaseIndex(0); // Visually reset to start setGoalIndex(0); setIsFinished(false); // Restart backend await runProgramm(); for (let i = 0; i < targetIndex; i++) { console.log(`Skipping phase ${i}...`); await nextPhase(); } suppressUpdates.current = false; setPhaseIndex(targetIndex); setIsPlaying(true); //Maybe you pause and then reset break; } } catch (err) { console.error(err); } finally { setLoading(false); } }; return { loading, isPlaying, isFinished, phaseIds, phaseNames, phaseIndex, goalIndex, activeIds, setActiveIds, resetExperiment, handleControlAction, }; } // ---------------------------------------------------------------------- // 2. Smaller Presentation Components // ---------------------------------------------------------------------- /** * Visual indicator of progress through experiment phases. */ function PhaseProgressBar({ phaseIds, phaseIndex, isFinished }: { phaseIds: string[], phaseIndex: number, isFinished: boolean }) { return (
{phaseIds.map((id, index) => { let statusClass = ""; if (isFinished || index < phaseIndex) statusClass = styles.completed; else if (index === phaseIndex) statusClass = styles.current; return ( {index + 1} ); })}
); } /** * Main control buttons (Play, Pause, Next, Reset). */ function ControlPanel({ loading, isPlaying, onAction, onReset }: { loading: boolean, isPlaying: boolean, onAction: (a: "pause" | "play" | "nextPhase" | "resetPhase") => void, onReset: () => void }) { return (

Experiment Controls

); } /** * Displays lists of Goals, Triggers, and Norms for the current phase. */ function PhaseDashboard({ phaseId, activeIds, setActiveIds, goalIndex }: { phaseId: string, activeIds: Record, setActiveIds: React.Dispatch>>, goalIndex: number }) { const getGoalsWithDepth = useProgramStore((s) => s.getGoalsWithDepth); const getTriggers = useProgramStore((s) => s.getTriggersInPhase); const getNorms = useProgramStore((s) => s.getNormsInPhase); // Prepare data view models const goals = getGoalsWithDepth(phaseId).map((g) => ({ ...g, id: g.id as string, name: g.name as string, achieved: activeIds[g.id as string] ?? false, level: g.level, // Pass this new property to the UI })); const triggers = (getTriggers(phaseId) as TriggerNode[]).map(t => ({ ...t, achieved: activeIds[t.id] ?? false, })); const norms = (getNorms(phaseId) as NormNodeData[]) .filter(n => !n.condition) .map(n => ({ ...n, label: n.norm })); const conditionalNorms = (getNorms(phaseId) as (NormNodeData & { id: string })[]) .filter(n => !!n.condition) .map(n => ({ ...n, achieved: activeIds[n.id] ?? false })); return ( <> ); } // ---------------------------------------------------------------------- // 3. Main Component // ---------------------------------------------------------------------- const MonitoringPage: React.FC = () => { const { loading, isPlaying, isFinished, phaseIds, phaseNames, phaseIndex, goalIndex, activeIds, setActiveIds, resetExperiment, handleControlAction } = useExperimentLogic(); if (phaseIds.length === 0) { return

No program loaded.

; } return (
{/* HEADER */}

Experiment Overview

{isFinished ? ( Experiment finished ) : ( <>Phase {phaseIndex + 1}: {phaseNames[phaseIndex]} )}

{/* MAIN GRID */}

Phase Overview

{isFinished ? (

All phases have been successfully completed.

) : ( )}
{/* LOGS TODO: add actual logs */} {/* FOOTER */}
); } export default MonitoringPage;