From b10dbae4885e8194cd21d9bcc6c0bc6fed37e3ef Mon Sep 17 00:00:00 2001 From: JGerla Date: Sat, 20 Dec 2025 22:58:22 +0100 Subject: [PATCH 01/98] test: added tests for the ProgramStore also added documentation ref: N25B-428 --- src/pages/VisProgPage/VisProg.tsx | 5 ++ src/utils/programStore.ts | 81 ++++++++++++++++++++++++ test/setupFlowTests.ts | 5 ++ test/utils/programStore.test.ts | 100 ++++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 src/utils/programStore.ts create mode 100644 test/utils/programStore.test.ts diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index 06e072c..1a3720b 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -9,6 +9,7 @@ import { import '@xyflow/react/dist/style.css'; import {useEffect} from "react"; import {useShallow} from 'zustand/react/shallow'; +import useProgramStore from "../../utils/programStore.ts"; import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx'; import useFlowStore from './visualProgrammingUI/VisProgStores.tsx'; import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx'; @@ -152,6 +153,10 @@ function runProgram() { ).then((res) => { if (!res.ok) throw new Error("Failed communicating with the backend.") console.log("Successfully sent the program to the backend."); + + // store reduced program in global program store for further use in the UI + // when the program was sent to the backend successfully: + useProgramStore.getState().setProgramState(structuredClone(program)); }).catch(() => console.log("Failed to send program to the backend.")); } diff --git a/src/utils/programStore.ts b/src/utils/programStore.ts new file mode 100644 index 0000000..e6bcc3a --- /dev/null +++ b/src/utils/programStore.ts @@ -0,0 +1,81 @@ +import {create} from "zustand"; + +// the type of a reduced program +export type ReducedProgram = { phases: Record[] }; + +/** + * the type definition of the programStore + */ +export type ProgramState = { + // Basic store functionality: + currentProgram: ReducedProgram; + setProgramState: (state: ReducedProgram) => void; + getProgramState: () => ReducedProgram; + + // Utility functions: + // to avoid having to manually go through the entire state for every instance where data is required + getPhaseIds: () => string[]; + getNormsInPhase: (currentPhaseId: string) => Record[]; + getGoalsInPhase: (currentPhaseId: string) => Record[]; + getTriggersInPhase: (currentPhaseId: string) => Record[]; + // if more specific utility functions are needed they can be added here: +} + +/** + * the ProgramStore can be used to access all information of the most recently sent program, + * it contains basic functions to set and get the current program. + * And it contains some utility functions that allow you to easily gain access + * to the norms, triggers and goals of a specific phase. + */ +const useProgramStore = create((set, get) => ({ + currentProgram: { phases: [] as Record[]}, + /** + * sets the current program by cloning the provided program using a structuredClone + */ + setProgramState: (program: ReducedProgram) => set({currentProgram: structuredClone(program)}), + /** + * gets the current program + */ + getProgramState: () => get().currentProgram, + + // utility functions: + /** + * gets the ids of all phases in the program + */ + getPhaseIds: () => get().currentProgram.phases.map(entry => entry["id"] as string), + /** + * gets the norms for the provided phase + */ + getNormsInPhase: (currentPhaseId) => { + const program = get().currentProgram; + const phase = program.phases.find(val => val["id"] === currentPhaseId); + if (phase) { + return phase["norms"] as Record[]; + } + throw new Error(`phase with id:"${currentPhaseId}" not found`) + }, + /** + * gets the goals for the provided phase + */ + getGoalsInPhase: (currentPhaseId) => { + const program = get().currentProgram; + const phase = program.phases.find(val => val["id"] === currentPhaseId); + if (phase) { + return phase["goals"] as Record[]; + } + throw new Error(`phase with id:"${currentPhaseId}" not found`) + }, + /** + * gets the triggers for the provided phase + */ + getTriggersInPhase: (currentPhaseId) => { + const program = get().currentProgram; + const phase = program.phases.find(val => val["id"] === currentPhaseId); + if (phase) { + return phase["triggers"] as Record[]; + } + throw new Error(`phase with id:"${currentPhaseId}" not found`) + } +})); + +export default useProgramStore; \ No newline at end of file diff --git a/test/setupFlowTests.ts b/test/setupFlowTests.ts index 3ce8c3a..c37cd0e 100644 --- a/test/setupFlowTests.ts +++ b/test/setupFlowTests.ts @@ -2,6 +2,11 @@ import '@testing-library/jest-dom'; import { cleanup } from '@testing-library/react'; import useFlowStore from '../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx'; +if (!globalThis.structuredClone) { + globalThis.structuredClone = (obj: any) => { + return JSON.parse(JSON.stringify(obj)); + }; +} // To make sure that the tests are working, it's important that you are using // this implementation of ResizeObserver and DOMMatrixReadOnly diff --git a/test/utils/programStore.test.ts b/test/utils/programStore.test.ts new file mode 100644 index 0000000..4109eac --- /dev/null +++ b/test/utils/programStore.test.ts @@ -0,0 +1,100 @@ +import useProgramStore, {type ReducedProgram} from "../../src/utils/programStore.ts"; + + +describe('useProgramStore', () => { + beforeEach(() => { + // Reset store before each test + useProgramStore.setState({ + currentProgram: { phases: [] }, + }); + }); + + const mockProgram: ReducedProgram = { + phases: [ + { + id: 'phase-1', + norms: [{ id: 'norm-1' }], + goals: [{ id: 'goal-1' }], + triggers: [{ id: 'trigger-1' }], + }, + { + id: 'phase-2', + norms: [{ id: 'norm-2' }], + goals: [{ id: 'goal-2' }], + triggers: [{ id: 'trigger-2' }], + }, + ], + }; + + it('should set and get the program state', () => { + useProgramStore.getState().setProgramState(mockProgram); + + const program = useProgramStore.getState().getProgramState(); + expect(program).toEqual(mockProgram); + }); + + it('should return the ids of all phases in the program', () => { + useProgramStore.getState().setProgramState(mockProgram); + + const phaseIds = useProgramStore.getState().getPhaseIds(); + expect(phaseIds).toEqual(['phase-1', 'phase-2']); + }); + + it('should return all norms for a given phase', () => { + useProgramStore.getState().setProgramState(mockProgram); + + const norms = useProgramStore.getState().getNormsInPhase('phase-1'); + expect(norms).toEqual([{ id: 'norm-1' }]); + }); + + it('should return all goals for a given phase', () => { + useProgramStore.getState().setProgramState(mockProgram); + + const goals = useProgramStore.getState().getGoalsInPhase('phase-2'); + expect(goals).toEqual([{ id: 'goal-2' }]); + }); + + it('should return all triggers for a given phase', () => { + useProgramStore.getState().setProgramState(mockProgram); + + const triggers = useProgramStore.getState().getTriggersInPhase('phase-1'); + expect(triggers).toEqual([{ id: 'trigger-1' }]); + }); + + it('throws if phase does not exist when getting norms', () => { + useProgramStore.getState().setProgramState(mockProgram); + + expect(() => + useProgramStore.getState().getNormsInPhase('missing-phase') + ).toThrow('phase with id:"missing-phase" not found'); + }); + + it('throws if phase does not exist when getting goals', () => { + useProgramStore.getState().setProgramState(mockProgram); + + expect(() => + useProgramStore.getState().getGoalsInPhase('missing-phase') + ).toThrow('phase with id:"missing-phase" not found'); + }); + + it('throws if phase does not exist when getting triggers', () => { + useProgramStore.getState().setProgramState(mockProgram); + + expect(() => + useProgramStore.getState().getTriggersInPhase('missing-phase') + ).toThrow('phase with id:"missing-phase" not found'); + }); + + // this test should be at the bottom to avoid conflicts with the previous tests + it('should clone program state when setting it (no shared references should exist)', () => { + useProgramStore.getState().setProgramState(mockProgram); + + const storedProgram = useProgramStore.getState().getProgramState(); + + // mutate original + (mockProgram.phases[0].norms as any[]).push({ id: 'norm-mutated' }); + + // store should NOT change + expect(storedProgram.phases[0]['norms']).toHaveLength(1); + }); +}); \ No newline at end of file -- 2.49.1 From f0fe520ea09c0c6eb60014512055f9dfee809cae Mon Sep 17 00:00:00 2001 From: JobvAlewijk Date: Tue, 30 Dec 2025 18:10:51 +0100 Subject: [PATCH 02/98] feat: first version of simple program shown shows up if you run the program ref: N25B-405 --- src/pages/SimpleProgram/SimpleProgram.tsx | 136 ++++++++++++++++++++++ src/pages/VisProgPage/VisProg.tsx | 24 +++- 2 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 src/pages/SimpleProgram/SimpleProgram.tsx diff --git a/src/pages/SimpleProgram/SimpleProgram.tsx b/src/pages/SimpleProgram/SimpleProgram.tsx new file mode 100644 index 0000000..b682b1a --- /dev/null +++ b/src/pages/SimpleProgram/SimpleProgram.tsx @@ -0,0 +1,136 @@ +import React from "react"; +import styles from "../VisProgPage/VisProg.module.css"; + +/* ---------- Types (mirrors backend / reducer output) ---------- */ + +type Norm = { + id: string; + label: string; + norm: string; +}; + +type Goal = { + id: string; + label: string; + description: string; + achieved: boolean; +}; + +type TriggerKeyword = { + id: string; + keyword: string; +}; + +type KeywordTrigger = { + id: string; + label: string; + type: string; + keywords: TriggerKeyword[]; +}; + +type Phase = { + id: string; + label: string; + norms: Norm[]; + goals: Goal[]; + triggers: KeywordTrigger[]; +}; + +type SimpleProgramProps = { + phases: Phase[]; +}; + + +/* ---------- Component ---------- */ + + +/** + * SimpleProgram + * + * Read-only oversight view for a reduced program. + * Displays norms, goals, and triggers grouped per phase. + */ +const SimpleProgram: React.FC = ({ phases }) => { + return ( +
+

Simple Program Overview

+ + {phases.map((phase) => ( +
+

{phase.label}

+ + {/* Norms */} +
+

Norms

+ {phase.norms.length === 0 ? ( +

No norms defined.

+ ) : ( +
    + {phase.norms.map((norm) => ( +
  • + {norm.label}: {norm.norm} +
  • + ))} +
+ )} +
+ + {/* Goals */} +
+

Goals

+ {phase.goals.length === 0 ? ( +

No goals defined.

+ ) : ( +
    + {phase.goals.map((goal) => ( +
  • + {goal.label}: {goal.description}{" "} + + [{goal.achieved ? "✔" : "❌"}] + +
  • + ))} +
+ )} +
+ + {/* Triggers */} +
+

Triggers

+ {phase.triggers.length === 0 ? ( +

No triggers defined.

+ ) : ( +
    + {phase.triggers.map((trigger) => ( +
  • + {trigger.label} ({trigger.type}) +
      + {trigger.keywords.map((kw) => ( +
    • {kw.keyword}
    • + ))} +
    +
  • + ))} +
+ )} +
+
+ ))} +
+ ); +}; + +export default SimpleProgram; diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index 06e072c..60d66de 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -7,7 +7,7 @@ import { MarkerType, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import {useEffect} from "react"; +import {useEffect, useState} from "react"; import {useShallow} from 'zustand/react/shallow'; import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx'; import useFlowStore from './visualProgrammingUI/VisProgStores.tsx'; @@ -15,6 +15,7 @@ import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx'; import styles from './VisProg.module.css' import { NodeReduces, NodeTypes } from './visualProgrammingUI/NodeRegistry.ts'; import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx'; +import SimpleProgram from "../SimpleProgram/SimpleProgram.tsx"; // --| config starting params for flow |-- @@ -138,7 +139,7 @@ function VisualProgrammingUI() { } // currently outputs the prepared program to the console -function runProgram() { +function runProgramm() { const phases = graphReducer(); const program = {phases} console.log(JSON.stringify(program, null, 2)); @@ -174,6 +175,25 @@ function graphReducer() { * @constructor */ function VisProgPage() { + const [showSimpleProgram, setShowSimpleProgram] = useState(false); + const [phases, setPhases] = useState([]); + + const runProgram = () => { + const reducedPhases = graphReducer(); + setPhases(reducedPhases); + setShowSimpleProgram(true); + runProgramm(); + }; + + if (showSimpleProgram) { + return ( + + ); + } + return ( <> -- 2.49.1 From b0a5e4770c372f74e3818519f35cb4e9b4fd94f0 Mon Sep 17 00:00:00 2001 From: JobvAlewijk Date: Tue, 30 Dec 2025 20:56:05 +0100 Subject: [PATCH 03/98] feat: improved visuals and structure ref: N25B-402 --- .../SimpleProgram/SimpleProgram.module.css | 167 ++++++++++++++ src/pages/SimpleProgram/SimpleProgram.tsx | 206 +++++++++++------- 2 files changed, 293 insertions(+), 80 deletions(-) create mode 100644 src/pages/SimpleProgram/SimpleProgram.module.css diff --git a/src/pages/SimpleProgram/SimpleProgram.module.css b/src/pages/SimpleProgram/SimpleProgram.module.css new file mode 100644 index 0000000..69cc65c --- /dev/null +++ b/src/pages/SimpleProgram/SimpleProgram.module.css @@ -0,0 +1,167 @@ +/* ---------- Layout ---------- */ + +.container { + height: 100%; + display: flex; + flex-direction: column; + background: #1e1e1e; + color: #f5f5f5; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: clamp(0.75rem, 2vw, 1.25rem); + background: #2a2a2a; + border-bottom: 1px solid #3a3a3a; +} + +.header h2 { + font-size: clamp(1rem, 2.2vw, 1.4rem); + font-weight: 600; +} + +.controls button { + margin-left: 0.5rem; + padding: 0.4rem 0.9rem; + border-radius: 6px; + border: none; + background: #111; + color: white; + cursor: pointer; +} + +.controls button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* ---------- Content ---------- */ + +.content { + flex: 1; + padding: 2%; +} + +/* ---------- Grid ---------- */ + +.phaseGrid { + height: 100%; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-rows: repeat(2, minmax(0, 1fr)); + gap: 2%; +} + +/* ---------- Box ---------- */ + +.box { + display: flex; + flex-direction: column; + background: #ffffff; + color: #1e1e1e; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25); +} + +.boxHeader { + padding: 0.6rem 0.9rem; + background: linear-gradient(135deg, #dcdcdc, #e9e9e9); + font-style: italic; + font-weight: 500; + font-size: clamp(0.9rem, 1.5vw, 1.05rem); + border-bottom: 1px solid #cfcfcf; +} + +.boxContent { + flex: 1; + padding: 0.8rem 1rem; + overflow-y: auto; +} + +/* ---------- Lists ---------- */ + +.iconList { + list-style: none; + padding: 0; + margin: 0; +} + +.iconList li { + display: flex; + align-items: center; + gap: 0.6rem; + margin-bottom: 0.5rem; + font-size: clamp(0.85rem, 1.3vw, 1rem); +} + +.bulletList { + margin: 0; + padding-left: 1.2rem; +} + +.bulletList li { + margin-bottom: 0.4rem; +} + +/* ---------- Icons ---------- */ + +.successIcon, +.failIcon { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.5rem; + height: 1.5rem; + border-radius: 4px; + font-weight: bold; + color: white; + flex-shrink: 0; +} + +.successIcon { + background: #3cb371; +} + +.failIcon { + background: #e5533d; +} + +/* ---------- Empty ---------- */ + +.empty { + opacity: 0.55; + font-style: italic; + font-size: 0.9rem; +} + +/* ---------- Responsive ---------- */ + +@media (max-width: 900px) { + .phaseGrid { + grid-template-columns: 1fr; + grid-template-rows: repeat(4, minmax(0, 1fr)); + gap: 1rem; + } +} + +.leftControls { + display: flex; + align-items: center; + gap: 1rem; +} + +.backButton { + background: transparent; + border: 1px solid #555; + color: #ddd; + padding: 0.35rem 0.75rem; + border-radius: 6px; + cursor: pointer; +} + +.backButton:hover { + background: #333; +} diff --git a/src/pages/SimpleProgram/SimpleProgram.tsx b/src/pages/SimpleProgram/SimpleProgram.tsx index b682b1a..d548108 100644 --- a/src/pages/SimpleProgram/SimpleProgram.tsx +++ b/src/pages/SimpleProgram/SimpleProgram.tsx @@ -1,7 +1,7 @@ import React from "react"; -import styles from "../VisProgPage/VisProg.module.css"; +import styles from "./SimpleProgram.module.css"; -/* ---------- Types (mirrors backend / reducer output) ---------- */ +/* ---------- Types ---------- */ type Norm = { id: string; @@ -40,95 +40,141 @@ type SimpleProgramProps = { phases: Phase[]; }; +/* ---------- Reusable UI ---------- */ -/* ---------- Component ---------- */ +type BoxProps = { + title: string; + children: React.ReactNode; +}; +const Box: React.FC = ({ title, children }) => ( +
+
{title}
+
{children}
+
+); + +/* ---------- Lists ---------- */ + +const GoalList: React.FC<{ goals: Goal[] }> = ({ goals }) => { + if (goals.length === 0) return

No goals defined.

; -/** - * SimpleProgram - * - * Read-only oversight view for a reduced program. - * Displays norms, goals, and triggers grouped per phase. - */ -const SimpleProgram: React.FC = ({ phases }) => { return ( -
-

Simple Program Overview

- - {phases.map((phase) => ( -
-

{phase.label}

+ ← Back + +

+ Phase {phaseIndex + 1} / {phases.length}: {phase.label} +

- {/* Norms */} -
-

Norms

- {phase.norms.length === 0 ? ( -

No norms defined.

- ) : ( -
    - {phase.norms.map((norm) => ( -
  • - {norm.label}: {norm.norm} -
  • - ))} -
- )} -
+
+ - {/* Goals */} -
-

Goals

- {phase.goals.length === 0 ? ( -

No goals defined.

- ) : ( -
    - {phase.goals.map((goal) => ( -
  • - {goal.label}: {goal.description}{" "} - - [{goal.achieved ? "✔" : "❌"}] - -
  • - ))} -
- )} -
- - {/* Triggers */} -
-

Triggers

- {phase.triggers.length === 0 ? ( -

No triggers defined.

- ) : ( -
    - {phase.triggers.map((trigger) => ( -
  • - {trigger.label} ({trigger.type}) -
      - {trigger.keywords.map((kw) => ( -
    • {kw.keyword}
    • - ))} -
    -
  • - ))} -
- )} -
+
- ))} + +
+ +
+
); }; -- 2.49.1 From cd1aa84f897bfdb70a940307b1cf52ca9f52f25e Mon Sep 17 00:00:00 2001 From: JobvAlewijk Date: Fri, 2 Jan 2026 20:43:20 +0100 Subject: [PATCH 04/98] feat: using programstore ref: N25B-399 --- src/pages/SimpleProgram/SimpleProgram.tsx | 53 +++++++---------------- src/pages/VisProgPage/VisProg.tsx | 20 +++++---- 2 files changed, 27 insertions(+), 46 deletions(-) diff --git a/src/pages/SimpleProgram/SimpleProgram.tsx b/src/pages/SimpleProgram/SimpleProgram.tsx index d548108..16cb512 100644 --- a/src/pages/SimpleProgram/SimpleProgram.tsx +++ b/src/pages/SimpleProgram/SimpleProgram.tsx @@ -1,5 +1,6 @@ import React from "react"; import styles from "./SimpleProgram.module.css"; +import useProgramStore from "../../utils/programStore.ts"; /* ---------- Types ---------- */ @@ -36,10 +37,6 @@ type Phase = { triggers: KeywordTrigger[]; }; -type SimpleProgramProps = { - phases: Phase[]; -}; - /* ---------- Reusable UI ---------- */ type BoxProps = { @@ -63,11 +60,7 @@ const GoalList: React.FC<{ goals: Goal[] }> = ({ goals }) => {
    {goals.map((goal) => (
  • - + {goal.achieved ? "✔" : "✖"} {goal.description} @@ -77,11 +70,8 @@ const GoalList: React.FC<{ goals: Goal[] }> = ({ goals }) => { ); }; -const TriggerList: React.FC<{ triggers: KeywordTrigger[] }> = ({ - triggers, -}) => { - if (triggers.length === 0) - return

    No triggers defined.

    ; +const TriggerList: React.FC<{ triggers: KeywordTrigger[] }> = ({ triggers }) => { + if (triggers.length === 0) return

    No triggers defined.

    ; return (
      @@ -133,48 +123,37 @@ const PhaseGrid: React.FC<{ phase: Phase }> = ({ phase }) => { /* ---------- Main Component ---------- */ -const SimpleProgram: React.FC = ({ phases }) => { +const SimpleProgram: React.FC = () => { + // Get the phases from the program store + const phases = useProgramStore((state) => state.currentProgram.phases) as Phase[]; const [phaseIndex, setPhaseIndex] = React.useState(0); + + // If no phases are available, display a message + if (phases.length === 0) return

      No program loaded.

      ; + const phase = phases[phaseIndex]; return (
      -

      Phase {phaseIndex + 1} / {phases.length}: {phase.label}

      - -
      -
      - -
      +
      + +
      ); }; diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index e289580..3db9a00 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -181,21 +181,23 @@ function graphReducer() { */ function VisProgPage() { const [showSimpleProgram, setShowSimpleProgram] = useState(false); - const [phases, setPhases] = useState([]); + const setProgramState = useProgramStore((state) => state.setProgramState); const runProgram = () => { - const reducedPhases = graphReducer(); - setPhases(reducedPhases); - setShowSimpleProgram(true); - runProgramm(); + const phases = graphReducer(); // reduce graph + setProgramState({ phases }); // <-- save to store + setShowSimpleProgram(true); // show SimpleProgram + runProgramm(); // send to backend if needed }; if (showSimpleProgram) { return ( - +
      + + +
      ); } -- 2.49.1 From d80ced547cc054e37f2192a5fb03dc0c53967178 Mon Sep 17 00:00:00 2001 From: JobvAlewijk Date: Fri, 2 Jan 2026 20:55:24 +0100 Subject: [PATCH 05/98] feat: SimpleProgram no longer relies on types ref: N25B-399 --- src/pages/SimpleProgram/SimpleProgram.tsx | 163 ++++++++++++---------- 1 file changed, 92 insertions(+), 71 deletions(-) diff --git a/src/pages/SimpleProgram/SimpleProgram.tsx b/src/pages/SimpleProgram/SimpleProgram.tsx index 16cb512..56020a4 100644 --- a/src/pages/SimpleProgram/SimpleProgram.tsx +++ b/src/pages/SimpleProgram/SimpleProgram.tsx @@ -2,41 +2,6 @@ import React from "react"; import styles from "./SimpleProgram.module.css"; import useProgramStore from "../../utils/programStore.ts"; -/* ---------- Types ---------- */ - -type Norm = { - id: string; - label: string; - norm: string; -}; - -type Goal = { - id: string; - label: string; - description: string; - achieved: boolean; -}; - -type TriggerKeyword = { - id: string; - keyword: string; -}; - -type KeywordTrigger = { - id: string; - label: string; - type: string; - keywords: TriggerKeyword[]; -}; - -type Phase = { - id: string; - label: string; - norms: Norm[]; - goals: Goal[]; - triggers: KeywordTrigger[]; -}; - /* ---------- Reusable UI ---------- */ type BoxProps = { @@ -53,70 +18,111 @@ const Box: React.FC = ({ title, children }) => ( /* ---------- Lists ---------- */ -const GoalList: React.FC<{ goals: Goal[] }> = ({ goals }) => { - if (goals.length === 0) return

      No goals defined.

      ; +const GoalList: React.FC<{ goals: unknown[] }> = ({ goals }) => { + if (!goals.length) { + return

      No goals defined.

      ; + } return (
        - {goals.map((goal) => ( -
      • - - {goal.achieved ? "✔" : "✖"} - - {goal.description} -
      • - ))} + {goals.map((g, idx) => { + const goal = g as { + id?: string; + description?: string; + achieved?: boolean; + }; + + return ( +
      • + + {goal.achieved ? "✔" : "✖"} + + {goal.description ?? "Unnamed goal"} +
      • + ); + })}
      ); }; -const TriggerList: React.FC<{ triggers: KeywordTrigger[] }> = ({ triggers }) => { - if (triggers.length === 0) return

      No triggers defined.

      ; +const TriggerList: React.FC<{ triggers: unknown[] }> = ({ triggers }) => { + if (!triggers.length) { + return

      No triggers defined.

      ; + } return (
        - {triggers.map((trigger) => ( -
      • - - {trigger.label} -
      • - ))} + {triggers.map((t, idx) => { + const trigger = t as { + id?: string; + label?: string; + }; + + return ( +
      • + + {trigger.label ?? "Unnamed trigger"} +
      • + ); + })}
      ); }; -const NormList: React.FC<{ norms: Norm[] }> = ({ norms }) => { - if (norms.length === 0) return

      No norms defined.

      ; +const NormList: React.FC<{ norms: unknown[] }> = ({ norms }) => { + if (!norms.length) { + return

      No norms defined.

      ; + } return (
        - {norms.map((norm) => ( -
      • {norm.norm}
      • - ))} + {norms.map((n, idx) => { + const norm = n as { + id?: string; + norm?: string; + }; + + return
      • {norm.norm ?? "Unnamed norm"}
      • ; + })}
      ); }; /* ---------- Phase Grid ---------- */ -const PhaseGrid: React.FC<{ phase: Phase }> = ({ phase }) => { +type PhaseGridProps = { + norms: unknown[]; + goals: unknown[]; + triggers: unknown[]; +}; + +const PhaseGrid: React.FC = ({ + norms, + goals, + triggers, +}) => { return (
      - + - + - +

      No conditional norms defined.

      + {/* Let er dus op dat deze erbij moeten */}
      ); }; @@ -124,35 +130,50 @@ const PhaseGrid: React.FC<{ phase: Phase }> = ({ phase }) => { /* ---------- Main Component ---------- */ const SimpleProgram: React.FC = () => { - // Get the phases from the program store - const phases = useProgramStore((state) => state.currentProgram.phases) as Phase[]; + const getPhaseIds = useProgramStore((s) => s.getPhaseIds); + const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase); + const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase); + const getTriggersInPhase = useProgramStore((s) => s.getTriggersInPhase); + + const phaseIds = getPhaseIds(); const [phaseIndex, setPhaseIndex] = React.useState(0); - // If no phases are available, display a message - if (phases.length === 0) return

      No program loaded.

      ; + if (phaseIds.length === 0) { + return

      No program loaded.

      ; + } - const phase = phases[phaseIndex]; + const phaseId = phaseIds[phaseIndex]; return (

      - Phase {phaseIndex + 1} / {phases.length}: {phase.label} + Phase {phaseIndex + 1} / {phaseIds.length}

      - -
      - +
      ); -- 2.49.1 From 7b05c7344c23f8b0c71b7e6f8811422e969beac4 Mon Sep 17 00:00:00 2001 From: JobvAlewijk Date: Fri, 2 Jan 2026 21:06:41 +0100 Subject: [PATCH 06/98] feat: added tests ref: N25B-399 --- src/pages/SimpleProgram/SimpleProgram.tsx | 64 ++++++++++------- test/pages/simpleProgram/SimpleProgram.tsx | 83 ++++++++++++++++++++++ 2 files changed, 120 insertions(+), 27 deletions(-) create mode 100644 test/pages/simpleProgram/SimpleProgram.tsx diff --git a/src/pages/SimpleProgram/SimpleProgram.tsx b/src/pages/SimpleProgram/SimpleProgram.tsx index 56020a4..0f63653 100644 --- a/src/pages/SimpleProgram/SimpleProgram.tsx +++ b/src/pages/SimpleProgram/SimpleProgram.tsx @@ -2,8 +2,9 @@ import React from "react"; import styles from "./SimpleProgram.module.css"; import useProgramStore from "../../utils/programStore.ts"; -/* ---------- Reusable UI ---------- */ - +/** + * Generic container box with a header and content area. + */ type BoxProps = { title: string; children: React.ReactNode; @@ -16,8 +17,10 @@ const Box: React.FC = ({ title, children }) => (
); -/* ---------- Lists ---------- */ - +/** + * Renders a list of goals for a phase. + * Expects goal-like objects from the program store. + */ const GoalList: React.FC<{ goals: unknown[] }> = ({ goals }) => { if (!goals.length) { return

No goals defined.

; @@ -49,6 +52,9 @@ const GoalList: React.FC<{ goals: unknown[] }> = ({ goals }) => { ); }; +/** + * Renders a list of triggers for a phase. + */ const TriggerList: React.FC<{ triggers: unknown[] }> = ({ triggers }) => { if (!triggers.length) { return

No triggers defined.

; @@ -73,6 +79,9 @@ const TriggerList: React.FC<{ triggers: unknown[] }> = ({ triggers }) => { ); }; +/** + * Renders a list of norms for a phase. + */ const NormList: React.FC<{ norms: unknown[] }> = ({ norms }) => { if (!norms.length) { return

No norms defined.

; @@ -92,8 +101,9 @@ const NormList: React.FC<{ norms: unknown[] }> = ({ norms }) => { ); }; -/* ---------- Phase Grid ---------- */ - +/** + * Displays all phase-related information in a grid layout. + */ type PhaseGridProps = { norms: unknown[]; goals: unknown[]; @@ -104,31 +114,31 @@ const PhaseGrid: React.FC = ({ norms, goals, triggers, -}) => { - return ( -
- - - +}) => ( +
+ + + - - - + + + - - - + + + - -

No conditional norms defined.

-
- {/* Let er dus op dat deze erbij moeten */} -
- ); -}; - -/* ---------- Main Component ---------- */ + +

No conditional norms defined.

+
+
+); +/** + * Main program viewer. + * Reads all data from the program store and allows + * navigating between phases. + */ const SimpleProgram: React.FC = () => { const getPhaseIds = useProgramStore((s) => s.getPhaseIds); const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase); diff --git a/test/pages/simpleProgram/SimpleProgram.tsx b/test/pages/simpleProgram/SimpleProgram.tsx new file mode 100644 index 0000000..22fcbbf --- /dev/null +++ b/test/pages/simpleProgram/SimpleProgram.tsx @@ -0,0 +1,83 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import SimpleProgram from "../../../src/pages/SimpleProgram/SimpleProgram"; +import useProgramStore from "../../../src/utils/programStore"; + +/** + * Helper to preload the program store before rendering. + */ +function loadProgram(phases: Record[]) { + useProgramStore.getState().setProgramState({ phases }); +} + +describe("SimpleProgram", () => { + beforeEach(() => { + loadProgram([]); + }); + + test("shows empty state when no program is loaded", () => { + render(); + expect(screen.getByText("No program loaded.")).toBeInTheDocument(); + }); + + test("renders first phase content", () => { + loadProgram([ + { + id: "phase-1", + norms: [{ id: "n1", norm: "Be polite" }], + goals: [{ id: "g1", description: "Finish task", achieved: true }], + triggers: [{ id: "t1", label: "Keyword trigger" }], + }, + ]); + + render(); + + expect(screen.getByText("Phase 1 / 1")).toBeInTheDocument(); + expect(screen.getByText("Be polite")).toBeInTheDocument(); + expect(screen.getByText("Finish task")).toBeInTheDocument(); + expect(screen.getByText("Keyword trigger")).toBeInTheDocument(); + }); + + test("allows navigating between phases", () => { + loadProgram([ + { + id: "phase-1", + norms: [], + goals: [], + triggers: [], + }, + { + id: "phase-2", + norms: [{ id: "n2", norm: "Be careful" }], + goals: [], + triggers: [], + }, + ]); + + render(); + + expect(screen.getByText("Phase 1 / 2")).toBeInTheDocument(); + + fireEvent.click(screen.getByText("Next ▶")); + + expect(screen.getByText("Phase 2 / 2")).toBeInTheDocument(); + expect(screen.getByText("Be careful")).toBeInTheDocument(); + }); + + test("prev button is disabled on first phase", () => { + loadProgram([ + { id: "phase-1", norms: [], goals: [], triggers: [] }, + ]); + + render(); + expect(screen.getByText("◀ Prev")).toBeDisabled(); + }); + + test("next button is disabled on last phase", () => { + loadProgram([ + { id: "phase-1", norms: [], goals: [], triggers: [] }, + ]); + + render(); + expect(screen.getByText("Next ▶")).toBeDisabled(); + }); +}); -- 2.49.1 From 7a89b0aedd18d1fc6d5be0b1414cd1dfafc4e382 Mon Sep 17 00:00:00 2001 From: Tuurminator69 Date: Sat, 3 Jan 2026 21:07:32 +0100 Subject: [PATCH 07/98] feat: added a skeleton for the monitoringpage. ref: N25B-398 --- .../MonitoringPage/MonitoringPage.module.css | 130 ++++++++++++++++++ src/pages/MonitoringPage/MonitoringPage.tsx | 118 ++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 src/pages/MonitoringPage/MonitoringPage.module.css create mode 100644 src/pages/MonitoringPage/MonitoringPage.tsx diff --git a/src/pages/MonitoringPage/MonitoringPage.module.css b/src/pages/MonitoringPage/MonitoringPage.module.css new file mode 100644 index 0000000..3db4884 --- /dev/null +++ b/src/pages/MonitoringPage/MonitoringPage.module.css @@ -0,0 +1,130 @@ +/* MonitoringPage UI*/ + +.dashboard-container { + display: grid; + grid-template-areas: + 'header header header' + 'main logs logs' + 'footer footer footer'; + grid-template-columns: 2fr 1fr 1fr; + gap: 1rem; + padding: 1rem; + background-color: #f5f5f5; + font-family: Arial, sans-serif; +} + +.experiment-overview { + grid-area: header; + display: flex; + justify-content: space-between; + align-items: flex-start; + background: white; + border-radius: 8px; + padding: 1rem; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); +} + +.phase-progress .phase { + display: inline-block; + width: 25px; + height: 25px; + border-radius: 5px; + margin: 0 3px; + text-align: center; + line-height: 25px; + background: #ccc; +} + +.phase.active { + background-color: #5cb85c; + color: white; +} + +.phase.current { + background-color: #f0ad4e; + color: white; +} + +.experiment-controls button { + margin-right: 5px; + border: none; + background: none; + cursor: pointer; + font-size: 1.2rem; +} + +.connection-status .connected { + color: green; + font-weight: bold; +} + +.phase-overview { + grid-area: main; + background: white; + border-radius: 8px; + padding: 1rem; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); +} + +.phase-overview section { + margin-bottom: 1rem; +} + +.phase-overview ul { + list-style-type: none; + padding: 0; +} + +.phase-overview li.checked::before { + content: '✔️ '; +} + +.logs { + grid-area: logs; + background: white; + border-radius: 8px; + padding: 1rem; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); +} + +.logs textarea { + width: 100%; + height: 200px; + margin-top: 0.5rem; +} + +.controls-section { + grid-area: footer; + display: flex; + justify-content: space-between; + gap: 1rem; + background: white; + border-radius: 8px; + padding: 1rem; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); +} + +.gestures, .speech, .direct-speech { + flex: 1; +} + +.direct-speech .speech-input { + display: flex; + margin-top: 0.5rem; +} + +.direct-speech input { + flex: 1; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px 0 0 4px; +} + +.direct-speech button { + background: #007bff; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 0 4px 4px 0; + cursor: pointer; +} diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx new file mode 100644 index 0000000..86142b7 --- /dev/null +++ b/src/pages/MonitoringPage/MonitoringPage.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import styles from './MonitoringPage.module.css' + +export default function ExperimentDashboard() { + return ( +
+
+
+

Experiment Overview

+

Phase name: Rhyming fish

+
+ 1 + 2 + 3 + 4 + 5 +
+
+ +
+

Experiment Controls

+
+ + + + +
+
+ +
+

Connection:

+

● Robot is connected

+
+
+ +
+
+

Goals

+
    +
  • Convince the RP that you are a fish
  • +
  • Reference Shakespeare
  • +
  • Give a compliment
  • +
+
+ +
+

Triggers

+
    +
  • Convince the RP that you are a fish
  • +
  • Reference Shakespeare
  • +
  • Give a compliment
  • +
+
+ +
+

Norms

+
    +
  • Rhyme when talking
  • +
  • Talk like a fish
  • +
+
+ +
+

Conditional Norms

+
    +
  • “RP is sad” - Be nice
  • +
+
+
+ + + +
+
+

Controls

+
    +
  • Gesture: Wave Left Hand
  • +
  • Gesture: Wave Right Hand
  • +
  • Gesture: Left Thumbs Up
  • +
  • Gesture: Right Thumbs Up
  • +
+
+ +
+

Speech Options

+
    +
  • "Hello, my name is pepper."
  • +
  • "How is the weather today?"
  • +
  • "I like your outfit, very pretty."
  • +
  • "How is your day going?"
  • +
+
+ +
+

Direct Pepper Speech

+
    +
  • [time] Send: *Previous message*
  • +
  • [time] Send: *Previous message*
  • +
  • [time] Send: *Previous message*
  • +
+
+ + +
+
+
+
+ ); +} -- 2.49.1 From e53e1a395849f9f2253a5468a2fae3234341e710 Mon Sep 17 00:00:00 2001 From: Tuurminator69 Date: Sat, 3 Jan 2026 21:59:51 +0100 Subject: [PATCH 08/98] feat: can go to a skeleton monitoringpage ref: N25B-398 --- src/App.tsx | 2 ++ src/pages/Home/Home.tsx | 1 + src/pages/MonitoringPage/MonitoringPage.tsx | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 75d423d..9ff7473 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx' import VisProg from "./pages/VisProgPage/VisProg.tsx"; import {useState} from "react"; import Logging from "./components/Logging/Logging.tsx"; +import MonitoringPage from './pages/MonitoringPage/MonitoringPage.tsx' function App(){ const [showLogs, setShowLogs] = useState(false); @@ -25,6 +26,7 @@ function App(){ } /> } /> } /> + } /> {showLogs && } diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index c998e25..491c62f 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -23,6 +23,7 @@ function Home() { Editor → Template → Connected Robots → + MonitoringPage → ) diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx index 86142b7..b2db6c2 100644 --- a/src/pages/MonitoringPage/MonitoringPage.tsx +++ b/src/pages/MonitoringPage/MonitoringPage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styles from './MonitoringPage.module.css' -export default function ExperimentDashboard() { +export default function MonitoringPage() { return (
-- 2.49.1 From 4bd67debf3a6a8a47c90fb3c0bf9d33ddea74c51 Mon Sep 17 00:00:00 2001 From: Tuurminator69 Date: Sun, 4 Jan 2026 20:07:44 +0100 Subject: [PATCH 09/98] feat: added and changed the monitoringpage a lot ref: N25B-398 --- .../MonitoringPage/MonitoringPage.module.css | 131 ++++++++++++------ src/pages/MonitoringPage/MonitoringPage.tsx | 83 ++++++----- 2 files changed, 134 insertions(+), 80 deletions(-) diff --git a/src/pages/MonitoringPage/MonitoringPage.module.css b/src/pages/MonitoringPage/MonitoringPage.module.css index 3db4884..451b229 100644 --- a/src/pages/MonitoringPage/MonitoringPage.module.css +++ b/src/pages/MonitoringPage/MonitoringPage.module.css @@ -1,88 +1,122 @@ -/* MonitoringPage UI*/ - -.dashboard-container { +.dashboardContainer { display: grid; + grid-template-columns: 2fr 1fr; /* Left = content, Right = logs */ + grid-template-rows: auto 1fr auto; /* Header, Main, Footer */ grid-template-areas: - 'header header header' - 'main logs logs' - 'footer footer footer'; - grid-template-columns: 2fr 1fr 1fr; + "header logs" + "main logs" + "footer footer"; gap: 1rem; padding: 1rem; background-color: #f5f5f5; font-family: Arial, sans-serif; } -.experiment-overview { +/* HEADER */ +.experimentOverview { grid-area: header; display: flex; justify-content: space-between; align-items: flex-start; - background: white; - border-radius: 8px; + background: #fff; padding: 1rem; box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + position: static; /* ensures it scrolls away */ } -.phase-progress .phase { +.phaseProgress { + margin-top: 0.5rem; +} + +.phase { display: inline-block; width: 25px; height: 25px; - border-radius: 5px; margin: 0 3px; text-align: center; line-height: 25px; background: #ccc; } -.phase.active { +.completed { background-color: #5cb85c; color: white; } -.phase.current { +.current { background-color: #f0ad4e; color: white; } -.experiment-controls button { - margin-right: 5px; - border: none; - background: none; - cursor: pointer; - font-size: 1.2rem; -} - -.connection-status .connected { +.connected { color: green; font-weight: bold; } -.phase-overview { +.pause{ + background-color: green; +} + +.next { + background-color: #ccc; +} + +.restartPhase{ + background-color: rgb(255, 123, 0); +} + +.restartExperiment{ + background-color: red; +} + +/* MAIN GRID */ +.phaseOverview { grid-area: main; - background: white; - border-radius: 8px; + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, auto); + gap: 1rem; + background: #fff; padding: 1rem; box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); } -.phase-overview section { - margin-bottom: 1rem; +.phaseBox { + background: #f9f9f9; + border: 1px solid #ddd; + padding: 1rem; + display: flex; + flex-direction: column; + box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.05); } -.phase-overview ul { - list-style-type: none; - padding: 0; +.phaseOverviewText { + grid-column: 1 / -1; /* make the title span across both columns */ + font-size: 1.4rem; + font-weight: 600; + margin: 0; /* remove default section margin */ + padding: 0.25rem 0; /* smaller internal space */ } -.phase-overview li.checked::before { +.phaseOverviewText h3{ + margin: 0; /* removes top/bottom whitespace */ + padding: 0; /* keeps spacing tight */ +} + +.phaseBox h3 { + margin-top: 0; + border-bottom: 1px solid #ddd; + padding-bottom: 0.4rem; +} + +.checked::before { content: '✔️ '; } +/* LOGS */ .logs { grid-area: logs; - background: white; - border-radius: 8px; + background: #fff; padding: 1rem; box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); } @@ -93,38 +127,49 @@ margin-top: 0.5rem; } -.controls-section { +/* FOOTER */ +.controlsSection { grid-area: footer; display: flex; justify-content: space-between; gap: 1rem; - background: white; - border-radius: 8px; + background: #fff; padding: 1rem; box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); } -.gestures, .speech, .direct-speech { +.gestures, +.speech, +.directSpeech { flex: 1; } -.direct-speech .speech-input { +.speechInput { display: flex; margin-top: 0.5rem; } -.direct-speech input { +.speechInput input { flex: 1; padding: 0.5rem; border: 1px solid #ccc; - border-radius: 4px 0 0 4px; } -.direct-speech button { +.speechInput button { background: #007bff; color: white; border: none; padding: 0.5rem 1rem; - border-radius: 0 4px 4px 0; cursor: pointer; } + +/* RESPONSIVE */ +@media (max-width: 900px) { + .phaseOverview { + grid-template-columns: 1fr; + } + + .controlsSection { + flex-direction: column; + } +} diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx index b2db6c2..3fe3b24 100644 --- a/src/pages/MonitoringPage/MonitoringPage.tsx +++ b/src/pages/MonitoringPage/MonitoringPage.tsx @@ -1,58 +1,65 @@ import React from 'react'; -import styles from './MonitoringPage.module.css' +import styles from './MonitoringPage.module.css'; export default function MonitoringPage() { return ( -
-
-
+
+ {/* HEADER */} +
+

Experiment Overview

Phase name: Rhyming fish

-
- 1 - 2 - 3 - 4 - 5 +
+ 1 + 2 + 3 + 4 + 5
-
+

Experiment Controls

-
- - - - +
+ + + +
-
+

Connection:

-

● Robot is connected

+

● Robot is connected

-
-
+ {/* MAIN GRID */} + +
+
+

Phase Overview

+
+ +

Goals

    -
  • Convince the RP that you are a fish
  • +
  • Convince the RP that you are a fish
  • Reference Shakespeare
  • Give a compliment
-
+

Triggers

    -
  • Convince the RP that you are a fish
  • +
  • Convince the RP that you are a fish
  • Reference Shakespeare
  • Give a compliment
-
+

Norms

  • Rhyme when talking
  • @@ -60,7 +67,7 @@ export default function MonitoringPage() {
-
+

Conditional Norms

  • “RP is sad” - Be nice
  • @@ -68,19 +75,21 @@ export default function MonitoringPage() {
-
{showLogs && } diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 491c62f..c998e25 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -23,7 +23,6 @@ function Home() { Editor → Template → Connected Robots → - MonitoringPage →
) -- 2.49.1 From 12ef2ef86e971bca7f1075c0bee0780172f8aa6c Mon Sep 17 00:00:00 2001 From: Pim Hutting Date: Tue, 6 Jan 2026 15:15:36 +0100 Subject: [PATCH 12/98] feat: added forced speech/gestures +overrides ref: N25B-400 --- src/pages/MonitoringPage/Components.tsx | 149 ++++++++++++++++++++ src/pages/MonitoringPage/MonitoringPage.tsx | 95 ++----------- 2 files changed, 161 insertions(+), 83 deletions(-) create mode 100644 src/pages/MonitoringPage/Components.tsx diff --git a/src/pages/MonitoringPage/Components.tsx b/src/pages/MonitoringPage/Components.tsx new file mode 100644 index 0000000..5dc05f6 --- /dev/null +++ b/src/pages/MonitoringPage/Components.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; +import styles from './MonitoringPage.module.css'; + +/** + * HELPER: Unified sender function + * In a real app, you might move this to a /services or /hooks folder + */ +const sendUserInterrupt = async (type: string, context: string) => { + try { + const response = await fetch("http://localhost:8000/button_pressed", { + 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); + } +}; + +// --- GESTURE COMPONENT --- +export const GestureControls: React.FC = () => { + const [selectedGesture, setSelectedGesture] = useState("animations/Stand/BodyTalk/Speaking/BodyTalk_1"); + + const gestures = [ + { label: "Body Talk 1", value: "animations/Stand/BodyTalk/Speaking/BodyTalk_1" }, + { label: "Thinking 8", value: "animations/Stand/Gestures/Thinking_8" }, + { label: "Thinking 1", value: "animations/Stand/Gestures/Thinking_1" }, + { label: "Happy", value: "animations/Stand/Emotions/Positive/Happy_1" }, + ]; + + return ( +
+

Gestures

+
+ + +
+
+ ); +}; + +// --- PRESET SPEECH COMPONENT --- +export const SpeechPresets: React.FC = () => { + const phrases = [ + { label: "Hello, I'm Pepper", text: "Hello, I'm Pepper" }, + { label: "Repeat please", text: "Could you repeat that please" }, + { label: "About yourself", text: "Tell me something about yourself" }, + ]; + + return ( +
+

Speech Presets

+
    + {phrases.map((phrase, i) => ( +
  • + +
  • + ))} +
+
+ ); +}; + +// --- DIRECT SPEECH (INPUT) COMPONENT --- +export const DirectSpeechInput: React.FC = () => { + const [text, setText] = useState(""); + + const handleSend = () => { + if (!text.trim()) return; + sendUserInterrupt("speech", text); + setText(""); // Clear after sending + }; + + return ( +
+

Direct Pepper Speech

+
+ setText(e.target.value)} + placeholder="Type message..." + onKeyDown={(e) => e.key === 'Enter' && handleSend()} + /> + +
+
+ ); +}; + +// --- interface for goals/triggers/norms/conditional norms --- +interface StatusListProps { + title: string; + items: any[]; + type: 'goal' | 'trigger' | 'norm'| 'cond_norm'; +} + +// --- STATUS LIST COMPONENT --- +export const StatusList: React.FC = ({ title, items, type }) => { + return ( +
+

{title}

+
    + {items.length > 0 ? ( + items.map((item, idx) => { + const isAchieved = item.achieved; + const showIndicator = type !== 'norm'; + const canOverride = showIndicator && !isAchieved; + + return ( +
  • + {showIndicator && ( + canOverride && sendUserInterrupt("override", String(item.id))} + title={canOverride ? `Send override for ID: ${item.id}` : 'Achieved'} + style={{ cursor: canOverride ? 'pointer' : 'default' }} + > + {isAchieved ? "✔️" : "❌"} + + )} + + + {item.description || item.label || item.norm} + +
  • + ); + }) + ) : ( +

    No {title.toLowerCase()} defined.

    + )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx index 32f7b6b..4c1385a 100644 --- a/src/pages/MonitoringPage/MonitoringPage.tsx +++ b/src/pages/MonitoringPage/MonitoringPage.tsx @@ -1,6 +1,7 @@ import React from 'react'; import styles from './MonitoringPage.module.css'; import useProgramStore from "../../utils/programStore.ts"; +import { GestureControls, SpeechPresets, DirectSpeechInput, StatusList } from './Components'; type Goal = { id?: string | number; description?: string; achieved?: boolean }; type Trigger = { id?: string | number; label?: string ; achieved?: boolean }; @@ -66,57 +67,14 @@ const MonitoringPage: React.FC = () => {

Phase Overview

-
-

Goals

-
    - {goals.length > 0 ? ( - goals.map((goal, idx) => ( -
  • - {goal.achieved ? "✔️" : "❌"} {goal.description} -
  • - ))) : ( -

    No goals defined.

    - ) - } -
-
- -
-

Triggers

-
    - {triggers.length > 0 ? ( - triggers.map((trigger, idx) => ( -
  • - {trigger.achieved ? "✔️" : "❌"} {trigger.label} -
  • - ))) : ( -

    No triggers defined.

    - ) - } -
-
- -
-

Norms

-
    - {norms.length > 0 ? ( - norms.map((norm, idx) => ( -
  • - {norm.norm} -
  • - ))) : ( -

    No norms defined.

    - ) - } -
-
- -
-

Conditional Norms

-
    -
  • “RP is sad” - Be nice
  • -
-
+ + + + {/* LOGS */} @@ -133,38 +91,9 @@ const MonitoringPage: React.FC = () => { {/* FOOTER */}
-
-

Controls

-
    -
  • Gesture: Wave Left Hand
  • -
  • Gesture: Wave Right Hand
  • -
  • Gesture: Left Thumbs Up
  • -
  • Gesture: Right Thumbs Up
  • -
-
- -
-

Speech Options

-
    -
  • \"Hello, my name is pepper.\"
  • -
  • \"How is the weather today?\"
  • -
  • \"I like your outfit, very pretty.\"
  • -
  • \"How is your day going?\"
  • -
-
- -
-

Direct Pepper Speech

-
    -
  • [time] Send: *Previous message*
  • -
  • [time] Send: *Previous message*
  • -
  • [time] Send: *Previous message*
  • -
-
- - -
-
+ + +
); -- 2.49.1 From 794e63808152704232d133f1dc658070a373a006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Wed, 7 Jan 2026 11:54:29 +0100 Subject: [PATCH 13/98] feat: start with functionality ref: N25B-400 --- src/pages/MonitoringPage/MonitoringPage.tsx | 24 ++++++++++++++++++- src/pages/MonitoringPage/MonitoringPageAPI.ts | 19 +++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/pages/MonitoringPage/MonitoringPageAPI.ts diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx index 32f7b6b..d19dbae 100644 --- a/src/pages/MonitoringPage/MonitoringPage.tsx +++ b/src/pages/MonitoringPage/MonitoringPage.tsx @@ -1,6 +1,7 @@ import React from 'react'; import styles from './MonitoringPage.module.css'; import useProgramStore from "../../utils/programStore.ts"; +import { nextPhase } from ".//MonitoringPageAPI.ts" type Goal = { id?: string | number; description?: string; achieved?: boolean }; type Trigger = { id?: string | number; label?: string ; achieved?: boolean }; @@ -13,6 +14,9 @@ const MonitoringPage: React.FC = () => { const getGoalsInPhase = useProgramStore((s) => s.getGoalsInPhase); const getTriggersInPhase = useProgramStore((s) => s.getTriggersInPhase); + // Can be used to block actions until feedback from CB. + const [loading, setLoading] = React.useState(false); + const phaseIds = getPhaseIds(); const [phaseIndex, setPhaseIndex] = React.useState(0); @@ -26,6 +30,18 @@ const MonitoringPage: React.FC = () => { const triggers = getTriggersInPhase(phaseId) as Trigger[]; const norms = getNormsInPhase(phaseId) as Norm[]; + // Handle logic of 'next' button. + const handleNext = async () => { + try { + setLoading(true); + await nextPhase(); + } catch (err) { + console.log("Monitoring Page could not advance to the next phase:") + console.error(err); + } finally { + setLoading(false); + } + }; return (
@@ -47,7 +63,13 @@ const MonitoringPage: React.FC = () => {

Experiment Controls

- +
diff --git a/src/pages/MonitoringPage/MonitoringPageAPI.ts b/src/pages/MonitoringPage/MonitoringPageAPI.ts new file mode 100644 index 0000000..d08f78c --- /dev/null +++ b/src/pages/MonitoringPage/MonitoringPageAPI.ts @@ -0,0 +1,19 @@ +const API_BASE = "http://localhost:8000/"; // Change depending on Pims interup agent/ correct endpoint + + +/** + * Sends an API call to the CB for going to the next phase. + * In case we can't go to the next phase, the function will throw an error. + */ +export async function nextPhase(): Promise { + const res = await fetch(`${API_BASE}/experiment/next`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!res.ok) { + throw new Error("Failed to advance to next phase"); + } +} \ No newline at end of file -- 2.49.1 From c9df87929bde2f075f5def091379705c4fe120ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Wed, 7 Jan 2026 15:09:44 +0100 Subject: [PATCH 14/98] feat: add the buttons for next, reset phase and reset experiment ref: N25B-400 --- src/pages/MonitoringPage/MonitoringPage.tsx | 40 +++++++++++--- src/pages/MonitoringPage/MonitoringPageAPI.ts | 55 +++++++++++++++---- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx index 6d985a1..1ffee92 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 } from './Components'; -import { nextPhase } from ".//MonitoringPageAPI.ts" +import { nextPhase, resetExperiment, resetPhase } from ".//MonitoringPageAPI.ts" type Goal = { id?: string | number; description?: string; achieved?: boolean }; type Trigger = { id?: string | number; label?: string ; achieved?: boolean }; @@ -32,18 +32,28 @@ const MonitoringPage: React.FC = () => { const norms = getNormsInPhase(phaseId) as Norm[]; // Handle logic of 'next' button. - const handleNext = async () => { + + const handleButton = async (button: string, _context?: string, _endpoint?: string) => { try { setLoading(true); - await nextPhase(); + switch (button) { + case "nextPhase": + await nextPhase(); + break; + case "resetPhase": + await resetPhase(); + break; + case "resetExperiment": + await resetExperiment(); + break; + default: + } } catch (err) { - console.log("Monitoring Page could not advance to the next phase:") console.error(err); } finally { setLoading(false); } - }; - + } return (
{/* HEADER */} @@ -66,13 +76,25 @@ const MonitoringPage: React.FC = () => { - - + +
diff --git a/src/pages/MonitoringPage/MonitoringPageAPI.ts b/src/pages/MonitoringPage/MonitoringPageAPI.ts index d08f78c..bd0a3ca 100644 --- a/src/pages/MonitoringPage/MonitoringPageAPI.ts +++ b/src/pages/MonitoringPage/MonitoringPageAPI.ts @@ -1,4 +1,23 @@ -const API_BASE = "http://localhost:8000/"; // Change depending on Pims interup agent/ correct endpoint +const API_BASE = "http://localhost:8000/button_pressed"; // Change depending on Pims interup agent/ correct endpoint + + +/** + * 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) => { + try { + const response = await fetch(`${API_BASE}${endpoint ?? ""}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type, context }), + }); + if (!response.ok) throw new Error("Backend response error"); + console.log(`API Call send - Type: ${type}, Context: ${context} ${endpoint ? `, Endpoint: ${endpoint}` : ""}`); + } catch (err) { + console.error(`Failed to send api call:`, err); + } +}; /** @@ -6,14 +25,28 @@ const API_BASE = "http://localhost:8000/"; // Change depending on Pims interup a * In case we can't go to the next phase, the function will throw an error. */ export async function nextPhase(): Promise { - const res = await fetch(`${API_BASE}/experiment/next`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); + const type = "next_phase" + const context = "" + sendAPICall(type, context) +} - if (!res.ok) { - throw new Error("Failed to advance to next phase"); - } -} \ No newline at end of file + +/** + * 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 { + const type = "reset_phase" + const context = "" + sendAPICall(type, context) +} + +/** + * Sends an API call to the CB for going to reset the experiment + * In case we can't go to the next phase, the function will throw an error. + */ +export async function resetExperiment(): Promise { + const type = "reset_experiment" + const context = "" + sendAPICall(type, context) +} -- 2.49.1 From a1e242e391bf6603e25a668b3ee67e0c0b32ccf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Wed, 7 Jan 2026 18:31:56 +0100 Subject: [PATCH 15/98] feat: added the functionality for the play, pause, next phase, reset phase, reset experiment buttons ref: N25B-400 --- src/pages/MonitoringPage/Components.tsx | 10 ++++- .../MonitoringPage/MonitoringPage.module.css | 8 +++- src/pages/MonitoringPage/MonitoringPage.tsx | 37 +++++++++++++++++-- src/pages/MonitoringPage/MonitoringPageAPI.ts | 12 ++++++ src/pages/VisProgPage/VisProg.tsx | 1 - 5 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/pages/MonitoringPage/Components.tsx b/src/pages/MonitoringPage/Components.tsx index 5dc05f6..1e21eb1 100644 --- a/src/pages/MonitoringPage/Components.tsx +++ b/src/pages/MonitoringPage/Components.tsx @@ -103,9 +103,17 @@ export const DirectSpeechInput: React.FC = () => { }; // --- interface for goals/triggers/norms/conditional norms --- +type StatusItem = { + id?: string | number; + achieved?: boolean; + description?: string; + label?: string; + norm?: string; +}; + interface StatusListProps { title: string; - items: any[]; + items: StatusItem[]; type: 'goal' | 'trigger' | 'norm'| 'cond_norm'; } diff --git a/src/pages/MonitoringPage/MonitoringPage.module.css b/src/pages/MonitoringPage/MonitoringPage.module.css index 451b229..1d76c46 100644 --- a/src/pages/MonitoringPage/MonitoringPage.module.css +++ b/src/pages/MonitoringPage/MonitoringPage.module.css @@ -53,12 +53,16 @@ font-weight: bold; } -.pause{ +.pausePlayInactive{ + background-color: gray; +} + +.pausePlayActive{ background-color: green; } .next { - background-color: #ccc; + background-color: lightgray; } .restartPhase{ diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx index 1ffee92..e6dbd25 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 } from './Components'; -import { nextPhase, resetExperiment, resetPhase } from ".//MonitoringPageAPI.ts" +import { nextPhase, pauseExperiment, playExperiment, resetExperiment, resetPhase } from ".//MonitoringPageAPI.ts" type Goal = { id?: string | number; description?: string; achieved?: boolean }; type Trigger = { id?: string | number; label?: string ; achieved?: boolean }; @@ -17,9 +17,10 @@ const MonitoringPage: React.FC = () => { // Can be used to block actions until feedback from CB. const [loading, setLoading] = React.useState(false); + const [isPlaying, setIsPlaying] = React.useState(false); const phaseIds = getPhaseIds(); - const [phaseIndex, setPhaseIndex] = React.useState(0); + const [phaseIndex, _] = React.useState(0); if (phaseIds.length === 0) { return

No program loaded.

; @@ -37,6 +38,12 @@ const MonitoringPage: React.FC = () => { try { setLoading(true); switch (button) { + case "pause": + await pauseExperiment(); + break; + case "play": + await playExperiment(); + break; case "nextPhase": await nextPhase(); break; @@ -73,7 +80,27 @@ const MonitoringPage: React.FC = () => {

Experiment Controls

- + {/*Pause button*/} + + + {/*Play button*/} + + + {/*Next button*/} + + {/*Restart Phase button*/} + + {/*Restart Experiment button*/}