Refactor of visual programming page to fully match the CB's program schema. Includes overhaul of UI elements for plan creation. #38

Merged
9828273 merged 23 commits from refactor/nodes-match-functionality into demo 2026-01-07 15:19:47 +00:00
6 changed files with 122 additions and 8 deletions
Showing only changes of commit 216b136a75 - Show all commits

View File

@@ -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()
); );

View File

@@ -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:
} }
} }

View File

@@ -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"
/> />
) : ( ) : (

View File

@@ -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>}

View File

@@ -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} />
); );
} }

View File

@@ -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)
});
})
}); });