diff --git a/packages/agentflow/examples/.env.example b/packages/agentflow/examples/.env.example index b8408dbdc56..a97b733d0d1 100644 --- a/packages/agentflow/examples/.env.example +++ b/packages/agentflow/examples/.env.example @@ -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/ +VITE_FLOW_ID= diff --git a/packages/agentflow/examples/README.md b/packages/agentflow/examples/README.md index 034c2d7d90f..79ec294b83e 100644 --- a/packages/agentflow/examples/README.md +++ b/packages/agentflow/examples/README.md @@ -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/` | **Note**: The `.env` file is gitignored and will not be committed to version control. Add your actual API key to `.env`, not `.env.example`. @@ -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** @@ -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 `` 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 diff --git a/packages/agentflow/examples/src/App.tsx b/packages/agentflow/examples/src/App.tsx index 9dac1d2fa5d..3f588772de9 100644 --- a/packages/agentflow/examples/src/App.tsx +++ b/packages/agentflow/examples/src/App.tsx @@ -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, @@ -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', @@ -115,7 +123,7 @@ function LoadingFallback() { } export default function App() { - const [selectedExample, setSelectedExample] = useState('basic') + const [selectedExample, setSelectedExample] = useState(agentflowId ? 'e2e' : 'basic') const [showProps, setShowProps] = useState(false) // Config loaded from environment variables diff --git a/packages/agentflow/examples/src/SaveToDbDialog.tsx b/packages/agentflow/examples/src/SaveToDbDialog.tsx new file mode 100644 index 00000000000..1bc81f9e285 --- /dev/null +++ b/packages/agentflow/examples/src/SaveToDbDialog.tsx @@ -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(null) + + const authHeaders: Record = { '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 ( +
+
+
Save flow to database?
+
+ No VITE_FLOW_ID is configured. The current flow will be saved to the database + as a new agentflow. +
+ {error &&
Error: {error}
} +
+ + +
+
+
+ ) +} diff --git a/packages/agentflow/examples/src/TestRunDialog.tsx b/packages/agentflow/examples/src/TestRunDialog.tsx new file mode 100644 index 00000000000..da65cee1a87 --- /dev/null +++ b/packages/agentflow/examples/src/TestRunDialog.tsx @@ -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(null) + const [error, setError] = useState(null) + + const authHeaders: Record = { '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 ( +
{ + if (e.target === e.currentTarget) onClose() + }} + onKeyDown={(e) => { + if (e.key === 'Escape') onClose() + }} + > +
+
+
+ Test Run + +
+ +
+ POST /api/v1/internal-prediction/{agentflowId} +
+ +