Files
pepperplus-ui/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx
Pim Hutting cada85e253 chore: added tests for monitoringpage
also moved some functions from VisProg outside VisProg.tsx into
VisProgLogic.tsx so I can reuse it for the reset experiment function
of monitor page
Also fixed a small merge error in TriggerNodes.tsx

ref: N25B-400
2026-01-18 12:38:23 +01:00

206 lines
7.6 KiB
TypeScript

import {
type NodeProps,
Position,
type Node,
} from '@xyflow/react';
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
import useFlowStore from '../VisProgStores';
import {PlanReduce, type Plan } from '../components/Plan';
import PlanEditorDialog from '../components/PlanEditor';
import {BeliefGlobalReduce} from "./BeliefGlobals.ts";
import type { GoalNode } from './GoalNode.tsx';
import { defaultPlan } from '../components/Plan.default.ts';
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
import { TextField } from '../../../../components/TextField.tsx';
/**
* The default data structure for a Trigger 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 Trigger node.
* @property droppable: Whether this node can be dropped from the toolbar (default: true).
* @property hasReduce - Whether this node supports reduction logic.
*/
export type TriggerNodeData = {
label: string;
name: string;
droppable: boolean;
condition?: string; // id of the belief
plan?: Plan;
hasReduce: boolean;
};
export type TriggerNode = Node<TriggerNodeData>
/**
* Defines how a Trigger node should be rendered
* @param props - Node properties provided by React Flow, including `id` and `data`.
* @returns The rendered TriggerNode React element (React.JSX.Element).
*/
export default function TriggerNode(props: NodeProps<TriggerNode>) {
const data = props.data;
const {updateNodeData} = useFlowStore();
const setName= (value: string) => {
updateNodeData(props.id, {...data, name: value})
}
return <>
<Toolbar nodeId={props.id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
<TextField
value={props.data.name}
setValue={(val) => setName(val)}
placeholder={"Name of this trigger..."}
/>
<div className={"flex-row gap-md"}>Triggers when the condition is met.</div>
<div className={"flex-row gap-md"}>Condition/ Belief is currently {data.condition ? "" : "not"} set. {data.condition ? "🟢" : "🔴"}</div>
<div className={"flex-row gap-md"}>Plan{data.plan ? (": " + data.plan.name) : ""} is currently {data.plan ? "" : "not"} set. {data.plan ? "🟢" : "🔴"}</div>
<MultiConnectionHandle type="source" position={Position.Right} id="TriggerSource" rules={[
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
]}/>
<SingleConnectionHandle
type="target"
position={Position.Bottom}
id="TriggerBeliefs"
style={{ left: '40%' }}
rules={[
allowOnlyConnectionsFromType(['basic_belief', "inferred_belief"]),
]}
/>
<MultiConnectionHandle
type="target"
position={Position.Bottom}
id="GoalTarget"
style={{ left: '60%' }}
rules={[
allowOnlyConnectionsFromType(['goal']),
]}
/>
<PlanEditorDialog
plan={data.plan}
onSave={(plan) => {
updateNodeData(props.id, {
...data,
plan,
});
}}
/>
</div>
</>;
}
/**
* Reduces each Trigger, including its children down into its core data.
* @param node - The Trigger 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 triggers.
*/
export function TriggerReduce(node: Node, nodes: Node[]) {
const data = node.data as TriggerNodeData;
const conditionNode = data.condition ? nodes.find((n)=>n.id===data.condition) : undefined
const conditionData = conditionNode ? BeliefGlobalReduce(conditionNode, nodes) : ""
return {
id: node.id,
name: node.data.name,
condition: conditionData, // Make sure we have a condition before reducing, or default to ""
plan: !data.plan ? "" : PlanReduce(nodes, data.plan), // Make sure we have a plan when reducing, or default to ""
}
}
export const TriggerTooltip = `
A trigger node is used to make Pepper execute a predefined plan -
consisting of one or more actions - when the connected beliefs are met`;
/**
* 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 TriggerConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
// no additional connection logic exists yet
const data = _thisNode.data as TriggerNodeData;
// If we got a belief connected, this is the condition for the norm.
const nodes = useFlowStore.getState().nodes;
const otherNode = nodes.find((x) => x.id === _sourceNodeId)
if (!otherNode) return;
if (otherNode.type === 'basic_belief'|| otherNode.type ==='inferred_belief') {
data.condition = _sourceNodeId;
}
else if (otherNode.type === 'goal') {
// First, let's see if we have a plan currently. If not, let's create a default plan with this goal inside.:)
if (!data.plan) {
data.plan = insertGoalInPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()} as Plan, otherNode as GoalNode)
}
// Else, lets just insert this goal into our current plan.
else {
data.plan = insertGoalInPlan(structuredClone(data.plan), otherNode as GoalNode)
}
}
}
/**
* 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 TriggerConnectionSource(_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 TriggerDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
// no additional connection logic exists yet
const data = _thisNode.data as TriggerNodeData;
// remove if the target of disconnection was our condition
if (_sourceNodeId == data.condition) data.condition = undefined
data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId)
}
/**
* 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 TriggerDisconnectionSource(_thisNode: Node, _targetNodeId: string) {
// no additional connection logic exists yet
}
// Definitions for the possible triggers, being keywords and emotions
/** Represents a single keyword trigger entry. */
type Keyword = { id: string, keyword: string };
/** Properties for an emotion-type trigger node. */
export type EmotionTriggerNodeProps = {
type: "emotion";
value: string;
}
/** Props for a keyword-type trigger node. */
export type KeywordTriggerNodeProps = {
type: "keywords";
value: Keyword[];
}
/** Union type for all possible Trigger node configurations. */
export type TriggerNodeProps = EmotionTriggerNodeProps | KeywordTriggerNodeProps;