From c4e3ab27b28a8ae9bdf20f04cd8e5dd0efe86278 Mon Sep 17 00:00:00 2001 From: Pim Hutting Date: Fri, 16 Jan 2026 12:57:22 +0100 Subject: [PATCH] chore: among other things, fixed connection issue fix: connection issue conditional norm now able to undo and are updated via pings goals are able to be achieved out of turn ref: N25B-400 --- .../MonitoringPage/MonitoringPage.module.css | 9 +- src/pages/MonitoringPage/MonitoringPage.tsx | 85 +++++++++++++------ src/pages/MonitoringPage/MonitoringPageAPI.ts | 61 +++++++------ .../MonitoringPageComponents.tsx | 25 ++++-- src/utils/programStore.ts | 5 ++ 5 files changed, 126 insertions(+), 59 deletions(-) diff --git a/src/pages/MonitoringPage/MonitoringPage.module.css b/src/pages/MonitoringPage/MonitoringPage.module.css index f52a784..5f23eea 100644 --- a/src/pages/MonitoringPage/MonitoringPage.module.css +++ b/src/pages/MonitoringPage/MonitoringPage.module.css @@ -164,6 +164,14 @@ font-size: 1.1rem; } +.statusIndicator.clickable { + cursor: pointer; +} + +.statusIndicator.clickable:hover { + transform: scale(1.2); +} + .clickable { cursor: pointer; } @@ -173,7 +181,6 @@ } .active { - cursor: default; opacity: 1; } diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx index 6c35b7b..5d34d95 100644 --- a/src/pages/MonitoringPage/MonitoringPage.tsx +++ b/src/pages/MonitoringPage/MonitoringPage.tsx @@ -2,7 +2,7 @@ import React from 'react'; import styles from './MonitoringPage.module.css'; import useProgramStore from "../../utils/programStore.ts"; import { GestureControls, SpeechPresets, DirectSpeechInput, StatusList, RobotConnected } from './MonitoringPageComponents.tsx'; -import { nextPhase, useExperimentLogger, pauseExperiment, playExperiment, resetExperiment, resetPhase, type ExperimentStreamData, type GoalUpdate, type TriggerUpdate, type CondNormsStateUpdate, type PhaseUpdate } from ".//MonitoringPageAPI.ts" +import { nextPhase, useExperimentLogger, useStatusLogger, 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'; @@ -35,6 +35,7 @@ export type ReducedNorm = { id: string; label?: string; norm?: string; condition const MonitoringPage: React.FC = () => { const getPhaseIds = useProgramStore((s) => s.getPhaseIds); + const getPhaseNames = useProgramStore((s) => s.getPhaseNames); const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase); const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase); const getTriggersInPhase = useProgramStore((s) => s.getTriggersInPhase); @@ -46,9 +47,10 @@ const MonitoringPage: React.FC = () => { const [isPlaying, setIsPlaying] = React.useState(false); const phaseIds = getPhaseIds(); + const phaseNames = getPhaseNames(); const [phaseIndex, setPhaseIndex] = React.useState(0); - + //see if we reached end node const [isFinished, setIsFinished] = React.useState(false); @@ -56,6 +58,7 @@ const MonitoringPage: React.FC = () => { // Check for phase updates 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 { @@ -73,8 +76,10 @@ const MonitoringPage: React.FC = () => { 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) { + console.log(`${data.type} received, id : ${data.id}`) + if (gIndex == -1) + {console.log(`goal to update with id ${payload.id} not found in current phase ${phaseNames[phaseIndex]}`)} + else { //set current goal to the goal that is just started setGoalIndex(gIndex); @@ -101,24 +106,33 @@ const MonitoringPage: React.FC = () => { ...prev, [payload.id]: payload.achieved })); - } - else if (data.type === 'cond_norms_state_update') { + } +}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex]); + +const handleStatusUpdate = React.useCallback((data: any) => { + if (data.type === 'cond_norms_state_update') { const payload = data as CondNormsStateUpdate; + setActiveIds((prev) => { + const hasChanges = payload.norms.some( + (normUpdate) => prev[normUpdate.id] !== normUpdate.active + ); + + if (!hasChanges) { + return prev; + } const nextState = { ...prev }; - // payload.norms is typed on the union, so safe to use directly payload.norms.forEach((normUpdate) => { nextState[normUpdate.id] = normUpdate.active; }); - return nextState; }); - //commented out to avoid cluttering - //console.log("Updated conditional norms state:", payload.norms); } -}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex]); - +}, []); + //For incoming phase, goals and trigger updates useExperimentLogger(handleStreamUpdate); + //For pings that update conditional norms + useStatusLogger(handleStatusUpdate); if (phaseIds.length === 0) { return

No program loaded.

; @@ -229,19 +243,40 @@ const MonitoringPage: React.FC = () => {

Experiment Overview

-

Phase {` ${phaseIndex + 1}`}

+

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

- {phaseIds.map((id, index) => ( - - {index + 1} - - ))} + {phaseIds.map((id, index) => { + // Determine the status of the phase indicator + let phaseStatusClass = ""; + + if (isFinished) { + // If the whole experiment is done, all squares are green (completed) + phaseStatusClass = styles.completed; + } else if (index < phaseIndex) { + // Past phases + phaseStatusClass = styles.completed; + } else if (index === phaseIndex) { + // The current phase being worked on + phaseStatusClass = styles.current; + } + + return ( + + {index + 1} + + ); + })}
@@ -314,7 +349,7 @@ const MonitoringPage: React.FC = () => { ) : ( <> - + diff --git a/src/pages/MonitoringPage/MonitoringPageAPI.ts b/src/pages/MonitoringPage/MonitoringPageAPI.ts index 8ac1b60..36cae5a 100644 --- a/src/pages/MonitoringPage/MonitoringPageAPI.ts +++ b/src/pages/MonitoringPage/MonitoringPageAPI.ts @@ -1,4 +1,5 @@ -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; +import { data } from 'react-router'; const API_BASE = "http://localhost:8000"; const API_BASE_BP = API_BASE + "/button_pressed"; //UserInterruptAgent endpoint @@ -6,26 +7,7 @@ const API_BASE_BP = API_BASE + "/button_pressed"; //UserInterruptAgent endpoint /** * HELPER: Unified sender function */ -export const sendUserInterrupt = async (type: string, context: string) => { - try { - const response = await fetch(API_BASE_BP, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({type, context}), - }); - if (!response.ok) throw new Error("Backend response error"); - console.log(`Interrupt Sent - Type: ${type}, Context: ${context}`); - } catch (err) { - console.error(`Failed to send interrupt:`, err); - } -}; - - -/** - * HELPER: Unified sender function - * In a real app, you might move this to a /services or /hooks folder - */ -const sendAPICall = async (type: string, context: string, endpoint?: string) => { +export const sendAPICall = async (type: string, context: string, endpoint?: string) => { try { const response = await fetch(`${API_BASE_BP}${endpoint ?? ""}`, { method: "POST", @@ -98,25 +80,56 @@ export type ExperimentStreamData = PhaseUpdate | GoalUpdate | TriggerUpdate | Co * via updates sent from the backend */ export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => void) { + const callbackRef = React.useRef(onUpdate); + // Ref is updated every time with on update + React.useEffect(() => { + callbackRef.current = onUpdate; + }, [onUpdate]); + useEffect(() => { + console.log("Connecting to Experiment Stream..."); const eventSource = new EventSource(`${API_BASE}/experiment_stream`); eventSource.onmessage = (event) => { try { const parsedData = JSON.parse(event.data) as ExperimentStreamData; - if (onUpdate) { - onUpdate(parsedData); - } + //call function using the ref + callbackRef.current?.(parsedData); } catch (err) { console.warn("Stream parse error:", err); } }; + eventSource.onerror = (err) => { console.error("SSE Connection Error:", err); eventSource.close(); }; + return () => { + console.log("Closing Experiment Stream..."); eventSource.close(); }; + }, []); +} +/** + * A hook that listens to the status stream that updates active conditional norms + * via updates sent from the backend + */ +export function useStatusLogger(onUpdate?: (data: any) => void) { + const callbackRef = React.useRef(onUpdate); + + React.useEffect(() => { + callbackRef.current = onUpdate; }, [onUpdate]); + + useEffect(() => { + const eventSource = new EventSource(`${API_BASE}/status_stream`); + eventSource.onmessage = (event) => { + try { + const parsedData = JSON.parse(event.data); + callbackRef.current?.(parsedData); + } catch (err) { console.warn("Status stream error:", err); } + }; + return () => eventSource.close(); + }, []); // LEGE dependency array } \ No newline at end of file diff --git a/src/pages/MonitoringPage/MonitoringPageComponents.tsx b/src/pages/MonitoringPage/MonitoringPageComponents.tsx index e40401c..34bb04a 100644 --- a/src/pages/MonitoringPage/MonitoringPageComponents.tsx +++ b/src/pages/MonitoringPage/MonitoringPageComponents.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import styles from './MonitoringPage.module.css'; -import { sendUserInterrupt } from './MonitoringPageAPI'; +import { sendAPICall } from './MonitoringPageAPI'; // --- GESTURE COMPONENT --- export const GestureControls: React.FC = () => { @@ -23,7 +23,7 @@ export const GestureControls: React.FC = () => { > {gestures.map(g => )} - @@ -47,7 +47,7 @@ export const SpeechPresets: React.FC = () => {
  • @@ -64,7 +64,7 @@ export const DirectSpeechInput: React.FC = () => { const handleSend = () => { if (!text.trim()) return; - sendUserInterrupt("speech", text); + sendAPICall("speech", text); setText(""); // Clear after sending }; @@ -92,6 +92,7 @@ type StatusItem = { description?: string; label?: string; norm?: string; + name?: string; }; interface StatusListProps { @@ -99,6 +100,7 @@ interface StatusListProps { items: StatusItem[]; type: 'goal' | 'trigger' | 'norm'| 'cond_norm'; activeIds: Record; + setActiveIds?: React.Dispatch>>; currentGoalIndex?: number; } @@ -108,6 +110,7 @@ export const StatusList: React.FC = ({ items, type, activeIds, + setActiveIds, currentGoalIndex // Destructure this prop }) => { return ( @@ -118,19 +121,23 @@ export const StatusList: React.FC = ({ if (item.id === undefined) return null; const isActive = !!activeIds[item.id]; const showIndicator = type !== 'norm'; - const canOverride = showIndicator && !isActive || (type === 'cond_norm' && isActive); - const isCurrentGoal = type === 'goal' && idx === currentGoalIndex; + const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive); + + const handleOverrideClick = () => { if (!canOverride) return; if (type === 'cond_norm' && isActive){ {/* Unachieve conditional norm */} - sendUserInterrupt("override_unachieve", String(item.id)); + sendAPICall("override_unachieve", String(item.id)); } else { + if(type === 'goal') + if(setActiveIds) + {setActiveIds(prev => ({ ...prev, [String(item.id)]: true }));} - sendUserInterrupt("override", String(item.id)); + sendAPICall("override", String(item.id)); } }; @@ -156,7 +163,7 @@ export const StatusList: React.FC = ({ borderRadius: '4px' }} > - {item.description || item.label || item.norm} + {item.name || item.description || item.label || item.norm} {isCurrentGoal && " (Current)"}
  • diff --git a/src/utils/programStore.ts b/src/utils/programStore.ts index e6bcc3a..ef46424 100644 --- a/src/utils/programStore.ts +++ b/src/utils/programStore.ts @@ -15,6 +15,7 @@ export type ProgramState = { // Utility functions: // to avoid having to manually go through the entire state for every instance where data is required getPhaseIds: () => string[]; + getPhaseNames: () => string[]; getNormsInPhase: (currentPhaseId: string) => Record[]; getGoalsInPhase: (currentPhaseId: string) => Record[]; getTriggersInPhase: (currentPhaseId: string) => Record[]; @@ -43,6 +44,10 @@ const useProgramStore = create((set, get) => ({ * gets the ids of all phases in the program */ getPhaseIds: () => get().currentProgram.phases.map(entry => entry["id"] as string), + /** + * gets the names of all phases in the program + */ + getPhaseNames: () => get().currentProgram.phases.map((entry) => (entry["name"] as string)), /** * gets the norms for the provided phase */