feat: The Big One UI #47
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -82,6 +84,10 @@ describe('MonitoringPage', () => {
|
|||||||
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 },
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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(
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user