@flowdrop/flowdrop 1.11.0 → 1.12.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 (46) hide show
  1. package/dist/api/enhanced-client.d.ts +29 -16
  2. package/dist/api/enhanced-client.js +0 -14
  3. package/dist/components/PipelineStatus.svelte +9 -12
  4. package/dist/components/WorkflowEditor.svelte +3 -0
  5. package/dist/components/interrupt/ChoicePrompt.svelte +24 -5
  6. package/dist/components/interrupt/ConfirmationPrompt.svelte +5 -0
  7. package/dist/components/interrupt/InterruptBubble.svelte +12 -0
  8. package/dist/components/interrupt/ReviewPrompt.svelte +20 -0
  9. package/dist/components/interrupt/TextInputPrompt.svelte +5 -0
  10. package/dist/components/nodes/GatewayNode.svelte +2 -6
  11. package/dist/components/nodes/WorkflowNode.svelte +2 -6
  12. package/dist/components/playground/ChatInput.svelte +359 -0
  13. package/dist/components/playground/ChatInput.svelte.d.ts +14 -0
  14. package/dist/components/playground/ChatPanel.svelte +100 -724
  15. package/dist/components/playground/ChatPanel.svelte.d.ts +9 -26
  16. package/dist/components/playground/ControlPanel.svelte +496 -0
  17. package/dist/components/playground/ControlPanel.svelte.d.ts +20 -0
  18. package/dist/components/playground/ExecutionConsole.svelte +163 -0
  19. package/dist/components/playground/ExecutionConsole.svelte.d.ts +14 -0
  20. package/dist/components/playground/MessageStream.svelte +283 -0
  21. package/dist/components/playground/MessageStream.svelte.d.ts +27 -0
  22. package/dist/components/playground/PipelineKanbanView.svelte +284 -0
  23. package/dist/components/playground/PipelineKanbanView.svelte.d.ts +11 -0
  24. package/dist/components/playground/PipelinePanel.svelte +204 -65
  25. package/dist/components/playground/PipelinePanel.svelte.d.ts +3 -1
  26. package/dist/components/playground/PipelineTableView.svelte +376 -0
  27. package/dist/components/playground/PipelineTableView.svelte.d.ts +11 -0
  28. package/dist/components/playground/Playground.svelte +262 -1200
  29. package/dist/components/playground/Playground.svelte.d.ts +0 -13
  30. package/dist/components/playground/PlaygroundStudio.svelte +35 -61
  31. package/dist/components/playground/PlaygroundStudio.svelte.d.ts +3 -1
  32. package/dist/components/playground/pipelineViewUtils.svelte.d.ts +22 -0
  33. package/dist/components/playground/pipelineViewUtils.svelte.js +77 -0
  34. package/dist/messages/defaults.d.ts +24 -0
  35. package/dist/messages/defaults.js +24 -0
  36. package/dist/playground/index.d.ts +6 -1
  37. package/dist/playground/index.js +6 -0
  38. package/dist/playground/mount.d.ts +3 -0
  39. package/dist/playground/mount.js +3 -2
  40. package/dist/stores/playgroundStore.svelte.d.ts +6 -0
  41. package/dist/stores/playgroundStore.svelte.js +21 -1
  42. package/dist/types/index.d.ts +28 -2
  43. package/dist/types/playground.d.ts +5 -2
  44. package/dist/types/playground.js +5 -7
  45. package/dist/utils/nodeStatus.js +15 -5
  46. package/package.json +1 -1
@@ -1,81 +1,48 @@
1
1
  <!--
2
2
  ChatPanel Component
3
-
4
- Clean conversational chat interface for the playground.
5
- Displays messages with chat bubbles and includes a simple input area.
6
- Styled with BEM syntax for a Langflow-like appearance.
3
+
4
+ Public conversational chat interface for the playground. Composes
5
+ MessageStream (message + interrupt feed) and ChatInput (textarea +
6
+ send/run/stop). Use this for chat-style agent interactions.
7
+
8
+ For view-only execution surfaces, prefer the MessageStream primitive
9
+ directly — ChatPanel's showChatInput/showRunButton flags are kept for
10
+ backwards compatibility but are deprecated.
7
11
  -->
8
12
 
9
13
  <script lang="ts">
10
14
  import Icon from '@iconify/svelte';
11
- import { tick } from 'svelte';
12
- import MessageBubble from './MessageBubble.svelte';
13
- import { InterruptBubble } from '../interrupt/index.js';
14
- import type { PlaygroundMessage } from '../../types/playground.js';
15
- import { hasEnableRunFlag } from '../../types/playground.js';
16
- import {
17
- isInterruptMetadata,
18
- extractInterruptMetadata,
19
- metadataToInterrupt
20
- } from '../../types/interrupt.js';
21
- import {
22
- getMessages,
23
- getChatMessages,
24
- getIsExecuting,
25
- getCanSendMessage,
26
- getSessionStatus,
27
- getCurrentSession
28
- } from '../../stores/playgroundStore.svelte.js';
29
- import {
30
- getInterruptsMap,
31
- interruptActions,
32
- getInterruptByMessageId
33
- } from '../../stores/interruptStore.svelte.js';
15
+ import MessageStream from './MessageStream.svelte';
16
+ import ChatInput from './ChatInput.svelte';
17
+ import { playgroundActions } from '../../stores/playgroundStore.svelte.js';
34
18
  import { m } from '../../messages/index.js';
35
19
 
36
- /**
37
- * Component props
38
- */
39
20
  interface Props {
40
- /** Whether to show timestamps on messages */
41
21
  showTimestamps?: boolean;
42
- /** Whether to auto-scroll to bottom on new messages */
43
22
  autoScroll?: boolean;
44
- /** Placeholder text for the input */
45
23
  placeholder?: string;
46
- /** Callback when user sends a message */
47
24
  onSendMessage?: (content: string) => void;
48
- /** Callback when user requests to stop execution */
49
25
  onStopExecution?: () => void;
50
- /** Whether to show log messages inline (false = hide them) */
51
26
  showLogsInline?: boolean;
52
- /** Whether to enable markdown rendering in messages */
53
27
  enableMarkdown?: boolean;
54
- /** Callback when an interrupt is resolved (to refresh messages) */
55
28
  onInterruptResolved?: () => void;
29
+ /** Render a "New session" CTA in the welcome state */
30
+ onCreateSession?: () => void;
56
31
  /**
57
- * Whether to show the chat text input (default: true)
58
- * When false, only the "Run" button is displayed.
32
+ * @deprecated Use `<MessageStream />` directly for view-only feeds.
33
+ * Kept for backwards compatibility with PlaygroundConfig URL params.
59
34
  */
60
35
  showChatInput?: boolean;
61
36
  /**
62
- * Whether to show the "Run" button (default: true)
63
- * When false, the Run button is hidden.
37
+ * @deprecated Use `<MessageStream />` directly for view-only feeds.
64
38
  */
65
39
  showRunButton?: boolean;
66
- /**
67
- * Predefined message to send when "Run" button is clicked
68
- * Used when showChatInput is false.
69
- */
70
40
  predefinedMessage?: string;
41
+ compactSystemMessages?: boolean;
71
42
  /**
72
- * Whether to display system messages in compact mode.
73
- * When true, system messages appear as minimal inline text
74
- * instead of full chat bubbles to reduce visual noise.
75
- * @default true
43
+ * @deprecated `showLogs` is now managed by playgroundStore.
44
+ * Setting it here syncs to the store on mount for backwards compatibility.
76
45
  */
77
- compactSystemMessages?: boolean;
78
- /** Whether log messages are visible — bindable so parent can host the toggle */
79
46
  showLogs?: boolean;
80
47
  }
81
48
 
@@ -88,494 +55,107 @@
88
55
  showLogsInline = false,
89
56
  enableMarkdown = true,
90
57
  onInterruptResolved,
58
+ onCreateSession,
91
59
  showChatInput = true,
92
60
  showRunButton = true,
93
61
  predefinedMessage,
94
62
  compactSystemMessages = true,
95
- showLogs = $bindable(true)
63
+ showLogs
96
64
  }: Props = $props();
97
65
 
98
- // Hoist playground branches — states/actions are read 8+ times each in the
99
- // template. Single getter walk per render instead of per-string.
100
66
  const states = $derived(m().playground.states);
101
- const actions = $derived(m().playground.actions);
102
- const chat = $derived(m().playground.chat);
103
67
 
104
- // Playground placeholders/labels are configurable per-instance (workflow
105
- // author) but fall back to the localized messages tree when not provided.
106
- const resolvedPlaceholder = $derived(placeholder ?? chat.placeholder);
107
- const resolvedPredefinedMessage = $derived(predefinedMessage ?? chat.predefinedRun);
108
-
109
- /**
110
- * Tracks whether the Run button is enabled.
111
- * Starts as true, becomes false after Run is clicked,
112
- * and is re-enabled when backend sends a message with enableRun: true metadata.
113
- */
114
- let runEnabled = $state(true);
115
-
116
- /**
117
- * Computed flag: true if both chat input and run button are hidden.
118
- * In this case, we show a helpful message to the user.
119
- */
120
68
  const noInputsAvailable = $derived(!showChatInput && !showRunButton);
121
69
 
122
- /** Input field value */
123
- let inputValue = $state('');
124
-
125
- /** Reference to the messages container for scrolling */
126
- let messagesContainer = $state<HTMLDivElement>();
127
-
128
- /** Reference to the input field */
129
- let inputField = $state<HTMLTextAreaElement>();
130
-
131
- /**
132
- * Filter messages based on local showLogs toggle.
133
- * The showLogsInline prop is still honoured as the initial hint when explicitly set to false.
134
- */
135
- const displayMessages = $derived(showLogs ? getMessages() : getChatMessages());
136
-
137
- // ---------------------------------------------------------------------------
138
- let previousMessageCount = $state(0);
139
- let userScrolledUp = $state(false);
140
-
141
- function handleScroll() {
142
- if (!messagesContainer) return;
143
- const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
144
- userScrolledUp = scrollHeight - scrollTop - clientHeight > 50;
145
- }
146
-
147
- function isFormFocused(): boolean {
148
- if (!messagesContainer) return false;
149
- const activeElement = document.activeElement;
150
- if (!activeElement) return false;
151
- // Check if active element is a form control inside the messages container
152
- const isFormControl =
153
- activeElement.tagName === 'INPUT' ||
154
- activeElement.tagName === 'TEXTAREA' ||
155
- activeElement.tagName === 'SELECT' ||
156
- activeElement.tagName === 'BUTTON' ||
157
- activeElement.getAttribute('contenteditable') === 'true';
158
- return isFormControl && messagesContainer.contains(activeElement);
159
- }
160
-
161
- /**
162
- * Check if a message is an interrupt request
163
- */
164
- function isInterruptMessage(message: PlaygroundMessage): boolean {
165
- return isInterruptMetadata(message.metadata as Record<string, unknown> | undefined);
166
- }
167
-
168
- /**
169
- * Sync interrupt messages to the interrupt store.
170
- * This effect runs when messages change and adds any new interrupt messages
171
- * to the interrupt store. We do this in an effect rather than during render
172
- * to avoid Svelte 5's state_unsafe_mutation error.
173
- *
174
- * If a message has status 'completed', the interrupt is marked as resolved
175
- * to show the "Confirmation Submitted" header, disabled buttons, and
176
- * "Response submitted" indicator.
177
- */
70
+ // Back-compat: sync legacy showLogs prop into the store whenever it changes.
178
71
  $effect(() => {
179
- // Get all messages that are interrupt requests
180
- const interruptMessages = displayMessages.filter(isInterruptMessage);
181
-
182
- for (const message of interruptMessages) {
183
- // Check if we already have this interrupt in the store
184
- const existing = getInterruptByMessageId(message.id);
185
- if (!existing) {
186
- // Extract and validate interrupt metadata
187
- const metadata = extractInterruptMetadata(
188
- message.metadata as Record<string, unknown> | undefined
189
- );
190
- if (metadata) {
191
- const interrupt = metadataToInterrupt(metadata, message.id, message.content);
192
- interruptActions.addInterrupt(interrupt);
193
-
194
- // If the message status is 'completed', mark the interrupt as resolved
195
- // This ensures completed interrupts show proper UI state:
196
- // - "Confirmation Submitted" header
197
- // - Disabled buttons
198
- // - "Response submitted" indicator
199
- if (message.status === 'completed') {
200
- interruptActions.resolveInterrupt(interrupt.id, metadata.response_value);
201
- }
202
- }
203
- }
72
+ if (showLogs !== undefined) {
73
+ playgroundActions.setShowLogs(showLogs);
204
74
  }
205
75
  });
206
-
207
- /**
208
- * Reactive map of message IDs to interrupts.
209
- * This ensures the component re-renders when interrupts are added to the store.
210
- */
211
- const interruptsByMessageId = $derived(
212
- new Map(
213
- Array.from(getInterruptsMap().values())
214
- .filter((i) => i.messageId)
215
- .map((i) => [i.messageId, i])
216
- )
217
- );
218
-
219
- /**
220
- * Get interrupt data for a message from the reactive map
221
- */
222
- function getInterruptForMessage(message: PlaygroundMessage) {
223
- return interruptsByMessageId.get(message.id);
224
- }
225
-
226
- /**
227
- * Check if we should show the welcome state
228
- */
229
- const showWelcome = $derived(!getCurrentSession() && displayMessages.length === 0);
230
-
231
- /**
232
- * Check if we should show the empty chat state (session exists but no messages)
233
- */
234
- const showEmptyChat = $derived(getCurrentSession() && displayMessages.length === 0);
235
-
236
- /**
237
- * Handle sending a message
238
- */
239
- function handleSend(): void {
240
- const trimmedValue = inputValue.trim();
241
- if (!trimmedValue || !getCanSendMessage()) {
242
- return;
243
- }
244
-
245
- onSendMessage?.(trimmedValue);
246
- inputValue = '';
247
-
248
- // Reset textarea height
249
- if (inputField) {
250
- inputField.style.height = 'auto';
251
- }
252
-
253
- // Re-focus the input
254
- tick().then(() => {
255
- inputField?.focus();
256
- });
257
- }
258
-
259
- /**
260
- * Handle keyboard events in the input
261
- */
262
- function handleKeydown(event: KeyboardEvent): void {
263
- if (event.key === 'Enter' && !event.shiftKey) {
264
- event.preventDefault();
265
- handleSend();
266
- }
267
- }
268
-
269
- /**
270
- * Handle stop execution
271
- */
272
- function handleStop(): void {
273
- onStopExecution?.();
274
- }
275
-
276
- /**
277
- * Handle "Run" button click when chat input is hidden.
278
- * Sends the predefined message to execute the workflow.
279
- * Disables the Run button after clicking until backend re-enables it.
280
- */
281
- function handleRun(): void {
282
- if (getIsExecuting() || !runEnabled) {
283
- return;
284
- }
285
- // Disable the Run button after clicking
286
- runEnabled = false;
287
- onSendMessage?.(resolvedPredefinedMessage);
288
- }
289
-
290
- /**
291
- * Track processed message IDs for enableRun detection
292
- * to avoid re-processing the same messages.
293
- */
294
- let processedEnableRunIds = $state(new Set<string>());
295
-
296
- /**
297
- * Watch for messages with enableRun: true metadata from the backend.
298
- * When detected, re-enable the Run button.
299
- */
300
- $effect(() => {
301
- // Check all messages for enableRun flag
302
- for (const message of displayMessages) {
303
- // Skip if already processed
304
- if (processedEnableRunIds.has(message.id)) {
305
- continue;
306
- }
307
- // Check if this message has the enableRun flag
308
- if (hasEnableRunFlag(message.metadata)) {
309
- // Mark as processed
310
- processedEnableRunIds = new Set([...processedEnableRunIds, message.id]);
311
- // Re-enable the Run button
312
- runEnabled = true;
313
- }
314
- }
315
- });
316
-
317
- /**
318
- * Reset runEnabled state when session changes.
319
- * This ensures a fresh state for each session.
320
- */
321
- $effect(() => {
322
- const session = getCurrentSession();
323
- if (session) {
324
- runEnabled = true;
325
- processedEnableRunIds = new Set();
326
- userScrolledUp = false;
327
- }
328
- });
329
-
330
- $effect(() => {
331
- const currentCount = displayMessages.length;
332
-
333
- if (!autoScroll || !messagesContainer) {
334
- previousMessageCount = currentCount;
335
- return;
336
- }
337
-
338
- const hasNewMessage = currentCount > previousMessageCount;
339
- previousMessageCount = currentCount;
340
-
341
- if (!hasNewMessage || userScrolledUp || isFormFocused()) return;
342
-
343
- tick().then(() => {
344
- if (messagesContainer) {
345
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
346
- }
347
- });
348
- });
349
-
350
- let wasExecuting = false;
351
-
352
- /**
353
- * Auto-focus input when execution completes
354
- */
355
- $effect(() => {
356
- const nowExecuting = getIsExecuting();
357
- if (wasExecuting && !nowExecuting && inputField) {
358
- tick().then(() => inputField?.focus());
359
- }
360
- wasExecuting = nowExecuting;
361
- });
362
-
363
- /**
364
- * Auto-resize textarea based on content
365
- */
366
- function handleInput(): void {
367
- if (inputField) {
368
- inputField.style.height = 'auto';
369
- inputField.style.height = `${Math.min(inputField.scrollHeight, 120)}px`;
370
- }
371
- }
372
76
  </script>
373
77
 
374
78
  <div class="chat-panel">
375
- <!-- Messages Container -->
376
- <div class="chat-panel__messages" bind:this={messagesContainer} onscroll={handleScroll}>
377
- {#if showWelcome}
378
- <!-- Welcome State (no session) -->
379
- <div class="chat-panel__welcome">
380
- <div class="chat-panel__welcome-icon">
381
- <svg
382
- width="48"
383
- height="48"
384
- viewBox="0 0 48 48"
385
- fill="none"
386
- xmlns="http://www.w3.org/2000/svg"
387
- >
388
- <path
389
- d="M8 16L24 8L40 16V32L24 40L8 32V16Z"
390
- stroke="currentColor"
391
- stroke-width="2"
392
- stroke-linejoin="round"
393
- />
394
- <path
395
- d="M8 16L24 24L40 16"
396
- stroke="currentColor"
397
- stroke-width="2"
398
- stroke-linejoin="round"
399
- />
400
- <path d="M24 24V40" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
401
- <path d="M16 12L32 20" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
402
- <path d="M16 36L32 28" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
403
- </svg>
404
- </div>
405
- {#if noInputsAvailable}
406
- <h2 class="chat-panel__welcome-title">{states.viewOnlyTitle}</h2>
407
- <p class="chat-panel__welcome-text">
408
- {states.viewOnlyText}
409
- </p>
410
- {:else if showChatInput}
411
- <h2 class="chat-panel__welcome-title">{states.newSessionTitle}</h2>
412
- <p class="chat-panel__welcome-text">{states.newSessionText}</p>
413
- {:else}
414
- <h2 class="chat-panel__welcome-title">{states.readyTitle}</h2>
415
- <p class="chat-panel__welcome-text">{states.readyText}</p>
416
- {/if}
417
- </div>
418
- {:else if showEmptyChat}
419
- <!-- Empty Chat State (session exists but no messages) -->
420
- <div class="chat-panel__welcome">
421
- <div class="chat-panel__welcome-icon">
422
- <svg
423
- width="48"
424
- height="48"
425
- viewBox="0 0 48 48"
426
- fill="none"
427
- xmlns="http://www.w3.org/2000/svg"
428
- >
429
- <path
430
- d="M8 16L24 8L40 16V32L24 40L8 32V16Z"
431
- stroke="currentColor"
432
- stroke-width="2"
433
- stroke-linejoin="round"
434
- />
435
- <path
436
- d="M8 16L24 24L40 16"
437
- stroke="currentColor"
438
- stroke-width="2"
439
- stroke-linejoin="round"
440
- />
441
- <path d="M24 24V40" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
442
- <path d="M16 12L32 20" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
443
- <path d="M16 36L32 28" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
444
- </svg>
445
- </div>
446
- {#if noInputsAvailable}
447
- <h2 class="chat-panel__welcome-title">{states.viewOnlyTitle}</h2>
448
- <p class="chat-panel__welcome-text">
449
- {states.viewOnlyText}
450
- </p>
451
- {:else if showChatInput}
452
- <h2 class="chat-panel__welcome-title">{states.newSessionTitle}</h2>
453
- <p class="chat-panel__welcome-text">{states.newSessionText}</p>
454
- {:else}
455
- <h2 class="chat-panel__welcome-title">{states.readyTitle}</h2>
456
- <p class="chat-panel__welcome-text">{states.readyText}</p>
457
- {/if}
458
- </div>
459
- {:else}
460
- <!-- Messages -->
461
- {#each displayMessages as message, index (message.id)}
462
- {#if isInterruptMessage(message)}
463
- {@const interrupt = getInterruptForMessage(message)}
464
- {#if interrupt}
465
- <InterruptBubble
466
- {interrupt}
467
- showTimestamp={showTimestamps}
468
- onResolved={onInterruptResolved}
469
- />
470
- {/if}
471
- {:else}
472
- <MessageBubble
473
- {message}
474
- showTimestamp={showTimestamps}
475
- isLast={index === displayMessages.length - 1}
476
- {enableMarkdown}
477
- {compactSystemMessages}
478
- />
479
- {/if}
480
- {/each}
79
+ <MessageStream
80
+ {showTimestamps}
81
+ {autoScroll}
82
+ {enableMarkdown}
83
+ allowLogs={showLogsInline}
84
+ {compactSystemMessages}
85
+ {onInterruptResolved}
86
+ welcome={welcomeState}
87
+ emptySession={emptyChatState}
88
+ />
89
+
90
+ <ChatInput
91
+ {placeholder}
92
+ {predefinedMessage}
93
+ {onSendMessage}
94
+ {onStopExecution}
95
+ showTextarea={showChatInput}
96
+ {showRunButton}
97
+ />
98
+ </div>
481
99
 
482
- {#if getIsExecuting()}
483
- <div class="chat-panel__typing">
484
- <div class="chat-panel__typing-indicator">
485
- <span></span>
486
- <span></span>
487
- <span></span>
488
- </div>
489
- <span class="chat-panel__typing-text">{states.processing}</span>
490
- </div>
491
- {/if}
100
+ {#snippet welcomeIcon()}
101
+ <div class="chat-panel__welcome-icon">
102
+ <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
103
+ <path
104
+ d="M8 16L24 8L40 16V32L24 40L8 32V16Z"
105
+ stroke="currentColor"
106
+ stroke-width="2"
107
+ stroke-linejoin="round"
108
+ />
109
+ <path d="M8 16L24 24L40 16" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
110
+ <path d="M24 24V40" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
111
+ <path d="M16 12L32 20" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
112
+ <path d="M16 36L32 28" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
113
+ </svg>
114
+ </div>
115
+ {/snippet}
116
+
117
+ {#snippet welcomeCopy()}
118
+ {#if noInputsAvailable}
119
+ <h2 class="chat-panel__welcome-title">{states.viewOnlyTitle}</h2>
120
+ <p class="chat-panel__welcome-text">{states.viewOnlyText}</p>
121
+ {:else if showChatInput}
122
+ <h2 class="chat-panel__welcome-title">{states.newSessionTitle}</h2>
123
+ <p class="chat-panel__welcome-text">{states.newSessionText}</p>
124
+ {:else}
125
+ <h2 class="chat-panel__welcome-title">{states.readyTitle}</h2>
126
+ <p class="chat-panel__welcome-text">{states.readyText}</p>
127
+ {/if}
128
+ {/snippet}
129
+
130
+ {#snippet welcomeState()}
131
+ <div class="chat-panel__welcome">
132
+ {@render welcomeIcon()}
133
+ {@render welcomeCopy()}
134
+ {#if onCreateSession}
135
+ <button type="button" class="chat-panel__create-session-btn" onclick={onCreateSession}>
136
+ <Icon icon="mdi:plus" />
137
+ New session
138
+ </button>
492
139
  {/if}
493
140
  </div>
141
+ {/snippet}
494
142
 
495
- <!-- Input Area -->
496
- <div class="chat-panel__input-area">
497
- {#if noInputsAvailable}
498
- <!-- No inputs available - show informational message -->
499
- <div class="chat-panel__no-inputs">
500
- <Icon icon="mdi:information-outline" />
501
- <span>{states.viewOnlyHelp}</span>
502
- </div>
503
- {:else}
504
- <div
505
- class="chat-panel__input-container"
506
- class:chat-panel__input-container--run-only={!showChatInput}
507
- >
508
- {#if showChatInput}
509
- <div class="chat-panel__input-wrapper">
510
- <textarea
511
- bind:this={inputField}
512
- bind:value={inputValue}
513
- class="chat-panel__input"
514
- placeholder={resolvedPlaceholder}
515
- rows="1"
516
- disabled={getIsExecuting()}
517
- onkeydown={handleKeydown}
518
- oninput={handleInput}
519
- ></textarea>
520
- </div>
521
- {/if}
522
-
523
- {#if getIsExecuting()}
524
- <button
525
- type="button"
526
- class="chat-panel__stop-btn"
527
- onclick={handleStop}
528
- title={actions.stopTitle}
529
- >
530
- <Icon icon="mdi:stop" />
531
- {actions.stop}
532
- </button>
533
- {:else if showChatInput}
534
- <button
535
- type="button"
536
- class="chat-panel__send-btn"
537
- onclick={handleSend}
538
- disabled={!inputValue.trim() || !getCanSendMessage()}
539
- title={actions.sendTitle}
540
- >
541
- {actions.send}
542
- </button>
543
- {:else if showRunButton}
544
- <button
545
- type="button"
546
- class="chat-panel__run-btn"
547
- onclick={handleRun}
548
- disabled={!runEnabled}
549
- title={runEnabled ? actions.runTitle : actions.runWaitingTitle}
550
- >
551
- <Icon icon="mdi:play" />
552
- {actions.run}
553
- </button>
554
- {/if}
555
- </div>
556
- {/if}
143
+ {#snippet emptyChatState()}
144
+ <div class="chat-panel__welcome">
145
+ {@render welcomeIcon()}
146
+ {@render welcomeCopy()}
557
147
  </div>
558
- </div>
148
+ {/snippet}
559
149
 
560
150
  <style>
561
151
  .chat-panel {
562
152
  display: flex;
563
153
  flex-direction: column;
564
154
  height: 100%;
565
- min-height: 0; /* Critical: allows flexbox to shrink properly */
155
+ min-height: 0;
566
156
  background-color: var(--fd-background);
567
157
  }
568
158
 
569
-
570
- /* Messages Container - Scrollable area that takes remaining space */
571
- .chat-panel__messages {
572
- flex: 1;
573
- min-height: 0; /* Critical: allows overflow to work in flex container */
574
- overflow-y: auto;
575
- padding: var(--fd-space-3xl);
576
- }
577
-
578
- /* Welcome State */
579
159
  .chat-panel__welcome {
580
160
  display: flex;
581
161
  flex-direction: column;
@@ -614,227 +194,23 @@
614
194
  margin: 0;
615
195
  }
616
196
 
617
- /* Typing Indicator */
618
- .chat-panel__typing {
619
- display: flex;
197
+ .chat-panel__create-session-btn {
198
+ display: inline-flex;
620
199
  align-items: center;
621
200
  gap: var(--fd-space-xs);
622
- padding: var(--fd-space-md) var(--fd-space-xl);
623
- margin-top: var(--fd-space-xs);
624
- background-color: var(--fd-muted);
625
- border-radius: var(--fd-radius-2xl);
626
- width: fit-content;
627
- }
628
-
629
- .chat-panel__typing-indicator {
630
- display: flex;
631
- gap: var(--fd-space-3xs);
632
- }
633
-
634
- .chat-panel__typing-indicator span {
635
- width: var(--fd-space-2xs);
636
- height: var(--fd-space-2xs);
637
- background-color: var(--fd-muted-foreground);
638
- border-radius: var(--fd-radius-full);
639
- animation: bounce 1.4s ease-in-out infinite;
640
- }
641
-
642
- .chat-panel__typing-indicator span:nth-child(1) {
643
- animation-delay: 0s;
644
- }
645
-
646
- .chat-panel__typing-indicator span:nth-child(2) {
647
- animation-delay: 0.2s;
648
- }
649
-
650
- .chat-panel__typing-indicator span:nth-child(3) {
651
- animation-delay: 0.4s;
652
- }
653
-
654
- @keyframes bounce {
655
- 0%,
656
- 60%,
657
- 100% {
658
- transform: translateY(0);
659
- }
660
- 30% {
661
- transform: translateY(-0.25rem);
662
- }
663
- }
664
-
665
- .chat-panel__typing-text {
666
- font-size: var(--fd-text-sm);
667
- color: var(--fd-muted-foreground);
668
- }
669
-
670
- /* Input Area - Always stays at bottom, never shrinks */
671
- .chat-panel__input-area {
672
- flex-shrink: 0;
673
- padding: var(--fd-space-xl) var(--fd-space-3xl) var(--fd-space-3xl);
674
- background-color: var(--fd-background);
675
- border-top: 1px solid var(--fd-border-muted);
676
- }
677
-
678
- .chat-panel__input-container {
679
- display: flex;
680
- align-items: flex-end;
681
- gap: var(--fd-space-md);
682
- max-width: 760px;
683
- margin: 0 auto;
684
- }
685
-
686
- .chat-panel__input-wrapper {
687
- flex: 1;
688
- display: flex;
689
- align-items: flex-end;
690
- background-color: var(--fd-background);
691
- border: 1px solid var(--fd-border);
692
- border-radius: var(--fd-radius-xl);
693
- padding: var(--fd-space-sm) var(--fd-space-md);
694
- transition:
695
- border-color var(--fd-transition-fast),
696
- box-shadow var(--fd-transition-fast);
697
- }
698
-
699
- .chat-panel__input-wrapper:focus-within {
700
- border-color: var(--fd-primary);
701
- box-shadow: 0 0 0 3px var(--fd-primary-muted);
702
- }
703
-
704
- .chat-panel__input {
705
- flex: 1;
706
- border: none;
707
- outline: none;
708
- resize: none;
709
- font-family: inherit;
710
- font-size: var(--fd-text-base);
711
- line-height: var(--fd-leading-normal);
712
- max-height: 120px;
713
- background: transparent;
714
- color: var(--fd-foreground);
715
- }
716
-
717
- .chat-panel__input::placeholder {
718
- color: var(--fd-muted-foreground);
719
- }
720
-
721
- .chat-panel__input:disabled {
722
- cursor: not-allowed;
723
- opacity: 0.6;
724
- }
725
-
726
- .chat-panel__send-btn {
727
- display: flex;
728
- align-items: center;
729
- justify-content: center;
730
- padding: var(--fd-space-sm) var(--fd-space-2xl);
731
- border: none;
732
- border-radius: var(--fd-radius-lg);
733
- background-color: var(--fd-foreground);
734
- color: var(--fd-background);
735
- font-size: var(--fd-text-sm);
736
- font-weight: 500;
737
- cursor: pointer;
738
- transition: all var(--fd-transition-fast);
739
- flex-shrink: 0;
740
- }
741
-
742
- .chat-panel__send-btn:hover:not(:disabled) {
743
- opacity: 0.85;
744
- }
745
-
746
- .chat-panel__send-btn:disabled {
747
- background-color: var(--fd-foreground);
748
- color: var(--fd-background);
749
- opacity: 0.3;
750
- cursor: not-allowed;
751
- }
752
-
753
- .chat-panel__stop-btn {
754
- display: flex;
755
- align-items: center;
756
- gap: var(--fd-space-3xs);
201
+ margin-top: var(--fd-space-2xl);
757
202
  padding: var(--fd-space-sm) var(--fd-space-xl);
758
203
  border: none;
759
- border-radius: var(--fd-radius-lg);
760
- background-color: var(--fd-error);
761
- color: var(--fd-error-foreground);
762
- font-size: var(--fd-text-sm);
763
- font-weight: 500;
764
- cursor: pointer;
765
- transition: background-color var(--fd-transition-fast);
766
- flex-shrink: 0;
767
- }
768
-
769
- .chat-panel__stop-btn:hover {
770
- background-color: var(--fd-error-hover);
771
- }
772
-
773
- /* Run button (when chat input is hidden) */
774
- .chat-panel__run-btn {
775
- display: flex;
776
- align-items: center;
777
- gap: var(--fd-space-3xs);
778
- padding: var(--fd-space-sm) var(--fd-space-2xl);
779
- border: none;
780
- border-radius: var(--fd-radius-lg);
781
- background-color: var(--fd-success);
782
- color: var(--fd-success-foreground);
783
- font-size: var(--fd-text-sm);
204
+ border-radius: var(--fd-radius-md);
205
+ background: var(--fd-primary);
206
+ color: var(--fd-primary-foreground);
207
+ font-size: var(--fd-text-base);
784
208
  font-weight: 500;
785
209
  cursor: pointer;
786
- transition: all var(--fd-transition-fast);
787
- flex-shrink: 0;
788
- }
789
-
790
- .chat-panel__run-btn:hover:not(:disabled) {
791
- background-color: var(--fd-success-hover);
792
- }
793
-
794
- .chat-panel__run-btn:disabled {
795
- background-color: var(--fd-border);
796
- color: var(--fd-muted-foreground);
797
- cursor: not-allowed;
798
- }
799
-
800
- /* Container modifier for run-only mode (no text input) */
801
- .chat-panel__input-container--run-only {
802
- justify-content: flex-end;
803
- }
804
-
805
- /* No inputs available message (view-only mode) */
806
- .chat-panel__no-inputs {
807
- display: flex;
808
- align-items: center;
809
- justify-content: center;
810
- gap: var(--fd-space-xs);
811
- padding: var(--fd-space-md) var(--fd-space-xl);
812
- background-color: var(--fd-muted);
813
- border-radius: var(--fd-radius-lg);
814
- color: var(--fd-muted-foreground);
815
- font-size: var(--fd-text-sm);
816
- max-width: 760px;
817
- margin: 0 auto;
210
+ transition: opacity var(--fd-transition-fast);
818
211
  }
819
212
 
820
- /* Responsive */
821
- @media (max-width: 640px) {
822
- .chat-panel__messages {
823
- padding: var(--fd-space-xl);
824
- }
825
-
826
- .chat-panel__input-area {
827
- padding: var(--fd-space-md) var(--fd-space-xl) var(--fd-space-xl);
828
- }
829
-
830
- .chat-panel__input-container {
831
- gap: var(--fd-space-xs);
832
- }
833
-
834
- .chat-panel__send-btn,
835
- .chat-panel__stop-btn,
836
- .chat-panel__run-btn {
837
- padding: var(--fd-space-xs) var(--fd-space-xl);
838
- }
213
+ .chat-panel__create-session-btn:hover {
214
+ opacity: 0.9;
839
215
  }
840
216
  </style>