feat: add node picker dropdown to action node form for cross-references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-28 19:47:10 -05:00
parent b0347bacd4
commit 663919d928
2 changed files with 79 additions and 10 deletions

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'
import { DynamicArrayField } from './DynamicArrayField'
import { useTreeEditorStore } from '@/store/treeEditorStore'
import { useTreeEditorStore, collectAllNodesFlat } from '@/store/treeEditorStore'
import { Link2, X } from 'lucide-react'
import { MarkdownContent } from '@/components/ui/MarkdownContent'
import { InfoTip } from '@/components/common/InfoTip'
import type { TreeStructure } from '@/types'
@@ -158,16 +159,65 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
/>
</div>
{/* Next step hint */}
{hasNextNode ? (
<p className="text-xs text-muted-foreground">
Next step is linked click it on the canvas to edit.
{/* Link to existing node */}
<div>
<label className="flex items-center gap-1.5 text-sm font-medium text-foreground">
<Link2 className="h-3.5 w-3.5" />
Next Step
</label>
{hasNextNode ? (
<div className="mt-1 flex items-center gap-2 rounded-md border border-primary/30 bg-primary/5 px-3 py-2">
<span className="flex-1 truncate text-sm text-foreground">
Linked to: {(() => {
const treeStructure = useTreeEditorStore.getState().treeStructure
const allNodes = collectAllNodesFlat(treeStructure)
const target = allNodes.find(n => n.id === node.next_node_id)
return target ? target.label : node.next_node_id
})()}
</span>
<button
type="button"
onClick={() => onUpdate({ next_node_id: undefined })}
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
title="Remove link"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
) : (
<select
value=""
onChange={(e) => {
if (e.target.value) {
onUpdate({ next_node_id: e.target.value })
}
}}
className={cn(
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
'bg-card text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
>
<option value="">Link to existing node...</option>
{(() => {
const treeStructure = useTreeEditorStore.getState().treeStructure
const allNodes = collectAllNodesFlat(treeStructure)
return allNodes
.filter(n => n.id !== node.id && n.type !== 'answer')
.map(n => (
<option key={n.id} value={n.id}>
{' '.repeat(n.depth)}{n.type === 'decision' ? '?' : n.type === 'action' ? '>' : '*'} {n.label}
</option>
))
})()}
</select>
)}
<p className="mt-1 text-xs text-muted-foreground">
{hasNextNode
? 'This action will navigate to the linked node.'
: 'Select a node to navigate to after this action, or save to create a new placeholder.'}
</p>
) : (
<p className="text-xs text-yellow-400/70">
Save to create a placeholder for the next step.
</p>
)}
</div>
</div>
)
}

View File

@@ -123,6 +123,25 @@ export const findNodeInTree = (
return null
}
/** Collect all nodes in the tree as a flat list with depth info. */
export function collectAllNodesFlat(
root: TreeStructure | null
): Array<{ id: string; label: string; type: string; depth: number }> {
if (!root) return []
const result: Array<{ id: string; label: string; type: string; depth: number }> = []
function walk(node: TreeStructure, depth: number) {
const label = node.type === 'decision'
? (node.question || 'Untitled Decision')
: (node.title || `Untitled ${node.type}`)
result.push({ id: node.id, label, type: node.type, depth })
node.children?.forEach(child => walk(child, depth + 1))
}
walk(root, 0)
return result
}
// Helper to find parent of a node
const findParentNode = (
nodeId: string,