Compare commits
4 Commits
dev
...
chore/clea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26894cc2d2 | ||
|
|
dcf4f01e68 | ||
|
|
1876138fe2 | ||
|
|
5930e24bf4 |
@@ -1,253 +0,0 @@
|
|||||||
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
|
||||||
// University within the Software Project course.
|
|
||||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
|
||||||
import {useRef, useState} from "react";
|
|
||||||
import useFlowStore from "../VisProgStores.tsx";
|
|
||||||
import styles from './PlanEditor.module.css';
|
|
||||||
import { GetActionValue, type Action, type ActionTypes, type Plan } from "../components/Plan";
|
|
||||||
import { defaultPlan } from "../components/Plan.default";
|
|
||||||
import { TextField } from "../../../../components/TextField";
|
|
||||||
import GestureValueEditor from "./GestureValueEditor";
|
|
||||||
|
|
||||||
type PlanEditorDialogProps = {
|
|
||||||
plan?: Plan;
|
|
||||||
onSave: (plan: Plan | undefined) => void;
|
|
||||||
description? : string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PlanEditorDialog({
|
|
||||||
plan,
|
|
||||||
onSave,
|
|
||||||
description,
|
|
||||||
}: PlanEditorDialogProps) {
|
|
||||||
// UseStates and references
|
|
||||||
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
|
||||||
const [draftPlan, setDraftPlan] = useState<Plan | null>(null);
|
|
||||||
const [newActionType, setNewActionType] = useState<ActionTypes>("speech");
|
|
||||||
const [newActionGestureType, setNewActionGestureType] = useState<boolean>(true);
|
|
||||||
const [newActionValue, setNewActionValue] = useState("");
|
|
||||||
const [hasInteractedWithPlan, setHasInteractedWithPlan] = useState<boolean>(false)
|
|
||||||
const { setScrollable } = useFlowStore();
|
|
||||||
const nodes = useFlowStore().nodes;
|
|
||||||
|
|
||||||
//Button Actions
|
|
||||||
const openCreate = () => {
|
|
||||||
setScrollable(false);
|
|
||||||
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()});
|
|
||||||
dialogRef.current?.showModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openCreateWithDescription = () => {
|
|
||||||
setScrollable(false);
|
|
||||||
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID(), name: description!});
|
|
||||||
setNewActionType("llm")
|
|
||||||
setNewActionValue(description!)
|
|
||||||
dialogRef.current?.showModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
const openEdit = () => {
|
|
||||||
setScrollable(false);
|
|
||||||
if (!plan) return;
|
|
||||||
setDraftPlan(structuredClone(plan));
|
|
||||||
dialogRef.current?.showModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
setScrollable(true);
|
|
||||||
dialogRef.current?.close();
|
|
||||||
setDraftPlan(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildAction = (): Action => {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
setHasInteractedWithPlan(true)
|
|
||||||
switch (newActionType) {
|
|
||||||
case "speech":
|
|
||||||
return { id, text: newActionValue, type: "speech" };
|
|
||||||
case "gesture":
|
|
||||||
return { id, gesture: newActionValue, isTag: newActionGestureType, type: "gesture" };
|
|
||||||
case "llm":
|
|
||||||
return { id, goal: newActionValue, type: "llm" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
{/* Create and edit buttons */}
|
|
||||||
{!plan && (
|
|
||||||
<button className={styles.nodeButton} onClick={description ? openCreateWithDescription : openCreate}>
|
|
||||||
Create Plan
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{plan && (
|
|
||||||
<button className={styles.nodeButton} onClick={openEdit}>
|
|
||||||
Edit Plan
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Start of dialog (plan editor) */}
|
|
||||||
<dialog
|
|
||||||
ref={dialogRef}
|
|
||||||
className={`${styles.planDialog}`}
|
|
||||||
//onWheel={(e) => e.stopPropagation()}
|
|
||||||
data-testid={"PlanEditorDialogTestID"}
|
|
||||||
>
|
|
||||||
<form method="dialog" className="flex-col gap-md">
|
|
||||||
<h3> {draftPlan?.id === plan?.id ? "Edit Plan" : "Create Plan"} </h3>
|
|
||||||
{/* Plan name text field */}
|
|
||||||
{draftPlan && (
|
|
||||||
<TextField
|
|
||||||
value={draftPlan.name}
|
|
||||||
setValue={(name) =>
|
|
||||||
setDraftPlan({ ...draftPlan, name })}
|
|
||||||
placeholder="Plan name"
|
|
||||||
data-testid="name_text_field"/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Entire "bottom" part (adder and steps) without cancel, confirm and reset */}
|
|
||||||
{draftPlan && (<div className={styles.planEditor}>
|
|
||||||
<div className={styles.planEditorLeft}>
|
|
||||||
{/* Left Side (Action Adder) */}
|
|
||||||
<h4>Add Action</h4>
|
|
||||||
{(!plan && description && draftPlan.steps.length === 0 && !hasInteractedWithPlan) && (<div className={styles.stepSuggestion}>
|
|
||||||
<label> Filled in as a suggestion! </label>
|
|
||||||
<label> Feel free to change! </label>
|
|
||||||
</div>)}
|
|
||||||
<label>
|
|
||||||
Action Type <wbr />
|
|
||||||
{/* Type selection */}
|
|
||||||
<select
|
|
||||||
value={newActionType}
|
|
||||||
onChange={(e) => {
|
|
||||||
setNewActionType(e.target.value as ActionTypes);
|
|
||||||
// Reset value when action type changes
|
|
||||||
setNewActionValue("");
|
|
||||||
}}>
|
|
||||||
<option value="speech">Speech Action</option>
|
|
||||||
<option value="gesture">Gesture Action</option>
|
|
||||||
<option value="llm">LLM Action</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* Action value editor*/}
|
|
||||||
{newActionType === "gesture" ? (
|
|
||||||
// Gesture get their own editor component
|
|
||||||
<GestureValueEditor
|
|
||||||
value={newActionValue}
|
|
||||||
setValue={setNewActionValue}
|
|
||||||
setType={setNewActionGestureType}
|
|
||||||
placeholder="Gesture name"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TextField
|
|
||||||
value={newActionValue}
|
|
||||||
setValue={setNewActionValue}
|
|
||||||
placeholder={
|
|
||||||
newActionType === "speech" ? "Speech text"
|
|
||||||
: "LLM goal"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Adding steps */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!newActionValue}
|
|
||||||
onClick={() => {
|
|
||||||
if (!draftPlan) return;
|
|
||||||
// Add action to steps
|
|
||||||
const action = buildAction();
|
|
||||||
setDraftPlan({
|
|
||||||
...draftPlan,
|
|
||||||
steps: [...draftPlan.steps, action],});
|
|
||||||
|
|
||||||
// Reset current action building
|
|
||||||
setNewActionValue("");
|
|
||||||
setNewActionType("speech");
|
|
||||||
}}>
|
|
||||||
Add Step
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Side (Steps shown) */}
|
|
||||||
<div className={styles.planEditorRight}>
|
|
||||||
<h4>Steps</h4>
|
|
||||||
|
|
||||||
{/* Show if there are no steps yet */}
|
|
||||||
{draftPlan.steps.length === 0 && (
|
|
||||||
<div className={styles.emptySteps}>
|
|
||||||
No steps yet
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
{/* Map over all steps */}
|
|
||||||
{draftPlan.steps.map((step, index) => (
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
key={step.id}
|
|
||||||
className={styles.planStep}
|
|
||||||
// Extra logic for screen readers to access using keyboard
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
setDraftPlan({
|
|
||||||
...draftPlan,
|
|
||||||
steps: draftPlan.steps.filter((s) => s.id !== step.id),});
|
|
||||||
}}}
|
|
||||||
onClick={() => {
|
|
||||||
setDraftPlan({
|
|
||||||
...draftPlan,
|
|
||||||
steps: draftPlan.steps.filter((s) => s.id !== step.id),});
|
|
||||||
}}>
|
|
||||||
|
|
||||||
<span className={styles.stepIndex}>{index + 1}.</span>
|
|
||||||
<span className={styles.stepType}>{step.type}:</span>
|
|
||||||
<span className={styles.stepName}>
|
|
||||||
{
|
|
||||||
// This just tries to find the goals name, i know it looks ugly:(
|
|
||||||
step.type === "goal"
|
|
||||||
? ((nodes.find(x => x.id === step.id)?.data.name as string) == "" ?
|
|
||||||
"unnamed goal": (nodes.find(x => x.id === step.id)?.data.name as string))
|
|
||||||
: (GetActionValue(step) ?? "")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Buttons */}
|
|
||||||
<div className="flex-row gap-md">
|
|
||||||
{/* Close button */}
|
|
||||||
<button type="button" onClick={close}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Confirm/ Create button */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!draftPlan}
|
|
||||||
onClick={() => {
|
|
||||||
if (!draftPlan) return;
|
|
||||||
onSave(draftPlan);
|
|
||||||
close();
|
|
||||||
}}>
|
|
||||||
{draftPlan?.id === plan?.id ? "Confirm" : "Create"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Reset button */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!draftPlan}
|
|
||||||
onClick={() => {
|
|
||||||
onSave(undefined);
|
|
||||||
close();
|
|
||||||
}}>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { TextField } from "../../../../../components/TextField";
|
||||||
|
import GestureValueEditor from "./GestureValueEditor";
|
||||||
|
import type { ActionTypes } from "./Plan";
|
||||||
|
import styles from './PlanEditor.module.css';
|
||||||
|
|
||||||
|
type ActionAdderProps = {
|
||||||
|
newActionType: ActionTypes;
|
||||||
|
setNewActionType: (t: ActionTypes) => void;
|
||||||
|
newActionValue: string;
|
||||||
|
setNewActionValue: (v: string) => void;
|
||||||
|
setNewActionGestureType: (b: boolean) => void;
|
||||||
|
onAdd: () => void;
|
||||||
|
showSuggestion: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export function ActionAdder({
|
||||||
|
newActionType,
|
||||||
|
setNewActionType,
|
||||||
|
newActionValue,
|
||||||
|
setNewActionValue,
|
||||||
|
setNewActionGestureType,
|
||||||
|
onAdd,
|
||||||
|
showSuggestion,
|
||||||
|
}: ActionAdderProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.planEditorLeft}>
|
||||||
|
<h4>Add Action</h4>
|
||||||
|
|
||||||
|
{showSuggestion && (
|
||||||
|
<div className={styles.stepSuggestion}>
|
||||||
|
<label>Filled in as a suggestion!</label>
|
||||||
|
<label>Feel free to change!</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Action Type <wbr />
|
||||||
|
<select
|
||||||
|
value={newActionType}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNewActionType(e.target.value as ActionTypes);
|
||||||
|
setNewActionValue("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="speech">Speech Action</option>
|
||||||
|
<option value="gesture">Gesture Action</option>
|
||||||
|
<option value="llm">LLM Action</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{newActionType === "gesture" ? (
|
||||||
|
<GestureValueEditor
|
||||||
|
value={newActionValue}
|
||||||
|
setValue={setNewActionValue}
|
||||||
|
setType={setNewActionGestureType}
|
||||||
|
placeholder="Gesture name"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
value={newActionValue}
|
||||||
|
setValue={setNewActionValue}
|
||||||
|
placeholder={newActionType === "speech" ? "Speech text" : "LLM goal"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button type="button" disabled={!newActionValue} onClick={onAdd}>
|
||||||
|
Add Step
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// University within the Software Project course.
|
// University within the Software Project course.
|
||||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||||
import { type Node } from "@xyflow/react"
|
import { type Node } from "@xyflow/react"
|
||||||
import { GoalReduce } from "../nodes/GoalNode"
|
import { GoalReduce } from "../../nodes/GoalNode"
|
||||||
|
|
||||||
|
|
||||||
export type Plan = {
|
export type Plan = {
|
||||||
@@ -124,4 +124,6 @@ export function GetActionValue(action: Action) {
|
|||||||
return returnAction.goal;
|
return returnAction.goal;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// University within the Software Project course.
|
// University within the Software Project course.
|
||||||
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||||
// This file is to avoid sharing both functions and components which eslint dislikes. :)
|
// This file is to avoid sharing both functions and components which eslint dislikes. :)
|
||||||
import type { GoalNode } from "../nodes/GoalNode"
|
import type { GoalNode } from "../../nodes/GoalNode"
|
||||||
import type { Goal, Plan } from "./Plan"
|
import type { Goal, Plan } from "./Plan"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,4 +35,5 @@ export function deleteGoalInPlanByID(plan: Plan, goalID: string) {
|
|||||||
steps: plan.steps.filter((x) => x.id !== goalID)
|
steps: plan.steps.filter((x) => x.id !== goalID)
|
||||||
}
|
}
|
||||||
return updatedPlan.steps.length == 0 ? undefined : updatedPlan
|
return updatedPlan.steps.length == 0 ? undefined : updatedPlan
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
// This program has been developed by students from the bachelor Computer Science at Utrecht
|
||||||
|
// University within the Software Project course.
|
||||||
|
// © Copyright Utrecht University (Department of Information and Computing Sciences)
|
||||||
|
import {useRef, useState} from "react";
|
||||||
|
import useFlowStore from "../../VisProgStores.tsx";
|
||||||
|
import styles from './PlanEditor.module.css';
|
||||||
|
import { type Action, type ActionTypes, type Plan } from "./Plan.tsx";
|
||||||
|
import { defaultPlan } from "./Plan.default.ts";
|
||||||
|
import { TextField } from "../../../../../components/TextField.tsx";
|
||||||
|
import { StepsList } from "./StepList.tsx";
|
||||||
|
import { ActionAdder } from "./ActionAdder.tsx";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type PlanEditorDialogProps = {
|
||||||
|
plan?: Plan;
|
||||||
|
onSave: (plan: Plan | undefined) => void;
|
||||||
|
description? : string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an action, as a step for a plan.
|
||||||
|
* @param type the type of action to build
|
||||||
|
* @param value the value of this action to build
|
||||||
|
* @param isGestureTag whether or not this action, restricted to gestures, is a tag.
|
||||||
|
* @returns An action
|
||||||
|
*/
|
||||||
|
function buildAction(
|
||||||
|
type: ActionTypes,
|
||||||
|
value: string,
|
||||||
|
isGestureTag: boolean
|
||||||
|
): Action {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "speech":
|
||||||
|
return { id, text: value, type: "speech" };
|
||||||
|
case "gesture":
|
||||||
|
return { id, gesture: value, isTag: isGestureTag, type: "gesture" };
|
||||||
|
case "llm":
|
||||||
|
return { id, goal: value, type: "llm" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function PlanEditorDialog({
|
||||||
|
plan,
|
||||||
|
onSave,
|
||||||
|
description,
|
||||||
|
}: PlanEditorDialogProps) {
|
||||||
|
// UseStates and references
|
||||||
|
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
||||||
|
const [draftPlan, setDraftPlan] = useState<Plan | null>(null);
|
||||||
|
const [newActionType, setNewActionType] = useState<ActionTypes>("speech");
|
||||||
|
const [newActionGestureType, setNewActionGestureType] = useState<boolean>(true);
|
||||||
|
const [newActionValue, setNewActionValue] = useState("");
|
||||||
|
const [hasInteractedWithPlan, setHasInteractedWithPlan] = useState<boolean>(false)
|
||||||
|
const { setScrollable } = useFlowStore();
|
||||||
|
const nodes = useFlowStore().nodes;
|
||||||
|
|
||||||
|
//Button Actions
|
||||||
|
const openCreate = () => {
|
||||||
|
setScrollable(false);
|
||||||
|
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()});
|
||||||
|
dialogRef.current?.showModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateWithDescription = () => {
|
||||||
|
setScrollable(false);
|
||||||
|
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID(), name: description!});
|
||||||
|
setNewActionType("llm")
|
||||||
|
setNewActionValue(description!)
|
||||||
|
dialogRef.current?.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEdit = () => {
|
||||||
|
setScrollable(false);
|
||||||
|
if (!plan) return;
|
||||||
|
setDraftPlan(structuredClone(plan));
|
||||||
|
dialogRef.current?.showModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
setScrollable(true);
|
||||||
|
dialogRef.current?.close();
|
||||||
|
setDraftPlan(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAction = () => {
|
||||||
|
if (!draftPlan) return;
|
||||||
|
// Add action to steps
|
||||||
|
const action = buildAction(newActionType, newActionValue, newActionGestureType);
|
||||||
|
setDraftPlan({
|
||||||
|
...draftPlan,
|
||||||
|
steps: [...draftPlan.steps, action],});
|
||||||
|
|
||||||
|
// Reset current action building
|
||||||
|
setNewActionValue("");
|
||||||
|
setNewActionType("speech");
|
||||||
|
setHasInteractedWithPlan(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const showSuggestion : boolean = (
|
||||||
|
!plan &&
|
||||||
|
!!description &&
|
||||||
|
draftPlan !== null &&
|
||||||
|
draftPlan.steps.length === 0 &&
|
||||||
|
!hasInteractedWithPlan)
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
{/* Create and edit buttons */}
|
||||||
|
{!plan && (
|
||||||
|
<button className={styles.nodeButton} onClick={description ? openCreateWithDescription : openCreate}>
|
||||||
|
Create Plan
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{plan && (
|
||||||
|
<button className={styles.nodeButton} onClick={openEdit}>
|
||||||
|
Edit Plan
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Start of dialog (plan editor) */}
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
className={`${styles.planDialog}`}
|
||||||
|
//onWheel={(e) => e.stopPropagation()}
|
||||||
|
data-testid={"PlanEditorDialogTestID"}
|
||||||
|
>
|
||||||
|
<form method="dialog" className="flex-col gap-md">
|
||||||
|
<h3> {draftPlan?.id === plan?.id ? "Edit Plan" : "Create Plan"} </h3>
|
||||||
|
{/* Plan name text field */}
|
||||||
|
{draftPlan && (
|
||||||
|
<TextField
|
||||||
|
value={draftPlan.name}
|
||||||
|
setValue={(name) =>
|
||||||
|
setDraftPlan({ ...draftPlan, name })}
|
||||||
|
placeholder="Plan name"
|
||||||
|
data-testid="name_text_field"/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Entire "bottom" part (adder and steps) without cancel, confirm and reset */}
|
||||||
|
{draftPlan && (<div className={styles.planEditor}>
|
||||||
|
<div className={styles.planEditorLeft}>
|
||||||
|
{/* Left Side (Action Adder) */}
|
||||||
|
<ActionAdder
|
||||||
|
newActionType={newActionType}
|
||||||
|
setNewActionType={setNewActionType}
|
||||||
|
newActionValue={newActionValue}
|
||||||
|
setNewActionValue={setNewActionValue}
|
||||||
|
setNewActionGestureType={setNewActionGestureType}
|
||||||
|
onAdd={addAction}
|
||||||
|
showSuggestion={showSuggestion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side (Steps shown) */}
|
||||||
|
<div className={styles.planEditorRight}>
|
||||||
|
<h4>Steps</h4>
|
||||||
|
{/* Map over all steps */}
|
||||||
|
<StepsList
|
||||||
|
steps={draftPlan.steps}
|
||||||
|
nodes={nodes}
|
||||||
|
onRemove={(id) =>
|
||||||
|
setDraftPlan({
|
||||||
|
...draftPlan,
|
||||||
|
steps: draftPlan.steps.filter(s => s.id !== id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="flex-row gap-md">
|
||||||
|
{/* Close button */}
|
||||||
|
<button type="button" onClick={close}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Confirm/ Create button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!draftPlan}
|
||||||
|
onClick={() => {
|
||||||
|
if (!draftPlan) return;
|
||||||
|
onSave(draftPlan);
|
||||||
|
close();
|
||||||
|
}}>
|
||||||
|
{draftPlan?.id === plan?.id ? "Confirm" : "Create"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Reset button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!draftPlan}
|
||||||
|
onClick={() => {
|
||||||
|
onSave(undefined);
|
||||||
|
close();
|
||||||
|
}}>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { GetActionValue, type PlanElement } from "./Plan";
|
||||||
|
import styles from './PlanEditor.module.css';
|
||||||
|
import { type Node} from "@xyflow/react"
|
||||||
|
|
||||||
|
type StepsListProps = {
|
||||||
|
steps: PlanElement[];
|
||||||
|
onRemove: (id: string) => void;
|
||||||
|
nodes: Node[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStepLabel(
|
||||||
|
step: PlanElement,
|
||||||
|
nodes: Node[],
|
||||||
|
): string {
|
||||||
|
if (step.type === "goal") {
|
||||||
|
// For goals, we lookup the value through the nodes in the diagram
|
||||||
|
const node = nodes.find(n => n.id === step.id);
|
||||||
|
return (node?.data?.name as string)?.trim() || "unnamed goal";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a goal, we lookup the correct action value of the action
|
||||||
|
return GetActionValue(step) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StepsList({ steps, onRemove, nodes }: StepsListProps) {
|
||||||
|
if (steps.length === 0) {
|
||||||
|
return <div className={styles.emptySteps}>No steps yet</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<div
|
||||||
|
key={step.id}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={styles.planStep}
|
||||||
|
onClick={() => onRemove(step.id)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
onRemove(step.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={styles.stepIndex}>{index + 1}.</span>
|
||||||
|
<span className={styles.stepType}>{step.type}:</span>
|
||||||
|
<span className={styles.stepName}>
|
||||||
|
{getStepLabel(step, nodes)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,11 +14,11 @@ import { TextField } from '../../../../components/TextField';
|
|||||||
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
import {DoesPlanIterate, HasCheckingSubGoal, PlanReduce, type Plan } from '../components/Plan';
|
import {DoesPlanIterate, HasCheckingSubGoal, PlanReduce, type Plan } from '../components/PlanEditor/Plan.tsx';
|
||||||
import PlanEditorDialog from '../components/PlanEditor';
|
import PlanEditorDialog from '../components/PlanEditor/PlanEditor.tsx';
|
||||||
import { MultilineTextField } from '../../../../components/MultilineTextField';
|
import { MultilineTextField } from '../../../../components/MultilineTextField';
|
||||||
import { defaultPlan } from '../components/Plan.default.ts';
|
import { defaultPlan } from '../components/PlanEditor/Plan.default.ts';
|
||||||
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
|
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditor/PlanEditingFunctions.tsx';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default data dot a phase node
|
* The default data dot a phase node
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import styles from '../../VisProg.module.css';
|
|||||||
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
import {PlanReduce, type Plan } from '../components/Plan';
|
import {PlanReduce, type Plan } from '../components/PlanEditor/Plan.tsx';
|
||||||
import PlanEditorDialog from '../components/PlanEditor';
|
import PlanEditorDialog from '../components/PlanEditor/PlanEditor.tsx';
|
||||||
import {BeliefGlobalReduce} from "./BeliefGlobals.ts";
|
import {BeliefGlobalReduce} from "./BeliefGlobals.ts";
|
||||||
import type { GoalNode } from './GoalNode.tsx';
|
import type { GoalNode } from './GoalNode.tsx';
|
||||||
import { defaultPlan } from '../components/Plan.default.ts';
|
import { defaultPlan } from '../components/PlanEditor/Plan.default.ts';
|
||||||
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
|
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditor/PlanEditingFunctions.tsx';
|
||||||
import { TextField } from '../../../../components/TextField.tsx';
|
import { TextField } from '../../../../components/TextField.tsx';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { renderWithProviders, screen } from '../../../../test-utils/test-utils.tsx';
|
import { renderWithProviders, screen } from '../../../../test-utils/test-utils.tsx';
|
||||||
import GestureValueEditor from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/GestureValueEditor';
|
import GestureValueEditor from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor/GestureValueEditor.tsx';
|
||||||
|
|
||||||
function TestHarness({ initialValue = '', initialType=true, placeholder = 'Gesture name' } : { initialValue?: string, initialType?: boolean, placeholder?: string }) {
|
function TestHarness({ initialValue = '', initialType=true, placeholder = 'Gesture name' } : { initialValue?: string, initialType?: boolean, placeholder?: string }) {
|
||||||
const [value, setValue] = useState(initialValue);
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { screen, within } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import type { Node } from '@xyflow/react';
|
import type { Node } from '@xyflow/react';
|
||||||
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
|
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
|
||||||
import PlanEditorDialog from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor';
|
import PlanEditorDialog from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor/PlanEditor.tsx';
|
||||||
import { PlanReduce, type Plan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan';
|
import { PlanReduce, type Plan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor/Plan.tsx';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { GoalReduce, type GoalNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.tsx';
|
import { GoalReduce, type GoalNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.tsx';
|
||||||
import { GoalNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts';
|
import { GoalNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts';
|
||||||
import { insertGoalInPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditingFunctions.tsx';
|
import { insertGoalInPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor/PlanEditingFunctions.tsx';
|
||||||
|
|
||||||
|
|
||||||
// Mock structuredClone
|
// Mock structuredClone
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgramming
|
|||||||
import type { Node } from '@xyflow/react';
|
import type { Node } from '@xyflow/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { GoalNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts';
|
import { GoalNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts';
|
||||||
import { defaultPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan.default.ts';
|
import { defaultPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor/Plan.default.ts';
|
||||||
|
|
||||||
describe('GoalNode', () => {
|
describe('GoalNode', () => {
|
||||||
let user: ReturnType<typeof userEvent.setup>;
|
let user: ReturnType<typeof userEvent.setup>;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type { Node } from '@xyflow/react';
|
|||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { TriggerNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.default.ts';
|
import { TriggerNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.default.ts';
|
||||||
import { BasicBeliefNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts';
|
import { BasicBeliefNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts';
|
||||||
import { defaultPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan.default.ts';
|
import { defaultPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor/Plan.default.ts';
|
||||||
import { NormNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.default.ts';
|
import { NormNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.default.ts';
|
||||||
import { GoalNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts';
|
import { GoalNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts';
|
||||||
import { act } from '@testing-library/react';
|
import { act } from '@testing-library/react';
|
||||||
|
|||||||
Reference in New Issue
Block a user