Merge branch 'dev' into refactor/node-encapsulation

This commit is contained in:
Björn Otgaar
2025-11-18 12:35:53 +01:00
36 changed files with 2535 additions and 55 deletions

View File

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

View File

@@ -10,7 +10,6 @@ type ToolbarProps = {
allowDelete: boolean;
};
/**
* Node Toolbar definition:
* handles: node deleting functionality

View File

@@ -0,0 +1,121 @@
import {Handle, type NodeProps, Position} from "@xyflow/react";
import useFlowStore from "../VisProgStores.tsx";
import styles from "../../VisProg.module.css";
import {RealtimeTextField, TextField} from "../../../../components/TextField.tsx";
import {Toolbar} from "./NodeComponents.tsx";
import {useState} from "react";
import duplicateIndices from "../../../../utils/duplicateIndices.ts";
import type { TriggerNode } from "../nodes/TriggerNode.tsx";
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>
// </>;
// }

View File

@@ -0,0 +1,11 @@
import type { GoalNodeData } from "./GoalNode";
/**
* Default data for this node
*/
export const GoalNodeDefaults: GoalNodeData = {
label: "Goal Node",
droppable: true,
GoalList: [],
hasReduce: true,
};

View File

@@ -0,0 +1,78 @@
import {
Handle,
type NodeProps,
Position,
type Connection,
type Edge,
type Node,
} from '@xyflow/react';
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
/**
* The default data dot a Goal node
* @param label: the label of this Goal
* @param droppable: whether this node is droppable from the drop bar (initialized as true)
* @param children: ID's of children of this node
*/
export type GoalNodeData = {
label: string;
droppable: boolean;
GoalList: string[];
hasReduce: boolean;
};
export type GoalNode = Node<GoalNodeData>
export function GoalNodeCanConnect(connection: Connection | Edge): boolean {
return (connection != undefined);
}
/**
* Defines how a Goal node should be rendered
* @param props NodeProps, like id, label, children
* @returns React.JSX.Element
*/
export default function GoalNode(props: NodeProps<Node>) {
const label_input_id = `Goal_${props.id}_label_input`;
const data = props.data as GoalNodeData;
return (
<>
<Toolbar nodeId={props.id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeGoal}`}>
<div className={"flex-row gap-sm"}>
<label htmlFor={label_input_id}></label>
{props.data.label as string}
</div>
{data.GoalList.map((Goal) => (<div>{Goal}</div>))}
<Handle type="target" position={Position.Right} id="phase"/>
</div>
</>
);
}
/**
* Reduces each Goal, including its children down into its relevant data.
* @param props: The Node Properties of this node.
*/
export function GoalReduce(node: Node, nodes: Node[]) {
// Replace this for nodes functionality
if (nodes.length <= -1) {
console.warn("Impossible nodes length in GoalReduce")
}
const data = node.data as GoalNodeData;
return {
label: data.label,
list: data.GoalList,
}
}
export function GoalConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) {
// Replace this for connection logic
if (thisNode == undefined && otherNode == undefined && isThisSource == false) {
console.warn("Impossible node connection called in EndConnects")
}
}

View File

@@ -0,0 +1,11 @@
import type { TriggerNodeData } from "./TriggerNode";
/**
* Default data for this node
*/
export const TriggerNodeDefaults: TriggerNodeData = {
label: "Trigger Node",
droppable: true,
TriggerList: [],
hasReduce: true,
};

View File

@@ -0,0 +1,78 @@
import {
Handle,
type NodeProps,
Position,
type Connection,
type Edge,
type Node,
} from '@xyflow/react';
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
/**
* The default data dot a Trigger node
* @param label: the label of this Trigger
* @param droppable: whether this node is droppable from the drop bar (initialized as true)
* @param children: ID's of children of this node
*/
export type TriggerNodeData = {
label: string;
droppable: boolean;
TriggerList: string[];
hasReduce: boolean;
};
export type TriggerNode = Node<TriggerNodeData>
export function TriggerNodeCanConnect(connection: Connection | Edge): boolean {
return (connection != undefined);
}
/**
* Defines how a Trigger node should be rendered
* @param props NodeProps, like id, label, children
* @returns React.JSX.Element
*/
export default function TriggerNode(props: NodeProps<Node>) {
const label_input_id = `Trigger_${props.id}_label_input`;
const data = props.data as TriggerNodeData;
return (
<>
<Toolbar nodeId={props.id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeTrigger}`}>
<div className={"flex-row gap-sm"}>
<label htmlFor={label_input_id}></label>
{props.data.label as string}
</div>
{data.TriggerList.map((Trigger) => (<div>{Trigger}</div>))}
<Handle type="target" position={Position.Right} id="phase"/>
</div>
</>
);
}
/**
* Reduces each Trigger, including its children down into its relevant data.
* @param props: The Node Properties of this node.
*/
export function TriggerReduce(node: Node, nodes: Node[]) {
// Replace this for nodes functionality
if (nodes.length <= -1) {
console.warn("Impossible nodes length in TriggerReduce")
}
const data = node.data as TriggerNodeData;
return {
label: data.label,
list: data.TriggerList,
}
}
export function TriggerConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) {
// Replace this for connection logic
if (thisNode == undefined && otherNode == undefined && isThisSource == false) {
console.warn("Impossible node connection called in EndConnects")
}
}