# Conflicts: # src/pages/VisProgPage/VisProg.tsx # src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx # src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx # src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.tsx # src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx
275 lines
9.9 KiB
TypeScript
275 lines
9.9 KiB
TypeScript
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<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, 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 <>
|
|
|
|
<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"}]),
|
|
]} title="Connect to any number of phaseNodes"/>
|
|
<SingleConnectionHandle
|
|
type="target"
|
|
position={Position.Bottom}
|
|
id="TriggerBeliefs"
|
|
style={{ left: '40%' }}
|
|
rules={[
|
|
allowOnlyConnectionsFromType(["basic_belief","inferred_belief"]),
|
|
]}
|
|
title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"
|
|
/>
|
|
|
|
<MultiConnectionHandle
|
|
type="target"
|
|
position={Position.Bottom}
|
|
id="GoalTarget"
|
|
style={{ left: '60%' }}
|
|
rules={[
|
|
allowOnlyConnectionsFromType(['goal']),
|
|
]}
|
|
title="Connect to any number of goalNodes"
|
|
/>
|
|
|
|
<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 (['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; |