feat: The Big One UI #47

Merged
j.gerla merged 115 commits from temp_screenshot_manual into dev 2026-01-28 08:27:30 +00:00
7 changed files with 96 additions and 66 deletions
Showing only changes of commit f73bbb9d02 - Show all commits

View File

@@ -77,11 +77,6 @@
color: white; color: white;
} }
.restartPhase{
background-color: rgb(255, 123, 0);
color: white;
}
.restartExperiment{ .restartExperiment{
background-color: red; background-color: red;
color: white; color: white;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback, useState } from 'react';
import styles from './MonitoringPage.module.css'; import styles from './MonitoringPage.module.css';
// Store & API // Store & API
@@ -52,16 +52,12 @@ function useExperimentLogic() {
const [phaseIndex, setPhaseIndex] = useState(0); const [phaseIndex, setPhaseIndex] = useState(0);
const [isFinished, setIsFinished] = useState(false); 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 phaseIds = getPhaseIds();
const phaseNames = getPhaseNames(); const phaseNames = getPhaseNames();
// --- Stream Handlers --- // --- Stream Handlers ---
const handleStreamUpdate = useCallback((data: ExperimentStreamData) => { const handleStreamUpdate = useCallback((data: ExperimentStreamData) => {
if (suppressUpdates.current) return;
if (data.type === 'phase_update' && data.id) { if (data.type === 'phase_update' && data.id) {
const payload = data as PhaseUpdate; const payload = data as PhaseUpdate;
console.log(`${data.type} received, id : ${data.id}`); console.log(`${data.type} received, id : ${data.id}`);
@@ -105,7 +101,6 @@ function useExperimentLogic() {
}, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]); }, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]);
const handleStatusUpdate = useCallback((data: unknown) => { const handleStatusUpdate = useCallback((data: unknown) => {
if (suppressUpdates.current) return;
const payload = data as CondNormsStateUpdate; const payload = data as CondNormsStateUpdate;
if (payload.type !== 'cond_norms_state_update') return; if (payload.type !== 'cond_norms_state_update') return;
@@ -145,7 +140,7 @@ function useExperimentLogic() {
} }
}, [setProgramState]); }, [setProgramState]);
const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "resetPhase") => { const handleControlAction = async (action: "pause" | "play" | "nextPhase") => {
try { try {
setLoading(true); setLoading(true);
switch (action) { switch (action) {
@@ -160,30 +155,6 @@ function useExperimentLogic() {
case "nextPhase": case "nextPhase":
await nextPhase(); await nextPhase();
break; 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) { } catch (err) {
console.error(err); console.error(err);
@@ -251,7 +222,7 @@ function ControlPanel({
}: { }: {
loading: boolean, loading: boolean,
isPlaying: boolean, isPlaying: boolean,
onAction: (a: "pause" | "play" | "nextPhase" | "resetPhase") => void, onAction: (a: "pause" | "play" | "nextPhase") => void,
onReset: () => void onReset: () => void
}) { }) {
return ( return (
@@ -276,12 +247,6 @@ function ControlPanel({
disabled={loading} disabled={loading}
></button> ></button>
<button
className={styles.restartPhase}
onClick={() => onAction("resetPhase")}
disabled={loading}
></button>
<button <button
className={styles.restartExperiment} className={styles.restartExperiment}
onClick={onReset} onClick={onReset}

View File

@@ -32,16 +32,6 @@ export async function nextPhase(): Promise<void> {
} }
/**
* Sends an API call to the CB for going to reset the currect phase
* In case we can't go to the next phase, the function will throw an error.
*/
export async function resetPhase(): Promise<void> {
const type = "reset_phase"
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
*/ */

View File

@@ -112,8 +112,8 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
updateNodeData(props.id, {...data, belief: {...data.belief, description: value}}); updateNodeData(props.id, {...data, belief: {...data.belief, description: value}});
} }
// These are the labels outputted by our emotion detection model // Use this
const emotionOptions = ["sad", "angry", "surprise", "fear", "happy", "disgust", "neutral"]; const emotionOptions = ["Happy", "Angry", "Sad", "Cheerful"]
let placeholder = "" let placeholder = ""

View File

@@ -51,6 +51,7 @@ describe('MonitoringPage', () => {
const mockGetPhaseNames = jest.fn(); const mockGetPhaseNames = jest.fn();
const mockGetNorms = jest.fn(); const mockGetNorms = jest.fn();
const mockGetGoals = jest.fn(); const mockGetGoals = jest.fn();
const mockGetGoalsWithDepth = jest.fn();
const mockGetTriggers = jest.fn(); const mockGetTriggers = jest.fn();
const mockSetProgramState = jest.fn(); const mockSetProgramState = jest.fn();
@@ -65,6 +66,7 @@ describe('MonitoringPage', () => {
getNormsInPhase: mockGetNorms, getNormsInPhase: mockGetNorms,
getGoalsInPhase: mockGetGoals, getGoalsInPhase: mockGetGoals,
getTriggersInPhase: mockGetTriggers, getTriggersInPhase: mockGetTriggers,
getGoalsWithDepth: mockGetGoalsWithDepth,
setProgramState: mockSetProgramState, setProgramState: mockSetProgramState,
}; };
return selector(state); return selector(state);
@@ -81,7 +83,11 @@ describe('MonitoringPage', () => {
// Default mock return values // Default mock return values
mockGetPhaseIds.mockReturnValue(['phase-1', 'phase-2']); mockGetPhaseIds.mockReturnValue(['phase-1', 'phase-2']);
mockGetPhaseNames.mockReturnValue(['Intro', 'Main']); mockGetPhaseNames.mockReturnValue(['Intro', 'Main']);
mockGetGoals.mockReturnValue([{ id: 'g1', name: 'Goal 1' }, { id: 'g2', name: 'Goal 2' }]); mockGetGoals.mockReturnValue([{ id: 'g1', name: 'Goal 1'}, { id: 'g2', name: 'Goal 2'}]);
mockGetGoalsWithDepth.mockReturnValue([
{ id: 'g1', name: 'Goal 1', level: 0 },
{ id: 'g2', name: 'Goal 2', level: 0 }
]);
mockGetTriggers.mockReturnValue([{ id: 't1', name: 'Trigger 1' }]); mockGetTriggers.mockReturnValue([{ id: 't1', name: 'Trigger 1' }]);
mockGetNorms.mockReturnValue([ mockGetNorms.mockReturnValue([
{ id: 'n1', norm: 'Norm 1', condition: null }, { id: 'n1', norm: 'Norm 1', condition: null },

View File

@@ -1,8 +1,7 @@
import { renderHook, act, cleanup } from '@testing-library/react'; import { renderHook, act, cleanup } from '@testing-library/react';
import { import {
sendAPICall, sendAPICall,
nextPhase, nextPhase,
resetPhase,
pauseExperiment, pauseExperiment,
playExperiment, playExperiment,
useExperimentLogger, useExperimentLogger,
@@ -116,14 +115,6 @@ describe('MonitoringPageAPI', () => {
); );
}); });
test('resetPhase sends correct params', async () => {
await resetPhase();
expect(globalThis.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ body: JSON.stringify({ type: 'reset_phase', context: '' }) })
);
});
test('pauseExperiment sends correct params', async () => { test('pauseExperiment sends correct params', async () => {
await pauseExperiment(); await pauseExperiment();
expect(globalThis.fetch).toHaveBeenCalledWith( expect(globalThis.fetch).toHaveBeenCalledWith(

View File

@@ -115,6 +115,89 @@ describe('useProgramStore', () => {
}); });
}); });
describe('getGoalsWithDepth', () => {
const complexProgram: ReducedProgram = {
phases: [
{
id: 'phase-nested',
goals: [
// Level 0: Root Goal 1
{
id: 'root-1',
name: 'Root Goal 1',
plan: {
steps: [
// This is an ACTION (no plan), should be ignored
{ id: 'action-1', type: 'speech' },
// Level 1: Child Goal
{
id: 'child-1',
name: 'Child Goal',
plan: {
steps: [
// Level 2: Grandchild Goal
{
id: 'grandchild-1',
name: 'Grandchild',
plan: { steps: [] } // Empty plan is still a plan
}
]
}
}
]
}
},
// Level 0: Root Goal 2 (Sibling)
{
id: 'root-2',
name: 'Root Goal 2',
plan: { steps: [] }
}
]
}
]
};
it('should flatten nested goals and assign correct depth levels', () => {
useProgramStore.getState().setProgramState(complexProgram);
const goals = useProgramStore.getState().getGoalsWithDepth('phase-nested');
// logic: Root 1 -> Child 1 -> Grandchild 1 -> Root 2
expect(goals).toHaveLength(4);
// Check Root 1
expect(goals[0]).toEqual(expect.objectContaining({ id: 'root-1', level: 0 }));
// Check Child 1
expect(goals[1]).toEqual(expect.objectContaining({ id: 'child-1', level: 1 }));
// Check Grandchild 1
expect(goals[2]).toEqual(expect.objectContaining({ id: 'grandchild-1', level: 2 }));
// Check Root 2
expect(goals[3]).toEqual(expect.objectContaining({ id: 'root-2', level: 0 }));
});
it('should ignore steps that are not goals (missing "plan" property)', () => {
useProgramStore.getState().setProgramState(complexProgram);
const goals = useProgramStore.getState().getGoalsWithDepth('phase-nested');
// The 'action-1' object should NOT be in the list
const action = goals.find(g => g.id === 'action-1');
expect(action).toBeUndefined();
});
it('throws if phase does not exist', () => {
useProgramStore.getState().setProgramState(complexProgram);
expect(() =>
useProgramStore.getState().getGoalsWithDepth('missing-phase')
).toThrow('phase with id:"missing-phase" not found');
});
});
it('should return the names of all phases in the program', () => { it('should return the names of all phases in the program', () => {
// Define a program specifically with names for this test // Define a program specifically with names for this test
const programWithNames: ReducedProgram = { const programWithNames: ReducedProgram = {