"use client"; import { useCallback, useState, useEffect, useMemo, useRef } from "react"; import { createPortal } from "react-dom"; import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { useWorkflowStore } from "@/store/workflowStore"; import { PromptNodeData } from "@/types"; type PromptNodeType = Node; export function PromptNode({ id, data, selected }: NodeProps) { const nodeData = data; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const getConnectedInputs = useWorkflowStore((state) => state.getConnectedInputs); const edges = useWorkflowStore((state) => state.edges); // Local state for prompt to prevent cursor jumping during typing const [localPrompt, setLocalPrompt] = useState(nodeData.prompt); const [isEditing, setIsEditing] = useState(false); // Variable naming dialog state const [showVarDialog, setShowVarDialog] = useState(false); const [varNameInput, setVarNameInput] = useState(nodeData.variableName || ""); // Check if this node has any incoming text connections const hasIncomingTextConnection = useMemo(() => { return edges.some((edge) => edge.target === id && edge.targetHandle === "text"); }, [edges, id]); // Track the last received text from connected LLM node to detect when it changes const lastReceivedTextRef = useRef(null); // Get connected text input and update prompt when LLM output changes useEffect(() => { if (hasIncomingTextConnection) { const { text } = getConnectedInputs(id); // Only update if the incoming text changed (LLM node ran again) if (text !== null && text !== lastReceivedTextRef.current) { lastReceivedTextRef.current = text; updateNodeData(id, { prompt: text }); } } else { // Clear tracking when connection is removed lastReceivedTextRef.current = null; } }, [hasIncomingTextConnection, id, getConnectedInputs, updateNodeData]); // Sync from props when not actively editing useEffect(() => { if (!isEditing) { setLocalPrompt(nodeData.prompt); } }, [nodeData.prompt, isEditing]); const handleChange = useCallback( (e: React.ChangeEvent) => { setLocalPrompt(e.target.value); }, [] ); const handleFocus = useCallback(() => { setIsEditing(true); }, []); const handleBlur = useCallback(() => { setIsEditing(false); if (localPrompt !== nodeData.prompt) { updateNodeData(id, { prompt: localPrompt }); } }, [id, localPrompt, nodeData.prompt, updateNodeData]); const handleSaveVariableName = useCallback(() => { updateNodeData(id, { variableName: varNameInput || undefined }); setShowVarDialog(false); }, [id, varNameInput, updateNodeData]); const handleClearVariableName = useCallback(() => { setVarNameInput(""); updateNodeData(id, { variableName: undefined }); setShowVarDialog(false); }, [id, updateNodeData]); const handleVariableNameChange = useCallback((e: React.ChangeEvent) => { // Allow only alphanumeric and underscore, max 30 chars const sanitized = e.target.value.replace(/[^a-zA-Z0-9_]/g, "").slice(0, 30); setVarNameInput(sanitized); }, []); return ( <> {/* Text input handle - for receiving text from LLM nodes */}