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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <p className={styles.empty}>No program loaded.</p>;
|
||||
@@ -229,19 +243,40 @@ const MonitoringPage: React.FC = () => {
|
||||
<header className={styles.experimentOverview}>
|
||||
<div className={styles.phaseName}>
|
||||
<h2>Experiment Overview</h2>
|
||||
<p><strong>Phase</strong> {` ${phaseIndex + 1}`} </p>
|
||||
<p>
|
||||
{isFinished ? (
|
||||
<strong>Experiment finished</strong>
|
||||
) : (
|
||||
<>
|
||||
<strong>Phase {phaseIndex + 1}:</strong> {phaseNames[phaseIndex]}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<div className={styles.phaseProgress}>
|
||||
{phaseIds.map((id, index) => (
|
||||
<span
|
||||
key={id}
|
||||
className={`${styles.phase} ${
|
||||
index < phaseIndex ? styles.completed :
|
||||
index === phaseIndex ? styles.current : ""
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
))}
|
||||
{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 (
|
||||
<span
|
||||
key={id}
|
||||
className={`${styles.phase} ${phaseStatusClass}`}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -314,7 +349,7 @@ const MonitoringPage: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<StatusList title="Goals" items={goals} type="goal" activeIds={activeIds} currentGoalIndex={goalIndex} />
|
||||
<StatusList title="Goals" items={goals} type="goal" activeIds={activeIds} setActiveIds = {setActiveIds} currentGoalIndex={goalIndex} />
|
||||
<StatusList title="Triggers" items={triggers} type="trigger" activeIds={activeIds} />
|
||||
<StatusList title="Norms" items={norms} type="norm" activeIds={activeIds} />
|
||||
<StatusList title="Conditional Norms" items={conditionalNorms} type="cond_norm" activeIds={activeIds} />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 => <option key={g.value} value={g.value}>{g.label}</option>)}
|
||||
</select>
|
||||
<button onClick={() => sendUserInterrupt("gesture", selectedGesture)}>
|
||||
<button onClick={() => sendAPICall("gesture", selectedGesture)}>
|
||||
Actuate
|
||||
</button>
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@ export const SpeechPresets: React.FC = () => {
|
||||
<li key={i}>
|
||||
<button
|
||||
className={styles.speechBtn}
|
||||
onClick={() => sendUserInterrupt("speech", phrase.text)}
|
||||
onClick={() => sendAPICall("speech", phrase.text)}
|
||||
>
|
||||
"{phrase.label}"
|
||||
</button>
|
||||
@@ -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<string, boolean>;
|
||||
setActiveIds?: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
||||
currentGoalIndex?: number;
|
||||
}
|
||||
|
||||
@@ -108,6 +110,7 @@ export const StatusList: React.FC<StatusListProps> = ({
|
||||
items,
|
||||
type,
|
||||
activeIds,
|
||||
setActiveIds,
|
||||
currentGoalIndex // Destructure this prop
|
||||
}) => {
|
||||
return (
|
||||
@@ -118,19 +121,23 @@ export const StatusList: React.FC<StatusListProps> = ({
|
||||
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<StatusListProps> = ({
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
{item.description || item.label || item.norm}
|
||||
{item.name || item.description || item.label || item.norm}
|
||||
{isCurrentGoal && " (Current)"}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -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<string, unknown>[];
|
||||
getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||
@@ -43,6 +44,10 @@ const useProgramStore = create<ProgramState>((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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user