Skip to content

Commit f82f6e3

Browse files
authored
Merge branch 'main' into fix/docs-env-variables-link
2 parents c596795 + 3542a04 commit f82f6e3

21 files changed

+812
-85
lines changed

packages/agentflow/examples/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ VITE_INSTANCE_URL=http://localhost:3000
55
# Important: Use an API Key, NOT a user authentication token
66
# Get this from: Flowise UI → Settings → API Keys → Create New Key
77
VITE_API_TOKEN=
8+
9+
# (Optional) Agentflow ID to load on startup
10+
# When set, the canvas loads the saved flow from the database and enables
11+
# Test Run and Run Status polling without needing to save first.
12+
# Get this from the URL when editing a flow in Flowise: /agentflows/<id>
13+
VITE_FLOW_ID=

packages/agentflow/examples/README.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@ The examples app uses environment variables for configuration. To set up:
6262

6363
**Environment Variables:**
6464

65-
- `VITE_INSTANCE_URL`: Flowise API server endpoint (maps to `apiBaseUrl` prop, default: `http://localhost:3000`)
66-
- `VITE_API_TOKEN`: Flowise API Key for programmatic access (required for authenticated endpoints)
65+
| Variable | Required | Description |
66+
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
67+
| `VITE_INSTANCE_URL` | No | Flowise API server endpoint (default: `http://localhost:3000`) |
68+
| `VITE_API_TOKEN` | Yes (authenticated) | Flowise API Key — get from Settings → API Keys |
69+
| `VITE_FLOW_ID` | No | Agentflow ID to load on startup. When set, the canvas loads the saved flow from the database and enables Test Run and Run Status polling without saving first. Copy the ID from the Flowise URL: `/agentflows/<id>` |
6770

6871
**Note**: The `.env` file is gitignored and will not be committed to version control. Add your actual API key to `.env`, not `.env.example`.
6972

@@ -81,7 +84,6 @@ Common causes:
8184
2. **Token not loaded**
8285
8386
- Restart dev server after editing `.env`: `pnpm dev`
84-
- Check browser console for: `[BasicExample] Environment check`
8587
8688
3. **Invalid API Key**
8789
@@ -94,14 +96,29 @@ Common causes:
9496
9597
## Examples
9698
99+
The app opens to the **E2E (Live Instance)** example when `VITE_FLOW_ID` is set, and falls back to **Basic Usage** otherwise.
100+
97101
### Basic Usage (`BasicExample.tsx`)
98102
99-
Demonstrates core usage:
103+
Minimal canvas integration — no database calls:
104+
105+
- Rendering the canvas with a hardcoded `initialFlow`
106+
- Tracking flow changes via `onFlowChange`
107+
- Local-only save via `onSave`
108+
- Imperative `fitView` / `clear` via ref
109+
110+
### E2E — Live Instance (`E2eExample.tsx`)
111+
112+
Full integration with a running Flowise instance. Requires `VITE_FLOW_ID` for the best experience:
113+
114+
- Loads the saved flow from the database on startup (`VITE_FLOW_ID`)
115+
- Editable flow title synced to the database on save
116+
- Save to DB — prompts to create a new chatflow when no ID is configured
117+
- Delete chatflow from the database
118+
- Test Run via `POST /api/v1/internal-prediction` with markdown-rendered response (disabled when flow has validation errors)
119+
- Run Status panel showing per-node execution results (manual refresh)
100120
101-
- Basic canvas rendering with `<Agentflow>` component
102-
- Passing `apiBaseUrl` and `initialFlow` props
103-
- Using the `ref` to access imperative methods (`validate`, `fitView`, `getFlow`, `clear`)
104-
- Handling `onFlowChange` and `onSave` callbacks
121+
> **API Token permissions required:** The `VITE_API_TOKEN` used for the E2E example must have **Create**, **Update**, and **Delete** permissions for Agentflows. A read-only key is not sufficient — save, rename, and delete operations will return 403.
105122
106123
### Additional Examples
107124

packages/agentflow/examples/src/App.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66

77
import { type ComponentType, lazy, Suspense, useState } from 'react'
88

9-
import { apiBaseUrl, token } from './config'
9+
import { agentflowId, apiBaseUrl, token } from './config'
1010
import {
1111
AllNodeTypesExampleProps,
1212
BasicExampleProps,
1313
CustomNodeExampleProps,
1414
CustomUIExampleProps,
1515
DarkModeExampleProps,
16+
E2eExampleProps,
1617
FilteredComponentsExampleProps,
1718
MultiNodeFlowProps,
1819
StatusIndicatorsExampleProps,
@@ -34,6 +35,13 @@ const examples: Array<{
3435
props: BasicExampleProps,
3536
component: lazy(() => import('./demos/BasicExample').then((m) => ({ default: m.BasicExample })))
3637
},
38+
{
39+
id: 'e2e',
40+
name: 'E2E (Live Instance)',
41+
description: 'Full integration: load/save/delete flow, test run, run status — requires VITE_FLOW_ID',
42+
props: E2eExampleProps,
43+
component: lazy(() => import('./demos/E2eExample').then((m) => ({ default: m.E2eExample })))
44+
},
3745
{
3846
id: 'multi-node',
3947
name: 'Multi-Node Flow',
@@ -115,7 +123,7 @@ function LoadingFallback() {
115123
}
116124

117125
export default function App() {
118-
const [selectedExample, setSelectedExample] = useState<ExampleId>('basic')
126+
const [selectedExample, setSelectedExample] = useState<ExampleId>(agentflowId ? 'e2e' : 'basic')
119127
const [showProps, setShowProps] = useState(false)
120128
// Config loaded from environment variables
121129

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* SaveToDbDialog
3+
*
4+
* Shown when the user clicks Save and no VITE_FLOW_ID is configured.
5+
* Creates a new agentflow via POST /api/v1/chatflows and reports the new ID.
6+
*/
7+
8+
import { useState } from 'react'
9+
10+
import type { FlowData } from '@flowiseai/agentflow'
11+
12+
import { apiBaseUrl, token } from './config'
13+
14+
interface SaveToDbDialogProps {
15+
flow: FlowData
16+
flowName: string
17+
onSaved: (agentflowId: string) => void
18+
onCancel: () => void
19+
}
20+
21+
export function SaveToDbDialog({ flow, flowName, onSaved, onCancel }: SaveToDbDialogProps) {
22+
const [saving, setSaving] = useState(false)
23+
const [error, setError] = useState<string | null>(null)
24+
25+
const authHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
26+
if (token) authHeaders['Authorization'] = `Bearer ${token}`
27+
28+
const handleConfirm = async () => {
29+
setSaving(true)
30+
setError(null)
31+
try {
32+
const body = {
33+
name: flowName,
34+
type: 'AGENTFLOW',
35+
flowData: JSON.stringify({ nodes: flow.nodes, edges: flow.edges, viewport: flow.viewport })
36+
}
37+
const res = await fetch(`${apiBaseUrl}/api/v1/chatflows`, {
38+
method: 'POST',
39+
headers: authHeaders,
40+
credentials: token ? 'omit' : 'include',
41+
body: JSON.stringify(body)
42+
})
43+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
44+
const created = await res.json()
45+
onSaved(created.id)
46+
} catch (e) {
47+
setError(e instanceof Error ? e.message : 'Save failed')
48+
setSaving(false)
49+
}
50+
}
51+
52+
return (
53+
<div
54+
style={{
55+
position: 'fixed',
56+
inset: 0,
57+
background: 'rgba(0,0,0,0.55)',
58+
zIndex: 1000,
59+
display: 'flex',
60+
alignItems: 'center',
61+
justifyContent: 'center'
62+
}}
63+
>
64+
<div
65+
style={{
66+
background: '#1e1e2e',
67+
border: '1px solid #313244',
68+
borderRadius: '8px',
69+
padding: '24px 28px',
70+
maxWidth: '400px',
71+
width: '90%',
72+
fontFamily: 'monospace',
73+
color: '#cdd6f4'
74+
}}
75+
>
76+
<div style={{ fontWeight: 700, fontSize: '14px', marginBottom: '12px', color: '#f9e2af' }}>Save flow to database?</div>
77+
<div style={{ fontSize: '13px', color: '#a6adc8', marginBottom: '20px', lineHeight: 1.6 }}>
78+
No <span style={{ color: '#f9e2af' }}>VITE_FLOW_ID</span> is configured. The current flow will be saved to the database
79+
as a new agentflow.
80+
</div>
81+
{error && <div style={{ color: '#f38ba8', fontSize: '12px', marginBottom: '12px' }}>Error: {error}</div>}
82+
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
83+
<button
84+
onClick={onCancel}
85+
disabled={saving}
86+
style={{
87+
padding: '6px 16px',
88+
background: '#45475a',
89+
color: '#cdd6f4',
90+
border: 'none',
91+
borderRadius: '4px',
92+
cursor: saving ? 'not-allowed' : 'pointer',
93+
fontFamily: 'monospace',
94+
fontSize: '12px',
95+
opacity: saving ? 0.5 : 1
96+
}}
97+
>
98+
Cancel
99+
</button>
100+
<button
101+
onClick={handleConfirm}
102+
disabled={saving}
103+
style={{
104+
padding: '6px 16px',
105+
background: '#a6e3a1',
106+
color: '#1e1e2e',
107+
border: 'none',
108+
borderRadius: '4px',
109+
cursor: saving ? 'not-allowed' : 'pointer',
110+
fontFamily: 'monospace',
111+
fontSize: '12px',
112+
fontWeight: 700,
113+
opacity: saving ? 0.7 : 1
114+
}}
115+
>
116+
{saving ? 'Saving…' : 'Save to DB'}
117+
</button>
118+
</div>
119+
</div>
120+
</div>
121+
)
122+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* TestRunDialog
3+
*
4+
* Sends a test question to POST /api/v1/internal-prediction/{agentflowId}
5+
* and displays the response, letting users verify the flow runs correctly.
6+
*/
7+
8+
import { useState } from 'react'
9+
10+
import MarkdownIt from 'markdown-it'
11+
12+
import { apiBaseUrl, token } from './config'
13+
14+
const md = new MarkdownIt({ linkify: true, breaks: true })
15+
16+
interface TestRunDialogProps {
17+
agentflowId: string
18+
onClose: () => void
19+
}
20+
21+
export function TestRunDialog({ agentflowId, onClose }: TestRunDialogProps) {
22+
const [question, setQuestion] = useState('')
23+
const [running, setRunning] = useState(false)
24+
const [result, setResult] = useState<string | null>(null)
25+
const [error, setError] = useState<string | null>(null)
26+
27+
const authHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
28+
if (token) authHeaders['Authorization'] = `Bearer ${token}`
29+
30+
const handleRun = async () => {
31+
if (!question.trim()) return
32+
setRunning(true)
33+
setResult(null)
34+
setError(null)
35+
try {
36+
const res = await fetch(`${apiBaseUrl}/api/v1/internal-prediction/${agentflowId}`, {
37+
method: 'POST',
38+
headers: authHeaders,
39+
credentials: token ? 'omit' : 'include',
40+
body: JSON.stringify({ question: question.trim(), streaming: false })
41+
})
42+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
43+
const data = await res.json()
44+
// Response shape: { text, question, chatId, ... }
45+
setResult(typeof data.text === 'string' ? data.text : JSON.stringify(data, null, 2))
46+
} catch (e) {
47+
setError(e instanceof Error ? e.message : 'Request failed')
48+
} finally {
49+
setRunning(false)
50+
}
51+
}
52+
53+
return (
54+
<div
55+
style={{
56+
position: 'fixed',
57+
inset: 0,
58+
background: 'rgba(0,0,0,0.45)',
59+
zIndex: 1000,
60+
display: 'flex',
61+
alignItems: 'center',
62+
justifyContent: 'center'
63+
}}
64+
role='presentation'
65+
onClick={(e) => {
66+
if (e.target === e.currentTarget) onClose()
67+
}}
68+
onKeyDown={(e) => {
69+
if (e.key === 'Escape') onClose()
70+
}}
71+
>
72+
<div
73+
style={{
74+
background: '#fff',
75+
borderRadius: '8px',
76+
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
77+
width: '520px',
78+
maxWidth: '95vw',
79+
maxHeight: '85vh',
80+
display: 'flex',
81+
flexDirection: 'column',
82+
overflow: 'hidden'
83+
}}
84+
>
85+
<div style={{ padding: '24px 28px', display: 'flex', flexDirection: 'column', gap: '14px', overflowY: 'auto' }}>
86+
<div style={{ display: 'flex', alignItems: 'center' }}>
87+
<span style={{ fontWeight: 700, fontSize: '15px', flex: 1 }}>Test Run</span>
88+
<button
89+
onClick={onClose}
90+
style={{ background: 'none', border: 'none', fontSize: '18px', cursor: 'pointer', color: '#666' }}
91+
>
92+
93+
</button>
94+
</div>
95+
96+
<div style={{ fontSize: '12px', color: '#888', fontFamily: 'monospace' }}>
97+
POST /api/v1/internal-prediction/<span style={{ color: '#1976d2' }}>{agentflowId}</span>
98+
</div>
99+
100+
<textarea
101+
value={question}
102+
onChange={(e) => setQuestion(e.target.value)}
103+
onKeyDown={(e) => {
104+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) handleRun()
105+
}}
106+
placeholder='Enter a question to send to the flow… (⌘+Enter to run)'
107+
rows={3}
108+
style={{
109+
width: '100%',
110+
padding: '10px',
111+
fontSize: '13px',
112+
border: '1px solid #ddd',
113+
borderRadius: '6px',
114+
resize: 'vertical',
115+
fontFamily: 'inherit',
116+
boxSizing: 'border-box'
117+
}}
118+
/>
119+
120+
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
121+
<button
122+
onClick={handleRun}
123+
disabled={running || !question.trim()}
124+
style={{
125+
padding: '7px 20px',
126+
background: running || !question.trim() ? '#ccc' : '#1976d2',
127+
color: '#fff',
128+
border: 'none',
129+
borderRadius: '5px',
130+
cursor: running || !question.trim() ? 'not-allowed' : 'pointer',
131+
fontWeight: 600,
132+
fontSize: '13px'
133+
}}
134+
>
135+
{running ? 'Running…' : 'Run'}
136+
</button>
137+
</div>
138+
139+
{error && (
140+
<div
141+
style={{
142+
background: '#fff3f3',
143+
border: '1px solid #f5c2c7',
144+
borderRadius: '6px',
145+
padding: '10px 14px',
146+
color: '#d32f2f',
147+
fontSize: '13px'
148+
}}
149+
>
150+
{error}
151+
</div>
152+
)}
153+
154+
{result !== null && (
155+
<div style={{ background: '#f6f8fa', border: '1px solid #e0e0e0', borderRadius: '6px', padding: '12px 14px' }}>
156+
<div style={{ fontSize: '11px', color: '#888', marginBottom: '6px', fontWeight: 600 }}>RESPONSE</div>
157+
<div style={{ fontSize: '13px', lineHeight: 1.6 }} dangerouslySetInnerHTML={{ __html: md.render(result) }} />
158+
</div>
159+
)}
160+
</div>
161+
</div>
162+
</div>
163+
)
164+
}

packages/agentflow/examples/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
*/
44
export const apiBaseUrl = import.meta.env.VITE_INSTANCE_URL || 'http://localhost:3000'
55
export const token = import.meta.env.VITE_API_TOKEN || undefined
6+
export const agentflowId = import.meta.env.VITE_FLOW_ID || undefined

0 commit comments

Comments
 (0)