From b10dbae4885e8194cd21d9bcc6c0bc6fed37e3ef Mon Sep 17 00:00:00 2001 From: JGerla Date: Sat, 20 Dec 2025 22:58:22 +0100 Subject: [PATCH 001/101] 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 002/101] 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 003/101] 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 004/101] 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 005/101] 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 006/101] 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 007/101] 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 008/101] 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 009/101] 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 012/101] 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 013/101] 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 014/101] 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 015/101] 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*/} +
; +} + +/** + * A button that opens a download dialog for experiment logs when pressed. + */ +function DownloadButton() { + const [showModal, setShowModal] = useState(false); + const [filenames, setFilenames] = useState(null); + + async function getFiles(): Promise { + const response = await fetch("http://localhost:8000/api/logs/files"); + const files = await response.json(); + files.sort(); + return files; + } + + useEffect(() => { + getFiles().then(setFilenames); + }, [showModal]); + + return <> + + setShowModal(false)} classname={"padding-0 round-lg"}> + { + setFilenames(null); + const files = await delayedResolve(getFiles(), 250); + setFilenames(files); + }} /> + + ; +} + +/** + * A component for rendering experiment logs. This component uses the `useLogs` hook with a filter to show only + * experiment logs. + */ +export default function ExperimentLogs() { + // Show only experiment logs in this logger + const filters = useMemo(() => new Map([ + [ + EXPERIMENT_FILTER_KEY, + { + predicate: (r) => r.name == EXPERIMENT_LOGGER_NAME, + priority: 999, + value: null, + } as LogFilterPredicate, + ], + ]), []); + + const { filteredLogs } = useLogs(filters); + + return ; +} \ No newline at end of file diff --git a/src/utils/capitalize.ts b/src/utils/capitalize.ts new file mode 100644 index 0000000..9142c62 --- /dev/null +++ b/src/utils/capitalize.ts @@ -0,0 +1,3 @@ +export default function (s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} diff --git a/src/utils/delayedResolve.ts b/src/utils/delayedResolve.ts new file mode 100644 index 0000000..702bc2e --- /dev/null +++ b/src/utils/delayedResolve.ts @@ -0,0 +1,7 @@ +export default async function (promise: Promise, minDelayMs: number): Promise { + const [result] = await Promise.all([ + promise, + new Promise(resolve => setTimeout(resolve, minDelayMs)) + ]); + return result; +} diff --git a/src/utils/priorityFiltering.ts b/src/utils/priorityFiltering.ts index 7638f34..e409790 100644 --- a/src/utils/priorityFiltering.ts +++ b/src/utils/priorityFiltering.ts @@ -4,7 +4,7 @@ export type PriorityFilterPredicate = { } /** - * Applies a list of priority predicates to an element. For all predicates that don't return null, if the ones with the highest level return true, then this function returns true. + * Applies a list of priority predicates to an element. For all predicates that don't return null, if the ones with the highest level return true, then this function returns true. Or conversely, if the one with the highest level returns false, then this function returns false. * @param element The element to apply the predicates to. * @param predicates The list of predicates to apply. */ diff --git a/test/components/Logging/Logging.test.tsx b/test/components/Logging/Logging.test.tsx index a3b6d09..36692a9 100644 --- a/test/components/Logging/Logging.test.tsx +++ b/test/components/Logging/Logging.test.tsx @@ -11,8 +11,6 @@ const loggingStoreRef: { current: null | { setState: (state: Partial void; - scrollToBottom: boolean; - setScrollToBottom: (scroll: boolean) => void; }; jest.mock("zustand", () => { @@ -59,8 +57,8 @@ type LoggingComponent = typeof import("../../../src/components/Logging/Logging.t let Logging: LoggingComponent; beforeAll(async () => { - if (!Element.prototype.scrollIntoView) { - Object.defineProperty(Element.prototype, "scrollIntoView", { + if (!Element.prototype.scrollTo) { + Object.defineProperty(Element.prototype, "scrollTo", { configurable: true, writable: true, value: function () {}, @@ -84,7 +82,6 @@ afterEach(() => { function resetLoggingStore() { loggingStoreRef.current?.setState({ showRelativeTime: false, - scrollToBottom: true, }); } @@ -151,7 +148,7 @@ describe("Logging component", () => { ]; mockUseLogs.mockReturnValue({filteredLogs: logs, distinctNames: new Set()}); - const scrollSpy = jest.spyOn(Element.prototype, "scrollIntoView").mockImplementation(() => {}); + const scrollSpy = jest.spyOn(Element.prototype, "scrollTo").mockImplementation(() => {}); const user = userEvent.setup(); const view = render(); @@ -175,7 +172,7 @@ describe("Logging component", () => { const logCell = makeCell({message: "Initial", firstRelativeCreated: 42}); mockUseLogs.mockReturnValue({filteredLogs: [logCell], distinctNames: new Set()}); - const scrollSpy = jest.spyOn(Element.prototype, "scrollIntoView").mockImplementation(() => {}); + const scrollSpy = jest.spyOn(Element.prototype, "scrollTo").mockImplementation(() => {}); render(); await waitFor(() => { @@ -209,7 +206,7 @@ describe("Logging component", () => { const initialMap = firstProps.filterPredicates; expect(initialMap).toBeInstanceOf(Map); - expect(initialMap.size).toBe(0); + expect(initialMap.size).toBe(1); // Initially, only filter out experiment logs expect(mockUseLogs).toHaveBeenCalledWith(initialMap); const updatedPredicate: LogFilterPredicate = { diff --git a/test/utils/capitalize.test.ts b/test/utils/capitalize.test.ts new file mode 100644 index 0000000..e6b7cf2 --- /dev/null +++ b/test/utils/capitalize.test.ts @@ -0,0 +1,34 @@ +import capitalize from "../../src/utils/capitalize.ts"; + +describe('capitalize', () => { + it('capitalizes the first letter of a lowercase word', () => { + expect(capitalize('hello')).toBe('Hello'); + }); + + it('keeps the first letter capitalized if already uppercase', () => { + expect(capitalize('Hello')).toBe('Hello'); + }); + + it('handles single character strings', () => { + expect(capitalize('a')).toBe('A'); + expect(capitalize('A')).toBe('A'); + }); + + it('returns empty string for empty input', () => { + expect(capitalize('')).toBe(''); + }); + + it('only capitalizes the first letter, leaving the rest unchanged', () => { + expect(capitalize('hELLO')).toBe('HELLO'); + expect(capitalize('hello world')).toBe('Hello world'); + }); + + it('handles strings starting with numbers', () => { + expect(capitalize('123abc')).toBe('123abc'); + }); + + it('handles strings starting with special characters', () => { + expect(capitalize('!hello')).toBe('!hello'); + expect(capitalize(' hello')).toBe(' hello'); + }); +}); diff --git a/test/utils/delayedResolve.test.ts b/test/utils/delayedResolve.test.ts new file mode 100644 index 0000000..2c111c6 --- /dev/null +++ b/test/utils/delayedResolve.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; +import delayedResolve from "../../src/utils/delayedResolve.ts"; + +describe('delayedResolve', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('returns the resolved value of the promise', async () => { + const resultPromise = delayedResolve(Promise.resolve('hello'), 100); + await jest.advanceTimersByTimeAsync(100); + expect(await resultPromise).toBe('hello'); + }); + + it('waits at least minDelayMs before resolving', async () => { + let resolved = false; + const resultPromise = delayedResolve(Promise.resolve('fast'), 100); + resultPromise.then(() => { resolved = true; }); + + await jest.advanceTimersByTimeAsync(50); + expect(resolved).toBe(false); + + await jest.advanceTimersByTimeAsync(50); + expect(resolved).toBe(true); + }); + + it('resolves immediately after slow promise if it exceeds minDelayMs', async () => { + let resolved = false; + const slowPromise = new Promise(resolve => + setTimeout(() => resolve('slow'), 150) + ); + const resultPromise = delayedResolve(slowPromise, 50); + resultPromise.then(() => { resolved = true; }); + + await jest.advanceTimersByTimeAsync(50); + expect(resolved).toBe(false); + + await jest.advanceTimersByTimeAsync(100); + expect(resolved).toBe(true); + expect(await resultPromise).toBe('slow'); + }); + + it('propagates rejections from the promise', async () => { + const error = new Error('test error'); + const rejectedPromise = Promise.reject(error); + + const resultPromise = delayedResolve(rejectedPromise, 100); + const assertion = expect(resultPromise).rejects.toThrow('test error'); + + await jest.advanceTimersByTimeAsync(100); + + await assertion; + }); + + it('works with different value types', async () => { + const test = async (value: T) => { + const resultPromise = delayedResolve(Promise.resolve(value), 10); + await jest.advanceTimersByTimeAsync(10); + return resultPromise; + }; + + expect(await test(42)).toBe(42); + expect(await test({ foo: 'bar' })).toEqual({ foo: 'bar' }); + expect(await test([1, 2, 3])).toEqual([1, 2, 3]); + expect(await test(null)).toBeNull(); + }); + + it('handles zero delay', async () => { + const resultPromise = delayedResolve(Promise.resolve('instant'), 0); + await jest.advanceTimersByTimeAsync(0); + expect(await resultPromise).toBe('instant'); + }); +}); -- 2.49.1 From 2ca0c9c4c0a3c55884ce45072222c063c9195491 Mon Sep 17 00:00:00 2001 From: Pim Hutting Date: Mon, 19 Jan 2026 18:17:47 +0100 Subject: [PATCH 063/101] chore: added Storms change --- .../VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx index 467187d..c0c1bf6 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx @@ -112,8 +112,8 @@ export default function BasicBeliefNode(props: NodeProps) { updateNodeData(props.id, {...data, belief: {...data.belief, description: value}}); } - // Use this - const emotionOptions = ["Happy", "Angry", "Sad", "Cheerful"] + // These are the labels outputted by our emotion detection model + const emotionOptions = ["sad", "angry", "surprise", "fear", "happy", "disgust", "neutral"]; let placeholder = "" -- 2.49.1 From 487ee30923df76e2befd653f66b008e840d292ad Mon Sep 17 00:00:00 2001 From: JGerla Date: Tue, 20 Jan 2026 10:22:08 +0100 Subject: [PATCH 064/101] feat: made jumpToNode select the node after focussing the editor ref: N25B-450 --- .../components/WarningSidebar.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx index fb740f6..fc3b347 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx @@ -1,10 +1,11 @@ -import {useReactFlow} from "@xyflow/react"; +import {useReactFlow, useStoreApi} from "@xyflow/react"; import clsx from "clsx"; import {useEffect, useState} from "react"; import useFlowStore from "../VisProgStores.tsx"; import { warningSummary, - type WarningSeverity, type EditorWarning + type WarningSeverity, + type EditorWarning, globalWarning } from "./EditorWarnings.tsx"; import styles from "./WarningSidebar.module.css"; @@ -106,8 +107,11 @@ function WarningListItem(props: { warning: EditorWarning }) { function useJumpToNode() { const { getNode, setCenter } = useReactFlow(); + const { addSelectedNodes } = useStoreApi().getState(); return (nodeId: string) => { + // user can't jump to global warning, so prevent further logic from running + if (nodeId === globalWarning) return; const node = getNode(nodeId); if (!node) return; @@ -118,7 +122,10 @@ function useJumpToNode() { position!.x + width / 2, position!.y + height / 2, { zoom: 2, duration: 300 } - ); + ).then(() => { + // select the node + addSelectedNodes([nodeId]); + }); }; } \ No newline at end of file -- 2.49.1 From 5d55ebaaa2b9e629ecd3a722f4cbc8df56a0b29a Mon Sep 17 00:00:00 2001 From: JGerla Date: Tue, 20 Jan 2026 11:53:51 +0100 Subject: [PATCH 065/101] feat: Added global warning for incomplete program chain ref: N25B-450 --- src/pages/VisProgPage/VisProg.tsx | 41 ++++++++++++++++++- .../visualProgrammingUI/VisProgStores.tsx | 7 +++- .../components/EditorWarnings.tsx | 1 + .../visualProgrammingUI/nodes/StartNode.tsx | 4 +- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index 032dfb3..a5f7887 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -4,7 +4,7 @@ import { Panel, ReactFlow, ReactFlowProvider, - MarkerType, + MarkerType, getOutgoers } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import {type CSSProperties, useEffect, useState} from "react"; @@ -12,6 +12,7 @@ import {useShallow} from 'zustand/react/shallow'; import orderPhaseNodeArray from "../../utils/orderPhaseNodes.ts"; import useProgramStore from "../../utils/programStore.ts"; import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx'; +import {type EditorWarning, globalWarning} from "./visualProgrammingUI/components/EditorWarnings.tsx"; import {WarningsSidebar} from "./visualProgrammingUI/components/WarningSidebar.tsx"; import type {PhaseNode} from "./visualProgrammingUI/nodes/PhaseNode.tsx"; import useFlowStore from './visualProgrammingUI/VisProgStores.tsx'; @@ -90,6 +91,26 @@ const VisProgUI = () => { window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }); + const {unregisterWarning, registerWarning} = useFlowStore(); + useEffect(() => { + + if (checkPhaseChain()) { + unregisterWarning(globalWarning,'INCOMPLETE_PROGRAM'); + } else { + // create global warning for incomplete program chain + const incompleteProgramWarning : EditorWarning = { + scope: { + id: globalWarning, + handleId: undefined + }, + type: 'INCOMPLETE_PROGRAM', + severity: "ERROR", + description: "there is no complete phase chain from the startNode to the EndNode" + } + + registerWarning(incompleteProgramWarning); + } + },[edges, registerWarning, unregisterWarning]) @@ -184,6 +205,24 @@ function graphReducer() { }); } +const checkPhaseChain = (): boolean => { + const {nodes, edges} = useFlowStore.getState(); + + function checkForCompleteChain(currentNodeId: string): boolean { + const outgoingPhases = getOutgoers({id: currentNodeId}, nodes, edges) + .filter(node => ["end", "phase"].includes(node.type!)); + + if (outgoingPhases.length === 0) return false; + if (outgoingPhases.some(node => node.type === "end" )) return true; + + const next = outgoingPhases.map(node => checkForCompleteChain(node.id)) + .find(result => result); + console.log(next); + return !!next; + } + + return checkForCompleteChain('start'); +}; /** diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx index 8305bb2..dd754bf 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx @@ -9,7 +9,7 @@ import { type XYPosition, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { editorWarningRegistry } from "./components/EditorWarnings.tsx"; +import {editorWarningRegistry} from "./components/EditorWarnings.tsx"; import type { FlowState } from './VisProgTypes'; import { NodeDefaults, @@ -50,7 +50,7 @@ function createNode(id: string, type: string, position: XYPosition, data: Record const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false) const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null}) - const initialNodes : Node[] = [startNode, endNode, initialPhaseNode,]; + const initialNodes : Node[] = [startNode, endNode, initialPhaseNode]; // Initial edges, leave empty as setting initial edges... // ...breaks logic that is dependent on connection events @@ -312,4 +312,7 @@ const useFlowStore = create(UndoRedo((set, get) => ({ })) ); + + export default useFlowStore; + diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx index a0c90d6..eb5a0f6 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx @@ -18,6 +18,7 @@ export type WarningType = | 'MISSING_INPUT' | 'MISSING_OUTPUT' | 'PLAN_IS_UNDEFINED' + | 'INCOMPLETE_PROGRAM' | string export type WarningSeverity = diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx index 3d6c2b2..fab9b93 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx @@ -7,7 +7,7 @@ import {useEffect} from "react"; import { Toolbar } from '../components/NodeComponents'; import styles from '../../VisProg.module.css'; import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx"; -import type {EditorWarning} from "../components/EditorWarnings.tsx"; +import {type EditorWarning} from "../components/EditorWarnings.tsx"; import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts"; import useFlowStore from "../VisProgStores.tsx"; @@ -48,8 +48,6 @@ export default function StartNode(props: NodeProps) { if (connections.length === 0) { registerWarning(noConnectionWarning); } else { unregisterWarning(props.id, `${noConnectionWarning.type}:source`); } }, [connections.length, props.id, registerWarning, unregisterWarning]); - - return ( <> -- 2.49.1 From a8f99653914db05d5ed5d408db8a9697d8457f30 Mon Sep 17 00:00:00 2001 From: Pim Hutting Date: Tue, 20 Jan 2026 12:31:34 +0100 Subject: [PATCH 066/101] chore: added recursive goals to monitor page --- src/pages/MonitoringPage/MonitoringPage.tsx | 9 ++-- .../MonitoringPageComponents.tsx | 10 ++-- src/utils/programStore.ts | 48 +++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx index 9252448..cd7f95e 100644 --- a/src/pages/MonitoringPage/MonitoringPage.tsx +++ b/src/pages/MonitoringPage/MonitoringPage.tsx @@ -306,14 +306,17 @@ function PhaseDashboard({ setActiveIds: React.Dispatch>>, goalIndex: number }) { - const getGoals = useProgramStore((s) => s.getGoalsInPhase); + const getGoalsWithDepth = useProgramStore((s) => s.getGoalsWithDepth); const getTriggers = useProgramStore((s) => s.getTriggersInPhase); const getNorms = useProgramStore((s) => s.getNormsInPhase); // Prepare data view models - const goals = (getGoals(phaseId) as GoalNode[]).map(g => ({ + const goals = getGoalsWithDepth(phaseId).map((g) => ({ ...g, - achieved: activeIds[g.id] ?? false, + id: g.id as string, + name: g.name as string, + achieved: activeIds[g.id as string] ?? false, + level: g.level, // Pass this new property to the UI })); const triggers = (getTriggers(phaseId) as TriggerNode[]).map(t => ({ diff --git a/src/pages/MonitoringPage/MonitoringPageComponents.tsx b/src/pages/MonitoringPage/MonitoringPageComponents.tsx index 6efeab5..d1d2854 100644 --- a/src/pages/MonitoringPage/MonitoringPageComponents.tsx +++ b/src/pages/MonitoringPage/MonitoringPageComponents.tsx @@ -91,13 +91,14 @@ export const DirectSpeechInput: React.FC = () => { }; // --- interface for goals/triggers/norms/conditional norms --- -type StatusItem = { +export type StatusItem = { id?: string | number; achieved?: boolean; description?: string; label?: string; norm?: string; name?: string; + level?: number; }; interface StatusListProps { @@ -129,7 +130,7 @@ export const StatusList: React.FC = ({ const isCurrentGoal = type === 'goal' && idx === currentGoalIndex; const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive); - + const indentation = (item.level || 0) * 20; const handleOverrideClick = () => { if (!canOverride) return; @@ -147,7 +148,10 @@ export const StatusList: React.FC = ({ }; return ( -
  • +
  • {showIndicator && ( [] }; +export type GoalWithDepth = Record & { level: number }; + /** * the type definition of the programStore */ @@ -18,6 +20,7 @@ export type ProgramState = { getPhaseNames: () => string[]; getNormsInPhase: (currentPhaseId: string) => Record[]; getGoalsInPhase: (currentPhaseId: string) => Record[]; + getGoalsWithDepth: (currentPhaseId: string) => GoalWithDepth[]; getTriggersInPhase: (currentPhaseId: string) => Record[]; // if more specific utility functions are needed they can be added here: } @@ -70,6 +73,51 @@ const useProgramStore = create((set, get) => ({ } throw new Error(`phase with id:"${currentPhaseId}" not found`) }, + + getGoalsWithDepth: (currentPhaseId: string) => { + const program = get().currentProgram; + const phase = program.phases.find(val => val["id"] === currentPhaseId); + + if (!phase) { + throw new Error(`phase with id:"${currentPhaseId}" not found`); + } + + const rootGoals = phase["goals"] as Record[]; + const flatList: GoalWithDepth[] = []; + + // Helper: Define this ONCE, outside the loop + const isGoal = (item: Record) => { + return item["plan"] !== undefined && item["plan"] !== null; + }; + + // Recursive helper function + const traverse = (goals: Record[], depth: number) => { + goals.forEach((goal) => { + // 1. Add the current goal to the list + flatList.push({ ...goal, level: depth }); + + // 2. Check for children + const plan = goal["plan"] as Record | undefined; + + if (plan && Array.isArray(plan["steps"])) { + const steps = plan["steps"] as Record[]; + + // 3. FILTER: Only recurse on steps that are actually goals + // If we just passed 'steps', we might accidentally add Actions/Speeches to the goal list + const childGoals = steps.filter(isGoal); + + if (childGoals.length > 0) { + traverse(childGoals, depth + 1); + } + } + }); + }; + + // Start traversal + traverse(rootGoals, 0); + + return flatList; + }, /** * gets the triggers for the provided phase */ -- 2.49.1 From 3f6d95683dbf620e391b75a9d1dfa7ad5a14b5af Mon Sep 17 00:00:00 2001 From: JGerla Date: Tue, 20 Jan 2026 12:50:29 +0100 Subject: [PATCH 067/101] feat: Added visual separation between global and node or handle specific warnings ref: N25B-450 --- .../components/WarningSidebar.module.css | 4 ++ .../components/WarningSidebar.tsx | 49 +++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css index 2a8241e..a3a60de 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css @@ -38,6 +38,10 @@ border: 2px solid currentColor; } +.warning-group-header { + background: ButtonFace; +} + .warnings-list { flex: 1; overflow-y: auto; diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx index fc3b347..da64e89 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx @@ -19,6 +19,9 @@ export function WarningsSidebar() { ? warnings : warnings.filter(w => w.severity === severityFilter); + + + return (
  • -- 2.49.1 From 883f0a95a6aa1fee1c2dee238ea53b77968d6e40 Mon Sep 17 00:00:00 2001 From: Pim Hutting Date: Tue, 20 Jan 2026 13:55:34 +0100 Subject: [PATCH 069/101] chore: only check if play is undefined --- src/utils/programStore.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/programStore.ts b/src/utils/programStore.ts index ac34ef4..4e12bb3 100644 --- a/src/utils/programStore.ts +++ b/src/utils/programStore.ts @@ -85,9 +85,8 @@ const useProgramStore = create((set, get) => ({ const rootGoals = phase["goals"] as Record[]; const flatList: GoalWithDepth[] = []; - // Helper: Define this ONCE, outside the loop const isGoal = (item: Record) => { - return item["plan"] !== undefined && item["plan"] !== null; + return item["plan"] !== undefined; }; // Recursive helper function -- 2.49.1 From 363054afda829f16083c0520429d947ee70bb5e1 Mon Sep 17 00:00:00 2001 From: JGerla Date: Tue, 20 Jan 2026 14:00:01 +0100 Subject: [PATCH 070/101] feat: updated visuals ref: N25B-450 --- .../visualProgrammingUI/components/WarningSidebar.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx index 113467d..eb2efee 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx @@ -119,10 +119,11 @@ function WarningListItem(props: { warning: EditorWarning }) {
    - {props.warning.scope.id} - {props.warning.scope.handleId && ( - @{props.warning.scope.handleId} - )} + {props.warning.type} + {/*{props.warning.scope.id}*/} + {/*{props.warning.scope.handleId && (*/} + {/* @{props.warning.scope.handleId}*/} + {/*)}*/}
    ); -- 2.49.1 From f73bbb9d02bfcc41b1ffa1fc658fee83ee20c68e Mon Sep 17 00:00:00 2001 From: Pim Hutting Date: Thu, 22 Jan 2026 10:15:20 +0100 Subject: [PATCH 071/101] chore: added tests and removed restart phase this version also has recursive goals functional --- .../MonitoringPage/MonitoringPage.module.css | 5 -- src/pages/MonitoringPage/MonitoringPage.tsx | 41 +-------- src/pages/MonitoringPage/MonitoringPageAPI.ts | 10 --- .../nodes/BasicBeliefNode.tsx | 4 +- .../monitoringPage/MonitoringPage.test.tsx | 8 +- .../monitoringPage/MonitoringPageAPI.test.ts | 11 +-- test/utils/programStore.test.ts | 83 +++++++++++++++++++ 7 files changed, 96 insertions(+), 66 deletions(-) diff --git a/src/pages/MonitoringPage/MonitoringPage.module.css b/src/pages/MonitoringPage/MonitoringPage.module.css index 5f23eea..183fe4b 100644 --- a/src/pages/MonitoringPage/MonitoringPage.module.css +++ b/src/pages/MonitoringPage/MonitoringPage.module.css @@ -77,11 +77,6 @@ color: white; } -.restartPhase{ - background-color: rgb(255, 123, 0); - color: white; -} - .restartExperiment{ background-color: red; color: white; diff --git a/src/pages/MonitoringPage/MonitoringPage.tsx b/src/pages/MonitoringPage/MonitoringPage.tsx index cd7f95e..3b79df9 100644 --- a/src/pages/MonitoringPage/MonitoringPage.tsx +++ b/src/pages/MonitoringPage/MonitoringPage.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import styles from './MonitoringPage.module.css'; // Store & API @@ -52,16 +52,12 @@ function useExperimentLogic() { const [phaseIndex, setPhaseIndex] = useState(0); 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 phaseNames = getPhaseNames(); // --- Stream Handlers --- const handleStreamUpdate = useCallback((data: ExperimentStreamData) => { - if (suppressUpdates.current) return; if (data.type === 'phase_update' && data.id) { const payload = data as PhaseUpdate; console.log(`${data.type} received, id : ${data.id}`); @@ -105,7 +101,6 @@ function useExperimentLogic() { }, [getPhaseIds, getGoalsInPhase, phaseIds, phaseIndex, phaseNames]); const handleStatusUpdate = useCallback((data: unknown) => { - if (suppressUpdates.current) return; const payload = data as CondNormsStateUpdate; if (payload.type !== 'cond_norms_state_update') return; @@ -145,7 +140,7 @@ function useExperimentLogic() { } }, [setProgramState]); - const handleControlAction = async (action: "pause" | "play" | "nextPhase" | "resetPhase") => { + const handleControlAction = async (action: "pause" | "play" | "nextPhase") => { try { setLoading(true); switch (action) { @@ -160,30 +155,6 @@ function useExperimentLogic() { case "nextPhase": await nextPhase(); 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) { console.error(err); @@ -251,7 +222,7 @@ function ControlPanel({ }: { loading: boolean, isPlaying: boolean, - onAction: (a: "pause" | "play" | "nextPhase" | "resetPhase") => void, + onAction: (a: "pause" | "play" | "nextPhase") => void, onReset: () => void }) { return ( @@ -276,12 +247,6 @@ function ControlPanel({ disabled={loading} >⏭ - - + + + - + ); }; @@ -221,7 +223,6 @@ const checkPhaseChain = (): boolean => { const next = outgoingPhases.map(node => checkForCompleteChain(node.id)) .find(result => result); - console.log(next); return !!next; } @@ -246,7 +247,6 @@ function VisProgPage() { // however this would cause unneeded updates // eslint-disable-next-line react-hooks/exhaustive-deps }, [severityIndex]); - console.log(severityIndex); return ( <> diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css index 461ff09..82168dc 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css @@ -1,12 +1,51 @@ .warnings-sidebar { - min-width: 320px; - max-width: 320px; - width: 320px; + min-width: auto; + max-width: 340px; + margin-right: 0; height: 100%; background: canvas; - border-left: 2px solid CanvasText; display: flex; + flex-direction: row; +} + +.warnings-toggle-bar { + background-color: ButtonFace; + justify-items: center; + align-content: center; + width: 1rem; + cursor: pointer; +} + +.warnings-toggle-bar.error:first-child:has(.arrow-right){ + background-color: hsl(from red h s 75%); +} +.warnings-toggle-bar.warning:first-child:has(.arrow-right) { + background-color: hsl(from orange h s 75%); +} +.warnings-toggle-bar.info:first-child:has(.arrow-right) { + background-color: hsl(from steelblue h s 75%); +} + +.warnings-toggle-bar:hover { + background-color: GrayText !important ; + .arrow-left { + border-right-color: ButtonFace; + transition: transform 0.15s ease-in-out; + transform: rotateY(180deg); + } + .arrow-right { + border-left-color: ButtonFace; + transition: transform 0.15s ease-in-out; + transform: rotateY(180deg); + } +} + + +.warnings-content { + width: 320px; + flex: 1; flex-direction: column; + border-left: 2px solid CanvasText; } .warnings-header { @@ -45,12 +84,14 @@ .warning-group-header { background: ButtonFace; - padding: 4px; + padding: 6px; + font-weight: bold; } .warnings-list { flex: 1; - overflow-y: auto; + min-height: 0; + overflow-y: scroll; } .warnings-empty { @@ -131,4 +172,32 @@ .warning-item .description { padding: 5px 10px; font-size: 0.8rem; -} \ No newline at end of file +} + +.auto-hide { + background-color: Canvas; + border-top: 2px solid CanvasText; + margin-top: auto; + width: 100%; + height: 2.5rem; + display: flex; + align-items: center; + padding: 0 12px; +} + +/* arrows for toggleBar */ +.arrow-right { + width: 0; + height: 0; + border-top: 0.5rem solid transparent; + border-bottom: 0.5rem solid transparent; + border-left: 0.6rem solid GrayText; +} + +.arrow-left { + width: 0; + height: 0; + border-top: 0.5rem solid transparent; + border-bottom: 0.5rem solid transparent; + border-right: 0.6rem solid GrayText; +} diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx index 49a22a7..9e030e1 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx @@ -11,23 +11,64 @@ import styles from "./WarningSidebar.module.css"; export function WarningsSidebar() { const warnings = useFlowStore.getState().getWarnings(); - + const [hide, setHide] = useState(false); const [severityFilter, setSeverityFilter] = useState('ALL'); + const [autoHide, setAutoHide] = useState(false); + + // let autohide change hide status only when autohide is toggled + // and allow for user to change the hide state even if autohide is enabled + const hasWarnings = warnings.length > 0; + useEffect(() => { + if (autoHide) { + setHide(!hasWarnings); + } + }, [autoHide, hasWarnings]); - useEffect(() => {}, [warnings]); const filtered = severityFilter === 'ALL' ? warnings : warnings.filter(w => w.severity === severityFilter); - return ( -