@flowdrop/flowdrop 1.3.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.
Files changed (114) hide show
  1. package/README.md +68 -24
  2. package/dist/adapters/WorkflowAdapter.js +2 -22
  3. package/dist/adapters/agentspec/autoLayout.d.ts +51 -5
  4. package/dist/adapters/agentspec/autoLayout.js +120 -23
  5. package/dist/chat/commandClassifier.d.ts +19 -0
  6. package/dist/chat/commandClassifier.js +30 -0
  7. package/dist/chat/index.d.ts +27 -0
  8. package/dist/chat/index.js +32 -0
  9. package/dist/chat/responseParser.d.ts +21 -0
  10. package/dist/chat/responseParser.js +87 -0
  11. package/dist/commands/batch.d.ts +18 -0
  12. package/dist/commands/batch.js +56 -0
  13. package/dist/commands/executor.d.ts +37 -0
  14. package/dist/commands/executor.js +1044 -0
  15. package/dist/commands/index.d.ts +14 -0
  16. package/dist/commands/index.js +17 -0
  17. package/dist/commands/parser.d.ts +16 -0
  18. package/dist/commands/parser.js +278 -0
  19. package/dist/commands/positioner.d.ts +19 -0
  20. package/dist/commands/positioner.js +33 -0
  21. package/dist/commands/storeIntegration.svelte.d.ts +16 -0
  22. package/dist/commands/storeIntegration.svelte.js +67 -0
  23. package/dist/commands/types.d.ts +343 -0
  24. package/dist/commands/types.js +45 -0
  25. package/dist/components/App.svelte +431 -17
  26. package/dist/components/App.svelte.d.ts +10 -0
  27. package/dist/components/CanvasBanner.stories.svelte +6 -2
  28. package/dist/components/CanvasController.svelte +38 -0
  29. package/dist/components/CanvasController.svelte.d.ts +32 -0
  30. package/dist/components/ConfigMappingRow.svelte +130 -0
  31. package/dist/components/ConfigMappingRow.svelte.d.ts +8 -0
  32. package/dist/components/ConfigPanel.svelte +56 -7
  33. package/dist/components/ConfigPanel.svelte.d.ts +2 -0
  34. package/dist/components/FlowDropEdge.svelte +8 -57
  35. package/dist/components/Logo.svelte +14 -14
  36. package/dist/components/LogsSidebar.svelte +5 -5
  37. package/dist/components/Navbar.svelte +58 -10
  38. package/dist/components/Navbar.svelte.d.ts +7 -0
  39. package/dist/components/NodeSidebar.svelte +238 -362
  40. package/dist/components/NodeSwapPicker.svelte +537 -0
  41. package/dist/components/NodeSwapPicker.svelte.d.ts +16 -0
  42. package/dist/components/PortMappingRow.svelte +209 -0
  43. package/dist/components/PortMappingRow.svelte.d.ts +12 -0
  44. package/dist/components/SwapMappingEditor.svelte +550 -0
  45. package/dist/components/SwapMappingEditor.svelte.d.ts +12 -0
  46. package/dist/components/WorkflowEditor.svelte +99 -4
  47. package/dist/components/WorkflowEditor.svelte.d.ts +8 -0
  48. package/dist/components/chat/AIChatPanel.svelte +658 -0
  49. package/dist/components/chat/AIChatPanel.svelte.d.ts +13 -0
  50. package/dist/components/chat/CommandPreview.svelte +184 -0
  51. package/dist/components/chat/CommandPreview.svelte.d.ts +9 -0
  52. package/dist/components/console/CommandConsole.stories.svelte +93 -0
  53. package/dist/components/console/CommandConsole.stories.svelte.d.ts +27 -0
  54. package/dist/components/console/CommandConsole.svelte +259 -0
  55. package/dist/components/console/CommandConsole.svelte.d.ts +11 -0
  56. package/dist/components/console/ConsoleAutocomplete.svelte +139 -0
  57. package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +21 -0
  58. package/dist/components/console/ConsoleInput.svelte +712 -0
  59. package/dist/components/console/ConsoleInput.svelte.d.ts +16 -0
  60. package/dist/components/console/ConsoleOutput.svelte +121 -0
  61. package/dist/components/console/ConsoleOutput.svelte.d.ts +11 -0
  62. package/dist/components/console/formatters.d.ts +26 -0
  63. package/dist/components/console/formatters.js +118 -0
  64. package/dist/components/interrupt/index.d.ts +1 -0
  65. package/dist/components/interrupt/index.js +1 -0
  66. package/dist/components/nodes/SimpleNode.stories.svelte +64 -0
  67. package/dist/components/nodes/SimpleNode.svelte +27 -11
  68. package/dist/components/nodes/SquareNode.stories.svelte +45 -0
  69. package/dist/components/nodes/SquareNode.svelte +27 -11
  70. package/dist/components/nodes/WorkflowNode.stories.svelte +63 -0
  71. package/dist/config/endpoints.d.ts +8 -0
  72. package/dist/config/endpoints.js +5 -0
  73. package/dist/core/index.d.ts +5 -0
  74. package/dist/core/index.js +9 -0
  75. package/dist/editor/index.d.ts +3 -1
  76. package/dist/editor/index.js +4 -2
  77. package/dist/helpers/proximityConnect.js +8 -1
  78. package/dist/helpers/workflowEditorHelper.d.ts +3 -53
  79. package/dist/helpers/workflowEditorHelper.js +13 -228
  80. package/dist/playground/index.d.ts +1 -1
  81. package/dist/playground/index.js +1 -1
  82. package/dist/schemas/v1/workflow.schema.json +107 -22
  83. package/dist/services/chatService.d.ts +65 -0
  84. package/dist/services/chatService.js +131 -0
  85. package/dist/services/historyService.d.ts +6 -4
  86. package/dist/services/historyService.js +21 -6
  87. package/dist/skins/slate.js +16 -0
  88. package/dist/stores/interruptStore.svelte.js +6 -1
  89. package/dist/stores/playgroundStore.svelte.d.ts +1 -1
  90. package/dist/stores/playgroundStore.svelte.js +11 -2
  91. package/dist/stores/portCoordinateStore.svelte.d.ts +4 -0
  92. package/dist/stores/portCoordinateStore.svelte.js +20 -26
  93. package/dist/stores/workflowStore.svelte.d.ts +31 -2
  94. package/dist/stores/workflowStore.svelte.js +84 -64
  95. package/dist/stories/EdgeDecorator.svelte +4 -4
  96. package/dist/styles/base.css +48 -0
  97. package/dist/svelte-app.d.ts +7 -1
  98. package/dist/svelte-app.js +4 -1
  99. package/dist/types/chat.d.ts +63 -0
  100. package/dist/types/chat.js +9 -0
  101. package/dist/types/events.d.ts +28 -2
  102. package/dist/types/events.js +1 -0
  103. package/dist/types/index.d.ts +8 -0
  104. package/dist/types/settings.d.ts +6 -0
  105. package/dist/types/settings.js +3 -0
  106. package/dist/utils/edgeStyling.d.ts +42 -0
  107. package/dist/utils/edgeStyling.js +176 -0
  108. package/dist/utils/nodeIds.d.ts +31 -0
  109. package/dist/utils/nodeIds.js +42 -0
  110. package/dist/utils/nodeSwap.d.ts +221 -0
  111. package/dist/utils/nodeSwap.js +686 -0
  112. package/package.json +6 -1
  113. package/dist/helpers/nodeLayoutHelper.d.ts +0 -14
  114. 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</h1>
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 pages build status" />
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/dt/@flowdrop/flowdrop.svg?maxAge=2592000&style=flat-square" alt="npm downloads" /></a>
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="#quickstart">Quickstart</a> •
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="#documentation">Docs</a>
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
- | 🎨 **Visual Editor Only** | Pure UI component. No hidden backend, no external dependencies |
74
- | 🔐 **You Own Everything** | Your data, your servers, your orchestration logic, your security policies |
75
- | 🔌 **Backend Agnostic** | Connect to any API: Drupal, Laravel, Express, FastAPI, or your own |
76
- | 🧩 **7 Built-in Node Types** | From simple icons to complex gateway logic |
77
- | 🎭 **Framework Flexible** | Use as Svelte component or mount into React, Vue, Angular, or vanilla JS |
78
- | 🐳 **Deploy Anywhere** | Runtime config means build once, deploy everywhere |
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 7 beautifully designed node types:
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
- Make it yours with CSS custom properties:
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
- --flowdrop-background-color: #0a0a0a;
199
- --flowdrop-primary-color: #6366f1;
200
- --flowdrop-border-color: #27272a;
201
- --flowdrop-text-color: #fafafa;
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
- | [API Documentation](https://flowdrop-io.github.io/flowdrop/) | REST API specification |
229
- | [Docker Guide](../../apps/example-client-docker/README.md) | Docker deployment guide |
230
- | [QUICK_START.md](./QUICK_START.md) | Get running in 5 minutes |
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 = generateStandardNodeId(nodeType, workflow.nodes);
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
- /** Horizontal spacing between layers (px) */
16
- horizontalSpacing: number;
17
- /** Vertical spacing between nodes in the same layer (px) */
18
- verticalSpacing: number;
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
- horizontalSpacing: 300,
14
- verticalSpacing: 150,
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 x = cfg.startX + layerIndex * cfg.horizontalSpacing;
80
- // Center nodes vertically
81
- const totalHeight = (nodesInLayer.length - 1) * cfg.verticalSpacing;
82
- const startY = cfg.startY - totalHeight / 2;
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
- x,
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
- // Use BFS but take the maximum layer for each node
101
- // (longest path ensures proper branching layout)
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
- // Take the max layer (longest path)
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;