feat: create dialog for plan creation in triggers, make sure to bind the correct things in triggers. Change the norms to take one condition, rather than a list. yes, tests are probably still broken.

ref: N25B-412
This commit is contained in:
Björn Otgaar
2025-12-16 18:21:19 +01:00
parent 0b29cb5858
commit c1ef924be1
5 changed files with 377 additions and 37 deletions

View File

@@ -9,9 +9,11 @@ import {
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import useFlowStore from '../VisProgStores';
import { useState } from 'react';
import { useState, useRef } from 'react';
import { RealtimeTextField, TextField } from '../../../../components/TextField';
import duplicateIndices from '../../../../utils/duplicateIndices';
import type { Action, ActionTypes, Plan } from '../components/Plan';
import { defaultPlan } from '../components/Plan.default';
/**
* The default data structure for a Trigger node
@@ -28,8 +30,8 @@ import duplicateIndices from '../../../../utils/duplicateIndices';
export type TriggerNodeData = {
label: string;
droppable: boolean;
triggerType: "keywords" | string;
triggers: Keyword[] | never;
condition?: string; // id of the belief
plan?: Plan;
hasReduce: boolean;
};
@@ -55,25 +57,265 @@ export function TriggerNodeCanConnect(connection: Connection | Edge): boolean {
export default function TriggerNode(props: NodeProps<TriggerNode>) {
const data = props.data;
const {updateNodeData} = useFlowStore();
const dialogRef = useRef<HTMLDialogElement | null>(null);
const [draftPlan, setDraftPlan] = useState<Plan | null>(null);
// Helpers for inside plan creation
const [newActionType, setNewActionType] = useState<ActionTypes>("speech");
const [newActionName, setNewActionName] = useState("");
const [newActionValue, setNewActionValue] = useState("");
// Create a new Plan
const openCreateDialog = () => {
setDraftPlan(JSON.parse(JSON.stringify(defaultPlan)));
dialogRef.current?.showModal();
};
// Edit our current plan
const openEditDialog = () => {
if (!data.plan) return;
setDraftPlan(JSON.parse(JSON.stringify(data.plan)));
dialogRef.current?.showModal();
};
// Close the creating/editing of our plan
const closeDialog = () => {
dialogRef.current?.close();
};
// Define the function for creating actions
const buildAction = (): Action => {
const id = crypto.randomUUID();
switch (newActionType) {
case "speech":
return {
id,
name: newActionName,
text: newActionValue,
type: "speech"
};
case "gesture":
return {
id,
name: newActionName,
gesture: newActionValue,
type: "gesture"
};
case "llm":
return {
id,
name: newActionName,
goal: newActionValue,
type: "llm"
};
}
};
const setKeywords = (keywords: Keyword[]) => {
updateNodeData(props.id, {...data, triggers: keywords});
}
return <>
<Toolbar nodeId={props.id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
{data.triggerType === "emotion" && (
<div className={"flex-row gap-md"}>Emotion?</div>
)}
{data.triggerType === "keywords" && (
<Keywords
keywords={data.triggers}
setKeywords={setKeywords}
/>
)}
<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>
<Handle type="source" position={Position.Right} id="TriggerSource"/>
<Handle type="target" position={Position.Bottom} id="ConditionTarget"/>
{/* We don't have a plan yet, show our create plan button */}
{!data.plan && (
<button
className={styles.nodeButton}
onClick={openCreateDialog}
>
Create Plan
</button>
)}
{/* We have a plan, show our edit plan button */}
{data.plan && (
<button
className={styles.nodeButton}
onClick={openEditDialog}
>
Edit Plan
</button>
)}
</div>
{/* Define how our dialog should work */}
<dialog ref={dialogRef}
className={styles.planDialog}
onCancel={(e) => e.preventDefault()}
>
<form method="dialog" className="flex-col gap-md">
<h3>{draftPlan?.id === data.plan?.id ? "Edit Plan" : "Create Plan"}</h3>
{/*Text field to edit the name of our draft plan*/}
<div className={styles.planEditor}>
{/* LEFT: plan info + add action */}
<div className={styles.planEditorLeft}>
{/* Plan name */}
{draftPlan && (
<div className="flex-col gap-sm">
<label htmlFor="plan-name">Plan Name</label>
<TextField
id="plan-name"
value={draftPlan.name}
setValue={(name) =>
setDraftPlan({
...draftPlan,
name,
})
}
/>
</div>
)}
{/* Add action UI */}
{draftPlan && (
<div className="flex-col gap-sm">
<h4>Add Action</h4>
<label>
Action Type <wbr></wbr> {/* Just moves the selector over a bit :) */}
<select
value={newActionType}
onChange={(e) =>
setNewActionType(e.target.value as ActionTypes)
}
>
<option value="speech">Speech Action</option>
<option value="gesture">Gesture Action</option>
<option value="llm">LLM Action</option>
</select>
</label>
<TextField
value={newActionName}
setValue={setNewActionName}
placeholder="Action name"
/>
<TextField
value={newActionValue}
setValue={setNewActionValue}
placeholder={
newActionType === "speech"
? "Speech text"
: newActionType === "gesture"
? "Gesture name"
: "LLM goal"
}
/>
<button
type="button"
disabled={!newActionName || !newActionValue}
onClick={() => {
if (!draftPlan) return;
const action = buildAction();
setDraftPlan({
...draftPlan,
steps: [...draftPlan.steps, action],
});
setNewActionName("");
setNewActionValue("");
setNewActionType("speech");
}}
>
Add Step
</button>
</div>
)}
</div>
{/* RIGHT: steps list */}
<div className={styles.planEditorRight}>
<h4>Steps</h4>
{draftPlan && draftPlan.steps.length === 0 && (
<div className={styles.emptySteps}>
No steps yet
</div>
)}
{draftPlan?.steps.map((step, index) => (
<div
key={step.id}
className={styles.planStep}
onClick={() => {
if (!draftPlan) return;
setDraftPlan({
...draftPlan,
steps: draftPlan.steps.filter((s) => s.id !== step.id),
});
}}
>
<span className={styles.stepIndex}>{index + 1}.</span>
<span className={styles.stepName}>{step.name}</span>
<span className={styles.stepType}>{step.type}</span>
</div>
))}
</div>
</div>
<div className="flex-row gap-md">
{/*Button to close the plan editor.*/}
<button
type="button"
onClick={() => {
setDraftPlan(null);
closeDialog();
}}
>
Cancel
</button>
{/*Button to save the draftPlan to the plan in the Node.*/}
<button
type="button"
disabled={!draftPlan}
onClick={() => {
if (!draftPlan) return;
updateNodeData(props.id, {
...data,
plan: draftPlan,
});
setDraftPlan(null);
closeDialog();
}}
>
{draftPlan?.id === data.plan?.id ? "Confirm" : "Create"}
</button>
{/*Button to reset the plan*/}
<button
type="button"
disabled={!draftPlan}
onClick={() => {
if (!draftPlan) return;
updateNodeData(props.id, {
...data,
plan: undefined,
});
setNewActionName("")
setNewActionType("speech")
setNewActionValue("")
setDraftPlan(defaultPlan)
}}
>
Reset
</button>
</div>
</form>
</dialog>
</>;
}
@@ -108,6 +350,11 @@ export function TriggerReduce(node: Node, _nodes: Node[]) {
*/
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.
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
data.condition = _sourceNodeId;
}
}
/**
@@ -126,6 +373,9 @@ export function TriggerConnectionSource(_thisNode: Node, _targetNodeId: string)
*/
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
}
/**