@flowdrop/flowdrop 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/enhanced-client.js +5 -1
- package/dist/components/PipelineStatus.svelte +17 -1
- package/dist/components/PipelineStatus.svelte.d.ts +2 -0
- package/dist/components/WorkflowEditor.svelte +26 -0
- package/dist/components/playground/ChatPanel.svelte +33 -235
- package/dist/components/playground/ExecutionList.svelte +2 -6
- package/dist/components/playground/MessageBubble.svelte +61 -4
- package/dist/components/playground/PipelinePanel.svelte +17 -7
- package/dist/components/playground/PipelinePanel.svelte.d.ts +2 -1
- package/dist/components/playground/Playground.svelte +122 -72
- package/dist/components/playground/PlaygroundStudio.svelte +404 -0
- package/dist/components/playground/PlaygroundStudio.svelte.d.ts +30 -0
- package/dist/editor/index.d.ts +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/playground/index.d.ts +7 -3
- package/dist/playground/index.js +14 -5
- package/dist/playground/mount.d.ts +7 -0
- package/dist/playground/mount.js +78 -81
- package/dist/services/nodeExecutionService.js +4 -2
- package/dist/services/playgroundService.d.ts +11 -4
- package/dist/services/playgroundService.js +22 -12
- package/dist/stores/playgroundStore.svelte.d.ts +22 -21
- package/dist/stores/playgroundStore.svelte.js +79 -58
- package/dist/types/playground.d.ts +3 -5
- package/package.json +1 -1
|
@@ -425,6 +425,10 @@ export class EnhancedFlowDropApiClient {
|
|
|
425
425
|
* Fetch pipeline data including job information and status
|
|
426
426
|
*/
|
|
427
427
|
async getPipelineData(pipelineId) {
|
|
428
|
-
|
|
428
|
+
const response = await this.request('pipelines.get', this.config.endpoints.pipelines.get, { id: pipelineId }, {}, 'get pipeline data');
|
|
429
|
+
if (!response.success || !response.data) {
|
|
430
|
+
throw new Error(response.error ?? 'Failed to fetch pipeline data');
|
|
431
|
+
}
|
|
432
|
+
return response.data;
|
|
429
433
|
}
|
|
430
434
|
}
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
runLabel?: string;
|
|
25
25
|
/** When true, suppresses breadcrumb and layout events (used inside playground panel) */
|
|
26
26
|
isEmbedded?: boolean;
|
|
27
|
+
/** Increments when new messages arrive — triggers an immediate pipeline data refresh */
|
|
28
|
+
refreshTrigger?: number;
|
|
27
29
|
onActionsReady?: (
|
|
28
30
|
actions: Array<{
|
|
29
31
|
label: string;
|
|
@@ -35,9 +37,13 @@
|
|
|
35
37
|
) => void;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady, runLabel, isEmbedded = false }: Props =
|
|
40
|
+
let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady, runLabel, isEmbedded = false, refreshTrigger = 0 }: Props =
|
|
39
41
|
$props();
|
|
40
42
|
|
|
43
|
+
// Track previous trigger value so the $effect only fires on increments, not on initial mount.
|
|
44
|
+
// svelte-ignore state_referenced_locally
|
|
45
|
+
let _prevRefreshTrigger = refreshTrigger;
|
|
46
|
+
|
|
41
47
|
// Initialize API client if not provided
|
|
42
48
|
// svelte-ignore state_referenced_locally — client created once from props
|
|
43
49
|
const client =
|
|
@@ -285,6 +291,16 @@
|
|
|
285
291
|
|
|
286
292
|
// Note: Interval cleanup is handled by the $effect above.
|
|
287
293
|
// In Svelte 5, $effect cleanup runs both on re-execution and component destroy.
|
|
294
|
+
|
|
295
|
+
// Refresh pipeline data whenever new messages arrive (e.g. log messages during execution).
|
|
296
|
+
// Debounced so burst arrivals collapse into one fetch.
|
|
297
|
+
$effect(() => {
|
|
298
|
+
const t = refreshTrigger;
|
|
299
|
+
if (t <= 0 || t === _prevRefreshTrigger) return;
|
|
300
|
+
_prevRefreshTrigger = t;
|
|
301
|
+
const timer = setTimeout(fetchPipelineData, 300);
|
|
302
|
+
return () => clearTimeout(timer);
|
|
303
|
+
});
|
|
288
304
|
</script>
|
|
289
305
|
|
|
290
306
|
<div class="pipeline-status-container" class:pipeline-status-container--embedded={isEmbedded}>
|
|
@@ -10,6 +10,8 @@ interface Props {
|
|
|
10
10
|
runLabel?: string;
|
|
11
11
|
/** When true, suppresses breadcrumb and layout events (used inside playground panel) */
|
|
12
12
|
isEmbedded?: boolean;
|
|
13
|
+
/** Increments when new messages arrive — triggers an immediate pipeline data refresh */
|
|
14
|
+
refreshTrigger?: number;
|
|
13
15
|
onActionsReady?: (actions: Array<{
|
|
14
16
|
label: string;
|
|
15
17
|
href: string;
|
|
@@ -266,6 +266,32 @@
|
|
|
266
266
|
}
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
+
// Apply nodeStatuses from the parent (PipelineStatus embedded mode) to flowNodes
|
|
270
|
+
// whenever they change. loadNodeExecutionInfo() only fires on pipelineId change,
|
|
271
|
+
// so this is the update path for subsequent refreshes (e.g. after HITL resolution).
|
|
272
|
+
$effect(() => {
|
|
273
|
+
const statuses = props.nodeStatuses;
|
|
274
|
+
if (!statuses || Object.keys(statuses).length === 0) return;
|
|
275
|
+
|
|
276
|
+
flowNodes = untrack(() => flowNodes).map((node) => {
|
|
277
|
+
const rawStatus = statuses[node.id];
|
|
278
|
+
if (!rawStatus) return node;
|
|
279
|
+
|
|
280
|
+
const existing = node.data.executionInfo ?? { status: 'idle' as const, executionCount: 0, isExecuting: false };
|
|
281
|
+
return {
|
|
282
|
+
...node,
|
|
283
|
+
data: {
|
|
284
|
+
...node.data,
|
|
285
|
+
executionInfo: {
|
|
286
|
+
...existing,
|
|
287
|
+
status: rawStatus === 'error' ? ('failed' as const) : rawStatus,
|
|
288
|
+
isExecuting: rawStatus === 'running'
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
269
295
|
// ---------------------------------------------------------------------------
|
|
270
296
|
// History restore callback
|
|
271
297
|
// ---------------------------------------------------------------------------
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
getMessages,
|
|
23
23
|
getChatMessages,
|
|
24
24
|
getIsExecuting,
|
|
25
|
+
getCanSendMessage,
|
|
25
26
|
getSessionStatus,
|
|
26
27
|
getCurrentSession
|
|
27
28
|
} from '../../stores/playgroundStore.svelte.js';
|
|
28
|
-
import type { PlaygroundExecution } from '../../types/playground.js';
|
|
29
29
|
import {
|
|
30
30
|
getInterruptsMap,
|
|
31
31
|
interruptActions,
|
|
@@ -135,74 +135,15 @@
|
|
|
135
135
|
const displayMessages = $derived(showLogs ? getMessages() : getChatMessages());
|
|
136
136
|
|
|
137
137
|
// ---------------------------------------------------------------------------
|
|
138
|
-
// Execution separators
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
|
|
141
|
-
type ChatItem =
|
|
142
|
-
| { type: 'message'; message: PlaygroundMessage; msgIndex: number }
|
|
143
|
-
| { type: 'separator'; key: string; label: string; status: PlaygroundExecution['status'] };
|
|
144
|
-
|
|
145
|
-
/** Map executionId → { label, status } derived from the current session */
|
|
146
|
-
const executionMeta = $derived(
|
|
147
|
-
new Map(
|
|
148
|
-
(getCurrentSession()?.executions ?? []).map((e, i) => [
|
|
149
|
-
e.id,
|
|
150
|
-
{ label: `Run #${i + 1}`, status: e.status }
|
|
151
|
-
])
|
|
152
|
-
)
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Interleave execution-boundary separators into the message list.
|
|
157
|
-
* A separator is inserted before the first message of each new execution.
|
|
158
|
-
*/
|
|
159
|
-
const chatItems = $derived(
|
|
160
|
-
(() => {
|
|
161
|
-
const items: ChatItem[] = [];
|
|
162
|
-
let lastExecId: string | null = null;
|
|
163
|
-
|
|
164
|
-
displayMessages.forEach((msg, i) => {
|
|
165
|
-
const execId = msg.executionId ?? null;
|
|
166
|
-
if (execId !== null && execId !== lastExecId) {
|
|
167
|
-
const meta = executionMeta.get(execId);
|
|
168
|
-
if (meta) {
|
|
169
|
-
items.push({ type: 'separator', key: `sep-${execId}`, label: meta.label, status: meta.status });
|
|
170
|
-
lastExecId = execId;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
items.push({ type: 'message', message: msg, msgIndex: i });
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return items;
|
|
177
|
-
})()
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Track previous message count for detecting new messages.
|
|
182
|
-
* We only want to auto-scroll when NEW messages are added,
|
|
183
|
-
* not when existing messages are updated.
|
|
184
|
-
*/
|
|
185
138
|
let previousMessageCount = $state(0);
|
|
139
|
+
let userScrolledUp = $state(false);
|
|
186
140
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
* Used to determine if we should auto-scroll when new messages arrive.
|
|
190
|
-
* If user has scrolled up to read previous messages, we don't interrupt them.
|
|
191
|
-
*
|
|
192
|
-
* @param threshold - Pixels from bottom to consider "near bottom"
|
|
193
|
-
* @returns True if user is within threshold of the bottom
|
|
194
|
-
*/
|
|
195
|
-
function isNearBottom(threshold: number = 100): boolean {
|
|
196
|
-
if (!messagesContainer) return true;
|
|
141
|
+
function handleScroll() {
|
|
142
|
+
if (!messagesContainer) return;
|
|
197
143
|
const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
|
|
198
|
-
|
|
144
|
+
userScrolledUp = scrollHeight - scrollTop - clientHeight > 50;
|
|
199
145
|
}
|
|
200
146
|
|
|
201
|
-
/**
|
|
202
|
-
* Check if a form element inside the messages container has focus.
|
|
203
|
-
* When user is interacting with a form (e.g., interrupt prompt),
|
|
204
|
-
* we should not auto-scroll as it disrupts their input.
|
|
205
|
-
*/
|
|
206
147
|
function isFormFocused(): boolean {
|
|
207
148
|
if (!messagesContainer) return false;
|
|
208
149
|
const activeElement = document.activeElement;
|
|
@@ -297,7 +238,7 @@
|
|
|
297
238
|
*/
|
|
298
239
|
function handleSend(): void {
|
|
299
240
|
const trimmedValue = inputValue.trim();
|
|
300
|
-
if (!trimmedValue ||
|
|
241
|
+
if (!trimmedValue || !getCanSendMessage()) {
|
|
301
242
|
return;
|
|
302
243
|
}
|
|
303
244
|
|
|
@@ -380,58 +321,25 @@
|
|
|
380
321
|
$effect(() => {
|
|
381
322
|
const session = getCurrentSession();
|
|
382
323
|
if (session) {
|
|
383
|
-
// Reset to enabled state for new/changed sessions
|
|
384
324
|
runEnabled = true;
|
|
385
|
-
// Clear processed IDs for the new session
|
|
386
325
|
processedEnableRunIds = new Set();
|
|
326
|
+
userScrolledUp = false;
|
|
387
327
|
}
|
|
388
328
|
});
|
|
389
329
|
|
|
390
|
-
/**
|
|
391
|
-
* Smart auto-scroll to bottom when NEW messages are added.
|
|
392
|
-
*
|
|
393
|
-
* Only scrolls if:
|
|
394
|
-
* 1. autoScroll prop is enabled
|
|
395
|
-
* 2. New messages were actually added (not just updates)
|
|
396
|
-
* 3. User is already near the bottom (hasn't scrolled up to read)
|
|
397
|
-
* 4. User is not interacting with a form inside the chat
|
|
398
|
-
*
|
|
399
|
-
* This prevents disruptive scrolling when:
|
|
400
|
-
* - User is reading previous messages
|
|
401
|
-
* - User is filling out an interrupt form
|
|
402
|
-
* - Messages are being updated (e.g., status changes)
|
|
403
|
-
*/
|
|
404
330
|
$effect(() => {
|
|
405
331
|
const currentCount = displayMessages.length;
|
|
406
332
|
|
|
407
|
-
// Skip if auto-scroll is disabled or no container
|
|
408
333
|
if (!autoScroll || !messagesContainer) {
|
|
409
334
|
previousMessageCount = currentCount;
|
|
410
335
|
return;
|
|
411
336
|
}
|
|
412
337
|
|
|
413
|
-
// Check if this is a NEW message (count increased)
|
|
414
338
|
const hasNewMessage = currentCount > previousMessageCount;
|
|
415
|
-
|
|
416
|
-
// Update the tracked count
|
|
417
339
|
previousMessageCount = currentCount;
|
|
418
340
|
|
|
419
|
-
|
|
420
|
-
if (!hasNewMessage) {
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Don't scroll if user has scrolled up to read previous messages
|
|
425
|
-
if (!isNearBottom()) {
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Don't scroll if user is interacting with a form
|
|
430
|
-
if (isFormFocused()) {
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
341
|
+
if (!hasNewMessage || userScrolledUp || isFormFocused()) return;
|
|
433
342
|
|
|
434
|
-
// Safe to scroll to bottom
|
|
435
343
|
tick().then(() => {
|
|
436
344
|
if (messagesContainer) {
|
|
437
345
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
@@ -439,50 +347,17 @@
|
|
|
439
347
|
});
|
|
440
348
|
});
|
|
441
349
|
|
|
442
|
-
|
|
443
|
-
* Track previous executing state to detect when execution completes
|
|
444
|
-
*/
|
|
445
|
-
let wasExecuting = $state(false);
|
|
350
|
+
let wasExecuting = false;
|
|
446
351
|
|
|
447
352
|
/**
|
|
448
|
-
* Auto-focus input when execution completes
|
|
353
|
+
* Auto-focus input when execution completes
|
|
449
354
|
*/
|
|
450
355
|
$effect(() => {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (wasExecuting && !currentlyExecuting && inputField) {
|
|
455
|
-
tick().then(() => {
|
|
456
|
-
inputField?.focus();
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Update tracking state
|
|
461
|
-
wasExecuting = currentlyExecuting;
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Focus input when session status changes to idle or completed
|
|
466
|
-
*/
|
|
467
|
-
$effect(() => {
|
|
468
|
-
const status = getSessionStatus();
|
|
469
|
-
if ((status === 'idle' || status === 'completed') && inputField && !getIsExecuting()) {
|
|
470
|
-
tick().then(() => {
|
|
471
|
-
inputField?.focus();
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Focus input when a new session is created/loaded
|
|
478
|
-
*/
|
|
479
|
-
$effect(() => {
|
|
480
|
-
const session = getCurrentSession();
|
|
481
|
-
if (session && inputField && !getIsExecuting()) {
|
|
482
|
-
tick().then(() => {
|
|
483
|
-
inputField?.focus();
|
|
484
|
-
});
|
|
356
|
+
const nowExecuting = getIsExecuting();
|
|
357
|
+
if (wasExecuting && !nowExecuting && inputField) {
|
|
358
|
+
tick().then(() => inputField?.focus());
|
|
485
359
|
}
|
|
360
|
+
wasExecuting = nowExecuting;
|
|
486
361
|
});
|
|
487
362
|
|
|
488
363
|
/**
|
|
@@ -498,7 +373,7 @@
|
|
|
498
373
|
|
|
499
374
|
<div class="chat-panel">
|
|
500
375
|
<!-- Messages Container -->
|
|
501
|
-
<div class="chat-panel__messages" bind:this={messagesContainer}>
|
|
376
|
+
<div class="chat-panel__messages" bind:this={messagesContainer} onscroll={handleScroll}>
|
|
502
377
|
{#if showWelcome}
|
|
503
378
|
<!-- Welcome State (no session) -->
|
|
504
379
|
<div class="chat-panel__welcome">
|
|
@@ -582,50 +457,25 @@
|
|
|
582
457
|
{/if}
|
|
583
458
|
</div>
|
|
584
459
|
{:else}
|
|
585
|
-
<!-- Messages
|
|
586
|
-
{#each
|
|
587
|
-
{#if
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
class:chat-panel__exec-sep--running={item.status === 'running'}
|
|
593
|
-
>
|
|
594
|
-
<span class="chat-panel__exec-sep-line"></span>
|
|
595
|
-
<span class="chat-panel__exec-sep-label">
|
|
596
|
-
{#if item.status === 'completed'}
|
|
597
|
-
<Icon icon="mdi:check-circle" class="chat-panel__exec-sep-icon" />
|
|
598
|
-
{:else if item.status === 'failed'}
|
|
599
|
-
<Icon icon="mdi:alert-circle" class="chat-panel__exec-sep-icon" />
|
|
600
|
-
{:else}
|
|
601
|
-
<Icon icon="mdi:play-circle" class="chat-panel__exec-sep-icon" />
|
|
602
|
-
{/if}
|
|
603
|
-
{item.label}
|
|
604
|
-
<span class="chat-panel__exec-sep-status">{item.status}</span>
|
|
605
|
-
</span>
|
|
606
|
-
<span class="chat-panel__exec-sep-line"></span>
|
|
607
|
-
</div>
|
|
608
|
-
{:else}
|
|
609
|
-
{@const message = item.message}
|
|
610
|
-
{@const index = item.msgIndex}
|
|
611
|
-
{#if isInterruptMessage(message)}
|
|
612
|
-
{@const interrupt = getInterruptForMessage(message)}
|
|
613
|
-
{#if interrupt}
|
|
614
|
-
<InterruptBubble
|
|
615
|
-
{interrupt}
|
|
616
|
-
showTimestamp={showTimestamps}
|
|
617
|
-
onResolved={onInterruptResolved}
|
|
618
|
-
/>
|
|
619
|
-
{/if}
|
|
620
|
-
{:else}
|
|
621
|
-
<MessageBubble
|
|
622
|
-
{message}
|
|
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}
|
|
623
467
|
showTimestamp={showTimestamps}
|
|
624
|
-
|
|
625
|
-
{enableMarkdown}
|
|
626
|
-
{compactSystemMessages}
|
|
468
|
+
onResolved={onInterruptResolved}
|
|
627
469
|
/>
|
|
628
470
|
{/if}
|
|
471
|
+
{:else}
|
|
472
|
+
<MessageBubble
|
|
473
|
+
{message}
|
|
474
|
+
showTimestamp={showTimestamps}
|
|
475
|
+
isLast={index === displayMessages.length - 1}
|
|
476
|
+
{enableMarkdown}
|
|
477
|
+
{compactSystemMessages}
|
|
478
|
+
/>
|
|
629
479
|
{/if}
|
|
630
480
|
{/each}
|
|
631
481
|
|
|
@@ -670,7 +520,7 @@
|
|
|
670
520
|
</div>
|
|
671
521
|
{/if}
|
|
672
522
|
|
|
673
|
-
{#if
|
|
523
|
+
{#if getIsExecuting()}
|
|
674
524
|
<button
|
|
675
525
|
type="button"
|
|
676
526
|
class="chat-panel__stop-btn"
|
|
@@ -685,7 +535,7 @@
|
|
|
685
535
|
type="button"
|
|
686
536
|
class="chat-panel__send-btn"
|
|
687
537
|
onclick={handleSend}
|
|
688
|
-
disabled={!inputValue.trim()}
|
|
538
|
+
disabled={!inputValue.trim() || !getCanSendMessage()}
|
|
689
539
|
title={actions.sendTitle}
|
|
690
540
|
>
|
|
691
541
|
{actions.send}
|
|
@@ -716,57 +566,6 @@
|
|
|
716
566
|
background-color: var(--fd-background);
|
|
717
567
|
}
|
|
718
568
|
|
|
719
|
-
/* Execution separator */
|
|
720
|
-
.chat-panel__exec-sep {
|
|
721
|
-
display: flex;
|
|
722
|
-
align-items: center;
|
|
723
|
-
gap: var(--fd-space-sm);
|
|
724
|
-
padding: var(--fd-space-md) var(--fd-space-3xl);
|
|
725
|
-
margin: var(--fd-space-sm) 0;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
.chat-panel__exec-sep-line {
|
|
729
|
-
flex: 1;
|
|
730
|
-
height: 1px;
|
|
731
|
-
background-color: var(--fd-border);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
.chat-panel__exec-sep-label {
|
|
735
|
-
display: flex;
|
|
736
|
-
align-items: center;
|
|
737
|
-
gap: var(--fd-space-3xs);
|
|
738
|
-
font-size: var(--fd-text-xs);
|
|
739
|
-
font-weight: 600;
|
|
740
|
-
white-space: nowrap;
|
|
741
|
-
color: var(--fd-muted-foreground);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
:global(.chat-panel__exec-sep-icon) {
|
|
745
|
-
font-size: 0.875rem;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
.chat-panel__exec-sep--completed .chat-panel__exec-sep-label {
|
|
749
|
-
color: var(--fd-success, #16a34a);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
.chat-panel__exec-sep--completed .chat-panel__exec-sep-line {
|
|
753
|
-
background-color: var(--fd-success-muted, rgba(22, 163, 74, 0.2));
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
.chat-panel__exec-sep--failed .chat-panel__exec-sep-label {
|
|
757
|
-
color: var(--fd-error);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
.chat-panel__exec-sep--failed .chat-panel__exec-sep-line {
|
|
761
|
-
background-color: var(--fd-error-muted);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
.chat-panel__exec-sep-status {
|
|
765
|
-
text-transform: uppercase;
|
|
766
|
-
letter-spacing: 0.04em;
|
|
767
|
-
opacity: 0.8;
|
|
768
|
-
font-size: var(--fd-text-2xs);
|
|
769
|
-
}
|
|
770
569
|
|
|
771
570
|
/* Messages Container - Scrollable area that takes remaining space */
|
|
772
571
|
.chat-panel__messages {
|
|
@@ -774,7 +573,6 @@
|
|
|
774
573
|
min-height: 0; /* Critical: allows overflow to work in flex container */
|
|
775
574
|
overflow-y: auto;
|
|
776
575
|
padding: var(--fd-space-3xl);
|
|
777
|
-
scroll-behavior: smooth;
|
|
778
576
|
}
|
|
779
577
|
|
|
780
578
|
/* Welcome State */
|
|
@@ -11,10 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
let { executions, activeExecutionId, latestExecutionId, onSelect }: Props = $props();
|
|
13
13
|
|
|
14
|
-
function label(index: number): string {
|
|
15
|
-
return `Run #${index + 1}`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
14
|
function statusIcon(status: PlaygroundExecution['status']): string {
|
|
19
15
|
if (status === 'completed') return 'mdi:check-circle';
|
|
20
16
|
if (status === 'failed') return 'mdi:alert-circle';
|
|
@@ -23,7 +19,7 @@
|
|
|
23
19
|
</script>
|
|
24
20
|
|
|
25
21
|
<div class="execution-list">
|
|
26
|
-
{#each executions as execution
|
|
22
|
+
{#each executions as execution (execution.id)}
|
|
27
23
|
<div
|
|
28
24
|
class="execution-list__item"
|
|
29
25
|
class:execution-list__item--active={execution.id === activeExecutionId}
|
|
@@ -43,7 +39,7 @@
|
|
|
43
39
|
class="execution-list__status-icon execution-list__status-icon--{execution.status}"
|
|
44
40
|
/>
|
|
45
41
|
{/if}
|
|
46
|
-
<span class="execution-list__label">{
|
|
42
|
+
<span class="execution-list__label">{execution.id}</span>
|
|
47
43
|
{#if execution.id === latestExecutionId}
|
|
48
44
|
<span class="execution-list__badge">latest</span>
|
|
49
45
|
{/if}
|
|
@@ -153,8 +153,17 @@
|
|
|
153
153
|
|
|
154
154
|
{#if useCompactMode}
|
|
155
155
|
<!-- Compact system message: minimal inline text without bubble -->
|
|
156
|
-
<div
|
|
157
|
-
|
|
156
|
+
<div
|
|
157
|
+
class="system-notice"
|
|
158
|
+
class:system-notice--last={isLast}
|
|
159
|
+
class:system-notice--warning={message.metadata?.level === 'warning'}
|
|
160
|
+
class:system-notice--error={message.metadata?.level === 'error'}
|
|
161
|
+
class:system-notice--debug={message.metadata?.level === 'debug'}
|
|
162
|
+
>
|
|
163
|
+
<Icon icon={getLogLevelIcon()} class="system-notice__icon" />
|
|
164
|
+
{#if message.metadata?.source}
|
|
165
|
+
<span class="system-notice__source">{message.metadata.source}</span>
|
|
166
|
+
{/if}
|
|
158
167
|
<span class="system-notice__text">{message.content}</span>
|
|
159
168
|
{#if showTimestamp}
|
|
160
169
|
<span class="system-notice__timestamp">{formatTimestamp(message.timestamp)}</span>
|
|
@@ -172,6 +181,9 @@
|
|
|
172
181
|
<Icon icon={getLogLevelIcon()} />
|
|
173
182
|
</div>
|
|
174
183
|
<div class="log-row__body">
|
|
184
|
+
{#if message.metadata?.source}
|
|
185
|
+
<span class="log-row__source">{message.metadata.source}</span>
|
|
186
|
+
{/if}
|
|
175
187
|
<span class="log-row__node">{message.metadata?.nodeLabel ?? message.nodeId ?? 'log'}</span>
|
|
176
188
|
<span class="log-row__text">{message.content}</span>
|
|
177
189
|
</div>
|
|
@@ -578,7 +590,7 @@
|
|
|
578
590
|
|
|
579
591
|
.log-row {
|
|
580
592
|
display: flex;
|
|
581
|
-
align-items:
|
|
593
|
+
align-items: center;
|
|
582
594
|
gap: var(--fd-space-sm);
|
|
583
595
|
padding: 0.1875rem var(--fd-space-xl);
|
|
584
596
|
border-left: 2px solid var(--fd-info);
|
|
@@ -636,6 +648,20 @@
|
|
|
636
648
|
overflow: hidden;
|
|
637
649
|
}
|
|
638
650
|
|
|
651
|
+
.log-row__source {
|
|
652
|
+
flex-shrink: 0;
|
|
653
|
+
font-size: 0.6rem;
|
|
654
|
+
text-transform: uppercase;
|
|
655
|
+
letter-spacing: 0.06em;
|
|
656
|
+
color: var(--fd-muted-foreground);
|
|
657
|
+
opacity: 0.6;
|
|
658
|
+
background-color: var(--fd-muted);
|
|
659
|
+
border: 1px solid var(--fd-border);
|
|
660
|
+
border-radius: var(--fd-radius-sm);
|
|
661
|
+
padding: 0 0.25rem;
|
|
662
|
+
line-height: 1.4;
|
|
663
|
+
}
|
|
664
|
+
|
|
639
665
|
.log-row__node {
|
|
640
666
|
flex-shrink: 0;
|
|
641
667
|
font-weight: 600;
|
|
@@ -688,11 +714,42 @@
|
|
|
688
714
|
color: var(--fd-border-strong);
|
|
689
715
|
}
|
|
690
716
|
|
|
691
|
-
.system-
|
|
717
|
+
.system-notice__source {
|
|
718
|
+
flex-shrink: 0;
|
|
719
|
+
font-size: 0.6rem;
|
|
720
|
+
text-transform: uppercase;
|
|
721
|
+
letter-spacing: 0.06em;
|
|
692
722
|
color: var(--fd-muted-foreground);
|
|
723
|
+
background-color: var(--fd-muted);
|
|
724
|
+
border: 1px solid var(--fd-border);
|
|
725
|
+
border-radius: var(--fd-radius-sm);
|
|
726
|
+
padding: 0 0.25rem;
|
|
727
|
+
line-height: 1.4;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.system-notice__text {
|
|
693
731
|
line-height: var(--fd-leading-tight);
|
|
694
732
|
}
|
|
695
733
|
|
|
734
|
+
.system-notice--warning {
|
|
735
|
+
color: var(--fd-warning);
|
|
736
|
+
}
|
|
737
|
+
.system-notice--warning :global(.system-notice__icon) {
|
|
738
|
+
color: var(--fd-warning);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.system-notice--error {
|
|
742
|
+
color: var(--fd-error);
|
|
743
|
+
}
|
|
744
|
+
.system-notice--error :global(.system-notice__icon) {
|
|
745
|
+
color: var(--fd-error);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.system-notice--debug {
|
|
749
|
+
color: var(--fd-border-strong);
|
|
750
|
+
opacity: 0.6;
|
|
751
|
+
}
|
|
752
|
+
|
|
696
753
|
.system-notice__timestamp {
|
|
697
754
|
flex-shrink: 0;
|
|
698
755
|
font-size: 0.625rem;
|
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
workflow: Workflow;
|
|
11
11
|
endpointConfig: EndpointConfig;
|
|
12
12
|
isPinned: boolean;
|
|
13
|
-
runLabel?: string;
|
|
14
13
|
/** All executions for the current session, oldest-first */
|
|
15
14
|
executions?: PlaygroundExecution[];
|
|
16
15
|
/** ID of the most-recent execution */
|
|
17
16
|
latestExecutionId?: string | null;
|
|
18
17
|
/** Called with an execution ID to pin it, or null to follow latest */
|
|
19
18
|
onSelectExecution?: (id: string | null) => void;
|
|
19
|
+
/** Increments when new messages arrive — forwarded to PipelineStatus for immediate refresh */
|
|
20
|
+
refreshTrigger?: number;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
let {
|
|
@@ -24,10 +25,10 @@
|
|
|
24
25
|
workflow,
|
|
25
26
|
endpointConfig,
|
|
26
27
|
isPinned,
|
|
27
|
-
runLabel,
|
|
28
28
|
executions = [],
|
|
29
29
|
latestExecutionId = null,
|
|
30
30
|
onSelectExecution,
|
|
31
|
+
refreshTrigger = 0,
|
|
31
32
|
}: Props = $props();
|
|
32
33
|
|
|
33
34
|
let runDropdownOpen = $state(false);
|
|
@@ -74,14 +75,13 @@
|
|
|
74
75
|
onclick={() => (runDropdownOpen = !runDropdownOpen)}
|
|
75
76
|
title="Switch run"
|
|
76
77
|
>
|
|
77
|
-
<span class="pipeline-panel__run-chip-label">{
|
|
78
|
+
<span class="pipeline-panel__run-chip-label">{pipelineId ?? 'Run'}</span>
|
|
78
79
|
<Icon icon={runDropdownOpen ? 'mdi:chevron-up' : 'mdi:chevron-down'} class="pipeline-panel__run-chip-chevron" />
|
|
79
80
|
</button>
|
|
80
81
|
|
|
81
82
|
{#if runDropdownOpen}
|
|
82
83
|
<div class="pipeline-panel__run-popover">
|
|
83
|
-
{#each [...executions].reverse() as exec
|
|
84
|
-
{@const index = executions.length - i}
|
|
84
|
+
{#each [...executions].reverse() as exec (exec.id)}
|
|
85
85
|
{@const isActive = pipelineId === exec.id}
|
|
86
86
|
<button
|
|
87
87
|
type="button"
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
icon={statusIcon(exec.status)}
|
|
97
97
|
class="pipeline-panel__run-status {statusClass(exec.status)}"
|
|
98
98
|
/>
|
|
99
|
-
<span>
|
|
99
|
+
<span class="pipeline-panel__run-id">{exec.id}</span>
|
|
100
100
|
{#if isActive}
|
|
101
101
|
<Icon icon="mdi:check" class="pipeline-panel__run-popover-check" />
|
|
102
102
|
{/if}
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
{#if pipelineId}
|
|
135
135
|
{#key pipelineId}
|
|
136
136
|
<div class="pipeline-panel__content">
|
|
137
|
-
<PipelineStatus {pipelineId} {workflow} {endpointConfig} {
|
|
137
|
+
<PipelineStatus {pipelineId} {workflow} {endpointConfig} runLabel={pipelineId ?? undefined} {refreshTrigger} isEmbedded={true} />
|
|
138
138
|
</div>
|
|
139
139
|
{/key}
|
|
140
140
|
{:else}
|
|
@@ -277,6 +277,16 @@
|
|
|
277
277
|
font-weight: 500;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
.pipeline-panel__run-id {
|
|
281
|
+
flex: 1;
|
|
282
|
+
min-width: 0;
|
|
283
|
+
overflow: hidden;
|
|
284
|
+
text-overflow: ellipsis;
|
|
285
|
+
white-space: nowrap;
|
|
286
|
+
font-family: var(--fd-font-mono, monospace);
|
|
287
|
+
font-size: var(--fd-text-xs);
|
|
288
|
+
}
|
|
289
|
+
|
|
280
290
|
:global(.pipeline-panel__run-popover-check) {
|
|
281
291
|
color: var(--fd-primary) !important;
|
|
282
292
|
margin-left: auto;
|