feat: initial commit - adding a plan to phases and different ui for phase order editing
ref: N25B-451
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ export const PhaseNodeDefaults: PhaseNodeData = {
|
|||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
nextPhaseId: null,
|
nextPhaseId: null,
|
||||||
isFirstPhase: false,
|
isFirstPhase: false,
|
||||||
|
plan: undefined,
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user