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 => )}
-