Browse Source

refactor: move array advanced options under collapsible section

handoff-20260429-1057
Marquis Boyd 5 months ago
parent
commit
60995a64d6
  1. 157
      src/components/nodes/ArrayNode.tsx

157
src/components/nodes/ArrayNode.tsx

@ -1,6 +1,6 @@
"use client"; "use client";
import { useCallback, useEffect, useMemo, useRef } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Handle, Node, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { Handle, Node, NodeProps, Position, useReactFlow } from "@xyflow/react";
import { BaseNode } from "./BaseNode"; import { BaseNode } from "./BaseNode";
import { useCommentNavigation } from "@/hooks/useCommentNavigation"; import { useCommentNavigation } from "@/hooks/useCommentNavigation";
@ -24,25 +24,38 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) {
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
const addNode = useWorkflowStore((state) => state.addNode); const addNode = useWorkflowStore((state) => state.addNode);
const onConnect = useWorkflowStore((state) => state.onConnect); const onConnect = useWorkflowStore((state) => state.onConnect);
const nodes = useWorkflowStore((state) => state.nodes); const hasIncomingTextConnection = useWorkflowStore((state) =>
const getConnectedInputs = useWorkflowStore((state) => state.getConnectedInputs); state.edges.some((edge) => {
const edges = useWorkflowStore((state) => state.edges); if (edge.target !== id) return false;
const { setNodes } = useReactFlow(); const handle = edge.targetHandle || "text";
const lastSyncedInputRef = useRef<string | null>(null); return handle === "text" || handle.startsWith("text-") || handle.includes("prompt");
const lastDerivedWriteRef = useRef<string | null>(null); })
);
const hasIncomingTextConnection = useMemo(() => { const connectedText = useWorkflowStore((state) => {
return edges.some((edge) => { const hasIncoming = state.edges.some((edge) => {
if (edge.target !== id) return false; if (edge.target !== id) return false;
const handle = edge.targetHandle || "text"; const handle = edge.targetHandle || "text";
return handle === "text" || handle.startsWith("text-") || handle.includes("prompt"); return handle === "text" || handle.startsWith("text-") || handle.includes("prompt");
}); });
}, [edges, id]); if (!hasIncoming) return null;
return state.getConnectedInputs(id).text;
});
const { setNodes, getNodes } = useReactFlow();
const lastSyncedInputRef = useRef<string | null>(null);
const lastDerivedWriteRef = useRef<string | null>(null);
const [showAdvanced, setShowAdvanced] = useState(false);
// Pull upstream text into this node whenever the connected input changes. // Pull upstream text into this node whenever the connected input changes.
useEffect(() => { useEffect(() => {
if (!hasIncomingTextConnection) return; if (!hasIncomingTextConnection) {
const { text } = getConnectedInputs(id); // Array node has no manual input field; clear stale upstream text on disconnect.
if (nodeData.inputText !== null && nodeData.inputText !== "") {
lastSyncedInputRef.current = null;
updateNodeData(id, { inputText: null });
}
return;
}
const text = connectedText;
if ( if (
text !== null && text !== null &&
text !== nodeData.inputText && text !== nodeData.inputText &&
@ -51,7 +64,7 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) {
lastSyncedInputRef.current = text; lastSyncedInputRef.current = text;
updateNodeData(id, { inputText: text }); updateNodeData(id, { inputText: text });
} }
}, [hasIncomingTextConnection, getConnectedInputs, id, nodeData.inputText, updateNodeData]); }, [connectedText, hasIncomingTextConnection, id, nodeData.inputText, updateNodeData]);
const parsed = useMemo(() => { const parsed = useMemo(() => {
return parseTextToArray(nodeData.inputText, { return parseTextToArray(nodeData.inputText, {
@ -83,20 +96,14 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) {
if (lastDerivedWriteRef.current === writeSignature) return; if (lastDerivedWriteRef.current === writeSignature) return;
lastDerivedWriteRef.current = writeSignature; lastDerivedWriteRef.current = writeSignature;
if ( updateNodeData(id, {
parsed.error !== nodeData.error || outputItems: parsed.items,
nextOutputText !== (nodeData.outputText ?? "[]") || outputText: nextOutputText,
!arraysEqual(parsed.items, nodeData.outputItems || []) error: parsed.error,
) { });
updateNodeData(id, {
outputItems: parsed.items,
outputText: nextOutputText,
error: parsed.error,
});
}
}, [id, nodeData.error, nodeData.outputItems, nodeData.outputText, parsed.error, parsed.items, updateNodeData]); }, [id, nodeData.error, nodeData.outputItems, nodeData.outputText, parsed.error, parsed.items, updateNodeData]);
const handleModeChange = useCallback( const handleBasicModeChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => { (e: React.ChangeEvent<HTMLSelectElement>) => {
updateNodeData(id, { splitMode: e.target.value as ArrayNodeData["splitMode"] }); updateNodeData(id, { splitMode: e.target.value as ArrayNodeData["splitMode"] });
}, },
@ -109,7 +116,7 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) {
const items = previewItems; const items = previewItems;
if (items.length === 0) return; if (items.length === 0) return;
const sourceNode = nodes.find((n) => n.id === id); const sourceNode = getNodes().find((n) => n.id === id);
if (!sourceNode) return; if (!sourceNode) return;
const sourceWidth = (sourceNode.style?.width as number) || 360; const sourceWidth = (sourceNode.style?.width as number) || 360;
@ -137,7 +144,7 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) {
}); });
updateNodeData(id, { selectedOutputIndex: previousSelected }); updateNodeData(id, { selectedOutputIndex: previousSelected });
}, [addNode, id, nodeData.selectedOutputIndex, nodes, onConnect, previewItems, updateNodeData]); }, [addNode, getNodes, id, nodeData.selectedOutputIndex, onConnect, previewItems, updateNodeData]);
// Reset selection if it no longer points to a valid parsed item. // Reset selection if it no longer points to a valid parsed item.
useEffect(() => { useEffect(() => {
@ -196,13 +203,12 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) {
<div className="grid grid-cols-[auto_1fr] gap-2 items-center"> <div className="grid grid-cols-[auto_1fr] gap-2 items-center">
<label className="text-[11px] text-neutral-400">Split</label> <label className="text-[11px] text-neutral-400">Split</label>
<select <select
value={nodeData.splitMode} value={nodeData.splitMode === "newline" ? "newline" : "delimiter"}
onChange={handleModeChange} onChange={handleBasicModeChange}
className="nodrag nopan bg-neutral-900 border border-neutral-700 rounded px-2 py-1 text-[11px] text-neutral-100 focus:outline-none focus:ring-1 focus:ring-neutral-600" className="nodrag nopan bg-neutral-900 border border-neutral-700 rounded px-2 py-1 text-[11px] text-neutral-100 focus:outline-none focus:ring-1 focus:ring-neutral-600"
> >
<option value="delimiter">Delimiter</option> <option value="delimiter">Delimiter</option>
<option value="newline">Newline</option> <option value="newline">Newline</option>
<option value="regex">Regex</option>
</select> </select>
</div> </div>
@ -218,37 +224,66 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) {
</div> </div>
)} )}
{nodeData.splitMode === "regex" && ( <div className="border border-neutral-700 rounded">
<div className="grid grid-cols-[auto_1fr] gap-2 items-center"> <button
<label className="text-[11px] text-neutral-400">Pattern</label> type="button"
<input onClick={() => setShowAdvanced((v) => !v)}
value={nodeData.regexPattern} className="nodrag nopan w-full flex items-center justify-between px-2 py-1 text-[11px] text-neutral-300 hover:bg-neutral-800/50"
onChange={(e) => updateNodeData(id, { regexPattern: e.target.value })} >
placeholder="/\\n+/" <span>Advanced</span>
className="nodrag nopan bg-neutral-900 border border-neutral-700 rounded px-2 py-1 text-[11px] text-neutral-100 focus:outline-none focus:ring-1 focus:ring-neutral-600" <span className="text-[10px] text-neutral-500">{showAdvanced ? "Hide" : "Show"}</span>
/> </button>
</div>
)}
<div className="flex items-center gap-3"> {showAdvanced && (
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300"> <div className="px-2 pb-2 pt-1 flex flex-col gap-2 border-t border-neutral-700">
<input <label className="flex items-center gap-1.5 text-[11px] text-neutral-300">
type="checkbox" <input
checked={nodeData.trimItems} type="checkbox"
onChange={(e) => updateNodeData(id, { trimItems: e.target.checked })} checked={nodeData.splitMode === "regex"}
className="nodrag nopan w-3 h-3" onChange={(e) =>
/> updateNodeData(id, {
Trim splitMode: e.target.checked ? "regex" : "delimiter",
</label> })
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300"> }
<input className="nodrag nopan w-3 h-3"
type="checkbox" />
checked={nodeData.removeEmpty} Use regex split
onChange={(e) => updateNodeData(id, { removeEmpty: e.target.checked })} </label>
className="nodrag nopan w-3 h-3"
/> {nodeData.splitMode === "regex" && (
Remove empty <div className="grid grid-cols-[auto_1fr] gap-2 items-center">
</label> <label className="text-[11px] text-neutral-400">Pattern</label>
<input
value={nodeData.regexPattern}
onChange={(e) => updateNodeData(id, { regexPattern: e.target.value })}
placeholder="/\\n+/"
className="nodrag nopan bg-neutral-900 border border-neutral-700 rounded px-2 py-1 text-[11px] text-neutral-100 focus:outline-none focus:ring-1 focus:ring-neutral-600"
/>
</div>
)}
<div className="flex items-center gap-3">
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300">
<input
type="checkbox"
checked={nodeData.trimItems}
onChange={(e) => updateNodeData(id, { trimItems: e.target.checked })}
className="nodrag nopan w-3 h-3"
/>
Trim
</label>
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300">
<input
type="checkbox"
checked={nodeData.removeEmpty}
onChange={(e) => updateNodeData(id, { removeEmpty: e.target.checked })}
className="nodrag nopan w-3 h-3"
/>
Remove empty
</label>
</div>
</div>
)}
</div> </div>
<div className="text-[10px] uppercase tracking-wide text-neutral-500"> <div className="text-[10px] uppercase tracking-wide text-neutral-500">

Loading…
Cancel
Save