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'; import useFlowStore from '../VisProgStores'; import { useState } from 'react'; import { RealtimeTextField, TextField } from '../../../../components/TextField'; import duplicateIndices from '../../../../utils/duplicateIndices'; /** * The default data structure for a Trigger node * * Represents configuration for a node that activates when a specific condition is met, * such as keywords being spoken or emotions detected. * * @property label: the display label of this Trigger node. * @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. */ export type TriggerNodeData = { label: string; droppable: boolean; triggerType: "keywords" | string; triggers: Keyword[] | never; hasReduce: boolean; }; export type TriggerNode = Node /** * Determines whether a Trigger node can connect to another node or edge. * * @param connection - The connection or edge being attempted to connect towards. * @returns `true` if the connection is defined; otherwise, `false`. */ export function TriggerNodeCanConnect(connection: Connection | Edge): boolean { return (connection != undefined); } /** * Defines how a Trigger node should be rendered * @param props - Node properties provided by React Flow, including `id` and `data`. * @returns The rendered TriggerNode React element (React.JSX.Element). */ export default function TriggerNode(props: NodeProps) { const data = props.data; const {updateNodeData} = useFlowStore(); const setKeywords = (keywords: Keyword[]) => { updateNodeData(props.id, {...data, triggers: keywords}); } return <>
{data.triggerType === "emotion" && (
Emotion?
)} {data.triggerType === "keywords" && ( )}
; } /** * Reduces each Trigger, including its children down into its core data. * @param node - The Trigger node to reduce. * @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. */ 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.triggers, } } /** * Handles logic that occurs when a connection is made involving a Trigger node. * * @param thisNode - The current Trigger node being connected. * @param otherNode - The other node involved in the connection. * @param isThisSource - Whether this node was the source of the connection. */ 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") } } // Definitions for the possible triggers, being keywords and emotions /** Represents a single keyword trigger entry. */ type Keyword = { id: string, keyword: string }; /** Properties for an emotion-type trigger node. */ export type EmotionTriggerNodeProps = { type: "emotion"; value: string; } /** Props for a keyword-type trigger node. */ export type KeywordTriggerNodeProps = { type: "keywords"; value: Keyword[]; } /** Union type for all possible Trigger node configurations. */ 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
{ if (!input) return; addKeyword(input); setInput(""); }} placeholder={"..."} className={"flex-1"} />
; } /** * 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([]); 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 <> Triggers when {keywords.length <= 1 ? "the keyword is" : "all keywords are"} spoken. {[...keywords].map(({id, keyword}, index) => { return
replace(id, val)} placeholder={"..."} className={"flex-1"} invalid={duplicates.includes(index)} />
; })} ; }