@d34dman/flowdrop 0.0.39 → 0.0.40

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">FlowDrop</h1>
6
6
 
7
7
  <p align="center">
8
- <img src="https://img.shields.io/github/actions/workflow/status/d34dman/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 pages build status" />
9
9
  <a href="https://www.npmjs.com/package/@d34dman/flowdrop"><img src="https://img.shields.io/npm/v/@d34dman/flowdrop?style=flat-square" alt="npm" /></a>
10
10
  <img src="https://img.shields.io/npm/unpacked-size/%40d34dman%2Fflowdrop?style=flat-square" alt="NPM Unpacked Size" />
11
11
  <img src="https://img.shields.io/npm/types/@d34dman/flowdrop?style=flat-square" alt="npm type definitions" />
@@ -12,6 +12,7 @@
12
12
  import MessageBubble from './MessageBubble.svelte';
13
13
  import { InterruptBubble } from '../interrupt/index.js';
14
14
  import type { PlaygroundMessage } from '../../types/playground.js';
15
+ import { hasEnableRunFlag } from '../../types/playground.js';
15
16
  import {
16
17
  isInterruptMetadata,
17
18
  extractInterruptMetadata,
@@ -50,6 +51,21 @@
50
51
  enableMarkdown?: boolean;
51
52
  /** Callback when an interrupt is resolved (to refresh messages) */
52
53
  onInterruptResolved?: () => void;
54
+ /**
55
+ * Whether to show the chat text input (default: true)
56
+ * When false, only the "Run" button is displayed.
57
+ */
58
+ showChatInput?: boolean;
59
+ /**
60
+ * Whether to show the "Run" button (default: true)
61
+ * When false, the Run button is hidden.
62
+ */
63
+ showRunButton?: boolean;
64
+ /**
65
+ * Predefined message to send when "Run" button is clicked
66
+ * Used when showChatInput is false.
67
+ */
68
+ predefinedMessage?: string;
53
69
  }
54
70
 
55
71
  let {
@@ -60,9 +76,25 @@
60
76
  onStopExecution,
61
77
  showLogsInline = false,
62
78
  enableMarkdown = true,
63
- onInterruptResolved
79
+ onInterruptResolved,
80
+ showChatInput = true,
81
+ showRunButton = true,
82
+ predefinedMessage = 'Run workflow'
64
83
  }: Props = $props();
65
84
 
85
+ /**
86
+ * Tracks whether the Run button is enabled.
87
+ * Starts as true, becomes false after Run is clicked,
88
+ * and is re-enabled when backend sends a message with enableRun: true metadata.
89
+ */
90
+ let runEnabled = $state(true);
91
+
92
+ /**
93
+ * Computed flag: true if both chat input and run button are hidden.
94
+ * In this case, we show a helpful message to the user.
95
+ */
96
+ const noInputsAvailable = $derived(!showChatInput && !showRunButton);
97
+
66
98
  /** Input field value */
67
99
  let inputValue = $state('');
68
100
 
@@ -192,6 +224,61 @@
192
224
  onStopExecution?.();
193
225
  }
194
226
 
227
+ /**
228
+ * Handle "Run" button click when chat input is hidden.
229
+ * Sends the predefined message to execute the workflow.
230
+ * Disables the Run button after clicking until backend re-enables it.
231
+ */
232
+ function handleRun(): void {
233
+ if ($isExecuting || !runEnabled) {
234
+ return;
235
+ }
236
+ // Disable the Run button after clicking
237
+ runEnabled = false;
238
+ onSendMessage?.(predefinedMessage);
239
+ }
240
+
241
+ /**
242
+ * Track processed message IDs for enableRun detection
243
+ * to avoid re-processing the same messages.
244
+ */
245
+ let processedEnableRunIds = $state(new Set<string>());
246
+
247
+ /**
248
+ * Watch for messages with enableRun: true metadata from the backend.
249
+ * When detected, re-enable the Run button.
250
+ */
251
+ $effect(() => {
252
+ // Check all messages for enableRun flag
253
+ for (const message of displayMessages) {
254
+ // Skip if already processed
255
+ if (processedEnableRunIds.has(message.id)) {
256
+ continue;
257
+ }
258
+ // Check if this message has the enableRun flag
259
+ if (hasEnableRunFlag(message.metadata)) {
260
+ // Mark as processed
261
+ processedEnableRunIds = new Set([...processedEnableRunIds, message.id]);
262
+ // Re-enable the Run button
263
+ runEnabled = true;
264
+ }
265
+ }
266
+ });
267
+
268
+ /**
269
+ * Reset runEnabled state when session changes.
270
+ * This ensures a fresh state for each session.
271
+ */
272
+ $effect(() => {
273
+ const session = $currentSession;
274
+ if (session) {
275
+ // Reset to enabled state for new/changed sessions
276
+ runEnabled = true;
277
+ // Clear processed IDs for the new session
278
+ processedEnableRunIds = new Set();
279
+ }
280
+ });
281
+
195
282
  /**
196
283
  * Auto-scroll to bottom when messages change
197
284
  */
@@ -293,8 +380,18 @@
293
380
  <path d="M16 36L32 28" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
294
381
  </svg>
295
382
  </div>
296
- <h2 class="chat-panel__welcome-title">New chat</h2>
297
- <p class="chat-panel__welcome-text">Test your flow with a chat prompt</p>
383
+ {#if noInputsAvailable}
384
+ <h2 class="chat-panel__welcome-title">View only</h2>
385
+ <p class="chat-panel__welcome-text">
386
+ This playground is in view-only mode. No inputs are available.
387
+ </p>
388
+ {:else if showChatInput}
389
+ <h2 class="chat-panel__welcome-title">New chat</h2>
390
+ <p class="chat-panel__welcome-text">Test your flow with a chat prompt</p>
391
+ {:else}
392
+ <h2 class="chat-panel__welcome-title">Ready to run</h2>
393
+ <p class="chat-panel__welcome-text">Click Run to execute your workflow</p>
394
+ {/if}
298
395
  </div>
299
396
  {:else if showEmptyChat}
300
397
  <!-- Empty Chat State (session exists but no messages) -->
@@ -324,8 +421,18 @@
324
421
  <path d="M16 36L32 28" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
325
422
  </svg>
326
423
  </div>
327
- <h2 class="chat-panel__welcome-title">New chat</h2>
328
- <p class="chat-panel__welcome-text">Test your flow with a chat prompt</p>
424
+ {#if noInputsAvailable}
425
+ <h2 class="chat-panel__welcome-title">View only</h2>
426
+ <p class="chat-panel__welcome-text">
427
+ This playground is in view-only mode. No inputs are available.
428
+ </p>
429
+ {:else if showChatInput}
430
+ <h2 class="chat-panel__welcome-title">New chat</h2>
431
+ <p class="chat-panel__welcome-text">Test your flow with a chat prompt</p>
432
+ {:else}
433
+ <h2 class="chat-panel__welcome-title">Ready to run</h2>
434
+ <p class="chat-panel__welcome-text">Click Run to execute your workflow</p>
435
+ {/if}
329
436
  </div>
330
437
  {:else}
331
438
  <!-- Messages -->
@@ -365,42 +472,66 @@
365
472
 
366
473
  <!-- Input Area -->
367
474
  <div class="chat-panel__input-area">
368
- <div class="chat-panel__input-container">
369
- <div class="chat-panel__input-wrapper">
370
- <textarea
371
- bind:this={inputField}
372
- bind:value={inputValue}
373
- class="chat-panel__input"
374
- {placeholder}
375
- rows="1"
376
- disabled={$isExecuting}
377
- onkeydown={handleKeydown}
378
- oninput={handleInput}
379
- ></textarea>
475
+ {#if noInputsAvailable}
476
+ <!-- No inputs available - show informational message -->
477
+ <div class="chat-panel__no-inputs">
478
+ <Icon icon="mdi:information-outline" />
479
+ <span>View-only mode. Workflow execution is controlled externally.</span>
380
480
  </div>
481
+ {:else}
482
+ <div
483
+ class="chat-panel__input-container"
484
+ class:chat-panel__input-container--run-only={!showChatInput}
485
+ >
486
+ {#if showChatInput}
487
+ <div class="chat-panel__input-wrapper">
488
+ <textarea
489
+ bind:this={inputField}
490
+ bind:value={inputValue}
491
+ class="chat-panel__input"
492
+ {placeholder}
493
+ rows="1"
494
+ disabled={$isExecuting}
495
+ onkeydown={handleKeydown}
496
+ oninput={handleInput}
497
+ ></textarea>
498
+ </div>
499
+ {/if}
381
500
 
382
- {#if $sessionStatus === 'running' || $isExecuting}
383
- <button
384
- type="button"
385
- class="chat-panel__stop-btn"
386
- onclick={handleStop}
387
- title="Stop execution"
388
- >
389
- <Icon icon="mdi:stop" />
390
- Stop
391
- </button>
392
- {:else}
393
- <button
394
- type="button"
395
- class="chat-panel__send-btn"
396
- onclick={handleSend}
397
- disabled={!inputValue.trim()}
398
- title="Send message"
399
- >
400
- Send
401
- </button>
402
- {/if}
403
- </div>
501
+ {#if $sessionStatus === 'running' || $isExecuting}
502
+ <button
503
+ type="button"
504
+ class="chat-panel__stop-btn"
505
+ onclick={handleStop}
506
+ title="Stop execution"
507
+ >
508
+ <Icon icon="mdi:stop" />
509
+ Stop
510
+ </button>
511
+ {:else if showChatInput}
512
+ <button
513
+ type="button"
514
+ class="chat-panel__send-btn"
515
+ onclick={handleSend}
516
+ disabled={!inputValue.trim()}
517
+ title="Send message"
518
+ >
519
+ Send
520
+ </button>
521
+ {:else if showRunButton}
522
+ <button
523
+ type="button"
524
+ class="chat-panel__run-btn"
525
+ onclick={handleRun}
526
+ disabled={!runEnabled}
527
+ title={runEnabled ? 'Run workflow' : 'Waiting for workflow to be ready...'}
528
+ >
529
+ <Icon icon="mdi:play" />
530
+ Run
531
+ </button>
532
+ {/if}
533
+ </div>
534
+ {/if}
404
535
  </div>
405
536
  </div>
406
537
 
@@ -616,6 +747,53 @@
616
747
  background-color: #dc2626;
617
748
  }
618
749
 
750
+ /* Run button (when chat input is hidden) */
751
+ .chat-panel__run-btn {
752
+ display: flex;
753
+ align-items: center;
754
+ gap: 0.375rem;
755
+ padding: 0.625rem 1.25rem;
756
+ border: none;
757
+ border-radius: 0.5rem;
758
+ background-color: #10b981;
759
+ color: #ffffff;
760
+ font-size: 0.875rem;
761
+ font-weight: 500;
762
+ cursor: pointer;
763
+ transition: all 0.15s ease;
764
+ flex-shrink: 0;
765
+ }
766
+
767
+ .chat-panel__run-btn:hover:not(:disabled) {
768
+ background-color: #059669;
769
+ }
770
+
771
+ .chat-panel__run-btn:disabled {
772
+ background-color: #e5e7eb;
773
+ color: #9ca3af;
774
+ cursor: not-allowed;
775
+ }
776
+
777
+ /* Container modifier for run-only mode (no text input) */
778
+ .chat-panel__input-container--run-only {
779
+ justify-content: flex-end;
780
+ }
781
+
782
+ /* No inputs available message (view-only mode) */
783
+ .chat-panel__no-inputs {
784
+ display: flex;
785
+ align-items: center;
786
+ justify-content: center;
787
+ gap: 0.5rem;
788
+ padding: 0.75rem 1rem;
789
+ background-color: #f3f4f6;
790
+ border-radius: 0.5rem;
791
+ color: #6b7280;
792
+ font-size: 0.875rem;
793
+ max-width: 800px;
794
+ margin: 0 auto;
795
+ }
796
+
619
797
  /* Responsive */
620
798
  @media (max-width: 640px) {
621
799
  .chat-panel__messages {
@@ -631,7 +809,8 @@
631
809
  }
632
810
 
633
811
  .chat-panel__send-btn,
634
- .chat-panel__stop-btn {
812
+ .chat-panel__stop-btn,
813
+ .chat-panel__run-btn {
635
814
  padding: 0.5rem 1rem;
636
815
  }
637
816
  }
@@ -18,6 +18,21 @@ interface Props {
18
18
  enableMarkdown?: boolean;
19
19
  /** Callback when an interrupt is resolved (to refresh messages) */
20
20
  onInterruptResolved?: () => void;
21
+ /**
22
+ * Whether to show the chat text input (default: true)
23
+ * When false, only the "Run" button is displayed.
24
+ */
25
+ showChatInput?: boolean;
26
+ /**
27
+ * Whether to show the "Run" button (default: true)
28
+ * When false, the Run button is hidden.
29
+ */
30
+ showRunButton?: boolean;
31
+ /**
32
+ * Predefined message to send when "Run" button is clicked
33
+ * Used when showChatInput is false.
34
+ */
35
+ predefinedMessage?: string;
21
36
  }
22
37
  declare const ChatPanel: import("svelte").Component<Props, {}, "">;
23
38
  type ChatPanel = ReturnType<typeof ChatPanel>;
@@ -73,8 +73,17 @@
73
73
  /** Track session pending delete */
74
74
  let pendingDeleteId = $state<string | null>(null);
75
75
 
76
+ /** Track if initial session has been loaded to prevent duplicate loads */
77
+ let initialSessionLoaded = $state(false);
78
+
79
+ /** Track the session ID that was loaded to detect prop changes */
80
+ let loadedInitialSessionId = $state<string | undefined>(undefined);
81
+
82
+ /** Track if auto-run has already been triggered to prevent duplicate executions */
83
+ let autoRunTriggered = $state(false);
84
+
76
85
  /**
77
- * Initialize the playground
86
+ * Initialize the playground on mount
78
87
  */
79
88
  onMount(() => {
80
89
  // Set endpoint config if provided
@@ -95,7 +104,15 @@
95
104
 
96
105
  // Resume initial session if provided
97
106
  if (initialSessionId) {
98
- await loadSession(initialSessionId);
107
+ await loadInitialSession(initialSessionId);
108
+ }
109
+
110
+ // Handle auto-run after initialization is complete
111
+ if (config.autoRun && !autoRunTriggered) {
112
+ autoRunTriggered = true;
113
+ const predefinedMessage = config.predefinedMessage ?? 'Run workflow';
114
+ console.log('[Playground] Auto-run triggered with message:', predefinedMessage);
115
+ await handleSendMessage(predefinedMessage);
99
116
  }
100
117
  } catch (err) {
101
118
  console.error('[Playground] Initialization error:', err);
@@ -106,6 +123,66 @@
106
123
  void initializePlayground();
107
124
  });
108
125
 
126
+ /**
127
+ * Handle reactive changes to initialSessionId prop
128
+ * This allows the initial session to be set after mount
129
+ */
130
+ $effect(() => {
131
+ // Skip if no initialSessionId provided
132
+ if (!initialSessionId) {
133
+ return;
134
+ }
135
+
136
+ // Skip if this session was already loaded
137
+ if (initialSessionLoaded && loadedInitialSessionId === initialSessionId) {
138
+ return;
139
+ }
140
+
141
+ // Skip if sessions haven't been loaded yet (will be handled by onMount)
142
+ const sessionList = get(sessions);
143
+ if (sessionList.length === 0 && get(isLoading)) {
144
+ return;
145
+ }
146
+
147
+ // Load the initial session if sessions are available
148
+ if (sessionList.length > 0 && !initialSessionLoaded) {
149
+ void loadInitialSession(initialSessionId);
150
+ }
151
+ });
152
+
153
+ /**
154
+ * Load the initial session with validation and error handling
155
+ *
156
+ * @param sessionId - The session ID to load
157
+ */
158
+ async function loadInitialSession(sessionId: string): Promise<void> {
159
+ // Validate session exists in loaded sessions
160
+ const sessionList = get(sessions);
161
+ const sessionExists = sessionList.some((s) => s.id === sessionId);
162
+
163
+ if (!sessionExists) {
164
+ console.warn(
165
+ `[Playground] Initial session "${sessionId}" not found in available sessions. ` +
166
+ `Available sessions: ${sessionList.map((s) => s.id).join(', ') || 'none'}`
167
+ );
168
+ // Don't set error - just log warning and let user pick a session
169
+ initialSessionLoaded = true;
170
+ loadedInitialSessionId = sessionId;
171
+ return;
172
+ }
173
+
174
+ try {
175
+ await loadSession(sessionId);
176
+ initialSessionLoaded = true;
177
+ loadedInitialSessionId = sessionId;
178
+ } catch (err) {
179
+ console.error('[Playground] Failed to load initial session:', err);
180
+ // Mark as attempted to prevent retry loops
181
+ initialSessionLoaded = true;
182
+ loadedInitialSessionId = sessionId;
183
+ }
184
+ }
185
+
109
186
  /**
110
187
  * Cleanup on destroy
111
188
  */
@@ -563,6 +640,9 @@
563
640
  autoScroll={config.autoScroll ?? true}
564
641
  showLogsInline={config.logDisplayMode === 'inline'}
565
642
  enableMarkdown={config.enableMarkdown ?? true}
643
+ showChatInput={config.showChatInput ?? true}
644
+ showRunButton={config.showRunButton ?? true}
645
+ predefinedMessage={config.predefinedMessage ?? 'Run workflow'}
566
646
  onSendMessage={handleSendMessage}
567
647
  onStopExecution={handleStopExecution}
568
648
  onInterruptResolved={handleInterruptResolved}
@@ -71,7 +71,8 @@ export const defaultEndpointConfig = {
71
71
  preferences: '/users/preferences'
72
72
  },
73
73
  system: {
74
- health: '/system/health',
74
+ /** Health check at root level (industry standard for K8s, Docker, load balancers) */
75
+ health: '/health',
75
76
  config: '/system/config',
76
77
  version: '/system/version'
77
78
  }
@@ -194,7 +194,57 @@ export interface PlaygroundConfig {
194
194
  logDisplayMode?: 'inline' | 'collapsible';
195
195
  /** Enable markdown rendering in messages (default: true) */
196
196
  enableMarkdown?: boolean;
197
+ /**
198
+ * Whether to show the chat text input (default: true)
199
+ * When false, only the "Run" button is displayed for workflow execution.
200
+ */
201
+ showChatInput?: boolean;
202
+ /**
203
+ * Whether to show the "Run" button (default: true)
204
+ * When false, the Run button is hidden. If both showChatInput and showRunButton
205
+ * are false, a helpful message is displayed to the user.
206
+ */
207
+ showRunButton?: boolean;
208
+ /**
209
+ * Predefined message to send when "Run" button is clicked (default: "Run workflow")
210
+ * Used when showChatInput is false to provide a default message for workflow execution.
211
+ */
212
+ predefinedMessage?: string;
213
+ /**
214
+ * Automatically run the workflow once when the playground loads (default: false)
215
+ * When true, the workflow will execute immediately using the predefinedMessage.
216
+ * This is useful for scenarios where the workflow should start without user interaction.
217
+ * Note: Only runs once per session - subsequent runs require clicking the Run button.
218
+ */
219
+ autoRun?: boolean;
197
220
  }
221
+ /**
222
+ * Metadata field to control Run button state from backend.
223
+ * When a message contains this field set to true, the Run button becomes enabled.
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * // Backend sends a message with this metadata to re-enable Run button:
228
+ * const message: PlaygroundMessage = {
229
+ * id: "msg-123",
230
+ * sessionId: "sess-456",
231
+ * role: "system",
232
+ * content: "Workflow completed. Ready for next run.",
233
+ * timestamp: new Date().toISOString(),
234
+ * metadata: {
235
+ * enableRun: true
236
+ * }
237
+ * };
238
+ * ```
239
+ */
240
+ export declare const ENABLE_RUN_METADATA_KEY = "enableRun";
241
+ /**
242
+ * Check if a message metadata contains the enableRun flag
243
+ *
244
+ * @param metadata - The message metadata to check
245
+ * @returns True if the metadata signals to enable the Run button
246
+ */
247
+ export declare function hasEnableRunFlag(metadata: PlaygroundMessageMetadata | undefined): boolean;
198
248
  /**
199
249
  * Display mode for the Playground component
200
250
  */
@@ -6,6 +6,35 @@
6
6
  *
7
7
  * @module types/playground
8
8
  */
9
+ /**
10
+ * Metadata field to control Run button state from backend.
11
+ * When a message contains this field set to true, the Run button becomes enabled.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Backend sends a message with this metadata to re-enable Run button:
16
+ * const message: PlaygroundMessage = {
17
+ * id: "msg-123",
18
+ * sessionId: "sess-456",
19
+ * role: "system",
20
+ * content: "Workflow completed. Ready for next run.",
21
+ * timestamp: new Date().toISOString(),
22
+ * metadata: {
23
+ * enableRun: true
24
+ * }
25
+ * };
26
+ * ```
27
+ */
28
+ export const ENABLE_RUN_METADATA_KEY = 'enableRun';
29
+ /**
30
+ * Check if a message metadata contains the enableRun flag
31
+ *
32
+ * @param metadata - The message metadata to check
33
+ * @returns True if the metadata signals to enable the Run button
34
+ */
35
+ export function hasEnableRunFlag(metadata) {
36
+ return metadata?.[ENABLE_RUN_METADATA_KEY] === true;
37
+ }
9
38
  /**
10
39
  * Chat input detection patterns for identifying chat nodes in workflows
11
40
  */
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@d34dman/flowdrop",
3
3
  "license": "MIT",
4
4
  "private": false,
5
- "version": "0.0.39",
5
+ "version": "0.0.40",
6
6
  "scripts": {
7
7
  "dev": "vite dev",
8
8
  "build": "vite build && npm run prepack",
@@ -27,7 +27,11 @@
27
27
  "test:all": "npm run test && npm run test:e2e",
28
28
  "format": "prettier --write .",
29
29
  "storybook": "storybook dev -p 6006",
30
- "build-storybook": "storybook build"
30
+ "build-storybook": "storybook build",
31
+ "api:lint": "redocly lint api/openapi.yaml --config api/redocly.yaml",
32
+ "api:bundle": "redocly bundle api/openapi.yaml -o api/bundled.yaml --config api/redocly.yaml",
33
+ "api:preview": "redocly preview-docs api/openapi.yaml --config api/redocly.yaml",
34
+ "api:docs": "redocly build-docs api/bundled.yaml -o api-docs/index.html"
31
35
  },
32
36
  "watch": {
33
37
  "build": {
@@ -138,7 +142,7 @@
138
142
  },
139
143
  "repository": {
140
144
  "type": "git",
141
- "url": "git+https://github.com/d34dman/flowdrop.git"
145
+ "url": "git+https://github.com/flowdrop-io/flowdrop.git"
142
146
  },
143
147
  "devDependencies": {
144
148
  "@chromatic-com/storybook": "^4.0.1",
@@ -146,6 +150,7 @@
146
150
  "@eslint/js": "^9.18.0",
147
151
  "@iconify/svelte": "^5.0.0",
148
152
  "@playwright/test": "^1.49.1",
153
+ "@redocly/cli": "^2.14.9",
149
154
  "@storybook/addon-docs": "^9.0.15",
150
155
  "@storybook/addon-svelte-csf": "^5.0.4",
151
156
  "@storybook/addon-vitest": "^9.0.15",