feat: added endpoint

ref:N25B-400
This commit is contained in:
Pim Hutting
2026-01-07 17:39:20 +01:00
28 changed files with 1927 additions and 133 deletions

View File

@@ -3,7 +3,6 @@ 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 {

View File

@@ -1,14 +1,13 @@
import React from 'react';
import React, { use } 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, useExperimentLogger } from ".//MonitoringPageAPI.ts"
type Goal = { id?: string | number; description?: string; achieved?: boolean };
type Trigger = { id?: string | number; label?: string ; achieved?: boolean };
type Norm = { id?: string | number; norm?: string };
const MonitoringPage: React.FC = () => {
const getPhaseIds = useProgramStore((s) => s.getPhaseIds);
const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase);
@@ -43,7 +42,7 @@ const MonitoringPage: React.FC = () => {
setLoading(false);
}
};
useExperimentLogger();
return (
<div className={styles.dashboardContainer}>
{/* HEADER */}

View File

@@ -1,4 +1,5 @@
const API_BASE = "http://localhost:8000/"; // Change depending on Pims interup agent/ correct endpoint
import React, { useEffect } from 'react';
const API_BASE = "http://localhost:8000"; // Change depending on Pims interup agent/ correct endpoint
/**
@@ -16,4 +17,37 @@ export async function nextPhase(): Promise<void> {
if (!res.ok) {
throw new Error("Failed to advance to next phase");
}
}
/**
* A hook that listens to the experiment stream and logs data to the console.
* It does not render anything.
*/
export function useExperimentLogger() {
useEffect(() => {
console.log("Starting Experiment Stream listener...");
const eventSource = new EventSource(`${API_BASE}/experiment_stream`);
eventSource.onmessage = (event) => {
try {
const parsedData = JSON.parse(event.data);
console.log(" [Experiment Update Received] ", parsedData);
} catch (err) {
console.warn("Received non-JSON experiment data:", event.data);
}
};
eventSource.onerror = (err) => {
console.error("SSE Connection Error (Experiment Stream):", err);
eventSource.close();
};
return () => {
console.log("Closing Experiment Stream listener...");
eventSource.close();
};
}, []);
}

View File

@@ -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);
}

View File

@@ -9,8 +9,10 @@ import {
import '@xyflow/react/dist/style.css';
import {useEffect, useState} from "react";
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 {PhaseNode} from "./visualProgrammingUI/nodes/PhaseNode.tsx";
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
import styles from './VisProg.module.css'
@@ -167,14 +169,15 @@ function runProgramm() {
*/
function graphReducer() {
const { nodes } = useFlowStore.getState();
return nodes
.filter((n) => n.type == 'phase')
return orderPhaseNodeArray(nodes.filter((n) => n.type == 'phase') as PhaseNode [])
.map((n) => {
const reducer = NodeReduces['phase'];
return reducer(n, nodes)
});
}
/**
* houses the entire page, so also UI elements
* that are not a part of the Visual Programming UI

View File

@@ -39,10 +39,10 @@ export const UndoRedo = (
* @param {BaseFlowState} state - the current state of the editor
* @returns {FlowSnapshot} - returns a snapshot of the current editor state
*/
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => ({
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
nodes: state.nodes,
edges: state.edges
});
}));
const initialState = config(set, get, api);

View File

@@ -46,6 +46,8 @@ import TriggerNode, {
TriggerReduce
} from "./nodes/TriggerNode";
import { TriggerNodeDefaults } from "./nodes/TriggerNode.default";
import BasicBeliefNode, { BasicBeliefConnectionSource, BasicBeliefConnectionTarget, BasicBeliefDisconnectionSource, BasicBeliefDisconnectionTarget, BasicBeliefReduce } from "./nodes/BasicBeliefNode";
import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default";
/**
* Registered node types in the visual programming system.
@@ -60,6 +62,7 @@ export const NodeTypes = {
norm: NormNode,
goal: GoalNode,
trigger: TriggerNode,
basic_belief: BasicBeliefNode,
};
/**
@@ -74,6 +77,7 @@ export const NodeDefaults = {
norm: NormNodeDefaults,
goal: GoalNodeDefaults,
trigger: TriggerNodeDefaults,
basic_belief: BasicBeliefNodeDefaults,
};
@@ -90,6 +94,7 @@ export const NodeReduces = {
norm: NormReduce,
goal: GoalReduce,
trigger: TriggerReduce,
basic_belief: BasicBeliefReduce,
}
@@ -107,6 +112,7 @@ export const NodeConnections = {
norm: NormConnectionTarget,
goal: GoalConnectionTarget,
trigger: TriggerConnectionTarget,
basic_belief: BasicBeliefConnectionTarget,
},
Sources: {
start: StartConnectionSource,
@@ -115,6 +121,7 @@ export const NodeConnections = {
norm: NormConnectionSource,
goal: GoalConnectionSource,
trigger: TriggerConnectionSource,
basic_belief: BasicBeliefConnectionSource
}
}
@@ -132,6 +139,7 @@ export const NodeDisconnections = {
norm: NormDisconnectionTarget,
goal: GoalDisconnectionTarget,
trigger: TriggerDisconnectionTarget,
basic_belief: BasicBeliefDisconnectionTarget,
},
Sources: {
start: StartDisconnectionSource,
@@ -140,6 +148,7 @@ export const NodeDisconnections = {
norm: NormDisconnectionSource,
goal: GoalDisconnectionSource,
trigger: TriggerDisconnectionSource,
basic_belief: BasicBeliefDisconnectionSource,
},
}
@@ -151,7 +160,6 @@ export const NodeDisconnections = {
export const NodeDeletes = {
start: () => false,
end: () => false,
test: () => false, // Used for coverage of universal/ undefined nodes
}
/**
@@ -164,5 +172,5 @@ export const NodesInPhase = {
start: () => false,
end: () => false,
phase: () => false,
test: () => false, // Used for coverage of universal/ undefined nodes
basic_belief: () => false,
}

View File

@@ -28,31 +28,31 @@ 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<string, unknown>, 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<string, unknown>, deletable?: boolean) {
const defaultData = NodeDefaults[type as keyof typeof NodeDefaults]
return {
id,
type,
position,
deletable,
data: {
...JSON.parse(JSON.stringify(defaultData)),
...data,
},
}
}
return {...defaultData, ...newData}
}
//* Initial nodes, created by using createNode. */
const initialNodes : Node[] = [
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false),
createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false),
createNode('phase-1', 'phase', {x:200, y:100}, {label: "Phase 1", children : []}),
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"], critical:false}),
createNode(crypto.randomUUID(), 'phase', {x:200, y:100}, {label: "Phase 1", children : []}),
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"], critical:false}),
];
// * Initial edges * /
const initialEdges: Edge[] = [
{ id: 'start-phase-1', source: 'start', target: 'phase-1' },
{ id: 'phase-1-end', source: 'phase-1', target: 'end' },
];
const initialEdges: Edge[] = []; // no initial edges as edge connect events don't fire when using initial edges
/**

View File

@@ -0,0 +1,30 @@
import {
Handle,
useNodeConnections,
type HandleType,
type Position
} from '@xyflow/react';
const LimitedConnectionCountHandle = (props: {
node_id: string,
type: HandleType,
position: Position,
connection_count: number,
id?: string
}) => {
const connections = useNodeConnections({
id: props.node_id,
handleType: props.type,
handleId: props.id,
});
return (
<Handle
{...props}
isConnectable={connections.length < props.connection_count}
/>
);
};
export default LimitedConnectionCountHandle;

View File

@@ -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,
};

View File

@@ -0,0 +1,193 @@
import {
Handle,
type NodeProps,
Position,
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 | 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 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<BasicBeliefNodeData>
/**
* 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 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
}
/**
* 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<BasicBeliefNode>) {
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,
value:
newType === "emotion"
? emotionOptions[0]
: data.belief.value,
},
});
}
// 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 (
<>
<Toolbar nodeId={props.id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeBasicBelief /*TODO: Change this*/}`}>
<div className={"flex-center-x gap-sm"}>
<label htmlFor={label_input_id}>Belief:</label>
</div>
<div className={"flex-row gap-sm"}>
<select
value={data.belief.type}
onChange={(e) => updateBeliefType(e.target.value as BeliefString)}
>
<option value="keyword">Keyword said:</option>
<option value="semantic">Detected with LLM:</option>
<option value="object">Object found:</option>
<option value="emotion">Emotion recognised:</option>
</select>
{wrapping}
{data.belief.type === "emotion" && (
<select
value={data.belief.value}
onChange={(e) => updateValue(e.target.value)}
>
{emotionOptions.map((emotion) => (
<option key={emotion} value={emotion.toLowerCase()}>
{emotion}
</option>
))}
</select>
)}
{data.belief.type !== "emotion" &&
(<TextField
id={label_input_id}
value={data.belief.value}
setValue={updateValue}
placeholder={placeholder}
/>)}
{wrapping}
</div>
<Handle type="source" position={Position.Right} id="source"/>
</div>
</>
);
};
/**
* 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: Node, _nodes: Node[]) {
const data = node.data as BasicBeliefNodeData;
return {
id: node.id,
type: data.belief.type,
value: data.belief.value
}
}

View File

@@ -1,9 +1,9 @@
import {
Handle,
type NodeProps,
Position,
type Node,
} from '@xyflow/react';
import LimitedConnectionCountHandle from "../components/CustomNodeHandles.tsx";
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
@@ -32,7 +32,13 @@ export default function EndNode(props: NodeProps<EndNode>) {
<div className={"flex-row gap-sm"}>
End
</div>
<Handle type="target" position={Position.Left} id="target"/>
<LimitedConnectionCountHandle
node_id={props.id}
type="target"
position={Position.Left}
connection_count={1}
id="target"
/>
</div>
</>
);

View File

@@ -6,6 +6,7 @@ import type { NormNodeData } from "./NormNode";
export const NormNodeDefaults: NormNodeData = {
label: "Norm Node",
droppable: true,
conditions: [],
norm: "",
hasReduce: true,
critical: false,

View File

@@ -8,6 +8,7 @@ import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import { TextField } from '../../../../components/TextField';
import useFlowStore from '../VisProgStores';
import { BasicBeliefReduce } from './BasicBeliefNode';
/**
* The default data dot a phase node
@@ -19,6 +20,7 @@ import useFlowStore from '../VisProgStores';
export type NormNodeData = {
label: string;
droppable: boolean;
conditions: string[]; // List of (basic) belief nodes' ids.
norm: string;
hasReduce: boolean;
critical: boolean;
@@ -67,7 +69,14 @@ export default function NormNode(props: NodeProps<NormNode>) {
onChange={(e) => setCritical(e.target.checked)}
/>
</div>
{data.conditions.length > 0 && (<div className={"flex-row gap-md align-center"} data-testid="norm-condition-information">
<label htmlFor={checkbox_id}>{data.conditions.length} condition{data.conditions.length > 1 ? "s" : ""}/ belief{data.conditions.length > 1 ? "s" : ""} attached.</label>
</div>)}
<Handle type="source" position={Position.Right} id="norms"/>
<Handle type="target" position={Position.Bottom} id="norms"/>
</div>
</>;
};
@@ -78,14 +87,29 @@ export default function NormNode(props: NodeProps<NormNode>) {
* @param node The Node Properties of this node.
* @param _nodes all the nodes in the graph
*/
export function NormReduce(node: Node, _nodes: Node[]) {
export function NormReduce(node: Node, nodes: Node[]) {
const data = node.data as NormNodeData;
return {
id: node.id,
label: data.label,
norm: data.norm,
critical: data.critical,
}
// conditions nodes - make sure to check for empty arrays
let conditionNodes: Node[] = [];
if (data.conditions)
conditionNodes = nodes.filter((node) => data.conditions.includes(node.id));
// Build the result object
const result: Record<string, unknown> = {
id: node.id,
label: data.label,
norm: data.norm,
critical: data.critical,
};
// Go over our conditionNodes. They should either be Basic (OR TODO: Inferred)
const reducer = BasicBeliefReduce;
result["basic_beliefs"] = conditionNodes.map((condition) => reducer(condition, nodes))
// When the Inferred is being implemented, you should follow the same kind of structure that PhaseNode has,
// dividing the conditions into basic and inferred, then calling the correct reducer on them.
return result
}
/**
@@ -94,7 +118,11 @@ export function NormReduce(node: Node, _nodes: Node[]) {
* @param _sourceNodeId the source of the received connection
*/
export function NormConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
// no additional connection logic exists yet
const data = _thisNode.data as NormNodeData;
// If we got a belief connected, this is a condition for the norm.
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
data.conditions.push(_sourceNodeId);
}
}
/**
@@ -112,7 +140,11 @@ export function NormConnectionSource(_thisNode: Node, _targetNodeId: string) {
* @param _sourceNodeId the source of the disconnected connection
*/
export function NormDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
// no additional connection logic exists yet
const data = _thisNode.data as NormNodeData;
// If we got a belief connected, this is a condition for the norm.
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
data.conditions = data.conditions.filter(id => id != _sourceNodeId);
}
}
/**

View File

@@ -8,4 +8,6 @@ export const PhaseNodeDefaults: PhaseNodeData = {
droppable: true,
children: [],
hasReduce: true,
nextPhaseId: null,
isFirstPhase: false,
};

View File

@@ -9,6 +9,7 @@ import styles from '../../VisProg.module.css';
import { NodeReduces, NodesInPhase, NodeTypes} from '../NodeRegistry';
import useFlowStore from '../VisProgStores';
import { TextField } from '../../../../components/TextField';
import LimitedConnectionCountHandle from "../components/CustomNodeHandles.tsx";
/**
* The default data dot a phase node
@@ -16,12 +17,15 @@ import { TextField } from '../../../../components/TextField';
* @param droppable: whether this node is droppable from the drop bar (initialized as true)
* @param children: ID's of children of this node
* @param hasReduce: whether this node has reducing functionality (true by default)
* @param nextPhaseId:
*/
export type PhaseNodeData = {
label: string;
droppable: boolean;
children: string[];
hasReduce: boolean;
nextPhaseId: string | "end" | null;
isFirstPhase: boolean;
};
export type PhaseNode = Node<PhaseNodeData>
@@ -50,9 +54,21 @@ export default function PhaseNode(props: NodeProps<PhaseNode>) {
placeholder={"Phase ..."}
/>
</div>
<Handle type="target" position={Position.Left} id="target"/>
<LimitedConnectionCountHandle
node_id={props.id}
type="target"
position={Position.Left}
connection_count={1}
id="target"
/>
<Handle type="target" position={Position.Bottom} id="norms"/>
<Handle type="source" position={Position.Right} id="source"/>
<LimitedConnectionCountHandle
node_id={props.id}
type="source"
position={Position.Right}
connection_count={1}
id="source"
/>
</div>
</>
);
@@ -65,8 +81,8 @@ export default function PhaseNode(props: NodeProps<PhaseNode>) {
* @returns A collection of all reduced nodes in this phase, starting with this phases' reduced data.
*/
export function PhaseReduce(node: Node, nodes: Node[]) {
const thisnode = node as PhaseNode;
const data = thisnode.data as PhaseNodeData;
const thisNode = node as PhaseNode;
const data = thisNode.data as PhaseNodeData;
// node typings that are not in phase
const nodesNotInPhase: string[] = Object.entries(NodesInPhase)
@@ -85,8 +101,8 @@ export function PhaseReduce(node: Node, nodes: Node[]) {
// Build the result object
const result: Record<string, unknown> = {
id: thisnode.id,
label: data.label,
id: thisNode.id,
label: data.label,
};
nodesInPhase.forEach((type) => {
@@ -109,13 +125,19 @@ export function PhaseReduce(node: Node, nodes: Node[]) {
* @param _sourceNodeId the source of the received connection
*/
export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
const node = _thisNode as PhaseNode
const data = node.data as PhaseNodeData
// we only add none phase nodes to the children
if (!(useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'phase'))) {
data.children.push(_sourceNodeId)
}
const data = _thisNode.data as PhaseNodeData
const nodes = useFlowStore.getState().nodes;
const sourceNode = nodes.find((node) => node.id === _sourceNodeId)!
switch (sourceNode.type) {
case "phase": break;
case "start": data.isFirstPhase = true; break;
// we only add none phase or start nodes to the children
// endNodes cannot be the source of an outgoing connection
// so we don't need to cover them with a special case
// before handling the default behavior
default: data.children.push(_sourceNodeId); break;
}
}
/**
@@ -124,7 +146,19 @@ export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
* @param _targetNodeId the target of the created connection
*/
export function PhaseConnectionSource(_thisNode: Node, _targetNodeId: string) {
// no additional connection logic exists yet
const data = _thisNode.data as PhaseNodeData
const nodes = useFlowStore.getState().nodes;
const targetNode = nodes.find((node) => node.id === _targetNodeId)
if (!targetNode) {throw new Error("Source node not found")}
// we set the nextPhaseId to the next target's id if the target is a phaseNode,
// or "end" if the target node is the end node
switch (targetNode.type) {
case 'phase': data.nextPhaseId = _targetNodeId; break;
case 'end': data.nextPhaseId = "end"; break;
default: break;
}
}
/**
@@ -133,9 +167,23 @@ export function PhaseConnectionSource(_thisNode: Node, _targetNodeId: string) {
* @param _sourceNodeId the source of the disconnected connection
*/
export function PhaseDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
const node = _thisNode as PhaseNode
const data = node.data as PhaseNodeData
data.children = data.children.filter((child) => { if (child != _sourceNodeId) return child; });
const data = _thisNode.data as PhaseNodeData
const nodes = useFlowStore.getState().nodes;
const sourceNode = nodes.find((node) => node.id === _sourceNodeId)
const sourceType = sourceNode ? sourceNode.type : "deleted";
switch (sourceType) {
case "phase": break;
case "start": data.isFirstPhase = false; break;
// we only add none phase or start nodes to the children
// endNodes cannot be the source of an outgoing connection
// so we don't need to cover them with a special case
// before handling the default behavior
default:
data.children = data.children.filter((child) => { if (child != _sourceNodeId) return child; });
break;
}
}
/**
@@ -144,5 +192,12 @@ export function PhaseDisconnectionTarget(_thisNode: Node, _sourceNodeId: string)
* @param _targetNodeId the target of the diconnected connection
*/
export function PhaseDisconnectionSource(_thisNode: Node, _targetNodeId: string) {
// no additional connection logic exists yet
const data = _thisNode.data as PhaseNodeData
const nodes = useFlowStore.getState().nodes;
// if the target is a phase or end node set the nextPhaseId to null,
// as we are no longer connected to a subsequent phaseNode or to the endNode
if (nodes.some((node) => node.id === _targetNodeId && ['phase', 'end'].includes(node.type!))){
data.nextPhaseId = null;
}
}

View File

@@ -1,9 +1,9 @@
import {
Handle,
type NodeProps,
Position,
type Node,
} from '@xyflow/react';
import LimitedConnectionCountHandle from "../components/CustomNodeHandles.tsx";
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
@@ -31,7 +31,13 @@ export default function StartNode(props: NodeProps<StartNode>) {
<div className={"flex-row gap-sm"}>
Start
</div>
<Handle type="source" position={Position.Right} id="source"/>
<LimitedConnectionCountHandle
node_id={props.id}
type="source"
position={Position.Right}
connection_count={1}
id="source"
/>
</div>
</>
);