@flowdrop/flowdrop 1.12.0 → 1.14.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/README.md +5 -0
- package/dist/components/ConfigForm.svelte +1 -0
- package/dist/components/ConfigPanel.svelte +7 -1
- package/dist/components/NodeSwapPicker.svelte +5 -1
- package/dist/components/PipelineStatus.svelte +11 -2
- package/dist/components/SchemaForm.svelte +1 -0
- package/dist/components/SettingsPanel.svelte +5 -1
- package/dist/components/WorkflowEditor.svelte +5 -1
- package/dist/components/chat/AIChatPanel.svelte +1 -5
- package/dist/components/form/FormAutocomplete.svelte +69 -15
- package/dist/components/form/FormField.svelte +21 -0
- package/dist/components/form/FormFieldLight.svelte +1 -0
- package/dist/components/interrupt/ChoicePrompt.svelte +5 -1
- package/dist/components/interrupt/InterruptBubble.svelte +75 -17
- package/dist/components/interrupt/InterruptBubble.svelte.d.ts +11 -0
- package/dist/components/playground/ChatBubble.svelte +287 -0
- package/dist/components/playground/ChatBubble.svelte.d.ts +10 -0
- package/dist/components/playground/ChatInput.svelte +11 -5
- package/dist/components/playground/ControlPanel.svelte +42 -29
- package/dist/components/playground/ExecutionConsole.svelte +5 -1
- package/dist/components/playground/ExecutionConsole.svelte.d.ts +2 -0
- package/dist/components/playground/ExecutionList.svelte +7 -2
- package/dist/components/playground/HierarchyTrail.svelte +88 -0
- package/dist/components/playground/HierarchyTrail.svelte.d.ts +7 -0
- package/dist/components/playground/LogRow.svelte +179 -0
- package/dist/components/playground/LogRow.svelte.d.ts +8 -0
- package/dist/components/playground/MessageBubble.stories.svelte +89 -0
- package/dist/components/playground/MessageBubble.svelte +23 -738
- package/dist/components/playground/MessageBubble.svelte.d.ts +3 -11
- package/dist/components/playground/MessageCard.svelte +107 -0
- package/dist/components/playground/MessageCard.svelte.d.ts +10 -0
- package/dist/components/playground/MessageMarkdown.svelte +170 -0
- package/dist/components/playground/MessageMarkdown.svelte.d.ts +7 -0
- package/dist/components/playground/MessageNotice.svelte +121 -0
- package/dist/components/playground/MessageNotice.svelte.d.ts +9 -0
- package/dist/components/playground/MessageStream.svelte +215 -10
- package/dist/components/playground/MessageStream.svelte.d.ts +5 -0
- package/dist/components/playground/MessageTagChip.svelte +117 -0
- package/dist/components/playground/MessageTagChip.svelte.d.ts +7 -0
- package/dist/components/playground/MessageTagStrip.svelte +37 -0
- package/dist/components/playground/MessageTagStrip.svelte.d.ts +7 -0
- package/dist/components/playground/PipelineKanbanView.svelte +40 -11
- package/dist/components/playground/PipelinePanel.svelte +5 -1
- package/dist/components/playground/PipelineTableView.svelte +20 -6
- package/dist/components/playground/Playground.svelte +84 -22
- package/dist/components/playground/PlaygroundStudio.svelte +99 -7
- package/dist/components/playground/messageDisplay.d.ts +19 -0
- package/dist/components/playground/messageDisplay.js +62 -0
- package/dist/components/playground/pipelineViewUtils.svelte.js +11 -4
- package/dist/form/autocomplete.d.ts +1 -0
- package/dist/form/autocomplete.js +1 -0
- package/dist/form/index.d.ts +17 -0
- package/dist/form/index.js +19 -0
- package/dist/messages/defaults.d.ts +5 -0
- package/dist/messages/defaults.js +6 -0
- package/dist/openapi/v1/openapi.yaml +6403 -0
- package/dist/schemas/v1/workflow.schema.json +46 -1
- package/dist/services/categoriesApi.d.ts +2 -1
- package/dist/services/categoriesApi.js +5 -3
- package/dist/services/playgroundService.d.ts +23 -4
- package/dist/services/playgroundService.js +22 -9
- package/dist/services/portConfigApi.d.ts +2 -1
- package/dist/services/portConfigApi.js +5 -3
- package/dist/stores/playgroundStore.svelte.d.ts +22 -1
- package/dist/stores/playgroundStore.svelte.js +109 -32
- package/dist/svelte-app.d.ts +1 -0
- package/dist/svelte-app.js +5 -5
- package/dist/types/index.d.ts +13 -0
- package/dist/types/playground.d.ts +112 -2
- package/dist/types/playground.js +14 -0
- package/package.json +12 -1
|
@@ -33,8 +33,11 @@
|
|
|
33
33
|
getError,
|
|
34
34
|
playgroundActions,
|
|
35
35
|
applyServerResponse,
|
|
36
|
-
getLatestSequenceNumber
|
|
36
|
+
getLatestSequenceNumber,
|
|
37
|
+
getOldestSequenceNumber,
|
|
38
|
+
setHasOlder
|
|
37
39
|
} from '../../stores/playgroundStore.svelte.js';
|
|
40
|
+
import type { PlaygroundMessagesApiResponse } from '../../types/playground.js';
|
|
38
41
|
import { interruptActions } from '../../stores/interruptStore.svelte.js';
|
|
39
42
|
import { logger } from '../../utils/logger.js';
|
|
40
43
|
|
|
@@ -67,6 +70,11 @@
|
|
|
67
70
|
let loadedInitialSessionId = $state<string | undefined>(undefined);
|
|
68
71
|
let autoRunTriggered = $state(false);
|
|
69
72
|
let isRefreshing = $state(false);
|
|
73
|
+
// Monotonic token so a slow session load can't overwrite a newer one when the
|
|
74
|
+
// user switches sessions faster than the network responds (last-load wins).
|
|
75
|
+
let loadToken = 0;
|
|
76
|
+
|
|
77
|
+
const messagePageSize = $derived(config.messagePageSize ?? 50);
|
|
70
78
|
|
|
71
79
|
// Vertical resizer state for the ExecutionConsole ↔ ControlPanel split.
|
|
72
80
|
let playgroundContentEl = $state<HTMLElement | null>(null);
|
|
@@ -90,16 +98,15 @@
|
|
|
90
98
|
}
|
|
91
99
|
});
|
|
92
100
|
|
|
93
|
-
const maxControlPanelHeight = $derived(
|
|
94
|
-
containerHeight ? Math.round(containerHeight * 0.6) : 600
|
|
95
|
-
);
|
|
101
|
+
const maxControlPanelHeight = $derived(containerHeight ? Math.round(containerHeight * 0.6) : 600);
|
|
96
102
|
|
|
97
103
|
function clampControlPanelHeight(h: number): number {
|
|
98
104
|
return Math.min(Math.max(h, 140), maxControlPanelHeight);
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
function handleVerticalResizerPointerDown(e: PointerEvent) {
|
|
102
|
-
if (playgroundContentEl)
|
|
108
|
+
if (playgroundContentEl)
|
|
109
|
+
dragContainerBottom = playgroundContentEl.getBoundingClientRect().bottom;
|
|
103
110
|
isVerticalResizing = true;
|
|
104
111
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
105
112
|
}
|
|
@@ -133,7 +140,9 @@
|
|
|
133
140
|
const sessionId = getCurrentSession()?.id;
|
|
134
141
|
if (sessionId) {
|
|
135
142
|
void playgroundService
|
|
136
|
-
.getMessages(sessionId,
|
|
143
|
+
.getMessages(sessionId, {
|
|
144
|
+
since: playgroundService.getLastSequenceNumber() ?? undefined
|
|
145
|
+
})
|
|
137
146
|
.then((response) => applyServerResponse(response))
|
|
138
147
|
.catch((err) => logger.error('[Playground] Visibility catchup failed:', err));
|
|
139
148
|
}
|
|
@@ -231,26 +240,79 @@
|
|
|
231
240
|
async function loadSession(sessionId: string): Promise<void> {
|
|
232
241
|
playgroundActions.setLoading(true);
|
|
233
242
|
playgroundActions.setError(null);
|
|
243
|
+
const token = ++loadToken;
|
|
234
244
|
|
|
235
245
|
try {
|
|
236
246
|
const session = await playgroundService.getSession(sessionId);
|
|
247
|
+
if (token !== loadToken) return; // a newer session load superseded us
|
|
237
248
|
playgroundActions.setCurrentSession(session);
|
|
238
249
|
|
|
239
|
-
|
|
250
|
+
// Load only the most recent page; older messages load on demand when the
|
|
251
|
+
// user scrolls up (loadOlderMessages). Clear right before applying the
|
|
252
|
+
// fresh page — not before the await — so switching sessions doesn't blank
|
|
253
|
+
// the view for the duration of the fetch.
|
|
254
|
+
const response = await playgroundService.getMessages(sessionId, {
|
|
255
|
+
latest: true,
|
|
256
|
+
limit: messagePageSize
|
|
257
|
+
});
|
|
258
|
+
if (token !== loadToken) return;
|
|
259
|
+
playgroundActions.clearMessages();
|
|
240
260
|
applyServerResponse(response);
|
|
261
|
+
setHasOlder(deriveHasOlder(response));
|
|
241
262
|
|
|
242
263
|
if (session.status !== 'idle') {
|
|
264
|
+
// Seed polling from the newest loaded message so it tails live updates
|
|
265
|
+
// instead of crawling forward from the start of the conversation.
|
|
243
266
|
startPolling(sessionId, true);
|
|
244
267
|
}
|
|
245
268
|
} catch (err) {
|
|
269
|
+
if (token !== loadToken) return; // don't surface a superseded load's error
|
|
246
270
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load session';
|
|
247
271
|
playgroundActions.setError(errorMessage);
|
|
248
272
|
logger.error('Failed to load session:', err);
|
|
249
273
|
} finally {
|
|
250
|
-
playgroundActions.setLoading(false);
|
|
274
|
+
if (token === loadToken) playgroundActions.setLoading(false);
|
|
251
275
|
}
|
|
252
276
|
}
|
|
253
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Load the page of messages immediately older than the oldest one currently
|
|
280
|
+
* shown. Triggered by scroll-up in MessageStream, which serializes calls and
|
|
281
|
+
* owns the in-flight/anchoring state. Bypasses applyServerResponse so a
|
|
282
|
+
* historical fetch never disturbs the live polling cursor or pipeline view.
|
|
283
|
+
*/
|
|
284
|
+
async function loadOlderMessages(): Promise<void> {
|
|
285
|
+
const sessionId = getCurrentSession()?.id;
|
|
286
|
+
const before = getOldestSequenceNumber();
|
|
287
|
+
if (!sessionId || before === null) return;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const response = await playgroundService.getMessages(sessionId, {
|
|
291
|
+
before,
|
|
292
|
+
limit: messagePageSize
|
|
293
|
+
});
|
|
294
|
+
// The session may have changed while the fetch was in flight — don't
|
|
295
|
+
// splice an old session's page into the new session's store.
|
|
296
|
+
if (getCurrentSession()?.id !== sessionId) return;
|
|
297
|
+
if (response.data && response.data.length > 0) {
|
|
298
|
+
playgroundActions.addMessages(response.data);
|
|
299
|
+
}
|
|
300
|
+
setHasOlder(deriveHasOlder(response));
|
|
301
|
+
} catch (err) {
|
|
302
|
+
logger.error('[Playground] Failed to load older messages:', err);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Whether older messages remain after a backward-pagination response. Prefer
|
|
308
|
+
* the server's explicit `hasOlder` flag; fall back to inferring from page
|
|
309
|
+
* fullness for backends that haven't adopted the field yet.
|
|
310
|
+
*/
|
|
311
|
+
function deriveHasOlder(response: PlaygroundMessagesApiResponse): boolean {
|
|
312
|
+
if (typeof response.hasOlder === 'boolean') return response.hasOlder;
|
|
313
|
+
return (response.data?.length ?? 0) >= messagePageSize;
|
|
314
|
+
}
|
|
315
|
+
|
|
254
316
|
async function handleCreateSession(): Promise<void> {
|
|
255
317
|
playgroundActions.setLoading(true);
|
|
256
318
|
playgroundActions.setError(null);
|
|
@@ -320,8 +382,10 @@
|
|
|
320
382
|
playgroundActions.addMessage(message);
|
|
321
383
|
// Only start polling if not already active — avoids resetting the cursor
|
|
322
384
|
// mid-session and re-fetching messages that are already in the store.
|
|
385
|
+
// Seed from the newest loaded message so polling tails live updates
|
|
386
|
+
// rather than crawling forward from the start of the conversation.
|
|
323
387
|
if (!playgroundService.isPolling()) {
|
|
324
|
-
startPolling(sessionId);
|
|
388
|
+
startPolling(sessionId, true);
|
|
325
389
|
}
|
|
326
390
|
} catch (err) {
|
|
327
391
|
const errorMessage = err instanceof Error ? err.message : 'Failed to send message';
|
|
@@ -370,10 +434,9 @@
|
|
|
370
434
|
if (!sessionId || isRefreshing) return;
|
|
371
435
|
isRefreshing = true;
|
|
372
436
|
try {
|
|
373
|
-
const response = await playgroundService.getMessages(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
);
|
|
437
|
+
const response = await playgroundService.getMessages(sessionId, {
|
|
438
|
+
since: playgroundService.getLastSequenceNumber() ?? undefined
|
|
439
|
+
});
|
|
377
440
|
applyServerResponse(response);
|
|
378
441
|
if (response.sessionStatus === 'running' && !playgroundService.isPolling()) {
|
|
379
442
|
startPolling(sessionId, true);
|
|
@@ -392,10 +455,9 @@
|
|
|
392
455
|
try {
|
|
393
456
|
// Catch up immediately rather than waiting for the next poll interval.
|
|
394
457
|
// Use the service's sequence cursor so we only fetch new messages.
|
|
395
|
-
const response = await playgroundService.getMessages(
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
);
|
|
458
|
+
const response = await playgroundService.getMessages(sessionId, {
|
|
459
|
+
since: playgroundService.getLastSequenceNumber() ?? undefined
|
|
460
|
+
});
|
|
399
461
|
applyServerResponse(response);
|
|
400
462
|
} catch (err) {
|
|
401
463
|
logger.error('[Playground] Failed to refresh after interrupt:', err);
|
|
@@ -430,10 +492,7 @@
|
|
|
430
492
|
</div>
|
|
431
493
|
{/if}
|
|
432
494
|
|
|
433
|
-
<div
|
|
434
|
-
class="playground__content"
|
|
435
|
-
bind:this={playgroundContentEl}
|
|
436
|
-
>
|
|
495
|
+
<div class="playground__content" bind:this={playgroundContentEl}>
|
|
437
496
|
{#if getIsLoading() && !getCurrentSession()}
|
|
438
497
|
<div class="playground__loading">
|
|
439
498
|
<Icon icon="mdi:loading" class="playground__loading-icon" />
|
|
@@ -444,11 +503,14 @@
|
|
|
444
503
|
showTimestamps={config.showTimestamps ?? true}
|
|
445
504
|
autoScroll={config.autoScroll ?? true}
|
|
446
505
|
enableMarkdown={config.enableMarkdown ?? true}
|
|
447
|
-
showLogsInline={config.logDisplayMode === 'inline'}
|
|
448
506
|
onInterruptResolved={handleInterruptResolved}
|
|
449
507
|
onCreateSession={getSessions().length === 0 ? handleCreateSession : undefined}
|
|
508
|
+
onLoadOlder={loadOlderMessages}
|
|
450
509
|
/>
|
|
451
510
|
|
|
511
|
+
<!-- Focusable ARIA splitter: keyboard/pointer handlers drive the resize -->
|
|
512
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
513
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
452
514
|
<div
|
|
453
515
|
class="playground__vertical-resizer"
|
|
454
516
|
class:playground__vertical-resizer--active={isVerticalResizing}
|
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
import Icon from '@iconify/svelte';
|
|
4
4
|
import Playground from './Playground.svelte';
|
|
5
5
|
import PipelinePanel from './PipelinePanel.svelte';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getPipelinePanelOpen,
|
|
8
|
+
pipelinePanelActions
|
|
9
|
+
} from '../../stores/pipelinePanelStore.svelte.js';
|
|
7
10
|
import {
|
|
8
11
|
getActiveExecutionId,
|
|
9
12
|
getPinnedExecutionId,
|
|
10
13
|
getLatestExecutionId,
|
|
11
14
|
getPipelineRefreshTrigger,
|
|
12
|
-
|
|
13
|
-
playgroundActions
|
|
15
|
+
getSelectableExecutions,
|
|
16
|
+
playgroundActions
|
|
14
17
|
} from '../../stores/playgroundStore.svelte.js';
|
|
15
18
|
import { setEndpointConfig, workflowApi } from '../../services/api.js';
|
|
16
19
|
import { logger } from '../../utils/logger.js';
|
|
@@ -57,14 +60,17 @@
|
|
|
57
60
|
initialPipelineWidth = 500,
|
|
58
61
|
onSessionNavigate,
|
|
59
62
|
onClose,
|
|
60
|
-
extraPipelineViews = []
|
|
63
|
+
extraPipelineViews = []
|
|
61
64
|
}: Props = $props();
|
|
62
65
|
|
|
66
|
+
// svelte-ignore state_referenced_locally — seed mutable state from the prop's initial value; workflow may load asynchronously below
|
|
63
67
|
let resolvedWorkflow = $state<Workflow | null>(workflowProp ?? null);
|
|
68
|
+
// svelte-ignore state_referenced_locally — initial loading flag derived from whether a workflow was provided up front
|
|
64
69
|
let workflowLoading = $state(workflowProp === undefined);
|
|
65
70
|
let workflowError = $state<string | null>(null);
|
|
66
71
|
|
|
67
72
|
let splitEl = $state<HTMLElement | null>(null);
|
|
73
|
+
// svelte-ignore state_referenced_locally — seed mutable width from the initial prop; it changes as the user drags the resizer
|
|
68
74
|
let pipelineWidth = $state(initialPipelineWidth);
|
|
69
75
|
let isResizing = $state(false);
|
|
70
76
|
let containerWidth = $state(0);
|
|
@@ -100,7 +106,8 @@
|
|
|
100
106
|
|
|
101
107
|
async function loadWorkflow(): Promise<void> {
|
|
102
108
|
if (!endpointConfig) {
|
|
103
|
-
workflowError =
|
|
109
|
+
workflowError =
|
|
110
|
+
'Provide a workflow prop or an endpointConfig so the workflow can be fetched.';
|
|
104
111
|
workflowLoading = false;
|
|
105
112
|
return;
|
|
106
113
|
}
|
|
@@ -149,13 +156,26 @@
|
|
|
149
156
|
}
|
|
150
157
|
</script>
|
|
151
158
|
|
|
152
|
-
<div
|
|
159
|
+
<div
|
|
160
|
+
class="playground-studio"
|
|
161
|
+
class:playground-studio--resizing={isResizing}
|
|
162
|
+
style="--playground-studio-min-chat-width: {minChatWidth}px"
|
|
163
|
+
>
|
|
153
164
|
<div class="playground-studio__panes" bind:this={splitEl}>
|
|
154
165
|
{#if getPipelinePanelOpen() && resolvedWorkflow && endpointConfig}
|
|
155
166
|
{@const activeId = getActiveExecutionId()}
|
|
156
|
-
{@const executions =
|
|
167
|
+
{@const executions = getSelectableExecutions()}
|
|
157
168
|
|
|
158
169
|
<div class="playground-studio__pipeline" style="width: {pipelineWidth}px;">
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
class="playground-studio__back-to-chat"
|
|
173
|
+
aria-label="Back to chat"
|
|
174
|
+
onclick={pipelinePanelActions.toggle}
|
|
175
|
+
>
|
|
176
|
+
<Icon icon="mdi:arrow-left" aria-hidden="true" />
|
|
177
|
+
<span>Back to chat</span>
|
|
178
|
+
</button>
|
|
159
179
|
<PipelinePanel
|
|
160
180
|
pipelineId={activeId}
|
|
161
181
|
workflow={resolvedWorkflow}
|
|
@@ -169,6 +189,9 @@
|
|
|
169
189
|
/>
|
|
170
190
|
</div>
|
|
171
191
|
|
|
192
|
+
<!-- Focusable ARIA splitter: keyboard/pointer handlers drive the resize -->
|
|
193
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
194
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
172
195
|
<div
|
|
173
196
|
class="playground-studio__resizer"
|
|
174
197
|
class:playground-studio__resizer--active={isResizing}
|
|
@@ -254,6 +277,13 @@
|
|
|
254
277
|
.playground-studio__pipeline {
|
|
255
278
|
overflow: hidden;
|
|
256
279
|
flex-shrink: 0;
|
|
280
|
+
position: relative;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* Mobile-only "back to chat" affordance. Hidden on wider viewports where
|
|
284
|
+
the ControlPanel's pipeline toggle remains reachable. */
|
|
285
|
+
.playground-studio__back-to-chat {
|
|
286
|
+
display: none;
|
|
257
287
|
}
|
|
258
288
|
|
|
259
289
|
/* Drag handle between the two panes */
|
|
@@ -375,4 +405,66 @@
|
|
|
375
405
|
.playground-studio__retry-btn:hover {
|
|
376
406
|
background-color: var(--fd-primary-hover);
|
|
377
407
|
}
|
|
408
|
+
|
|
409
|
+
/* ============================================================
|
|
410
|
+
Mobile layout (< 768px)
|
|
411
|
+
Switch from side-by-side panes to one-at-a-time fullscreen.
|
|
412
|
+
The pipeline panel, when open, covers the chat. Users toggle
|
|
413
|
+
between them via the pipeline panel button. The resizer is
|
|
414
|
+
hidden — at this width there's nothing to resize.
|
|
415
|
+
============================================================ */
|
|
416
|
+
@media (max-width: 768px) {
|
|
417
|
+
.playground-studio__pipeline {
|
|
418
|
+
/* Override the JS-driven width — take the whole row */
|
|
419
|
+
width: 100% !important;
|
|
420
|
+
flex: 1 1 auto;
|
|
421
|
+
display: flex;
|
|
422
|
+
flex-direction: column;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.playground-studio__resizer {
|
|
426
|
+
display: none;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* When pipeline is open (chat is NOT solo), hide chat to give the
|
|
430
|
+
pipeline the full viewport. When pipeline closes, chat goes back
|
|
431
|
+
to full-width via the existing --solo class. */
|
|
432
|
+
.playground-studio__chat:not(.playground-studio__chat--solo) {
|
|
433
|
+
display: none;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.playground-studio__back-to-chat {
|
|
437
|
+
display: inline-flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
gap: var(--fd-space-2xs);
|
|
440
|
+
align-self: flex-start;
|
|
441
|
+
margin: var(--fd-space-xs);
|
|
442
|
+
padding: var(--fd-space-2xs) var(--fd-space-md);
|
|
443
|
+
min-height: 2.5rem;
|
|
444
|
+
font-size: var(--fd-text-sm);
|
|
445
|
+
font-weight: 500;
|
|
446
|
+
font-family: inherit;
|
|
447
|
+
color: var(--fd-foreground);
|
|
448
|
+
background-color: var(--fd-card);
|
|
449
|
+
border: 1px solid var(--fd-border);
|
|
450
|
+
border-radius: var(--fd-radius-md);
|
|
451
|
+
cursor: pointer;
|
|
452
|
+
transition: background-color var(--fd-transition-fast);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.playground-studio__back-to-chat:hover {
|
|
456
|
+
background-color: var(--fd-muted);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.playground-studio__back-to-chat:focus-visible {
|
|
460
|
+
outline: 2px solid var(--fd-ring);
|
|
461
|
+
outline-offset: 2px;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
@media (prefers-reduced-motion: reduce) {
|
|
466
|
+
.playground-studio__back-to-chat {
|
|
467
|
+
transition: none;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
378
470
|
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatters / icon maps / label resolver for the message layout
|
|
3
|
+
* components. Pure functions only — i18n strings come in via the `roles`
|
|
4
|
+
* argument; the helper has no runtime dependency on the messages context.
|
|
5
|
+
*/
|
|
6
|
+
import type { PlaygroundMessage, PlaygroundMessageLevel, PlaygroundMessageRole } from '../../types/playground.js';
|
|
7
|
+
import type { Messages } from '../../messages/types.js';
|
|
8
|
+
export type RoleLabels = Messages['playground']['roles'];
|
|
9
|
+
export declare function formatTimestamp(timestamp: string): string;
|
|
10
|
+
export declare function formatDuration(ms: number): string;
|
|
11
|
+
export declare function getLogLevelIcon(level: PlaygroundMessageLevel | undefined): string;
|
|
12
|
+
export declare function getRoleIcon(role: PlaygroundMessageRole): string;
|
|
13
|
+
/**
|
|
14
|
+
* Localised author label. Backend-supplied overrides win:
|
|
15
|
+
* - user → metadata.userName (display name)
|
|
16
|
+
* - log → metadata.nodeLabel (human-readable node label)
|
|
17
|
+
* Anything else returns the role's i18n default.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getRoleLabel(message: Pick<PlaygroundMessage, 'role' | 'metadata'>, roles: RoleLabels): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatters / icon maps / label resolver for the message layout
|
|
3
|
+
* components. Pure functions only — i18n strings come in via the `roles`
|
|
4
|
+
* argument; the helper has no runtime dependency on the messages context.
|
|
5
|
+
*/
|
|
6
|
+
export function formatTimestamp(timestamp) {
|
|
7
|
+
return new Date(timestamp).toLocaleTimeString('en-US', {
|
|
8
|
+
hour12: false,
|
|
9
|
+
hour: '2-digit',
|
|
10
|
+
minute: '2-digit',
|
|
11
|
+
second: '2-digit'
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function formatDuration(ms) {
|
|
15
|
+
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(2)}s`;
|
|
16
|
+
}
|
|
17
|
+
export function getLogLevelIcon(level) {
|
|
18
|
+
switch (level) {
|
|
19
|
+
case 'error':
|
|
20
|
+
return 'mdi:alert-circle';
|
|
21
|
+
case 'warning':
|
|
22
|
+
return 'mdi:alert';
|
|
23
|
+
case 'debug':
|
|
24
|
+
return 'mdi:bug';
|
|
25
|
+
default:
|
|
26
|
+
return 'mdi:information';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function getRoleIcon(role) {
|
|
30
|
+
switch (role) {
|
|
31
|
+
case 'user':
|
|
32
|
+
return 'mdi:account';
|
|
33
|
+
case 'assistant':
|
|
34
|
+
return 'mdi:robot';
|
|
35
|
+
case 'system':
|
|
36
|
+
return 'mdi:cog';
|
|
37
|
+
case 'log':
|
|
38
|
+
return 'mdi:console';
|
|
39
|
+
default:
|
|
40
|
+
return 'mdi:message';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Localised author label. Backend-supplied overrides win:
|
|
45
|
+
* - user → metadata.userName (display name)
|
|
46
|
+
* - log → metadata.nodeLabel (human-readable node label)
|
|
47
|
+
* Anything else returns the role's i18n default.
|
|
48
|
+
*/
|
|
49
|
+
export function getRoleLabel(message, roles) {
|
|
50
|
+
switch (message.role) {
|
|
51
|
+
case 'user':
|
|
52
|
+
return message.metadata?.userName ?? roles.you;
|
|
53
|
+
case 'assistant':
|
|
54
|
+
return roles.assistant;
|
|
55
|
+
case 'system':
|
|
56
|
+
return roles.system;
|
|
57
|
+
case 'log':
|
|
58
|
+
return message.metadata?.nodeLabel ?? roles.log;
|
|
59
|
+
default:
|
|
60
|
+
return roles.message;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { EnhancedFlowDropApiClient } from '../../api/enhanced-client.js';
|
|
2
2
|
import { logger } from '../../utils/logger.js';
|
|
3
3
|
const KNOWN_STATUSES = new Set([
|
|
4
|
-
'idle',
|
|
5
|
-
'
|
|
4
|
+
'idle',
|
|
5
|
+
'pending',
|
|
6
|
+
'running',
|
|
7
|
+
'paused',
|
|
8
|
+
'interrupted',
|
|
9
|
+
'completed',
|
|
10
|
+
'skipped',
|
|
11
|
+
'failed',
|
|
12
|
+
'cancelled'
|
|
6
13
|
]);
|
|
7
14
|
export function resolveStatus(raw) {
|
|
8
15
|
if (!raw)
|
|
@@ -36,7 +43,7 @@ export function createPipelineDataFetcher(getPipelineId, endpointConfig) {
|
|
|
36
43
|
status: info.status,
|
|
37
44
|
last_executed: info.last_executed,
|
|
38
45
|
execution_time: info.execution_time,
|
|
39
|
-
error: info.error
|
|
46
|
+
error: info.error
|
|
40
47
|
};
|
|
41
48
|
}
|
|
42
49
|
nodeStatusMap = map;
|
|
@@ -48,7 +55,7 @@ export function createPipelineDataFetcher(getPipelineId, endpointConfig) {
|
|
|
48
55
|
label: col.label,
|
|
49
56
|
statuses: col.statuses,
|
|
50
57
|
icon: col.icon,
|
|
51
|
-
color: col.color
|
|
58
|
+
color: col.color
|
|
52
59
|
}));
|
|
53
60
|
}
|
|
54
61
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FormAutocomplete } from '../components/form/FormAutocomplete.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FormAutocomplete } from '../components/form/FormAutocomplete.svelte';
|
package/dist/form/index.d.ts
CHANGED
|
@@ -75,3 +75,20 @@ export type { FieldSchema, FieldType, FieldFormat, FieldOption, OneOfItem, Schem
|
|
|
75
75
|
export { isFieldOptionArray, isOneOfArray, normalizeOptions, oneOfToOptions, getSchemaOptions } from '../components/form/types.js';
|
|
76
76
|
export { fieldComponentRegistry, hiddenFieldMatcher, checkboxGroupMatcher, enumSelectMatcher, textareaMatcher, rangeMatcher, textFieldMatcher, numberFieldMatcher, toggleMatcher, selectOptionsMatcher, arrayMatcher } from './fieldRegistry.js';
|
|
77
77
|
export type { FieldComponentProps, FieldMatcher, FieldMatcherRegistration, FieldComponent, FieldComponentRegistration } from './fieldRegistry.js';
|
|
78
|
+
/**
|
|
79
|
+
* Use with Svelte's `getContext` inside custom field components registered
|
|
80
|
+
* via `fieldComponentRegistry` to read sibling form field values.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```svelte
|
|
84
|
+
* <script lang="ts">
|
|
85
|
+
* import { getContext } from 'svelte';
|
|
86
|
+
* import { FORM_VALUES_KEY, type FormValuesGetter } from '@flowdrop/flowdrop/form';
|
|
87
|
+
*
|
|
88
|
+
* const getFormValues = getContext<FormValuesGetter | undefined>(FORM_VALUES_KEY);
|
|
89
|
+
* const account = $derived(getFormValues?.()['account']);
|
|
90
|
+
* </script>
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export declare const FORM_VALUES_KEY: "flowdrop:getFormValues";
|
|
94
|
+
export type FormValuesGetter = () => Record<string, unknown>;
|
package/dist/form/index.js
CHANGED
|
@@ -89,3 +89,22 @@ export {
|
|
|
89
89
|
fieldComponentRegistry,
|
|
90
90
|
// Built-in matchers for custom components
|
|
91
91
|
hiddenFieldMatcher, checkboxGroupMatcher, enumSelectMatcher, textareaMatcher, rangeMatcher, textFieldMatcher, numberFieldMatcher, toggleMatcher, selectOptionsMatcher, arrayMatcher } from './fieldRegistry.js';
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Context keys (for custom field components)
|
|
94
|
+
// ============================================================================
|
|
95
|
+
/**
|
|
96
|
+
* Use with Svelte's `getContext` inside custom field components registered
|
|
97
|
+
* via `fieldComponentRegistry` to read sibling form field values.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```svelte
|
|
101
|
+
* <script lang="ts">
|
|
102
|
+
* import { getContext } from 'svelte';
|
|
103
|
+
* import { FORM_VALUES_KEY, type FormValuesGetter } from '@flowdrop/flowdrop/form';
|
|
104
|
+
*
|
|
105
|
+
* const getFormValues = getContext<FormValuesGetter | undefined>(FORM_VALUES_KEY);
|
|
106
|
+
* const account = $derived(getFormValues?.()['account']);
|
|
107
|
+
* </script>
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export const FORM_VALUES_KEY = 'flowdrop:getFormValues';
|
|
@@ -310,6 +310,11 @@ export declare const defaultMessages: {
|
|
|
310
310
|
}) => string;
|
|
311
311
|
readonly executionDuration: "Execution duration";
|
|
312
312
|
};
|
|
313
|
+
readonly messageAnnotations: {
|
|
314
|
+
readonly hierarchyOf: ({ path }: {
|
|
315
|
+
path: string;
|
|
316
|
+
}) => string;
|
|
317
|
+
};
|
|
313
318
|
readonly sessions: {
|
|
314
319
|
readonly header: "Sessions";
|
|
315
320
|
readonly newSession: "New Session";
|
|
@@ -288,6 +288,12 @@ export const defaultMessages = {
|
|
|
288
288
|
nodeId: ({ id }) => `Node ID: ${id}`,
|
|
289
289
|
executionDuration: 'Execution duration'
|
|
290
290
|
},
|
|
291
|
+
// ARIA labels for message annotations. The hierarchy trail names the
|
|
292
|
+
// actual path so AT users hear "From: ForEach Loop / Greeter" rather
|
|
293
|
+
// than a generic "hierarchy".
|
|
294
|
+
messageAnnotations: {
|
|
295
|
+
hierarchyOf: ({ path }) => `From: ${path}`
|
|
296
|
+
},
|
|
291
297
|
sessions: {
|
|
292
298
|
header: 'Sessions',
|
|
293
299
|
newSession: 'New Session',
|