feat: fix a lot of small changes to match cb, add functionality for all plans, add tests for the new plan editor. even more i dont really know anymore.
ref: N25B-412
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
import { useRef, useState } from "react";
|
||||
import styles from '../../VisProg.module.css';
|
||||
import { GetActionValue, type Action, type ActionTypes, type Plan } from "../components/Plan";
|
||||
import { defaultPlan } from "../components/Plan.default";
|
||||
import { TextField } from "../../../../components/TextField";
|
||||
|
||||
type PlanEditorDialogProps = {
|
||||
plan?: Plan;
|
||||
onSave: (plan: Plan | undefined) => void;
|
||||
description? : string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds an element to a React.JSX.Element that allows for the creation and editing of plans.
|
||||
* Renders a dialog in the current screen with buttons and text fields for names, actions and other configurability.
|
||||
* @param param0: Takes in a current plan, which can be undefined and a function which is called on saving with the potential plan.
|
||||
* @returns: JSX.Element
|
||||
* @example
|
||||
* ```
|
||||
* // Within a Node's default JSX Element function
|
||||
* <PlanEditorDialog
|
||||
* plan={data.plan}
|
||||
* onSave={(plan) => {
|
||||
* updateNodeData(props.id, {
|
||||
* ...data,
|
||||
* plan,
|
||||
* });
|
||||
* }}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
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 [newActionValue, setNewActionValue] = useState("");
|
||||
|
||||
//Button Actions
|
||||
const openCreate = () => {
|
||||
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()});
|
||||
dialogRef.current?.showModal();
|
||||
};
|
||||
|
||||
const openCreateWithDescription = () => {
|
||||
setDraftPlan({...structuredClone(defaultPlan), id: crypto.randomUUID(), name: description!});
|
||||
setNewActionType("llm")
|
||||
setNewActionValue(description!)
|
||||
dialogRef.current?.showModal();
|
||||
}
|
||||
|
||||
|
||||
|
||||
const openEdit = () => {
|
||||
if (!plan) return;
|
||||
setDraftPlan(structuredClone(plan));
|
||||
dialogRef.current?.showModal();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
dialogRef.current?.close();
|
||||
setDraftPlan(null);
|
||||
};
|
||||
|
||||
const buildAction = (): Action => {
|
||||
const id = crypto.randomUUID();
|
||||
switch (newActionType) {
|
||||
case "speech":
|
||||
return { id, text: newActionValue, type: "speech" };
|
||||
case "gesture":
|
||||
return { id, gesture: newActionValue, 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}
|
||||
onCancel={(e) => e.preventDefault()}
|
||||
>
|
||||
<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) && (<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)}>
|
||||
<option value="speech">Speech Action</option>
|
||||
<option value="gesture">Gesture Action</option>
|
||||
<option value="llm">LLM Action</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{/* Action value editor */}
|
||||
<TextField
|
||||
value={newActionValue}
|
||||
setValue={setNewActionValue}
|
||||
|
||||
// TODO: Can be done with a switch in case there's more types to come.
|
||||
placeholder={
|
||||
newActionType === "speech" ? "Speech text"
|
||||
: newActionType === "gesture" ? "Gesture name"
|
||||
: "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, create a div for them that deletes them
|
||||
if clicked on and add the index, name and type. as spans */}
|
||||
{draftPlan.steps.map((step, index) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className={styles.planStep}
|
||||
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}>{
|
||||
step.type == "goal" ? ""/* TODO: Add support for goals */
|
||||
: GetActionValue(step)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)} {/* End Action Editor and steps shower */}
|
||||
|
||||
{/* 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user