From 10d5a15c886b09e849f4b9c85289beb76cbb0fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Thu, 11 Dec 2025 14:12:26 +0100 Subject: [PATCH 01/10] feat: basic belief node with the basic belief types defined in KB. ref: N25B-408 --- src/App.css | 8 + src/pages/VisProgPage/VisProg.module.css | 13 ++ .../visualProgrammingUI/NodeRegistry.ts | 9 +- .../nodes/BasicBeliefNode.default.ts | 12 ++ .../nodes/BasicBeliefNode.tsx | 174 ++++++++++++++++++ 5 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts create mode 100644 src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx diff --git a/src/App.css b/src/App.css index a241d03..8e078f6 100644 --- a/src/App.css +++ b/src/App.css @@ -248,3 +248,11 @@ button.no-button { text-decoration: underline; } } + +.flex-center-x { + display: flex; + justify-content: center; /* horizontal centering */ + text-align: center; /* center multi-line text */ + width: 100%; /* allow it to stretch */ + flex-wrap: wrap; /* optional: let text wrap naturally */ +} \ No newline at end of file diff --git a/src/pages/VisProgPage/VisProg.module.css b/src/pages/VisProgPage/VisProg.module.css index 5f2aa78..14619c5 100644 --- a/src/pages/VisProgPage/VisProg.module.css +++ b/src/pages/VisProgPage/VisProg.module.css @@ -71,6 +71,11 @@ filter: drop-shadow(0 0 0.25rem red); } +.node-basic_belief { + outline: plum solid 2pt; + filter: drop-shadow(0 0 0.25rem plum); +} + .draggable-node { padding: 3px 10px; background-color: canvas; @@ -126,3 +131,11 @@ outline: red solid 2pt; filter: drop-shadow(0 0 0.25rem red); } + +.draggable-node-basic_belief { + padding: 3px 10px; + background-color: canvas; + border-radius: 5pt; + outline: plum solid 2pt; + filter: drop-shadow(0 0 0.25rem plum); +} \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts index 8812434..04dabf1 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts +++ b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts @@ -10,6 +10,8 @@ import GoalNode, { GoalConnects, GoalReduce } from "./nodes/GoalNode"; import { GoalNodeDefaults } from "./nodes/GoalNode.default"; import TriggerNode, { TriggerConnects, TriggerReduce } from "./nodes/TriggerNode"; import { TriggerNodeDefaults } from "./nodes/TriggerNode.default"; +import BasicBeliefNode, { BasicBeliefConnects, BasicBeliefReduce } from "./nodes/BasicBeliefNode"; +import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default"; /** * Registered node types in the visual programming system. @@ -24,6 +26,7 @@ export const NodeTypes = { norm: NormNode, goal: GoalNode, trigger: TriggerNode, + basic_belief: BasicBeliefNode, }; /** @@ -38,6 +41,7 @@ export const NodeDefaults = { norm: NormNodeDefaults, goal: GoalNodeDefaults, trigger: TriggerNodeDefaults, + basic_belief: BasicBeliefNodeDefaults, }; @@ -54,6 +58,7 @@ export const NodeReduces = { norm: NormReduce, goal: GoalReduce, trigger: TriggerReduce, + basic_belief: BasicBeliefReduce, } @@ -69,6 +74,7 @@ export const NodeConnects = { norm: NormConnects, goal: GoalConnects, trigger: TriggerConnects, + basic_belief: BasicBeliefConnects, } /** @@ -79,7 +85,6 @@ export const NodeConnects = { export const NodeDeletes = { start: () => false, end: () => false, - test: () => false, // Used for coverage of universal/ undefined nodes } /** @@ -92,5 +97,5 @@ export const NodesInPhase = { start: () => false, end: () => false, phase: () => false, - test: () => false, // Used for coverage of universal/ undefined nodes + // basic_belief: () => false, } \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts new file mode 100644 index 0000000..72066c4 --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts @@ -0,0 +1,12 @@ +import type { BasicBeliefNodeData } from "./BasicBeliefNode"; + + +/** + * Default data for this node + */ +export const BasicBeliefNodeDefaults: BasicBeliefNodeData = { + label: "Belief", + droppable: true, + belief: {type: "keyword", id: "help", value: "help", label: "Keyword said:"}, + hasReduce: true, +}; \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx new file mode 100644 index 0000000..317a1eb --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx @@ -0,0 +1,174 @@ +import { + Handle, + type NodeProps, + Position, + type Connection, + type Edge, + type Node, +} from '@xyflow/react'; +import { Toolbar } from '../components/NodeComponents'; +import styles from '../../VisProg.module.css'; +import useFlowStore from '../VisProgStores'; +import { TextField } from '../../../../components/TextField'; + +/** + * The default data structure for a BasicBelief node + * + * Represents configuration for a node that activates when a specific condition is met, + * such as keywords being spoken or emotions detected. + * + * @property label: the display label of this BasicBelief node. + * @property droppable: Whether this node can be dropped from the toolbar (default: true). + * @property BasicBeliefType - The type of BasicBelief ("keywords" or a custom string). + * @property BasicBeliefs - The list of keyword BasicBeliefs (if applicable). + * @property hasReduce - Whether this node supports reduction logic. + */ +export type BasicBeliefNodeData = { + label: string; + droppable: boolean; + belief: BasicBeliefType; + hasReduce: boolean; +}; + +// These are all the types a basic belief could be. +type BasicBeliefType = Keyword | Semantic | Object | 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 Emotion = { type: "emotion", id: string, value: string, label: "Emotion recognised:"}; + +export type BasicBeliefNode = Node + + +/** + * Determines whether a BasicBelief node can connect to another node or edge. + * + * @param connection - The connection or edge being attempted to connect towards. + * @returns `true` if the connection is defined; otherwise, `false`. + */ +export function BasicBeliefNodeCanConnect(connection: Connection | Edge): boolean { + return (connection != undefined); +} + +/** + * Defines how a BasicBelief node should be rendered + * @param props - Node properties provided by React Flow, including `id` and `data`. + * @returns The rendered BasicBeliefNode React element (React.JSX.Element). + */ +export default function BasicBeliefNode(props: NodeProps) { + const data = props.data; + const {updateNodeData} = useFlowStore(); + const updateValue = (value: string) => updateNodeData(props.id, {...data, belief: {...data.belief, value: value}}); + const label_input_id = `basic_belief_${props.id}_label_input`; + + type BeliefString = BasicBeliefType["type"]; + + function updateBeliefType(newType: BeliefString) { + updateNodeData(props.id, { + ...data, + belief: { + ...data.belief, + type: newType, + }, + }); + } + + + // Use this + const emotionOptions = ["Happy", "Angry", "Sad", "Cheerful"] + + + let placeholder = "" + let wrapping = "" + switch (props.data.belief.type) { + case ("keyword"): + placeholder = "keyword..." + wrapping = '"' + break; + case ("semantic"): + placeholder = "word..." + wrapping = '"' + break; + case ("object"): + placeholder = "object..." + break; + case ("emotion"): + // TODO: emotion should probably be a drop-down menu rather than a string + // So this placeholder won't hold for always + placeholder = "emotion..." + break; + default: + break; + } + + return ( + <> + +
+
+ +
+
+ + {wrapping} + + {data.belief.type === "emotion" && ( + + )} + + + {data.belief.type !== "emotion" && + ()} + {wrapping} +
+ +
+ + ); +}; + +/** + * Reduces each BasicBelief, including its children down into its core data. + * @param node - The BasicBelief node to reduce. + * @param _nodes - The list of all nodes in the current flow graph. + * @returns A simplified object containing the node label and its list of BasicBeliefs. + */ +export function BasicBeliefReduce(node: BasicBeliefNode, _nodes: Node[]) { + const data = node.data; + return { + id: node.id, + type: data.belief.type, + value: data.belief.value + } +} + +/** + * 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 From 9d4f10213e1011bd4486fef5f009f40255f41ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Mon, 15 Dec 2025 11:59:12 +0100 Subject: [PATCH 02/10] fix: update the recducer in phases to account for node-specific reducing ref: N25B-408 --- src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts | 2 +- src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts index 04dabf1..97f5de6 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts +++ b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts @@ -97,5 +97,5 @@ export const NodesInPhase = { start: () => false, end: () => false, phase: () => false, - // basic_belief: () => false, + basic_belief: () => false, } \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx index c8ea2c0..112cfbf 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx @@ -96,7 +96,7 @@ export function PhaseReduce(node: Node, nodes: Node[]) { console.warn(`No reducer found for node type ${type}`); result[type + "s"] = []; } else { - result[type + "s"] = typedChildren.map((child) => reducer(child, nodes)); + result[type + "s"] = typedChildren.map((child) => reducer(child as any, nodes)); } }); From f22fe38e225234de561671a6464a895c8053637a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Mon, 15 Dec 2025 12:01:39 +0100 Subject: [PATCH 03/10] fix: revert the reduce change for eslint- might be done later in other way ref: N25B-408 --- .../VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx | 4 ++-- src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx index 317a1eb..4f73a2d 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx @@ -155,8 +155,8 @@ export default function BasicBeliefNode(props: NodeProps) { * @param _nodes - The list of all nodes in the current flow graph. * @returns A simplified object containing the node label and its list of BasicBeliefs. */ -export function BasicBeliefReduce(node: BasicBeliefNode, _nodes: Node[]) { - const data = node.data; +export function BasicBeliefReduce(node: Node, _nodes: Node[]) { + const data = node.data as BasicBeliefNodeData; return { id: node.id, type: data.belief.type, diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx index 112cfbf..c8ea2c0 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx @@ -96,7 +96,7 @@ export function PhaseReduce(node: Node, nodes: Node[]) { console.warn(`No reducer found for node type ${type}`); result[type + "s"] = []; } else { - result[type + "s"] = typedChildren.map((child) => reducer(child as any, nodes)); + result[type + "s"] = typedChildren.map((child) => reducer(child, nodes)); } }); From 757435e9f83e7511f17a4544a6e168d9a6029b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Mon, 15 Dec 2025 12:09:53 +0100 Subject: [PATCH 04/10] fix: fix the tests and creation of nodes. ref: N25B-408 --- .../visualProgrammingUI/VisProgStores.tsx | 22 ++++++++++--------- .../nodes/UniversalNodes.test.tsx | 16 ++++++++------ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx index 5154a8e..3952e72 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx @@ -23,17 +23,19 @@ import { UndoRedo } from "./EditorUndoRedo.ts"; * @param deletable - Optional flag to indicate if the node can be deleted (can be deleted by default). * @returns A fully initialized Node object ready to be added to the flow. */ -function createNode(id: string, type: string, position: XYPosition, data: Record, deletable? : boolean) { - const defaultData = NodeDefaults[type as keyof typeof NodeDefaults] - const newData = { - id: id, - type: type, - position: position, - data: data, - deletable: deletable, +function createNode(id: string, type: string, position: XYPosition, data: Record, deletable?: boolean) { + const defaultData = NodeDefaults[type as keyof typeof NodeDefaults] + return { + id, + type, + position, + deletable, + data: { + ...defaultData, + ...data, + }, + } } - return {...defaultData, ...newData} -} //* Initial nodes, created by using createNode. */ const initialNodes : Node[] = [ diff --git a/test/pages/visProgPage/visualProgrammingUI/nodes/UniversalNodes.test.tsx b/test/pages/visProgPage/visualProgrammingUI/nodes/UniversalNodes.test.tsx index 7fb0709..ba19230 100644 --- a/test/pages/visProgPage/visualProgrammingUI/nodes/UniversalNodes.test.tsx +++ b/test/pages/visProgPage/visualProgrammingUI/nodes/UniversalNodes.test.tsx @@ -15,14 +15,16 @@ describe('NormNode', () => { function createNode(id: string, type: string, position: XYPosition, data: Record, deletable?: boolean) { const defaultData = NodeDefaults[type as keyof typeof NodeDefaults] - const newData = { - id: id, - type: type, - position: position, - data: data, - deletable: deletable, + return { + id, + type, + position, + deletable, + data: { + ...defaultData, + ...data, + }, } - return {...defaultData, ...newData} } From ae8ef317a4c814187ee994ca643a790abb280fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Mon, 15 Dec 2025 13:04:53 +0100 Subject: [PATCH 05/10] test: tests for belief node ref: N25B-408 --- .../nodes/BasicBeliefNode.tsx | 3 +- .../nodes/BeliefNode.test.tsx | 743 ++++++++++++++++++ 2 files changed, 744 insertions(+), 2 deletions(-) create mode 100644 test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx index 4f73a2d..4942d48 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx @@ -46,8 +46,7 @@ export type BasicBeliefNode = Node * @param connection - The connection or edge being attempted to connect towards. * @returns `true` if the connection is defined; otherwise, `false`. */ -export function BasicBeliefNodeCanConnect(connection: Connection | Edge): boolean { - return (connection != undefined); +export function BasicBeliefNodeCanConnect(_connection: Connection | Edge) { } /** diff --git a/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx b/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx new file mode 100644 index 0000000..07a69ef --- /dev/null +++ b/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx @@ -0,0 +1,743 @@ +// BasicBeliefNode.test.tsx +import { describe, it, beforeEach } from '@jest/globals'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders } from '../.././/./../../test-utils/test-utils'; +import BasicBeliefNode, { type BasicBeliefNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode'; +import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores'; +import type { Node } from '@xyflow/react'; +import '@testing-library/jest-dom'; + +describe('BasicBeliefNode', () => { + let user: ReturnType; + + beforeEach(() => { + user = userEvent.setup(); + }); + + describe('Rendering', () => { + it('should render the basic belief node with keyword type by default', () => { + const mockNode: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'keyword', id: 'help', value: 'help', label: 'Keyword said:' }, + hasReduce: true, + }, + }; + + renderWithProviders( + + ); + + expect(screen.getByText('Belief:')).toBeInTheDocument(); + expect(screen.getByDisplayValue('Keyword said:')).toBeInTheDocument(); + expect(screen.getByDisplayValue('help')).toBeInTheDocument(); + }); + + it('should render with semantic belief type', () => { + const mockNode: Node = { + id: 'belief-2', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'semantic', id: 'test', value: 'test value', label: 'Detected with LLM:' }, + hasReduce: true, + }, + }; + + renderWithProviders( + + ); + + expect(screen.getByDisplayValue('Detected with LLM:')).toBeInTheDocument(); + expect(screen.getByDisplayValue('test value')).toBeInTheDocument(); + }); + + it('should render with object belief type', () => { + const mockNode: Node = { + id: 'belief-3', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'object', id: 'obj1', value: 'cup', label: 'Object found:' }, + hasReduce: true, + }, + }; + + renderWithProviders( + + ); + + expect(screen.getByDisplayValue('Object found:')).toBeInTheDocument(); + expect(screen.getByDisplayValue('cup')).toBeInTheDocument(); + }); + + it('should render with emotion belief type and select dropdown', () => { + const mockNode: Node = { + id: 'belief-4', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'emotion', id: 'em1', value: 'happy', label: 'Emotion recognised:' }, + hasReduce: true, + }, + }; + + renderWithProviders( + + ); + + expect(screen.getByDisplayValue('Emotion recognised:')).toBeInTheDocument(); + // For emotion type, we should check that the select has the correct value selected + const selectElement = screen.getByDisplayValue('Happy'); + expect(selectElement).toBeInTheDocument(); + expect((selectElement as HTMLSelectElement).value).toBe('happy'); + }); + + it('should render emotion dropdown with all emotion options', () => { + const mockNode: Node = { + id: 'belief-5', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'emotion', id: 'em1', value: 'happy', label: 'Emotion recognised:' }, + hasReduce: true, + }, + }; + + renderWithProviders( + + ); + + const selectElement = screen.getByDisplayValue('Happy'); + expect(selectElement).toBeInTheDocument(); + + // Check that all emotion options are present + expect(screen.getByText('Happy')).toBeInTheDocument(); + expect(screen.getByText('Angry')).toBeInTheDocument(); + expect(screen.getByText('Sad')).toBeInTheDocument(); + expect(screen.getByText('Cheerful')).toBeInTheDocument(); + }); + + it('should render without wrapping quotes for object type', () => { + const mockNode: Node = { + id: 'belief-6', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'object', id: 'obj1', value: 'chair', label: 'Object found:' }, + hasReduce: true, + }, + }; + + renderWithProviders( + + ); + + // Object type should not have wrapping quotes + const inputs = screen.getAllByDisplayValue('chair'); + expect(inputs.length).toBe(1); // Only the text input, no extra quote elements + }); + }); + + describe('User Interactions', () => { + it('should update belief type when select is changed', async () => { + const mockNode: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'keyword', id: 'kw1', value: 'hello', label: 'Keyword said:' }, + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const select = screen.getByDisplayValue('Keyword said:'); + await user.selectOptions(select, 'semantic'); + + await waitFor(() => { + const state = useFlowStore.getState(); + const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node; + expect(updatedNode?.data.belief.type).toBe('semantic'); + // Note: The component doesn't update the label when changing type + // So we can't test for label change + }); + }); + + it('should update text value when typing for keyword type', async () => { + const mockNode: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'keyword', id: 'kw1', value: '', label: 'Keyword said:' }, + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByPlaceholderText('keyword...'); + await user.type(input, 'help me{enter}'); + + await waitFor(() => { + const state = useFlowStore.getState(); + const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node; + expect(updatedNode?.data.belief.value).toBe('help me'); + }); + }); + + it('should update text value when typing for semantic type', async () => { + const mockNode: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'semantic', id: 'sem1', value: 'initial', label: 'Detected with LLM:' }, + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByDisplayValue('initial') as HTMLInputElement; + + // Clear the input + for (let i = 0; i < 'initial'.length; i++) { + await user.type(input, '{backspace}'); + } + await user.type(input, 'new semantic value{enter}'); + + await waitFor(() => { + const state = useFlowStore.getState(); + const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node; + expect(updatedNode?.data.belief.value).toBe('new semantic value'); + }); + }); + + it('should update emotion value when selecting from dropdown', async () => { + const mockNode: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'emotion', id: 'em1', value: 'happy', label: 'Emotion recognised:' }, + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const select = screen.getByDisplayValue('Happy'); + await user.selectOptions(select, 'sad'); + + await waitFor(() => { + const state = useFlowStore.getState(); + const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node; + expect(updatedNode?.data.belief.value).toBe('sad'); + }); + }); + + it('should preserve value when switching between text-based belief types', async () => { + const mockNode: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'keyword', id: 'kw1', value: 'test value', label: 'Keyword said:' }, + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + // Switch from keyword to semantic + const typeSelect = screen.getByDisplayValue('Keyword said:'); + await user.selectOptions(typeSelect, 'semantic'); + + await waitFor(() => { + const state = useFlowStore.getState(); + const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node; + expect(updatedNode?.data.belief.type).toBe('semantic'); + expect(updatedNode?.data.belief.value).toBe('test value'); // Value should be preserved + }); + }); + + it('should preserve value when switching from text type to emotion type', async () => { + const mockNode: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'keyword', id: 'kw1', value: 'some text', label: 'Keyword said:' }, + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + // Switch from keyword to emotion + const typeSelect = screen.getByDisplayValue('Keyword said:'); + await user.selectOptions(typeSelect, 'emotion'); + + await waitFor(() => { + const state = useFlowStore.getState(); + const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node; + 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'); + }); + }); + }); + + // ... rest of the tests remain the same, just fixing the Integration with Store section ... + + describe('Integration with Store', () => { + it('should properly update the store when changing belief value', async () => { + const mockNode: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'keyword', id: 'kw1', value: '', label: 'Keyword said:' }, + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByPlaceholderText('keyword...'); + await user.type(input, 'emergency{enter}'); + + await waitFor(() => { + const state = useFlowStore.getState(); + expect(state.nodes).toHaveLength(1); + expect(state.nodes[0].id).toBe('belief-1'); + const beliefData = state.nodes[0].data as BasicBeliefNodeData; + expect(beliefData.belief.value).toBe('emergency'); + expect(beliefData.belief.type).toBe('keyword'); + }); + }); + + it('should properly update the store when changing belief type', async () => { + const mockNode: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'keyword', id: 'kw1', value: 'test', label: 'Keyword said:' }, + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const select = screen.getByDisplayValue('Keyword said:'); + await user.selectOptions(select, 'object'); + + await waitFor(() => { + const state = useFlowStore.getState(); + const beliefData = state.nodes[0].data as BasicBeliefNodeData; + expect(beliefData.belief.type).toBe('object'); + // Note: The component doesn't update the label when changing type + expect(beliefData.belief.value).toBe('test'); // Value should be preserved + }); + }); + + it('should not affect other nodes when updating one belief node', async () => { + const belief1: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief 1', + droppable: true, + belief: { type: 'keyword', id: 'kw1', value: 'hello', label: 'Keyword said:' }, + hasReduce: true, + }, + }; + + const belief2: Node = { + id: 'belief-2', + type: 'basic_belief', + position: { x: 100, y: 0 }, + data: { + label: 'Belief 2', + droppable: true, + belief: { type: 'object', id: 'obj1', value: 'chair', label: 'Object found:' }, + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [belief1, belief2], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByDisplayValue('hello') as HTMLInputElement; + + // Clear the input + for (let i = 0; i < 'hello'.length; i++) { + await user.type(input, '{backspace}'); + } + await user.type(input, 'goodbye{enter}'); + + await waitFor(() => { + const state = useFlowStore.getState(); + const updatedBelief1 = state.nodes.find(n => n.id === 'belief-1') as Node; + const unchangedBelief2 = state.nodes.find(n => n.id === 'belief-2') as Node; + + expect(updatedBelief1.data.belief.value).toBe('goodbye'); + expect(unchangedBelief2.data.belief.value).toBe('chair'); + expect(unchangedBelief2.data.belief.type).toBe('object'); + }); + }); + + it('should handle multiple rapid updates to belief value', async () => { + const mockNode: Node = { + id: 'belief-1', + type: 'basic_belief', + position: { x: 0, y: 0 }, + data: { + label: 'Belief', + droppable: true, + belief: { type: 'semantic', id: 'sem1', value: 'initial', label: 'Detected with LLM:' }, + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByDisplayValue('initial') as HTMLInputElement; + + await user.type(input, '1'); + await waitFor(() => { + const state = useFlowStore.getState(); + const nodeData = state.nodes[0].data as BasicBeliefNodeData; + expect(nodeData.belief.value).toBe('initial'); + }); + + await user.type(input, '2'); + await waitFor(() => { + const state = useFlowStore.getState(); + const nodeData = state.nodes[0].data as BasicBeliefNodeData; + expect(nodeData.belief.value).toBe('initial'); + }); + + await user.type(input, '{enter}'); + await waitFor(() => { + const state = useFlowStore.getState(); + const nodeData = state.nodes[0].data as BasicBeliefNodeData; + expect(nodeData.belief.value).toBe('initial12'); + }); + }); + }); +}); \ No newline at end of file From 7925023f25984f79390a9548340143b600863759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Mon, 15 Dec 2025 14:51:58 +0100 Subject: [PATCH 06/10] fix: fix issues ariving from dev merge ref: N25B-408 --- .../visualProgrammingUI/NodeRegistry.ts | 6 ++- .../nodes/BasicBeliefNode.tsx | 39 +++++++++++++++---- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts index d142663..023440c 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts +++ b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts @@ -46,7 +46,7 @@ import TriggerNode, { TriggerReduce } from "./nodes/TriggerNode"; import { TriggerNodeDefaults } from "./nodes/TriggerNode.default"; -import BasicBeliefNode, { BasicBeliefConnects, BasicBeliefReduce } from "./nodes/BasicBeliefNode"; +import BasicBeliefNode, { BasicBeliefConnectionSource, BasicBeliefConnectionTarget, BasicBeliefDisconnectionSource, BasicBeliefDisconnectionTarget, BasicBeliefReduce } from "./nodes/BasicBeliefNode"; import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default"; /** @@ -112,6 +112,7 @@ export const NodeConnections = { norm: NormConnectionTarget, goal: GoalConnectionTarget, trigger: TriggerConnectionTarget, + basic_belief: BasicBeliefConnectionTarget, }, Sources: { start: StartConnectionSource, @@ -120,6 +121,7 @@ export const NodeConnections = { norm: NormConnectionSource, goal: GoalConnectionSource, trigger: TriggerConnectionSource, + basic_belief: BasicBeliefConnectionSource } } @@ -137,6 +139,7 @@ export const NodeDisconnections = { norm: NormDisconnectionTarget, goal: GoalDisconnectionTarget, trigger: TriggerDisconnectionTarget, + basic_belief: BasicBeliefDisconnectionTarget, }, Sources: { start: StartDisconnectionSource, @@ -145,6 +148,7 @@ export const NodeDisconnections = { norm: NormDisconnectionSource, goal: GoalDisconnectionSource, trigger: TriggerDisconnectionSource, + basic_belief: BasicBeliefDisconnectionSource, }, } diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx index 4942d48..a8f7ceb 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx @@ -2,8 +2,6 @@ import { Handle, type NodeProps, Position, - type Connection, - type Edge, type Node, } from '@xyflow/react'; import { Toolbar } from '../components/NodeComponents'; @@ -41,12 +39,39 @@ export type BasicBeliefNode = Node /** - * Determines whether a BasicBelief node can connect to another node or edge. - * - * @param connection - The connection or edge being attempted to connect towards. - * @returns `true` if the connection is defined; otherwise, `false`. + * This function is called whenever a connection is made with this node type as the target + * @param _thisNode the node of this node type which function is called + * @param _sourceNodeId the source of the received connection */ -export function BasicBeliefNodeCanConnect(_connection: Connection | Edge) { +export function BasicBeliefConnectionTarget(_thisNode: Node, _sourceNodeId: string) { + // no additional connection logic exists yet +} + +/** + * This function is called whenever a connection is made with this node type as the source + * @param _thisNode the node of this node type which function is called + * @param _targetNodeId the target of the created connection + */ +export function BasicBeliefConnectionSource(_thisNode: Node, _targetNodeId: string) { + // no additional connection logic exists yet +} + +/** + * This function is called whenever a connection is disconnected with this node type as the target + * @param _thisNode the node of this node type which function is called + * @param _sourceNodeId the source of the disconnected connection + */ +export function BasicBeliefDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) { + // no additional connection logic exists yet +} + +/** + * This function is called whenever a connection is disconnected with this node type as the source + * @param _thisNode the node of this node type which function is called + * @param _targetNodeId the target of the diconnected connection + */ +export function BasicBeliefDisconnectionSource(_thisNode: Node, _targetNodeId: string) { + // no additional connection logic exists yet } /** From bd2ffe622f6ed79f6e29042930b85833920d7825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Mon, 5 Jan 2026 10:24:08 +0100 Subject: [PATCH 07/10] chore: remove old connect function from basic belief --- .../visualProgrammingUI/nodes/BasicBeliefNode.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx index a8f7ceb..57c4d87 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx @@ -186,13 +186,4 @@ export function BasicBeliefReduce(node: Node, _nodes: Node[]) { type: data.belief.type, value: data.belief.value } -} - -/** - * 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 From 8f1367ed83ad7983e3561750a82e611aa99ad4ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Mon, 5 Jan 2026 10:30:29 +0100 Subject: [PATCH 08/10] chore: emotion dropdown doesnt automatically assign new value --- .../VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx index 57c4d87..183cfcb 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx @@ -93,6 +93,10 @@ export default function BasicBeliefNode(props: NodeProps) { belief: { ...data.belief, type: newType, + value: + newType === "emotion" + ? emotionOptions[0] + : data.belief.value, }, }); } From 9f26edb6ec9ae685f6a0817051f3815c74b60397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Mon, 5 Jan 2026 10:34:45 +0100 Subject: [PATCH 09/10] chore: dont use object, use detected object. --- .../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 183cfcb..821569f 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 From 01d73b777ab213c35f5c01c83ae6877fc0cf48cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Mon, 5 Jan 2026 10:37:05 +0100 Subject: [PATCH 10/10] chore: fix one test --- .../visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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'); }); }); });