Refactor of visual programming page to fully match the CB's program schema. Includes overhaul of UI elements for plan creation. #38
@@ -10,6 +10,7 @@ import styles from './GestureValueEditor.module.css'
|
|||||||
type GestureValueEditorProps = {
|
type GestureValueEditorProps = {
|
||||||
value: string;
|
value: string;
|
||||||
setValue: (value: string) => void;
|
setValue: (value: string) => void;
|
||||||
|
setType: (value: boolean) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -443,6 +444,7 @@ const GESTURE_SINGLES = [
|
|||||||
export default function GestureValueEditor({
|
export default function GestureValueEditor({
|
||||||
value,
|
value,
|
||||||
setValue,
|
setValue,
|
||||||
|
setType,
|
||||||
placeholder = "Gesture name",
|
placeholder = "Gesture name",
|
||||||
}: GestureValueEditorProps) {
|
}: GestureValueEditorProps) {
|
||||||
|
|
||||||
@@ -465,10 +467,12 @@ export default function GestureValueEditor({
|
|||||||
|
|
||||||
if (newMode === "single") {
|
if (newMode === "single") {
|
||||||
setValue(customValue || value);
|
setValue(customValue || value);
|
||||||
|
setType(false);
|
||||||
setFilteredSuggestions(GESTURE_SINGLES);
|
setFilteredSuggestions(GESTURE_SINGLES);
|
||||||
setShowSuggestions(true);
|
setShowSuggestions(true);
|
||||||
} else {
|
} else {
|
||||||
// Clear value if it does not match a valid tag
|
// Clear value if it does not match a valid tag
|
||||||
|
setType(true);
|
||||||
const isValidTag = GESTURE_TAGS.some(
|
const isValidTag = GESTURE_TAGS.some(
|
||||||
tag => tag.toLowerCase() === value.toLowerCase()
|
tag => tag.toLowerCase() === value.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export type Goal = {
|
|||||||
// Actions
|
// Actions
|
||||||
export type Action = SpeechAction | GestureAction | LLMAction
|
export type Action = SpeechAction | GestureAction | LLMAction
|
||||||
export type SpeechAction = { id: string, text: string, type:"speech" }
|
export type SpeechAction = { id: string, text: string, type:"speech" }
|
||||||
export type GestureAction = { id: string, gesture: string, type:"gesture" }
|
export type GestureAction = { id: string, gesture: string, isTag: boolean, type:"gesture" }
|
||||||
export type LLMAction = { id: string, goal: string, type:"llm" }
|
export type LLMAction = { id: string, goal: string, type:"llm" }
|
||||||
|
|
||||||
export type ActionTypes = "speech" | "gesture" | "llm";
|
export type ActionTypes = "speech" | "gesture" | "llm";
|
||||||
@@ -29,7 +29,40 @@ export function PlanReduce(plan?: Plan) {
|
|||||||
return {
|
return {
|
||||||
name: plan.name,
|
name: plan.name,
|
||||||
id: plan.id,
|
id: plan.id,
|
||||||
steps: plan.steps,
|
steps: plan.steps.map((x) => StepReduce(x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Extract the wanted information from a plan element.
|
||||||
|
function StepReduce(planElement: PlanElement) {
|
||||||
|
// We have different types of plan elements, requiring differnt types of output
|
||||||
|
switch (planElement.type) {
|
||||||
|
case ("speech"):
|
||||||
|
return {
|
||||||
|
id: planElement.id,
|
||||||
|
text: planElement.text,
|
||||||
|
}
|
||||||
|
case ("gesture"):
|
||||||
|
return {
|
||||||
|
id: planElement.id,
|
||||||
|
gesture: {
|
||||||
|
type: planElement.isTag ? "tag" : "single",
|
||||||
|
name: planElement.gesture
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case ("llm"):
|
||||||
|
return {
|
||||||
|
id: planElement.id,
|
||||||
|
goal: planElement.goal,
|
||||||
|
}
|
||||||
|
case ("goal"):
|
||||||
|
return {
|
||||||
|
id: planElement.id,
|
||||||
|
plan: planElement.plan,
|
||||||
|
can_fail: planElement.can_fail,
|
||||||
|
};
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default function PlanEditorDialog({
|
|||||||
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
||||||
const [draftPlan, setDraftPlan] = useState<Plan | null>(null);
|
const [draftPlan, setDraftPlan] = useState<Plan | null>(null);
|
||||||
const [newActionType, setNewActionType] = useState<ActionTypes>("speech");
|
const [newActionType, setNewActionType] = useState<ActionTypes>("speech");
|
||||||
|
const [newActionGestureType, setNewActionGestureType] = useState<boolean>(true);
|
||||||
const [newActionValue, setNewActionValue] = useState("");
|
const [newActionValue, setNewActionValue] = useState("");
|
||||||
const { setScrollable } = useFlowStore();
|
const { setScrollable } = useFlowStore();
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ export default function PlanEditorDialog({
|
|||||||
case "speech":
|
case "speech":
|
||||||
return { id, text: newActionValue, type: "speech" };
|
return { id, text: newActionValue, type: "speech" };
|
||||||
case "gesture":
|
case "gesture":
|
||||||
return { id, gesture: newActionValue, type: "gesture" };
|
return { id, gesture: newActionValue, isTag: newActionGestureType, type: "gesture" };
|
||||||
case "llm":
|
case "llm":
|
||||||
return { id, goal: newActionValue, type: "llm" };
|
return { id, goal: newActionValue, type: "llm" };
|
||||||
}
|
}
|
||||||
@@ -127,6 +128,7 @@ export default function PlanEditorDialog({
|
|||||||
<GestureValueEditor
|
<GestureValueEditor
|
||||||
value={newActionValue}
|
value={newActionValue}
|
||||||
setValue={setNewActionValue}
|
setValue={setNewActionValue}
|
||||||
|
setType={setNewActionGestureType}
|
||||||
placeholder="Gesture name"
|
placeholder="Gesture name"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label> {!data.plan ? "No plan set to execute while goal is not reached. 🔴" : "Will follow plan '" + data.plan.name + "' until goal is met. 🟢"} </label>
|
<label> {!data.plan ? "No plan set to execute while goal is not reached. 🔴" : "Will follow plan '" + data.plan.name + "' until all steps complete. 🟢"} </label>
|
||||||
</div>
|
</div>
|
||||||
{data.plan && (<div className={"flex-row gap-md align-center " + (planIterate ? "" : styles.planNoIterate)}>
|
{data.plan && (<div className={"flex-row gap-md align-center " + (planIterate ? "" : styles.planNoIterate)}>
|
||||||
{planIterate ? "" : <s></s>}
|
{planIterate ? "" : <s></s>}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ 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/GestureValueEditor';
|
||||||
|
|
||||||
function TestHarness({ initialValue = '', placeholder = 'Gesture name' } : { initialValue?: string, 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);
|
||||||
|
const [_, setType] = useState(initialType)
|
||||||
return (
|
return (
|
||||||
<GestureValueEditor value={value} setValue={setValue} placeholder={placeholder} />
|
<GestureValueEditor value={value} setValue={setValue} setType={setType} placeholder={placeholder} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { screen, within } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
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';
|
||||||
import type { Plan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan';
|
import { PlanReduce, type Plan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
// Mock structuredClone
|
// Mock structuredClone
|
||||||
@@ -28,12 +28,48 @@ describe('PlanEditorDialog', () => {
|
|||||||
steps: [],
|
steps: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extendedPlan: Plan = {
|
||||||
|
id: 'extended-plan-1',
|
||||||
|
name: 'extended test plan',
|
||||||
|
steps: [
|
||||||
|
// Step 1: A wave tag gesture
|
||||||
|
{
|
||||||
|
id: 'firststep',
|
||||||
|
type: 'gesture',
|
||||||
|
isTag: true,
|
||||||
|
gesture: "hello"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step 2: A single tag gesture
|
||||||
|
{
|
||||||
|
id: 'secondstep',
|
||||||
|
type: 'gesture',
|
||||||
|
isTag: false,
|
||||||
|
gesture: "somefolder/somegesture"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step 3: A LLM action
|
||||||
|
{
|
||||||
|
id: 'thirdstep',
|
||||||
|
type: 'llm',
|
||||||
|
goal: 'ask the user something or whatever'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Step 4: A speech action
|
||||||
|
{
|
||||||
|
id: 'fourthstep',
|
||||||
|
type: 'speech',
|
||||||
|
text: "I'm a cyborg ninja :>"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const planWithSteps: Plan = {
|
const planWithSteps: Plan = {
|
||||||
id: 'plan-2',
|
id: 'plan-2',
|
||||||
name: 'Existing Plan',
|
name: 'Existing Plan',
|
||||||
steps: [
|
steps: [
|
||||||
{ id: 'step-1', text: 'Hello world', type: 'speech' as const },
|
{ id: 'step-1', text: 'Hello world', type: 'speech' as const },
|
||||||
{ id: 'step-2', gesture: 'Wave', type: 'gesture' as const },
|
{ id: 'step-2', gesture: 'Wave', isTag:true, type: 'gesture' as const },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -429,4 +465,42 @@ describe('PlanEditorDialog', () => {
|
|||||||
expect(llmInput).toBeInTheDocument();
|
expect(llmInput).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Plan reducing', () => {
|
||||||
|
it('should correctly reduce the plan given the elements of the plan', () => {
|
||||||
|
const testplan = extendedPlan
|
||||||
|
const expectedResult = {
|
||||||
|
name: "extended test plan",
|
||||||
|
id: "extended-plan-1",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "firststep",
|
||||||
|
gesture: {
|
||||||
|
type: "tag",
|
||||||
|
name: "hello"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "secondstep",
|
||||||
|
gesture: {
|
||||||
|
type: "single",
|
||||||
|
name: "somefolder/somegesture"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "thirdstep",
|
||||||
|
goal: "ask the user something or whatever"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fourthstep",
|
||||||
|
text: "I'm a cyborg ninja :>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualResult = PlanReduce(testplan)
|
||||||
|
|
||||||
|
expect(actualResult).toEqual(expectedResult)
|
||||||
|
});
|
||||||
|
})
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user