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:
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user