@flowdrop/flowdrop 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -24
- package/dist/adapters/WorkflowAdapter.js +2 -22
- package/dist/adapters/agentspec/autoLayout.d.ts +51 -5
- package/dist/adapters/agentspec/autoLayout.js +120 -23
- package/dist/chat/commandClassifier.d.ts +19 -0
- package/dist/chat/commandClassifier.js +30 -0
- package/dist/chat/index.d.ts +27 -0
- package/dist/chat/index.js +32 -0
- package/dist/chat/responseParser.d.ts +21 -0
- package/dist/chat/responseParser.js +87 -0
- package/dist/commands/batch.d.ts +18 -0
- package/dist/commands/batch.js +56 -0
- package/dist/commands/executor.d.ts +37 -0
- package/dist/commands/executor.js +1044 -0
- package/dist/commands/index.d.ts +14 -0
- package/dist/commands/index.js +17 -0
- package/dist/commands/parser.d.ts +16 -0
- package/dist/commands/parser.js +278 -0
- package/dist/commands/positioner.d.ts +19 -0
- package/dist/commands/positioner.js +33 -0
- package/dist/commands/storeIntegration.svelte.d.ts +16 -0
- package/dist/commands/storeIntegration.svelte.js +67 -0
- package/dist/commands/types.d.ts +343 -0
- package/dist/commands/types.js +45 -0
- package/dist/components/App.svelte +351 -12
- package/dist/components/App.svelte.d.ts +3 -0
- package/dist/components/CanvasController.svelte +38 -0
- package/dist/components/CanvasController.svelte.d.ts +32 -0
- package/dist/components/ConfigMappingRow.svelte +130 -0
- package/dist/components/ConfigMappingRow.svelte.d.ts +8 -0
- package/dist/components/ConfigPanel.svelte +56 -7
- package/dist/components/ConfigPanel.svelte.d.ts +2 -0
- package/dist/components/FlowDropEdge.svelte +2 -10
- package/dist/components/LogsSidebar.svelte +5 -5
- package/dist/components/NodeSidebar.svelte +15 -49
- package/dist/components/NodeSwapPicker.svelte +537 -0
- package/dist/components/NodeSwapPicker.svelte.d.ts +16 -0
- package/dist/components/PortMappingRow.svelte +209 -0
- package/dist/components/PortMappingRow.svelte.d.ts +12 -0
- package/dist/components/SwapMappingEditor.svelte +550 -0
- package/dist/components/SwapMappingEditor.svelte.d.ts +12 -0
- package/dist/components/WorkflowEditor.svelte +99 -4
- package/dist/components/WorkflowEditor.svelte.d.ts +8 -0
- package/dist/components/chat/AIChatPanel.svelte +658 -0
- package/dist/components/chat/AIChatPanel.svelte.d.ts +13 -0
- package/dist/components/chat/CommandPreview.svelte +184 -0
- package/dist/components/chat/CommandPreview.svelte.d.ts +9 -0
- package/dist/components/console/CommandConsole.stories.svelte +93 -0
- package/dist/components/console/CommandConsole.stories.svelte.d.ts +27 -0
- package/dist/components/console/CommandConsole.svelte +259 -0
- package/dist/components/console/CommandConsole.svelte.d.ts +11 -0
- package/dist/components/console/ConsoleAutocomplete.svelte +139 -0
- package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +21 -0
- package/dist/components/console/ConsoleInput.svelte +712 -0
- package/dist/components/console/ConsoleInput.svelte.d.ts +16 -0
- package/dist/components/console/ConsoleOutput.svelte +121 -0
- package/dist/components/console/ConsoleOutput.svelte.d.ts +11 -0
- package/dist/components/console/formatters.d.ts +26 -0
- package/dist/components/console/formatters.js +118 -0
- package/dist/components/interrupt/index.d.ts +1 -0
- package/dist/components/interrupt/index.js +1 -0
- package/dist/config/endpoints.d.ts +8 -0
- package/dist/config/endpoints.js +5 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +9 -0
- package/dist/editor/index.d.ts +3 -1
- package/dist/editor/index.js +4 -2
- package/dist/helpers/proximityConnect.js +8 -1
- package/dist/helpers/workflowEditorHelper.d.ts +3 -53
- package/dist/helpers/workflowEditorHelper.js +13 -228
- package/dist/playground/index.d.ts +1 -1
- package/dist/playground/index.js +1 -1
- package/dist/schemas/v1/workflow.schema.json +107 -22
- package/dist/services/chatService.d.ts +65 -0
- package/dist/services/chatService.js +131 -0
- package/dist/services/historyService.d.ts +6 -4
- package/dist/services/historyService.js +21 -6
- package/dist/stores/interruptStore.svelte.js +6 -1
- package/dist/stores/playgroundStore.svelte.d.ts +1 -1
- package/dist/stores/playgroundStore.svelte.js +11 -2
- package/dist/stores/portCoordinateStore.svelte.d.ts +4 -0
- package/dist/stores/portCoordinateStore.svelte.js +20 -26
- package/dist/stores/workflowStore.svelte.d.ts +31 -2
- package/dist/stores/workflowStore.svelte.js +84 -64
- package/dist/types/chat.d.ts +63 -0
- package/dist/types/chat.js +9 -0
- package/dist/types/events.d.ts +28 -2
- package/dist/types/events.js +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/settings.d.ts +6 -0
- package/dist/types/settings.js +3 -0
- package/dist/utils/edgeStyling.d.ts +42 -0
- package/dist/utils/edgeStyling.js +176 -0
- package/dist/utils/nodeIds.d.ts +31 -0
- package/dist/utils/nodeIds.js +42 -0
- package/dist/utils/nodeSwap.d.ts +221 -0
- package/dist/utils/nodeSwap.js +686 -0
- package/package.json +6 -1
- package/dist/helpers/nodeLayoutHelper.d.ts +0 -14
- package/dist/helpers/nodeLayoutHelper.js +0 -19
package/README.md
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
<img src="https://raw.githubusercontent.com/flowdrop-io/flowdrop/main/libs/flowdrop/static/logo.svg" alt="FlowDrop" width="120" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center">FlowDrop
|
|
5
|
+
<h1 align="center">FlowDrop™</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<img src="https://img.shields.io/github/actions/workflow/status/flowdrop-io/flowdrop/docker-publish.yml?style=flat-square&label=Build" alt="GitHub
|
|
8
|
+
<img src="https://img.shields.io/github/actions/workflow/status/flowdrop-io/flowdrop/docker-publish.yml?style=flat-square&label=Build" alt="GitHub build status" />
|
|
9
9
|
<a href="https://www.npmjs.com/package/@flowdrop/flowdrop"><img src="https://img.shields.io/npm/v/@flowdrop/flowdrop?style=flat-square" alt="npm" /></a>
|
|
10
10
|
<img src="https://img.shields.io/npm/unpacked-size/%40flowdrop%2Fflowdrop?style=flat-square" alt="NPM Unpacked Size" />
|
|
11
11
|
<img src="https://img.shields.io/npm/types/@flowdrop/flowdrop?style=flat-square" alt="npm type definitions" />
|
|
12
|
-
<a href="http://npmjs.com/package/@flowdrop/flowdrop"><img src="https://img.shields.io/npm/
|
|
12
|
+
<a href="http://npmjs.com/package/@flowdrop/flowdrop"><img alt="NPM Downloads" src="https://img.shields.io/npm/d18m/%40flowdrop%2Fflowdrop"></a>
|
|
13
|
+
|
|
14
|
+
|
|
13
15
|
</p>
|
|
14
16
|
|
|
15
17
|
<p align="center">
|
|
@@ -22,10 +24,10 @@
|
|
|
22
24
|
</p>
|
|
23
25
|
|
|
24
26
|
<p align="center">
|
|
25
|
-
<a href="
|
|
27
|
+
<a href="https://docs.flowdrop.io/getting-started/installation">Quickstart</a> •
|
|
26
28
|
<a href="#features">Features</a> •
|
|
27
29
|
<a href="#integration">Integration</a> •
|
|
28
|
-
<a href="
|
|
30
|
+
<a href="https://docs.flowdrop.io">Docs</a>
|
|
29
31
|
</p>
|
|
30
32
|
|
|
31
33
|
<p align="center">
|
|
@@ -70,12 +72,12 @@ You get a production-ready workflow UI. You keep full control of everything else
|
|
|
70
72
|
|
|
71
73
|
| | |
|
|
72
74
|
| ---------------------------- | ------------------------------------------------------------------------- |
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
75
|
+
| **Visual Editor Only** | Pure UI component. No hidden backend, no external dependencies |
|
|
76
|
+
| **You Own Everything** | Your data, your servers, your orchestration logic, your security policies |
|
|
77
|
+
| **Backend Agnostic** | Connect to any API: Drupal, Laravel, Express, FastAPI, or your own |
|
|
78
|
+
| **8 Built-in Node Types** | From simple icons to complex gateway logic |
|
|
79
|
+
| **Framework Flexible** | Use as Svelte component or mount into React, Vue, Angular, or vanilla JS |
|
|
80
|
+
| **Deploy Anywhere** | Runtime config means build once, deploy everywhere |
|
|
79
81
|
|
|
80
82
|
## Architecture Notes
|
|
81
83
|
|
|
@@ -85,7 +87,7 @@ You get a production-ready workflow UI. You keep full control of everything else
|
|
|
85
87
|
|
|
86
88
|
## Node Types
|
|
87
89
|
|
|
88
|
-
FlowDrop ships with
|
|
90
|
+
FlowDrop ships with 8 beautifully designed node types:
|
|
89
91
|
|
|
90
92
|
| Type | Purpose |
|
|
91
93
|
| ---------- | --------------------------------------- |
|
|
@@ -96,6 +98,7 @@ FlowDrop ships with 7 beautifully designed node types:
|
|
|
96
98
|
| `gateway` | Conditional branching logic |
|
|
97
99
|
| `terminal` | Start/end workflow points |
|
|
98
100
|
| `note` | Markdown documentation blocks |
|
|
101
|
+
| `idea` | Conceptual BPMN-like flow nodes |
|
|
99
102
|
|
|
100
103
|
<p align="center">
|
|
101
104
|
<img src="https://raw.githubusercontent.com/flowdrop-io/flowdrop/main/libs/flowdrop/static/Node-Types.jpg" alt="FlowDrop Node Types" width="800" />
|
|
@@ -104,6 +107,49 @@ FlowDrop ships with 7 beautifully designed node types:
|
|
|
104
107
|
<em>From simple triggers to complex branching logic, each node type is designed for specific workflow patterns.</em>
|
|
105
108
|
</p>
|
|
106
109
|
|
|
110
|
+
## Themes
|
|
111
|
+
|
|
112
|
+
FlowDrop includes a theme system with built-in light/dark support:
|
|
113
|
+
|
|
114
|
+
```svelte
|
|
115
|
+
<script lang="ts">
|
|
116
|
+
import { WorkflowEditor } from "@flowdrop/flowdrop";
|
|
117
|
+
import "@flowdrop/flowdrop/styles";
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<!-- Built-in themes: 'default' or 'minimal' -->
|
|
121
|
+
<WorkflowEditor theme="minimal" />
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Themes bundle a visual skin (CSS token palette) with behavioral UI defaults. You can also pass a custom theme object with your own skin tokens for full control over the light and dark palettes.
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
// Via the mount API
|
|
128
|
+
const app = await mountFlowDropApp(container, {
|
|
129
|
+
theme: "minimal",
|
|
130
|
+
// or a custom theme object:
|
|
131
|
+
// theme: { name: 'minimal', skin: { tokens: { primary: '#e11d48' } } }
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Sub-Module Exports
|
|
136
|
+
|
|
137
|
+
FlowDrop provides tree-shakeable sub-module exports so you can import only what you need:
|
|
138
|
+
|
|
139
|
+
| Export Path | Contents |
|
|
140
|
+
| --- | --- |
|
|
141
|
+
| `@flowdrop/flowdrop` | Full library (components, stores, services, types) |
|
|
142
|
+
| `@flowdrop/flowdrop/core` | Types and utilities only (no heavy dependencies) |
|
|
143
|
+
| `@flowdrop/flowdrop/editor` | WorkflowEditor, stores, services |
|
|
144
|
+
| `@flowdrop/flowdrop/form` | SchemaForm, form fields, registry |
|
|
145
|
+
| `@flowdrop/flowdrop/form/code` | Code editor field (CodeMirror) |
|
|
146
|
+
| `@flowdrop/flowdrop/form/markdown` | Markdown editor field |
|
|
147
|
+
| `@flowdrop/flowdrop/display` | MarkdownDisplay component |
|
|
148
|
+
| `@flowdrop/flowdrop/playground` | Playground components and services |
|
|
149
|
+
| `@flowdrop/flowdrop/settings` | SettingsPanel, stores, services |
|
|
150
|
+
| `@flowdrop/flowdrop/styles` | Base CSS stylesheet |
|
|
151
|
+
| `@flowdrop/flowdrop/schema` | Workflow JSON schema |
|
|
152
|
+
|
|
107
153
|
## Integration
|
|
108
154
|
|
|
109
155
|
### Svelte (Native)
|
|
@@ -173,8 +219,7 @@ Connect to any backend in seconds:
|
|
|
173
219
|
```typescript
|
|
174
220
|
import { createEndpointConfig } from "@flowdrop/flowdrop";
|
|
175
221
|
|
|
176
|
-
const config = createEndpointConfig({
|
|
177
|
-
baseUrl: "https://api.example.com",
|
|
222
|
+
const config = createEndpointConfig("https://api.example.com", {
|
|
178
223
|
endpoints: {
|
|
179
224
|
nodes: { list: "/nodes", get: "/nodes/{id}" },
|
|
180
225
|
workflows: {
|
|
@@ -185,20 +230,19 @@ const config = createEndpointConfig({
|
|
|
185
230
|
execute: "/workflows/{id}/execute",
|
|
186
231
|
},
|
|
187
232
|
},
|
|
188
|
-
auth: { type: "bearer", token: "your-token" },
|
|
189
233
|
});
|
|
190
234
|
```
|
|
191
235
|
|
|
192
236
|
## Customization
|
|
193
237
|
|
|
194
|
-
|
|
238
|
+
The recommended way to customize FlowDrop's appearance is through the [theme system](#themes). For fine-grained control, you can also override individual CSS custom properties:
|
|
195
239
|
|
|
196
240
|
```css
|
|
197
241
|
:root {
|
|
198
|
-
--
|
|
199
|
-
--
|
|
200
|
-
--
|
|
201
|
-
--
|
|
242
|
+
--fd-background: #0a0a0a;
|
|
243
|
+
--fd-primary: #6366f1;
|
|
244
|
+
--fd-border: #27272a;
|
|
245
|
+
--fd-foreground: #fafafa;
|
|
202
246
|
}
|
|
203
247
|
```
|
|
204
248
|
|
|
@@ -225,10 +269,9 @@ Runtime configuration means you build once and deploy to staging, production, or
|
|
|
225
269
|
|
|
226
270
|
| Resource | Description |
|
|
227
271
|
| ------------------------------------------------------------ | ------------------------ |
|
|
228
|
-
| [
|
|
229
|
-
| [
|
|
230
|
-
| [
|
|
231
|
-
| [CHANGELOG.md](./CHANGELOG.md) | Version history |
|
|
272
|
+
| [QUICK_START.md](https://docs.flowdrop.io/getting-started/installation/) | Get running in 5 minutes |
|
|
273
|
+
| [API Documentation](https://api.flowdrop.io/v1/) | REST API specification |
|
|
274
|
+
| [CHANGELOG.md](https://github.com/flowdrop-io/flowdrop/blob/main/libs/flowdrop/CHANGELOG.md) | Version history |
|
|
232
275
|
|
|
233
276
|
## Development
|
|
234
277
|
|
|
@@ -237,6 +280,7 @@ pnpm install # Install dependencies
|
|
|
237
280
|
pnpm dev # Start dev server
|
|
238
281
|
pnpm build # Build library
|
|
239
282
|
pnpm test # Run all tests
|
|
283
|
+
pnpm storybook # Launch Storybook
|
|
240
284
|
```
|
|
241
285
|
|
|
242
286
|
## Contributing
|
|
@@ -14,27 +14,7 @@
|
|
|
14
14
|
* - Systems that need to generate or modify workflows programmatically
|
|
15
15
|
*/
|
|
16
16
|
import { v4 as uuidv4 } from "uuid";
|
|
17
|
-
|
|
18
|
-
* Generate a unique node ID based on node type and existing nodes
|
|
19
|
-
* Format: <node_type>.<number>
|
|
20
|
-
* Example: boolean_gateway.1, calculator.2
|
|
21
|
-
*/
|
|
22
|
-
function generateStandardNodeId(nodeTypeId, existingNodes) {
|
|
23
|
-
// Count how many nodes of this type already exist
|
|
24
|
-
const existingNodeIds = existingNodes
|
|
25
|
-
.filter((node) => node.data?.metadata?.id === nodeTypeId)
|
|
26
|
-
.map((node) => node.id);
|
|
27
|
-
// Extract the numbers from existing IDs with the same prefix
|
|
28
|
-
const existingNumbers = existingNodeIds
|
|
29
|
-
.map((id) => {
|
|
30
|
-
const match = id.match(new RegExp(`^${nodeTypeId}\\.(\\d+)$`));
|
|
31
|
-
return match ? parseInt(match[1], 10) : 0;
|
|
32
|
-
})
|
|
33
|
-
.filter((num) => num > 0);
|
|
34
|
-
// Find the next available number (highest + 1)
|
|
35
|
-
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
|
|
36
|
-
return `${nodeTypeId}.${nextNumber}`;
|
|
37
|
-
}
|
|
17
|
+
import { generateNodeId } from "../utils/nodeIds.js";
|
|
38
18
|
/**
|
|
39
19
|
* Workflow Adapter Class
|
|
40
20
|
* Provides a clean API for workflow operations without exposing SvelteFlow internals
|
|
@@ -70,7 +50,7 @@ export class WorkflowAdapter {
|
|
|
70
50
|
throw new Error(`Node type '${nodeType}' not found`);
|
|
71
51
|
}
|
|
72
52
|
// Generate node ID based on node type and existing nodes
|
|
73
|
-
const nodeId =
|
|
53
|
+
const nodeId = generateNodeId(nodeType, workflow.nodes);
|
|
74
54
|
const node = {
|
|
75
55
|
id: nodeId,
|
|
76
56
|
type: nodeType,
|
|
@@ -10,25 +10,71 @@
|
|
|
10
10
|
* 4. Fan out branches vertically from BranchingNode
|
|
11
11
|
*/
|
|
12
12
|
import type { AgentSpecFlow } from "../../types/agentspec.js";
|
|
13
|
+
/** Measured dimensions for a node */
|
|
14
|
+
export interface NodeDimensions {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
13
18
|
/** Layout configuration */
|
|
14
19
|
export interface AutoLayoutConfig {
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
|
|
20
|
+
/** Minimum horizontal gap between the right edge of one layer and the left edge of the next (px) */
|
|
21
|
+
horizontalGap: number;
|
|
22
|
+
/** Minimum vertical gap between the bottom edge of one node and the top edge of the next in the same layer (px) */
|
|
23
|
+
verticalGap: number;
|
|
19
24
|
/** Starting X position */
|
|
20
25
|
startX: number;
|
|
21
26
|
/** Starting Y position */
|
|
22
27
|
startY: number;
|
|
28
|
+
/** Fallback node width when measured dimensions are unavailable */
|
|
29
|
+
defaultNodeWidth: number;
|
|
30
|
+
/** Fallback node height when measured dimensions are unavailable */
|
|
31
|
+
defaultNodeHeight: number;
|
|
23
32
|
}
|
|
24
33
|
/**
|
|
25
34
|
* Compute node positions for an Agent Spec flow using layered layout.
|
|
35
|
+
* Takes actual node dimensions into account to prevent overlap.
|
|
26
36
|
*
|
|
27
37
|
* @param flow - The Agent Spec flow to layout
|
|
28
38
|
* @param config - Optional layout configuration
|
|
39
|
+
* @param nodeDimensions - Optional map of node name to measured {width, height}
|
|
29
40
|
* @returns Map of node name to {x, y} position
|
|
30
41
|
*/
|
|
31
|
-
export declare function computeAutoLayout(flow: AgentSpecFlow, config?: Partial<AutoLayoutConfig>): Map<string, {
|
|
42
|
+
export declare function computeAutoLayout(flow: AgentSpecFlow, config?: Partial<AutoLayoutConfig>, nodeDimensions?: Map<string, NodeDimensions>): Map<string, {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
}>;
|
|
46
|
+
/** Input position for beautify: existing node placement */
|
|
47
|
+
export interface NodePosition {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
}
|
|
51
|
+
/** Beautify configuration */
|
|
52
|
+
export interface BeautifyLayoutConfig {
|
|
53
|
+
/** Minimum horizontal gap between the right edge of one column and the left edge of the next (px) */
|
|
54
|
+
horizontalGap: number;
|
|
55
|
+
/** Minimum vertical gap between the bottom edge of one node and the top edge of the next in the same column (px) */
|
|
56
|
+
verticalGap: number;
|
|
57
|
+
/** Fallback node width when measured dimensions are unavailable */
|
|
58
|
+
defaultNodeWidth: number;
|
|
59
|
+
/** Fallback node height when measured dimensions are unavailable */
|
|
60
|
+
defaultNodeHeight: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Beautify existing node positions: preserve relative column/row ordering
|
|
64
|
+
* but apply uniform spacing based on actual node dimensions.
|
|
65
|
+
*
|
|
66
|
+
* Algorithm:
|
|
67
|
+
* 1. Cluster nodes into columns by X proximity (gap threshold = median width)
|
|
68
|
+
* 2. Sort columns left-to-right by their median X
|
|
69
|
+
* 3. Within each column, sort nodes top-to-bottom by their original Y
|
|
70
|
+
* 4. Re-position with uniform horizontal and vertical gaps
|
|
71
|
+
*
|
|
72
|
+
* @param positions - Current node positions (keyed by node id)
|
|
73
|
+
* @param config - Optional spacing configuration
|
|
74
|
+
* @param nodeDimensions - Optional map of node id to measured {width, height}
|
|
75
|
+
* @returns Map of node id to new {x, y} position
|
|
76
|
+
*/
|
|
77
|
+
export declare function computeBeautifyLayout(positions: Map<string, NodePosition>, config?: Partial<BeautifyLayoutConfig>, nodeDimensions?: Map<string, NodeDimensions>): Map<string, {
|
|
32
78
|
x: number;
|
|
33
79
|
y: number;
|
|
34
80
|
}>;
|
|
@@ -10,36 +10,41 @@
|
|
|
10
10
|
* 4. Fan out branches vertically from BranchingNode
|
|
11
11
|
*/
|
|
12
12
|
const DEFAULT_CONFIG = {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
horizontalGap: 120,
|
|
14
|
+
verticalGap: 40,
|
|
15
15
|
startX: 100,
|
|
16
16
|
startY: 100,
|
|
17
|
+
defaultNodeWidth: 220,
|
|
18
|
+
defaultNodeHeight: 150,
|
|
17
19
|
};
|
|
18
20
|
/**
|
|
19
21
|
* Compute node positions for an Agent Spec flow using layered layout.
|
|
22
|
+
* Takes actual node dimensions into account to prevent overlap.
|
|
20
23
|
*
|
|
21
24
|
* @param flow - The Agent Spec flow to layout
|
|
22
25
|
* @param config - Optional layout configuration
|
|
26
|
+
* @param nodeDimensions - Optional map of node name to measured {width, height}
|
|
23
27
|
* @returns Map of node name to {x, y} position
|
|
24
28
|
*/
|
|
25
|
-
export function computeAutoLayout(flow, config = {}) {
|
|
29
|
+
export function computeAutoLayout(flow, config = {}, nodeDimensions) {
|
|
26
30
|
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
27
31
|
const positions = new Map();
|
|
28
32
|
if (flow.nodes.length === 0)
|
|
29
33
|
return positions;
|
|
34
|
+
const getDims = (name) => nodeDimensions?.get(name) ?? {
|
|
35
|
+
width: cfg.defaultNodeWidth,
|
|
36
|
+
height: cfg.defaultNodeHeight,
|
|
37
|
+
};
|
|
30
38
|
// Build adjacency list from control-flow edges
|
|
31
39
|
const adjacency = new Map();
|
|
32
|
-
const inDegree = new Map();
|
|
33
40
|
for (const node of flow.nodes) {
|
|
34
41
|
adjacency.set(node.name, []);
|
|
35
|
-
inDegree.set(node.name, 0);
|
|
36
42
|
}
|
|
37
43
|
for (const edge of flow.control_flow_connections) {
|
|
38
44
|
const neighbors = adjacency.get(edge.from_node);
|
|
39
45
|
if (neighbors) {
|
|
40
46
|
neighbors.push(edge.to_node);
|
|
41
47
|
}
|
|
42
|
-
inDegree.set(edge.to_node, (inDegree.get(edge.to_node) || 0) + 1);
|
|
43
48
|
}
|
|
44
49
|
// Also consider data-flow edges for connectivity (but don't affect layering priority)
|
|
45
50
|
if (flow.data_flow_connections) {
|
|
@@ -74,21 +79,118 @@ export function computeAutoLayout(flow, config = {}) {
|
|
|
74
79
|
}
|
|
75
80
|
// Sort layers and assign positions
|
|
76
81
|
const sortedLayers = Array.from(layerGroups.keys()).sort((a, b) => a - b);
|
|
82
|
+
// Compute X positions layer by layer, using the widest node in each layer
|
|
83
|
+
const layerXPositions = new Map();
|
|
84
|
+
let currentX = cfg.startX;
|
|
77
85
|
for (const layerIndex of sortedLayers) {
|
|
86
|
+
layerXPositions.set(layerIndex, currentX);
|
|
87
|
+
// Advance X by the widest node in this layer + horizontal gap
|
|
78
88
|
const nodesInLayer = layerGroups.get(layerIndex);
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
const maxWidth = Math.max(...nodesInLayer.map((name) => getDims(name).width));
|
|
90
|
+
currentX += maxWidth + cfg.horizontalGap;
|
|
91
|
+
}
|
|
92
|
+
// Compute Y positions within each layer, using actual node heights
|
|
93
|
+
for (const layerIndex of sortedLayers) {
|
|
94
|
+
const nodesInLayer = layerGroups.get(layerIndex);
|
|
95
|
+
const x = layerXPositions.get(layerIndex);
|
|
96
|
+
// Calculate total height of this column (sum of node heights + gaps)
|
|
97
|
+
const heights = nodesInLayer.map((name) => getDims(name).height);
|
|
98
|
+
const totalHeight = heights.reduce((sum, h) => sum + h, 0) +
|
|
99
|
+
(nodesInLayer.length - 1) * cfg.verticalGap;
|
|
100
|
+
// Center the column vertically around startY
|
|
101
|
+
let y = cfg.startY - totalHeight / 2;
|
|
83
102
|
for (let i = 0; i < nodesInLayer.length; i++) {
|
|
84
|
-
positions.set(nodesInLayer[i], {
|
|
85
|
-
|
|
86
|
-
y: startY + i * cfg.verticalSpacing,
|
|
87
|
-
});
|
|
103
|
+
positions.set(nodesInLayer[i], { x, y });
|
|
104
|
+
y += heights[i] + cfg.verticalGap;
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
107
|
return positions;
|
|
91
108
|
}
|
|
109
|
+
const DEFAULT_BEAUTIFY_CONFIG = {
|
|
110
|
+
horizontalGap: 120,
|
|
111
|
+
verticalGap: 40,
|
|
112
|
+
defaultNodeWidth: 220,
|
|
113
|
+
defaultNodeHeight: 150,
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Beautify existing node positions: preserve relative column/row ordering
|
|
117
|
+
* but apply uniform spacing based on actual node dimensions.
|
|
118
|
+
*
|
|
119
|
+
* Algorithm:
|
|
120
|
+
* 1. Cluster nodes into columns by X proximity (gap threshold = median width)
|
|
121
|
+
* 2. Sort columns left-to-right by their median X
|
|
122
|
+
* 3. Within each column, sort nodes top-to-bottom by their original Y
|
|
123
|
+
* 4. Re-position with uniform horizontal and vertical gaps
|
|
124
|
+
*
|
|
125
|
+
* @param positions - Current node positions (keyed by node id)
|
|
126
|
+
* @param config - Optional spacing configuration
|
|
127
|
+
* @param nodeDimensions - Optional map of node id to measured {width, height}
|
|
128
|
+
* @returns Map of node id to new {x, y} position
|
|
129
|
+
*/
|
|
130
|
+
export function computeBeautifyLayout(positions, config = {}, nodeDimensions) {
|
|
131
|
+
const cfg = { ...DEFAULT_BEAUTIFY_CONFIG, ...config };
|
|
132
|
+
const result = new Map();
|
|
133
|
+
if (positions.size === 0)
|
|
134
|
+
return result;
|
|
135
|
+
const getDims = (id) => nodeDimensions?.get(id) ?? {
|
|
136
|
+
width: cfg.defaultNodeWidth,
|
|
137
|
+
height: cfg.defaultNodeHeight,
|
|
138
|
+
};
|
|
139
|
+
// Collect all nodes sorted by X
|
|
140
|
+
const entries = Array.from(positions.entries()).map(([id, pos]) => ({
|
|
141
|
+
id,
|
|
142
|
+
x: pos.x,
|
|
143
|
+
y: pos.y,
|
|
144
|
+
}));
|
|
145
|
+
entries.sort((a, b) => a.x - b.x);
|
|
146
|
+
// Determine clustering threshold: half the median node width
|
|
147
|
+
const widths = entries.map((e) => getDims(e.id).width);
|
|
148
|
+
const sortedWidths = [...widths].sort((a, b) => a - b);
|
|
149
|
+
const medianWidth = sortedWidths[Math.floor(sortedWidths.length / 2)];
|
|
150
|
+
const clusterThreshold = medianWidth * 0.75;
|
|
151
|
+
// Cluster into columns by X proximity
|
|
152
|
+
const columns = [];
|
|
153
|
+
let currentColumn = [entries[0]];
|
|
154
|
+
for (let i = 1; i < entries.length; i++) {
|
|
155
|
+
const prevX = currentColumn[currentColumn.length - 1].x;
|
|
156
|
+
if (entries[i].x - prevX > clusterThreshold) {
|
|
157
|
+
columns.push(currentColumn);
|
|
158
|
+
currentColumn = [entries[i]];
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
currentColumn.push(entries[i]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
columns.push(currentColumn);
|
|
165
|
+
// Sort each column's nodes top-to-bottom by original Y
|
|
166
|
+
for (const col of columns) {
|
|
167
|
+
col.sort((a, b) => a.y - b.y);
|
|
168
|
+
}
|
|
169
|
+
// Compute the global vertical center from the original positions
|
|
170
|
+
const allYs = entries.map((e) => e.y);
|
|
171
|
+
const globalCenterY = (Math.min(...allYs) + Math.max(...allYs)) / 2;
|
|
172
|
+
// Assign new positions column by column
|
|
173
|
+
let currentX = entries[0].x; // Start from the leftmost original X
|
|
174
|
+
for (const col of columns) {
|
|
175
|
+
// Find the widest node in this column
|
|
176
|
+
const maxWidth = Math.max(...col.map((e) => getDims(e.id).width));
|
|
177
|
+
// Calculate total height of this column
|
|
178
|
+
const heights = col.map((e) => getDims(e.id).height);
|
|
179
|
+
const totalHeight = heights.reduce((sum, h) => sum + h, 0) +
|
|
180
|
+
(col.length - 1) * cfg.verticalGap;
|
|
181
|
+
// Center column vertically around the global center
|
|
182
|
+
let y = globalCenterY - totalHeight / 2;
|
|
183
|
+
for (let i = 0; i < col.length; i++) {
|
|
184
|
+
result.set(col[i].id, { x: currentX, y });
|
|
185
|
+
y += heights[i] + cfg.verticalGap;
|
|
186
|
+
}
|
|
187
|
+
currentX += maxWidth + cfg.horizontalGap;
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Layer Assignment (for auto-layout)
|
|
193
|
+
// ============================================================================
|
|
92
194
|
/**
|
|
93
195
|
* Assign layers using longest path from the start node (modified BFS).
|
|
94
196
|
* This ensures branching nodes fan out properly and convergence points
|
|
@@ -97,28 +199,23 @@ export function computeAutoLayout(flow, config = {}) {
|
|
|
97
199
|
function assignLayers(startNode, adjacency, nodeCount) {
|
|
98
200
|
const layers = new Map();
|
|
99
201
|
layers.set(startNode, 0);
|
|
100
|
-
//
|
|
101
|
-
//
|
|
202
|
+
// Longest-path BFS: re-queue neighbors whenever their layer increases.
|
|
203
|
+
// This ensures convergence nodes (reached via multiple branches) are
|
|
204
|
+
// placed at the depth of the longest path, not the shortest.
|
|
102
205
|
const queue = [startNode];
|
|
103
|
-
const visited = new Set();
|
|
104
206
|
let iterations = 0;
|
|
105
207
|
const maxIterations = nodeCount * nodeCount + 100; // Safety limit for cycles
|
|
106
208
|
while (queue.length > 0 && iterations < maxIterations) {
|
|
107
209
|
iterations++;
|
|
108
210
|
const current = queue.shift();
|
|
109
|
-
if (visited.has(current))
|
|
110
|
-
continue;
|
|
111
|
-
visited.add(current);
|
|
112
211
|
const currentLayer = layers.get(current) || 0;
|
|
113
212
|
const neighbors = adjacency.get(current) || [];
|
|
114
213
|
for (const neighbor of neighbors) {
|
|
115
214
|
const existingLayer = layers.get(neighbor);
|
|
116
215
|
const newLayer = currentLayer + 1;
|
|
117
|
-
//
|
|
216
|
+
// Only update and re-queue when we find a longer path
|
|
118
217
|
if (existingLayer === undefined || newLayer > existingLayer) {
|
|
119
218
|
layers.set(neighbor, newLayer);
|
|
120
|
-
}
|
|
121
|
-
if (!visited.has(neighbor)) {
|
|
122
219
|
queue.push(neighbor);
|
|
123
220
|
}
|
|
124
221
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Classifier for LLM Chat Interface
|
|
3
|
+
*
|
|
4
|
+
* Determines whether a DSL command is read-only or mutating,
|
|
5
|
+
* so the UI knows which commands can auto-execute and which
|
|
6
|
+
* need user approval.
|
|
7
|
+
*
|
|
8
|
+
* @module chat/commandClassifier
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Determine whether a DSL command type is mutating (modifies workflow state).
|
|
12
|
+
*
|
|
13
|
+
* Read-only commands (list_nodes, list_edges, list_types, info, get_config, help)
|
|
14
|
+
* return false. All other commands are considered mutating and return true.
|
|
15
|
+
*
|
|
16
|
+
* @param commandType - The command type string (e.g., "add", "list_nodes")
|
|
17
|
+
* @returns true if the command modifies workflow state, false if read-only
|
|
18
|
+
*/
|
|
19
|
+
export declare function isMutatingCommand(commandType: string): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Classifier for LLM Chat Interface
|
|
3
|
+
*
|
|
4
|
+
* Determines whether a DSL command is read-only or mutating,
|
|
5
|
+
* so the UI knows which commands can auto-execute and which
|
|
6
|
+
* need user approval.
|
|
7
|
+
*
|
|
8
|
+
* @module chat/commandClassifier
|
|
9
|
+
*/
|
|
10
|
+
/** Commands that only read workflow state without modifying it */
|
|
11
|
+
const READ_ONLY_COMMANDS = new Set([
|
|
12
|
+
"list_nodes",
|
|
13
|
+
"list_edges",
|
|
14
|
+
"list_types",
|
|
15
|
+
"info",
|
|
16
|
+
"get_config",
|
|
17
|
+
"help",
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Determine whether a DSL command type is mutating (modifies workflow state).
|
|
21
|
+
*
|
|
22
|
+
* Read-only commands (list_nodes, list_edges, list_types, info, get_config, help)
|
|
23
|
+
* return false. All other commands are considered mutating and return true.
|
|
24
|
+
*
|
|
25
|
+
* @param commandType - The command type string (e.g., "add", "list_nodes")
|
|
26
|
+
* @returns true if the command modifies workflow state, false if read-only
|
|
27
|
+
*/
|
|
28
|
+
export function isMutatingCommand(commandType) {
|
|
29
|
+
return !READ_ONLY_COMMANDS.has(commandType);
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowDrop Chat Module
|
|
3
|
+
*
|
|
4
|
+
* Provides the LLM Chat Interface for natural language workflow building.
|
|
5
|
+
* Includes components for chat UI and command preview, utilities for
|
|
6
|
+
* parsing LLM responses and classifying commands, and all chat types.
|
|
7
|
+
*
|
|
8
|
+
* @module chat
|
|
9
|
+
*
|
|
10
|
+
* @example In Svelte:
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <script>
|
|
13
|
+
* import { AIChatPanel } from "@flowdrop/flowdrop/chat";
|
|
14
|
+
* </script>
|
|
15
|
+
*
|
|
16
|
+
* <AIChatPanel
|
|
17
|
+
* nodeTypes={nodeTypes}
|
|
18
|
+
* workflowId="wf-123"
|
|
19
|
+
* onUIAction={handleUIAction}
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export { default as AIChatPanel } from "../components/chat/AIChatPanel.svelte";
|
|
24
|
+
export { default as CommandPreview } from "../components/chat/CommandPreview.svelte";
|
|
25
|
+
export { extractCommands } from "./responseParser.js";
|
|
26
|
+
export { isMutatingCommand } from "./commandClassifier.js";
|
|
27
|
+
export type { ChatMessageRole, ChatHistoryMessage, ChatRequest, ChatResponse, ExtractedCommands, CommandExecutionStatus, CommandPreviewItem, } from "../types/chat.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowDrop Chat Module
|
|
3
|
+
*
|
|
4
|
+
* Provides the LLM Chat Interface for natural language workflow building.
|
|
5
|
+
* Includes components for chat UI and command preview, utilities for
|
|
6
|
+
* parsing LLM responses and classifying commands, and all chat types.
|
|
7
|
+
*
|
|
8
|
+
* @module chat
|
|
9
|
+
*
|
|
10
|
+
* @example In Svelte:
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <script>
|
|
13
|
+
* import { AIChatPanel } from "@flowdrop/flowdrop/chat";
|
|
14
|
+
* </script>
|
|
15
|
+
*
|
|
16
|
+
* <AIChatPanel
|
|
17
|
+
* nodeTypes={nodeTypes}
|
|
18
|
+
* workflowId="wf-123"
|
|
19
|
+
* onUIAction={handleUIAction}
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Chat Components
|
|
25
|
+
// ============================================================================
|
|
26
|
+
export { default as AIChatPanel } from "../components/chat/AIChatPanel.svelte";
|
|
27
|
+
export { default as CommandPreview } from "../components/chat/CommandPreview.svelte";
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Chat Utilities
|
|
30
|
+
// ============================================================================
|
|
31
|
+
export { extractCommands } from "./responseParser.js";
|
|
32
|
+
export { isMutatingCommand } from "./commandClassifier.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Parser for LLM Chat Interface
|
|
3
|
+
*
|
|
4
|
+
* Extracts DSL commands from LLM markdown responses by parsing
|
|
5
|
+
* fenced code blocks (```flowdrop or bare ```).
|
|
6
|
+
*
|
|
7
|
+
* @module chat/responseParser
|
|
8
|
+
*/
|
|
9
|
+
import type { ExtractedCommands } from "../types/chat.js";
|
|
10
|
+
/**
|
|
11
|
+
* Extract DSL commands from an LLM response string.
|
|
12
|
+
*
|
|
13
|
+
* Parses fenced code blocks labeled `flowdrop` (preferred) or bare
|
|
14
|
+
* fenced code blocks (fallback). Text outside code blocks becomes
|
|
15
|
+
* the explanation. Empty lines and comment lines inside code blocks
|
|
16
|
+
* are skipped.
|
|
17
|
+
*
|
|
18
|
+
* @param llmResponse - The raw LLM response text (may contain markdown)
|
|
19
|
+
* @returns Extracted commands and explanation text
|
|
20
|
+
*/
|
|
21
|
+
export declare function extractCommands(llmResponse: string): ExtractedCommands;
|