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

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