diff --git a/src/pages/VisProgPage/VisProg.module.css b/src/pages/VisProgPage/VisProg.module.css index 14619c5..cd61b5e 100644 --- a/src/pages/VisProgPage/VisProg.module.css +++ b/src/pages/VisProgPage/VisProg.module.css @@ -138,4 +138,68 @@ border-radius: 5pt; outline: plum solid 2pt; filter: drop-shadow(0 0 0.25rem plum); +} + +.planDialog { + width: 80vw; + max-width: 900px; + padding: 1rem; + border: none; + border-radius: 8px; +} + +.planDialog::backdrop { + background: rgba(0, 0, 0, 0.4); +} + +.planEditor { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + min-width: 600px; +} + +.planEditorLeft { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.planEditorRight { + display: flex; + flex-direction: column; + gap: 0.5rem; + border-left: 1px solid var(--border-color, #ccc); + padding-left: 1rem; + max-height: 300px; + overflow-y: auto; +} + +.planStep { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + transition: text-decoration 0.2s; +} + +.planStep:hover { + text-decoration: line-through; +} + +.stepType { + margin-left: auto; + opacity: 0.7; + font-size: 0.85em; +} + +.stepIndex { + opacity: 0.6; +} + + + +.emptySteps { + opacity: 0.5; + font-style: italic; } \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/Plan.default.ts b/src/pages/VisProgPage/visualProgrammingUI/components/Plan.default.ts new file mode 100644 index 0000000..b2ea31b --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/components/Plan.default.ts @@ -0,0 +1,7 @@ +import type { Plan } from "./Plan"; + +export const defaultPlan: Plan = { + name: "Default Plan", + id: "-1", + steps: [], +} \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/Plan.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/Plan.tsx new file mode 100644 index 0000000..a74cbf2 --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/components/Plan.tsx @@ -0,0 +1,25 @@ +export type Plan = { + name: string, + id: string, + steps: PlanElement[], +} + +export type PlanElement = Goal | Action + +export type Goal = { + id: string, + name: string, + plan: Plan, + can_fail: boolean, + type: "goal" +} + +// Actions +export type Action = SpeechAction | GestureAction | LLMAction +export type SpeechAction = { name: string, id: string, text: string, type:"speech" } +export type GestureAction = { name: string, id: string, gesture: string, type:"gesture" } +export type LLMAction = { name: string, id: string, goal: string, type:"llm" } + +export type ActionTypes = "speech" | "gesture" | "llm"; + + diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.tsx index 4e94834..ef65215 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.tsx @@ -20,7 +20,7 @@ import { BasicBeliefReduce } from './BasicBeliefNode'; export type NormNodeData = { label: string; droppable: boolean; - conditions: string[]; // List of (basic) belief nodes' ids. + condition?: string; // id of this node's belief. norm: string; hasReduce: boolean; critical: boolean; @@ -70,13 +70,12 @@ export default function NormNode(props: NodeProps) { /> - {data.conditions.length > 0 && (
- + {data.condition && (
+
)} - - - + +
; }; @@ -90,11 +89,6 @@ export default function NormNode(props: NodeProps) { export function NormReduce(node: Node, nodes: Node[]) { const data = node.data as NormNodeData; - // conditions nodes - make sure to check for empty arrays - let conditionNodes: Node[] = []; - if (data.conditions) - conditionNodes = nodes.filter((node) => data.conditions.includes(node.id)); - // Build the result object const result: Record = { id: node.id, @@ -103,12 +97,14 @@ export function NormReduce(node: Node, nodes: Node[]) { critical: data.critical, }; - // Go over our conditionNodes. They should either be Basic (OR TODO: Inferred) - const reducer = BasicBeliefReduce; - result["basic_beliefs"] = conditionNodes.map((condition) => reducer(condition, nodes)) + if (data.condition) { + const reducer = BasicBeliefReduce; // TODO: also add inferred. + const conditionNode = nodes.find((node) => node.id === data.condition); + // In case something went wrong, and our condition doesn't actually exist; + if (conditionNode == undefined) return result; + result["belief"] = reducer(conditionNode, nodes) + } - // When the Inferred is being implemented, you should follow the same kind of structure that PhaseNode has, - // dividing the conditions into basic and inferred, then calling the correct reducer on them. return result } @@ -119,9 +115,9 @@ export function NormReduce(node: Node, nodes: Node[]) { */ export function NormConnectionTarget(_thisNode: Node, _sourceNodeId: string) { const data = _thisNode.data as NormNodeData; - // If we got a belief connected, this is a condition for the norm. + // 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.conditions.push(_sourceNodeId); + data.condition = _sourceNodeId; } } @@ -141,10 +137,8 @@ export function NormConnectionSource(_thisNode: Node, _targetNodeId: string) { */ export function NormDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) { const data = _thisNode.data as NormNodeData; - // If we got a belief connected, this is a 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.conditions = data.conditions.filter(id => id != _sourceNodeId); - } + // remove if the target of disconnection was our condition + if (_sourceNodeId == data.condition) data.condition = undefined } /** diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx index cad7015..6e3d940 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx @@ -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) { const data = props.data; const {updateNodeData} = useFlowStore(); + const dialogRef = useRef(null); + const [draftPlan, setDraftPlan] = useState(null); + + // Helpers for inside plan creation + const [newActionType, setNewActionType] = useState("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 <>
- {data.triggerType === "emotion" && ( -
Emotion?
- )} - {data.triggerType === "keywords" && ( - - )} +
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 ? "🟢" : "🔴"}
+ + + {/* We don't have a plan yet, show our create plan button */} + {!data.plan && ( + + )} + + {/* We have a plan, show our edit plan button */} + {data.plan && ( + + )}
+ + {/* Define how our dialog should work */} + e.preventDefault()} + > +
+

{draftPlan?.id === data.plan?.id ? "Edit Plan" : "Create Plan"}

+ + {/*Text field to edit the name of our draft plan*/} +
+ {/* LEFT: plan info + add action */} +
+ {/* Plan name */} + {draftPlan && ( +
+ + + setDraftPlan({ + ...draftPlan, + name, + }) + } + /> +
+ )} + + {/* Add action UI */} + {draftPlan && ( +
+

Add Action

+ + + + + + + + +
+ )} +
+ + {/* RIGHT: steps list */} +
+

Steps

+ + {draftPlan && draftPlan.steps.length === 0 && ( +
+ No steps yet +
+ )} + + {draftPlan?.steps.map((step, index) => ( +
{ + if (!draftPlan) return; + setDraftPlan({ + ...draftPlan, + steps: draftPlan.steps.filter((s) => s.id !== step.id), + }); + }} + > + {index + 1}. + {step.name} + {step.type} +
+ ))} +
+
+ +
+ {/*Button to close the plan editor.*/} + + + {/*Button to save the draftPlan to the plan in the Node.*/} + + + {/*Button to reset the plan*/} + +
+
+
; } @@ -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 } /**