Skip to content

Commit ca09232

Browse files
authored
Merge branch 'main' into fix/required-vars
2 parents 8ed2410 + 66374bf commit ca09232

37 files changed

Lines changed: 1836 additions & 298 deletions

File tree

packages/agentflow/TESTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ These modules carry the highest risk. Test in the same PR when modifying.
3636
| `src/infrastructure/api/nodes.ts` | `getAllNodes`, `getNodeByName`, `getNodeIconUrl` | ✅ Done |
3737
| `src/infrastructure/store/AgentflowContext.tsx` | `agentflowReducer` (all actions), `normalizeNodes`. Remaining: `deleteNode()`, `duplicateNode()`, `updateNodeData()`, `getFlowData()` | 🟡 Partial |
3838
| `src/useAgentflow.ts` | `getFlow()`, `toJSON()`, `validate()`, `addNode()`, `clear()` | ⬜ Not yet — thin wrapper |
39-
| `src/features/canvas/hooks/useFlowHandlers.ts` | `onConnect`, `onNodesChange`, `onEdgesChange`, `onAddNode` | ⬜ Not yet — coupled to ReactFlow |
39+
| `src/features/canvas/hooks/useFlowHandlers.ts` | `handleConnect`, `handleNodesChange`, `handleEdgesChange`, `handleAddNode` — synchronous `onFlowChange` callbacks, dirty tracking, viewport resolution, change filtering | ✅ Done |
4040

4141
### Tier 2 — Feature Hooks & Dialogs
4242

@@ -70,7 +70,7 @@ Mostly JSX with minimal logic. Only add tests if business logic is introduced.
7070
| `src/atoms/NodeInputHandler.tsx` | If input rendering or position calculation logic changes | ⬜ Not yet |
7171
| `src/features/canvas/components/ConnectionLine.tsx` | If edge label determination logic becomes more complex | ⬜ Not yet |
7272
| `src/features/canvas/components/NodeStatusIndicator.tsx` | If status-to-color/icon mapping expands | ⬜ Not yet |
73-
| `src/Agentflow.tsx` | Integration test — dark mode, ThemeProvider, CSS variables, header rendering, generate flow dialog, imperative ref | ✅ Done |
73+
| `src/Agentflow.tsx` | Integration test — dark mode, ThemeProvider, CSS variables, header rendering, keyboard shortcuts (Cmd+S / Ctrl+S save), generate flow dialog, imperative ref | ✅ Done |
7474

7575
Files that are pure styling or data constants (`styled.ts`, `nodeIcons.ts`, `MainCard.tsx`, `Input.tsx`, etc.) do not need dedicated tests.
7676

@@ -127,6 +127,7 @@ Key features:
127127
- **Coverage thresholds**: uniform 80% floor (`branches`, `functions`, `lines`, `statements`) enforced per-path:
128128
- `./src/Agentflow.tsx`
129129
- `./src/core/`
130+
- `./src/features/canvas/hooks/useFlowHandlers.ts`
130131
- `./src/features/node-palette/search.ts`
131132
- `./src/infrastructure/api/`
132133
- **Coverage exclusions**:

packages/agentflow/examples/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function LoadingFallback() {
100100

101101
export default function App() {
102102
const [selectedExample, setSelectedExample] = useState<ExampleId>('basic')
103-
const [showProps, setShowProps] = useState(true)
103+
const [showProps, setShowProps] = useState(false)
104104
// Config loaded from environment variables
105105

106106
const currentExample = examples.find((e) => e.id === selectedExample)
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/**
2+
* FlowStatePanel Component
3+
*
4+
* Displays live onFlowChange data and saved flow snapshots
5+
* in a dark-themed, resizable side panel with copy support.
6+
*/
7+
8+
import { useCallback, useEffect, useRef, useState } from 'react'
9+
10+
import type { FlowData } from '@flowiseai/agentflow'
11+
12+
type FlowStatePanelTab = 'live' | 'saved'
13+
14+
interface FlowStatePanelProps {
15+
currentFlow: FlowData | null
16+
savedFlow: FlowData | null
17+
changeCount: number
18+
}
19+
20+
export function FlowStatePanel({ currentFlow, savedFlow, changeCount }: FlowStatePanelProps) {
21+
const [tab, setTab] = useState<FlowStatePanelTab>('live')
22+
const [copied, setCopied] = useState(false)
23+
const [width, setWidth] = useState(300)
24+
const dragging = useRef(false)
25+
const flow = tab === 'live' ? currentFlow : savedFlow
26+
27+
const startX = useRef(0)
28+
const startWidth = useRef(0)
29+
30+
const resizeBy = useCallback((delta: number) => {
31+
setWidth((w) => Math.max(200, Math.min(800, w + delta)))
32+
}, [])
33+
34+
const onMouseMove = useCallback((moveEvent: MouseEvent) => {
35+
if (!dragging.current) return
36+
const newWidth = Math.max(200, Math.min(800, startWidth.current + (startX.current - moveEvent.clientX)))
37+
setWidth(newWidth)
38+
}, [])
39+
40+
const onMouseUp = useCallback(() => {
41+
dragging.current = false
42+
document.removeEventListener('mousemove', onMouseMove)
43+
document.removeEventListener('mouseup', onMouseUp)
44+
document.body.style.cursor = ''
45+
document.body.style.userSelect = ''
46+
}, [onMouseMove])
47+
48+
// Clean up global listeners on unmount to prevent memory leaks
49+
useEffect(() => {
50+
return () => {
51+
document.removeEventListener('mousemove', onMouseMove)
52+
document.removeEventListener('mouseup', onMouseUp)
53+
document.body.style.cursor = ''
54+
document.body.style.userSelect = ''
55+
}
56+
}, [onMouseMove, onMouseUp])
57+
58+
const handleMouseDown = useCallback(
59+
(e: React.MouseEvent) => {
60+
e.preventDefault()
61+
dragging.current = true
62+
startX.current = e.clientX
63+
startWidth.current = width
64+
65+
document.addEventListener('mousemove', onMouseMove)
66+
document.addEventListener('mouseup', onMouseUp)
67+
document.body.style.cursor = 'col-resize'
68+
document.body.style.userSelect = 'none'
69+
},
70+
[width, onMouseMove, onMouseUp]
71+
)
72+
73+
const handleKeyDown = useCallback(
74+
(e: React.KeyboardEvent) => {
75+
if (e.key === 'ArrowLeft') {
76+
e.preventDefault()
77+
resizeBy(-20)
78+
} else if (e.key === 'ArrowRight') {
79+
e.preventDefault()
80+
resizeBy(20)
81+
}
82+
},
83+
[resizeBy]
84+
)
85+
86+
return (
87+
<div
88+
style={{
89+
width: `${width}px`,
90+
minHeight: 0,
91+
background: '#1e1e2e',
92+
color: '#cdd6f4',
93+
display: 'flex',
94+
flexDirection: 'column',
95+
fontSize: '13px',
96+
fontFamily: 'monospace',
97+
borderLeft: '1px solid #313244',
98+
overflow: 'hidden',
99+
position: 'relative'
100+
}}
101+
>
102+
{/* Drag handle */}
103+
<button
104+
aria-label='Resize panel'
105+
onMouseDown={handleMouseDown}
106+
onKeyDown={handleKeyDown}
107+
style={{
108+
position: 'absolute',
109+
left: 0,
110+
top: 0,
111+
bottom: 0,
112+
width: '4px',
113+
cursor: 'col-resize',
114+
zIndex: 10,
115+
padding: 0,
116+
border: 'none',
117+
background: 'transparent'
118+
}}
119+
/>
120+
{/* Tabs */}
121+
<div style={{ display: 'flex', borderBottom: '1px solid #313244' }}>
122+
<button
123+
onClick={() => setTab('live')}
124+
style={{
125+
flex: 1,
126+
padding: '10px',
127+
background: tab === 'live' ? '#313244' : 'transparent',
128+
color: tab === 'live' ? '#cba6f7' : '#6c7086',
129+
border: 'none',
130+
cursor: 'pointer',
131+
fontFamily: 'monospace',
132+
fontSize: '12px',
133+
fontWeight: 600
134+
}}
135+
>
136+
onFlowChange ({changeCount})
137+
</button>
138+
<button
139+
onClick={() => setTab('saved')}
140+
style={{
141+
flex: 1,
142+
padding: '10px',
143+
background: tab === 'saved' ? '#313244' : 'transparent',
144+
color: tab === 'saved' ? '#a6e3a1' : '#6c7086',
145+
border: 'none',
146+
cursor: 'pointer',
147+
fontFamily: 'monospace',
148+
fontSize: '12px',
149+
fontWeight: 600
150+
}}
151+
>
152+
onSave {savedFlow ? '(1)' : '(0)'}
153+
</button>
154+
</div>
155+
156+
{/* Summary stats + copy button */}
157+
{flow && (
158+
<div
159+
style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 14px', borderBottom: '1px solid #313244' }}
160+
>
161+
<span>
162+
<span style={{ color: '#89b4fa' }}>nodes:</span> {flow.nodes.length}
163+
</span>
164+
<span>
165+
<span style={{ color: '#f9e2af' }}>edges:</span> {flow.edges.length}
166+
</span>
167+
<button
168+
onClick={() => {
169+
navigator.clipboard.writeText(JSON.stringify(flow, null, 2))
170+
setCopied(true)
171+
setTimeout(() => setCopied(false), 1500)
172+
}}
173+
style={{
174+
marginLeft: 'auto',
175+
padding: '3px 10px',
176+
background: copied ? '#a6e3a1' : '#45475a',
177+
color: copied ? '#1e1e2e' : '#cdd6f4',
178+
border: 'none',
179+
borderRadius: '4px',
180+
cursor: 'pointer',
181+
fontFamily: 'monospace',
182+
fontSize: '11px',
183+
transition: 'all 0.15s'
184+
}}
185+
>
186+
{copied ? 'Copied!' : 'Copy'}
187+
</button>
188+
</div>
189+
)}
190+
191+
{/* JSON payload */}
192+
<div style={{ flex: 1, overflow: 'auto', padding: '10px 14px' }}>
193+
{flow ? (
194+
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word', lineHeight: 1.5 }}>
195+
{JSON.stringify(flow, null, 2)}
196+
</pre>
197+
) : (
198+
<div style={{ color: '#6c7086', padding: '20px 0', textAlign: 'center' }}>
199+
{tab === 'live'
200+
? 'Interact with the canvas to see live flow data.'
201+
: 'Click Save or press Cmd+S to capture a snapshot.'}
202+
</div>
203+
)}
204+
</div>
205+
</div>
206+
)
207+
}

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

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
/**
22
* Basic Example
33
*
4-
* Demonstrates basic @flowiseai/agentflow usage with imperative methods.
4+
* Demonstrates basic @flowiseai/agentflow usage with imperative methods,
5+
* onFlowChange tracking, and save flow functionality.
56
*/
67

7-
import { useRef, useState } from 'react'
8+
import { useCallback, useRef, useState } from 'react'
89

910
import type { AgentFlowInstance, FlowData, ValidationResult } from '@flowiseai/agentflow'
1011
import { Agentflow } from '@flowiseai/agentflow'
1112

1213
import { apiBaseUrl, token } from '../config'
14+
import { FlowStatePanel } from '../FlowStatePanel'
1315

1416
// Example flow data
1517
const initialFlow: FlowData = {
@@ -33,18 +35,22 @@ const initialFlow: FlowData = {
3335
}
3436

3537
export function BasicExample() {
36-
// Config loaded from environment variables
3738
const agentflowRef = useRef<AgentFlowInstance>(null)
3839
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
40+
const [currentFlow, setCurrentFlow] = useState<FlowData | null>(null)
41+
const [savedFlow, setSavedFlow] = useState<FlowData | null>(null)
42+
const [changeCount, setChangeCount] = useState(0)
3943

40-
const handleFlowChange = (flow: FlowData) => {
41-
console.log('Flow changed:', flow)
42-
}
44+
const handleFlowChange = useCallback((flow: FlowData) => {
45+
setCurrentFlow(flow)
46+
setChangeCount((c) => c + 1)
47+
console.log('onFlowChange:', flow)
48+
}, [])
4349

44-
const handleSave = (flow: FlowData) => {
45-
console.log('Flow saved:', flow)
46-
alert('Flow saved! Check console for data.')
47-
}
50+
const handleSave = useCallback((flow: FlowData) => {
51+
setSavedFlow(flow)
52+
console.log('onSave:', flow)
53+
}, [])
4854

4955
const handleValidate = () => {
5056
if (agentflowRef.current) {
@@ -98,17 +104,20 @@ export function BasicExample() {
98104
)}
99105
</div>
100106

101-
{/* Canvas */}
102-
<div style={{ flex: 1 }}>
103-
<Agentflow
104-
ref={agentflowRef}
105-
apiBaseUrl={apiBaseUrl}
106-
token={token ?? undefined}
107-
initialFlow={initialFlow}
108-
onFlowChange={handleFlowChange}
109-
onSave={handleSave}
110-
showDefaultHeader={true}
111-
/>
107+
{/* Canvas + Flow State Panel */}
108+
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
109+
<div style={{ flex: 1 }}>
110+
<Agentflow
111+
ref={agentflowRef}
112+
apiBaseUrl={apiBaseUrl}
113+
token={token ?? undefined}
114+
initialFlow={initialFlow}
115+
onFlowChange={handleFlowChange}
116+
onSave={handleSave}
117+
showDefaultHeader={true}
118+
/>
119+
</div>
120+
<FlowStatePanel currentFlow={currentFlow} savedFlow={savedFlow} changeCount={changeCount} />
112121
</div>
113122
</div>
114123
)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,11 @@ export function CustomUIExample() {
260260
apiBaseUrl={apiBaseUrl}
261261
token={token ?? undefined}
262262
initialFlow={initialFlow}
263-
renderHeader={(props) => <CustomHeader {...props} />}
264-
renderNodePalette={(props) => <CustomPalette {...props} />}
263+
renderHeader={(props: HeaderRenderProps) => <CustomHeader {...props} />}
264+
renderNodePalette={(props: PaletteRenderProps) => <CustomPalette {...props} />}
265265
showDefaultHeader={false}
266266
showDefaultPalette={false}
267-
onSave={(flow) => {
267+
onSave={(flow: FlowData) => {
268268
console.log('Saving flow:', flow)
269269
alert('Flow saved! Check console.')
270270
}}

packages/agentflow/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ module.exports = {
3838
coverageThreshold: {
3939
'./src/Agentflow.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
4040
'./src/core/': { branches: 80, functions: 80, lines: 80, statements: 80 },
41+
'./src/features/canvas/hooks/useFlowHandlers.ts': { branches: 80, functions: 80, lines: 80, statements: 80 },
4142
'./src/features/node-palette/search.ts': { branches: 80, functions: 80, lines: 80, statements: 80 },
4243
'./src/infrastructure/api/': { branches: 80, functions: 80, lines: 80, statements: 80 }
4344
},

packages/agentflow/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@
4444
"*.css"
4545
],
4646
"scripts": {
47-
"build": "tsc && NODE_ENV=production vite build",
47+
"build": "tsc && cross-env NODE_ENV=production vite build",
4848
"clean": "rimraf dist",
49-
"dev": "NODE_ENV=development vite",
50-
"dev:example": "NODE_ENV=development vite --config examples/vite.config.ts",
49+
"dev": "cross-env NODE_ENV=development vite",
50+
"dev:example": "cross-env NODE_ENV=development vite --config examples/vite.config.ts",
5151
"format": "prettier --write \"{src,examples}/**/*.{ts,tsx,js,jsx,json,css,md}\"",
5252
"format:check": "prettier --check \"{src,examples}/**/*.{ts,tsx,js,jsx,json,css,md}\"",
5353
"lint": "eslint \"{src,examples/src}/**/*.{js,jsx,ts,tsx,json,md}\"",
@@ -89,6 +89,7 @@
8989
"eslint-plugin-simple-import-sort": "^12.0.0",
9090
"jest": "^29.7.0",
9191
"jest-environment-jsdom": "^29.7.0",
92+
"cross-env": "^7.0.3",
9293
"rimraf": "^5.0.5",
9394
"ts-jest": "^29.3.2",
9495
"typescript": "^5.4.5",

0 commit comments

Comments
 (0)