update to node site

This commit is contained in:
2026-04-15 05:02:45 +10:00
parent 3c45406f6f
commit 7e7e6f2c22
710 changed files with 10532 additions and 8324 deletions

View File

@@ -0,0 +1,40 @@
/**
* @file BindEventNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
/**
* @UCLASS(BlueprintType)
* Binds the OnMessage event on a ChatNode. When a message arrives, updates
* the username/message output pins and fires the event_out exec pin.
*/
export class BindEventNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "bind_event";
/**
* @UFUNCTION(BlueprintCallable)
* @param {import('./NodeExecutionContext.js').NodeExecutionContext} ctx
*/
async BlueprintCallable_Execute(ctx) {
const chatInstance = ctx.BlueprintPure_ResolveInput("target");
const node = ctx.BlueprintPure_GetNode();
if (!chatInstance || typeof chatInstance.setOnMessage !== "function" || !node) return;
chatInstance.setOnMessage((username, message) => {
const usernamePin = node.outputs.find(p => p.id === "username");
const messagePin = node.outputs.find(p => p.id === "message");
if (usernamePin) usernamePin.defaultValue = username;
if (messagePin) messagePin.defaultValue = message;
// Fire event_out — async, fire-and-forget
ctx.BlueprintCallable_RunExecPin("event_out").catch(console.error);
});
}
}
NodeRegistry.UCLASS_Register(BindEventNode);

View File

@@ -0,0 +1,43 @@
/**
* @file ButtonNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
/**
* @UCLASS(BlueprintType)
* Renders a play button in the header that triggers node execution.
*/
export class ButtonNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "button";
/**
* @UFUNCTION(BlueprintNativeEvent)
* @param {HTMLElement} article
* @param {import('../GraphNode.js').GraphNode} graphNode
* @param {import('./NodeRenderContext.js').NodeRenderContext} renderCtx
*/
BlueprintNativeEvent_OnRender(article, graphNode, renderCtx) {
const header = article.querySelector(".node-header");
if (!header) return;
const btn = document.createElement("button");
btn.className = "node-run-btn";
btn.title = "Execute";
btn.innerHTML = `<svg width="9" height="9" viewBox="0 0 9 9" fill="currentColor" aria-hidden="true"><polygon points="1,0 9,4.5 1,9"/></svg>`;
btn.addEventListener("click", (e) => {
e.stopPropagation();
renderCtx.BlueprintCallable_TriggerNodeExecute(graphNode.id);
btn.classList.add("node-run-btn--flash");
btn.addEventListener("animationend", () => btn.classList.remove("node-run-btn--flash"), { once: true });
});
header.appendChild(btn);
}
}
NodeRegistry.UCLASS_Register(ButtonNode);

View File

@@ -0,0 +1,29 @@
/**
* @file ChatConnectNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
/**
* @UCLASS(BlueprintType)
* Calls connect() on a ChatNode instance received via the target input.
*/
export class ChatConnectNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "chat_connect";
/**
* @UFUNCTION(BlueprintCallable)
* @param {import('./NodeExecutionContext.js').NodeExecutionContext} ctx
*/
async BlueprintCallable_Execute(ctx) {
const chatInstance = ctx.BlueprintPure_ResolveInput("target");
if (chatInstance && typeof chatInstance.connect === "function") {
chatInstance.connect();
}
}
}
NodeRegistry.UCLASS_Register(ChatConnectNode);

View File

@@ -0,0 +1,49 @@
/**
* @file CustomEventNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
const HAND_SVG = `<svg viewBox="0 0 106.17 122.88" fill="currentColor"><path d="M29.96,67.49c-0.16-0.09-0.32-0.19-0.47-0.31c-1.95-1.56-4.08-3.29-5.94-4.81c-2.69-2.2-5.8-4.76-7.97-6.55 c-1.49-1.23-3.17-2.07-4.75-2.39c-1.02-0.2-1.95-0.18-2.67,0.12c-0.59,0.24-1.1,0.72-1.45,1.48c-0.45,0.99-0.66,2.41-0.54,4.32 c0.11,1.69,0.7,3.55,1.48,5.33c1.16,2.63,2.73,5.04,3.89,6.59c0.07,0.09,0.13,0.19,0.19,0.29l23.32,33.31 c0.3,0.43,0.47,0.91,0.53,1.4l0.01,0c0.46,3.85,1.28,6.73,2.49,8.54c0.88,1.31,2.01,1.98,3.42,1.94l0.07,0v-0.01h36.38 c0.09,0,0.17,0,0.26,0.01c2.28-0.03,4.36-0.71,6.25-2.02c2.09-1.44,3.99-3.68,5.72-6.7c0.03-0.05,0.06-0.11,0.1-0.16 c0.67-1.15,1.55-2.6,2.41-4.02c3.72-6.13,6.96-11.45,7.35-19.04L99.8,74.34c-0.02-0.15-0.03-0.3-0.03-0.45 c0-0.14,0.02-1.13,0.03-2.46c0.09-6.92,0.19-15.48-6.14-16.56h-4.05l-0.04,0c-0.02,1.95-0.15,3.93-0.27,5.86 c-0.11,1.71-0.21,3.37-0.21,4.95c0,1.7-1.38,3.08-3.08,3.08c-1.7,0-3.08-1.38-3.08-3.08c0-1.58,0.12-3.42,0.24-5.33 c0.41-6.51,0.89-13.99-4.33-14.93H74.8c-0.23,0-0.45-0.02-0.66-0.07c0.04,2.36-0.12,4.81-0.27,7.16c-0.11,1.71-0.21,3.37-0.21,4.95 c0,1.7-1.38,3.08-3.08,3.08c-1.7,0-3.08-1.38-3.08-3.08c0-1.58,0.12-3.42,0.24-5.33c0.41-6.51,0.89-13.99-4.33-14.93h-4.05 c-0.28,0-0.55-0.04-0.8-0.11V49c0,1.7-1.38,3.08-3.08,3.08c-1.7,0-3.08-1.38-3.08-3.08V17.05c0-5.35-2.18-8.73-4.97-10.14 c-1.02-0.52-2.12-0.78-3.21-0.78c-1.08,0-2.18,0.26-3.19,0.77c-2.76,1.4-4.92,4.79-4.92,10.28v56c0,1.7-1.38,3.08-3.08,3.08 c-1.7,0-3.08-1.38-3.08-3.08V67.49L29.96,67.49z M58.57,31.15c0.26-0.07,0.53-0.11,0.8-0.11h4.24c0.24,0,0.47,0.03,0.69,0.08 c5.65,0.88,8.17,4.18,9.2,8.43c0.39-0.18,0.83-0.29,1.3-0.29h4.24c0.24,0,0.47,0.03,0.69,0.08c6.08,0.94,8.53,4.69,9.41,9.41 c0.15-0.02,0.31-0.04,0.47-0.04h4.24c0.24,0,0.47,0.03,0.69,0.08c11.64,1.8,11.5,13.37,11.38,22.71c0,0.33-0.01,0.68-0.01,2.35 l0,0.07l0.24,10.77c0.01,0.11,0.01,0.23,0,0.34c-0.45,9.16-4.07,15.12-8.24,21.98c-0.7,1.14-1.41,2.32-2.34,3.93 c-0.02,0.04-0.04,0.08-0.07,0.13c-2.18,3.8-4.7,6.71-7.57,8.69c-2.92,2.02-6.16,3.06-9.71,3.1c-0.09,0.01-0.19,0.01-0.28,0.01 H41.58v-0.01c-3.66,0.07-6.5-1.53-8.59-4.66c-1.68-2.51-2.79-6.03-3.4-10.47L6.73,75.07c-0.03-0.04-0.07-0.08-0.1-0.12 c-1.36-1.82-3.21-4.65-4.59-7.79C1,64.8,0.21,62.24,0.05,59.74c-0.2-2.97,0.22-5.36,1.06-7.23c1.05-2.32,2.72-3.83,4.74-4.66 c1.89-0.77,4.01-0.88,6.16-0.45c2.57,0.51,5.22,1.81,7.49,3.68c1.86,1.54,4.95,4.07,7.95,6.52l2.52,2.06V17.18 c0-8.14,3.63-13.39,8.28-15.76C40.12,0.47,42.17,0,44.23,0c2.05,0,4.1,0.48,5.98,1.43c4.69,2.37,8.36,7.62,8.36,15.62V31.15 L58.57,31.15z"/></svg>`;
/**
* @UCLASS(BlueprintType)
* Entry-point event node. Adds pulsate animation and a hand-pointer hint.
*/
export class CustomEventNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "custom_event";
/**
* @UFUNCTION(BlueprintNativeEvent)
* @param {HTMLElement} article
* @param {import('../GraphNode.js').GraphNode} graphNode
*/
BlueprintNativeEvent_OnRender(article, graphNode) {
const outputs = article.querySelector(".node-outputs");
if (!outputs) return;
const execPin = outputs.querySelector('[data-pin-id="exec_out"]');
if (!execPin) return;
execPin.classList.add("pin--pulsate");
execPin.style.position = "relative";
const handHint = document.createElement("div");
handHint.className = "pin-hint-hand";
handHint.innerHTML = HAND_SVG;
handHint.style.color = "#fff";
execPin.appendChild(handHint);
// Dismiss hand permanently on first click of the pin (handle or label)
const dismiss = () => {
article.classList.add("hint-dismissed");
execPin.removeEventListener("click", dismiss);
};
execPin.addEventListener("click", dismiss);
}
}
NodeRegistry.UCLASS_Register(CustomEventNode);

View File

@@ -0,0 +1,38 @@
/**
* @file GetMatrixChatNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
import { GlobalVariables } from '../GlobalVariables.js';
/**
* @UCLASS(BlueprintType)
* Pure getter node that exposes the "MatrixChat" global as an object output.
* Styled with a semi-transparent blue header to visually denote a variable getter.
*/
export class GetMatrixChatNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "get_matrix_chat";
/**
* @UFUNCTION(BlueprintNativeEvent)
* @param {HTMLElement} article
* @param {import('../GraphNode.js').GraphNode} graphNode
*/
BlueprintNativeEvent_OnRender(article, graphNode) {
const header = article.querySelector(".node-header");
if (header) {
header.style.background = "rgba(100, 181, 246, 0.2)";
header.style.borderColor = "rgba(100, 181, 246, 0.4)";
}
const outputPin = graphNode.outputs.find(p => p.id === "value");
if (outputPin && GlobalVariables.has("MatrixChat")) {
outputPin.defaultValue = GlobalVariables.get("MatrixChat");
}
}
}
NodeRegistry.UCLASS_Register(GetMatrixChatNode);

View File

@@ -0,0 +1,35 @@
/**
* @file InfoNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
/**
* @UCLASS(BlueprintType)
* Renders a markdown body below the node header.
*/
export class InfoNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "info";
/**
* @UFUNCTION(BlueprintNativeEvent)
* @param {HTMLElement} article
* @param {import('../GraphNode.js').GraphNode} graphNode
* @param {import('./NodeRenderContext.js').NodeRenderContext} renderCtx
*/
BlueprintNativeEvent_OnRender(article, graphNode, renderCtx) {
if (!graphNode.markdownSrc) return;
const body = document.createElement("div");
body.className = "node-body";
body.setAttribute("aria-live", "polite");
article.appendChild(body);
renderCtx.BlueprintCallable_LoadMarkdown(graphNode.markdownSrc, body);
}
}
NodeRegistry.UCLASS_Register(InfoNode);

View File

@@ -0,0 +1,43 @@
/**
* @file LottieNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
/**
* @UCLASS(BlueprintType)
* Renders a Lottie animation JSON file inside a node body using the
* `<lottie-player>` web component (loaded via CDN in index.html).
* Set `lottieSrc` on the graph node data to point at a Lottie JSON file.
*/
export class LottieNode extends NodeBase {
/** @type {string} */
static NodeType = "lottie";
/**
* @UFUNCTION(BlueprintNativeEvent)
* @param {HTMLElement} article
* @param {import('../GraphNode.js').GraphNode} graphNode
*/
BlueprintNativeEvent_OnRender(article, graphNode) {
if (!graphNode.lottieSrc) return;
const body = document.createElement("div");
body.className = "node-body node-body--lottie";
const player = document.createElement("lottie-player");
player.setAttribute("src", graphNode.lottieSrc);
player.setAttribute("background", "transparent");
player.setAttribute("speed", "1");
player.setAttribute("loop", "");
player.setAttribute("autoplay", "");
player.className = "node-lottie";
body.appendChild(player);
article.appendChild(body);
}
}
NodeRegistry.UCLASS_Register(LottieNode);

View File

@@ -0,0 +1,84 @@
/**
* @file MatrixChatNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
import { ChatNode } from '../ChatNode.js';
import { GlobalVariables } from '../GlobalVariables.js';
/**
* @UCLASS(BlueprintType)
* Renders a full Matrix chat UI inside the node body.
* Instantiates a ChatNode and registers it as the "MatrixChat" global.
*/
export class MatrixChatNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "chat";
/**
* @UFUNCTION(BlueprintNativeEvent)
* @param {HTMLElement} article
* @param {import('../GraphNode.js').GraphNode} graphNode
* @param {import('./NodeRenderContext.js').NodeRenderContext} renderCtx
*/
BlueprintNativeEvent_OnRender(article, graphNode, renderCtx) {
const body = document.createElement("div");
body.className = "node-body node-body--chat";
const statusBar = document.createElement("div");
statusBar.className = "chat-status-bar";
const statusEl = document.createElement("span");
statusEl.className = "chat-status chat-status-disconnected";
statusEl.textContent = "Not connected";
statusBar.appendChild(statusEl);
const messagesContainer = document.createElement("div");
messagesContainer.className = "chat-messages";
const inputContainer = document.createElement("div");
inputContainer.className = "chat-input-container";
const messageInput = document.createElement("input");
messageInput.type = "text";
messageInput.className = "chat-input";
messageInput.placeholder = "Type a message or /nick...";
messageInput.addEventListener("focus", () => {
// On mobile the virtual keyboard shrinks the viewport. Delay lets the
// keyboard finish animating before we scroll the input into view.
window.setTimeout(() => {
messageInput.scrollIntoView({ behavior: "smooth", block: "nearest" });
}, 300);
});
inputContainer.appendChild(messageInput);
body.appendChild(statusBar);
body.appendChild(messagesContainer);
body.appendChild(inputContainer);
article.appendChild(body);
const homeserverPin = graphNode.inputs.find(p => p.id === "homeserver");
const roomPin = graphNode.inputs.find(p => p.id === "room");
const homeserver = homeserverPin?.defaultValue || "https://matrix.org";
const roomAlias = roomPin?.defaultValue || "#general:matrix.org";
const chatInstance = new ChatNode(homeserver, roomAlias);
// Pass resolver function so chat can resolve name at connect time
chatInstance.initialize(
messagesContainer,
messageInput,
statusEl,
inputContainer,
homeserver,
roomAlias,
() => renderCtx.BlueprintPure_ResolveInputValue?.(graphNode.id, "guestName") || ""
);
renderCtx.BlueprintCallable_RegisterChatInstance(graphNode.id, chatInstance);
GlobalVariables.set("MatrixChat", chatInstance);
}
}
NodeRegistry.UCLASS_Register(MatrixChatNode);

View File

@@ -0,0 +1,53 @@
/**
* @file NodeBase.js
* Abstract base class for all Blueprint graph node types.
*
* Method prefixes mirror Unreal Engine UFUNCTION macro conventions:
* BlueprintNativeEvent_ — has a native default implementation; subclasses may override
* BlueprintCallable_ — has exec-pin side effects; called during graph traversal
* BlueprintPure_ — side-effect free; does not advance exec flow
*/
/**
* @typedef {import('../GraphNode.js').GraphNode} GraphNode
* @typedef {import('./NodeRenderContext.js').NodeRenderContext} NodeRenderContext
* @typedef {import('./NodeExecutionContext.js').NodeExecutionContext} NodeExecutionContext
*/
/**
* @UCLASS(Abstract)
* Base class all node handlers inherit from. Registered via NodeRegistry.
*/
export class NodeBase {
/**
* The graph type string that maps to this class in the registry.
* Must be overridden by every subclass.
* @UCLASS(BlueprintType)
* @type {string}
*/
static NodeType = "";
/**
* Called after the node's base DOM (header, pins) has been built.
* Override to inject type-specific UI into the article element.
*
* @UFUNCTION(BlueprintNativeEvent)
* @param {HTMLElement} article
* @param {GraphNode} graphNode
* @param {NodeRenderContext} renderCtx
*/
// eslint-disable-next-line no-unused-vars
BlueprintNativeEvent_OnRender(article, graphNode, renderCtx) {}
/**
* Called when this node is reached during exec graph traversal.
* Override to implement node-specific behaviour.
* Exec flow continues automatically after this returns.
*
* @UFUNCTION(BlueprintCallable)
* @param {NodeExecutionContext} ctx
* @returns {Promise<void>}
*/
// eslint-disable-next-line no-unused-vars
async BlueprintCallable_Execute(ctx) {}
}

View File

@@ -0,0 +1,113 @@
/**
* @file NodeExecutionContext.js
* Runtime context passed to node handlers during graph traversal.
*/
/**
* @typedef {import('../GraphNode.js').GraphNode} GraphNode
* @typedef {import('../GraphConnection.js').GraphConnection} GraphConnection
* @typedef {import('../NodeRenderer.js').NodeRenderer} NodeRenderer
*/
/**
* Provides controlled access to runtime execution services.
* Passed to BlueprintCallable_Execute on every node handler invocation.
*/
export class NodeExecutionContext {
/** @type {string} */
#nodeId;
/** @type {NodeRenderer} */
#nodeRenderer;
/** @type {(nodeId: string, pinId: string) => any} */
#resolveInputFn;
/** @type {(nodeId: string, pinId: string) => Promise<void>} */
#runExecPinFn;
/** @type {((nodeId: string, value: string) => void) | null} */
#onPrint;
/** @type {((connId: string, kind: string) => Promise<void>) | null} */
#onStep;
/**
* @param {string} nodeId
* @param {NodeRenderer} nodeRenderer
* @param {(nodeId: string, pinId: string) => any} resolveInputFn
* @param {(nodeId: string, pinId: string) => Promise<void>} runExecPinFn
* @param {((nodeId: string, value: string) => void) | null} onPrint
* @param {((connId: string, kind: string) => Promise<void>) | null} onStep
*/
constructor(nodeId, nodeRenderer, resolveInputFn, runExecPinFn, onPrint, onStep) {
this.#nodeId = nodeId;
this.#nodeRenderer = nodeRenderer;
this.#resolveInputFn = resolveInputFn;
this.#runExecPinFn = runExecPinFn;
this.#onPrint = onPrint;
this.#onStep = onStep;
}
/**
* Returns the ID of the currently executing node.
*
* @UFUNCTION(BlueprintPure)
* @returns {string}
*/
BlueprintPure_GetNodeId() {
return this.#nodeId;
}
/**
* Returns the GraphNode data for the currently executing node.
*
* @UFUNCTION(BlueprintPure)
* @returns {GraphNode | null}
*/
BlueprintPure_GetNode() {
return this.#nodeRenderer.getNodes().find(n => n.id === this.#nodeId) ?? null;
}
/**
* Resolves the runtime value of a named input pin, following connections.
*
* @UFUNCTION(BlueprintPure)
* @param {string} pinId
* @returns {any}
*/
BlueprintPure_ResolveInput(pinId) {
return this.#resolveInputFn(this.#nodeId, pinId);
}
/**
* Returns all live graph connections.
*
* @UFUNCTION(BlueprintPure)
* @returns {GraphConnection[]}
*/
BlueprintPure_GetConnections() {
return this.#nodeRenderer.getConnections();
}
/**
* Triggers exec flow from an output pin on this node.
*
* @UFUNCTION(BlueprintCallable)
* @param {string} pinId
* @returns {Promise<void>}
*/
BlueprintCallable_RunExecPin(pinId) {
return this.#runExecPinFn(this.#nodeId, pinId);
}
/**
* Fires the print callback with a string value.
*
* @UFUNCTION(BlueprintCallable)
* @param {string} value
*/
BlueprintCallable_Print(value) {
this.#onPrint?.(this.#nodeId, value);
}
}

View File

@@ -0,0 +1,50 @@
/**
* @file NodeRegistry.js
* Static registry mapping node type strings to their handler instances.
*/
import { NodeBase } from './NodeBase.js';
/**
* Static registry for all Blueprint node types.
* Node classes self-register by calling UCLASS_Register during module init.
*/
export class NodeRegistry {
/** @type {Map<string, NodeBase>} */
static #registry = new Map();
/**
* Registers a node class by its static NodeType.
* Call once per class, typically at the bottom of the class file.
*
* @UFUNCTION(BlueprintCallable)
* @param {typeof NodeBase} NodeClass
*/
static UCLASS_Register(NodeClass) {
if (!NodeClass.NodeType) {
throw new Error(`[NodeRegistry] "${NodeClass.name}" is missing a static NodeType.`);
}
this.#registry.set(NodeClass.NodeType, new NodeClass());
}
/**
* Returns the handler instance for a given node type, or null.
*
* @UFUNCTION(BlueprintPure)
* @param {string} nodeType
* @returns {NodeBase | null}
*/
static BlueprintPure_Get(nodeType) {
return this.#registry.get(nodeType) ?? null;
}
/**
* Returns all currently registered type strings.
*
* @UFUNCTION(BlueprintPure)
* @returns {string[]}
*/
static BlueprintPure_GetRegisteredTypes() {
return [...this.#registry.keys()];
}
}

View File

@@ -0,0 +1,137 @@
/**
* @file NodeRenderContext.js
* Render-time context passed to node handlers when building their DOM.
*/
/**
* @typedef {import('../GraphNode.js').GraphNode} GraphNode
*/
/**
* Wraps all render-time services nodes may need during BlueprintNativeEvent_OnRender.
* Keeps NodeRenderer internals encapsulated while giving nodes controlled access.
*/
export class NodeRenderContext {
/** @type {(src: string, target: HTMLElement) => Promise<void>} */
#loadMarkdown;
/** @type {(p: Promise<void>) => void} */
#addMarkdownPromise;
/** @type {((nodeId: string, pinId?: string) => void) | null} */
#onNodeExecute;
/** @type {(nodeId: string, instance: any) => void} */
#registerChatInstance;
/** @type {(nodeId: string, toggleEl: HTMLButtonElement) => void} */
#startTimer;
/** @type {(nodeId: string) => void} */
#stopTimer;
/** @type {() => string} */
#svgPlay;
/** @type {() => string} */
#svgStop;
/** @type {(nodeId: string, pinId: string) => any} */
#resolveInputValue;
/**
* @param {{
* loadMarkdown: (src: string, target: HTMLElement) => Promise<void>,
* addMarkdownPromise: (p: Promise<void>) => void,
* onNodeExecute: ((nodeId: string, pinId?: string) => void) | null,
* registerChatInstance: (nodeId: string, instance: any) => void,
* startTimer: (nodeId: string, toggleEl: HTMLButtonElement) => void,
* stopTimer: (nodeId: string) => void,
* svgPlay: () => string,
* svgStop: () => string,
* resolveInputValue: (nodeId: string, pinId: string) => any,
* }} callbacks
*/
constructor(callbacks) {
this.#loadMarkdown = callbacks.loadMarkdown;
this.#addMarkdownPromise = callbacks.addMarkdownPromise;
this.#onNodeExecute = callbacks.onNodeExecute;
this.#registerChatInstance = callbacks.registerChatInstance;
this.#startTimer = callbacks.startTimer;
this.#stopTimer = callbacks.stopTimer;
this.#svgPlay = callbacks.svgPlay;
this.#svgStop = callbacks.svgStop;
this.#resolveInputValue = callbacks.resolveInputValue;
}
/**
* @UFUNCTION(BlueprintCallable)
* @param {string} src
* @param {HTMLElement} target
*/
BlueprintCallable_LoadMarkdown(src, target) {
this.#addMarkdownPromise(this.#loadMarkdown(src, target));
}
/**
* @UFUNCTION(BlueprintCallable)
* @param {string} nodeId
* @param {string} [pinId]
*/
BlueprintCallable_TriggerNodeExecute(nodeId, pinId) {
this.#onNodeExecute?.(nodeId, pinId);
}
/**
* @UFUNCTION(BlueprintCallable)
* @param {string} nodeId
* @param {any} instance
*/
BlueprintCallable_RegisterChatInstance(nodeId, instance) {
this.#registerChatInstance(nodeId, instance);
}
/**
* @UFUNCTION(BlueprintCallable)
* @param {string} nodeId
* @param {HTMLButtonElement} toggleEl
*/
BlueprintCallable_StartTimer(nodeId, toggleEl) {
this.#startTimer(nodeId, toggleEl);
}
/**
* @UFUNCTION(BlueprintCallable)
* @param {string} nodeId
*/
BlueprintCallable_StopTimer(nodeId) {
this.#stopTimer(nodeId);
}
/**
* @UFUNCTION(BlueprintPure)
* @returns {string}
*/
BlueprintPure_GetSvgPlay() {
return this.#svgPlay();
}
/**
* @UFUNCTION(BlueprintPure)
* @returns {string}
*/
BlueprintPure_GetSvgStop() {
return this.#svgStop();
}
/**
* @UFUNCTION(BlueprintPure)
* Resolves an input pin value by following connections.
* @param {string} nodeId
* @param {string} pinId
* @returns {any}
*/
BlueprintPure_ResolveInputValue(nodeId, pinId) {
return this.#resolveInputValue(nodeId, pinId);
}
}

View File

@@ -0,0 +1,27 @@
/**
* @file PrintNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
/**
* @UCLASS(BlueprintType)
* Logs a string value and fires it through the print callback.
*/
export class PrintNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "print";
/**
* @UFUNCTION(BlueprintCallable)
* @param {import('./NodeExecutionContext.js').NodeExecutionContext} ctx
*/
async BlueprintCallable_Execute(ctx) {
const value = ctx.BlueprintPure_ResolveInput("value");
ctx.BlueprintCallable_Print(value);
}
}
NodeRegistry.UCLASS_Register(PrintNode);

View File

@@ -0,0 +1,38 @@
/**
* @file PureNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
/**
* @UCLASS(BlueprintType)
* Pure data node — optionally renders an image body. No exec logic.
*/
export class PureNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "pure";
/**
* @UFUNCTION(BlueprintNativeEvent)
* @param {HTMLElement} article
* @param {import('../GraphNode.js').GraphNode} graphNode
*/
BlueprintNativeEvent_OnRender(article, graphNode) {
if (!graphNode.imageSrc) return;
const body = document.createElement("div");
body.className = "node-body node-body--image";
const img = document.createElement("img");
img.src = graphNode.imageSrc;
img.alt = "";
img.className = "node-image";
body.appendChild(img);
article.appendChild(body);
}
}
NodeRegistry.UCLASS_Register(PureNode);

View File

@@ -0,0 +1,124 @@
/**
* @file RandomNameNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
/**
* @UCLASS(BlueprintType)
* Generates random names from first/second part combinations.
* Pre-runs on load and has a button to regenerate.
*/
export class RandomNameNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "random_name";
/** @type {Array<string>} */
static #firstParts = [];
/** @type {Array<string>} */
static #secondParts = [];
/** @type {Promise<void> | null} */
static #loadPromise = null;
/**
* Load name parts from JSON
*/
static async #loadNameParts() {
if (this.#loadPromise) return this.#loadPromise;
this.#loadPromise = (async () => {
try {
const response = await fetch('data/nameParts.json');
const data = await response.json();
this.#firstParts = data.firstParts || [];
this.#secondParts = data.secondParts || [];
} catch (err) {
console.error('Failed to load name parts:', err);
this.#firstParts = ['Random'];
this.#secondParts = ['Name'];
}
})();
return this.#loadPromise;
}
/**
* Generate random name
* @returns {string}
*/
static generateName() {
if (this.#firstParts.length === 0 || this.#secondParts.length === 0) {
return 'Loading...';
}
const first = this.#firstParts[Math.floor(Math.random() * this.#firstParts.length)];
const second = this.#secondParts[Math.floor(Math.random() * this.#secondParts.length)];
return `${first}${second}`;
}
/**
* @UFUNCTION(BlueprintNativeEvent)
* @param {HTMLElement} article
* @param {import('../GraphNode.js').GraphNode} graphNode
* @param {import('./NodeRenderContext.js').NodeRenderContext} renderCtx
*/
async BlueprintNativeEvent_OnRender(article, graphNode, renderCtx) {
await RandomNameNode.#loadNameParts();
// Generate initial name if not set
if (!graphNode._generatedName) {
graphNode._generatedName = RandomNameNode.generateName();
}
console.log('[RandomNameNode] Generated name:', graphNode._generatedName, 'for node', graphNode.id);
// Add button to header
const header = article.querySelector(".node-header");
if (header) {
const btn = document.createElement("button");
btn.className = "node-run-btn";
btn.title = "Generate New Name";
btn.innerHTML = `<svg width="9" height="9" viewBox="0 0 9 9" fill="currentColor" aria-hidden="true"><polygon points="1,0 9,4.5 1,9"/></svg>`;
btn.addEventListener("click", (e) => {
e.stopPropagation();
graphNode._generatedName = RandomNameNode.generateName();
nameDisplay.textContent = graphNode._generatedName;
btn.classList.add("node-run-btn--flash");
btn.addEventListener("animationend", () => btn.classList.remove("node-run-btn--flash"), { once: true });
});
header.appendChild(btn);
}
// Display name in body
const body = document.createElement("div");
body.className = "node-body node-body--text";
const nameDisplay = document.createElement("div");
nameDisplay.className = "node-name-display";
nameDisplay.textContent = graphNode._generatedName;
body.appendChild(nameDisplay);
article.appendChild(body);
}
/**
* @UFUNCTION(BlueprintNativeEvent)
* @param {import('../GraphNode.js').GraphNode} graphNode
* @param {string} pinId
* @returns {any}
*/
BlueprintNativeEvent_GetOutputValue(graphNode, pinId) {
if (pinId === 'name') {
return graphNode._generatedName || '';
}
return null;
}
}
NodeRegistry.UCLASS_Register(RandomNameNode);

View File

@@ -0,0 +1,36 @@
/**
* @file SequenceNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
/**
* @UCLASS(BlueprintType)
* Fires all then_N outputs in order, then continues exec flow.
*/
export class SequenceNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "sequence";
/**
* @UFUNCTION(BlueprintCallable)
* @param {import('./NodeExecutionContext.js').NodeExecutionContext} ctx
*/
async BlueprintCallable_Execute(ctx) {
const node = ctx.BlueprintPure_GetNode();
if (!node) return;
// Fire every then_N output in order
const thenPins = node.outputs
.filter(p => p.id.startsWith("then_"))
.sort((a, b) => a.id.localeCompare(b.id));
for (const pin of thenPins) {
await ctx.BlueprintCallable_RunExecPin(pin.id);
}
}
}
NodeRegistry.UCLASS_Register(SequenceNode);

View File

@@ -0,0 +1,46 @@
/**
* @file TimerNode.js
* @UCLASS(BlueprintType, Blueprintable)
*/
import { NodeBase } from './NodeBase.js';
import { NodeRegistry } from './NodeRegistry.js';
/**
* @UCLASS(BlueprintType)
* Renders a toggle button in the header that starts/stops an interval timer.
*/
export class TimerNode extends NodeBase {
/** @UCLASS(BlueprintType) */
static NodeType = "timer";
/**
* @UFUNCTION(BlueprintNativeEvent)
* @param {HTMLElement} article
* @param {import('../GraphNode.js').GraphNode} graphNode
* @param {import('./NodeRenderContext.js').NodeRenderContext} renderCtx
*/
BlueprintNativeEvent_OnRender(article, graphNode, renderCtx) {
const header = article.querySelector(".node-header");
if (!header) return;
const toggle = document.createElement("button");
toggle.className = "node-timer-btn";
toggle.title = "Start timer";
toggle.innerHTML = renderCtx.BlueprintPure_GetSvgPlay();
toggle.addEventListener("click", (e) => {
e.stopPropagation();
const running = toggle.dataset.running === "true";
if (running) {
renderCtx.BlueprintCallable_StopTimer(graphNode.id);
} else {
renderCtx.BlueprintCallable_StartTimer(graphNode.id, toggle);
}
});
header.appendChild(toggle);
}
}
NodeRegistry.UCLASS_Register(TimerNode);

View File

@@ -0,0 +1,25 @@
/**
* @file index.js
* Imports all node classes so they self-register with NodeRegistry.
* Import this module once at app startup before rendering any nodes.
*/
export { NodeBase } from './NodeBase.js';
export { NodeRegistry } from './NodeRegistry.js';
export { NodeExecutionContext } from './NodeExecutionContext.js';
export { NodeRenderContext } from './NodeRenderContext.js';
// Node types — each file calls NodeRegistry.UCLASS_Register on load
export { PrintNode } from './PrintNode.js';
export { SequenceNode } from './SequenceNode.js';
export { CustomEventNode } from './CustomEventNode.js';
export { ButtonNode } from './ButtonNode.js';
export { TimerNode } from './TimerNode.js';
export { InfoNode } from './InfoNode.js';
export { PureNode } from './PureNode.js';
export { MatrixChatNode } from './MatrixChatNode.js';
export { GetMatrixChatNode } from './GetMatrixChatNode.js';
export { ChatConnectNode } from './ChatConnectNode.js';
export { BindEventNode } from './BindEventNode.js';
export { LottieNode } from './LottieNode.js';
export { RandomNameNode } from './RandomNameNode.js';