Compare commits

...

1 Commits

Author SHA1 Message Date
Björn Otgaar
47e7207c32 feat: initial commit - adding a plan to phases and different ui for phase order editing
ref: N25B-451
2026-01-14 19:44:53 +01:00
4 changed files with 80 additions and 15 deletions

View File

@@ -6,7 +6,6 @@
overscroll-behavior: contain; overscroll-behavior: contain;
} }
.planDialog::backdrop { .planDialog::backdrop {
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.4);
} }
@@ -68,4 +67,17 @@
font-style: italic; font-style: italic;
} }
.dragHandle {
margin-left: auto;
cursor: grab;
opacity: 0.5;
user-select: none;
}
.dragHandle:active {
cursor: grabbing;
}
.planStepDragging {
opacity: 0.4;
}

View File

@@ -6,16 +6,26 @@ import { defaultPlan } from "../components/Plan.default";
import { TextField } from "../../../../components/TextField"; import { TextField } from "../../../../components/TextField";
import GestureValueEditor from "./GestureValueEditor"; import GestureValueEditor from "./GestureValueEditor";
/**
* The properties of a plan editor.
* @property plan: The optional plan loaded into this editor.
* @property onSave: The function that will be called upon save.
* @property description: Optional description which is already set.
* @property onlyEditPhasing: Optional boolean to toggle
* whether or not this editor is part of the phase editing.
*/
type PlanEditorDialogProps = { type PlanEditorDialogProps = {
plan?: Plan; plan?: Plan;
onSave: (plan: Plan | undefined) => void; onSave: (plan: Plan | undefined) => void;
description? : string; description? : string;
onlyEditPhasing? : boolean;
}; };
export default function PlanEditorDialog({ export default function PlanEditorDialog({
plan, plan,
onSave, onSave,
description, description,
onlyEditPhasing = false,
}: PlanEditorDialogProps) { }: PlanEditorDialogProps) {
// UseStates and references // UseStates and references
const dialogRef = useRef<HTMLDialogElement | null>(null); const dialogRef = useRef<HTMLDialogElement | null>(null);
@@ -24,10 +34,11 @@ export default function PlanEditorDialog({
const [newActionGestureType, setNewActionGestureType] = useState<boolean>(true); const [newActionGestureType, setNewActionGestureType] = useState<boolean>(true);
const [newActionValue, setNewActionValue] = useState(""); const [newActionValue, setNewActionValue] = useState("");
const [hasInteractedWithPlan, setHasInteractedWithPlan] = useState<boolean>(false) const [hasInteractedWithPlan, setHasInteractedWithPlan] = useState<boolean>(false)
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const { setScrollable } = useFlowStore(); const { setScrollable } = useFlowStore();
const nodes = useFlowStore().nodes; const nodes = useFlowStore().nodes;
//Button Actions // Button Actions
const openCreate = () => { const openCreate = () => {
setScrollable(false); setScrollable(false);
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()}); setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()});
@@ -89,9 +100,9 @@ export default function PlanEditorDialog({
data-testid={"PlanEditorDialogTestID"} data-testid={"PlanEditorDialogTestID"}
> >
<form method="dialog" className="flex-col gap-md"> <form method="dialog" className="flex-col gap-md">
<h3> {draftPlan?.id === plan?.id ? "Edit Plan" : "Create Plan"} </h3> <h3> {onlyEditPhasing ? "Editing Phase Ordering" : (draftPlan?.id === plan?.id ? "Edit Plan" : "Create Plan")} </h3>
{/* Plan name text field */} {/* Plan name text field */}
{draftPlan && ( {(draftPlan && !onlyEditPhasing) && (
<TextField <TextField
value={draftPlan.name} value={draftPlan.name}
setValue={(name) => setValue={(name) =>
@@ -104,12 +115,14 @@ export default function PlanEditorDialog({
{draftPlan && (<div className={styles.planEditor}> {draftPlan && (<div className={styles.planEditor}>
<div className={styles.planEditorLeft}> <div className={styles.planEditorLeft}>
{/* Left Side (Action Adder) */} {/* Left Side (Action Adder) */}
<h4>Add Action</h4> <h4>{onlyEditPhasing ? "You can't add any actions, only rearrange the steps." : "Add Action"}</h4>
{(!plan && description && draftPlan.steps.length === 0 && !hasInteractedWithPlan) && (<div className={styles.stepSuggestion}> {(!plan && description && draftPlan.steps.length === 0 && !hasInteractedWithPlan) && (<div className={styles.stepSuggestion}>
<label> Filled in as a suggestion! </label> <label> Filled in as a suggestion! </label>
<label> Feel free to change! </label> <label> Feel free to change! </label>
</div>)} </div>)}
<label>
{(!onlyEditPhasing) && (<label>
Action Type <wbr /> Action Type <wbr />
{/* Type selection */} {/* Type selection */}
<select <select
@@ -123,10 +136,10 @@ export default function PlanEditorDialog({
<option value="gesture">Gesture Action</option> <option value="gesture">Gesture Action</option>
<option value="llm">LLM Action</option> <option value="llm">LLM Action</option>
</select> </select>
</label> </label>)}
{/* Action value editor*/} {/* Action value editor*/}
{newActionType === "gesture" ? ( {!onlyEditPhasing && newActionType === "gesture" ? (
// Gesture get their own editor component // Gesture get their own editor component
<GestureValueEditor <GestureValueEditor
value={newActionValue} value={newActionValue}
@@ -134,17 +147,20 @@ export default function PlanEditorDialog({
setType={setNewActionGestureType} setType={setNewActionGestureType}
placeholder="Gesture name" placeholder="Gesture name"
/> />
) : ( )
<TextField :
// Only show the text field if we're not just rearranging.
(!onlyEditPhasing &&
(<TextField
value={newActionValue} value={newActionValue}
setValue={setNewActionValue} setValue={setNewActionValue}
placeholder={ placeholder={
newActionType === "speech" ? "Speech text" newActionType === "speech" ? "Speech text"
: "LLM goal" : "LLM goal"
} }
/> />)
)} )}
{/* Adding steps */} {/* Adding steps */}
<button <button
type="button" type="button"

View File

@@ -10,4 +10,5 @@ export const PhaseNodeDefaults: PhaseNodeData = {
hasReduce: true, hasReduce: true,
nextPhaseId: null, nextPhaseId: null,
isFirstPhase: false, isFirstPhase: false,
plan: undefined,
}; };

View File

@@ -10,6 +10,11 @@ import {allowOnlyConnectionsFromType, noSelfConnections} from "../HandleRules.ts
import { NodeReduces, NodesInPhase, NodeTypes} from '../NodeRegistry'; import { NodeReduces, NodesInPhase, NodeTypes} from '../NodeRegistry';
import useFlowStore from '../VisProgStores'; import useFlowStore from '../VisProgStores';
import { TextField } from '../../../../components/TextField'; import { TextField } from '../../../../components/TextField';
import PlanEditorDialog from '../components/PlanEditor.tsx';
import type { Plan } from '../components/Plan.tsx';
import { insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
import { defaultPlan } from '../components/Plan.default.ts';
import type { GoalNode } from './GoalNode.tsx';
/** /**
* The default data dot a phase node * The default data dot a phase node
@@ -26,6 +31,7 @@ export type PhaseNodeData = {
hasReduce: boolean; hasReduce: boolean;
nextPhaseId: string | "end" | null; nextPhaseId: string | "end" | null;
isFirstPhase: boolean; isFirstPhase: boolean;
plan?: Plan;
}; };
export type PhaseNode = Node<PhaseNodeData> export type PhaseNode = Node<PhaseNodeData>
@@ -54,6 +60,24 @@ export default function PhaseNode(props: NodeProps<PhaseNode>) {
placeholder={"Phase ..."} placeholder={"Phase ..."}
/> />
</div> </div>
{(data.plan && data.plan.steps.length > 0) && (<div>
<PlanEditorDialog
plan={data.plan}
onSave={(plan) => {
updateNodeData(props.id, {
...data,
plan,
});
}}
description={props.data.label}
onlyEditPhasing={true}
/>
</div>)}
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[ <SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
noSelfConnections, noSelfConnections,
allowOnlyConnectionsFromType(["phase", "start"]), allowOnlyConnectionsFromType(["phase", "start"]),
@@ -129,9 +153,9 @@ export const PhaseTooltip = `
*/ */
export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) { export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
const data = _thisNode.data as PhaseNodeData const data = _thisNode.data as PhaseNodeData
const nodes = useFlowStore.getState().nodes; const nodes = useFlowStore.getState().nodes;
const sourceNode = nodes.find((node) => node.id === _sourceNodeId)! const sourceNode = nodes.find((node) => node.id === _sourceNodeId)!
switch (sourceNode.type) { switch (sourceNode.type) {
case "phase": break; case "phase": break;
case "start": data.isFirstPhase = true; break; case "start": data.isFirstPhase = true; break;
@@ -139,6 +163,18 @@ export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
// endNodes cannot be the source of an outgoing connection // endNodes cannot be the source of an outgoing connection
// so we don't need to cover them with a special case // so we don't need to cover them with a special case
// before handling the default behavior // before handling the default behavior
case "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, sourceNode as GoalNode)
}
// Else, lets just insert this goal into our current plan.
else {
data.plan = insertGoalInPlan(structuredClone(data.plan), sourceNode as GoalNode)
}
break;
}
default: data.children.push(_sourceNodeId); break; default: data.children.push(_sourceNodeId); break;
} }
} }