Skip to content

Commit ac5f55b

Browse files
jocelynlin-wdclaude
andcommitted
feat(agentflow): add field visibility engine with show/hide conditions
Add InputParam show/hide condition support that controls field visibility based on current input values. Includes regex, array, boolean, and nested path matching with parity to upstream genericHelper.js. - Add fieldVisibility utility (evaluateFieldVisibility, stripHiddenFieldValues) - Add show/hide fields to InputParam type - Integrate visibility into EditNodeDialog (re-evaluate on change, preserve hidden values in state) - Update useOpenNodeEditor to fall back to node.data.inputs when API schema is unavailable - Move connectionValidation from utils/ to validation/ - Skip hidden params in validateNode required-input checks - Add CustomNodeExample demo with live visibility state panel - Update ARCHITECTURE.md and TESTS.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4d857f8 commit ac5f55b

19 files changed

Lines changed: 757 additions & 22 deletions

packages/agentflow/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,10 @@ core/
122122
│ └── ...
123123
├── validation/ # Flow validation logic
124124
│ ├── flowValidation.ts # validateFlow, validateNode
125+
│ ├── connectionValidation.ts # isValidConnectionAgentflowV2
125126
│ └── ...
126127
├── utils/ # Generic utilities
127128
│ ├── nodeFactory.ts # initNode, getUniqueNodeId
128-
│ ├── connectionValidation.ts
129129
│ └── ...
130130
└── index.ts # Barrel export (use sparingly)
131131
```

packages/agentflow/TESTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ These modules carry the highest risk. Test in the same PR when modifying.
2424
<!-- prettier-ignore -->
2525
| File | Key exports to test | Status |
2626
| --- | --- | --- |
27-
| `src/core/validation/` | `validateFlow`, `validateNode` — empty flows, missing/multiple starts, disconnected nodes, cycles, required inputs | ✅ Done |
28-
| `src/core/utils/` | `getUniqueNodeId`, `getUniqueNodeLabel`, `initNode`, `generateExportFlowData`, `isValidConnectionAgentflowV2` | ✅ Done |
27+
| `src/core/validation/` | `validateFlow`, `validateNode` — empty flows, missing/multiple starts, disconnected nodes, cycles, required inputs; `isValidConnectionAgentflowV2` — self-connections, cycle detection | ✅ Done |
28+
| `src/core/utils/` | `getUniqueNodeId`, `getUniqueNodeLabel`, `initNode`, `generateExportFlowData` | ✅ Done |
2929
| `src/core/node-catalog/` | `filterNodesByComponents`, `isAgentflowNode`, `groupNodesByCategory` | ✅ Done |
3030
| `src/core/node-config/` | `getAgentflowIcon`, `getNodeColor` | ✅ Done |
3131
| `src/core/theme/tokens.ts` | All design tokens — node colors, light/dark variants, spacing scale, semantic colors, ReactFlow colors, shadows, border radius, gradients | ✅ Done |
@@ -53,7 +53,7 @@ Test when adding features or fixing bugs in these areas.
5353
| `src/infrastructure/store/ConfigContext.tsx` | `ConfigProvider` — theme detection (light/dark/system), media query listener | ✅ Done |
5454
| `src/features/generator/GenerateFlowDialog.tsx` | Dialog state machine — API call flow, error handling, progress state | ✅ Done |
5555
| `src/features/node-editor/EditNodeDialog.tsx` | Label editing — keyboard handling (Enter/Escape), node data updates | ✅ Done |
56-
| `src/features/canvas/hooks/useOpenNodeEditor.ts` | `openNodeEditor()` — node/schema lookup, inputValues initialization, early returns | ✅ Done |
56+
| `src/features/canvas/hooks/useOpenNodeEditor.ts` | `openNodeEditor()` — node/schema lookup, inputValues initialization, early returns, fallback to `node.data.inputs` when API schema unavailable, API schema priority over `data.inputs` | ✅ Done |
5757

5858
### Tier 3 — UI Components
5959

packages/agentflow/examples/src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { apiBaseUrl, token } from './config'
1010
import {
1111
AllNodeTypesExampleProps,
1212
BasicExampleProps,
13+
CustomNodeExampleProps,
1314
CustomUIExampleProps,
1415
DarkModeExampleProps,
1516
FilteredComponentsExampleProps,
@@ -53,6 +54,13 @@ const examples: Array<{
5354
props: StatusIndicatorsExampleProps,
5455
component: lazy(() => import('./demos/StatusIndicatorsExample').then((m) => ({ default: m.StatusIndicatorsExample })))
5556
},
57+
{
58+
id: 'custom-node',
59+
name: 'Custom Node',
60+
description: 'Node with self-contained InputParam definitions and show/hide conditions',
61+
props: CustomNodeExampleProps,
62+
component: lazy(() => import('./demos/CustomNodeExample').then((m) => ({ default: m.CustomNodeExample })))
63+
},
5664
{
5765
id: 'custom-ui',
5866
name: 'Custom UI',

packages/agentflow/examples/src/demos/AllNodeTypesExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export function AllNodeTypesExample() {
242242
token={token ?? undefined}
243243
initialFlow={allNodesFlow}
244244
showDefaultHeader={false}
245-
readOnly={true}
245+
readOnly={false}
246246
/>
247247
</div>
248248
</div>
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/**
2+
* Custom Node Example
3+
*
4+
* Demonstrates how a node can carry its own InputParam[] definitions
5+
* in data.inputs, bypassing the API schema. The node includes show/hide
6+
* conditions so double-clicking it opens the real EditNodeDialog with
7+
* conditional field visibility.
8+
*
9+
* A side panel shows the live visibility state (input values, stripped
10+
* values, and per-field visibility map) as the node is edited.
11+
*/
12+
13+
import { useCallback, useMemo, useState } from 'react'
14+
15+
import type { FlowData, HeaderRenderProps, InputParam } from '@flowiseai/agentflow'
16+
import { Agentflow, evaluateFieldVisibility, stripHiddenFieldValues } from '@flowiseai/agentflow'
17+
18+
import { apiBaseUrl, token } from '../config'
19+
20+
// ── Custom node InputParam definitions with show/hide conditions ──────────
21+
22+
const customNodeInputParams: InputParam[] = [
23+
{
24+
id: 'provider',
25+
name: 'provider',
26+
label: 'Model Provider',
27+
type: 'options',
28+
options: [
29+
{ label: 'OpenAI', name: 'openAI' },
30+
{ label: 'Google', name: 'google' },
31+
{ label: 'Anthropic', name: 'anthropic' }
32+
]
33+
},
34+
{
35+
id: 'openAIModel',
36+
name: 'openAIModel',
37+
label: 'OpenAI Model',
38+
type: 'options',
39+
options: [
40+
{ label: 'GPT-4o', name: 'gpt-4o' },
41+
{ label: 'GPT-4o Mini', name: 'gpt-4o-mini' },
42+
{ label: 'o1', name: 'o1' }
43+
],
44+
show: { provider: 'openAI' }
45+
},
46+
{
47+
id: 'googleModel',
48+
name: 'googleModel',
49+
label: 'Google Model',
50+
type: 'options',
51+
options: [
52+
{ label: 'Gemini 2.0 Flash', name: 'gemini-2.0-flash' },
53+
{ label: 'Gemini 2.5 Pro', name: 'gemini-2.5-pro' }
54+
],
55+
show: { provider: 'google' }
56+
},
57+
{
58+
id: 'anthropicModel',
59+
name: 'anthropicModel',
60+
label: 'Anthropic Model',
61+
type: 'options',
62+
options: [
63+
{ label: 'Claude Sonnet 4', name: 'claude-sonnet-4' },
64+
{ label: 'Claude Opus 4', name: 'claude-opus-4' }
65+
],
66+
show: { provider: 'anthropic' }
67+
},
68+
{
69+
id: 'enableMemory',
70+
name: 'enableMemory',
71+
label: 'Enable Memory',
72+
type: 'boolean'
73+
},
74+
{
75+
id: 'memoryType',
76+
name: 'memoryType',
77+
label: 'Memory Type',
78+
type: 'options',
79+
options: [
80+
{ label: 'Buffer Window', name: 'bufferWindow' },
81+
{ label: 'Token Buffer', name: 'tokenBuffer' },
82+
{ label: 'Summary', name: 'summary' }
83+
],
84+
show: { enableMemory: true }
85+
},
86+
{
87+
id: 'windowSize',
88+
name: 'windowSize',
89+
label: 'Window Size',
90+
type: 'number',
91+
default: 5,
92+
show: { enableMemory: true, memoryType: 'bufferWindow' }
93+
},
94+
{
95+
id: 'maxTokens',
96+
name: 'maxTokens',
97+
label: 'Max Tokens',
98+
type: 'number',
99+
default: 2000,
100+
show: { enableMemory: true, memoryType: 'tokenBuffer' }
101+
},
102+
{
103+
id: 'outputFormat',
104+
name: 'outputFormat',
105+
label: 'Output Format',
106+
type: 'options',
107+
options: [
108+
{ label: 'Text', name: 'text' },
109+
{ label: 'JSON', name: 'json' },
110+
{ label: 'Markdown', name: 'markdown' }
111+
]
112+
},
113+
{
114+
id: 'schema',
115+
name: 'schema',
116+
label: 'Output Schema',
117+
type: 'string',
118+
placeholder: 'Define the output structure...',
119+
hide: { outputFormat: 'text' }
120+
},
121+
{
122+
id: 'apiKey',
123+
name: 'apiKey',
124+
label: 'API Key',
125+
type: 'string',
126+
description: 'Shown for any provider (regex match)',
127+
show: { provider: '(openAI|google|anthropic)' }
128+
},
129+
{
130+
id: 'streamingSupport',
131+
name: 'streamingSupport',
132+
label: 'Enable Streaming',
133+
type: 'boolean',
134+
description: 'Available for OpenAI and Anthropic',
135+
show: { provider: ['openAI', 'anthropic'] }
136+
}
137+
]
138+
139+
// ── Canvas flow data with a custom node carrying its own input definitions ─
140+
141+
const initialInputValues: Record<string, unknown> = { provider: '', enableMemory: false, outputFormat: 'text' }
142+
143+
const canvasFlow: FlowData = {
144+
nodes: [
145+
{
146+
id: 'customNode_0',
147+
type: 'agentflowNode',
148+
position: { x: 300, y: 150 },
149+
data: {
150+
id: 'customNode_0',
151+
name: 'customNodeDemo',
152+
label: 'Custom Node',
153+
color: '#64B5F6',
154+
inputs: customNodeInputParams,
155+
inputValues: initialInputValues,
156+
outputAnchors: [{ id: 'customNode_0-output-0', name: 'output', label: 'Output', type: 'string' }]
157+
}
158+
}
159+
],
160+
edges: [],
161+
viewport: { x: 0, y: 0, zoom: 1 }
162+
}
163+
164+
// ── Side panel for live visibility state ───────────────────────────────────
165+
166+
type VisibilityTab = 'values' | 'stripped' | 'visibility'
167+
168+
function VisibilityStatePanel({ inputValues }: { inputValues: Record<string, unknown> }) {
169+
const [tab, setTab] = useState<VisibilityTab>('values')
170+
171+
const evaluated = useMemo(() => evaluateFieldVisibility(customNodeInputParams, inputValues), [inputValues])
172+
const stripped = useMemo(() => stripHiddenFieldValues(customNodeInputParams, inputValues), [inputValues])
173+
const visibilityMap = useMemo(() => Object.fromEntries(evaluated.map((p) => [p.name, p.display])), [evaluated])
174+
175+
const visibleCount = evaluated.filter((p) => p.display).length
176+
const hiddenCount = evaluated.filter((p) => !p.display).length
177+
178+
const tabData: Record<VisibilityTab, unknown> = {
179+
values: inputValues,
180+
stripped,
181+
visibility: visibilityMap
182+
}
183+
184+
const tabButton = (id: VisibilityTab, label: string) => (
185+
<button
186+
onClick={() => setTab(id)}
187+
style={{
188+
flex: 1,
189+
padding: '10px',
190+
background: tab === id ? '#313244' : 'transparent',
191+
color: tab === id ? '#cba6f7' : '#6c7086',
192+
border: 'none',
193+
cursor: 'pointer',
194+
fontFamily: 'monospace',
195+
fontSize: '11px',
196+
fontWeight: 600
197+
}}
198+
>
199+
{label}
200+
</button>
201+
)
202+
203+
return (
204+
<div
205+
style={{
206+
width: '300px',
207+
minHeight: 0,
208+
background: '#1e1e2e',
209+
color: '#cdd6f4',
210+
display: 'flex',
211+
flexDirection: 'column',
212+
fontSize: '13px',
213+
fontFamily: 'monospace',
214+
borderLeft: '1px solid #313244',
215+
overflow: 'hidden'
216+
}}
217+
>
218+
{/* Tabs */}
219+
<div style={{ display: 'flex', borderBottom: '1px solid #313244' }}>
220+
{tabButton('values', 'Input Values')}
221+
{tabButton('stripped', 'Stripped')}
222+
{tabButton('visibility', 'Visibility')}
223+
</div>
224+
225+
{/* Stats */}
226+
<div style={{ display: 'flex', gap: '12px', padding: '10px 14px', borderBottom: '1px solid #313244' }}>
227+
<span>
228+
<span style={{ color: '#a6e3a1' }}>visible:</span> {visibleCount}
229+
</span>
230+
<span>
231+
<span style={{ color: '#f38ba8' }}>hidden:</span> {hiddenCount}
232+
</span>
233+
</div>
234+
235+
{/* JSON payload */}
236+
<div style={{ flex: 1, overflow: 'auto', padding: '10px 14px' }}>
237+
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word', lineHeight: 1.5 }}>
238+
{JSON.stringify(tabData[tab], null, 2)}
239+
</pre>
240+
</div>
241+
</div>
242+
)
243+
}
244+
245+
// ── Component ──────────────────────────────────────────────────────────────
246+
247+
export function CustomNodeExample() {
248+
const [inputValues, setInputValues] = useState<Record<string, unknown>>(initialInputValues)
249+
250+
const handleFlowChange = useCallback((flow: FlowData) => {
251+
const node = flow.nodes.find((n) => n.id === 'customNode_0')
252+
if (node?.data?.inputValues) {
253+
setInputValues(node.data.inputValues)
254+
}
255+
}, [])
256+
257+
const renderHeader = useCallback(
258+
(_props: HeaderRenderProps) => (
259+
<div
260+
style={{
261+
padding: '8px 16px',
262+
background: '#fafafa',
263+
borderBottom: '1px solid #e0e0e0',
264+
fontSize: '13px',
265+
color: '#666'
266+
}}
267+
>
268+
<strong>Custom Node Demo</strong> — Double-click the node to open EditNodeDialog with show/hide conditions.
269+
</div>
270+
),
271+
[]
272+
)
273+
274+
return (
275+
<div style={{ display: 'flex', height: '100%' }}>
276+
<div style={{ flex: 1 }}>
277+
<Agentflow
278+
apiBaseUrl={apiBaseUrl}
279+
token={token ?? undefined}
280+
initialFlow={canvasFlow}
281+
onFlowChange={handleFlowChange}
282+
renderHeader={renderHeader}
283+
/>
284+
</div>
285+
<VisibilityStatePanel inputValues={inputValues} />
286+
</div>
287+
)
288+
}
289+
290+
export const CustomNodeExampleProps = {
291+
apiBaseUrl: '{from environment variables}',
292+
token: '{from environment variables}',
293+
initialFlow: 'FlowData (custom node with data.inputs)',
294+
onFlowChange: '(flow: FlowData) => void',
295+
renderHeader: '(props: HeaderRenderProps) => ReactNode'
296+
}

packages/agentflow/examples/src/demos/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './AllNodeTypesExample'
22
export * from './BasicExample'
3+
export * from './CustomNodeExample'
34
export * from './CustomUIExample'
45
export * from './DarkModeExample'
56
export * from './FilteredComponentsExample'

packages/agentflow/src/core/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ export interface InputParam {
128128
description?: string
129129
acceptVariable?: boolean
130130
additionalParams?: boolean
131+
show?: Record<string, unknown>
132+
hide?: Record<string, unknown>
131133
display?: boolean
132134
}
133135

0 commit comments

Comments
 (0)