chore: replace plan editor functionality into own folder with own files.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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[],
|
||||
}
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user