|
|
@ -125,12 +125,15 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) { |
|
|
const promptHeight = 220; |
|
|
const promptHeight = 220; |
|
|
const verticalGap = 24; |
|
|
const verticalGap = 24; |
|
|
|
|
|
|
|
|
|
|
|
const promptNodeIds: string[] = []; |
|
|
|
|
|
|
|
|
items.forEach((item, index) => { |
|
|
items.forEach((item, index) => { |
|
|
const promptNodeId = addNode( |
|
|
const promptNodeId = addNode( |
|
|
"prompt", |
|
|
"prompt", |
|
|
{ x: baseX, y: baseY + index * (promptHeight + verticalGap) }, |
|
|
{ x: baseX, y: baseY + index * (promptHeight + verticalGap) }, |
|
|
{ prompt: item } |
|
|
{ prompt: item } |
|
|
); |
|
|
); |
|
|
|
|
|
promptNodeIds.push(promptNodeId); |
|
|
|
|
|
|
|
|
// Pass the array item index directly as an edge data override
|
|
|
// Pass the array item index directly as an edge data override
|
|
|
// instead of mutating selectedOutputIndex in a loop.
|
|
|
// instead of mutating selectedOutputIndex in a loop.
|
|
|
@ -144,7 +147,16 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) { |
|
|
{ arrayItemIndex: index } |
|
|
{ arrayItemIndex: index } |
|
|
); |
|
|
); |
|
|
}); |
|
|
}); |
|
|
}, [addNode, getNodes, id, onConnect, previewItems]); |
|
|
|
|
|
|
|
|
// Deferred fix-up: the PromptNode text-sync effect may overwrite the
|
|
|
|
|
|
// individual item text before the edge arrayItemIndex data is fully
|
|
|
|
|
|
// settled. Re-apply the correct per-item text after effects have run.
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
items.forEach((item, index) => { |
|
|
|
|
|
updateNodeData(promptNodeIds[index], { prompt: item }); |
|
|
|
|
|
}); |
|
|
|
|
|
}, 0); |
|
|
|
|
|
}, [addNode, getNodes, id, 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(() => { |
|
|
@ -156,9 +168,10 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) { |
|
|
|
|
|
|
|
|
// Auto-resize node height to fit all parsed lines so users don't need to scroll.
|
|
|
// Auto-resize node height to fit all parsed lines so users don't need to scroll.
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
const baseHeight = 360; |
|
|
const headerHeight = 180; |
|
|
const perItemHeight = 30; |
|
|
const perItemHeight = 28; |
|
|
const newHeight = Math.max(baseHeight, 270 + previewItems.length * perItemHeight); |
|
|
const itemsMinHeight = 60; |
|
|
|
|
|
const newHeight = headerHeight + Math.max(itemsMinHeight, previewItems.length * perItemHeight + 8); |
|
|
|
|
|
|
|
|
setNodes((nodes) => |
|
|
setNodes((nodes) => |
|
|
nodes.map((node) => { |
|
|
nodes.map((node) => { |
|
|
@ -174,8 +187,8 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) { |
|
|
id={id} |
|
|
id={id} |
|
|
selected={selected} |
|
|
selected={selected} |
|
|
hasError={!!nodeData.error} |
|
|
hasError={!!nodeData.error} |
|
|
minWidth={320} |
|
|
minWidth={300} |
|
|
minHeight={300} |
|
|
minHeight={220} |
|
|
> |
|
|
> |
|
|
<Handle type="target" position={Position.Left} id="text" data-handletype="text" /> |
|
|
<Handle type="target" position={Position.Left} id="text" data-handletype="text" /> |
|
|
|
|
|
|
|
|
@ -187,7 +200,7 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) { |
|
|
type="button" |
|
|
type="button" |
|
|
onClick={handleAutoRouteToPrompts} |
|
|
onClick={handleAutoRouteToPrompts} |
|
|
disabled={previewItems.length === 0} |
|
|
disabled={previewItems.length === 0} |
|
|
className="nodrag nopan absolute top-2 right-2 z-10 shrink-0 p-1 rounded border border-neutral-600 bg-neutral-800/90 text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 disabled:opacity-50 disabled:cursor-not-allowed" |
|
|
className="nodrag nopan absolute top-2 right-2 z-10 shrink-0 p-1 bg-[#1a1a1a] rounded-md text-neutral-400 hover:text-neutral-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" |
|
|
title="Auto-route to Prompts" |
|
|
title="Auto-route to Prompts" |
|
|
> |
|
|
> |
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|
|
@ -195,13 +208,13 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) { |
|
|
</svg> |
|
|
</svg> |
|
|
</button> |
|
|
</button> |
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-2 flex-1 min-h-0"> |
|
|
<div className="flex flex-col gap-2 pt-3 flex-1 min-h-0"> |
|
|
<div className="grid grid-cols-[auto_1fr] gap-2 items-center"> |
|
|
<div className="flex items-center gap-2 max-w-[75%]"> |
|
|
<label className="text-[11px] text-neutral-400">Split</label> |
|
|
<label className="shrink-0 text-[11px] text-neutral-400">Split</label> |
|
|
<select |
|
|
<select |
|
|
value={nodeData.splitMode} |
|
|
value={nodeData.splitMode} |
|
|
onChange={handleBasicModeChange} |
|
|
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 flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white" |
|
|
> |
|
|
> |
|
|
<option value="delimiter">Delimiter</option> |
|
|
<option value="delimiter">Delimiter</option> |
|
|
<option value="newline">Newline</option> |
|
|
<option value="newline">Newline</option> |
|
|
@ -210,69 +223,69 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) { |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
{nodeData.splitMode === "delimiter" && ( |
|
|
{nodeData.splitMode === "delimiter" && ( |
|
|
<div className="grid grid-cols-[auto_1fr] gap-2 items-center"> |
|
|
<div className="flex items-center gap-2 max-w-[75%]"> |
|
|
<label className="text-[11px] text-neutral-400">By</label> |
|
|
<label className="shrink-0 text-[11px] text-neutral-400">By</label> |
|
|
<input |
|
|
<input |
|
|
value={nodeData.delimiter} |
|
|
value={nodeData.delimiter} |
|
|
onChange={(e) => updateNodeData(id, { delimiter: e.target.value })} |
|
|
onChange={(e) => updateNodeData(id, { delimiter: e.target.value })} |
|
|
placeholder="*" |
|
|
placeholder="*" |
|
|
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 flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white" |
|
|
/> |
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
{nodeData.splitMode === "regex" && ( |
|
|
{nodeData.splitMode === "regex" && ( |
|
|
<div className="grid grid-cols-[auto_1fr] gap-2 items-center"> |
|
|
<div className="flex items-center gap-2 max-w-[75%]"> |
|
|
<label className="text-[11px] text-neutral-400">By</label> |
|
|
<label className="shrink-0 text-[11px] text-neutral-400">By</label> |
|
|
<input |
|
|
<input |
|
|
value={nodeData.regexPattern} |
|
|
value={nodeData.regexPattern} |
|
|
onChange={(e) => updateNodeData(id, { regexPattern: e.target.value })} |
|
|
onChange={(e) => updateNodeData(id, { regexPattern: e.target.value })} |
|
|
placeholder="/\\n+/" |
|
|
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" |
|
|
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white" |
|
|
/> |
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
<div className="border border-neutral-700 rounded"> |
|
|
<div> |
|
|
<button |
|
|
<button |
|
|
type="button" |
|
|
type="button" |
|
|
onClick={() => setShowAdvanced((v) => !v)} |
|
|
onClick={() => setShowAdvanced((v) => !v)} |
|
|
className="nodrag nopan w-full flex items-center justify-between px-2 py-1 text-[11px] text-neutral-300 hover:bg-neutral-800/50" |
|
|
className="nodrag nopan flex items-center gap-1 text-[11px] text-neutral-500 hover:text-neutral-300 transition-colors" |
|
|
> |
|
|
> |
|
|
|
|
|
<svg className={`w-3 h-3 transition-transform ${showAdvanced ? "rotate-90" : ""}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /> |
|
|
|
|
|
</svg> |
|
|
<span>Advanced</span> |
|
|
<span>Advanced</span> |
|
|
<span className="text-[10px] text-neutral-500">{showAdvanced ? "Hide" : "Show"}</span> |
|
|
|
|
|
</button> |
|
|
</button> |
|
|
|
|
|
|
|
|
{showAdvanced && ( |
|
|
{showAdvanced && ( |
|
|
<div className="px-2 pb-2 pt-1 flex flex-col gap-2 border-t border-neutral-700"> |
|
|
<div className="px-2 pt-1.5 pb-0.5 flex items-center gap-3"> |
|
|
<div className="flex items-center gap-3"> |
|
|
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300"> |
|
|
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300"> |
|
|
<input |
|
|
<input |
|
|
type="checkbox" |
|
|
type="checkbox" |
|
|
checked={nodeData.trimItems} |
|
|
checked={nodeData.trimItems} |
|
|
onChange={(e) => updateNodeData(id, { trimItems: e.target.checked })} |
|
|
onChange={(e) => updateNodeData(id, { trimItems: e.target.checked })} |
|
|
className="nodrag nopan w-3 h-3 rounded bg-[#1a1a1a] text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0" |
|
|
className="nodrag nopan w-3 h-3" |
|
|
/> |
|
|
/> |
|
|
Trim |
|
|
Trim |
|
|
</label> |
|
|
</label> |
|
|
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300"> |
|
|
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300"> |
|
|
<input |
|
|
<input |
|
|
type="checkbox" |
|
|
type="checkbox" |
|
|
checked={nodeData.removeEmpty} |
|
|
checked={nodeData.removeEmpty} |
|
|
onChange={(e) => updateNodeData(id, { removeEmpty: e.target.checked })} |
|
|
onChange={(e) => updateNodeData(id, { removeEmpty: e.target.checked })} |
|
|
className="nodrag nopan w-3 h-3 rounded bg-[#1a1a1a] text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0" |
|
|
className="nodrag nopan w-3 h-3" |
|
|
/> |
|
|
/> |
|
|
Remove empty |
|
|
Remove empty |
|
|
</label> |
|
|
</label> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div className="text-[10px] uppercase tracking-wide text-neutral-500"> |
|
|
<div className="mt-1 text-[10px] uppercase tracking-wide text-neutral-500"> |
|
|
Parsed Items ({previewItems.length}) |
|
|
Parsed Items ({previewItems.length}) |
|
|
</div> |
|
|
</div> |
|
|
<div className="relative flex-1 min-h-[90px] border border-neutral-700 rounded bg-neutral-900/40"> |
|
|
<div className="relative min-h-[50px] border border-neutral-700/40 rounded-md bg-[#1a1a1a]"> |
|
|
{nodeData.error ? ( |
|
|
{nodeData.error ? ( |
|
|
<div className="p-2 text-[11px] text-red-400">{nodeData.error}</div> |
|
|
<div className="p-2 text-[11px] text-red-400">{nodeData.error}</div> |
|
|
) : previewItems.length === 0 ? ( |
|
|
) : previewItems.length === 0 ? ( |
|
|
@ -282,24 +295,23 @@ export function ArrayNode({ id, data, selected }: NodeProps<ArrayNodeType>) { |
|
|
{previewItems.map((item, index) => { |
|
|
{previewItems.map((item, index) => { |
|
|
const isSelected = nodeData.selectedOutputIndex === index; |
|
|
const isSelected = nodeData.selectedOutputIndex === index; |
|
|
return ( |
|
|
return ( |
|
|
<div key={`${index}-${item}`} className="relative pr-8"> |
|
|
<button |
|
|
<button |
|
|
key={`${index}-${item}`} |
|
|
type="button" |
|
|
type="button" |
|
|
onClick={() => |
|
|
onClick={() => |
|
|
updateNodeData(id, { |
|
|
updateNodeData(id, { |
|
|
selectedOutputIndex: isSelected ? null : index, |
|
|
selectedOutputIndex: isSelected ? null : index, |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
className={`nodrag nopan w-[calc(100%-1rem)] mx-2 my-1 rounded border px-2 py-1 text-[11px] text-left truncate transition-colors ${ |
|
|
className={`nodrag nopan w-[calc(100%-1rem)] mx-2 my-0.5 rounded-md px-2 py-1 text-[11px] text-left truncate transition-colors ${ |
|
|
isSelected |
|
|
isSelected |
|
|
? "border-blue-500 bg-blue-900/40 text-blue-200" |
|
|
? "bg-blue-900/40 text-blue-200 ring-1 ring-blue-500/60" |
|
|
: "border-neutral-700 bg-neutral-900/80 text-neutral-200 hover:bg-neutral-800" |
|
|
: "bg-neutral-800/60 text-neutral-300 hover:bg-neutral-700/60" |
|
|
}`}
|
|
|
}`}
|
|
|
title={isSelected ? "Selected for next connection (click to unselect)" : "Click to select for next connection"} |
|
|
title={isSelected ? "Selected for next connection (click to unselect)" : "Click to select for next connection"} |
|
|
> |
|
|
> |
|
|
{index + 1}. {item} |
|
|
{index + 1}. {item} |
|
|
</button> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
); |
|
|
); |
|
|
})} |
|
|
})} |
|
|
</div> |
|
|
</div> |
|
|
|