@d34dman/flowdrop 0.0.60 → 0.0.62

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 (207) hide show
  1. package/README.md +6 -0
  2. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  3. package/dist/adapters/agentspec/AgentSpecAdapter.js +3 -1
  4. package/dist/api/client.d.ts +4 -0
  5. package/dist/api/client.js +6 -1
  6. package/dist/api/enhanced-client.js +7 -6
  7. package/dist/components/App.svelte +143 -219
  8. package/dist/components/CanvasBanner.stories.svelte +25 -0
  9. package/dist/components/CanvasBanner.stories.svelte.d.ts +27 -0
  10. package/dist/components/CanvasBanner.svelte +2 -2
  11. package/dist/components/ConfigForm.svelte +37 -36
  12. package/dist/components/ConfigPanel.stories.svelte +38 -0
  13. package/dist/components/ConfigPanel.stories.svelte.d.ts +27 -0
  14. package/dist/components/ConfigPanel.svelte +2 -2
  15. package/dist/components/ConnectionLine.svelte +2 -2
  16. package/dist/components/FlowDropZone.svelte +18 -2
  17. package/dist/components/FlowDropZone.svelte.d.ts +2 -0
  18. package/dist/components/LoadingSpinner.stories.svelte +30 -0
  19. package/dist/components/LoadingSpinner.stories.svelte.d.ts +27 -0
  20. package/dist/components/Logo.stories.svelte +22 -0
  21. package/dist/components/Logo.stories.svelte.d.ts +27 -0
  22. package/dist/components/Logo.svelte +33 -13
  23. package/dist/components/Logo.svelte.d.ts +1 -1
  24. package/dist/components/MarkdownDisplay.stories.svelte +21 -0
  25. package/dist/components/MarkdownDisplay.stories.svelte.d.ts +27 -0
  26. package/dist/components/MarkdownDisplay.svelte +4 -3
  27. package/dist/components/Navbar.stories.svelte +41 -0
  28. package/dist/components/Navbar.stories.svelte.d.ts +27 -0
  29. package/dist/components/Navbar.svelte +4 -4
  30. package/dist/components/NodeSidebar.svelte +12 -12
  31. package/dist/components/NodeStatusOverlay.stories.svelte +74 -0
  32. package/dist/components/NodeStatusOverlay.stories.svelte.d.ts +27 -0
  33. package/dist/components/PipelineStatus.svelte +11 -4
  34. package/dist/components/PortCoordinateTracker.svelte +1 -1
  35. package/dist/components/SchemaForm.stories.svelte +101 -0
  36. package/dist/components/SchemaForm.stories.svelte.d.ts +27 -0
  37. package/dist/components/SchemaForm.svelte +17 -12
  38. package/dist/components/SettingsModal.svelte +3 -3
  39. package/dist/components/SettingsPanel.svelte +23 -22
  40. package/dist/components/StatusIcon.stories.svelte +60 -0
  41. package/dist/components/StatusIcon.stories.svelte.d.ts +27 -0
  42. package/dist/components/StatusIcon.svelte +7 -0
  43. package/dist/components/StatusLabel.stories.svelte +17 -0
  44. package/dist/components/StatusLabel.stories.svelte.d.ts +27 -0
  45. package/dist/components/ThemeToggle.stories.svelte +25 -0
  46. package/dist/components/ThemeToggle.stories.svelte.d.ts +27 -0
  47. package/dist/components/ThemeToggle.svelte +8 -8
  48. package/dist/components/UniversalNode.svelte +1 -1
  49. package/dist/components/WorkflowEditor.svelte +298 -294
  50. package/dist/components/form/FormAutocomplete.svelte +20 -19
  51. package/dist/components/form/FormCheckboxGroup.stories.svelte +28 -0
  52. package/dist/components/form/FormCheckboxGroup.stories.svelte.d.ts +27 -0
  53. package/dist/components/form/FormField.svelte +3 -3
  54. package/dist/components/form/FormFieldLight.svelte +2 -2
  55. package/dist/components/form/FormFieldWrapper.stories.svelte +31 -0
  56. package/dist/components/form/FormFieldWrapper.stories.svelte.d.ts +27 -0
  57. package/dist/components/form/FormFieldset.svelte +7 -7
  58. package/dist/components/form/FormNumberField.stories.svelte +33 -0
  59. package/dist/components/form/FormNumberField.stories.svelte.d.ts +27 -0
  60. package/dist/components/form/FormRangeField.stories.svelte +31 -0
  61. package/dist/components/form/FormRangeField.stories.svelte.d.ts +27 -0
  62. package/dist/components/form/FormSelect.stories.svelte +50 -0
  63. package/dist/components/form/FormSelect.stories.svelte.d.ts +27 -0
  64. package/dist/components/form/FormTemplateEditor.svelte +2 -1
  65. package/dist/components/form/FormTextField.stories.svelte +30 -0
  66. package/dist/components/form/FormTextField.stories.svelte.d.ts +27 -0
  67. package/dist/components/form/FormTextarea.stories.svelte +31 -0
  68. package/dist/components/form/FormTextarea.stories.svelte.d.ts +27 -0
  69. package/dist/components/form/FormToggle.stories.svelte +30 -0
  70. package/dist/components/form/FormToggle.stories.svelte.d.ts +27 -0
  71. package/dist/components/form/FormUISchemaRenderer.svelte +1 -1
  72. package/dist/components/form/types.d.ts +15 -47
  73. package/dist/components/interrupt/ChoicePrompt.stories.svelte +43 -0
  74. package/dist/components/interrupt/ChoicePrompt.stories.svelte.d.ts +27 -0
  75. package/dist/components/interrupt/ChoicePrompt.svelte +24 -24
  76. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte +49 -0
  77. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte.d.ts +27 -0
  78. package/dist/components/interrupt/ConfirmationPrompt.svelte +19 -19
  79. package/dist/components/interrupt/FormPrompt.svelte +15 -15
  80. package/dist/components/interrupt/InterruptBubble.svelte +202 -236
  81. package/dist/components/interrupt/InterruptBubble.svelte.d.ts +1 -1
  82. package/dist/components/interrupt/ReviewPrompt.stories.svelte +46 -0
  83. package/dist/components/interrupt/ReviewPrompt.stories.svelte.d.ts +27 -0
  84. package/dist/components/interrupt/ReviewPrompt.svelte +842 -0
  85. package/dist/components/interrupt/ReviewPrompt.svelte.d.ts +23 -0
  86. package/dist/components/interrupt/TextInputPrompt.stories.svelte +34 -0
  87. package/dist/components/interrupt/TextInputPrompt.stories.svelte.d.ts +27 -0
  88. package/dist/components/interrupt/TextInputPrompt.svelte +21 -21
  89. package/dist/components/nodes/GatewayNode.stories.svelte +76 -0
  90. package/dist/components/nodes/GatewayNode.stories.svelte.d.ts +26 -0
  91. package/dist/components/nodes/GatewayNode.svelte +19 -17
  92. package/dist/components/nodes/IdeaNode.stories.svelte +48 -0
  93. package/dist/components/nodes/IdeaNode.stories.svelte.d.ts +26 -0
  94. package/dist/components/nodes/IdeaNode.svelte +10 -26
  95. package/dist/components/nodes/NotesNode.stories.svelte +69 -0
  96. package/dist/components/nodes/NotesNode.stories.svelte.d.ts +26 -0
  97. package/dist/components/nodes/NotesNode.svelte +8 -8
  98. package/dist/components/nodes/SimpleNode.stories.svelte +101 -0
  99. package/dist/components/nodes/SimpleNode.stories.svelte.d.ts +26 -0
  100. package/dist/components/nodes/SimpleNode.svelte +16 -24
  101. package/dist/components/nodes/SquareNode.stories.svelte +56 -0
  102. package/dist/components/nodes/SquareNode.stories.svelte.d.ts +26 -0
  103. package/dist/components/nodes/SquareNode.svelte +13 -21
  104. package/dist/components/nodes/TerminalNode.stories.svelte +25 -0
  105. package/dist/components/nodes/TerminalNode.stories.svelte.d.ts +26 -0
  106. package/dist/components/nodes/TerminalNode.svelte +6 -6
  107. package/dist/components/nodes/ToolNode.stories.svelte +71 -0
  108. package/dist/components/nodes/ToolNode.stories.svelte.d.ts +26 -0
  109. package/dist/components/nodes/ToolNode.svelte +7 -15
  110. package/dist/components/nodes/WorkflowNode.stories.svelte +50 -0
  111. package/dist/components/nodes/WorkflowNode.stories.svelte.d.ts +26 -0
  112. package/dist/components/nodes/WorkflowNode.svelte +13 -13
  113. package/dist/components/playground/ChatPanel.svelte +48 -48
  114. package/dist/components/playground/ExecutionLogs.svelte +23 -23
  115. package/dist/components/playground/InputCollector.svelte +24 -24
  116. package/dist/components/playground/MessageBubble.stories.svelte +49 -0
  117. package/dist/components/playground/MessageBubble.stories.svelte.d.ts +27 -0
  118. package/dist/components/playground/MessageBubble.svelte +49 -46
  119. package/dist/components/playground/Playground.svelte +203 -172
  120. package/dist/components/playground/PlaygroundModal.svelte +5 -5
  121. package/dist/components/playground/SessionManager.svelte +26 -26
  122. package/dist/config/constants.d.ts +22 -0
  123. package/dist/config/constants.js +22 -0
  124. package/dist/config/endpoints.d.ts +19 -0
  125. package/dist/config/runtimeConfig.js +2 -1
  126. package/dist/core/index.d.ts +5 -2
  127. package/dist/core/index.js +9 -1
  128. package/dist/editor/index.d.ts +13 -9
  129. package/dist/editor/index.js +15 -11
  130. package/dist/form/code.d.ts +2 -1
  131. package/dist/form/code.js +1 -3
  132. package/dist/form/markdown.d.ts +2 -1
  133. package/dist/form/markdown.js +1 -3
  134. package/dist/helpers/workflowEditorHelper.js +13 -9
  135. package/dist/mocks/app-forms.js +1 -0
  136. package/dist/mocks/app-navigation.js +3 -1
  137. package/dist/mocks/app-stores.d.ts +4 -4
  138. package/dist/playground/index.d.ts +5 -4
  139. package/dist/playground/index.js +15 -11
  140. package/dist/playground/mount.d.ts +20 -1
  141. package/dist/playground/mount.js +24 -6
  142. package/dist/services/agentSpecExecutionService.js +2 -1
  143. package/dist/services/api.js +10 -18
  144. package/dist/services/apiVariableService.js +2 -1
  145. package/dist/services/autoSaveService.d.ts +3 -3
  146. package/dist/services/autoSaveService.js +21 -17
  147. package/dist/services/categoriesApi.js +13 -5
  148. package/dist/services/draftStorage.js +5 -4
  149. package/dist/services/dynamicSchemaService.js +4 -4
  150. package/dist/services/globalSave.d.ts +60 -11
  151. package/dist/services/globalSave.js +160 -83
  152. package/dist/services/historyService.d.ts +2 -1
  153. package/dist/services/historyService.js +7 -3
  154. package/dist/services/interruptService.js +9 -8
  155. package/dist/services/nodeExecutionService.js +14 -6
  156. package/dist/services/playgroundService.d.ts +3 -2
  157. package/dist/services/playgroundService.js +8 -7
  158. package/dist/services/portConfigApi.js +11 -7
  159. package/dist/services/toastService.d.ts +1 -1
  160. package/dist/services/toastService.js +6 -5
  161. package/dist/services/variableService.js +3 -2
  162. package/dist/settings/index.d.ts +1 -1
  163. package/dist/settings/index.js +1 -1
  164. package/dist/stores/{categoriesStore.d.ts → categoriesStore.svelte.d.ts} +3 -3
  165. package/dist/stores/{categoriesStore.js → categoriesStore.svelte.js} +15 -18
  166. package/dist/stores/editorStateMachine.svelte.d.ts +42 -0
  167. package/dist/stores/editorStateMachine.svelte.js +132 -0
  168. package/dist/stores/{historyStore.d.ts → historyStore.svelte.d.ts} +18 -15
  169. package/dist/stores/{historyStore.js → historyStore.svelte.js} +40 -21
  170. package/dist/stores/{interruptStore.d.ts → interruptStore.svelte.d.ts} +16 -15
  171. package/dist/stores/{interruptStore.js → interruptStore.svelte.js} +85 -94
  172. package/dist/stores/{playgroundStore.d.ts → playgroundStore.svelte.d.ts} +52 -34
  173. package/dist/stores/{playgroundStore.js → playgroundStore.svelte.js} +193 -100
  174. package/dist/stores/{portCoordinateStore.d.ts → portCoordinateStore.svelte.d.ts} +10 -4
  175. package/dist/stores/{portCoordinateStore.js → portCoordinateStore.svelte.js} +38 -35
  176. package/dist/stores/{settingsStore.d.ts → settingsStore.svelte.d.ts} +45 -28
  177. package/dist/stores/{settingsStore.js → settingsStore.svelte.js} +169 -128
  178. package/dist/stores/{workflowStore.d.ts → workflowStore.svelte.d.ts} +101 -65
  179. package/dist/stores/{workflowStore.js → workflowStore.svelte.js} +285 -239
  180. package/dist/stories/CanvasDecorator.svelte +50 -0
  181. package/dist/stories/CanvasDecorator.svelte.d.ts +8 -0
  182. package/dist/stories/NodeDecorator.svelte +74 -0
  183. package/dist/stories/NodeDecorator.svelte.d.ts +8 -0
  184. package/dist/stories/utils.d.ts +93 -0
  185. package/dist/stories/utils.js +122 -0
  186. package/dist/styles/base.css +114 -61
  187. package/dist/styles/toast.css +2 -2
  188. package/dist/styles/tokens.css +250 -185
  189. package/dist/svelte-app.d.ts +0 -6
  190. package/dist/svelte-app.js +13 -31
  191. package/dist/types/index.d.ts +2 -0
  192. package/dist/types/interrupt.d.ts +89 -5
  193. package/dist/types/interrupt.js +13 -1
  194. package/dist/types/playground.d.ts +42 -1
  195. package/dist/types/playground.js +38 -0
  196. package/dist/types/settings.js +1 -1
  197. package/dist/utils/colors.js +4 -4
  198. package/dist/utils/connections.js +33 -8
  199. package/dist/utils/icons.js +1 -1
  200. package/dist/utils/logger.d.ts +47 -0
  201. package/dist/utils/logger.js +72 -0
  202. package/dist/utils/nodeWrapper.js +1 -1
  203. package/dist/utils/sanitize.d.ts +19 -0
  204. package/dist/utils/sanitize.js +31 -0
  205. package/dist/utils/validation.d.ts +29 -0
  206. package/dist/utils/validation.js +39 -0
  207. package/package.json +243 -232
@@ -5,7 +5,7 @@
5
5
  -->
6
6
 
7
7
  <script lang="ts">
8
- import { onMount, tick } from 'svelte';
8
+ import { onMount } from 'svelte';
9
9
  import MainLayout from './layouts/MainLayout.svelte';
10
10
  import WorkflowEditor from './WorkflowEditor.svelte';
11
11
  import NodeSidebar from './NodeSidebar.svelte';
@@ -28,18 +28,21 @@
28
28
  import type { FlowDropEventHandlers, FlowDropFeatures } from '../types/events.js';
29
29
  import { mergeFeatures } from '../types/events.js';
30
30
  import {
31
- workflowStore,
31
+ getWorkflowStore,
32
32
  workflowActions,
33
- workflowName,
34
- workflowFormat,
33
+ getWorkflowName,
34
+ getWorkflowFormat,
35
35
  markAsSaved
36
- } from '../stores/workflowStore.js';
36
+ } from '../stores/workflowStore.svelte.js';
37
+ import { globalSaveWorkflow, globalExportWorkflow } from '../services/globalSave.js';
37
38
  import { apiToasts, dismissToast } from '../services/toastService.js';
38
39
  import { initAutoSave } from '../services/autoSaveService.js';
39
- import { uiSettings } from '../stores/settingsStore.js';
40
+ import { getUiSettings } from '../stores/settingsStore.svelte.js';
40
41
  import { initializePortCompatibility } from '../utils/connections.js';
41
42
  import { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
42
43
  import { workflowFormatRegistry } from '../registry/workflowFormatRegistry.js';
44
+ import { logger } from '../utils/logger.js';
45
+ import { validateWorkflowData } from '../utils/validation.js';
43
46
 
44
47
  /**
45
48
  * Configuration props for runtime customization
@@ -120,10 +123,11 @@
120
123
  return navbarTitle;
121
124
  }
122
125
  // Default workflow title logic
123
- if (!$workflowName || $workflowName === 'Untitled Workflow') {
126
+ const wfName = getWorkflowName();
127
+ if (!wfName || wfName === 'Untitled Workflow') {
124
128
  return 'Workflow / New Workflow';
125
129
  }
126
- return `Workflow / ${$workflowName}`;
130
+ return `Workflow / ${wfName}`;
127
131
  });
128
132
 
129
133
  let nodes = $state<NodeMetadata[]>([]);
@@ -175,15 +179,16 @@
175
179
 
176
180
  // Workflow configuration values
177
181
  let workflowConfigValues = $derived({
178
- name: $workflowName || '',
179
- description: $workflowStore?.description || '',
180
- format: $workflowStore?.metadata?.format || 'flowdrop'
182
+ name: getWorkflowName() || '',
183
+ description: getWorkflowStore()?.description || '',
184
+ format: getWorkflowStore()?.metadata?.format || 'flowdrop'
181
185
  });
182
186
 
183
187
  // Get the current node from the workflow store
184
188
  let selectedNodeForConfig = $derived(() => {
185
- if (!selectedNodeId || !$workflowStore) return null;
186
- return $workflowStore.nodes.find((node) => node.id === selectedNodeId) || null;
189
+ const wf = getWorkflowStore();
190
+ if (!selectedNodeId || !wf) return null;
191
+ return wf.nodes.find((node) => node.id === selectedNodeId) || null;
187
192
  });
188
193
 
189
194
  // WorkflowEditor reference for save functionality
@@ -380,11 +385,13 @@
380
385
  /**
381
386
  * Handle workflow configuration save
382
387
  */
383
- async function handleWorkflowSave(config: any): Promise<void> {
388
+ async function handleWorkflowSave(config: Record<string, unknown>): Promise<void> {
384
389
  // Update the workflow store
385
- if ($workflowStore) {
386
- $workflowStore.name = config.name;
387
- $workflowStore.description = config.description;
390
+ if (getWorkflowStore()) {
391
+ workflowActions.batchUpdate({
392
+ name: config.name as string | undefined,
393
+ description: config.description as string | undefined
394
+ });
388
395
  }
389
396
 
390
397
  // Close the sidebar
@@ -394,209 +401,101 @@
394
401
  try {
395
402
  await saveWorkflow();
396
403
  } catch (error) {
397
- console.error('Failed to save workflow to backend:', error);
404
+ logger.error('Failed to save workflow to backend:', error);
398
405
  // Note: We don't throw the error here to avoid breaking the UI flow
399
406
  // The user can still manually save via the main Save button if needed
400
407
  }
401
408
  }
402
409
 
403
410
  /**
404
- * Save workflow - exposed API function
411
+ * Save workflow - thin wrapper that delegates to globalSaveWorkflow().
405
412
  *
406
- * Integrates with event handlers for enterprise customization.
407
- * Uses enhanced API client with authProvider support when available.
413
+ * All save logic (blur flush, metadata construction, API call, event hooks,
414
+ * toast notifications) lives in globalSave.ts the single source of truth.
408
415
  */
409
416
  async function saveWorkflow(): Promise<void> {
410
- // Flush any pending form changes by blurring the active element.
411
- // This ensures focusout handlers (like ConfigForm's handleFormBlur)
412
- // sync local state to the global store before we read it.
413
- if (document.activeElement instanceof HTMLElement) {
414
- document.activeElement.blur();
415
- }
416
-
417
- // Wait for any pending DOM updates before saving
418
- await tick();
419
-
420
- // Use current workflow from global store
421
- const workflowToSave = $workflowStore;
422
-
423
- if (!workflowToSave) {
424
- return;
425
- }
426
-
427
- // Call onBeforeSave if provided - allows cancellation
428
- if (eventHandlers?.onBeforeSave) {
429
- const shouldContinue = await eventHandlers.onBeforeSave(workflowToSave);
430
- if (shouldContinue === false) {
431
- // Save cancelled by event handler
432
- return;
433
- }
434
- }
435
-
436
- // Show loading toast (if enabled)
437
- const loadingToast = features.showToasts ? apiToasts.loading('Saving workflow') : null;
438
-
439
- try {
440
- // Import uuid for new workflow ID generation
441
- const { v4: uuidv4 } = await import('uuid');
442
-
443
- // Determine the workflow ID
444
- let workflowId: string;
445
- if (workflowToSave.id) {
446
- workflowId = workflowToSave.id;
447
- } else {
448
- workflowId = uuidv4();
449
- }
417
+ await globalSaveWorkflow({
418
+ apiClient: apiClient ?? undefined,
419
+ eventHandlers,
420
+ features,
421
+ onMarkAsSaved: markAsSaved
422
+ });
423
+ }
450
424
 
451
- // Create workflow object for saving (spread existing metadata to preserve format, tags, etc.)
452
- const finalWorkflow: Workflow = {
453
- id: workflowId,
454
- name: workflowToSave.name || 'Untitled Workflow',
455
- description: workflowToSave.description || '',
456
- nodes: workflowToSave.nodes || [],
457
- edges: workflowToSave.edges || [],
458
- metadata: {
459
- ...workflowToSave.metadata,
460
- version: workflowToSave.metadata?.version || '1.0.0',
461
- format: workflowToSave.metadata?.format || DEFAULT_WORKFLOW_FORMAT,
462
- createdAt: workflowToSave.metadata?.createdAt || new Date().toISOString(),
463
- updatedAt: new Date().toISOString()
464
- }
465
- };
425
+ /**
426
+ * Export workflow - thin wrapper that delegates to globalExportWorkflow().
427
+ *
428
+ * All export logic (flush, metadata construction, file download) lives
429
+ * in globalSave.ts the single source of truth.
430
+ */
431
+ async function exportWorkflow(): Promise<void> {
432
+ await globalExportWorkflow({ features });
433
+ }
466
434
 
467
- // Use enhanced client with authProvider if available, otherwise fall back to legacy api
468
- let savedWorkflow: Workflow;
469
- if (apiClient) {
470
- // Check if this is an existing workflow (non-UUID ID indicates existing)
471
- const isExistingWorkflow =
472
- finalWorkflow.id &&
473
- finalWorkflow.id.length > 0 &&
474
- !finalWorkflow.id.match(
475
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
476
- );
477
-
478
- if (isExistingWorkflow) {
479
- savedWorkflow = await apiClient.updateWorkflow(finalWorkflow.id, finalWorkflow);
480
- } else {
481
- savedWorkflow = await apiClient.saveWorkflow(finalWorkflow);
435
+ /**
436
+ * Import workflow from a JSON file
437
+ *
438
+ * Reads the selected file, validates its structure, and loads it into the workflow store.
439
+ */
440
+ function importWorkflow(file: File): void {
441
+ const reader = new FileReader();
442
+ reader.onload = (event) => {
443
+ try {
444
+ const text = event.target?.result;
445
+ if (typeof text !== 'string') {
446
+ throw new Error('Could not read file contents.');
482
447
  }
483
- } else {
484
- // Fall back to legacy workflowApi
485
- const { workflowApi } = await import('../services/api.js');
486
- savedWorkflow = await workflowApi.saveWorkflow(finalWorkflow);
487
- }
488
-
489
- // Update the workflow ID if it changed (new workflow)
490
- // Keep our current workflow state, only update ID and metadata from backend
491
- if (savedWorkflow.id && savedWorkflow.id !== finalWorkflow.id) {
492
- workflowActions.batchUpdate({
493
- nodes: finalWorkflow.nodes,
494
- edges: finalWorkflow.edges,
495
- name: finalWorkflow.name,
496
- metadata: {
497
- ...finalWorkflow.metadata,
498
- ...savedWorkflow.metadata
448
+ const data = JSON.parse(text);
449
+ const validation = validateWorkflowData(data);
450
+ if (!validation.valid) {
451
+ if (features.showToasts) {
452
+ apiToasts.error('Import workflow', validation.error ?? 'Invalid workflow JSON');
499
453
  }
500
- });
501
- }
502
-
503
- // Mark as saved (clears dirty state)
504
- markAsSaved();
505
-
506
- // Dismiss loading toast and show success
507
- if (loadingToast) {
508
- dismissToast(loadingToast);
454
+ logger.warn('Workflow import validation failed:', validation.error);
455
+ return;
456
+ }
457
+ workflowActions.initialize(data as Workflow);
458
+ if (features.showToasts) {
459
+ apiToasts.success('Import workflow', 'Workflow imported successfully');
460
+ }
461
+ if (eventHandlers?.onWorkflowLoad) {
462
+ eventHandlers.onWorkflowLoad(data as Workflow);
463
+ }
464
+ } catch (error) {
465
+ const errorObj = error instanceof Error ? error : new Error('Unknown error occurred');
466
+ logger.error('Workflow import failed:', errorObj);
467
+ if (features.showToasts) {
468
+ apiToasts.error('Import workflow', errorObj.message);
469
+ }
509
470
  }
471
+ };
472
+ reader.onerror = () => {
473
+ const message = 'Failed to read the selected file.';
474
+ logger.error(message);
510
475
  if (features.showToasts) {
511
- apiToasts.success('Save workflow', 'Workflow saved successfully');
512
- }
513
-
514
- // Call onAfterSave if provided
515
- if (eventHandlers?.onAfterSave) {
516
- await eventHandlers.onAfterSave(savedWorkflow);
517
- }
518
- } catch (error) {
519
- // Dismiss loading toast
520
- if (loadingToast) {
521
- dismissToast(loadingToast);
522
- }
523
-
524
- const errorObj = error instanceof Error ? error : new Error('Unknown error occurred');
525
-
526
- // Call onSaveError if provided
527
- if (eventHandlers?.onSaveError && workflowToSave) {
528
- await eventHandlers.onSaveError(errorObj, workflowToSave);
529
- }
530
-
531
- // Check if parent wants to handle API errors
532
- let suppressToast = false;
533
- if (eventHandlers?.onApiError) {
534
- suppressToast = eventHandlers.onApiError(errorObj, 'save') === true;
535
- }
536
-
537
- // Show error toast if not suppressed
538
- if (features.showToasts && !suppressToast) {
539
- apiToasts.error('Save workflow', errorObj.message);
476
+ apiToasts.error('Import workflow', message);
540
477
  }
541
-
542
- throw error; // Re-throw to allow calling code to handle if needed
543
- }
478
+ };
479
+ reader.readAsText(file);
544
480
  }
545
481
 
546
482
  /**
547
- * Export workflow - exposed API function
483
+ * Handle file input change event for workflow import
548
484
  */
549
- async function exportWorkflow(): Promise<void> {
550
- try {
551
- // Wait for any pending DOM updates before exporting
552
- await tick();
553
-
554
- // Use current workflow from global store
555
- const workflowToExport = $workflowStore;
556
-
557
- if (!workflowToExport) {
558
- return;
559
- }
560
-
561
- // Create workflow object for export (spread existing metadata to preserve format, tags, etc.)
562
- const finalWorkflow = {
563
- id: workflowToExport.id || 'untitled-workflow',
564
- name: workflowToExport.name || 'Untitled Workflow',
565
- nodes: workflowToExport.nodes || [],
566
- edges: workflowToExport.edges || [],
567
- metadata: {
568
- ...workflowToExport.metadata,
569
- version: workflowToExport.metadata?.version || '1.0.0',
570
- format: workflowToExport.metadata?.format || DEFAULT_WORKFLOW_FORMAT,
571
- createdAt: workflowToExport.metadata?.createdAt || new Date().toISOString(),
572
- updatedAt: new Date().toISOString()
573
- }
574
- };
575
-
576
- // Create and download the file
577
- const dataStr = JSON.stringify(finalWorkflow, null, 2);
578
- const dataBlob = new Blob([dataStr], { type: 'application/json' });
579
- const url = URL.createObjectURL(dataBlob);
580
- const link = document.createElement('a');
581
- link.href = url;
582
- link.download = `${finalWorkflow.name}.json`;
583
- link.click();
584
- URL.revokeObjectURL(url);
585
- } catch {
586
- // Export failed
485
+ function handleImportFileChange(event: Event): void {
486
+ const input = event.target as HTMLInputElement;
487
+ const file = input.files?.[0];
488
+ if (file) {
489
+ importWorkflow(file);
587
490
  }
588
- }
589
-
590
- // Expose save and export functions globally for external access
591
- if (typeof window !== 'undefined') {
592
- window.flowdropSave = saveWorkflow;
593
- window.flowdropExport = exportWorkflow;
491
+ // Reset input so same file can be re-imported
492
+ input.value = '';
594
493
  }
595
494
 
596
495
  // Function to handle clicks outside the sidebar
597
496
  function handleCanvasClick(event: MouseEvent): void {
598
497
  // Check if the click is outside the right sidebar
599
- const rightSidebar = document.querySelector('.flowdrop-sidebar--right');
498
+ const rightSidebar = document.querySelector('.flowdrop-main-layout__sidebar--right');
600
499
  if (rightSidebar && !rightSidebar.contains(event.target as Node)) {
601
500
  // Close sidebar when clicking outside of it
602
501
  if (isConfigSidebarOpen) {
@@ -657,10 +556,10 @@
657
556
  },
658
557
  onError: (error) => {
659
558
  // Don't show toast for auto-save errors to avoid noise
660
- console.warn('Auto-save failed:', error);
559
+ logger.warn('Auto-save failed:', error);
661
560
  },
662
561
  onSuccess: () => {
663
- console.debug('Auto-saved workflow');
562
+ logger.debug('Auto-saved workflow');
664
563
  }
665
564
  });
666
565
 
@@ -672,7 +571,7 @@
672
571
 
673
572
  // Monitor workflow store changes for testing node drag updates
674
573
  $effect(() => {
675
- const currentWorkflow = $workflowStore;
574
+ const currentWorkflow = getWorkflowStore();
676
575
  if (currentWorkflow) {
677
576
  // Workflow store updated
678
577
  }
@@ -689,7 +588,12 @@
689
588
  * Calculate left sidebar width based on collapsed state
690
589
  * When collapsed, use 48px; otherwise use user-configured width
691
590
  */
692
- const leftSidebarWidth = $derived($uiSettings.sidebarCollapsed ? 48 : $uiSettings.sidebarWidth);
591
+ const leftSidebarWidth = $derived(
592
+ getUiSettings().sidebarCollapsed ? 48 : getUiSettings().sidebarWidth
593
+ );
594
+
595
+ // File input reference for workflow import
596
+ let fileInputRef = $state<HTMLInputElement | null>(null);
693
597
  </script>
694
598
 
695
599
  <svelte:head>
@@ -697,6 +601,15 @@
697
601
  <meta name="description" content="A modern drag-and-drop workflow editor for LLM applications" />
698
602
  </svelte:head>
699
603
 
604
+ <!-- Hidden file input for workflow JSON import -->
605
+ <input
606
+ bind:this={fileInputRef}
607
+ type="file"
608
+ accept=".json,application/json"
609
+ style="display: none;"
610
+ onchange={handleImportFileChange}
611
+ />
612
+
700
613
  <!-- MainLayout wrapper for workflow editor -->
701
614
  <MainLayout
702
615
  showHeader={showNavbar}
@@ -707,8 +620,8 @@
707
620
  headerHeight={60}
708
621
  {leftSidebarWidth}
709
622
  rightSidebarWidth={400}
710
- leftSidebarMinWidth={$uiSettings.sidebarCollapsed ? 48 : 280}
711
- leftSidebarMaxWidth={$uiSettings.sidebarCollapsed ? 48 : 450}
623
+ leftSidebarMinWidth={getUiSettings().sidebarCollapsed ? 48 : 280}
624
+ leftSidebarMaxWidth={getUiSettings().sidebarCollapsed ? 48 : 450}
712
625
  rightSidebarMinWidth={320}
713
626
  rightSidebarMaxWidth={550}
714
627
  enableLeftSplitPane={false}
@@ -742,6 +655,16 @@
742
655
  exportWorkflow();
743
656
  }
744
657
  },
658
+ {
659
+ label: 'Import',
660
+ href: '#import',
661
+ icon: 'heroicons:arrow-up-tray',
662
+ variant: 'outline',
663
+ onclick: (e) => {
664
+ e.preventDefault();
665
+ fileInputRef?.click();
666
+ }
667
+ },
745
668
  {
746
669
  label: 'Workflow Settings',
747
670
  href: '#settings',
@@ -760,7 +683,7 @@
760
683
 
761
684
  <!-- Left Sidebar: Node Components -->
762
685
  {#snippet leftSidebar()}
763
- <NodeSidebar {nodes} activeFormat={$workflowFormat} />
686
+ <NodeSidebar {nodes} activeFormat={getWorkflowFormat()} />
764
687
  {/snippet}
765
688
 
766
689
  <!-- Right Sidebar: Configuration or Workflow Settings -->
@@ -768,10 +691,10 @@
768
691
  {#if isWorkflowSettingsOpen}
769
692
  <ConfigPanel
770
693
  title="Workflow Settings"
771
- id={$workflowStore?.id}
694
+ id={getWorkflowStore()?.id}
772
695
  details={[
773
- { label: 'Nodes', value: String($workflowStore?.nodes?.length ?? 0) },
774
- { label: 'Connections', value: String($workflowStore?.edges?.length ?? 0) }
696
+ { label: 'Nodes', value: String(getWorkflowStore()?.nodes?.length ?? 0) },
697
+ { label: 'Connections', value: String(getWorkflowStore()?.edges?.length ?? 0) }
775
698
  ]}
776
699
  configTitle="Settings"
777
700
  onClose={() => (isWorkflowSettingsOpen = false)}
@@ -783,18 +706,19 @@
783
706
  showUIExtensions={false}
784
707
  onChange={(config) => {
785
708
  // Sync workflow settings changes immediately on field blur
786
- if ($workflowStore) {
709
+ const wf = getWorkflowStore();
710
+ if (wf) {
787
711
  const newFormat = (config.format as string) || DEFAULT_WORKFLOW_FORMAT;
788
- const currentFormat = $workflowStore.metadata?.format || DEFAULT_WORKFLOW_FORMAT;
712
+ const currentFormat = wf.metadata?.format || DEFAULT_WORKFLOW_FORMAT;
789
713
 
790
714
  // Warn about incompatible nodes when format changes
791
715
  if (newFormat !== currentFormat) {
792
- const incompatibleNodes = $workflowStore.nodes?.filter((node) => {
716
+ const incompatibleNodes = wf.nodes?.filter((node) => {
793
717
  const formats = node.data?.metadata?.formats;
794
718
  return formats && formats.length > 0 && !formats.includes(newFormat);
795
719
  });
796
720
  if (incompatibleNodes && incompatibleNodes.length > 0) {
797
- console.warn(
721
+ logger.warn(
798
722
  `Format changed to '${newFormat}'. ${incompatibleNodes.length} node(s) are not compatible with this format and may not export correctly:`,
799
723
  incompatibleNodes.map((n) => n.data?.label || n.type)
800
724
  );
@@ -805,7 +729,7 @@
805
729
  name: config.name as string,
806
730
  description: config.description as string | undefined,
807
731
  metadata: {
808
- ...$workflowStore.metadata,
732
+ ...wf.metadata,
809
733
  format: newFormat
810
734
  }
811
735
  });
@@ -814,7 +738,7 @@
814
738
  />
815
739
  </ConfigPanel>
816
740
  {:else if selectedNodeForConfig()}
817
- {@const currentNode = selectedNodeForConfig()}
741
+ {@const currentNode = selectedNodeForConfig()!}
818
742
  <ConfigPanel
819
743
  title={currentNode.data.label}
820
744
  id={currentNode.id}
@@ -828,9 +752,9 @@
828
752
  <ConfigForm
829
753
  {authProvider}
830
754
  node={currentNode}
831
- workflowId={$workflowStore?.id}
832
- workflowNodes={$workflowStore?.nodes}
833
- workflowEdges={$workflowStore?.edges}
755
+ workflowId={getWorkflowStore()?.id}
756
+ workflowNodes={getWorkflowStore()?.nodes}
757
+ workflowEdges={getWorkflowStore()?.edges}
834
758
  onChange={async (updatedConfig, uiExtensions) => {
835
759
  // Sync config changes to workflow immediately on field blur
836
760
  if (selectedNodeId && currentNode) {
@@ -857,10 +781,10 @@
857
781
 
858
782
  // Update the local editor state to reflect config changes immediately
859
783
  // This is needed for nodeType changes to take effect visually
860
- workflowEditorRef.updateNodeData(selectedNodeId, updatedData);
784
+ workflowEditorRef?.updateNodeData(selectedNodeId, updatedData);
861
785
 
862
786
  // Refresh edge positions in case config changes affect handles
863
- await workflowEditorRef.refreshEdgePositions(selectedNodeId);
787
+ await workflowEditorRef?.refreshEdgePositions(selectedNodeId);
864
788
  }
865
789
  }}
866
790
  />
@@ -869,9 +793,9 @@
869
793
  {/snippet}
870
794
 
871
795
  <!-- Main Content: Workflow Editor with Error Status -->
872
- <!-- Status Display -->
796
+ <!-- Status Display: aria-live announces API errors dynamically without requiring focus -->
873
797
  {#if error}
874
- <div class="flowdrop-status flowdrop-status--error">
798
+ <div class="flowdrop-status flowdrop-status--error" aria-live="polite" aria-atomic="true">
875
799
  <div class="flowdrop-status__content">
876
800
  <div class="flowdrop-flex flowdrop-gap--3">
877
801
  <div class="flowdrop-status__indicator flowdrop-status__indicator--error"></div>
@@ -920,7 +844,7 @@
920
844
  {/if}
921
845
 
922
846
  <!-- Main Editor Area -->
923
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
847
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions — interactive workflow canvas region with keyboard support -->
924
848
  <div
925
849
  class="flowdrop-editor-main"
926
850
  class:pipeline-view={!!pipelineId}
@@ -934,7 +858,7 @@
934
858
  {nodes}
935
859
  {height}
936
860
  {width}
937
- {endpointConfig}
861
+ endpointConfig={endpointConfig ?? undefined}
938
862
  {isConfigSidebarOpen}
939
863
  selectedNodeForConfig={selectedNodeForConfig()}
940
864
  {openConfigSidebar}
@@ -0,0 +1,25 @@
1
+ <script module>
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
+ import CanvasBanner from "./CanvasBanner.svelte";
4
+ import CanvasDecorator from "../stories/CanvasDecorator.svelte";
5
+
6
+ const { Story } = defineMeta({
7
+ title: "Display/CanvasBanner",
8
+ tags: ["autodocs"],
9
+ parameters: {
10
+ layout: "centered",
11
+ },
12
+ });
13
+ </script>
14
+
15
+ <Story name="Default">
16
+ <CanvasDecorator>
17
+ <CanvasBanner title="Empty Canvas" description="Drag nodes from the sidebar to get started" iconName="heroicons:squares-plus" />
18
+ </CanvasDecorator>
19
+ </Story>
20
+
21
+ <Story name="No Icon">
22
+ <CanvasDecorator>
23
+ <CanvasBanner title="No Workflows" description="Create your first workflow to begin" />
24
+ </CanvasDecorator>
25
+ </Story>
@@ -0,0 +1,27 @@
1
+ export default CanvasBanner;
2
+ type CanvasBanner = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ declare const CanvasBanner: $$__sveltets_2_IsomorphicComponent<{
10
+ [x: string]: never;
11
+ }, {
12
+ [evt: string]: CustomEvent<any>;
13
+ }, {}, {}, string>;
14
+ import CanvasBanner from "./CanvasBanner.svelte";
15
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
16
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
17
+ $$bindings?: Bindings;
18
+ } & Exports;
19
+ (internal: unknown, props: {
20
+ $$events?: Events;
21
+ $$slots?: Slots;
22
+ }): Exports & {
23
+ $set?: any;
24
+ $on?: any;
25
+ };
26
+ z_$$bindings?: Bindings;
27
+ }
@@ -47,11 +47,11 @@
47
47
  font-size: 1.125rem;
48
48
  font-weight: 700;
49
49
  margin-bottom: 0.5rem;
50
- color: #111827;
50
+ color: var(--fd-foreground, #111827);
51
51
  }
52
52
 
53
53
  .flowdrop-canvas-banner__description {
54
54
  font-size: 0.875rem;
55
- color: #6b7280;
55
+ color: var(--fd-muted-foreground, #6b7280);
56
56
  }
57
57
  </style>