chore: replace plan editor functionality into own folder with own files.

This commit is contained in:
Björn Otgaar
2026-01-28 12:26:57 +01:00
parent 1876138fe2
commit dcf4f01e68
16 changed files with 365 additions and 301 deletions

View File

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

View File

@@ -0,0 +1,168 @@
/*
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)
*/
.gestureEditor {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.modeSelector {
display: flex;
align-items: center;
gap: 12px;
}
.modeLabel {
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
white-space: nowrap;
}
.toggleContainer {
display: flex;
background: rgba(78, 78, 78, 0.411);
border-radius: 6px;
padding: 2px;
border: 1px solid var(--border-color);
}
.toggleButton {
padding: 6px 12px;
background: none;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-secondary);
}
.toggleButton:hover {
background: none;
}
.toggleButton.active {
box-shadow: 0 0 1px 0 rgba(9, 255, 0, 0.733);
}
.valueEditor {
width: 100%;
}
.textInput {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s ease;
}
.textInput:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.1);
}
.tagSelector {
display: flex;
flex-direction: column;
gap: 8px;
}
.tagSelect {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
background-color: rgba(135, 135, 135, 0.296);
cursor: pointer;
}
.tagSelect:focus {
outline: none;
border-color: rgb(0, 149, 25);
}
.tagList {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-height: 120px;
overflow-y: auto;
padding: 4px;
border: 1px solid rgba(var(--primary-rgb), 0.1);
border-radius: 4px;
background: var(--primary-color);
}
.tagButton {
padding: 4px 8px;
border: 1px solid gray;
border-radius: 4px;
background: var(--primary-rgb);
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.tagButton:hover {
background: gray;
border-color: gray;
}
.tagButton.selected {
background: rgba(var(--primary-rgb), 0.5);
color: var(--primary-rgb);
border-color: rgb(27, 223, 60);
}
.suggestionsDropdownLeft {
position: absolute;
left: -220px;
top: 120px;
width: 200px;
max-height: 20vh;
overflow-y: auto;
background: var(--dropdown-menu-background-color);
border-radius: 12px;
box-shadow: 0 8px 24px var(--dropdown-menu-border);
}
.suggestionsDropdownLeft::before {
content: "Gesture Suggestions";
display: block;
padding: 8px 12px;
font-weight: 600;
border-bottom: 1px solid var(--border-light);
}
.suggestionItem {
padding: 8px 12px;
cursor: pointer;
transition: background-color 0.2s ease;
font-size: 14px;
border-bottom: 1px solid var(--border-light);
}
.suggestionItem:last-child {
border-bottom: none;
}
.suggestionItem:hover {
background-color: var(--background-hover);
}
.suggestionItem:active {
background-color: var(--primary-color-light);
}

View File

@@ -0,0 +1,614 @@
// 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 { useState, useRef } from "react";
import styles from './GestureValueEditor.module.css'
/**
* Props for the GestureValueEditor component.
* - value: current gesture value (controlled by parent)
* - setValue: callback to update the gesture value in parent state
* - placeholder: optional placeholder text for the input field
*/
type GestureValueEditorProps = {
value: string;
setValue: (value: string) => void;
setType: (value: boolean) => void;
placeholder?: string;
};
/**
* List of high-level gesture "tags".
* These are human-readable categories or semantic labels.
* In a real app, these would likely be loaded from an external source.
*/
const GESTURE_TAGS = ["above", "affirmative", "afford", "agitated", "all", "allright", "alright", "any",
"assuage", "attemper", "back", "bashful", "beg", "beseech", "blank",
"body language", "bored", "bow", "but", "call", "calm", "choose", "choice", "cloud",
"cogitate", "cool", "crazy", "disappointed", "down", "earth", "empty", "embarrassed",
"enthusiastic", "entire", "estimate", "except", "exalted", "excited", "explain", "far",
"field", "floor", "forlorn", "friendly", "front", "frustrated", "gentle", "gift",
"give", "ground", "happy", "hello", "her", "here", "hey", "hi", "him", "hopeless",
"hysterical", "I", "implore", "indicate", "joyful", "me", "meditate", "modest",
"negative", "nervous", "no", "not know", "nothing", "offer", "ok", "once upon a time",
"oppose", "or", "pacify", "pick", "placate", "please", "present", "proffer", "quiet",
"reason", "refute", "reject", "rousing", "sad", "select", "shamefaced", "show",
"show sky", "sky", "soothe", "sun", "supplicate", "tablet", "tall", "them", "there",
"think", "timid", "top", "unless", "up", "upstairs", "void", "warm", "winner", "yeah",
"yes", "yoo-hoo", "you", "your", "zero", "zestful"];
/**
* List of concrete gesture animation paths.
* These represent specific animation assets and are used in "single" mode
* with autocomplete-style selection, also would be loaded from an external source.
*/
const GESTURE_SINGLES = [
"animations/Stand/BodyTalk/Listening/Listening_1",
"animations/Stand/BodyTalk/Listening/Listening_2",
"animations/Stand/BodyTalk/Listening/Listening_3",
"animations/Stand/BodyTalk/Listening/Listening_4",
"animations/Stand/BodyTalk/Listening/Listening_5",
"animations/Stand/BodyTalk/Listening/Listening_6",
"animations/Stand/BodyTalk/Listening/Listening_7",
"animations/Stand/BodyTalk/Speaking/BodyTalk_1",
"animations/Stand/BodyTalk/Speaking/BodyTalk_10",
"animations/Stand/BodyTalk/Speaking/BodyTalk_11",
"animations/Stand/BodyTalk/Speaking/BodyTalk_12",
"animations/Stand/BodyTalk/Speaking/BodyTalk_13",
"animations/Stand/BodyTalk/Speaking/BodyTalk_14",
"animations/Stand/BodyTalk/Speaking/BodyTalk_15",
"animations/Stand/BodyTalk/Speaking/BodyTalk_16",
"animations/Stand/BodyTalk/Speaking/BodyTalk_2",
"animations/Stand/BodyTalk/Speaking/BodyTalk_3",
"animations/Stand/BodyTalk/Speaking/BodyTalk_4",
"animations/Stand/BodyTalk/Speaking/BodyTalk_5",
"animations/Stand/BodyTalk/Speaking/BodyTalk_6",
"animations/Stand/BodyTalk/Speaking/BodyTalk_7",
"animations/Stand/BodyTalk/Speaking/BodyTalk_8",
"animations/Stand/BodyTalk/Speaking/BodyTalk_9",
"animations/Stand/BodyTalk/Thinking/Remember_1",
"animations/Stand/BodyTalk/Thinking/Remember_2",
"animations/Stand/BodyTalk/Thinking/Remember_3",
"animations/Stand/BodyTalk/Thinking/ThinkingLoop_1",
"animations/Stand/BodyTalk/Thinking/ThinkingLoop_2",
"animations/Stand/Emotions/Negative/Angry_1",
"animations/Stand/Emotions/Negative/Angry_2",
"animations/Stand/Emotions/Negative/Angry_3",
"animations/Stand/Emotions/Negative/Angry_4",
"animations/Stand/Emotions/Negative/Anxious_1",
"animations/Stand/Emotions/Negative/Bored_1",
"animations/Stand/Emotions/Negative/Bored_2",
"animations/Stand/Emotions/Negative/Disappointed_1",
"animations/Stand/Emotions/Negative/Exhausted_1",
"animations/Stand/Emotions/Negative/Exhausted_2",
"animations/Stand/Emotions/Negative/Fear_1",
"animations/Stand/Emotions/Negative/Fear_2",
"animations/Stand/Emotions/Negative/Fearful_1",
"animations/Stand/Emotions/Negative/Frustrated_1",
"animations/Stand/Emotions/Negative/Humiliated_1",
"animations/Stand/Emotions/Negative/Hurt_1",
"animations/Stand/Emotions/Negative/Hurt_2",
"animations/Stand/Emotions/Negative/Late_1",
"animations/Stand/Emotions/Negative/Sad_1",
"animations/Stand/Emotions/Negative/Sad_2",
"animations/Stand/Emotions/Negative/Shocked_1",
"animations/Stand/Emotions/Negative/Sorry_1",
"animations/Stand/Emotions/Negative/Surprise_1",
"animations/Stand/Emotions/Negative/Surprise_2",
"animations/Stand/Emotions/Negative/Surprise_3",
"animations/Stand/Emotions/Neutral/Alienated_1",
"animations/Stand/Emotions/Neutral/AskForAttention_1",
"animations/Stand/Emotions/Neutral/AskForAttention_2",
"animations/Stand/Emotions/Neutral/AskForAttention_3",
"animations/Stand/Emotions/Neutral/Cautious_1",
"animations/Stand/Emotions/Neutral/Confused_1",
"animations/Stand/Emotions/Neutral/Determined_1",
"animations/Stand/Emotions/Neutral/Embarrassed_1",
"animations/Stand/Emotions/Neutral/Hesitation_1",
"animations/Stand/Emotions/Neutral/Innocent_1",
"animations/Stand/Emotions/Neutral/Lonely_1",
"animations/Stand/Emotions/Neutral/Mischievous_1",
"animations/Stand/Emotions/Neutral/Puzzled_1",
"animations/Stand/Emotions/Neutral/Sneeze",
"animations/Stand/Emotions/Neutral/Stubborn_1",
"animations/Stand/Emotions/Neutral/Suspicious_1",
"animations/Stand/Emotions/Positive/Amused_1",
"animations/Stand/Emotions/Positive/Confident_1",
"animations/Stand/Emotions/Positive/Ecstatic_1",
"animations/Stand/Emotions/Positive/Enthusiastic_1",
"animations/Stand/Emotions/Positive/Excited_1",
"animations/Stand/Emotions/Positive/Excited_2",
"animations/Stand/Emotions/Positive/Excited_3",
"animations/Stand/Emotions/Positive/Happy_1",
"animations/Stand/Emotions/Positive/Happy_2",
"animations/Stand/Emotions/Positive/Happy_3",
"animations/Stand/Emotions/Positive/Happy_4",
"animations/Stand/Emotions/Positive/Hungry_1",
"animations/Stand/Emotions/Positive/Hysterical_1",
"animations/Stand/Emotions/Positive/Interested_1",
"animations/Stand/Emotions/Positive/Interested_2",
"animations/Stand/Emotions/Positive/Laugh_1",
"animations/Stand/Emotions/Positive/Laugh_2",
"animations/Stand/Emotions/Positive/Laugh_3",
"animations/Stand/Emotions/Positive/Mocker_1",
"animations/Stand/Emotions/Positive/Optimistic_1",
"animations/Stand/Emotions/Positive/Peaceful_1",
"animations/Stand/Emotions/Positive/Proud_1",
"animations/Stand/Emotions/Positive/Proud_2",
"animations/Stand/Emotions/Positive/Proud_3",
"animations/Stand/Emotions/Positive/Relieved_1",
"animations/Stand/Emotions/Positive/Shy_1",
"animations/Stand/Emotions/Positive/Shy_2",
"animations/Stand/Emotions/Positive/Sure_1",
"animations/Stand/Emotions/Positive/Winner_1",
"animations/Stand/Emotions/Positive/Winner_2",
"animations/Stand/Gestures/Angry_1",
"animations/Stand/Gestures/Angry_2",
"animations/Stand/Gestures/Angry_3",
"animations/Stand/Gestures/BowShort_1",
"animations/Stand/Gestures/BowShort_2",
"animations/Stand/Gestures/BowShort_3",
"animations/Stand/Gestures/But_1",
"animations/Stand/Gestures/CalmDown_1",
"animations/Stand/Gestures/CalmDown_2",
"animations/Stand/Gestures/CalmDown_3",
"animations/Stand/Gestures/CalmDown_4",
"animations/Stand/Gestures/CalmDown_5",
"animations/Stand/Gestures/CalmDown_6",
"animations/Stand/Gestures/Choice_1",
"animations/Stand/Gestures/ComeOn_1",
"animations/Stand/Gestures/Confused_1",
"animations/Stand/Gestures/Confused_2",
"animations/Stand/Gestures/CountFive_1",
"animations/Stand/Gestures/CountFour_1",
"animations/Stand/Gestures/CountMore_1",
"animations/Stand/Gestures/CountOne_1",
"animations/Stand/Gestures/CountThree_1",
"animations/Stand/Gestures/CountTwo_1",
"animations/Stand/Gestures/Desperate_1",
"animations/Stand/Gestures/Desperate_2",
"animations/Stand/Gestures/Desperate_3",
"animations/Stand/Gestures/Desperate_4",
"animations/Stand/Gestures/Desperate_5",
"animations/Stand/Gestures/DontUnderstand_1",
"animations/Stand/Gestures/Enthusiastic_3",
"animations/Stand/Gestures/Enthusiastic_4",
"animations/Stand/Gestures/Enthusiastic_5",
"animations/Stand/Gestures/Everything_1",
"animations/Stand/Gestures/Everything_2",
"animations/Stand/Gestures/Everything_3",
"animations/Stand/Gestures/Everything_4",
"animations/Stand/Gestures/Everything_6",
"animations/Stand/Gestures/Excited_1",
"animations/Stand/Gestures/Explain_1",
"animations/Stand/Gestures/Explain_10",
"animations/Stand/Gestures/Explain_11",
"animations/Stand/Gestures/Explain_2",
"animations/Stand/Gestures/Explain_3",
"animations/Stand/Gestures/Explain_4",
"animations/Stand/Gestures/Explain_5",
"animations/Stand/Gestures/Explain_6",
"animations/Stand/Gestures/Explain_7",
"animations/Stand/Gestures/Explain_8",
"animations/Stand/Gestures/Far_1",
"animations/Stand/Gestures/Far_2",
"animations/Stand/Gestures/Far_3",
"animations/Stand/Gestures/Follow_1",
"animations/Stand/Gestures/Give_1",
"animations/Stand/Gestures/Give_2",
"animations/Stand/Gestures/Give_3",
"animations/Stand/Gestures/Give_4",
"animations/Stand/Gestures/Give_5",
"animations/Stand/Gestures/Give_6",
"animations/Stand/Gestures/Great_1",
"animations/Stand/Gestures/HeSays_1",
"animations/Stand/Gestures/HeSays_2",
"animations/Stand/Gestures/HeSays_3",
"animations/Stand/Gestures/Hey_1",
"animations/Stand/Gestures/Hey_10",
"animations/Stand/Gestures/Hey_2",
"animations/Stand/Gestures/Hey_3",
"animations/Stand/Gestures/Hey_4",
"animations/Stand/Gestures/Hey_6",
"animations/Stand/Gestures/Hey_7",
"animations/Stand/Gestures/Hey_8",
"animations/Stand/Gestures/Hey_9",
"animations/Stand/Gestures/Hide_1",
"animations/Stand/Gestures/Hot_1",
"animations/Stand/Gestures/Hot_2",
"animations/Stand/Gestures/IDontKnow_1",
"animations/Stand/Gestures/IDontKnow_2",
"animations/Stand/Gestures/IDontKnow_3",
"animations/Stand/Gestures/IDontKnow_4",
"animations/Stand/Gestures/IDontKnow_5",
"animations/Stand/Gestures/IDontKnow_6",
"animations/Stand/Gestures/Joy_1",
"animations/Stand/Gestures/Kisses_1",
"animations/Stand/Gestures/Look_1",
"animations/Stand/Gestures/Look_2",
"animations/Stand/Gestures/Maybe_1",
"animations/Stand/Gestures/Me_1",
"animations/Stand/Gestures/Me_2",
"animations/Stand/Gestures/Me_4",
"animations/Stand/Gestures/Me_7",
"animations/Stand/Gestures/Me_8",
"animations/Stand/Gestures/Mime_1",
"animations/Stand/Gestures/Mime_2",
"animations/Stand/Gestures/Next_1",
"animations/Stand/Gestures/No_1",
"animations/Stand/Gestures/No_2",
"animations/Stand/Gestures/No_3",
"animations/Stand/Gestures/No_4",
"animations/Stand/Gestures/No_5",
"animations/Stand/Gestures/No_6",
"animations/Stand/Gestures/No_7",
"animations/Stand/Gestures/No_8",
"animations/Stand/Gestures/No_9",
"animations/Stand/Gestures/Nothing_1",
"animations/Stand/Gestures/Nothing_2",
"animations/Stand/Gestures/OnTheEvening_1",
"animations/Stand/Gestures/OnTheEvening_2",
"animations/Stand/Gestures/OnTheEvening_3",
"animations/Stand/Gestures/OnTheEvening_4",
"animations/Stand/Gestures/OnTheEvening_5",
"animations/Stand/Gestures/Please_1",
"animations/Stand/Gestures/Please_2",
"animations/Stand/Gestures/Please_3",
"animations/Stand/Gestures/Reject_1",
"animations/Stand/Gestures/Reject_2",
"animations/Stand/Gestures/Reject_3",
"animations/Stand/Gestures/Reject_4",
"animations/Stand/Gestures/Reject_5",
"animations/Stand/Gestures/Reject_6",
"animations/Stand/Gestures/Salute_1",
"animations/Stand/Gestures/Salute_2",
"animations/Stand/Gestures/Salute_3",
"animations/Stand/Gestures/ShowFloor_1",
"animations/Stand/Gestures/ShowFloor_2",
"animations/Stand/Gestures/ShowFloor_3",
"animations/Stand/Gestures/ShowFloor_4",
"animations/Stand/Gestures/ShowFloor_5",
"animations/Stand/Gestures/ShowSky_1",
"animations/Stand/Gestures/ShowSky_10",
"animations/Stand/Gestures/ShowSky_11",
"animations/Stand/Gestures/ShowSky_12",
"animations/Stand/Gestures/ShowSky_2",
"animations/Stand/Gestures/ShowSky_3",
"animations/Stand/Gestures/ShowSky_4",
"animations/Stand/Gestures/ShowSky_5",
"animations/Stand/Gestures/ShowSky_6",
"animations/Stand/Gestures/ShowSky_7",
"animations/Stand/Gestures/ShowSky_8",
"animations/Stand/Gestures/ShowSky_9",
"animations/Stand/Gestures/ShowTablet_1",
"animations/Stand/Gestures/ShowTablet_2",
"animations/Stand/Gestures/ShowTablet_3",
"animations/Stand/Gestures/Shy_1",
"animations/Stand/Gestures/Stretch_1",
"animations/Stand/Gestures/Stretch_2",
"animations/Stand/Gestures/Surprised_1",
"animations/Stand/Gestures/TakePlace_1",
"animations/Stand/Gestures/TakePlace_2",
"animations/Stand/Gestures/Take_1",
"animations/Stand/Gestures/Thinking_1",
"animations/Stand/Gestures/Thinking_2",
"animations/Stand/Gestures/Thinking_3",
"animations/Stand/Gestures/Thinking_4",
"animations/Stand/Gestures/Thinking_5",
"animations/Stand/Gestures/Thinking_6",
"animations/Stand/Gestures/Thinking_7",
"animations/Stand/Gestures/Thinking_8",
"animations/Stand/Gestures/This_1",
"animations/Stand/Gestures/This_10",
"animations/Stand/Gestures/This_11",
"animations/Stand/Gestures/This_12",
"animations/Stand/Gestures/This_13",
"animations/Stand/Gestures/This_14",
"animations/Stand/Gestures/This_15",
"animations/Stand/Gestures/This_2",
"animations/Stand/Gestures/This_3",
"animations/Stand/Gestures/This_4",
"animations/Stand/Gestures/This_5",
"animations/Stand/Gestures/This_6",
"animations/Stand/Gestures/This_7",
"animations/Stand/Gestures/This_8",
"animations/Stand/Gestures/This_9",
"animations/Stand/Gestures/WhatSThis_1",
"animations/Stand/Gestures/WhatSThis_10",
"animations/Stand/Gestures/WhatSThis_11",
"animations/Stand/Gestures/WhatSThis_12",
"animations/Stand/Gestures/WhatSThis_13",
"animations/Stand/Gestures/WhatSThis_14",
"animations/Stand/Gestures/WhatSThis_15",
"animations/Stand/Gestures/WhatSThis_16",
"animations/Stand/Gestures/WhatSThis_2",
"animations/Stand/Gestures/WhatSThis_3",
"animations/Stand/Gestures/WhatSThis_4",
"animations/Stand/Gestures/WhatSThis_5",
"animations/Stand/Gestures/WhatSThis_6",
"animations/Stand/Gestures/WhatSThis_7",
"animations/Stand/Gestures/WhatSThis_8",
"animations/Stand/Gestures/WhatSThis_9",
"animations/Stand/Gestures/Whisper_1",
"animations/Stand/Gestures/Wings_1",
"animations/Stand/Gestures/Wings_2",
"animations/Stand/Gestures/Wings_3",
"animations/Stand/Gestures/Wings_4",
"animations/Stand/Gestures/Wings_5",
"animations/Stand/Gestures/Yes_1",
"animations/Stand/Gestures/Yes_2",
"animations/Stand/Gestures/Yes_3",
"animations/Stand/Gestures/YouKnowWhat_1",
"animations/Stand/Gestures/YouKnowWhat_2",
"animations/Stand/Gestures/YouKnowWhat_3",
"animations/Stand/Gestures/YouKnowWhat_4",
"animations/Stand/Gestures/YouKnowWhat_5",
"animations/Stand/Gestures/YouKnowWhat_6",
"animations/Stand/Gestures/You_1",
"animations/Stand/Gestures/You_2",
"animations/Stand/Gestures/You_3",
"animations/Stand/Gestures/You_4",
"animations/Stand/Gestures/You_5",
"animations/Stand/Gestures/Yum_1",
"animations/Stand/Reactions/EthernetOff_1",
"animations/Stand/Reactions/EthernetOn_1",
"animations/Stand/Reactions/Heat_1",
"animations/Stand/Reactions/Heat_2",
"animations/Stand/Reactions/LightShine_1",
"animations/Stand/Reactions/LightShine_2",
"animations/Stand/Reactions/LightShine_3",
"animations/Stand/Reactions/LightShine_4",
"animations/Stand/Reactions/SeeColor_1",
"animations/Stand/Reactions/SeeColor_2",
"animations/Stand/Reactions/SeeColor_3",
"animations/Stand/Reactions/SeeSomething_1",
"animations/Stand/Reactions/SeeSomething_3",
"animations/Stand/Reactions/SeeSomething_4",
"animations/Stand/Reactions/SeeSomething_5",
"animations/Stand/Reactions/SeeSomething_6",
"animations/Stand/Reactions/SeeSomething_7",
"animations/Stand/Reactions/SeeSomething_8",
"animations/Stand/Reactions/ShakeBody_1",
"animations/Stand/Reactions/ShakeBody_2",
"animations/Stand/Reactions/ShakeBody_3",
"animations/Stand/Reactions/TouchHead_1",
"animations/Stand/Reactions/TouchHead_2",
"animations/Stand/Reactions/TouchHead_3",
"animations/Stand/Reactions/TouchHead_4",
"animations/Stand/Waiting/AirGuitar_1",
"animations/Stand/Waiting/BackRubs_1",
"animations/Stand/Waiting/Bandmaster_1",
"animations/Stand/Waiting/Binoculars_1",
"animations/Stand/Waiting/BreathLoop_1",
"animations/Stand/Waiting/BreathLoop_2",
"animations/Stand/Waiting/BreathLoop_3",
"animations/Stand/Waiting/CallSomeone_1",
"animations/Stand/Waiting/Drink_1",
"animations/Stand/Waiting/DriveCar_1",
"animations/Stand/Waiting/Fitness_1",
"animations/Stand/Waiting/Fitness_2",
"animations/Stand/Waiting/Fitness_3",
"animations/Stand/Waiting/FunnyDancer_1",
"animations/Stand/Waiting/HappyBirthday_1",
"animations/Stand/Waiting/Helicopter_1",
"animations/Stand/Waiting/HideEyes_1",
"animations/Stand/Waiting/HideHands_1",
"animations/Stand/Waiting/Innocent_1",
"animations/Stand/Waiting/Knight_1",
"animations/Stand/Waiting/KnockEye_1",
"animations/Stand/Waiting/KungFu_1",
"animations/Stand/Waiting/LookHand_1",
"animations/Stand/Waiting/LookHand_2",
"animations/Stand/Waiting/LoveYou_1",
"animations/Stand/Waiting/Monster_1",
"animations/Stand/Waiting/MysticalPower_1",
"animations/Stand/Waiting/PlayHands_1",
"animations/Stand/Waiting/PlayHands_2",
"animations/Stand/Waiting/PlayHands_3",
"animations/Stand/Waiting/Relaxation_1",
"animations/Stand/Waiting/Relaxation_2",
"animations/Stand/Waiting/Relaxation_3",
"animations/Stand/Waiting/Relaxation_4",
"animations/Stand/Waiting/Rest_1",
"animations/Stand/Waiting/Robot_1",
"animations/Stand/Waiting/ScratchBack_1",
"animations/Stand/Waiting/ScratchBottom_1",
"animations/Stand/Waiting/ScratchEye_1",
"animations/Stand/Waiting/ScratchHand_1",
"animations/Stand/Waiting/ScratchHead_1",
"animations/Stand/Waiting/ScratchLeg_1",
"animations/Stand/Waiting/ScratchTorso_1",
"animations/Stand/Waiting/ShowMuscles_1",
"animations/Stand/Waiting/ShowMuscles_2",
"animations/Stand/Waiting/ShowMuscles_3",
"animations/Stand/Waiting/ShowMuscles_4",
"animations/Stand/Waiting/ShowMuscles_5",
"animations/Stand/Waiting/ShowSky_1",
"animations/Stand/Waiting/ShowSky_2",
"animations/Stand/Waiting/SpaceShuttle_1",
"animations/Stand/Waiting/Stretch_1",
"animations/Stand/Waiting/Stretch_2",
"animations/Stand/Waiting/TakePicture_1",
"animations/Stand/Waiting/Taxi_1",
"animations/Stand/Waiting/Think_1",
"animations/Stand/Waiting/Think_2",
"animations/Stand/Waiting/Think_3",
"animations/Stand/Waiting/Think_4",
"animations/Stand/Waiting/Waddle_1",
"animations/Stand/Waiting/Waddle_2",
"animations/Stand/Waiting/WakeUp_1",
"animations/Stand/Waiting/Zombie_1"]
/**
* Returns a gesture value editor component.
* @returns JSX.Element
*/
export default function GestureValueEditor({
value,
setValue,
setType,
placeholder = "Gesture name",
}: GestureValueEditorProps) {
/** Input mode: semantic tag vs concrete animation path */
const [mode, setMode] = useState<"single" | "tag">("tag");
/** Raw text value for single-gesture input */
const [customValue, setCustomValue] = useState("");
/** Autocomplete dropdown state */
const [showSuggestions, setShowSuggestions] = useState(true);
const [filteredSuggestions, setFilteredSuggestions] = useState<string[]>([]);
/** Reserved for future click-outside / positioning logic */
const containerRef = useRef<HTMLDivElement>(null);
/** Switch between tag and single input modes */
const handleModeChange = (newMode: "single" | "tag") => {
setMode(newMode);
if (newMode === "single") {
setValue(customValue || value);
setType(false);
setFilteredSuggestions(GESTURE_SINGLES);
setShowSuggestions(true);
} else {
// Clear value if it does not match a valid tag
setType(true);
const isValidTag = GESTURE_TAGS.some(
tag => tag.toLowerCase() === value.toLowerCase()
);
if (!isValidTag) setValue("");
setShowSuggestions(false);
}
};
/** Select a semantic gesture tag */
const handleTagSelect = (tag: string) => {
setValue(tag);
};
/** Update single-gesture input and filter suggestions */
const handleCustomChange = (newValue: string) => {
setCustomValue(newValue);
setValue(newValue);
if (newValue.trim() === "") {
setFilteredSuggestions(GESTURE_SINGLES);
setShowSuggestions(true);
} else {
const filtered = GESTURE_SINGLES.filter(single =>
single.toLowerCase().includes(newValue.toLowerCase())
);
setFilteredSuggestions(filtered);
setShowSuggestions(filtered.length > 0);
}
};
/** Commit autocomplete selection */
const handleSuggestionSelect = (suggestion: string) => {
setCustomValue(suggestion);
setValue(suggestion);
setShowSuggestions(false);
};
/** Refresh suggestions on refocus */
const handleInputFocus = () => {
if (!customValue.trim()) return;
const filtered = GESTURE_SINGLES.filter(single =>
single.toLowerCase().includes(customValue.toLowerCase())
);
setFilteredSuggestions(filtered);
setShowSuggestions(filtered.length > 0);
};
/** Exists to allow delayed blur handling if needed */
const handleInputBlur = (_e: React.FocusEvent) => {};
/** Build the JSX component */
return (
<div className={styles.gestureEditor} ref={containerRef}>
{/* Mode toggle */}
<div className={styles.modeSelector}>
<label className={styles.modeLabel}>Input Mode:</label>
<div className={styles.toggleContainer}>
<button
type="button"
className={`${styles.toggleButton} ${mode === "single" ? styles.active : ""}`}
onClick={() => handleModeChange("single")}
>
Single
</button>
<button
type="button"
className={`${styles.toggleButton} ${mode === "tag" ? styles.active : ""}`}
onClick={() => handleModeChange("tag")}
>
Tag
</button>
</div>
</div>
<div className={styles.valueEditor} data-testid={"valueEditorTestID"}>
{mode === "single" ? (
<div className={styles.autocompleteContainer}>
{showSuggestions && (
<div className={styles.suggestionsDropdownLeft}>
{filteredSuggestions.map((suggestion) => (
<div
key={suggestion}
className={styles.suggestionItem}
onClick={() => handleSuggestionSelect(suggestion)}
onMouseDown={(e) => e.preventDefault()} // prevent blur before click
>
{suggestion}
</div>
))}
</div>
)}
<input
type="text"
value={customValue}
onChange={(e) => handleCustomChange(e.target.value)}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
placeholder={placeholder}
className={`${styles.textInput} ${showSuggestions ? styles.textInputWithSuggestions : ''}`}
autoComplete="off"
/>
</div>
) : (
<div className={styles.tagSelector}>
<select
value={value}
onChange={(e) => handleTagSelect(e.target.value)}
className={styles.tagSelect}
data-testid={"tagSelectorTestID"}
>
<option value="" >Select a gesture tag...</option>
{GESTURE_TAGS.map((tag) => (
<option key={tag} value={tag}>{tag}</option>
))}
</select>
<div className={styles.tagList}>
{GESTURE_TAGS.map((tag) => (
<button
key={tag}
type="button"
className={`${styles.tagButton} ${value === tag ? styles.selected : ""}`}
onClick={() => handleTagSelect(tag)}
>
{tag}
</button>
))}
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
// 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 type { Plan, PlanElement } from "./Plan";
export const defaultPlan: Plan = {
name: "Default Plan",
id: "-1",
steps: [] as PlanElement[],
}

View File

@@ -0,0 +1,129 @@
// 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 { type Node } from "@xyflow/react"
import { GoalReduce } from "../../nodes/GoalNode"
export type Plan = {
name: string,
id: string,
steps: PlanElement[],
}
export type PlanElement = Goal | Action
export type Goal = {
id: string // we let the reducer figure out the rest dynamically
type: "goal"
}
// Actions
export type Action = SpeechAction | GestureAction | LLMAction
export type SpeechAction = { id: string, text: string, type:"speech" }
export type GestureAction = { id: string, gesture: string, isTag: boolean, type:"gesture" }
export type LLMAction = { id: string, goal: string, type:"llm" }
export type ActionTypes = "speech" | "gesture" | "llm";
// Extract the wanted information from a plan within the reducing of nodes
export function PlanReduce(_nodes: Node[], plan?: Plan, ) {
if (!plan) return ""
return {
id: plan.id,
steps: plan.steps.map((x) => StepReduce(x, _nodes))
}
}
// Extract the wanted information from a plan element.
function StepReduce(planElement: PlanElement, _nodes: Node[]) : Record<string, unknown> {
// We have different types of plan elements, requiring differnt types of output
const nodes = _nodes
const thisNode = _nodes.find((x) => x.id === planElement.id)
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 thisNode ? GoalReduce(thisNode, nodes) : {}
}
}
/**
* Finds out whether the plan can iterate multiple times, or always stops after one action.
* This comes down to checking if the plan only has speech/ gesture actions, or others as well.
* @param plan: the plan to check
* @returns: a boolean
*/
export function DoesPlanIterate( _nodes: Node[], plan?: Plan,) : boolean {
// TODO: should recursively check plans that have goals (and thus more plans) in them.
if (!plan) return false
return plan.steps.filter((step) => step.type == "llm").length > 0 ||
(
// Find the goal node of this step
plan.steps.filter((step) => step.type == "goal").map((goalStep) => {
const goalId = goalStep.id;
const goalNode = _nodes.find((x) => x.id === goalId);
// In case we don't find any valid plan, this node doesn't iterate
if (!goalNode || !goalNode.data.plan) return false;
// Otherwise, check if this node can fail - if so, we should have the option to iterate
return (goalNode && goalNode.data.plan && goalNode.data.can_fail)
})
).includes(true);
}
/**
* Checks if any of the plan's goal steps has its can_fail value set to true.
* @param plan: plan to check
* @param _nodes: nodes in flow store.
*/
export function HasCheckingSubGoal(plan: Plan, _nodes: Node[]) {
const goalSteps = plan.steps.filter((x) => x.type == "goal");
return goalSteps.map((goalStep) => {
// Find the goal node and check its can_fail data boolean.
const goalId = goalStep.id;
const goalNode = _nodes.find((x) => x.id === goalId);
return (goalNode && goalNode.data.can_fail)
}).includes(true);
}
/**
* Returns the value of the action.
* Since typescript can't polymorphicly access the value field,
* we need to switch over the types and return the correct field.
* @param action: action to retrieve the value from
* @returns string | undefined
*/
export function GetActionValue(action: Action) {
let returnAction;
switch (action.type) {
case "gesture":
returnAction = action as GestureAction
return returnAction.gesture;
case "speech":
returnAction = action as SpeechAction
return returnAction.text;
case "llm":
returnAction = action as LLMAction
return returnAction.goal;
default:
}
}

View File

@@ -0,0 +1,39 @@
// 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)
// This file is to avoid sharing both functions and components which eslint dislikes. :)
import type { GoalNode } from "../../nodes/GoalNode"
import type { Goal, Plan } from "./Plan"
/**
* Inserts a goal into a plan
* @param plan: plan to insert goal into
* @param goalNode: the goal node to insert into the plan.
* @returns: a new plan with the goal inside.
*/
export function insertGoalInPlan(plan: Plan, goalNode: GoalNode): Plan {
const planElement : Goal = {
id: goalNode.id,
type: "goal",
}
return {
...plan,
steps: [...plan.steps, planElement],
}
}
/**
* Deletes a goal from a plan
* @param plan: plan to delete goal from
* @param goalID: the goal node to delete.
* @returns: a new plan with the goal removed.
*/
export function deleteGoalInPlanByID(plan: Plan, goalID: string) {
const updatedPlan = {...plan,
steps: plan.steps.filter((x) => x.id !== goalID)
}
return updatedPlan.steps.length == 0 ? undefined : updatedPlan
}

View File

@@ -0,0 +1,76 @@
/*
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)
*/
.planDialog {
overflow:visible;
width: 80vw;
max-width: 900px;
transition: width 0.25s ease;
overscroll-behavior: contain;
}
.planDialog::backdrop {
background: rgba(0, 0, 0, 0.4);
}
.planEditor {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
min-width: 600px;
}
.planEditorLeft {
position: relative;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.planEditorRight {
display: flex;
flex-direction: column;
gap: 0.5rem;
border-left: 1px solid var(--border-color, #ccc);
padding-left: 1rem;
max-height: 300px;
overflow-y: auto;
}
.planStep {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: text-decoration 0.2s;
}
.planStep:hover {
text-decoration: line-through;
}
.stepType {
opacity: 0.7;
font-size: 0.85em;
}
.stepIndex {
opacity: 0.6;
}
.emptySteps {
opacity: 0.5;
font-style: italic;
}
.stepSuggestion {
opacity: 0.5;
font-style: italic;
}

View File

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

View File

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