Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/agentflow/examples/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ VITE_INSTANCE_URL=http://localhost:3000
# Important: Use an API Key, NOT a user authentication token
# Get this from: Flowise UI → Settings → API Keys → Create New Key
VITE_API_TOKEN=

# (Optional) 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 needing to save first.
# Get this from the URL when editing a flow in Flowise: /agentflows/<id>
VITE_FLOW_ID=
33 changes: 25 additions & 8 deletions packages/agentflow/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ The examples app uses environment variables for configuration. To set up:

**Environment Variables:**

- `VITE_INSTANCE_URL`: Flowise API server endpoint (maps to `apiBaseUrl` prop, default: `http://localhost:3000`)
- `VITE_API_TOKEN`: Flowise API Key for programmatic access (required for authenticated endpoints)
| Variable | Required | Description |
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VITE_INSTANCE_URL` | No | Flowise API server endpoint (default: `http://localhost:3000`) |
| `VITE_API_TOKEN` | Yes (authenticated) | Flowise API Key — get from Settings → API Keys |
| `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>` |

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

Expand All @@ -81,7 +84,6 @@ Common causes:
2. **Token not loaded**

- Restart dev server after editing `.env`: `pnpm dev`
- Check browser console for: `[BasicExample] Environment check`

3. **Invalid API Key**

Expand All @@ -94,14 +96,29 @@ Common causes:

## Examples

The app opens to the **E2E (Live Instance)** example when `VITE_FLOW_ID` is set, and falls back to **Basic Usage** otherwise.

### Basic Usage (`BasicExample.tsx`)

Demonstrates core usage:
Minimal canvas integration — no database calls:

- Rendering the canvas with a hardcoded `initialFlow`
- Tracking flow changes via `onFlowChange`
- Local-only save via `onSave`
- Imperative `fitView` / `clear` via ref

### E2E — Live Instance (`E2eExample.tsx`)

Full integration with a running Flowise instance. Requires `VITE_FLOW_ID` for the best experience:

- Loads the saved flow from the database on startup (`VITE_FLOW_ID`)
- Editable flow title synced to the database on save
- Save to DB — prompts to create a new chatflow when no ID is configured
- Delete chatflow from the database
- Test Run via `POST /api/v1/internal-prediction` with markdown-rendered response (disabled when flow has validation errors)
- Run Status panel showing per-node execution results (manual refresh)

- Basic canvas rendering with `<Agentflow>` component
- Passing `apiBaseUrl` and `initialFlow` props
- Using the `ref` to access imperative methods (`validate`, `fitView`, `getFlow`, `clear`)
- Handling `onFlowChange` and `onSave` callbacks
> **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.

### Additional Examples

Expand Down
12 changes: 10 additions & 2 deletions packages/agentflow/examples/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@

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

import { apiBaseUrl, token } from './config'
import { agentflowId, apiBaseUrl, token } from './config'
import {
AllNodeTypesExampleProps,
BasicExampleProps,
CustomNodeExampleProps,
CustomUIExampleProps,
DarkModeExampleProps,
E2eExampleProps,
FilteredComponentsExampleProps,
MultiNodeFlowProps,
StatusIndicatorsExampleProps,
Expand All @@ -34,6 +35,13 @@ const examples: Array<{
props: BasicExampleProps,
component: lazy(() => import('./demos/BasicExample').then((m) => ({ default: m.BasicExample })))
},
{
id: 'e2e',
name: 'E2E (Live Instance)',
description: 'Full integration: load/save/delete flow, test run, run status — requires VITE_FLOW_ID',
props: E2eExampleProps,
component: lazy(() => import('./demos/E2eExample').then((m) => ({ default: m.E2eExample })))
},
{
id: 'multi-node',
name: 'Multi-Node Flow',
Expand Down Expand Up @@ -115,7 +123,7 @@ function LoadingFallback() {
}

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

Expand Down
122 changes: 122 additions & 0 deletions packages/agentflow/examples/src/SaveToDbDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* SaveToDbDialog
*
* Shown when the user clicks Save and no VITE_FLOW_ID is configured.
* Creates a new agentflow via POST /api/v1/chatflows and reports the new ID.
*/

import { useState } from 'react'

import type { FlowData } from '@flowiseai/agentflow'

import { apiBaseUrl, token } from './config'

interface SaveToDbDialogProps {
flow: FlowData
flowName: string
onSaved: (agentflowId: string) => void
onCancel: () => void
}

export function SaveToDbDialog({ flow, flowName, onSaved, onCancel }: SaveToDbDialogProps) {
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)

const authHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
if (token) authHeaders['Authorization'] = `Bearer ${token}`

const handleConfirm = async () => {
setSaving(true)
setError(null)
try {
const body = {
name: flowName,
type: 'AGENTFLOW',
flowData: JSON.stringify({ nodes: flow.nodes, edges: flow.edges, viewport: flow.viewport })
}
const res = await fetch(`${apiBaseUrl}/api/v1/chatflows`, {
method: 'POST',
headers: authHeaders,
credentials: token ? 'omit' : 'include',
body: JSON.stringify(body)
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const created = await res.json()
onSaved(created.id)
} catch (e) {
setError(e instanceof Error ? e.message : 'Save failed')
setSaving(false)
}
}

return (
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.55)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<div
style={{
background: '#1e1e2e',
border: '1px solid #313244',
borderRadius: '8px',
padding: '24px 28px',
maxWidth: '400px',
width: '90%',
fontFamily: 'monospace',
color: '#cdd6f4'
}}
>
<div style={{ fontWeight: 700, fontSize: '14px', marginBottom: '12px', color: '#f9e2af' }}>Save flow to database?</div>
<div style={{ fontSize: '13px', color: '#a6adc8', marginBottom: '20px', lineHeight: 1.6 }}>
No <span style={{ color: '#f9e2af' }}>VITE_FLOW_ID</span> is configured. The current flow will be saved to the database
as a new agentflow.
</div>
{error && <div style={{ color: '#f38ba8', fontSize: '12px', marginBottom: '12px' }}>Error: {error}</div>}
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
<button
onClick={onCancel}
disabled={saving}
style={{
padding: '6px 16px',
background: '#45475a',
color: '#cdd6f4',
border: 'none',
borderRadius: '4px',
cursor: saving ? 'not-allowed' : 'pointer',
fontFamily: 'monospace',
fontSize: '12px',
opacity: saving ? 0.5 : 1
}}
>
Cancel
</button>
<button
onClick={handleConfirm}
disabled={saving}
style={{
padding: '6px 16px',
background: '#a6e3a1',
color: '#1e1e2e',
border: 'none',
borderRadius: '4px',
cursor: saving ? 'not-allowed' : 'pointer',
fontFamily: 'monospace',
fontSize: '12px',
fontWeight: 700,
opacity: saving ? 0.7 : 1
}}
>
{saving ? 'Saving…' : 'Save to DB'}
</button>
</div>
</div>
</div>
)
}
164 changes: 164 additions & 0 deletions packages/agentflow/examples/src/TestRunDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* TestRunDialog
*
* Sends a test question to POST /api/v1/internal-prediction/{agentflowId}
* and displays the response, letting users verify the flow runs correctly.
*/

import { useState } from 'react'

import MarkdownIt from 'markdown-it'

import { apiBaseUrl, token } from './config'

const md = new MarkdownIt({ linkify: true, breaks: true })

interface TestRunDialogProps {
agentflowId: string
onClose: () => void
}

export function TestRunDialog({ agentflowId, onClose }: TestRunDialogProps) {
const [question, setQuestion] = useState('')
const [running, setRunning] = useState(false)
const [result, setResult] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)

const authHeaders: Record<string, string> = { 'Content-Type': 'application/json' }
if (token) authHeaders['Authorization'] = `Bearer ${token}`

const handleRun = async () => {
if (!question.trim()) return
setRunning(true)
setResult(null)
setError(null)
try {
const res = await fetch(`${apiBaseUrl}/api/v1/internal-prediction/${agentflowId}`, {
method: 'POST',
headers: authHeaders,
credentials: token ? 'omit' : 'include',
body: JSON.stringify({ question: question.trim(), streaming: false })
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
// Response shape: { text, question, chatId, ... }
setResult(typeof data.text === 'string' ? data.text : JSON.stringify(data, null, 2))
} catch (e) {
setError(e instanceof Error ? e.message : 'Request failed')
} finally {
setRunning(false)
}
}

return (
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.45)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
role='presentation'
onClick={(e) => {
if (e.target === e.currentTarget) onClose()
}}
onKeyDown={(e) => {
if (e.key === 'Escape') onClose()
}}
>
<div
style={{
background: '#fff',
borderRadius: '8px',
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
width: '520px',
maxWidth: '95vw',
maxHeight: '85vh',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}
>
<div style={{ padding: '24px 28px', display: 'flex', flexDirection: 'column', gap: '14px', overflowY: 'auto' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ fontWeight: 700, fontSize: '15px', flex: 1 }}>Test Run</span>
<button
onClick={onClose}
style={{ background: 'none', border: 'none', fontSize: '18px', cursor: 'pointer', color: '#666' }}
>
</button>
</div>

<div style={{ fontSize: '12px', color: '#888', fontFamily: 'monospace' }}>
POST /api/v1/internal-prediction/<span style={{ color: '#1976d2' }}>{agentflowId}</span>
</div>

<textarea
value={question}
onChange={(e) => setQuestion(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) handleRun()
}}
placeholder='Enter a question to send to the flow… (⌘+Enter to run)'
rows={3}
style={{
width: '100%',
padding: '10px',
fontSize: '13px',
border: '1px solid #ddd',
borderRadius: '6px',
resize: 'vertical',
fontFamily: 'inherit',
boxSizing: 'border-box'
}}
/>

<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button
onClick={handleRun}
disabled={running || !question.trim()}
style={{
padding: '7px 20px',
background: running || !question.trim() ? '#ccc' : '#1976d2',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: running || !question.trim() ? 'not-allowed' : 'pointer',
fontWeight: 600,
fontSize: '13px'
}}
>
{running ? 'Running…' : 'Run'}
</button>
</div>

{error && (
<div
style={{
background: '#fff3f3',
border: '1px solid #f5c2c7',
borderRadius: '6px',
padding: '10px 14px',
color: '#d32f2f',
fontSize: '13px'
}}
>
{error}
</div>
)}

{result !== null && (
<div style={{ background: '#f6f8fa', border: '1px solid #e0e0e0', borderRadius: '6px', padding: '12px 14px' }}>
<div style={{ fontSize: '11px', color: '#888', marginBottom: '6px', fontWeight: 600 }}>RESPONSE</div>
<div style={{ fontSize: '13px', lineHeight: 1.6 }} dangerouslySetInnerHTML={{ __html: md.render(result) }} />
Comment thread
jocelynlin-wd marked this conversation as resolved.
</div>
)}
</div>
</div>
</div>
)
}
1 change: 1 addition & 0 deletions packages/agentflow/examples/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
*/
export const apiBaseUrl = import.meta.env.VITE_INSTANCE_URL || 'http://localhost:3000'
export const token = import.meta.env.VITE_API_TOKEN || undefined
export const agentflowId = import.meta.env.VITE_FLOW_ID || undefined
Loading
Loading