@flowdrop/flowdrop 1.8.1 → 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 +31 -8
- package/dist/components/PipelineStatus.svelte.d.ts +5 -0
- package/dist/components/WorkflowEditor.svelte +26 -0
- package/dist/components/chat/AIChatPanel.svelte +16 -5
- package/dist/components/playground/ChatPanel.svelte +31 -108
- package/dist/components/playground/ChatPanel.svelte.d.ts +3 -1
- package/dist/components/playground/ExecutionList.svelte +138 -0
- package/dist/components/playground/ExecutionList.svelte.d.ts +10 -0
- package/dist/components/playground/MessageBubble.svelte +281 -156
- package/dist/components/playground/PipelinePanel.svelte +382 -0
- package/dist/components/playground/PipelinePanel.svelte.d.ts +20 -0
- package/dist/components/playground/Playground.svelte +707 -174
- package/dist/components/playground/Playground.svelte.d.ts +6 -0
- 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/globalSave.d.ts +7 -0
- package/dist/services/globalSave.js +5 -1
- 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/pipelinePanelStore.svelte.d.ts +6 -0
- package/dist/stores/pipelinePanelStore.svelte.js +24 -0
- package/dist/stores/playgroundStore.svelte.d.ts +26 -21
- package/dist/stores/playgroundStore.svelte.js +134 -55
- package/dist/svelte-app.js +25 -2
- package/dist/types/playground.d.ts +15 -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
|
}
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
apiClient?: EnhancedFlowDropApiClient;
|
|
22
22
|
baseUrl?: string;
|
|
23
23
|
endpointConfig?: EndpointConfig;
|
|
24
|
+
runLabel?: string;
|
|
25
|
+
/** When true, suppresses breadcrumb and layout events (used inside playground panel) */
|
|
26
|
+
isEmbedded?: boolean;
|
|
27
|
+
/** Increments when new messages arrive — triggers an immediate pipeline data refresh */
|
|
28
|
+
refreshTrigger?: number;
|
|
24
29
|
onActionsReady?: (
|
|
25
30
|
actions: Array<{
|
|
26
31
|
label: string;
|
|
@@ -32,9 +37,13 @@
|
|
|
32
37
|
) => void;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady }: Props =
|
|
40
|
+
let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady, runLabel, isEmbedded = false, refreshTrigger = 0 }: Props =
|
|
36
41
|
$props();
|
|
37
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
|
+
|
|
38
47
|
// Initialize API client if not provided
|
|
39
48
|
// svelte-ignore state_referenced_locally — client created once from props
|
|
40
49
|
const client =
|
|
@@ -213,8 +222,9 @@
|
|
|
213
222
|
};
|
|
214
223
|
});
|
|
215
224
|
|
|
216
|
-
// Send pipeline breadcrumbs to layout when they change
|
|
225
|
+
// Send pipeline breadcrumbs to layout when they change (skip when embedded in playground)
|
|
217
226
|
$effect(() => {
|
|
227
|
+
if (isEmbedded) return;
|
|
218
228
|
if (pipelineStatus && pipelineId && workflow) {
|
|
219
229
|
const sp = m().status.pipeline;
|
|
220
230
|
const breadcrumbs = [
|
|
@@ -239,7 +249,9 @@
|
|
|
239
249
|
icon: 'mdi:source-branch'
|
|
240
250
|
},
|
|
241
251
|
{
|
|
242
|
-
label:
|
|
252
|
+
label: runLabel
|
|
253
|
+
? `${runLabel} – ${pipelineStatus}`
|
|
254
|
+
: sp.pipelineCrumb({ id: pipelineId, status: pipelineStatus }),
|
|
243
255
|
icon: 'mdi:play-circle'
|
|
244
256
|
}
|
|
245
257
|
];
|
|
@@ -279,13 +291,23 @@
|
|
|
279
291
|
|
|
280
292
|
// Note: Interval cleanup is handled by the $effect above.
|
|
281
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
|
+
});
|
|
282
304
|
</script>
|
|
283
305
|
|
|
284
|
-
<div class="pipeline-status-container">
|
|
306
|
+
<div class="pipeline-status-container" class:pipeline-status-container--embedded={isEmbedded}>
|
|
285
307
|
<!-- Workflow Visualization using App component -->
|
|
286
308
|
<App
|
|
287
309
|
{workflow}
|
|
288
|
-
height=
|
|
310
|
+
height={isEmbedded ? '100%' : '100vh'}
|
|
289
311
|
width="100%"
|
|
290
312
|
showNavbar={false}
|
|
291
313
|
disableSidebar={true}
|
|
@@ -310,8 +332,9 @@
|
|
|
310
332
|
background: var(--fd-layout-background, var(--fd-muted));
|
|
311
333
|
}
|
|
312
334
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
background:
|
|
335
|
+
.pipeline-status-container--embedded {
|
|
336
|
+
height: 100%;
|
|
337
|
+
background: var(--fd-muted);
|
|
338
|
+
--fd-layout-background: var(--fd-muted);
|
|
316
339
|
}
|
|
317
340
|
</style>
|
|
@@ -7,6 +7,11 @@ interface Props {
|
|
|
7
7
|
apiClient?: EnhancedFlowDropApiClient;
|
|
8
8
|
baseUrl?: string;
|
|
9
9
|
endpointConfig?: EndpointConfig;
|
|
10
|
+
runLabel?: string;
|
|
11
|
+
/** When true, suppresses breadcrumb and layout events (used inside playground panel) */
|
|
12
|
+
isEmbedded?: boolean;
|
|
13
|
+
/** Increments when new messages arrive — triggers an immediate pipeline data refresh */
|
|
14
|
+
refreshTrigger?: number;
|
|
10
15
|
onActionsReady?: (actions: Array<{
|
|
11
16
|
label: string;
|
|
12
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
|
// ---------------------------------------------------------------------------
|
|
@@ -191,10 +191,22 @@
|
|
|
191
191
|
const msg = displayMessages[messageIndex];
|
|
192
192
|
if (!msg?.commandPreview) return;
|
|
193
193
|
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
const
|
|
194
|
+
// Refuse to run the batch if any command failed to parse. A corrupted batch
|
|
195
|
+
// (e.g. multiline set without """) causes partial execution and can hang the
|
|
196
|
+
// app — rejecting the whole batch is safer than executing the healthy subset.
|
|
197
|
+
const parseErrorCount = msg.commandPreview.filter((c) => c.status === 'error').length;
|
|
198
|
+
if (parseErrorCount > 0) {
|
|
199
|
+
for (const cmd of msg.commandPreview) {
|
|
200
|
+
if (cmd.status === 'pending') {
|
|
201
|
+
cmd.status = 'error';
|
|
202
|
+
cmd.result = 'Batch refused: fix parse errors before executing';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
appendErrorToHistory(
|
|
206
|
+
`Batch was not executed: ${parseErrorCount} command${parseErrorCount > 1 ? 's have' : ' has'} parse errors. Dismiss this batch and ask the AI to provide corrected commands.`
|
|
207
|
+
);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
198
210
|
|
|
199
211
|
const context = getCommandContext();
|
|
200
212
|
if (!context) {
|
|
@@ -277,7 +289,6 @@
|
|
|
277
289
|
}
|
|
278
290
|
|
|
279
291
|
if (
|
|
280
|
-
!hadParseErrors &&
|
|
281
292
|
getBehaviorSettings().chatAutoRetry &&
|
|
282
293
|
workflowId &&
|
|
283
294
|
autoRetryCount < MAX_AUTO_RETRIES
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
getMessages,
|
|
23
23
|
getChatMessages,
|
|
24
24
|
getIsExecuting,
|
|
25
|
+
getCanSendMessage,
|
|
25
26
|
getSessionStatus,
|
|
26
27
|
getCurrentSession
|
|
27
28
|
} from '../../stores/playgroundStore.svelte.js';
|
|
@@ -74,6 +75,8 @@
|
|
|
74
75
|
* @default true
|
|
75
76
|
*/
|
|
76
77
|
compactSystemMessages?: boolean;
|
|
78
|
+
/** Whether log messages are visible — bindable so parent can host the toggle */
|
|
79
|
+
showLogs?: boolean;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
let {
|
|
@@ -88,7 +91,8 @@
|
|
|
88
91
|
showChatInput = true,
|
|
89
92
|
showRunButton = true,
|
|
90
93
|
predefinedMessage,
|
|
91
|
-
compactSystemMessages = true
|
|
94
|
+
compactSystemMessages = true,
|
|
95
|
+
showLogs = $bindable(true)
|
|
92
96
|
}: Props = $props();
|
|
93
97
|
|
|
94
98
|
// Hoist playground branches — states/actions are read 8+ times each in the
|
|
@@ -125,36 +129,21 @@
|
|
|
125
129
|
let inputField = $state<HTMLTextAreaElement>();
|
|
126
130
|
|
|
127
131
|
/**
|
|
128
|
-
* Filter messages based on
|
|
132
|
+
* Filter messages based on local showLogs toggle.
|
|
133
|
+
* The showLogsInline prop is still honoured as the initial hint when explicitly set to false.
|
|
129
134
|
*/
|
|
130
|
-
const displayMessages = $derived(
|
|
135
|
+
const displayMessages = $derived(showLogs ? getMessages() : getChatMessages());
|
|
131
136
|
|
|
132
|
-
|
|
133
|
-
* Track previous message count for detecting new messages.
|
|
134
|
-
* We only want to auto-scroll when NEW messages are added,
|
|
135
|
-
* not when existing messages are updated.
|
|
136
|
-
*/
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
137
138
|
let previousMessageCount = $state(0);
|
|
139
|
+
let userScrolledUp = $state(false);
|
|
138
140
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
* Used to determine if we should auto-scroll when new messages arrive.
|
|
142
|
-
* If user has scrolled up to read previous messages, we don't interrupt them.
|
|
143
|
-
*
|
|
144
|
-
* @param threshold - Pixels from bottom to consider "near bottom"
|
|
145
|
-
* @returns True if user is within threshold of the bottom
|
|
146
|
-
*/
|
|
147
|
-
function isNearBottom(threshold: number = 100): boolean {
|
|
148
|
-
if (!messagesContainer) return true;
|
|
141
|
+
function handleScroll() {
|
|
142
|
+
if (!messagesContainer) return;
|
|
149
143
|
const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
|
|
150
|
-
|
|
144
|
+
userScrolledUp = scrollHeight - scrollTop - clientHeight > 50;
|
|
151
145
|
}
|
|
152
146
|
|
|
153
|
-
/**
|
|
154
|
-
* Check if a form element inside the messages container has focus.
|
|
155
|
-
* When user is interacting with a form (e.g., interrupt prompt),
|
|
156
|
-
* we should not auto-scroll as it disrupts their input.
|
|
157
|
-
*/
|
|
158
147
|
function isFormFocused(): boolean {
|
|
159
148
|
if (!messagesContainer) return false;
|
|
160
149
|
const activeElement = document.activeElement;
|
|
@@ -249,7 +238,7 @@
|
|
|
249
238
|
*/
|
|
250
239
|
function handleSend(): void {
|
|
251
240
|
const trimmedValue = inputValue.trim();
|
|
252
|
-
if (!trimmedValue ||
|
|
241
|
+
if (!trimmedValue || !getCanSendMessage()) {
|
|
253
242
|
return;
|
|
254
243
|
}
|
|
255
244
|
|
|
@@ -332,58 +321,25 @@
|
|
|
332
321
|
$effect(() => {
|
|
333
322
|
const session = getCurrentSession();
|
|
334
323
|
if (session) {
|
|
335
|
-
// Reset to enabled state for new/changed sessions
|
|
336
324
|
runEnabled = true;
|
|
337
|
-
// Clear processed IDs for the new session
|
|
338
325
|
processedEnableRunIds = new Set();
|
|
326
|
+
userScrolledUp = false;
|
|
339
327
|
}
|
|
340
328
|
});
|
|
341
329
|
|
|
342
|
-
/**
|
|
343
|
-
* Smart auto-scroll to bottom when NEW messages are added.
|
|
344
|
-
*
|
|
345
|
-
* Only scrolls if:
|
|
346
|
-
* 1. autoScroll prop is enabled
|
|
347
|
-
* 2. New messages were actually added (not just updates)
|
|
348
|
-
* 3. User is already near the bottom (hasn't scrolled up to read)
|
|
349
|
-
* 4. User is not interacting with a form inside the chat
|
|
350
|
-
*
|
|
351
|
-
* This prevents disruptive scrolling when:
|
|
352
|
-
* - User is reading previous messages
|
|
353
|
-
* - User is filling out an interrupt form
|
|
354
|
-
* - Messages are being updated (e.g., status changes)
|
|
355
|
-
*/
|
|
356
330
|
$effect(() => {
|
|
357
331
|
const currentCount = displayMessages.length;
|
|
358
332
|
|
|
359
|
-
// Skip if auto-scroll is disabled or no container
|
|
360
333
|
if (!autoScroll || !messagesContainer) {
|
|
361
334
|
previousMessageCount = currentCount;
|
|
362
335
|
return;
|
|
363
336
|
}
|
|
364
337
|
|
|
365
|
-
// Check if this is a NEW message (count increased)
|
|
366
338
|
const hasNewMessage = currentCount > previousMessageCount;
|
|
367
|
-
|
|
368
|
-
// Update the tracked count
|
|
369
339
|
previousMessageCount = currentCount;
|
|
370
340
|
|
|
371
|
-
|
|
372
|
-
if (!hasNewMessage) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Don't scroll if user has scrolled up to read previous messages
|
|
377
|
-
if (!isNearBottom()) {
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Don't scroll if user is interacting with a form
|
|
382
|
-
if (isFormFocused()) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
341
|
+
if (!hasNewMessage || userScrolledUp || isFormFocused()) return;
|
|
385
342
|
|
|
386
|
-
// Safe to scroll to bottom
|
|
387
343
|
tick().then(() => {
|
|
388
344
|
if (messagesContainer) {
|
|
389
345
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
@@ -391,50 +347,17 @@
|
|
|
391
347
|
});
|
|
392
348
|
});
|
|
393
349
|
|
|
394
|
-
|
|
395
|
-
* Track previous executing state to detect when execution completes
|
|
396
|
-
*/
|
|
397
|
-
let wasExecuting = $state(false);
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Auto-focus input when execution completes or session becomes ready
|
|
401
|
-
*/
|
|
402
|
-
$effect(() => {
|
|
403
|
-
const currentlyExecuting = getIsExecuting();
|
|
404
|
-
|
|
405
|
-
// Focus input when execution completes (was executing, now not)
|
|
406
|
-
if (wasExecuting && !currentlyExecuting && inputField) {
|
|
407
|
-
tick().then(() => {
|
|
408
|
-
inputField?.focus();
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Update tracking state
|
|
413
|
-
wasExecuting = currentlyExecuting;
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Focus input when session status changes to idle or completed
|
|
418
|
-
*/
|
|
419
|
-
$effect(() => {
|
|
420
|
-
const status = getSessionStatus();
|
|
421
|
-
if ((status === 'idle' || status === 'completed') && inputField && !getIsExecuting()) {
|
|
422
|
-
tick().then(() => {
|
|
423
|
-
inputField?.focus();
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
});
|
|
350
|
+
let wasExecuting = false;
|
|
427
351
|
|
|
428
352
|
/**
|
|
429
|
-
*
|
|
353
|
+
* Auto-focus input when execution completes
|
|
430
354
|
*/
|
|
431
355
|
$effect(() => {
|
|
432
|
-
const
|
|
433
|
-
if (
|
|
434
|
-
tick().then(() =>
|
|
435
|
-
inputField?.focus();
|
|
436
|
-
});
|
|
356
|
+
const nowExecuting = getIsExecuting();
|
|
357
|
+
if (wasExecuting && !nowExecuting && inputField) {
|
|
358
|
+
tick().then(() => inputField?.focus());
|
|
437
359
|
}
|
|
360
|
+
wasExecuting = nowExecuting;
|
|
438
361
|
});
|
|
439
362
|
|
|
440
363
|
/**
|
|
@@ -450,7 +373,7 @@
|
|
|
450
373
|
|
|
451
374
|
<div class="chat-panel">
|
|
452
375
|
<!-- Messages Container -->
|
|
453
|
-
<div class="chat-panel__messages" bind:this={messagesContainer}>
|
|
376
|
+
<div class="chat-panel__messages" bind:this={messagesContainer} onscroll={handleScroll}>
|
|
454
377
|
{#if showWelcome}
|
|
455
378
|
<!-- Welcome State (no session) -->
|
|
456
379
|
<div class="chat-panel__welcome">
|
|
@@ -537,7 +460,6 @@
|
|
|
537
460
|
<!-- Messages -->
|
|
538
461
|
{#each displayMessages as message, index (message.id)}
|
|
539
462
|
{#if isInterruptMessage(message)}
|
|
540
|
-
<!-- Render interrupt inline -->
|
|
541
463
|
{@const interrupt = getInterruptForMessage(message)}
|
|
542
464
|
{#if interrupt}
|
|
543
465
|
<InterruptBubble
|
|
@@ -598,7 +520,7 @@
|
|
|
598
520
|
</div>
|
|
599
521
|
{/if}
|
|
600
522
|
|
|
601
|
-
{#if
|
|
523
|
+
{#if getIsExecuting()}
|
|
602
524
|
<button
|
|
603
525
|
type="button"
|
|
604
526
|
class="chat-panel__stop-btn"
|
|
@@ -613,7 +535,7 @@
|
|
|
613
535
|
type="button"
|
|
614
536
|
class="chat-panel__send-btn"
|
|
615
537
|
onclick={handleSend}
|
|
616
|
-
disabled={!inputValue.trim()}
|
|
538
|
+
disabled={!inputValue.trim() || !getCanSendMessage()}
|
|
617
539
|
title={actions.sendTitle}
|
|
618
540
|
>
|
|
619
541
|
{actions.send}
|
|
@@ -644,13 +566,13 @@
|
|
|
644
566
|
background-color: var(--fd-background);
|
|
645
567
|
}
|
|
646
568
|
|
|
569
|
+
|
|
647
570
|
/* Messages Container - Scrollable area that takes remaining space */
|
|
648
571
|
.chat-panel__messages {
|
|
649
572
|
flex: 1;
|
|
650
573
|
min-height: 0; /* Critical: allows overflow to work in flex container */
|
|
651
574
|
overflow-y: auto;
|
|
652
575
|
padding: var(--fd-space-3xl);
|
|
653
|
-
scroll-behavior: smooth;
|
|
654
576
|
}
|
|
655
577
|
|
|
656
578
|
/* Welcome State */
|
|
@@ -757,7 +679,7 @@
|
|
|
757
679
|
display: flex;
|
|
758
680
|
align-items: flex-end;
|
|
759
681
|
gap: var(--fd-space-md);
|
|
760
|
-
max-width:
|
|
682
|
+
max-width: 760px;
|
|
761
683
|
margin: 0 auto;
|
|
762
684
|
}
|
|
763
685
|
|
|
@@ -822,8 +744,9 @@
|
|
|
822
744
|
}
|
|
823
745
|
|
|
824
746
|
.chat-panel__send-btn:disabled {
|
|
825
|
-
background-color: var(--fd-
|
|
826
|
-
color: var(--fd-
|
|
747
|
+
background-color: var(--fd-foreground);
|
|
748
|
+
color: var(--fd-background);
|
|
749
|
+
opacity: 0.3;
|
|
827
750
|
cursor: not-allowed;
|
|
828
751
|
}
|
|
829
752
|
|
|
@@ -890,7 +813,7 @@
|
|
|
890
813
|
border-radius: var(--fd-radius-lg);
|
|
891
814
|
color: var(--fd-muted-foreground);
|
|
892
815
|
font-size: var(--fd-text-sm);
|
|
893
|
-
max-width:
|
|
816
|
+
max-width: 760px;
|
|
894
817
|
margin: 0 auto;
|
|
895
818
|
}
|
|
896
819
|
|
|
@@ -40,7 +40,9 @@ interface Props {
|
|
|
40
40
|
* @default true
|
|
41
41
|
*/
|
|
42
42
|
compactSystemMessages?: boolean;
|
|
43
|
+
/** Whether log messages are visible — bindable so parent can host the toggle */
|
|
44
|
+
showLogs?: boolean;
|
|
43
45
|
}
|
|
44
|
-
declare const ChatPanel: import("svelte").Component<Props, {}, "">;
|
|
46
|
+
declare const ChatPanel: import("svelte").Component<Props, {}, "showLogs">;
|
|
45
47
|
type ChatPanel = ReturnType<typeof ChatPanel>;
|
|
46
48
|
export default ChatPanel;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Icon from '@iconify/svelte';
|
|
3
|
+
import type { PlaygroundExecution } from '../../types/playground.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
executions: PlaygroundExecution[];
|
|
7
|
+
activeExecutionId: string | null;
|
|
8
|
+
latestExecutionId: string | null;
|
|
9
|
+
onSelect: (executionId: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { executions, activeExecutionId, latestExecutionId, onSelect }: Props = $props();
|
|
13
|
+
|
|
14
|
+
function statusIcon(status: PlaygroundExecution['status']): string {
|
|
15
|
+
if (status === 'completed') return 'mdi:check-circle';
|
|
16
|
+
if (status === 'failed') return 'mdi:alert-circle';
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<div class="execution-list">
|
|
22
|
+
{#each executions as execution (execution.id)}
|
|
23
|
+
<div
|
|
24
|
+
class="execution-list__item"
|
|
25
|
+
class:execution-list__item--active={execution.id === activeExecutionId}
|
|
26
|
+
class:execution-list__item--running={execution.status === 'running'}
|
|
27
|
+
class:execution-list__item--completed={execution.status === 'completed'}
|
|
28
|
+
class:execution-list__item--failed={execution.status === 'failed'}
|
|
29
|
+
role="button"
|
|
30
|
+
tabindex="0"
|
|
31
|
+
onclick={() => onSelect(execution.id)}
|
|
32
|
+
onkeydown={(e) => e.key === 'Enter' && onSelect(execution.id)}
|
|
33
|
+
>
|
|
34
|
+
{#if execution.status === 'running'}
|
|
35
|
+
<span class="execution-list__running-dot" aria-hidden="true"></span>
|
|
36
|
+
{:else if statusIcon(execution.status)}
|
|
37
|
+
<Icon
|
|
38
|
+
icon={statusIcon(execution.status)}
|
|
39
|
+
class="execution-list__status-icon execution-list__status-icon--{execution.status}"
|
|
40
|
+
/>
|
|
41
|
+
{/if}
|
|
42
|
+
<span class="execution-list__label">{execution.id}</span>
|
|
43
|
+
{#if execution.id === latestExecutionId}
|
|
44
|
+
<span class="execution-list__badge">latest</span>
|
|
45
|
+
{/if}
|
|
46
|
+
</div>
|
|
47
|
+
{/each}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
/* Match the visual weight of .playground__session items */
|
|
52
|
+
.execution-list {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
gap: var(--fd-space-3xs);
|
|
56
|
+
padding: 0 var(--fd-space-sm);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.execution-list__item {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: var(--fd-space-xs);
|
|
63
|
+
padding: var(--fd-space-sm) var(--fd-space-md);
|
|
64
|
+
border-radius: var(--fd-radius-md);
|
|
65
|
+
border-left: 3px solid transparent;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
font-size: var(--fd-text-sm);
|
|
68
|
+
color: var(--fd-foreground);
|
|
69
|
+
transition:
|
|
70
|
+
background-color var(--fd-transition-fast),
|
|
71
|
+
border-left-color var(--fd-transition-fast);
|
|
72
|
+
user-select: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.execution-list__item:hover {
|
|
76
|
+
background-color: var(--fd-muted);
|
|
77
|
+
border-left-color: var(--fd-border);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.execution-list__item--active {
|
|
81
|
+
background-color: var(--fd-primary-muted);
|
|
82
|
+
border-left-color: var(--fd-primary);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.execution-list__item--active:hover {
|
|
86
|
+
background-color: var(--fd-primary-muted);
|
|
87
|
+
border-left-color: var(--fd-primary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.execution-list__label {
|
|
91
|
+
flex: 1;
|
|
92
|
+
min-width: 0;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
text-overflow: ellipsis;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.execution-list__item--active .execution-list__label {
|
|
99
|
+
color: var(--fd-primary);
|
|
100
|
+
font-weight: 500;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.execution-list__badge {
|
|
104
|
+
font-size: var(--fd-text-2xs);
|
|
105
|
+
font-weight: 600;
|
|
106
|
+
text-transform: uppercase;
|
|
107
|
+
letter-spacing: 0.04em;
|
|
108
|
+
color: var(--fd-success);
|
|
109
|
+
flex-shrink: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.execution-list__running-dot {
|
|
113
|
+
width: 6px;
|
|
114
|
+
height: 6px;
|
|
115
|
+
border-radius: 50%;
|
|
116
|
+
background-color: var(--fd-success);
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@keyframes pulse {
|
|
122
|
+
0%, 100% { opacity: 1; }
|
|
123
|
+
50% { opacity: 0.4; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
:global(.execution-list__status-icon) {
|
|
127
|
+
flex-shrink: 0;
|
|
128
|
+
font-size: var(--fd-text-sm);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
:global(.execution-list__status-icon--completed) {
|
|
132
|
+
color: var(--fd-success, #22c55e);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
:global(.execution-list__status-icon--failed) {
|
|
136
|
+
color: var(--fd-error, #ef4444);
|
|
137
|
+
}
|
|
138
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PlaygroundExecution } from '../../types/playground.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
executions: PlaygroundExecution[];
|
|
4
|
+
activeExecutionId: string | null;
|
|
5
|
+
latestExecutionId: string | null;
|
|
6
|
+
onSelect: (executionId: string) => void;
|
|
7
|
+
}
|
|
8
|
+
declare const ExecutionList: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ExecutionList = ReturnType<typeof ExecutionList>;
|
|
10
|
+
export default ExecutionList;
|