Files
pepperplus-ui/src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.tsx
2026-01-28 10:34:36 +00:00

203 lines
7.9 KiB
TypeScript

// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {getConnectedEdges, type Node, type NodeProps, Position, useNodeConnections} from '@xyflow/react';
import {useEffect, useState} from "react";
import styles from '../../VisProg.module.css';
import type {EditorWarning} from "../components/EditorWarnings.tsx";
import {Toolbar} from '../components/NodeComponents.tsx';
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
import useFlowStore from "../VisProgStores.tsx";
import {BeliefGlobalReduce, noBeliefCycles, noMatchingLeftRightBelief} from "./BeliefGlobals.ts";
import switchStyles from './InferredBeliefNode.module.css';
/**
* The default data structure for an InferredBelief node
*/
export type InferredBeliefNodeData = {
label: string;
droppable: boolean;
inferredBelief: InferredBelief;
hasReduce: boolean;
};
/**
* stores a boolean to represent the operator
* and a left and right BeliefNode (can be both an inferred and a basic belief)
* in the form of their corresponding id's
*/
export type InferredBelief = {
left: string | undefined,
operator: boolean,
right: string | undefined,
}
export type InferredBeliefNode = Node<InferredBeliefNodeData>;
/**
* 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 InferredBeliefConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
const data = _thisNode.data as InferredBeliefNodeData;
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId
&& ['basic_belief', 'inferred_belief'].includes(node.type!)))
) {
const connectedEdges = getConnectedEdges([_thisNode], useFlowStore.getState().edges);
switch(connectedEdges.find(edge => edge.source === _sourceNodeId)?.targetHandle){
case 'beliefLeft': data.inferredBelief.left = _sourceNodeId; break;
case 'beliefRight': data.inferredBelief.right = _sourceNodeId; break;
}
}
}
/**
* 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 InferredBeliefConnectionSource(_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 InferredBeliefDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
const data = _thisNode.data as InferredBeliefNodeData;
if (_sourceNodeId === data.inferredBelief.left) data.inferredBelief.left = undefined;
if (_sourceNodeId === data.inferredBelief.right) data.inferredBelief.right = undefined;
}
/**
* 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 InferredBeliefDisconnectionSource(_thisNode: Node, _targetNodeId: string) {
// no additional connection logic exists yet
}
export const InferredBeliefTooltip = `
Combines two beliefs into a single belief using logical inference,
the node can be toggled between using "AND" and "OR" mode for inference`;
/**
* Defines how an InferredBelief node should be rendered
* @param {NodeProps<InferredBeliefNode>} props - Node properties provided by React Flow, including `id` and `data`.
* @returns The rendered InferredBeliefNode React element. (React.JSX.Element)
*/
export default function InferredBeliefNode(props: NodeProps<InferredBeliefNode>) {
const data = props.data;
const { updateNodeData, registerWarning, unregisterWarning } = useFlowStore();
// start of as an AND operator, true: "AND", false: "OR"
const [enforceAllBeliefs, setEnforceAllBeliefs] = useState(true);
// used to toggle operator
function onToggle() {
const newOperator = !enforceAllBeliefs; // compute the new value
setEnforceAllBeliefs(newOperator);
updateNodeData(props.id, {
...data,
inferredBelief: {
...data.inferredBelief,
operator: enforceAllBeliefs,
}
});
}
const beliefConnections = useNodeConnections({
id: props.id,
handleType: "target",
})
useEffect(() => {
const noBeliefsWarning : EditorWarning = {
scope: {
id: props.id,
handleId: undefined
},
type: 'MISSING_INPUT',
severity: 'ERROR',
description: `This AND/OR node is missing one or more beliefs,
please make sure to use both inputs of an AND/OR node`
};
if (beliefConnections.length < 2){
registerWarning(noBeliefsWarning);
return;
}
unregisterWarning(props.id, noBeliefsWarning.type);
},[beliefConnections.length, props.id, registerWarning, unregisterWarning])
return (
<>
<Toolbar nodeId={props.id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeInferredBelief}`}>
{/* The checkbox used to toggle the operator between 'AND' and 'OR' */}
<label className={switchStyles.operatorSwitch}>
<input
type="checkbox"
checked={data.inferredBelief.operator}
onChange={onToggle}
/>
<div className={switchStyles.switchVisual}></div>
<div className={switchStyles.switchLabels}>
<span title={"Belief is fulfilled if either of the supplied beliefs is true"}>OR</span>
<span title={"Belief is fulfilled if all of the supplied beliefs are true"}>AND</span>
</div>
</label>
{/* outgoing connections */}
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
allowOnlyConnectionsFromType(["norm", "trigger"]),
noBeliefCycles,
noMatchingLeftRightBelief
]}/>
{/* incoming connections */}
<SingleConnectionHandle type="target" position={Position.Left} style={{top: '30%'}} id="beliefLeft" rules={[
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"]),
noBeliefCycles,
noMatchingLeftRightBelief
]}/>
<SingleConnectionHandle type="target" position={Position.Left} style={{top: '70%'}} id="beliefRight" rules={[
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"]),
noBeliefCycles,
noMatchingLeftRightBelief
]}/>
</div>
</>
);
};
/**
* Reduces each BasicBelief, including its children down into its core data.
* @param {Node} node - The BasicBelief node to reduce.
* @param {Node[]} 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 InferredBeliefReduce(node: Node, nodes: Node[]) {
const data = node.data as InferredBeliefNodeData;
const leftBelief = nodes.find((node) => node.id === data.inferredBelief.left);
const rightBelief = nodes.find((node) => node.id === data.inferredBelief.right);
if (!leftBelief) { throw new Error("No Left belief found")}
if (!rightBelief) { throw new Error("No Right Belief found")}
const result: Record<string, unknown> = {
id: node.id,
left: BeliefGlobalReduce(leftBelief, nodes),
operator: data.inferredBelief.operator ? "AND" : "OR",
right: BeliefGlobalReduce(rightBelief, nodes),
};
return result
}