From 3e73e78ee9b3cbce369335fb6e81e0454aac6073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Tue, 18 Nov 2025 13:25:13 +0100 Subject: [PATCH] chore: merge the rest of the nodes back into this structure, and make sure that start and end nodes are not deletable. --- src/pages/VisProgPage/VisProg.tsx | 48 +++---- .../visualProgrammingUI/NodeRegistry.ts | 19 +++ .../visualProgrammingUI/VisProgStores.tsx | 23 ++- .../visualProgrammingUI/nodes/EndNode.tsx | 2 +- .../nodes/GoalNode.default.ts | 3 +- .../visualProgrammingUI/nodes/GoalNode.tsx | 57 ++++++-- .../visualProgrammingUI/nodes/StartNode.tsx | 2 +- .../nodes/TriggerNode.default.ts | 3 +- .../visualProgrammingUI/nodes/TriggerNode.tsx | 134 +++++++++++++++--- 9 files changed, 223 insertions(+), 68 deletions(-) diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index 70a0339..5489e3c 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -64,35 +64,34 @@ const VisProgUI = () => { } = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore return ( -
-
- - - {/* contains the drag and drop panel for nodes */} - - - - -
+
+ + + {/* contains the drag and drop panel for nodes */} + + + +
); }; + /** * Places the VisProgUI component inside a ReactFlowProvider * @@ -112,6 +111,7 @@ function VisualProgrammingUI() { function runProgram() { const program = graphReducer(); console.log(program); + console.log(JSON.stringify(program, null, 2)); } /** diff --git a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts index 6a98c0a..e02f5f2 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts +++ b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts @@ -6,12 +6,18 @@ import { EndNodeDefaults } from "./nodes/EndNode.default"; import { StartNodeDefaults } from "./nodes/StartNode.default"; import { PhaseNodeDefaults } from "./nodes/PhaseNode.default"; import { NormNodeDefaults } from "./nodes/NormNode.default"; +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"; export const NodeTypes = { start: StartNode, end: EndNode, phase: PhaseNode, norm: NormNode, + goal: GoalNode, + trigger: TriggerNode, }; // Default node data for creation @@ -20,6 +26,8 @@ export const NodeDefaults = { end: EndNodeDefaults, phase: PhaseNodeDefaults, norm: NormNodeDefaults, + goal: GoalNodeDefaults, + trigger: TriggerNodeDefaults, }; export const NodeReduces = { @@ -27,6 +35,8 @@ export const NodeReduces = { end: EndReduce, phase: PhaseReduce, norm: NormReduce, + goal: GoalReduce, + trigger: TriggerReduce, } export const NodeConnects = { @@ -34,4 +44,13 @@ export const NodeConnects = { end: EndConnects, phase: PhaseConnects, norm: NormConnects, + goal: GoalConnects, + trigger: TriggerConnects, +} + +// Function to tell the visual program if we're allowed to delete them... +// Right now it doesn't take in any values, but that could also be done later. +export const NodeDeletes = { + start: () => false, + end: () => false, } \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx index f38013f..49c296b 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 type { FlowState } from './VisProgTypes'; -import { NodeDefaults, NodeConnects } from './NodeRegistry'; +import { NodeDefaults, NodeConnects, NodeDeletes } from './NodeRegistry'; /** @@ -20,21 +20,22 @@ import { NodeDefaults, NodeConnects } from './NodeRegistry'; * @param data the data in the node to create * @constructor */ -function createNode(id: string, type: string, position: XYPosition, data: Record) { +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 {...defaultData, ...newData} } //* Initial nodes, created by using createNode. */ const initialNodes : Node[] = [ - createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}), - createNode('end', 'end', {x: 370, y: 100}, {label: "End"}), + createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false), + createNode('end', 'end', {x: 370, y: 100}, {label: "End"}, false), createNode('phase-1', 'phase', {x:200, y:100}, {label: "Phase 1", children: ['end', 'start']}), createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"]}), ]; @@ -92,12 +93,20 @@ const useFlowStore = create((set, get) => ({ set({ edgeReconnectSuccessful: true }); }, - deleteNode: (nodeId) => - set({ + deleteNode: (nodeId) => { + // Let's find our node to check if they have a special deletion function + const ourNode = get().nodes.find((n)=>n.id==nodeId); + const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1] + + // If there's no function, OR, our function tells us we can delete it, let's do so... + if (ourFunction == undefined || ourFunction()) { + set({ nodes: get().nodes.filter((n) => n.id !== nodeId), edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId), - }), + })} + }, + setNodes: (nodes) => set({ nodes }), setEdges: (edges) => set({ edges }), diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx index c7007e6..b7159b6 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx @@ -18,7 +18,7 @@ export type EndNode = Node export default function EndNode(props: NodeProps) { return ( <> - +
End diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts b/src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts index a55832e..fc4d3aa 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts @@ -6,6 +6,7 @@ import type { GoalNodeData } from "./GoalNode"; export const GoalNodeDefaults: GoalNodeData = { label: "Goal Node", droppable: true, - GoalList: [], + description: "The robot will strive towards this goal", + achieved: false, hasReduce: true, }; \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.tsx index 799c199..ce0b119 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.tsx @@ -8,6 +8,8 @@ import { } from '@xyflow/react'; import { Toolbar } from '../components/NodeComponents'; import styles from '../../VisProg.module.css'; +import { TextField } from '../../../../components/TextField'; +import useFlowStore from '../VisProgStores'; /** * The default data dot a Goal node @@ -17,8 +19,9 @@ import styles from '../../VisProg.module.css'; */ export type GoalNodeData = { label: string; + description: string; droppable: boolean; - GoalList: string[]; + achieved: boolean; hasReduce: boolean; }; @@ -37,23 +40,47 @@ export function GoalNodeCanConnect(connection: Connection | Edge): boolean { * @returns React.JSX.Element */ export default function GoalNode(props: NodeProps) { - const label_input_id = `Goal_${props.id}_label_input`; const data = props.data as GoalNodeData; - return ( - <> - -
-
- - {props.data.label as string} -
- {data.GoalList.map((Goal) => (
{Goal}
))} - + const {updateNodeData} = useFlowStore(); + + const text_input_id = `goal_${props.id}_text_input`; + const checkbox_id = `goal_${props.id}_checkbox`; + + const setDescription = (value: string) => { + updateNodeData(props.id, {...data, description: value}); + } + + const setAchieved = (value: boolean) => { + updateNodeData(props.id, {...data, achieved: value}); + } + + return <> + +
+
+ + setDescription(val)} + placeholder={"To ..."} + />
- - ); +
+ + setAchieved(e.target.checked)} + /> +
+ +
+ ; } + /** * Reduces each Goal, including its children down into its relevant data. * @param props: The Node Properties of this node. @@ -66,7 +93,7 @@ export function GoalReduce(node: Node, nodes: Node[]) { const data = node.data as GoalNodeData; return { label: data.label, - list: data.GoalList, + achieved: data.achieved, } } diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx index a3a3ce6..d99a6ef 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx @@ -20,7 +20,7 @@ export type StartNode = Node export default function StartNode(props: NodeProps) { return ( <> - +
Start diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.default.ts b/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.default.ts index d3edeca..725f0d8 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.default.ts +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.default.ts @@ -6,6 +6,7 @@ import type { TriggerNodeData } from "./TriggerNode"; export const TriggerNodeDefaults: TriggerNodeData = { label: "Trigger Node", droppable: true, - TriggerList: [], + triggers: [{id: "help-trigger", keyword:"help"}], + triggerType: "keywords", hasReduce: true, }; \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx index f9424ac..97a792e 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx @@ -8,6 +8,10 @@ import { } from '@xyflow/react'; import { Toolbar } from '../components/NodeComponents'; import styles from '../../VisProg.module.css'; +import useFlowStore from '../VisProgStores'; +import { useState } from 'react'; +import { RealtimeTextField, TextField } from '../../../../components/TextField'; +import duplicateIndices from '../../../../utils/duplicateIndices'; /** * The default data dot a Trigger node @@ -18,12 +22,12 @@ import styles from '../../VisProg.module.css'; export type TriggerNodeData = { label: string; droppable: boolean; - TriggerList: string[]; + triggerType: unknown; + triggers: [unknown]; hasReduce: boolean; }; - export type TriggerNode = Node @@ -37,21 +41,28 @@ export function TriggerNodeCanConnect(connection: Connection | Edge): boolean { * @returns React.JSX.Element */ export default function TriggerNode(props: NodeProps) { - const label_input_id = `Trigger_${props.id}_label_input`; - const data = props.data as TriggerNodeData; - return ( - <> - -
-
- - {props.data.label as string} -
- {data.TriggerList.map((Trigger) => (
{Trigger}
))} - -
- - ); + const data = props.data as TriggerNodeData + const {updateNodeData} = useFlowStore(); + + const setKeywords = (keywords: Keyword[]) => { + updateNodeData(props.id, {...data, triggers: keywords}); + } + + return <> + +
+ {data.triggerType === "emotion" && ( +
Emotion?
+ )} + {data.triggerType === "keywords" && ( + + )} + +
+ ; } /** @@ -66,7 +77,7 @@ export function TriggerReduce(node: Node, nodes: Node[]) { const data = node.data as TriggerNodeData; return { label: data.label, - list: data.TriggerList, + list: data.triggers, } } @@ -75,4 +86,91 @@ export function TriggerConnects(thisNode: Node, otherNode: Node, isThisSource: b if (thisNode == undefined && otherNode == undefined && isThisSource == false) { console.warn("Impossible node connection called in EndConnects") } +} + + +export type EmotionTriggerNodeProps = { + type: "emotion"; + value: string; +} + +type Keyword = { id: string, keyword: string }; + +export type KeywordTriggerNodeProps = { + type: "keywords"; + value: Keyword[]; +} + +export type TriggerNodeProps = EmotionTriggerNodeProps | KeywordTriggerNodeProps; + +function KeywordAdder({ addKeyword }: { addKeyword: (keyword: string) => void }) { + const [input, setInput] = useState(""); + + const text_input_id = "keyword_adder_input"; + + return
+ + { + if (!input) return; + addKeyword(input); + setInput(""); + }} + placeholder={"..."} + className={"flex-1"} + /> +
; +} + +function Keywords({ + keywords, + setKeywords, +}: { + keywords: Keyword[]; + setKeywords: (keywords: Keyword[]) => void; +}) { + type Interpolatable = string | number | boolean | bigint | null | undefined; + + const inputElementId = (id: Interpolatable) => `keyword_${id}_input`; + + /** Indices of duplicates in the keyword array. */ + const [duplicates, setDuplicates] = useState([]); + + function replace(id: string, value: string) { + value = value.trim(); + const newKeywords = value === "" + ? keywords.filter((kw) => kw.id != id) + : keywords.map((kw) => kw.id === id ? {...kw, keyword: value} : kw); + setKeywords(newKeywords); + setDuplicates(duplicateIndices(newKeywords.map((kw) => kw.keyword))); + } + + function add(value: string) { + value = value.trim(); + if (value === "") return; + const newKeywords = [...keywords, {id: crypto.randomUUID(), keyword: value}]; + setKeywords(newKeywords); + setDuplicates(duplicateIndices(newKeywords.map((kw) => kw.keyword))); + } + + return <> + Triggers when {keywords.length <= 1 ? "the keyword is" : "all keywords are"} spoken. + {[...keywords].map(({id, keyword}, index) => { + return
+ + replace(id, val)} + placeholder={"..."} + className={"flex-1"} + invalid={duplicates.includes(index)} + /> +
; + })} + + ; } \ No newline at end of file