Refactor of visual programming page to fully match the CB's program schema. Includes overhaul of UI elements for plan creation. #38
@@ -9,6 +9,7 @@ import {
|
|||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
import {useShallow} from 'zustand/react/shallow';
|
import {useShallow} from 'zustand/react/shallow';
|
||||||
|
import useProgramStore from "../../utils/programStore.ts";
|
||||||
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
||||||
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
||||||
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
||||||
@@ -155,6 +156,10 @@ function runProgram() {
|
|||||||
).then((res) => {
|
).then((res) => {
|
||||||
if (!res.ok) throw new Error("Failed communicating with the backend.")
|
if (!res.ok) throw new Error("Failed communicating with the backend.")
|
||||||
console.log("Successfully sent the program to 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."));
|
}).catch(() => console.log("Failed to send program to the backend."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ export type BasicBeliefNodeData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// These are all the types a basic belief could be.
|
// 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 Keyword = { type: "keyword", id: string, value: string, label: "Keyword said:"};
|
||||||
type Semantic = { type: "semantic", id: string, value: string, label: "Detected with LLM:"};
|
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:"};
|
type Emotion = { type: "emotion", id: string, value: string, label: "Emotion recognised:"};
|
||||||
|
|
||||||
export type BasicBeliefNode = Node<BasicBeliefNodeData>
|
export type BasicBeliefNode = Node<BasicBeliefNodeData>
|
||||||
@@ -93,6 +93,10 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
|||||||
belief: {
|
belief: {
|
||||||
...data.belief,
|
...data.belief,
|
||||||
type: newType,
|
type: newType,
|
||||||
|
value:
|
||||||
|
newType === "emotion"
|
||||||
|
? emotionOptions[0]
|
||||||
|
: data.belief.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -203,12 +207,3 @@ export function BasicBeliefReduce(node: Node, _nodes: Node[]) {
|
|||||||
}
|
}
|
||||||
return result
|
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) {
|
|
||||||
}
|
|
||||||
81
src/utils/programStore.ts
Normal file
81
src/utils/programStore.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import {create} from "zustand";
|
||||||
|
|
||||||
|
// the type of a reduced program
|
||||||
|
export type ReducedProgram = { phases: Record<string, unknown>[] };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<string, unknown>[];
|
||||||
|
getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||||
|
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||||
|
// 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<ProgramState>((set, get) => ({
|
||||||
|
currentProgram: { phases: [] as Record<string, unknown>[]},
|
||||||
|
/**
|
||||||
|
* 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<string, unknown>[];
|
||||||
|
}
|
||||||
|
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<string, unknown>[];
|
||||||
|
}
|
||||||
|
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<string, unknown>[];
|
||||||
|
}
|
||||||
|
throw new Error(`phase with id:"${currentPhaseId}" not found`)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useProgramStore;
|
||||||
@@ -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<BasicBeliefNodeData> = {
|
const mockNode: Node<BasicBeliefNodeData> = {
|
||||||
id: 'belief-1',
|
id: 'belief-1',
|
||||||
type: 'basic_belief',
|
type: 'basic_belief',
|
||||||
@@ -512,7 +512,7 @@ describe('BasicBeliefNode', () => {
|
|||||||
expect(updatedNode?.data.belief.type).toBe('emotion');
|
expect(updatedNode?.data.belief.type).toBe('emotion');
|
||||||
// The component doesn't reset the value when changing types
|
// 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
|
// 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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import '@testing-library/jest-dom';
|
|||||||
import { cleanup } from '@testing-library/react';
|
import { cleanup } from '@testing-library/react';
|
||||||
import useFlowStore from '../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
|
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
|
// To make sure that the tests are working, it's important that you are using
|
||||||
// this implementation of ResizeObserver and DOMMatrixReadOnly
|
// this implementation of ResizeObserver and DOMMatrixReadOnly
|
||||||
|
|||||||
116
test/utils/programStore.test.ts
Normal file
116
test/utils/programStore.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user