feat: merged demo into dev #43
75
src/components/MultilineTextField.tsx
Normal file
75
src/components/MultilineTextField.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import styles from "./TextField.module.css";
|
||||||
|
|
||||||
|
export function MultilineTextField({
|
||||||
|
value = "",
|
||||||
|
setValue,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
ariaLabel,
|
||||||
|
invalid = false,
|
||||||
|
minRows = 3,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
id?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
invalid?: boolean;
|
||||||
|
minRows?: number;
|
||||||
|
}) {
|
||||||
|
const [readOnly, setReadOnly] = useState(true);
|
||||||
|
const [inputValue, setInputValue] = useState(value);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
// Auto-grow logic
|
||||||
|
useEffect(() => {
|
||||||
|
const el = textareaRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
el.style.height = "auto";
|
||||||
|
el.style.height = `${el.scrollHeight}px`;
|
||||||
|
}, [inputValue]);
|
||||||
|
|
||||||
|
const onCommit = () => {
|
||||||
|
setReadOnly(true);
|
||||||
|
setValue(inputValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
(e.target as HTMLTextAreaElement).blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
rows={minRows}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onFocus={() => setReadOnly(false)}
|
||||||
|
onBlur={onCommit}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
readOnly={readOnly}
|
||||||
|
id={id}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={`
|
||||||
|
${readOnly ? "drag" : "nodrag"}
|
||||||
|
flex-1
|
||||||
|
${styles.textField}
|
||||||
|
${styles.multiline}
|
||||||
|
${invalid ? styles.invalid : ""}
|
||||||
|
${className ?? ""}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 5pt;
|
border-radius: 5pt;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
max-width: 50vw;
|
||||||
|
min-width: 10vw;
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: canvas;
|
background-color: canvas;
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
@@ -25,3 +27,13 @@
|
|||||||
.text-field:read-only:hover:not(.invalid) {
|
.text-field:read-only:hover:not(.invalid) {
|
||||||
border-color: color-mix(in srgb, canvas, #777 10%);
|
border-color: color-mix(in srgb, canvas, #777 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multiline {
|
||||||
|
resize: none; /* no manual resizing */
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow: hidden; /* needed for auto-grow */
|
||||||
|
max-width: 100%;
|
||||||
|
width: 95%;
|
||||||
|
min-width: 95%;
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ export function RealtimeTextField({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
id={id}
|
id={id}
|
||||||
// ReactFlow uses the "drag" / "nodrag" classes to enable / disable dragging of nodes
|
// ReactFlow uses the "drag" / "nodrag" classes to enable / disable dragging of nodes
|
||||||
className={`${readOnly ? "drag" : "nodrag"} ${styles.textField} ${invalid ? styles.invalid : ""} ${className}`}
|
className={`${readOnly ? "drag" : "nodrag"} flex-1 ${styles.textField} ${invalid ? styles.invalid : ""} ${className}`}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,8 +59,21 @@ button:focus-visible {
|
|||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
|
||||||
--accent-color: #00AAAA;
|
--accent-color: #00AAAA;
|
||||||
|
--select-color: rgba(gray);
|
||||||
|
|
||||||
|
--dropdown-menu-background-color: rgb(247, 247, 247);
|
||||||
|
--dropdown-menu-border: rgba(207, 207, 207, 0.986);
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
color: #ffffff;
|
||||||
|
--select-color: rgba(gray);
|
||||||
|
--dropdown-menu-background-color: rgba(39, 39, 39, 0.986);
|
||||||
|
--dropdown-menu-border: rgba(65, 65, 65, 0.986);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -139,3 +139,9 @@
|
|||||||
outline: plum solid 2pt;
|
outline: plum solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem plum);
|
filter: drop-shadow(0 0 0.25rem plum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.planNoIterate {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-style: italic;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
@@ -51,7 +51,8 @@ const selector = (state: FlowState) => ({
|
|||||||
undo: state.undo,
|
undo: state.undo,
|
||||||
redo: state.redo,
|
redo: state.redo,
|
||||||
beginBatchAction: state.beginBatchAction,
|
beginBatchAction: state.beginBatchAction,
|
||||||
endBatchAction: state.endBatchAction
|
endBatchAction: state.endBatchAction,
|
||||||
|
scrollable: state.scrollable
|
||||||
});
|
});
|
||||||
|
|
||||||
// --| define ReactFlow editor |--
|
// --| define ReactFlow editor |--
|
||||||
@@ -75,7 +76,8 @@ const VisProgUI = () => {
|
|||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
beginBatchAction,
|
beginBatchAction,
|
||||||
endBatchAction
|
endBatchAction,
|
||||||
|
scrollable
|
||||||
} = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore
|
} = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore
|
||||||
|
|
||||||
// adds ctrl+z and ctrl+y support to respectively undo and redo actions
|
// adds ctrl+z and ctrl+y support to respectively undo and redo actions
|
||||||
@@ -104,6 +106,7 @@ const VisProgUI = () => {
|
|||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onNodeDragStart={beginBatchAction}
|
onNodeDragStart={beginBatchAction}
|
||||||
onNodeDragStop={endBatchAction}
|
onNodeDragStop={endBatchAction}
|
||||||
|
preventScrolling={scrollable}
|
||||||
snapToGrid
|
snapToGrid
|
||||||
fitView
|
fitView
|
||||||
proOptions={{hideAttribution: true}}
|
proOptions={{hideAttribution: true}}
|
||||||
|
|||||||
@@ -41,16 +41,15 @@ function createNode(id: string, type: string, position: XYPosition, data: Record
|
|||||||
...data,
|
...data,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//* Initial nodes, created by using createNode. */
|
||||||
|
// Start and End don't need to apply the UUID, since they are technically never compiled into a program.
|
||||||
|
const startNode = createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false)
|
||||||
|
const endNode = createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false)
|
||||||
|
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:200, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
|
||||||
|
|
||||||
//* Initial nodes, created by using createNode. */
|
const initialNodes : Node[] = [startNode, endNode, initialPhaseNode,];
|
||||||
const initialNodes : Node[] = [
|
|
||||||
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false),
|
|
||||||
createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false),
|
|
||||||
createNode('phase-1', 'phase', {x:200, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null}),
|
|
||||||
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"], critical:false}),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Initial edges, leave empty as setting initial edges...
|
// Initial edges, leave empty as setting initial edges...
|
||||||
// ...breaks logic that is dependent on connection events
|
// ...breaks logic that is dependent on connection events
|
||||||
@@ -72,6 +71,15 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
|||||||
nodes: initialNodes,
|
nodes: initialNodes,
|
||||||
edges: initialEdges,
|
edges: initialEdges,
|
||||||
edgeReconnectSuccessful: true,
|
edgeReconnectSuccessful: true,
|
||||||
|
scrollable: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handles changing the scrollable state of the editor,
|
||||||
|
* this is used to control if scrolling is captured by the editor
|
||||||
|
* or if it's available to other components within the reactFlowProvider
|
||||||
|
* @param {boolean} val - the desired state
|
||||||
|
*/
|
||||||
|
setScrollable: (val) => set({scrollable: val}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles changes to nodes triggered by ReactFlow.
|
* Handles changes to nodes triggered by ReactFlow.
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ export type FlowState = {
|
|||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
edgeReconnectSuccessful: boolean;
|
edgeReconnectSuccessful: boolean;
|
||||||
|
scrollable: boolean;
|
||||||
|
|
||||||
|
/** Handler for managing scrollable state */
|
||||||
|
setScrollable: (value: boolean) => void;
|
||||||
|
|
||||||
/** Handler for changes to nodes triggered by ReactFlow */
|
/** Handler for changes to nodes triggered by ReactFlow */
|
||||||
onNodesChange: OnNodesChange;
|
onNodesChange: OnNodesChange;
|
||||||
|
|||||||
@@ -69,23 +69,11 @@ function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeP
|
|||||||
* @param position - The XY position in the flow canvas where the node will appear.
|
* @param position - The XY position in the flow canvas where the node will appear.
|
||||||
*/
|
*/
|
||||||
function addNodeToFlow(nodeType: keyof typeof NodeTypes, position: XYPosition) {
|
function addNodeToFlow(nodeType: keyof typeof NodeTypes, position: XYPosition) {
|
||||||
const { nodes, addNode } = useFlowStore.getState();
|
const { addNode } = useFlowStore.getState();
|
||||||
|
|
||||||
// Load any predefined data for this node type.
|
// Load any predefined data for this node type.
|
||||||
const defaultData = NodeDefaults[nodeType] ?? {}
|
const defaultData = NodeDefaults[nodeType] ?? {}
|
||||||
|
const id = crypto.randomUUID();
|
||||||
// Currently, we find out what the Id is by checking the last node and adding one.
|
|
||||||
const sameTypeNodes = nodes.filter((node) => node.type === nodeType);
|
|
||||||
const nextNumber =
|
|
||||||
sameTypeNodes.length > 0
|
|
||||||
? (() => {
|
|
||||||
const lastNode = sameTypeNodes[sameTypeNodes.length - 1];
|
|
||||||
const parts = lastNode.id.split('-');
|
|
||||||
const lastNum = Number(parts[1]);
|
|
||||||
return Number.isNaN(lastNum) ? sameTypeNodes.length + 1 : lastNum + 1;
|
|
||||||
})()
|
|
||||||
: 1;
|
|
||||||
const id = `${nodeType}-${nextNumber}`;
|
|
||||||
|
|
||||||
// Create new node
|
// Create new node
|
||||||
const newNode = {
|
const newNode = {
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
|
||||||
|
.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,611 @@
|
|||||||
|
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,7 @@
|
|||||||
|
import type { Plan } from "./Plan";
|
||||||
|
|
||||||
|
export const defaultPlan: Plan = {
|
||||||
|
name: "Default Plan",
|
||||||
|
id: "-1",
|
||||||
|
steps: [],
|
||||||
|
}
|
||||||
101
src/pages/VisProgPage/visualProgrammingUI/components/Plan.tsx
Normal file
101
src/pages/VisProgPage/visualProgrammingUI/components/Plan.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
export type Plan = {
|
||||||
|
name: string,
|
||||||
|
id: string,
|
||||||
|
steps: PlanElement[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlanElement = Goal | Action
|
||||||
|
|
||||||
|
export type Goal = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
plan: Plan,
|
||||||
|
can_fail: boolean,
|
||||||
|
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(plan?: Plan) {
|
||||||
|
if (!plan) return ""
|
||||||
|
return {
|
||||||
|
id: plan.id,
|
||||||
|
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:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,71 @@
|
|||||||
|
.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,243 @@
|
|||||||
|
import {useRef, useState} from "react";
|
||||||
|
import useFlowStore from "../VisProgStores.tsx";
|
||||||
|
import styles from './PlanEditor.module.css';
|
||||||
|
import { GetActionValue, type Action, type ActionTypes, type Plan } from "../components/Plan";
|
||||||
|
import { defaultPlan } from "../components/Plan.default";
|
||||||
|
import { TextField } from "../../../../components/TextField";
|
||||||
|
import GestureValueEditor from "./GestureValueEditor";
|
||||||
|
|
||||||
|
type PlanEditorDialogProps = {
|
||||||
|
plan?: Plan;
|
||||||
|
onSave: (plan: Plan | undefined) => void;
|
||||||
|
description? : string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 { setScrollable } = useFlowStore();
|
||||||
|
|
||||||
|
//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 buildAction = (): Action => {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
switch (newActionType) {
|
||||||
|
case "speech":
|
||||||
|
return { id, text: newActionValue, type: "speech" };
|
||||||
|
case "gesture":
|
||||||
|
return { id, gesture: newActionValue, isTag: newActionGestureType, type: "gesture" };
|
||||||
|
case "llm":
|
||||||
|
return { id, goal: newActionValue, type: "llm" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
{/* Create and edit buttons */}
|
||||||
|
{!plan && (
|
||||||
|
<button className={styles.nodeButton} onClick={description ? openCreateWithDescription : openCreate}>
|
||||||
|
Create Plan
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{plan && (
|
||||||
|
<button className={styles.nodeButton} onClick={openEdit}>
|
||||||
|
Edit Plan
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Start of dialog (plan editor) */}
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
className={`${styles.planDialog}`}
|
||||||
|
//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) */}
|
||||||
|
<h4>Add Action</h4>
|
||||||
|
{(!plan && description && draftPlan.steps.length === 0) && (<div className={styles.stepSuggestion}>
|
||||||
|
<label> Filled in as a suggestion! </label>
|
||||||
|
<label> Feel free to change! </label>
|
||||||
|
</div>)}
|
||||||
|
<label>
|
||||||
|
Action Type <wbr />
|
||||||
|
{/* Type selection */}
|
||||||
|
<select
|
||||||
|
value={newActionType}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNewActionType(e.target.value as ActionTypes);
|
||||||
|
// Reset value when action type changes
|
||||||
|
setNewActionValue("");
|
||||||
|
}}>
|
||||||
|
<option value="speech">Speech Action</option>
|
||||||
|
<option value="gesture">Gesture Action</option>
|
||||||
|
<option value="llm">LLM Action</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Action value editor*/}
|
||||||
|
{newActionType === "gesture" ? (
|
||||||
|
// Gesture get their own editor component
|
||||||
|
<GestureValueEditor
|
||||||
|
value={newActionValue}
|
||||||
|
setValue={setNewActionValue}
|
||||||
|
setType={setNewActionGestureType}
|
||||||
|
placeholder="Gesture name"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
value={newActionValue}
|
||||||
|
setValue={setNewActionValue}
|
||||||
|
placeholder={
|
||||||
|
newActionType === "speech" ? "Speech text"
|
||||||
|
: "LLM goal"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Adding steps */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!newActionValue}
|
||||||
|
onClick={() => {
|
||||||
|
if (!draftPlan) return;
|
||||||
|
// Add action to steps
|
||||||
|
const action = buildAction();
|
||||||
|
setDraftPlan({
|
||||||
|
...draftPlan,
|
||||||
|
steps: [...draftPlan.steps, action],});
|
||||||
|
|
||||||
|
// Reset current action building
|
||||||
|
setNewActionValue("");
|
||||||
|
setNewActionType("speech");
|
||||||
|
}}>
|
||||||
|
Add Step
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side (Steps shown) */}
|
||||||
|
<div className={styles.planEditorRight}>
|
||||||
|
<h4>Steps</h4>
|
||||||
|
|
||||||
|
{/* Show if there are no steps yet */}
|
||||||
|
{draftPlan.steps.length === 0 && (
|
||||||
|
<div className={styles.emptySteps}>
|
||||||
|
No steps yet
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Map over all steps */}
|
||||||
|
{draftPlan.steps.map((step, index) => (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
key={step.id}
|
||||||
|
className={styles.planStep}
|
||||||
|
// Extra logic for screen readers to access using keyboard
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
setDraftPlan({
|
||||||
|
...draftPlan,
|
||||||
|
steps: draftPlan.steps.filter((s) => s.id !== step.id),});
|
||||||
|
}}}
|
||||||
|
onClick={() => {
|
||||||
|
setDraftPlan({
|
||||||
|
...draftPlan,
|
||||||
|
steps: draftPlan.steps.filter((s) => s.id !== step.id),});
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<span className={styles.stepIndex}>{index + 1}.</span>
|
||||||
|
<span className={styles.stepType}>{step.type}:</span>
|
||||||
|
<span className={styles.stepName}>{
|
||||||
|
step.type == "goal" ? ""/* TODO: Add support for goals */
|
||||||
|
: GetActionValue(step)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,6 @@ import type { BasicBeliefNodeData } from "./BasicBeliefNode";
|
|||||||
export const BasicBeliefNodeDefaults: BasicBeliefNodeData = {
|
export const BasicBeliefNodeDefaults: BasicBeliefNodeData = {
|
||||||
label: "Belief",
|
label: "Belief",
|
||||||
droppable: true,
|
droppable: true,
|
||||||
belief: {type: "keyword", id: "help", value: "help", label: "Keyword said:"},
|
belief: {type: "keyword", id: "", value: "", label: "Keyword said:"},
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
};
|
};
|
||||||
@@ -9,6 +9,7 @@ import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
|||||||
import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
import { TextField } from '../../../../components/TextField';
|
import { TextField } from '../../../../components/TextField';
|
||||||
|
import { MultilineTextField } from '../../../../components/MultilineTextField';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default data structure for a BasicBelief node
|
* The default data structure for a BasicBelief node
|
||||||
@@ -32,7 +33,7 @@ export type BasicBeliefNodeData = {
|
|||||||
// These are all the types a basic belief could be.
|
// These are all the types a basic belief could be.
|
||||||
type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion
|
type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion
|
||||||
type Keyword = { type: "keyword", id: string, value: string, label: "Keyword said:"};
|
type Keyword = { type: "keyword", id: string, value: string, label: "Keyword said:"};
|
||||||
type Semantic = { type: "semantic", id: string, value: string, label: "Detected with LLM:"};
|
type Semantic = { type: "semantic", id: string, value: string, description: string, label: "Detected with LLM:"};
|
||||||
type DetectedObject = { type: "object", id: string, value: string, label: "Object found:"};
|
type DetectedObject = { type: "object", id: string, value: string, label: "Object found:"};
|
||||||
type Emotion = { type: "emotion", id: string, value: string, label: "Emotion recognised:"};
|
type Emotion = { type: "emotion", id: string, value: string, label: "Emotion recognised:"};
|
||||||
|
|
||||||
@@ -103,6 +104,10 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const setBeliefDescription = (value: string) => {
|
||||||
|
updateNodeData(props.id, {...data, belief: {...data.belief, description: value}});
|
||||||
|
}
|
||||||
|
|
||||||
// Use this
|
// Use this
|
||||||
const emotionOptions = ["Happy", "Angry", "Sad", "Cheerful"]
|
const emotionOptions = ["Happy", "Angry", "Sad", "Cheerful"]
|
||||||
|
|
||||||
@@ -115,7 +120,7 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
|||||||
wrapping = '"'
|
wrapping = '"'
|
||||||
break;
|
break;
|
||||||
case ("semantic"):
|
case ("semantic"):
|
||||||
placeholder = "word..."
|
placeholder = "description..."
|
||||||
wrapping = '"'
|
wrapping = '"'
|
||||||
break;
|
break;
|
||||||
case ("object"):
|
case ("object"):
|
||||||
@@ -148,7 +153,6 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
|||||||
<option value="emotion">Emotion recognised:</option>
|
<option value="emotion">Emotion recognised:</option>
|
||||||
</select>
|
</select>
|
||||||
{wrapping}
|
{wrapping}
|
||||||
|
|
||||||
{data.belief.type === "emotion" && (
|
{data.belief.type === "emotion" && (
|
||||||
<select
|
<select
|
||||||
value={data.belief.value}
|
value={data.belief.value}
|
||||||
@@ -162,7 +166,6 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
|||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{data.belief.type !== "emotion" &&
|
{data.belief.type !== "emotion" &&
|
||||||
(<TextField
|
(<TextField
|
||||||
id={label_input_id}
|
id={label_input_id}
|
||||||
@@ -172,6 +175,15 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
|||||||
/>)}
|
/>)}
|
||||||
{wrapping}
|
{wrapping}
|
||||||
</div>
|
</div>
|
||||||
|
{data.belief.type === "semantic" && (
|
||||||
|
<div className={"flex-wrap padding-sm"}>
|
||||||
|
<MultilineTextField
|
||||||
|
value={data.belief.description}
|
||||||
|
setValue={setBeliefDescription}
|
||||||
|
placeholder={"Describe the desciption of this LLM belief..."}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||||
allowOnlyConnectionsFromType(["norm", "trigger"]),
|
allowOnlyConnectionsFromType(["norm", "trigger"]),
|
||||||
]}/>
|
]}/>
|
||||||
@@ -188,9 +200,26 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
|||||||
*/
|
*/
|
||||||
export function BasicBeliefReduce(node: Node, _nodes: Node[]) {
|
export function BasicBeliefReduce(node: Node, _nodes: Node[]) {
|
||||||
const data = node.data as BasicBeliefNodeData;
|
const data = node.data as BasicBeliefNodeData;
|
||||||
return {
|
const result: Record<string, unknown> = {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: data.belief.type,
|
};
|
||||||
value: data.belief.value
|
|
||||||
|
switch (data.belief.type) {
|
||||||
|
case "emotion":
|
||||||
|
result["emotion"] = data.belief.value;
|
||||||
|
break;
|
||||||
|
case "keyword":
|
||||||
|
result["keyword"] = data.belief.value;
|
||||||
|
break;
|
||||||
|
case "object":
|
||||||
|
result["object"] = data.belief.value;
|
||||||
|
break;
|
||||||
|
case "semantic":
|
||||||
|
result["name"] = data.belief.value;
|
||||||
|
result["description"] = data.belief.description;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,10 @@ import type { GoalNodeData } from "./GoalNode";
|
|||||||
*/
|
*/
|
||||||
export const GoalNodeDefaults: GoalNodeData = {
|
export const GoalNodeDefaults: GoalNodeData = {
|
||||||
label: "Goal Node",
|
label: "Goal Node",
|
||||||
|
name: "",
|
||||||
droppable: true,
|
droppable: true,
|
||||||
description: "The robot will strive towards this goal",
|
description: "",
|
||||||
achieved: false,
|
achieved: false,
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
|
can_fail: false,
|
||||||
};
|
};
|
||||||
@@ -9,20 +9,28 @@ import { TextField } from '../../../../components/TextField';
|
|||||||
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
|
import { DoesPlanIterate, PlanReduce, type Plan } from '../components/Plan';
|
||||||
|
import PlanEditorDialog from '../components/PlanEditor';
|
||||||
|
import { MultilineTextField } from '../../../../components/MultilineTextField';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default data dot a phase node
|
* The default data dot a phase node
|
||||||
* @param label: the label of this phase
|
* @param label: the label of this phase
|
||||||
* @param droppable: whether this node is droppable from the drop bar (initialized as true)
|
* @param droppable: whether this node is droppable from the drop bar (initialized as true)
|
||||||
* @param desciption: description of the goal
|
* @param desciption: description of the goal - this will be checked for completion
|
||||||
* @param hasReduce: whether this node has reducing functionality (true by default)
|
* @param hasReduce: whether this node has reducing functionality (true by default)
|
||||||
|
* @param can_fail: whether this plan should be checked- this plan could possible fail
|
||||||
|
* @param plan: The (possible) attached plan to this goal
|
||||||
*/
|
*/
|
||||||
export type GoalNodeData = {
|
export type GoalNodeData = {
|
||||||
label: string;
|
label: string;
|
||||||
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
droppable: boolean;
|
droppable: boolean;
|
||||||
achieved: boolean;
|
achieved: boolean;
|
||||||
hasReduce: boolean;
|
hasReduce: boolean;
|
||||||
|
can_fail: boolean;
|
||||||
|
plan?: Plan;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GoalNode = Node<GoalNodeData>
|
export type GoalNode = Node<GoalNodeData>
|
||||||
@@ -38,13 +46,18 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
|||||||
|
|
||||||
const text_input_id = `goal_${id}_text_input`;
|
const text_input_id = `goal_${id}_text_input`;
|
||||||
const checkbox_id = `goal_${id}_checkbox`;
|
const checkbox_id = `goal_${id}_checkbox`;
|
||||||
|
const planIterate = DoesPlanIterate(data.plan);
|
||||||
|
|
||||||
const setDescription = (value: string) => {
|
const setDescription = (value: string) => {
|
||||||
updateNodeData(id, {...data, description: value});
|
updateNodeData(id, {...data, description: value});
|
||||||
}
|
}
|
||||||
|
|
||||||
const setAchieved = (value: boolean) => {
|
const setName= (value: string) => {
|
||||||
updateNodeData(id, {...data, achieved: value});
|
updateNodeData(id, {...data, name: value})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFailable = (value: boolean) => {
|
||||||
|
updateNodeData(id, {...data, can_fail: value});
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
@@ -54,18 +67,49 @@ export default function GoalNode({id, data}: NodeProps<GoalNode>) {
|
|||||||
<label htmlFor={text_input_id}>Goal:</label>
|
<label htmlFor={text_input_id}>Goal:</label>
|
||||||
<TextField
|
<TextField
|
||||||
id={text_input_id}
|
id={text_input_id}
|
||||||
value={data.description}
|
value={data.name}
|
||||||
setValue={(val) => setDescription(val)}
|
setValue={(val) => setName(val)}
|
||||||
placeholder={"To ..."}
|
placeholder={"To ..."}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex-row gap-md align-center"}>
|
|
||||||
<label htmlFor={checkbox_id}>Achieved:</label>
|
{data.can_fail && (<div>
|
||||||
|
<label htmlFor={text_input_id}>Description/ Condition of goal:</label>
|
||||||
|
<div className={"flex-wrap"}>
|
||||||
|
<MultilineTextField
|
||||||
|
id={text_input_id}
|
||||||
|
value={data.description}
|
||||||
|
setValue={setDescription}
|
||||||
|
placeholder={"Describe the condition of this goal..."}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>)}
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
{data.plan && (<div className={"flex-row gap-md align-center " + (planIterate ? "" : styles.planNoIterate)}>
|
||||||
|
{planIterate ? "" : <s></s>}
|
||||||
|
<label htmlFor={checkbox_id}>{!planIterate ? "This plan always succeeds!" : "Check if this plan fails"}:</label>
|
||||||
<input
|
<input
|
||||||
id={checkbox_id}
|
id={checkbox_id}
|
||||||
type={"checkbox"}
|
type={"checkbox"}
|
||||||
checked={data.achieved || false}
|
disabled={!planIterate}
|
||||||
onChange={(e) => setAchieved(e.target.checked)}
|
checked={!planIterate || data.can_fail}
|
||||||
|
onChange={(e) => planIterate ? setFailable(e.target.checked) : setFailable(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<PlanEditorDialog
|
||||||
|
plan={data.plan}
|
||||||
|
onSave={(plan) => {
|
||||||
|
updateNodeData(id, {
|
||||||
|
...data,
|
||||||
|
plan,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
description={data.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MultiConnectionHandle type="source" position={Position.Right} id="GoalSource" rules={[
|
<MultiConnectionHandle type="source" position={Position.Right} id="GoalSource" rules={[
|
||||||
@@ -85,9 +129,10 @@ export function GoalReduce(node: Node, _nodes: Node[]) {
|
|||||||
const data = node.data as GoalNodeData;
|
const data = node.data as GoalNodeData;
|
||||||
return {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
label: data.label,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
achieved: data.achieved,
|
can_fail: data.can_fail,
|
||||||
|
plan: data.plan ? PlanReduce(data.plan) : "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { NormNodeData } from "./NormNode";
|
|||||||
export const NormNodeDefaults: NormNodeData = {
|
export const NormNodeDefaults: NormNodeData = {
|
||||||
label: "Norm Node",
|
label: "Norm Node",
|
||||||
droppable: true,
|
droppable: true,
|
||||||
conditions: [],
|
condition: undefined,
|
||||||
norm: "",
|
norm: "",
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
critical: false,
|
critical: false,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { Toolbar } from '../components/NodeComponents';
|
import { Toolbar } from '../components/NodeComponents';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import { TextField } from '../../../../components/TextField';
|
import { TextField } from '../../../../components/TextField';
|
||||||
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
import { BasicBeliefReduce } from './BasicBeliefNode';
|
import { BasicBeliefReduce } from './BasicBeliefNode';
|
||||||
@@ -21,7 +21,7 @@ import { BasicBeliefReduce } from './BasicBeliefNode';
|
|||||||
export type NormNodeData = {
|
export type NormNodeData = {
|
||||||
label: string;
|
label: string;
|
||||||
droppable: boolean;
|
droppable: boolean;
|
||||||
conditions: string[]; // List of (basic) belief nodes' ids.
|
condition?: string; // id of this node's belief.
|
||||||
norm: string;
|
norm: string;
|
||||||
hasReduce: boolean;
|
hasReduce: boolean;
|
||||||
critical: boolean;
|
critical: boolean;
|
||||||
@@ -71,15 +71,16 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data.conditions.length > 0 && (<div className={"flex-row gap-md align-center"} data-testid="norm-condition-information">
|
|
||||||
<label htmlFor={checkbox_id}>{data.conditions.length} condition{data.conditions.length > 1 ? "s" : ""}/ belief{data.conditions.length > 1 ? "s" : ""} attached.</label>
|
{data.condition && (<div className={"flex-row gap-md align-center"} data-testid="norm-condition-information">
|
||||||
|
<label htmlFor={checkbox_id}>Condition/ Belief attached.</label>
|
||||||
</div>)}
|
</div>)}
|
||||||
|
|
||||||
|
|
||||||
<MultiConnectionHandle type="source" position={Position.Right} id="norms" rules={[
|
<MultiConnectionHandle type="source" position={Position.Right} id="norms" rules={[
|
||||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}])
|
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}])
|
||||||
]}/>
|
]}/>
|
||||||
<MultiConnectionHandle type="target" position={Position.Bottom} id="beliefs" rules={[
|
<SingleConnectionHandle type="target" position={Position.Bottom} id="beliefs" rules={[
|
||||||
allowOnlyConnectionsFromType(["basic_belief"])
|
allowOnlyConnectionsFromType(["basic_belief"])
|
||||||
]}/>
|
]}/>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,11 +97,6 @@ export function NormReduce(node: Node, nodes: Node[]) {
|
|||||||
const data = node.data as NormNodeData;
|
const data = node.data as NormNodeData;
|
||||||
|
|
||||||
// conditions nodes - make sure to check for empty arrays
|
// conditions nodes - make sure to check for empty arrays
|
||||||
let conditionNodes: Node[] = [];
|
|
||||||
if (data.conditions)
|
|
||||||
conditionNodes = nodes.filter((node) => data.conditions.includes(node.id));
|
|
||||||
|
|
||||||
// Build the result object
|
|
||||||
const result: Record<string, unknown> = {
|
const result: Record<string, unknown> = {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
label: data.label,
|
label: data.label,
|
||||||
@@ -108,12 +104,13 @@ export function NormReduce(node: Node, nodes: Node[]) {
|
|||||||
critical: data.critical,
|
critical: data.critical,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Go over our conditionNodes. They should either be Basic (OR TODO: Inferred)
|
if (data.condition) {
|
||||||
const reducer = BasicBeliefReduce;
|
const reducer = BasicBeliefReduce; // TODO: also add inferred.
|
||||||
result["basic_beliefs"] = conditionNodes.map((condition) => reducer(condition, nodes))
|
const conditionNode = nodes.find((node) => node.id === data.condition);
|
||||||
|
// In case something went wrong, and our condition doesn't actually exist;
|
||||||
// When the Inferred is being implemented, you should follow the same kind of structure that PhaseNode has,
|
if (conditionNode == undefined) return result;
|
||||||
// dividing the conditions into basic and inferred, then calling the correct reducer on them.
|
result["condition"] = reducer(conditionNode, nodes)
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +121,9 @@ export function NormReduce(node: Node, nodes: Node[]) {
|
|||||||
*/
|
*/
|
||||||
export function NormConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
export function NormConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||||
const data = _thisNode.data as NormNodeData;
|
const data = _thisNode.data as NormNodeData;
|
||||||
// If we got a belief connected, this is a condition for the norm.
|
// If we got a belief connected, this is the condition for the norm.
|
||||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
|
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
|
||||||
data.conditions.push(_sourceNodeId);
|
data.condition = _sourceNodeId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,10 +143,8 @@ export function NormConnectionSource(_thisNode: Node, _targetNodeId: string) {
|
|||||||
*/
|
*/
|
||||||
export function NormDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
export function NormDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||||
const data = _thisNode.data as NormNodeData;
|
const data = _thisNode.data as NormNodeData;
|
||||||
// If we got a belief connected, this is a condition for the norm.
|
// remove if the target of disconnection was our condition
|
||||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
|
if (_sourceNodeId == data.condition) data.condition = undefined
|
||||||
data.conditions = data.conditions.filter(id => id != _sourceNodeId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function PhaseReduce(node: Node, nodes: Node[]) {
|
|||||||
// Build the result object
|
// Build the result object
|
||||||
const result: Record<string, unknown> = {
|
const result: Record<string, unknown> = {
|
||||||
id: thisNode.id,
|
id: thisNode.id,
|
||||||
label: data.label,
|
name: data.label,
|
||||||
};
|
};
|
||||||
|
|
||||||
nodesInPhase.forEach((type) => {
|
nodesInPhase.forEach((type) => {
|
||||||
|
|||||||
@@ -6,7 +6,5 @@ import type { TriggerNodeData } from "./TriggerNode";
|
|||||||
export const TriggerNodeDefaults: TriggerNodeData = {
|
export const TriggerNodeDefaults: TriggerNodeData = {
|
||||||
label: "Trigger Node",
|
label: "Trigger Node",
|
||||||
droppable: true,
|
droppable: true,
|
||||||
triggers: [],
|
|
||||||
triggerType: "keywords",
|
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
};
|
};
|
||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import { Toolbar } from '../components/NodeComponents';
|
import { Toolbar } from '../components/NodeComponents';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||||
import useFlowStore from '../VisProgStores';
|
import useFlowStore from '../VisProgStores';
|
||||||
import { useState } from 'react';
|
import { PlanReduce, type Plan } from '../components/Plan';
|
||||||
import { RealtimeTextField, TextField } from '../../../../components/TextField';
|
import PlanEditorDialog from '../components/PlanEditor';
|
||||||
import duplicateIndices from '../../../../utils/duplicateIndices';
|
import { BasicBeliefReduce } from './BasicBeliefNode';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default data structure for a Trigger node
|
* The default data structure for a Trigger node
|
||||||
@@ -22,15 +22,13 @@ import duplicateIndices from '../../../../utils/duplicateIndices';
|
|||||||
*
|
*
|
||||||
* @property label: the display label of this Trigger node.
|
* @property label: the display label of this Trigger node.
|
||||||
* @property droppable: Whether this node can be dropped from the toolbar (default: true).
|
* @property droppable: Whether this node can be dropped from the toolbar (default: true).
|
||||||
* @property triggerType - The type of trigger ("keywords" or a custom string).
|
|
||||||
* @property triggers - The list of keyword triggers (if applicable).
|
|
||||||
* @property hasReduce - Whether this node supports reduction logic.
|
* @property hasReduce - Whether this node supports reduction logic.
|
||||||
*/
|
*/
|
||||||
export type TriggerNodeData = {
|
export type TriggerNodeData = {
|
||||||
label: string;
|
label: string;
|
||||||
droppable: boolean;
|
droppable: boolean;
|
||||||
triggerType: "keywords" | string;
|
condition?: string; // id of the belief
|
||||||
triggers: Keyword[] | never;
|
plan?: Plan;
|
||||||
hasReduce: boolean;
|
hasReduce: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,6 +41,7 @@ export type TriggerNode = Node<TriggerNodeData>
|
|||||||
*
|
*
|
||||||
* @param connection - The connection or edge being attempted to connect towards.
|
* @param connection - The connection or edge being attempted to connect towards.
|
||||||
* @returns `true` if the connection is defined; otherwise, `false`.
|
* @returns `true` if the connection is defined; otherwise, `false`.
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
export function TriggerNodeCanConnect(connection: Connection | Edge): boolean {
|
export function TriggerNodeCanConnect(connection: Connection | Edge): boolean {
|
||||||
return (connection != undefined);
|
return (connection != undefined);
|
||||||
@@ -57,25 +56,29 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
|||||||
const data = props.data;
|
const data = props.data;
|
||||||
const {updateNodeData} = useFlowStore();
|
const {updateNodeData} = useFlowStore();
|
||||||
|
|
||||||
const setKeywords = (keywords: Keyword[]) => {
|
|
||||||
updateNodeData(props.id, {...data, triggers: keywords});
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||||
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
|
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
|
||||||
{data.triggerType === "emotion" && (
|
<div className={"flex-row gap-md"}>Triggers when the condition is met.</div>
|
||||||
<div className={"flex-row gap-md"}>Emotion?</div>
|
<div className={"flex-row gap-md"}>Condition/ Belief is currently {data.condition ? "" : "not"} set. {data.condition ? "🟢" : "🔴"}</div>
|
||||||
)}
|
<div className={"flex-row gap-md"}>Plan{data.plan ? (": " + data.plan.name) : ""} is currently {data.plan ? "" : "not"} set. {data.plan ? "🟢" : "🔴"}</div>
|
||||||
{data.triggerType === "keywords" && (
|
|
||||||
<Keywords
|
|
||||||
keywords={data.triggers}
|
|
||||||
setKeywords={setKeywords}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<MultiConnectionHandle type="source" position={Position.Right} id="TriggerSource" rules={[
|
<MultiConnectionHandle type="source" position={Position.Right} id="TriggerSource" rules={[
|
||||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
|
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}]),
|
||||||
]}/>
|
]}/>
|
||||||
|
<SingleConnectionHandle type="target" position={Position.Bottom} id="beliefs" rules={[
|
||||||
|
allowOnlyConnectionsFromType(["basic_belief"])
|
||||||
|
]}/>
|
||||||
|
|
||||||
|
|
||||||
|
<PlanEditorDialog
|
||||||
|
plan={data.plan}
|
||||||
|
onSave={(plan) => {
|
||||||
|
updateNodeData(props.id, {
|
||||||
|
...data,
|
||||||
|
plan,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
@@ -86,22 +89,16 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
|||||||
* @param _nodes - The list of all nodes in the current flow graph.
|
* @param _nodes - The list of all nodes in the current flow graph.
|
||||||
* @returns A simplified object containing the node label and its list of triggers.
|
* @returns A simplified object containing the node label and its list of triggers.
|
||||||
*/
|
*/
|
||||||
export function TriggerReduce(node: Node, _nodes: Node[]) {
|
export function TriggerReduce(node: Node, nodes: Node[]) {
|
||||||
const data = node.data;
|
const data = node.data as TriggerNodeData;
|
||||||
switch (data.triggerType) {
|
const conditionNode = data.condition ? nodes.find((n)=>n.id===data.condition) : undefined
|
||||||
case "keywords":
|
const conditionData = conditionNode ? BasicBeliefReduce(conditionNode, nodes) : ""
|
||||||
return {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: "keywords",
|
condition: conditionData, // Make sure we have a condition before reducing, or default to ""
|
||||||
label: data.label,
|
plan: !data.plan ? "" : PlanReduce(data.plan), // Make sure we have a plan when reducing, or default to ""
|
||||||
keywords: data.triggers,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
id: node.id,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,6 +108,11 @@ export function TriggerReduce(node: Node, _nodes: Node[]) {
|
|||||||
*/
|
*/
|
||||||
export function TriggerConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
export function TriggerConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||||
// no additional connection logic exists yet
|
// no additional connection logic exists yet
|
||||||
|
const data = _thisNode.data as TriggerNodeData;
|
||||||
|
// If we got a belief connected, this is the condition for the norm.
|
||||||
|
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
|
||||||
|
data.condition = _sourceNodeId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,6 +131,9 @@ export function TriggerConnectionSource(_thisNode: Node, _targetNodeId: string)
|
|||||||
*/
|
*/
|
||||||
export function TriggerDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
export function TriggerDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||||
// no additional connection logic exists yet
|
// no additional connection logic exists yet
|
||||||
|
const data = _thisNode.data as TriggerNodeData;
|
||||||
|
// remove if the target of disconnection was our condition
|
||||||
|
if (_sourceNodeId == data.condition) data.condition = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,91 +164,3 @@ export type KeywordTriggerNodeProps = {
|
|||||||
|
|
||||||
/** Union type for all possible Trigger node configurations. */
|
/** Union type for all possible Trigger node configurations. */
|
||||||
export type TriggerNodeProps = EmotionTriggerNodeProps | KeywordTriggerNodeProps;
|
export type TriggerNodeProps = EmotionTriggerNodeProps | KeywordTriggerNodeProps;
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders an input element that allows users to add new keyword triggers.
|
|
||||||
*
|
|
||||||
* When the input is committed, the `addKeyword` callback is called with the new keyword.
|
|
||||||
*
|
|
||||||
* @param param0 - An object containing the `addKeyword` function.
|
|
||||||
* @returns A React element(React.JSX.Element) providing an input for adding keywords.
|
|
||||||
*/
|
|
||||||
function KeywordAdder({ addKeyword }: { addKeyword: (keyword: string) => void }) {
|
|
||||||
const [input, setInput] = useState("");
|
|
||||||
|
|
||||||
const text_input_id = "keyword_adder_input";
|
|
||||||
|
|
||||||
return <div className={"flex-row gap-md"}>
|
|
||||||
<label htmlFor={text_input_id}>New Keyword:</label>
|
|
||||||
<RealtimeTextField
|
|
||||||
id={text_input_id}
|
|
||||||
value={input}
|
|
||||||
setValue={setInput}
|
|
||||||
onCommit={() => {
|
|
||||||
if (!input) return;
|
|
||||||
addKeyword(input);
|
|
||||||
setInput("");
|
|
||||||
}}
|
|
||||||
placeholder={"..."}
|
|
||||||
className={"flex-1"}
|
|
||||||
/>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays and manages a list of keyword triggers for a Trigger node.
|
|
||||||
* Handles adding, editing, and removing keywords, as well as detecting duplicate entries.
|
|
||||||
*
|
|
||||||
* @param keywords - The current list of keyword triggers.
|
|
||||||
* @param setKeywords - A callback to update the keyword list in the parent node.
|
|
||||||
* @returns A React element(React.JSX.Element) for editing keyword triggers.
|
|
||||||
*/
|
|
||||||
function Keywords({
|
|
||||||
keywords,
|
|
||||||
setKeywords,
|
|
||||||
}: {
|
|
||||||
keywords: Keyword[];
|
|
||||||
setKeywords: (keywords: Keyword[]) => void;
|
|
||||||
}) {
|
|
||||||
type Interpolatable = string | number | boolean | bigint | null | undefined;
|
|
||||||
|
|
||||||
const inputElementId = (id: Interpolatable) => `keyword_${id}_input`;
|
|
||||||
|
|
||||||
/** Indices of duplicates in the keyword array. */
|
|
||||||
const [duplicates, setDuplicates] = useState<number[]>([]);
|
|
||||||
|
|
||||||
function replace(id: string, value: string) {
|
|
||||||
value = value.trim();
|
|
||||||
const newKeywords = value === ""
|
|
||||||
? keywords.filter((kw) => kw.id != id)
|
|
||||||
: keywords.map((kw) => kw.id === id ? {...kw, keyword: value} : kw);
|
|
||||||
setKeywords(newKeywords);
|
|
||||||
setDuplicates(duplicateIndices(newKeywords.map((kw) => kw.keyword)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function add(value: string) {
|
|
||||||
value = value.trim();
|
|
||||||
if (value === "") return;
|
|
||||||
const newKeywords = [...keywords, {id: crypto.randomUUID(), keyword: value}];
|
|
||||||
setKeywords(newKeywords);
|
|
||||||
setDuplicates(duplicateIndices(newKeywords.map((kw) => kw.keyword)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<span>Triggers when {keywords.length <= 1 ? "the keyword is" : "all keywords are"} spoken.</span>
|
|
||||||
{[...keywords].map(({id, keyword}, index) => {
|
|
||||||
return <div key={id} className={"flex-row gap-md"}>
|
|
||||||
<label htmlFor={inputElementId(id)}>Keyword:</label>
|
|
||||||
<TextField
|
|
||||||
id={inputElementId(id)}
|
|
||||||
value={keyword}
|
|
||||||
setValue={(val) => replace(id, val)}
|
|
||||||
placeholder={"..."}
|
|
||||||
className={"flex-1"}
|
|
||||||
invalid={duplicates.includes(index)}
|
|
||||||
/>
|
|
||||||
</div>;
|
|
||||||
})}
|
|
||||||
<KeywordAdder addKeyword={add} />
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
@@ -95,7 +95,11 @@ describe("Drag & drop node creation", () => {
|
|||||||
const node = nodes[0];
|
const node = nodes[0];
|
||||||
|
|
||||||
expect(node.type).toBe("phase");
|
expect(node.type).toBe("phase");
|
||||||
expect(node.id).toBe("phase-1");
|
|
||||||
|
// UUID Expression
|
||||||
|
expect(node.id).toMatch(
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||||
|
);
|
||||||
|
|
||||||
// screenToFlowPosition was mocked to subtract 100
|
// screenToFlowPosition was mocked to subtract 100
|
||||||
expect(node.position).toEqual({
|
expect(node.position).toEqual({
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { renderWithProviders, screen } from '../../../../test-utils/test-utils.tsx';
|
||||||
|
import GestureValueEditor from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/GestureValueEditor';
|
||||||
|
|
||||||
|
function TestHarness({ initialValue = '', initialType=true, placeholder = 'Gesture name' } : { initialValue?: string, initialType?: boolean, placeholder?: string }) {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
const [_, setType] = useState(initialType)
|
||||||
|
return (
|
||||||
|
<GestureValueEditor value={value} setValue={setValue} setType={setType} placeholder={placeholder} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GestureValueEditor', () => {
|
||||||
|
let user: ReturnType<typeof userEvent.setup>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = userEvent.setup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders in tag mode by default and allows selecting a tag via button and select', async () => {
|
||||||
|
renderWithProviders(<TestHarness />);
|
||||||
|
|
||||||
|
// Tag selector should be present
|
||||||
|
const select = screen.getByTestId('tagSelectorTestID') as HTMLSelectElement;
|
||||||
|
expect(select).toBeInTheDocument();
|
||||||
|
expect(select.value).toBe('');
|
||||||
|
|
||||||
|
// Choose a tag via select
|
||||||
|
await user.selectOptions(select, 'happy');
|
||||||
|
expect(select.value).toBe('happy');
|
||||||
|
|
||||||
|
// The corresponding tag button should reflect the selection (have the selected class)
|
||||||
|
const happyButton = screen.getByRole('button', { name: /happy/i });
|
||||||
|
expect(happyButton).toBeInTheDocument();
|
||||||
|
expect(happyButton.className).toMatch(/selected/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('switches to single mode and shows suggestions list', async () => {
|
||||||
|
renderWithProviders(<TestHarness initialValue={'happy'} />);
|
||||||
|
|
||||||
|
const singleButton = screen.getByRole('button', { name: /^single$/i });
|
||||||
|
await user.click(singleButton);
|
||||||
|
|
||||||
|
// Input should be present with placeholder
|
||||||
|
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
|
||||||
|
expect(input).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Because switching to single populates suggestions, we expect at least one suggestion item
|
||||||
|
const suggestion = await screen.findByText(/Listening_1/);
|
||||||
|
expect(suggestion).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('typing filters suggestions and selecting a suggestion commits the value and hides the list', async () => {
|
||||||
|
renderWithProviders(<TestHarness />);
|
||||||
|
|
||||||
|
// Switch to single mode
|
||||||
|
await user.click(screen.getByRole('button', { name: /^single$/i }));
|
||||||
|
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
|
||||||
|
|
||||||
|
// Type a substring that matches some suggestions
|
||||||
|
await user.type(input, 'Listening_2');
|
||||||
|
|
||||||
|
// The suggestion should appear and include the text we typed
|
||||||
|
const matching = await screen.findByText(/Listening_2/);
|
||||||
|
expect(matching).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Click the suggestion
|
||||||
|
await user.click(matching);
|
||||||
|
|
||||||
|
// After selecting, input should contain that suggestion and suggestions should be hidden
|
||||||
|
expect(input.value).toContain('Listening_2');
|
||||||
|
expect(screen.queryByText(/Listening_1/)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('typing a non-matching string hides the suggestions list', async () => {
|
||||||
|
renderWithProviders(<TestHarness />);
|
||||||
|
await user.click(screen.getByRole('button', { name: /^single$/i }));
|
||||||
|
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
|
||||||
|
|
||||||
|
await user.type(input, 'no-match-zzz');
|
||||||
|
|
||||||
|
// There should be no suggestion that includes that gibberish
|
||||||
|
expect(screen.queryByText(/no-match-zzz/)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('switching back to tag mode clears value when it is not a valid tag and preserves it when it is', async () => {
|
||||||
|
renderWithProviders(<TestHarness />);
|
||||||
|
|
||||||
|
// Switch to single mode and pick a suggestion (which is not a semantic tag)
|
||||||
|
await user.click(screen.getByRole('button', { name: /^single$/i }));
|
||||||
|
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
|
||||||
|
await user.type(input, 'Listening_3');
|
||||||
|
const suggestion = await screen.findByText(/Listening_3/);
|
||||||
|
await user.click(suggestion);
|
||||||
|
|
||||||
|
// Switch back to tag mode -> value should be cleared (not in tag list)
|
||||||
|
await user.click(screen.getByRole('button', { name: /^tag$/i }));
|
||||||
|
const select = screen.getByTestId('tagSelectorTestID') as HTMLSelectElement;
|
||||||
|
expect(select.value).toBe('');
|
||||||
|
|
||||||
|
// Now pick a valid tag and switch to single then back to tag
|
||||||
|
await user.selectOptions(select, 'happy');
|
||||||
|
expect(select.value).toBe('happy');
|
||||||
|
|
||||||
|
// Switch to single and then back to tag; since 'happy' is a valid tag, it should remain
|
||||||
|
await user.click(screen.getByRole('button', { name: /^single$/i }));
|
||||||
|
await user.click(screen.getByRole('button', { name: /^tag$/i }));
|
||||||
|
expect(select.value).toBe('happy');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('focus on input re-shows filtered suggestions when customValue is present', async () => {
|
||||||
|
renderWithProviders(<TestHarness />);
|
||||||
|
|
||||||
|
// Switch to single mode and type to filter
|
||||||
|
await user.click(screen.getByRole('button', { name: /^single$/i }));
|
||||||
|
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
|
||||||
|
|
||||||
|
await user.type(input, 'Listening_4');
|
||||||
|
const found = await screen.findByText(/Listening_4/);
|
||||||
|
expect(found).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Blur the input
|
||||||
|
input.blur();
|
||||||
|
expect(found).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Focus the input again and ensure the suggestions remain or reappear
|
||||||
|
await user.click(input);
|
||||||
|
const foundAgain = await screen.findByText(/Listening_4/);
|
||||||
|
expect(foundAgain).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
// PlanEditorDialog.test.tsx
|
||||||
|
import { describe, it, beforeEach, jest } from '@jest/globals';
|
||||||
|
import { screen, within } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
|
||||||
|
import PlanEditorDialog from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor';
|
||||||
|
import { PlanReduce, type Plan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
// Mock structuredClone
|
||||||
|
(globalThis as any).structuredClone = jest.fn((val) => JSON.parse(JSON.stringify(val)));
|
||||||
|
|
||||||
|
// UUID Regex for checking ID's
|
||||||
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
|
describe('PlanEditorDialog', () => {
|
||||||
|
let user: ReturnType<typeof userEvent.setup>;
|
||||||
|
const mockOnSave = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = userEvent.setup();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultPlan: Plan = {
|
||||||
|
id: 'plan-1',
|
||||||
|
name: 'Test Plan',
|
||||||
|
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 = {
|
||||||
|
id: 'plan-2',
|
||||||
|
name: 'Existing Plan',
|
||||||
|
steps: [
|
||||||
|
{ id: 'step-1', text: 'Hello world', type: 'speech' as const },
|
||||||
|
{ id: 'step-2', gesture: 'Wave', isTag:true, type: 'gesture' as const },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDialog = (props: Partial<React.ComponentProps<typeof PlanEditorDialog>> = {}) => {
|
||||||
|
const defaultProps = {
|
||||||
|
plan: undefined,
|
||||||
|
onSave: mockOnSave,
|
||||||
|
description: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderWithProviders(<PlanEditorDialog {...defaultProps} {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('should show "Create Plan" button when no plan is provided', () => {
|
||||||
|
renderDialog();
|
||||||
|
// The button should be visible
|
||||||
|
expect(screen.getByRole('button', { name: 'Create Plan' })).toBeInTheDocument();
|
||||||
|
// The dialog content should NOT be visible initially
|
||||||
|
expect(screen.queryByText(/Add Action/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show "Edit Plan" button when a plan is provided', () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
expect(screen.getByRole('button', { name: 'Edit Plan' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show "Create Plan" button when a plan exists', () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
// Query for the button text specifically, not dialog title
|
||||||
|
expect(screen.queryByRole('button', { name: 'Create Plan' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Dialog Interactions', () => {
|
||||||
|
it('should open dialog with "Create Plan" title when creating new plan', async () => {
|
||||||
|
renderDialog();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||||
|
|
||||||
|
|
||||||
|
// One for button, one for dialog.
|
||||||
|
expect(screen.getAllByText('Create Plan').length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open dialog with "Edit Plan" title when editing existing plan', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
// One for button, one for dialog
|
||||||
|
expect(screen.getAllByText('Edit Plan').length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pre-fill plan name when editing', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
|
||||||
|
expect(nameInput.value).toBe(defaultPlan.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close dialog when cancel button is clicked', async () => {
|
||||||
|
renderDialog();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||||
|
await user.click(screen.getByText('Cancel'));
|
||||||
|
|
||||||
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Plan Creation', () => {
|
||||||
|
it('should create a new plan with default values', async () => {
|
||||||
|
renderDialog();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||||
|
|
||||||
|
// One for the button, one for the dialog
|
||||||
|
expect(screen.getAllByText('Create Plan').length).toEqual(2);
|
||||||
|
|
||||||
|
const nameInput = screen.getByPlaceholderText('Plan name');
|
||||||
|
expect(nameInput).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should auto-fill with description when provided', async () => {
|
||||||
|
const description = 'Achieve world peace';
|
||||||
|
renderDialog({ description });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||||
|
|
||||||
|
// Check if plan name is pre-filled with description
|
||||||
|
const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
|
||||||
|
expect(nameInput.value).toBe(description);
|
||||||
|
|
||||||
|
// Check if action type is set to LLM
|
||||||
|
const actionTypeSelect = screen.getByLabelText(/Action Type/i) as HTMLSelectElement;
|
||||||
|
expect(actionTypeSelect.value).toBe('llm');
|
||||||
|
|
||||||
|
// Check if suggestion text is shown
|
||||||
|
expect(screen.getByText('Filled in as a suggestion!')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Feel free to change!')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow changing plan name', async () => {
|
||||||
|
renderDialog();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||||
|
|
||||||
|
const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
|
||||||
|
const newName = 'My Custom Plan';
|
||||||
|
|
||||||
|
// Instead of clear(), select all text and type new value
|
||||||
|
await user.click(nameInput);
|
||||||
|
await user.keyboard('{Control>}a{/Control}'); // Select all (Ctrl+A)
|
||||||
|
await user.keyboard(newName);
|
||||||
|
|
||||||
|
expect(nameInput.value).toBe(newName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Action Management', () => {
|
||||||
|
it('should add a speech action to the plan', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
const actionTypeSelect = screen.getByLabelText(/Action Type/i);
|
||||||
|
const actionValueInput = screen.getByPlaceholderText("Speech text")
|
||||||
|
const addButton = screen.getByText('Add Step');
|
||||||
|
|
||||||
|
// Set up a speech action
|
||||||
|
await user.selectOptions(actionTypeSelect, 'speech');
|
||||||
|
await user.type(actionValueInput, 'Hello there!');
|
||||||
|
|
||||||
|
await user.click(addButton);
|
||||||
|
|
||||||
|
// Check if step was added
|
||||||
|
expect(screen.getByText('speech:')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Hello there!')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a gesture action to the plan', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /edit plan/i }));
|
||||||
|
|
||||||
|
const actionTypeSelect = screen.getByLabelText(/Action Type/i);
|
||||||
|
const addButton = screen.getByText('Add Step');
|
||||||
|
|
||||||
|
// Set up a gesture action
|
||||||
|
await user.selectOptions(actionTypeSelect, 'gesture');
|
||||||
|
|
||||||
|
// Find the input field after type change
|
||||||
|
const select = screen.getByTestId("tagSelectorTestID")
|
||||||
|
const options = within(select).getAllByRole('option')
|
||||||
|
|
||||||
|
await user.selectOptions(select, options[1])
|
||||||
|
await user.click(addButton);
|
||||||
|
|
||||||
|
// Check if step was added
|
||||||
|
expect(screen.getByText('gesture:')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add an LLM action to the plan', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
const actionTypeSelect = screen.getByLabelText(/Action Type/i);
|
||||||
|
const addButton = screen.getByText('Add Step');
|
||||||
|
|
||||||
|
// Set up an LLM action
|
||||||
|
await user.selectOptions(actionTypeSelect, 'llm');
|
||||||
|
|
||||||
|
// Find the input field after type change
|
||||||
|
const llmInput = screen.getByPlaceholderText(/LLM goal|text/i);
|
||||||
|
await user.type(llmInput, 'Generate a story');
|
||||||
|
|
||||||
|
await user.click(addButton);
|
||||||
|
|
||||||
|
// Check if step was added
|
||||||
|
expect(screen.getByText('llm:')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Generate a story')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable "Add Step" button when action value is empty', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
const addButton = screen.getByText('Add Step');
|
||||||
|
expect(addButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset action form after adding a step', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
|
||||||
|
const actionValueInput = screen.getByPlaceholderText("Speech text")
|
||||||
|
const addButton = screen.getByText('Add Step');
|
||||||
|
|
||||||
|
await user.type(actionValueInput, 'Test speech');
|
||||||
|
await user.click(addButton);
|
||||||
|
|
||||||
|
// Action value should be cleared
|
||||||
|
expect(actionValueInput).toHaveValue('');
|
||||||
|
// Action type should be reset to speech (default)
|
||||||
|
const actionTypeSelect = screen.getByLabelText(/Action Type/i) as HTMLSelectElement;
|
||||||
|
expect(actionTypeSelect.value).toBe('speech');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Step Management', () => {
|
||||||
|
it('should show existing steps when editing a plan', async () => {
|
||||||
|
renderDialog({ plan: planWithSteps });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
// Check if existing steps are shown
|
||||||
|
expect(screen.getByText('speech:')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Hello world')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('gesture:')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Wave')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show "No steps yet" message when plan has no steps', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
expect(screen.getByText('No steps yet')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove a step when clicked', async () => {
|
||||||
|
renderDialog({ plan: planWithSteps });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
// Initially have 2 steps
|
||||||
|
expect(screen.getByText('speech:')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('gesture:')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Click on the first step to remove it
|
||||||
|
await user.click(screen.getByText('Hello world'));
|
||||||
|
|
||||||
|
// First step should be removed
|
||||||
|
expect(screen.queryByText('Hello world')).not.toBeInTheDocument();
|
||||||
|
// Second step should still exist
|
||||||
|
expect(screen.getByText('Wave')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Save Functionality', () => {
|
||||||
|
it('should call onSave with new plan when creating', async () => {
|
||||||
|
renderDialog();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||||
|
|
||||||
|
// Set plan name
|
||||||
|
const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
|
||||||
|
await user.click(nameInput);
|
||||||
|
await user.keyboard('{Control>}a{/Control}');
|
||||||
|
await user.keyboard('My New Plan');
|
||||||
|
|
||||||
|
// Add a step
|
||||||
|
const actionValueInput = screen.getByPlaceholderText(/text/i);
|
||||||
|
await user.type(actionValueInput, 'First step');
|
||||||
|
await user.click(screen.getByText('Add Step'));
|
||||||
|
|
||||||
|
// Save the plan
|
||||||
|
await user.click(screen.getByText('Create'));
|
||||||
|
|
||||||
|
expect(mockOnSave).toHaveBeenCalledWith({
|
||||||
|
id: expect.stringMatching(uuidRegex),
|
||||||
|
name: 'My New Plan',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: expect.stringMatching(uuidRegex),
|
||||||
|
text: 'First step',
|
||||||
|
type: 'speech',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSave with updated plan when editing', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
// Change plan name
|
||||||
|
const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
|
||||||
|
await user.click(nameInput);
|
||||||
|
await user.keyboard('{Control>}a{/Control}');
|
||||||
|
await user.keyboard('Updated Plan Name');
|
||||||
|
|
||||||
|
// Add a step
|
||||||
|
const actionValueInput = screen.getByPlaceholderText(/text/i);
|
||||||
|
await user.type(actionValueInput, 'New speech action');
|
||||||
|
await user.click(screen.getByText('Add Step'));
|
||||||
|
|
||||||
|
// Save the plan
|
||||||
|
await user.click(screen.getByText('Confirm'));
|
||||||
|
|
||||||
|
expect(mockOnSave).toHaveBeenCalledWith({
|
||||||
|
id: defaultPlan.id,
|
||||||
|
name: 'Updated Plan Name',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: expect.stringMatching(uuidRegex),
|
||||||
|
text: 'New speech action',
|
||||||
|
type: 'speech',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSave with undefined when reset button is clicked', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
await user.click(screen.getByText('Reset'));
|
||||||
|
|
||||||
|
expect(mockOnSave).toHaveBeenCalledWith(undefined);
|
||||||
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable save button when no draft plan exists', async () => {
|
||||||
|
renderDialog();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||||
|
|
||||||
|
// The save button should be enabled since draftPlan exists after clicking Create Plan
|
||||||
|
const saveButton = screen.getByText('Create');
|
||||||
|
expect(saveButton).not.toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Step Indexing', () => {
|
||||||
|
it('should show correct step numbers', async () => {
|
||||||
|
renderDialog({ plan: defaultPlan });
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||||
|
|
||||||
|
// Add multiple steps
|
||||||
|
const actionValueInput = screen.getByPlaceholderText(/text/i);
|
||||||
|
const addButton = screen.getByText('Add Step');
|
||||||
|
|
||||||
|
await user.type(actionValueInput, 'First');
|
||||||
|
await user.click(addButton);
|
||||||
|
|
||||||
|
await user.type(actionValueInput, 'Second');
|
||||||
|
await user.click(addButton);
|
||||||
|
|
||||||
|
await user.type(actionValueInput, 'Third');
|
||||||
|
await user.click(addButton);
|
||||||
|
|
||||||
|
// Check step numbers
|
||||||
|
expect(screen.getByText('1.')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('2.')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('3.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Action Type Switching', () => {
|
||||||
|
it('should update placeholder text when action type changes', async () => {
|
||||||
|
renderDialog();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||||
|
|
||||||
|
const actionTypeSelect = screen.getByLabelText(/Action Type/i);
|
||||||
|
|
||||||
|
// Check speech placeholder
|
||||||
|
await user.selectOptions(actionTypeSelect, 'speech');
|
||||||
|
// The placeholder might be set dynamically, so we need to check the input
|
||||||
|
const speechInput = screen.getByPlaceholderText(/text/i);
|
||||||
|
expect(speechInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check gesture placeholder
|
||||||
|
await user.selectOptions(actionTypeSelect, 'gesture');
|
||||||
|
const gestureInput = screen.getByTestId("valueEditorTestID")
|
||||||
|
expect(gestureInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check LLM placeholder
|
||||||
|
await user.selectOptions(actionTypeSelect, 'llm');
|
||||||
|
const llmInput = screen.getByPlaceholderText(/LLM|text/i);
|
||||||
|
expect(llmInput).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Plan reducing', () => {
|
||||||
|
it('should correctly reduce the plan given the elements of the plan', () => {
|
||||||
|
const testplan = extendedPlan
|
||||||
|
const expectedResult = {
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -14,7 +14,6 @@ describe('BasicBeliefNode', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = userEvent.setup();
|
user = userEvent.setup();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('should render the basic belief node with keyword type by default', () => {
|
it('should render the basic belief node with keyword type by default', () => {
|
||||||
const mockNode: Node<BasicBeliefNodeData> = {
|
const mockNode: Node<BasicBeliefNodeData> = {
|
||||||
@@ -59,7 +58,7 @@ describe('BasicBeliefNode', () => {
|
|||||||
data: {
|
data: {
|
||||||
label: 'Belief',
|
label: 'Belief',
|
||||||
droppable: true,
|
droppable: true,
|
||||||
belief: { type: 'semantic', id: 'test', value: 'test value', label: 'Detected with LLM:' },
|
belief: { type: 'semantic', id: 'test', value: 'test value', description: "test description", label: 'Detected with LLM:' },
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -333,7 +332,7 @@ describe('BasicBeliefNode', () => {
|
|||||||
data: {
|
data: {
|
||||||
label: 'Belief',
|
label: 'Belief',
|
||||||
droppable: true,
|
droppable: true,
|
||||||
belief: { type: 'semantic', id: 'sem1', value: 'initial', label: 'Detected with LLM:' },
|
belief: { type: 'semantic', id: 'test', value: 'test value', description: "test description", label: 'Detected with LLM:' },
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -360,10 +359,10 @@ describe('BasicBeliefNode', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const input = screen.getByDisplayValue('initial') as HTMLInputElement;
|
const input = screen.getByDisplayValue('test value') as HTMLInputElement;
|
||||||
|
|
||||||
// Clear the input
|
// Clear the input
|
||||||
for (let i = 0; i < 'initial'.length; i++) {
|
for (let i = 0; i < 'test value'.length; i++) {
|
||||||
await user.type(input, '{backspace}');
|
await user.type(input, '{backspace}');
|
||||||
}
|
}
|
||||||
await user.type(input, 'new semantic value{enter}');
|
await user.type(input, 'new semantic value{enter}');
|
||||||
@@ -689,7 +688,7 @@ describe('BasicBeliefNode', () => {
|
|||||||
data: {
|
data: {
|
||||||
label: 'Belief',
|
label: 'Belief',
|
||||||
droppable: true,
|
droppable: true,
|
||||||
belief: { type: 'semantic', id: 'sem1', value: 'initial', label: 'Detected with LLM:' },
|
belief: { type: 'semantic', id: 'test', value: 'test value', description: "test description", label: 'Detected with LLM:' },
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -716,27 +715,27 @@ describe('BasicBeliefNode', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const input = screen.getByDisplayValue('initial') as HTMLInputElement;
|
const input = screen.getByDisplayValue('test value') as HTMLInputElement;
|
||||||
|
|
||||||
await user.type(input, '1');
|
await user.type(input, '1');
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const state = useFlowStore.getState();
|
const state = useFlowStore.getState();
|
||||||
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
|
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
|
||||||
expect(nodeData.belief.value).toBe('initial');
|
expect(nodeData.belief.value).toBe('test value');
|
||||||
});
|
});
|
||||||
|
|
||||||
await user.type(input, '2');
|
await user.type(input, '2');
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const state = useFlowStore.getState();
|
const state = useFlowStore.getState();
|
||||||
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
|
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
|
||||||
expect(nodeData.belief.value).toBe('initial');
|
expect(nodeData.belief.value).toBe('test value');
|
||||||
});
|
});
|
||||||
|
|
||||||
await user.type(input, '{enter}');
|
await user.type(input, '{enter}');
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const state = useFlowStore.getState();
|
const state = useFlowStore.getState();
|
||||||
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
|
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
|
||||||
expect(nodeData.belief.value).toBe('initial12');
|
expect(nodeData.belief.value).toBe('test value12');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -404,6 +404,16 @@ describe('NormNode', () => {
|
|||||||
|
|
||||||
describe('NormReduce Function', () => {
|
describe('NormReduce Function', () => {
|
||||||
it('should reduce a norm node to its essential data', () => {
|
it('should reduce a norm node to its essential data', () => {
|
||||||
|
|
||||||
|
const condition: Node = {
|
||||||
|
id: "belief-1",
|
||||||
|
type: 'basic_belief',
|
||||||
|
position: {x: 10, y: 10},
|
||||||
|
data: {
|
||||||
|
...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const normNode: Node = {
|
const normNode: Node = {
|
||||||
id: 'norm-1',
|
id: 'norm-1',
|
||||||
type: 'norm',
|
type: 'norm',
|
||||||
@@ -414,18 +424,21 @@ describe('NormNode', () => {
|
|||||||
droppable: true,
|
droppable: true,
|
||||||
norm: 'Never harm humans',
|
norm: 'Never harm humans',
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
|
condition: "belief-1"
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const allNodes: Node[] = [normNode];
|
const allNodes: Node[] = [normNode, condition];
|
||||||
const result = NormReduce(normNode, allNodes);
|
const result = NormReduce(normNode, allNodes);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
id: 'norm-1',
|
id: 'norm-1',
|
||||||
label: 'Safety Norm',
|
label: 'Safety Norm',
|
||||||
norm: 'Never harm humans',
|
norm: 'Never harm humans',
|
||||||
critical: false,
|
critical: false,
|
||||||
basic_beliefs: [],
|
condition: {
|
||||||
|
id: "belief-1",
|
||||||
|
keyword: ""
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -917,17 +930,8 @@ describe('NormNode', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockBelief2: Node = {
|
|
||||||
id: 'basic_belief-2',
|
|
||||||
type: 'basic_belief',
|
|
||||||
position: {x:300, y:300},
|
|
||||||
data: {
|
|
||||||
...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useFlowStore.setState({
|
useFlowStore.setState({
|
||||||
nodes: [mockNode, mockBelief1, mockBelief2],
|
nodes: [mockNode, mockBelief1],
|
||||||
edges: [],
|
edges: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -938,16 +942,11 @@ describe('NormNode', () => {
|
|||||||
sourceHandle: null,
|
sourceHandle: null,
|
||||||
targetHandle: null,
|
targetHandle: null,
|
||||||
});
|
});
|
||||||
useFlowStore.getState().onConnect({
|
|
||||||
source: 'basic_belief-2',
|
|
||||||
target: 'norm-1',
|
|
||||||
sourceHandle: null,
|
|
||||||
targetHandle: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = useFlowStore.getState();
|
const state = useFlowStore.getState();
|
||||||
const updatedNorm = state.nodes.find(n => n.id === 'norm-1');
|
const updatedNorm = state.nodes.find(n => n.id === 'norm-1');
|
||||||
expect(updatedNorm?.data.conditions).toEqual(["basic_belief-1", "basic_belief-2"]);
|
expect(updatedNorm?.data.condition).toEqual("basic_belief-1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -78,8 +78,10 @@ describe('PhaseNode', () => {
|
|||||||
|
|
||||||
// Find nodes
|
// Find nodes
|
||||||
const nodes = useFlowStore.getState().nodes;
|
const nodes = useFlowStore.getState().nodes;
|
||||||
const p1 = nodes.find((x) => x.id === 'phase-1')!;
|
const phaseNodes = nodes.filter((x) => x.type === 'phase');
|
||||||
const p2 = nodes.find((x) => x.id === 'phase-2')!;
|
const p1 = phaseNodes[0];
|
||||||
|
const p2 = phaseNodes[1];
|
||||||
|
|
||||||
|
|
||||||
// expect same value, not same reference
|
// expect same value, not same reference
|
||||||
expect(p1.data.children).not.toBe(p2.data.children);
|
expect(p1.data.children).not.toBe(p2.data.children);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, it, beforeEach } from '@jest/globals';
|
import { describe, it, beforeEach } from '@jest/globals';
|
||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
|
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
|
||||||
import TriggerNode, {
|
import TriggerNode, {
|
||||||
TriggerReduce,
|
TriggerReduce,
|
||||||
@@ -11,12 +10,15 @@ import TriggerNode, {
|
|||||||
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||||
import type { Node } from '@xyflow/react';
|
import type { Node } from '@xyflow/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
import { TriggerNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.default.ts';
|
||||||
|
import { BasicBeliefNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts';
|
||||||
|
import { defaultPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan.default.ts';
|
||||||
|
import { NormNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.default.ts';
|
||||||
|
|
||||||
describe('TriggerNode', () => {
|
describe('TriggerNode', () => {
|
||||||
let user: ReturnType<typeof userEvent.setup>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = userEvent.setup();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
@@ -26,11 +28,7 @@ describe('TriggerNode', () => {
|
|||||||
type: 'trigger',
|
type: 'trigger',
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
data: {
|
data: {
|
||||||
label: 'Keyword Trigger',
|
...JSON.parse(JSON.stringify(TriggerNodeDefaults)),
|
||||||
droppable: true,
|
|
||||||
triggerType: 'keywords',
|
|
||||||
triggers: [],
|
|
||||||
hasReduce: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,161 +49,58 @@ describe('TriggerNode', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText(/Triggers when the keyword is spoken/i)).toBeInTheDocument();
|
expect(screen.getByText(/Triggers when the condition is met/i)).toBeInTheDocument();
|
||||||
expect(screen.getByPlaceholderText('...')).toBeInTheDocument();
|
expect(screen.getByText(/Belief is currently/i)).toBeInTheDocument();
|
||||||
});
|
expect(screen.getByText(/Plan is currently/i)).toBeInTheDocument();
|
||||||
|
|
||||||
it('should render TriggerNode with emotion type', () => {
|
|
||||||
const mockNode: Node<TriggerNodeData> = {
|
|
||||||
id: 'trigger-2',
|
|
||||||
type: 'trigger',
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
data: {
|
|
||||||
label: 'Emotion Trigger',
|
|
||||||
droppable: true,
|
|
||||||
triggerType: 'emotion',
|
|
||||||
triggers: [],
|
|
||||||
hasReduce: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
renderWithProviders(
|
|
||||||
<TriggerNode
|
|
||||||
id={mockNode.id}
|
|
||||||
type={mockNode.type as string}
|
|
||||||
data={mockNode.data as any}
|
|
||||||
selected={false}
|
|
||||||
isConnectable={true}
|
|
||||||
zIndex={0}
|
|
||||||
dragging={false}
|
|
||||||
selectable={true}
|
|
||||||
deletable={true}
|
|
||||||
draggable={true}
|
|
||||||
positionAbsoluteX={0}
|
|
||||||
positionAbsoluteY={0}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText(/Emotion\?/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('User Interactions', () => {
|
|
||||||
it('should add a new keyword', async () => {
|
|
||||||
const mockNode: Node<TriggerNodeData> = {
|
|
||||||
id: 'trigger-1',
|
|
||||||
type: 'trigger',
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
data: {
|
|
||||||
label: 'Keyword Trigger',
|
|
||||||
droppable: true,
|
|
||||||
triggerType: 'keywords',
|
|
||||||
triggers: [],
|
|
||||||
hasReduce: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
useFlowStore.setState({ nodes: [mockNode], edges: [] });
|
|
||||||
|
|
||||||
renderWithProviders(
|
|
||||||
<TriggerNode
|
|
||||||
id={mockNode.id}
|
|
||||||
type={mockNode.type as string}
|
|
||||||
data={mockNode.data as any}
|
|
||||||
selected={false}
|
|
||||||
isConnectable={true}
|
|
||||||
zIndex={0}
|
|
||||||
dragging={false}
|
|
||||||
selectable={true}
|
|
||||||
deletable={true}
|
|
||||||
draggable={true}
|
|
||||||
positionAbsoluteX={0}
|
|
||||||
positionAbsoluteY={0}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText('...');
|
|
||||||
await user.type(input, 'hello{enter}');
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const node = useFlowStore.getState().nodes.find(n => n.id === 'trigger-1') as Node<TriggerNodeData> | undefined;
|
|
||||||
expect(node?.data.triggers.length).toBe(1);
|
|
||||||
expect(node?.data.triggers[0].keyword).toBe('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove a keyword when cleared', async () => {
|
|
||||||
const mockNode: Node<TriggerNodeData> = {
|
|
||||||
id: 'trigger-1',
|
|
||||||
type: 'trigger',
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
data: {
|
|
||||||
label: 'Keyword Trigger',
|
|
||||||
droppable: true,
|
|
||||||
triggerType: 'keywords',
|
|
||||||
triggers: [{ id: 'kw1', keyword: 'hello' }],
|
|
||||||
hasReduce: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
useFlowStore.setState({ nodes: [mockNode], edges: [] });
|
|
||||||
|
|
||||||
renderWithProviders(
|
|
||||||
<TriggerNode
|
|
||||||
id={mockNode.id}
|
|
||||||
type={mockNode.type as string}
|
|
||||||
data={mockNode.data as any}
|
|
||||||
selected={false}
|
|
||||||
isConnectable={true}
|
|
||||||
zIndex={0}
|
|
||||||
dragging={false}
|
|
||||||
selectable={true}
|
|
||||||
deletable={true}
|
|
||||||
draggable={true}
|
|
||||||
positionAbsoluteX={0}
|
|
||||||
positionAbsoluteY={0}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const input = screen.getByDisplayValue('hello');
|
|
||||||
for (let i = 0; i < 'hello'.length; i++) {
|
|
||||||
await user.type(input, '{backspace}');
|
|
||||||
}
|
|
||||||
await user.type(input, '{enter}');
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const node = useFlowStore.getState().nodes.find(n => n.id === 'trigger-1') as Node<TriggerNodeData> | undefined;
|
|
||||||
expect(node?.data.triggers.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('TriggerReduce Function', () => {
|
describe('TriggerReduce Function', () => {
|
||||||
it('should reduce a trigger node to its essential data', () => {
|
it('should reduce a trigger node to its essential data', () => {
|
||||||
|
const conditionNode: Node = {
|
||||||
|
id: 'belief-1',
|
||||||
|
type: 'basic_belief',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const triggerNode: Node = {
|
const triggerNode: Node = {
|
||||||
id: 'trigger-1',
|
id: 'trigger-1',
|
||||||
type: 'trigger',
|
type: 'trigger',
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
data: {
|
data: {
|
||||||
label: 'Keyword Trigger',
|
...JSON.parse(JSON.stringify(TriggerNodeDefaults)),
|
||||||
droppable: true,
|
condition: "belief-1",
|
||||||
triggerType: 'keywords',
|
plan: defaultPlan
|
||||||
triggers: [{ id: 'kw1', keyword: 'hello' }],
|
|
||||||
hasReduce: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const allNodes: Node[] = [triggerNode];
|
useFlowStore.setState({
|
||||||
const result = TriggerReduce(triggerNode, allNodes);
|
nodes: [conditionNode, triggerNode],
|
||||||
|
edges: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
useFlowStore.getState().onConnect({
|
||||||
|
source: 'belief-1',
|
||||||
|
target: 'trigger-1',
|
||||||
|
sourceHandle: null,
|
||||||
|
targetHandle: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = TriggerReduce(triggerNode, useFlowStore.getState().nodes);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
id: 'trigger-1',
|
id: 'trigger-1',
|
||||||
type: 'keywords',
|
condition: {
|
||||||
label: 'Keyword Trigger',
|
id: "belief-1",
|
||||||
keywords: [{ id: 'kw1', keyword: 'hello' }],
|
keyword: "",
|
||||||
});
|
},
|
||||||
|
plan: {
|
||||||
|
id: expect.anything(),
|
||||||
|
steps: [],
|
||||||
|
},});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -217,11 +112,8 @@ describe('TriggerNode', () => {
|
|||||||
type: 'trigger',
|
type: 'trigger',
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
data: {
|
data: {
|
||||||
|
...JSON.parse(JSON.stringify(TriggerNodeDefaults)),
|
||||||
label: 'Trigger 1',
|
label: 'Trigger 1',
|
||||||
droppable: true,
|
|
||||||
triggerType: 'keywords',
|
|
||||||
triggers: [],
|
|
||||||
hasReduce: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -230,10 +122,8 @@ describe('TriggerNode', () => {
|
|||||||
type: 'norm',
|
type: 'norm',
|
||||||
position: { x: 100, y: 0 },
|
position: { x: 100, y: 0 },
|
||||||
data: {
|
data: {
|
||||||
|
...JSON.parse(JSON.stringify(NormNodeDefaults)),
|
||||||
label: 'Norm 1',
|
label: 'Norm 1',
|
||||||
droppable: true,
|
|
||||||
norm: 'test',
|
|
||||||
hasReduce: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ describe('Universal Nodes', () => {
|
|||||||
// Verify the correct structure is present using NodesInPhase
|
// Verify the correct structure is present using NodesInPhase
|
||||||
expect(result).toHaveLength(nodeType !== 'phase' ? 1 : 2);
|
expect(result).toHaveLength(nodeType !== 'phase' ? 1 : 2);
|
||||||
expect(result[0]).toHaveProperty('id', 'phase-1');
|
expect(result[0]).toHaveProperty('id', 'phase-1');
|
||||||
expect(result[0]).toHaveProperty('label', 'Test Phase');
|
expect(result[0]).toHaveProperty('name', 'Test Phase');
|
||||||
|
|
||||||
// Restore mocks
|
// Restore mocks
|
||||||
phaseReduceSpy.mockRestore();
|
phaseReduceSpy.mockRestore();
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export const mockReactFlow = () => {
|
|||||||
width: 200,
|
width: 200,
|
||||||
height: 200,
|
height: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -95,3 +96,16 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (typeof HTMLDialogElement !== 'undefined') {
|
||||||
|
if (!HTMLDialogElement.prototype.showModal) {
|
||||||
|
HTMLDialogElement.prototype.showModal = function () {
|
||||||
|
// basic behavior: mark as open
|
||||||
|
this.setAttribute('open', '');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!HTMLDialogElement.prototype.close) {
|
||||||
|
HTMLDialogElement.prototype.close = function () {
|
||||||
|
this.removeAttribute('open');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user