Compare commits

...

2 Commits

Author SHA1 Message Date
af196529f8 chore: remove hooks 2026-02-02 14:39:20 +01:00
901159ae2d feat: stop experiment button 2026-02-02 14:39:07 +01:00
5 changed files with 81 additions and 79 deletions

17
package-lock.json generated
View File

@@ -31,7 +31,6 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0", "globals": "^16.4.0",
"husky": "^9.1.7",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0", "jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0", "jest-environment-jsdom": "^30.2.0",
@@ -5544,22 +5543,6 @@
"node": ">=10.17.0" "node": ">=10.17.0"
} }
}, },
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true,
"license": "MIT",
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",

View File

@@ -8,8 +8,7 @@
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint src test", "lint": "eslint src test",
"preview": "vite preview", "preview": "vite preview",
"test": "jest", "test": "jest"
"prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@neodrag/react": "^2.3.1", "@neodrag/react": "^2.3.1",
@@ -35,7 +34,6 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0", "globals": "^16.4.0",
"husky": "^9.1.7",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0", "jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0", "jest-environment-jsdom": "^30.2.0",

View File

@@ -98,6 +98,11 @@ University within the Software Project course.
color: white; color: white;
} }
.stop {
background-color: red;
color: white;
}
.restartExperiment{ .restartExperiment{
background-color: red; background-color: red;
color: white; color: white;

View File

@@ -6,17 +6,18 @@ import styles from './MonitoringPage.module.css';
// Store & API // Store & API
import useProgramStore from "../../utils/programStore"; import useProgramStore from "../../utils/programStore";
import { import {
nextPhase, nextPhase,
useExperimentLogger, stopExperiment,
useStatusLogger, useExperimentLogger,
pauseExperiment, useStatusLogger,
playExperiment, pauseExperiment,
type ExperimentStreamData, playExperiment,
type GoalUpdate, type ExperimentStreamData,
type TriggerUpdate, type GoalUpdate,
type CondNormsStateUpdate, type TriggerUpdate,
type PhaseUpdate type CondNormsStateUpdate,
type PhaseUpdate
} from "./MonitoringPageAPI"; } from "./MonitoringPageAPI";
import { graphReducer, runProgram } from '../VisProgPage/VisProgLogic.ts'; import { graphReducer, runProgram } from '../VisProgPage/VisProgLogic.ts';
@@ -26,12 +27,12 @@ import type { GoalNode } from '../VisProgPage/visualProgrammingUI/nodes/GoalNode
import type { TriggerNode } from '../VisProgPage/visualProgrammingUI/nodes/TriggerNode'; import type { TriggerNode } from '../VisProgPage/visualProgrammingUI/nodes/TriggerNode';
// Sub-components // Sub-components
import { import {
GestureControls, GestureControls,
SpeechPresets, SpeechPresets,
DirectSpeechInput, DirectSpeechInput,
StatusList, StatusList,
RobotConnected RobotConnected
} from './MonitoringPageComponents'; } from './MonitoringPageComponents';
import ExperimentLogs from "./components/ExperimentLogs.tsx"; import ExperimentLogs from "./components/ExperimentLogs.tsx";
@@ -76,7 +77,7 @@ function useExperimentLogic() {
setGoalIndex(0); setGoalIndex(0);
} }
} }
} }
else if (data.type === 'goal_update') { else if (data.type === 'goal_update') {
const payload = data as GoalUpdate; const payload = data as GoalUpdate;
const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as GoalNode[]; const currentPhaseGoals = getGoalsInPhase(phaseIds[phaseIndex]) as GoalNode[];
@@ -97,7 +98,7 @@ function useExperimentLogic() {
return nextState; return nextState;
}); });
} }
} }
else if (data.type === 'trigger_update') { else if (data.type === 'trigger_update') {
const payload = data as TriggerUpdate; const payload = data as TriggerUpdate;
setActiveIds((prev) => ({ ...prev, [payload.id]: payload.achieved })); setActiveIds((prev) => ({ ...prev, [payload.id]: payload.achieved }));
@@ -111,7 +112,7 @@ function useExperimentLogic() {
setActiveIds((prev) => { setActiveIds((prev) => {
const hasChanges = payload.norms.some((u) => prev[u.id] !== u.active); const hasChanges = payload.norms.some((u) => prev[u.id] !== u.active);
if (!hasChanges) return prev; if (!hasChanges) return prev;
const nextState = { ...prev }; const nextState = { ...prev };
payload.norms.forEach((u) => { nextState[u.id] = u.active; }); payload.norms.forEach((u) => { nextState[u.id] = u.active; });
return nextState; return nextState;
@@ -144,7 +145,7 @@ function useExperimentLogic() {
} }
}, [setProgramState]); }, [setProgramState]);
const handleControlAction = async (action: "pause" | "play" | "nextPhase") => { const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "stop") => {
try { try {
setLoading(true); setLoading(true);
switch (action) { switch (action) {
@@ -159,6 +160,9 @@ function useExperimentLogic() {
case "nextPhase": case "nextPhase":
await nextPhase(); await nextPhase();
break; break;
case "stop":
await stopExperiment();
break;
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -189,14 +193,14 @@ function useExperimentLogic() {
/** /**
* Visual indicator of progress through experiment phases. * Visual indicator of progress through experiment phases.
*/ */
function PhaseProgressBar({ function PhaseProgressBar({
phaseIds, phaseIds,
phaseIndex, phaseIndex,
isFinished isFinished
}: { }: {
phaseIds: string[], phaseIds: string[],
phaseIndex: number, phaseIndex: number,
isFinished: boolean isFinished: boolean
}) { }) {
return ( return (
<div className={styles.phaseProgress}> <div className={styles.phaseProgress}>
@@ -218,16 +222,16 @@ function PhaseProgressBar({
/** /**
* Main control buttons (Play, Pause, Next, Reset). * Main control buttons (Play, Pause, Next, Reset).
*/ */
function ControlPanel({ function ControlPanel({
loading, loading,
isPlaying, isPlaying,
onAction, onAction,
onReset onReset
}: { }: {
loading: boolean, loading: boolean,
isPlaying: boolean, isPlaying: boolean,
onAction: (a: "pause" | "play" | "nextPhase") => void, onAction: (a: "pause" | "play" | "nextPhase" | "stop") => void,
onReset: () => void onReset: () => void
}) { }) {
return ( return (
<div className={styles.experimentControls}> <div className={styles.experimentControls}>
@@ -245,17 +249,23 @@ function ControlPanel({
disabled={loading} disabled={loading}
></button> ></button>
<button <button
className={styles.next} className={styles.next}
onClick={() => onAction("nextPhase")} onClick={() => onAction("nextPhase")}
disabled={loading} disabled={loading}
></button> ></button>
<button <button
className={styles.restartExperiment} className={styles.restartExperiment}
onClick={onReset} onClick={onReset}
disabled={loading} disabled={loading}
></button> ></button>
<button
className={styles.stop}
onClick={() => onAction("stop")}
disabled={loading}
></button>
</div> </div>
</div> </div>
); );
@@ -353,11 +363,11 @@ const MonitoringPage: React.FC = () => {
<PhaseProgressBar phaseIds={phaseIds} phaseIndex={phaseIndex} isFinished={isFinished} /> <PhaseProgressBar phaseIds={phaseIds} phaseIndex={phaseIndex} isFinished={isFinished} />
</div> </div>
<ControlPanel <ControlPanel
loading={loading} loading={loading}
isPlaying={isPlaying} isPlaying={isPlaying}
onAction={handleControlAction} onAction={handleControlAction}
onReset={resetExperiment} onReset={resetExperiment}
/> />
<div className={styles.connectionStatus}> <div className={styles.connectionStatus}>
@@ -370,17 +380,17 @@ const MonitoringPage: React.FC = () => {
<section className={styles.phaseOverviewText}> <section className={styles.phaseOverviewText}>
<h3>Phase Overview</h3> <h3>Phase Overview</h3>
</section> </section>
{isFinished ? ( {isFinished ? (
<div className={styles.finishedMessage}> <div className={styles.finishedMessage}>
<p>All phases have been successfully completed.</p> <p>All phases have been successfully completed.</p>
</div> </div>
) : ( ) : (
<PhaseDashboard <PhaseDashboard
phaseId={phaseIds[phaseIndex]} phaseId={phaseIds[phaseIndex]}
activeIds={activeIds} activeIds={activeIds}
setActiveIds={setActiveIds} setActiveIds={setActiveIds}
goalIndex={goalIndex} goalIndex={goalIndex}
/> />
)} )}
</main> </main>
@@ -398,4 +408,4 @@ const MonitoringPage: React.FC = () => {
); );
} }
export default MonitoringPage; export default MonitoringPage;

View File

@@ -32,6 +32,12 @@ export async function nextPhase(): Promise<void> {
sendAPICall(type, context) sendAPICall(type, context)
} }
export async function stopExperiment(): Promise<void> {
const type = "stop"
const context = ""
sendAPICall(type, context)
}
/** /**
* Sends an API call to the CB for going to pause experiment * Sends an API call to the CB for going to pause experiment
@@ -95,14 +101,14 @@ export function useExperimentLogger(onUpdate?: (data: ExperimentStreamData) => v
console.log("Closing Experiment Stream..."); console.log("Closing Experiment Stream...");
eventSource.close(); eventSource.close();
}; };
}, []); }, []);
} }
/** /**
* A hook that listens to the status stream that updates active conditional norms * A hook that listens to the status stream that updates active conditional norms
* via updates sent from the backend * via updates sent from the backend
*/ */
export function useStatusLogger(onUpdate?: (data: ExperimentStreamData) => void) { export function useStatusLogger(onUpdate?: (data: ExperimentStreamData) => void) {
const callbackRef = React.useRef(onUpdate); const callbackRef = React.useRef(onUpdate);
React.useEffect(() => { React.useEffect(() => {