import { type NodeProps, Position, type Node, useNodeConnections } from '@xyflow/react'; import {useEffect} from "react"; import type {EditorWarning} from "../components/EditorWarnings.tsx"; 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 /** * 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) { const data = props.data; const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore(); const setName= (value: string) => { updateNodeData(props.id, {...data, name: value}) } const beliefInput = useNodeConnections({ id: props.id, handleType: "target", handleId: "TriggerBeliefs" }) const outputCons = useNodeConnections({ id: props.id, handleType: "source", handleId: "TriggerSource" }) useEffect(() => { const noPhaseConnectionWarning : EditorWarning = { scope: { id: props.id, handleId: 'TriggerSource' }, type: 'MISSING_OUTPUT', severity: 'INFO', description: "This triggerNode is missing a condition/belief, please make sure to connect a belief node to " }; if (outputCons.length === 0){ registerWarning(noPhaseConnectionWarning); return; } unregisterWarning(props.id, `${noPhaseConnectionWarning.type}:${noPhaseConnectionWarning.scope.handleId}`); },[outputCons.length, props.id, registerWarning, unregisterWarning]) useEffect(() => { const noBeliefWarning : EditorWarning = { scope: { id: props.id, handleId: 'TriggerBeliefs' }, type: 'MISSING_INPUT', severity: 'ERROR', description: "This triggerNode is missing a condition/belief, please make sure to connect a belief node to " }; if (beliefInput.length === 0 && outputCons.length !== 0){ registerWarning(noBeliefWarning); return; } unregisterWarning(props.id, `${noBeliefWarning.type}:${noBeliefWarning.scope.handleId}`); },[beliefInput.length, outputCons.length, props.id, registerWarning, unregisterWarning]) useEffect(() => { const noPlanWarning : EditorWarning = { scope: { id: props.id, handleId: undefined }, type: 'PLAN_IS_UNDEFINED', severity: 'ERROR', description: "This triggerNode is missing a plan, please make sure to create a plan by using the create plan button" }; if (!data.plan && outputCons.length !== 0){ registerWarning(noPlanWarning); return; } unregisterWarning(props.id, noPlanWarning.type); },[data.plan, outputCons.length, props.id, registerWarning, unregisterWarning]) return <>
setName(val)} placeholder={"Name of this trigger..."} />
Triggers when the condition is met.
Condition/ Belief is currently {data.condition ? "" : "not"} set. {data.condition ? "🟢" : "🔴"}
Plan{data.plan ? (": " + data.plan.name) : ""} is currently {data.plan ? "" : "not"} set. {data.plan ? "🟢" : "🔴"}
{ updateNodeData(props.id, { ...data, plan, }); }} />
; } /** * 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 (['basic_belief', 'inferred_belief'].includes(otherNode.type!)) { 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;