diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index c54666a..8ccbc7d 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'; @@ -155,6 +156,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/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx index b8e4f53..795fbd7 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx @@ -29,10 +29,10 @@ export type BasicBeliefNodeData = { }; // These are all the types a basic belief could be. -type BasicBeliefType = Keyword | Semantic | Object | Emotion +type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion type Keyword = { type: "keyword", id: string, value: string, label: "Keyword said:"}; type Semantic = { type: "semantic", id: string, value: string, label: "Detected with LLM:"}; -type Object = { type: "object", id: string, value: string, label: "Object found:"}; +type DetectedObject = { type: "object", id: string, value: string, label: "Object found:"}; type Emotion = { type: "emotion", id: string, value: string, label: "Emotion recognised:"}; export type BasicBeliefNode = Node @@ -93,6 +93,10 @@ export default function BasicBeliefNode(props: NodeProps) { belief: { ...data.belief, type: newType, + value: + newType === "emotion" + ? emotionOptions[0] + : data.belief.value, }, }); } @@ -202,13 +206,4 @@ export function BasicBeliefReduce(node: Node, _nodes: Node[]) { break; } return result -} - -/** - * This function is called whenever a connection is made with this node type (BasicBelief) - * @param _thisNode the node of this node type which function is called - * @param _otherNode the other node which was part of the connection - * @param _isThisSource whether this instance of the node was the source in the connection, true = yes. - */ -export function BasicBeliefConnects(_thisNode: Node, _otherNode: Node, _isThisSource: boolean) { } \ No newline at end of file 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/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx b/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx index 07a69ef..98eaccc 100644 --- a/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx +++ b/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx @@ -467,7 +467,7 @@ describe('BasicBeliefNode', () => { }); }); - it('should preserve value when switching from text type to emotion type', async () => { + it('should automatically choose the first option when switching to emotion type, and carry on to the text values', async () => { const mockNode: Node = { id: 'belief-1', type: 'basic_belief', @@ -512,7 +512,7 @@ describe('BasicBeliefNode', () => { expect(updatedNode?.data.belief.type).toBe('emotion'); // The component doesn't reset the value when changing types // So it keeps the old value even though it doesn't make sense for emotion type - expect(updatedNode?.data.belief.value).toBe('some text'); + expect(updatedNode?.data.belief.value).toBe('Happy'); }); }); }); diff --git a/test/setupFlowTests.ts b/test/setupFlowTests.ts index 35d6486..aaffff0 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..ba78b88 --- /dev/null +++ b/test/utils/programStore.test.ts @@ -0,0 +1,116 @@ +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'); + }); + + it('should clone program state when setting it (no shared references should exist)', () => { + const changeableMockProgram: 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' }], + }, + ], + }; + + useProgramStore.getState().setProgramState(changeableMockProgram); + + const storedProgram = useProgramStore.getState().getProgramState(); + + // mutate original + (changeableMockProgram.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