build: fixed small merge conflict
ref: N25B-142
This commit is contained in:
@@ -1,19 +1,9 @@
|
||||
/* editor UI */
|
||||
|
||||
.outer-editor-container {
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
justify-self: center;
|
||||
padding: 10px;
|
||||
align-items: center;
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.inner-editor-container {
|
||||
outline-style: solid;
|
||||
border-radius: 10pt;
|
||||
width: 90%;
|
||||
box-sizing: border-box;
|
||||
margin: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -81,6 +71,16 @@
|
||||
filter: drop-shadow(0 0 0.25rem forestgreen);
|
||||
}
|
||||
|
||||
.node-goal {
|
||||
outline: yellow solid 2pt;
|
||||
filter: drop-shadow(0 0 0.25rem yellow);
|
||||
}
|
||||
|
||||
.node-trigger {
|
||||
outline: teal solid 2pt;
|
||||
filter: drop-shadow(0 0 0.25rem teal);
|
||||
}
|
||||
|
||||
.node-phase {
|
||||
outline: dodgerblue solid 2pt;
|
||||
filter: drop-shadow(0 0 0.25rem dodgerblue);
|
||||
@@ -112,6 +112,22 @@
|
||||
filter: drop-shadow(0 0 0.25rem forestgreen);
|
||||
}
|
||||
|
||||
.draggable-node-goal {
|
||||
padding: 3px 10px;
|
||||
background-color: canvas;
|
||||
border-radius: 5pt;
|
||||
outline: yellow solid 2pt;
|
||||
filter: drop-shadow(0 0 0.25rem yellow);
|
||||
}
|
||||
|
||||
.draggable-node-trigger {
|
||||
padding: 3px 10px;
|
||||
background-color: canvas;
|
||||
border-radius: 5pt;
|
||||
outline: teal solid 2pt;
|
||||
filter: drop-shadow(0 0 0.25rem teal);
|
||||
}
|
||||
|
||||
.draggable-node-phase {
|
||||
padding: 3px 10px;
|
||||
background-color: canvas;
|
||||
|
||||
@@ -13,13 +13,15 @@ import {
|
||||
StartNodeComponent,
|
||||
EndNodeComponent,
|
||||
PhaseNodeComponent,
|
||||
NormNodeComponent
|
||||
NormNodeComponent,
|
||||
GoalNodeComponent,
|
||||
} from './visualProgrammingUI/components/NodeDefinitions.tsx';
|
||||
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
||||
import graphReducer from "./visualProgrammingUI/GraphReducer.ts";
|
||||
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
||||
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
||||
import styles from './VisProg.module.css'
|
||||
import TriggerNodeComponent from "./visualProgrammingUI/components/TriggerNodeComponent.tsx";
|
||||
|
||||
// --| config starting params for flow |--
|
||||
|
||||
@@ -30,7 +32,9 @@ const NODE_TYPES = {
|
||||
start: StartNodeComponent,
|
||||
end: EndNodeComponent,
|
||||
phase: PhaseNodeComponent,
|
||||
norm: NormNodeComponent
|
||||
norm: NormNodeComponent,
|
||||
goal: GoalNodeComponent,
|
||||
trigger: TriggerNodeComponent,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -80,30 +84,28 @@ const VisProgUI = () => {
|
||||
} = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore
|
||||
|
||||
return (
|
||||
<div className={styles.outerEditorContainer}>
|
||||
<div className={styles.innerEditorContainer}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}
|
||||
nodeTypes={NODE_TYPES}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onReconnect={onReconnect}
|
||||
onReconnectStart={onReconnectStart}
|
||||
onReconnectEnd={onReconnectEnd}
|
||||
onConnect={onConnect}
|
||||
snapToGrid
|
||||
fitView
|
||||
proOptions={{hideAttribution: true}}
|
||||
>
|
||||
<Panel position="top-center" className={styles.dndPanel}>
|
||||
<DndToolbar/> {/* contains the drag and drop panel for nodes */}
|
||||
</Panel>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<div className={`${styles.innerEditorContainer} round-lg border-lg`}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}
|
||||
nodeTypes={NODE_TYPES}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onReconnect={onReconnect}
|
||||
onReconnectStart={onReconnectStart}
|
||||
onReconnectEnd={onReconnectEnd}
|
||||
onConnect={onConnect}
|
||||
snapToGrid
|
||||
fitView
|
||||
proOptions={{hideAttribution: true}}
|
||||
>
|
||||
<Panel position="top-center" className={styles.dndPanel}>
|
||||
<DndToolbar/> {/* contains the drag and drop panel for nodes */}
|
||||
</Panel>
|
||||
<Controls/>
|
||||
<Background/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -128,6 +130,7 @@ function VisualProgrammingUI() {
|
||||
function runProgram() {
|
||||
const program = graphReducer();
|
||||
console.log(program);
|
||||
console.log(JSON.stringify(program, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,14 +15,15 @@ import type {
|
||||
Phase,
|
||||
PhaseReducer,
|
||||
PreparedGraph,
|
||||
PreparedPhase
|
||||
PreparedPhase, Reduced, TriggerReducer
|
||||
} from "./GraphReducerTypes.ts";
|
||||
import type {
|
||||
AppNode,
|
||||
GoalNode,
|
||||
NormNode,
|
||||
PhaseNode
|
||||
PhaseNode, TriggerNode
|
||||
} from "./VisProgTypes.tsx";
|
||||
import type {TriggerNodeProps} from "./components/TriggerNodeComponent.tsx";
|
||||
|
||||
/**
|
||||
* Reduces the current graph inside the visual programming editor into a BehaviorProgram
|
||||
@@ -31,13 +32,15 @@ import type {
|
||||
* @param {PhaseReducer} phaseReducer
|
||||
* @param {NormReducer} normReducer
|
||||
* @param {GoalReducer} goalReducer
|
||||
* @param {TriggerReducer} triggerReducer
|
||||
* @returns {BehaviorProgram}
|
||||
*/
|
||||
export default function graphReducer(
|
||||
graphPreprocessor: GraphPreprocessor = defaultGraphPreprocessor,
|
||||
phaseReducer: PhaseReducer = defaultPhaseReducer,
|
||||
normReducer: NormReducer = defaultNormReducer,
|
||||
goalReducer: GoalReducer = defaultGoalReducer
|
||||
goalReducer: GoalReducer = defaultGoalReducer,
|
||||
triggerReducer: TriggerReducer = defaultTriggerReducer,
|
||||
) : BehaviorProgram {
|
||||
const nodes: AppNode[] = useFlowStore.getState().nodes;
|
||||
const edges: Edge[] = useFlowStore.getState().edges;
|
||||
@@ -47,7 +50,8 @@ export default function graphReducer(
|
||||
phaseReducer(
|
||||
preparedPhase,
|
||||
normReducer,
|
||||
goalReducer
|
||||
goalReducer,
|
||||
triggerReducer,
|
||||
));
|
||||
};
|
||||
|
||||
@@ -58,12 +62,14 @@ export default function graphReducer(
|
||||
* @param {PreparedPhase} phase
|
||||
* @param {NormReducer} normReducer
|
||||
* @param {GoalReducer} goalReducer
|
||||
* @param {TriggerReducer} triggerReducer
|
||||
* @returns {Phase}
|
||||
*/
|
||||
export function defaultPhaseReducer(
|
||||
phase: PreparedPhase,
|
||||
normReducer: NormReducer = defaultNormReducer,
|
||||
goalReducer: GoalReducer = defaultGoalReducer
|
||||
goalReducer: GoalReducer = defaultGoalReducer,
|
||||
triggerReducer: TriggerReducer = defaultTriggerReducer,
|
||||
) : Phase {
|
||||
return {
|
||||
id: phase.phaseNode.id,
|
||||
@@ -71,7 +77,8 @@ export function defaultPhaseReducer(
|
||||
nextPhaseId: phase.nextPhaseId,
|
||||
phaseData: {
|
||||
norms: phase.connectedNorms.map(normReducer),
|
||||
goals: phase.connectedGoals.map(goalReducer)
|
||||
goals: phase.connectedGoals.map(goalReducer),
|
||||
triggers: phase.connectedTriggers.map(triggerReducer),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,11 +89,12 @@ export function defaultPhaseReducer(
|
||||
* @param {GoalNode} node
|
||||
* @returns {GoalData}
|
||||
*/
|
||||
function defaultGoalReducer(node: GoalNode) : GoalData {
|
||||
function defaultGoalReducer(node: GoalNode) : Reduced<GoalData> {
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.data.label,
|
||||
value: node.data.value
|
||||
description: node.data.description,
|
||||
achieved: node.data.achieved,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +104,7 @@ function defaultGoalReducer(node: GoalNode) : GoalData {
|
||||
* @param {NormNode} node
|
||||
* @returns {NormData}
|
||||
*/
|
||||
function defaultNormReducer(node: NormNode) :NormData {
|
||||
function defaultNormReducer(node: NormNode) :Reduced<NormData> {
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.data.label,
|
||||
@@ -104,6 +112,13 @@ function defaultNormReducer(node: NormNode) :NormData {
|
||||
}
|
||||
}
|
||||
|
||||
function defaultTriggerReducer(node: TriggerNode): Reduced<TriggerNodeProps> {
|
||||
return {
|
||||
id: node.id,
|
||||
...node.data,
|
||||
}
|
||||
}
|
||||
|
||||
// Graph preprocessing functions:
|
||||
|
||||
/**
|
||||
@@ -117,6 +132,7 @@ function defaultNormReducer(node: NormNode) :NormData {
|
||||
export function defaultGraphPreprocessor(nodes: AppNode[], edges: Edge[]) : PreparedGraph {
|
||||
const norms : NormNode[] = nodes.filter((node) => node.type === 'norm') as NormNode[];
|
||||
const goals : GoalNode[] = nodes.filter((node) => node.type === 'goal') as GoalNode[];
|
||||
const triggers : TriggerNode[] = nodes.filter((node) => node.type === 'trigger') as TriggerNode[];
|
||||
const orderedPhases : OrderedPhases = orderPhases(nodes, edges);
|
||||
|
||||
return orderedPhases.phaseNodes.map((phase: PhaseNode) : PreparedPhase => {
|
||||
@@ -125,7 +141,8 @@ export function defaultGraphPreprocessor(nodes: AppNode[], edges: Edge[]) : Prep
|
||||
phaseNode: phase,
|
||||
nextPhaseId: nextPhase as string,
|
||||
connectedNorms: getIncomers({id: phase.id}, norms,edges),
|
||||
connectedGoals: getIncomers({id: phase.id}, goals,edges)
|
||||
connectedGoals: getIncomers({id: phase.id}, goals,edges),
|
||||
connectedTriggers: getIncomers({id: phase.id}, triggers, edges),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -150,32 +167,29 @@ export function orderPhases(nodes: AppNode[],edges: Edge[]) : OrderedPhases {
|
||||
) : OrderedPhases => {
|
||||
// get the current phase and the next phases;
|
||||
const currentPhase = phases[currentIndex];
|
||||
const nextPhaseNodes = getOutgoers(currentPhase,phaseNodes,edges);
|
||||
const nextNodes = getOutgoers(currentPhase,nodes, edges);
|
||||
const nextPhaseNodes = getOutgoers(currentPhase, phaseNodes, edges);
|
||||
const nextNodes = getOutgoers(currentPhase, nodes, edges);
|
||||
|
||||
// handles adding of the next phase to the chain, and error handle if an invalid state is received
|
||||
if (nextPhaseNodes.length === 1 && nextNodes.length === 1) {
|
||||
connections.set(currentPhase.id, nextPhaseNodes[0].id);
|
||||
return nextPhase(phases.push(nextPhaseNodes[0] as PhaseNode) - 1, {phaseNodes: phases, connections: connections});
|
||||
} else {
|
||||
// handle erroneous states
|
||||
if (nextNodes.length === 0){
|
||||
throw new Error(`| INVALID PROGRAM | the source handle of "${currentPhase.id}" doesn't have any outgoing connections`);
|
||||
} else {
|
||||
if (nextNodes.length > 1) {
|
||||
throw new Error(`| INVALID PROGRAM | the source handle of "${currentPhase.id}" connects to too many targets`);
|
||||
} else {
|
||||
if (nextNodes[0].type === "end"){
|
||||
connections.set(currentPhase.id, "end");
|
||||
// returns the final output of the function
|
||||
return { phaseNodes: phases, connections: connections};
|
||||
} else {
|
||||
throw new Error(`| INVALID PROGRAM | the node "${nextNodes[0].id}" that "${currentPhase.id}" connects to is not a phase or end node`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// handle erroneous states
|
||||
if (nextNodes.length === 0) {
|
||||
throw new Error(`| INVALID PROGRAM | the source handle of "${currentPhase.id}" doesn't have any outgoing connections`);
|
||||
}
|
||||
if (nextNodes.length > 1) {
|
||||
throw new Error(`| INVALID PROGRAM | the source handle of "${currentPhase.id}" connects to too many targets`);
|
||||
}
|
||||
if (nextNodes[0].type === "end") {
|
||||
connections.set(currentPhase.id, "end");
|
||||
// returns the final output of the function
|
||||
return {phaseNodes: phases, connections: connections};
|
||||
}
|
||||
throw new Error(`| INVALID PROGRAM | the node "${nextNodes[0].id}" that "${currentPhase.id}" connects to is not a phase or end node`);
|
||||
}
|
||||
|
||||
// initializes the Map describing the connections between phase nodes
|
||||
// we need this Map to make sure we preserve this information,
|
||||
// so we don't need to do checks on the entire set of edges in further stages of the reduction algorithm
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type {Edge} from "@xyflow/react";
|
||||
import type {AppNode, GoalNode, NormNode, PhaseNode} from "./VisProgTypes.tsx";
|
||||
import type {AppNode, GoalNode, NormNode, PhaseNode, TriggerNode} from "./VisProgTypes.tsx";
|
||||
import type {TriggerNodeProps} from "./components/TriggerNodeComponent.tsx";
|
||||
|
||||
|
||||
export type Reduced<T> = { id: string } & T;
|
||||
|
||||
/**
|
||||
* defines how a norm is represented in the simplified behavior program
|
||||
*/
|
||||
export type NormData = {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
@@ -15,9 +17,9 @@ export type NormData = {
|
||||
* defines how a goal is represented in the simplified behavior program
|
||||
*/
|
||||
export type GoalData = {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
description: string;
|
||||
achieved: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -27,6 +29,7 @@ export type GoalData = {
|
||||
export type PhaseData = {
|
||||
norms: NormData[];
|
||||
goals: GoalData[];
|
||||
triggers: TriggerNodeProps[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -55,12 +58,14 @@ export type BehaviorProgram = Phase[];
|
||||
|
||||
|
||||
|
||||
export type NormReducer = (node: NormNode) => NormData;
|
||||
export type GoalReducer = (node: GoalNode) => GoalData;
|
||||
export type NormReducer = (node: NormNode) => Reduced<NormData>;
|
||||
export type GoalReducer = (node: GoalNode) => Reduced<GoalData>;
|
||||
export type TriggerReducer = (node: TriggerNode) => Reduced<TriggerNodeProps>;
|
||||
export type PhaseReducer = (
|
||||
preparedPhase: PreparedPhase,
|
||||
normReducer: NormReducer,
|
||||
goalReducer: GoalReducer
|
||||
goalReducer: GoalReducer,
|
||||
triggerReducer: TriggerReducer,
|
||||
) => Phase;
|
||||
|
||||
/**
|
||||
@@ -90,6 +95,7 @@ export type PreparedPhase = {
|
||||
nextPhaseId: string;
|
||||
connectedNorms: NormNode[];
|
||||
connectedGoals: GoalNode[];
|
||||
connectedTriggers: TriggerNode[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,18 +6,21 @@ import {
|
||||
type OnConnect,
|
||||
type OnReconnect,
|
||||
} from '@xyflow/react';
|
||||
import type {TriggerNodeProps} from "./components/TriggerNodeComponent.tsx";
|
||||
|
||||
|
||||
type defaultNodeData = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
type OurNode<T> = Node<T & defaultNodeData>;
|
||||
|
||||
export type StartNode = Node<defaultNodeData, 'start'>;
|
||||
export type EndNode = Node<defaultNodeData, 'end'>;
|
||||
export type GoalNode = Node<defaultNodeData & { value: string; }, 'goal'>;
|
||||
export type GoalNode = Node<defaultNodeData & { description: string; achieved: boolean; }, 'goal'>;
|
||||
export type NormNode = Node<defaultNodeData & { value: string; }, 'norm'>;
|
||||
export type PhaseNode = Node<defaultNodeData & { number: number; }, 'phase'>;
|
||||
|
||||
export type TriggerNode = OurNode<TriggerNodeProps>;
|
||||
|
||||
/**
|
||||
* a type meant to house different node types, currently not used
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'react';
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
import styles from "../../VisProg.module.css"
|
||||
import type {AppNode, PhaseNode, NormNode} from "../VisProgTypes.tsx";
|
||||
import type {AppNode, PhaseNode, NormNode, GoalNode, TriggerNode} from "../VisProgTypes.tsx";
|
||||
|
||||
|
||||
|
||||
@@ -106,10 +106,48 @@ export function addNode(nodeType: string, position: XYPosition) {
|
||||
id: `norm-${normNumber}`,
|
||||
type: nodeType,
|
||||
position,
|
||||
data: {label: `new norm node`, value: "Pepper should be formal"},
|
||||
data: {label: `new norm node`, value: ""},
|
||||
}
|
||||
return normNode;
|
||||
}
|
||||
case "goal":
|
||||
{
|
||||
const goalNodes= nds.filter((node) => node.type === 'goal');
|
||||
let goalNumber
|
||||
if (goalNodes.length > 0) {
|
||||
const finalGoalId : number = +(goalNodes[goalNodes.length - 1].id.split('-')[1]);
|
||||
goalNumber = finalGoalId + 1;
|
||||
} else {
|
||||
goalNumber = 1;
|
||||
}
|
||||
|
||||
const goalNode : GoalNode = {
|
||||
id: `goal-${goalNumber}`,
|
||||
type: nodeType,
|
||||
position,
|
||||
data: {label: `new goal node`, description: "", achieved: false},
|
||||
}
|
||||
return goalNode;
|
||||
}
|
||||
case "trigger":
|
||||
{
|
||||
const triggerNodes= nds.filter((node) => node.type === 'trigger');
|
||||
let triggerNumber
|
||||
if (triggerNodes.length > 0) {
|
||||
const finalGoalId : number = +(triggerNodes[triggerNodes.length - 1].id.split('-')[1]);
|
||||
triggerNumber = finalGoalId + 1;
|
||||
} else {
|
||||
triggerNumber = 1;
|
||||
}
|
||||
|
||||
const triggerNode : TriggerNode = {
|
||||
id: `trigger-${triggerNumber}`,
|
||||
type: nodeType,
|
||||
position,
|
||||
data: {label: `new trigger node`, type: "keywords", value: []},
|
||||
}
|
||||
return triggerNode;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Node ${nodeType} not found`);
|
||||
}
|
||||
@@ -161,6 +199,12 @@ export function DndToolbar() {
|
||||
<DraggableNode className={styles.draggableNodeNorm} nodeType="norm" onDrop={handleNodeDrop}>
|
||||
norm Node
|
||||
</DraggableNode>
|
||||
<DraggableNode className={styles.draggableNodeGoal} nodeType="goal" onDrop={handleNodeDrop}>
|
||||
goal Node
|
||||
</DraggableNode>
|
||||
<DraggableNode className={styles.draggableNodeTrigger} nodeType="trigger" onDrop={handleNodeDrop}>
|
||||
trigger Node
|
||||
</DraggableNode>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,8 +11,9 @@ import type {
|
||||
StartNode,
|
||||
EndNode,
|
||||
PhaseNode,
|
||||
NormNode
|
||||
NormNode, GoalNode
|
||||
} from "../VisProgTypes.tsx";
|
||||
import {TextField} from "../../../../components/TextField.tsx";
|
||||
|
||||
//Toolbar definitions
|
||||
|
||||
@@ -44,56 +45,6 @@ export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
|
||||
</NodeToolbar>);
|
||||
}
|
||||
|
||||
// Renaming component
|
||||
|
||||
/**
|
||||
* Adds a component that can be used to edit a node's label entry inside its Data
|
||||
* can be added to any custom node that has a label inside its Data
|
||||
*
|
||||
* @param {string} nodeLabel
|
||||
* @param {string} nodeId
|
||||
* @returns {React.JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export function EditableName({nodeLabel = "new node", nodeId} : { nodeLabel : string, nodeId: string}) {
|
||||
const {updateNodeData} = useFlowStore();
|
||||
|
||||
const updateData = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
const input = event.target.value;
|
||||
updateNodeData(nodeId, {label: input});
|
||||
event.currentTarget.setAttribute("readOnly", "true");
|
||||
window.getSelection()?.empty();
|
||||
event.currentTarget.classList.replace("nodrag", "drag"); // enable dragging of the node with cursor on the input box
|
||||
};
|
||||
|
||||
const updateOnEnter = (event: React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "Enter") (event.target as HTMLInputElement).blur(); };
|
||||
|
||||
const enableEditing = (event: React.MouseEvent<HTMLInputElement>) => {
|
||||
if(event.currentTarget.hasAttribute("readOnly")) {
|
||||
event.currentTarget.removeAttribute("readOnly"); // enable editing
|
||||
event.currentTarget.select(); // select the text input
|
||||
window.getSelection()?.collapseToEnd(); // move the caret to the end of the current value
|
||||
event.currentTarget.classList.replace("drag", "nodrag"); // disable dragging using input box
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.NodeTextBar }>
|
||||
<label>name: </label>
|
||||
<input
|
||||
className={`drag ${styles.nodeTextInput}`} // prevents dragging the component when user has focused the text input
|
||||
type={"text"}
|
||||
defaultValue={nodeLabel}
|
||||
onKeyDown={updateOnEnter}
|
||||
onBlur={updateData}
|
||||
onClick={enableEditing}
|
||||
maxLength={25}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Definitions of Nodes
|
||||
|
||||
@@ -148,11 +99,25 @@ export const EndNodeComponent = ({id, data}: NodeProps<EndNode>) => {
|
||||
* @constructor
|
||||
*/
|
||||
export const PhaseNodeComponent = ({id, data}: NodeProps<PhaseNode>) => {
|
||||
const {updateNodeData} = useFlowStore();
|
||||
|
||||
const updateLabel = (value: string) => updateNodeData(id, {...data, label: value});
|
||||
|
||||
const label_input_id = `phase_${id}_label_input`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar nodeId={id} allowDelete={true}/>
|
||||
<div className={`${styles.defaultNode} ${styles.nodePhase}`}>
|
||||
<EditableName nodeLabel={data.label} nodeId={id}/>
|
||||
<div className={"flex-row gap-sm"}>
|
||||
<label htmlFor={label_input_id}>Name:</label>
|
||||
<TextField
|
||||
id={label_input_id}
|
||||
value={data.label}
|
||||
setValue={updateLabel}
|
||||
placeholder={"Phase ..."}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} id="target"/>
|
||||
<Handle type="target" position={Position.Bottom} id="norms"/>
|
||||
<Handle type="source" position={Position.Right} id="source"/>
|
||||
@@ -167,17 +132,69 @@ export const PhaseNodeComponent = ({id, data}: NodeProps<PhaseNode>) => {
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {defaultNodeData & {value: string}} data
|
||||
* @returns {React.JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export const NormNodeComponent = ({id, data}: NodeProps<NormNode>) => {
|
||||
return (
|
||||
<>
|
||||
<Toolbar nodeId={id} allowDelete={true}/>
|
||||
<div className={`${styles.defaultNode} ${styles.nodeNorm}`}>
|
||||
<EditableName nodeLabel={data.label} nodeId={id}/>
|
||||
<Handle type="source" position={Position.Right} id="NormSource"/>
|
||||
const {updateNodeData} = useFlowStore();
|
||||
|
||||
const text_input_id = `norm_${id}_text_input`;
|
||||
|
||||
const setValue = (value: string) => {
|
||||
updateNodeData(id, {value: value});
|
||||
}
|
||||
|
||||
return <>
|
||||
<Toolbar nodeId={id} allowDelete={true}/>
|
||||
<div className={`${styles.defaultNode} ${styles.nodeNorm}`}>
|
||||
<div className={"flex-row gap-sm"}>
|
||||
<label htmlFor={text_input_id}>Norm:</label>
|
||||
<TextField
|
||||
id={text_input_id}
|
||||
value={data.value}
|
||||
setValue={(val) => setValue(val)}
|
||||
placeholder={"Pepper should ..."}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
<Handle type="source" position={Position.Right} id="NormSource"/>
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
export const GoalNodeComponent = ({id, data}: NodeProps<GoalNode>) => {
|
||||
const {updateNodeData} = useFlowStore();
|
||||
|
||||
const text_input_id = `goal_${id}_text_input`;
|
||||
const checkbox_id = `goal_${id}_checkbox`;
|
||||
|
||||
const setDescription = (value: string) => {
|
||||
updateNodeData(id, {...data, description: value});
|
||||
}
|
||||
|
||||
const setAchieved = (value: boolean) => {
|
||||
updateNodeData(id, {...data, achieved: value});
|
||||
}
|
||||
|
||||
return <>
|
||||
<Toolbar nodeId={id} allowDelete={true}/>
|
||||
<div className={`${styles.defaultNode} ${styles.nodeGoal} flex-col gap-sm`}>
|
||||
<div className={"flex-row gap-md"}>
|
||||
<label htmlFor={text_input_id}>Goal:</label>
|
||||
<TextField
|
||||
id={text_input_id}
|
||||
value={data.description}
|
||||
setValue={(val) => setDescription(val)}
|
||||
placeholder={"To ..."}
|
||||
/>
|
||||
</div>
|
||||
<div className={"flex-row gap-md align-center"}>
|
||||
<label htmlFor={checkbox_id}>Achieved:</label>
|
||||
<input
|
||||
id={checkbox_id}
|
||||
type={"checkbox"}
|
||||
value={data.achieved ? "checked" : ""}
|
||||
onChange={(e) => setAchieved(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="source" position={Position.Right} id="GoalSource"/>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import {Handle, type NodeProps, Position} from "@xyflow/react";
|
||||
import type {TriggerNode} from "../VisProgTypes.tsx";
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
import styles from "../../VisProg.module.css";
|
||||
import {RealtimeTextField, TextField} from "../../../../components/TextField.tsx";
|
||||
import {Toolbar} from "./NodeDefinitions.tsx";
|
||||
import {useState} from "react";
|
||||
import duplicateIndices from "../../../../utils/duplicateIndices.ts";
|
||||
|
||||
export type EmotionTriggerNodeProps = {
|
||||
type: "emotion";
|
||||
value: string;
|
||||
}
|
||||
|
||||
type Keyword = { id: string, keyword: string };
|
||||
|
||||
export type KeywordTriggerNodeProps = {
|
||||
type: "keywords";
|
||||
value: Keyword[];
|
||||
}
|
||||
|
||||
export type TriggerNodeProps = EmotionTriggerNodeProps | KeywordTriggerNodeProps;
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
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} />
|
||||
</>;
|
||||
}
|
||||
|
||||
export default function TriggerNodeComponent({
|
||||
id,
|
||||
data,
|
||||
}: NodeProps<TriggerNode>) {
|
||||
const {updateNodeData} = useFlowStore();
|
||||
|
||||
const setKeywords = (keywords: Keyword[]) => {
|
||||
updateNodeData(id, {...data, value: keywords});
|
||||
}
|
||||
|
||||
return <>
|
||||
<Toolbar nodeId={id} allowDelete={true}/>
|
||||
<div className={`${styles.defaultNode} ${styles.nodeTrigger} flex-col gap-sm`}>
|
||||
{data.type === "emotion" && (
|
||||
<div className={"flex-row gap-md"}>Emotion?</div>
|
||||
)}
|
||||
{data.type === "keywords" && (
|
||||
<Keywords
|
||||
keywords={data.value}
|
||||
setKeywords={setKeywords}
|
||||
/>
|
||||
)}
|
||||
<Handle type="source" position={Position.Right} id="TriggerSource"/>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
Reference in New Issue
Block a user