Skip to content

Commit a77194c

Browse files
feat(agentflow): optional cavasActions to allow additional buttons next to validate (#6224)
* feat(agentflow): optional cavasActions to allow additional buttons next to validate - Updated Agentflow component to support canvasActions prop for rendering additional action buttons in the canvas overlay. - Enhanced type definitions to include canvasActions in AgentflowProps. - Added tests to verify the rendering of canvasActions in the Agentflow component. - Added a new example demonstrating custom FABs alongside the validation button. * update jsdoc to match actual implementation
1 parent 62a5d1d commit a77194c

7 files changed

Lines changed: 137 additions & 10 deletions

File tree

packages/agentflow/examples/src/App.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
DarkModeExampleProps,
1616
FilteredComponentsExampleProps,
1717
MultiNodeFlowProps,
18-
StatusIndicatorsExampleProps
18+
StatusIndicatorsExampleProps,
19+
ValidationActionsExampleProps
1920
} from './demos'
2021
import { PropsDisplay } from './PropsDisplay'
2122

@@ -81,6 +82,13 @@ const examples: Array<{
8182
description: 'Restrict available nodes with presets',
8283
props: FilteredComponentsExampleProps,
8384
component: lazy(() => import('./demos/FilteredComponentsExample').then((m) => ({ default: m.FilteredComponentsExample })))
85+
},
86+
{
87+
id: 'canvas-actions',
88+
name: 'Canvas Actions',
89+
description: 'Custom FABs alongside the validation button via canvasActions',
90+
props: ValidationActionsExampleProps,
91+
component: lazy(() => import('./demos/ValidationActionsExample').then((m) => ({ default: m.ValidationActionsExample })))
8492
}
8593
]
8694

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Canvas Actions Example
3+
*
4+
* Demonstrates how to add custom FAB buttons alongside the built-in validation
5+
* button in the top-right canvas overlay using the `canvasActions` prop.
6+
*
7+
* Mirrors the legacy v2 pattern where a chat FAB and validation FAB sit side-by-side.
8+
*/
9+
10+
import { useState } from 'react'
11+
12+
import { Agentflow } from '@flowiseai/agentflow'
13+
import { Box, Dialog, DialogContent, DialogTitle, Fab, IconButton, Typography } from '@mui/material'
14+
import { IconMessage, IconX } from '@tabler/icons-react'
15+
16+
import { apiBaseUrl, token } from '../config'
17+
18+
function ChatFab() {
19+
const [open, setOpen] = useState(false)
20+
21+
return (
22+
<>
23+
<Fab
24+
size='small'
25+
aria-label='chat'
26+
title='Chat'
27+
onClick={() => setOpen(true)}
28+
sx={{
29+
color: 'white',
30+
backgroundColor: 'secondary.main',
31+
'&:hover': {
32+
backgroundColor: 'secondary.main',
33+
backgroundImage: 'linear-gradient(rgb(0 0 0/10%) 0 0)'
34+
}
35+
}}
36+
>
37+
<IconMessage />
38+
</Fab>
39+
40+
<Dialog open={open} onClose={() => setOpen(false)} maxWidth='sm' fullWidth>
41+
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
42+
Chat
43+
<IconButton size='small' onClick={() => setOpen(false)}>
44+
<IconX size={18} />
45+
</IconButton>
46+
</DialogTitle>
47+
<DialogContent>
48+
<Box sx={{ py: 4, textAlign: 'center' }}>
49+
<Typography variant='body2' color='text.secondary'>
50+
Your chat UI goes here. Full control — bring any component.
51+
</Typography>
52+
</Box>
53+
</DialogContent>
54+
</Dialog>
55+
</>
56+
)
57+
}
58+
59+
export function ValidationActionsExample() {
60+
return (
61+
<div style={{ width: '100%', height: '100%' }}>
62+
<Agentflow apiBaseUrl={apiBaseUrl} token={token ?? undefined} canvasActions={<ChatFab />} />
63+
</div>
64+
)
65+
}
66+
67+
export const ValidationActionsExampleProps = {
68+
apiBaseUrl: '{from environment variables}',
69+
token: '{from environment variables}',
70+
canvasActions: '<ChatFab />'
71+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './DarkModeExample'
66
export * from './FilteredComponentsExample'
77
export * from './MultiNodeFlow'
88
export * from './StatusIndicatorsExample'
9+
export * from './ValidationActionsExample'

packages/agentflow/src/Agentflow.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,30 @@ describe('Agentflow Component', () => {
428428
})
429429
})
430430

431+
describe('Canvas Actions', () => {
432+
it('should render canvasActions content in the canvas', async () => {
433+
const { getByTestId } = render(
434+
<Agentflow {...defaultProps} canvasActions={<button data-testid='custom-action'>My Button</button>} />
435+
)
436+
437+
await waitFor(() => {
438+
expect(getByTestId('custom-action')).toBeInTheDocument()
439+
})
440+
})
441+
442+
it('should not render canvasActions in read-only mode', async () => {
443+
const { container, queryByTestId } = render(
444+
<Agentflow {...defaultProps} readOnly={true} canvasActions={<button data-testid='custom-action'>My Button</button>} />
445+
)
446+
447+
await waitFor(() => {
448+
expect(container.querySelector('.agentflow-container')).toBeInTheDocument()
449+
})
450+
451+
expect(queryByTestId('custom-action')).not.toBeInTheDocument()
452+
})
453+
})
454+
431455
describe('Imperative Ref', () => {
432456
it('should expose agentflow instance via ref', async () => {
433457
const ref = createRef<AgentFlowInstance>()

packages/agentflow/src/Agentflow.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function AgentflowCanvas({
4141
showDefaultHeader = true,
4242
enableGenerator = true,
4343
showDefaultPalette = true,
44+
canvasActions,
4445
renderHeader,
4546
renderNodePalette
4647
}: {
@@ -52,6 +53,7 @@ function AgentflowCanvas({
5253
showDefaultHeader?: boolean
5354
showDefaultPalette?: boolean
5455
enableGenerator?: boolean
56+
canvasActions?: AgentflowProps['canvasActions']
5557
renderHeader?: AgentflowProps['renderHeader']
5658
renderNodePalette?: AgentflowProps['renderNodePalette']
5759
}) {
@@ -272,14 +274,17 @@ function AgentflowCanvas({
272274
</StyledFab>
273275
)}
274276

275-
{/* Validation Feedback - positioned at top right */}
277+
{/* Canvas action buttons - positioned at top right */}
276278
{!readOnly && (
277-
<ValidationFeedback
278-
nodes={nodes as FlowNode[]}
279-
edges={edges as FlowEdge[]}
280-
availableNodes={availableNodes}
281-
setNodes={setLocalNodes as React.Dispatch<React.SetStateAction<FlowNode[]>>}
282-
/>
279+
<div style={{ position: 'absolute', right: 20, top: 20, zIndex: 1001, display: 'flex', gap: 8 }}>
280+
<ValidationFeedback
281+
nodes={nodes as FlowNode[]}
282+
edges={edges as FlowEdge[]}
283+
availableNodes={availableNodes}
284+
setNodes={setLocalNodes as React.Dispatch<React.SetStateAction<FlowNode[]>>}
285+
/>
286+
{canvasActions}
287+
</div>
283288
)}
284289

285290
<ReactFlow
@@ -368,7 +373,8 @@ export const Agentflow = forwardRef<AgentFlowInstance, AgentflowProps>(function
368373
renderHeader,
369374
renderNodePalette,
370375
showDefaultHeader = true,
371-
showDefaultPalette = true
376+
showDefaultPalette = true,
377+
canvasActions
372378
} = props
373379

374380
return (
@@ -394,6 +400,7 @@ export const Agentflow = forwardRef<AgentFlowInstance, AgentflowProps>(function
394400
showDefaultPalette={showDefaultPalette}
395401
renderHeader={renderHeader}
396402
renderNodePalette={renderNodePalette}
403+
canvasActions={canvasActions}
397404
/>
398405
</ReactFlowProvider>
399406
</AgentflowProvider>
@@ -416,6 +423,7 @@ const AgentflowCanvasWithRef = forwardRef<
416423
enableGenerator?: boolean
417424
renderHeader?: AgentflowProps['renderHeader']
418425
renderNodePalette?: AgentflowProps['renderNodePalette']
426+
canvasActions?: AgentflowProps['canvasActions']
419427
}
420428
>(function AgentflowCanvasWithRef(props, ref) {
421429
const agentflow = useAgentflow()

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,21 @@ export interface AgentflowProps {
5959
/** Whether the canvas is read-only */
6060
readOnly?: boolean
6161

62+
/**
63+
* Additional buttons rendered in the top-right canvas overlay, to the right of the built-in
64+
* validation FAB. Consumers have full control over content — pass any ReactNode (FABs, icon
65+
* buttons, popovers, dialogs, etc.). Hidden when `readOnly` is true.
66+
*
67+
* @example
68+
* // Add a chat button alongside the validation FAB (mirrors legacy v2 pattern)
69+
* canvasActions={
70+
* <Fab size="small" color="secondary" onClick={() => setShowChat(true)}>
71+
* <IconMessage />
72+
* </Fab>
73+
* }
74+
*/
75+
canvasActions?: ReactNode
76+
6277
/** Custom header renderer - receives save/export handlers */
6378
renderHeader?: (props: HeaderRenderProps) => ReactNode
6479

packages/agentflow/src/features/canvas/components/ValidationFeedback.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ function ValidationFeedbackComponent({ nodes, edges, availableNodes, setNodes }:
144144
return (
145145
<>
146146
<ClickAwayListener onClickAway={handleClose}>
147-
<div ref={containerRef} style={{ position: 'absolute', right: 20, top: 20, zIndex: 1001 }}>
147+
<div ref={containerRef} style={{ position: 'relative' }}>
148148
<Fab
149149
size='small'
150150
aria-label='validation'

0 commit comments

Comments
 (0)