@flowdrop/flowdrop 1.13.0 → 1.15.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 +41 -21
- 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 +28 -16
- 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 +23 -12
- package/dist/components/interrupt/ChoicePrompt.svelte +5 -1
- package/dist/components/interrupt/InterruptBubble.svelte +4 -5
- package/dist/components/nodes/AtomNode.svelte +280 -0
- package/dist/components/nodes/AtomNode.svelte.d.ts +26 -0
- package/dist/components/playground/ChatBubble.svelte +6 -8
- 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/LogRow.svelte +2 -1
- package/dist/components/playground/MessageBubble.svelte +1 -4
- package/dist/components/playground/MessageCard.svelte +2 -1
- package/dist/components/playground/MessageMarkdown.svelte +15 -5
- package/dist/components/playground/MessageNotice.svelte +2 -1
- package/dist/components/playground/MessageStream.svelte +138 -17
- package/dist/components/playground/MessageStream.svelte.d.ts +5 -0
- package/dist/components/playground/MessageTagChip.svelte +24 -6
- 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 +94 -27
- package/dist/components/playground/PlaygroundStudio.svelte +21 -7
- package/dist/components/playground/pipelineViewUtils.svelte.js +11 -4
- package/dist/helpers/proximityConnect.d.ts +4 -1
- package/dist/helpers/proximityConnect.js +17 -1
- package/dist/openapi/v1/openapi.yaml +6466 -0
- package/dist/playground/mount.js +2 -2
- package/dist/registry/builtinNodes.d.ts +1 -1
- package/dist/registry/builtinNodes.js +13 -0
- package/dist/schemas/v1/workflow.schema.json +86 -3
- package/dist/services/playgroundService.d.ts +23 -4
- package/dist/services/playgroundService.js +22 -9
- package/dist/stores/playgroundStore.svelte.d.ts +29 -2
- package/dist/stores/playgroundStore.svelte.js +120 -35
- package/dist/types/index.d.ts +38 -3
- package/dist/types/playground.d.ts +36 -2
- package/dist/utils/formMerge.d.ts +36 -0
- package/dist/utils/formMerge.js +70 -0
- package/dist/utils/nodeTypes.js +1 -0
- package/package.json +7 -1
|
@@ -2,10 +2,34 @@
|
|
|
2
2
|
import type { KanbanColumnDef } from '../../types/index.js';
|
|
3
3
|
|
|
4
4
|
const DEFAULT_COLUMNS: KanbanColumnDef[] = [
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
{
|
|
6
|
+
key: 'pending',
|
|
7
|
+
label: 'Pending',
|
|
8
|
+
statuses: ['idle', 'pending'],
|
|
9
|
+
icon: 'mdi:clock-outline',
|
|
10
|
+
color: 'var(--fd-muted-foreground)'
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
key: 'in_progress',
|
|
14
|
+
label: 'In Progress',
|
|
15
|
+
statuses: ['running', 'paused', 'interrupted'],
|
|
16
|
+
icon: 'mdi:play-circle-outline',
|
|
17
|
+
color: 'var(--fd-warning)'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
key: 'done',
|
|
21
|
+
label: 'Done',
|
|
22
|
+
statuses: ['completed', 'skipped'],
|
|
23
|
+
icon: 'mdi:check-circle',
|
|
24
|
+
color: 'var(--fd-success)'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: 'failed',
|
|
28
|
+
label: 'Failed',
|
|
29
|
+
statuses: ['failed', 'cancelled'],
|
|
30
|
+
icon: 'mdi:alert-circle',
|
|
31
|
+
color: 'var(--fd-error)'
|
|
32
|
+
}
|
|
9
33
|
];
|
|
10
34
|
</script>
|
|
11
35
|
|
|
@@ -13,7 +37,11 @@
|
|
|
13
37
|
import { onMount } from 'svelte';
|
|
14
38
|
import Icon from '@iconify/svelte';
|
|
15
39
|
import { createPipelineDataFetcher, resolveStatus } from './pipelineViewUtils.svelte.js';
|
|
16
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
getStatusLabel,
|
|
42
|
+
getStatusTextColor,
|
|
43
|
+
getStatusBackgroundColor
|
|
44
|
+
} from '../../utils/nodeStatus.js';
|
|
17
45
|
import type { NodeStatus } from './pipelineViewUtils.svelte.js';
|
|
18
46
|
import type { Workflow, WorkflowNode } from '../../types/index.js';
|
|
19
47
|
import type { EndpointConfig } from '../../config/endpoints.js';
|
|
@@ -27,6 +55,7 @@
|
|
|
27
55
|
|
|
28
56
|
let { pipelineId, workflow, endpointConfig, refreshTrigger = 0 }: Props = $props();
|
|
29
57
|
|
|
58
|
+
// svelte-ignore state_referenced_locally — endpointConfig is consumed once to build the API client; it must be stable
|
|
30
59
|
const fetcher = createPipelineDataFetcher(() => pipelineId, endpointConfig);
|
|
31
60
|
|
|
32
61
|
$effect(() => {
|
|
@@ -88,10 +117,7 @@
|
|
|
88
117
|
style="--col-color: {col.color ?? 'var(--fd-muted-foreground)'}"
|
|
89
118
|
>
|
|
90
119
|
<div class="pipeline-kanban__column-header">
|
|
91
|
-
<Icon
|
|
92
|
-
icon={col.icon ?? 'mdi:circle-outline'}
|
|
93
|
-
class="pipeline-kanban__col-icon"
|
|
94
|
-
/>
|
|
120
|
+
<Icon icon={col.icon ?? 'mdi:circle-outline'} class="pipeline-kanban__col-icon" />
|
|
95
121
|
<span class="pipeline-kanban__col-label">{col.label}</span>
|
|
96
122
|
<span class="pipeline-kanban__col-count">{items.length}</span>
|
|
97
123
|
</div>
|
|
@@ -104,8 +130,11 @@
|
|
|
104
130
|
{#if showStatusPill}
|
|
105
131
|
<span
|
|
106
132
|
class="pipeline-kanban__card-status"
|
|
107
|
-
style="color: {getStatusTextColor(
|
|
108
|
-
|
|
133
|
+
style="color: {getStatusTextColor(
|
|
134
|
+
status
|
|
135
|
+
)}; background-color: {getStatusBackgroundColor(status)}"
|
|
136
|
+
>{getStatusLabel(status)}</span
|
|
137
|
+
>
|
|
109
138
|
{/if}
|
|
110
139
|
</div>
|
|
111
140
|
<span class="pipeline-kanban__card-type">{node.data.metadata.id}</span>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const VIEW_MODE_KEY = 'fd-pipeline-view-mode';
|
|
3
3
|
const BUILTIN_VIEWS = ['graph', 'kanban', 'table'] as const;
|
|
4
4
|
// `string & {}` preserves autocomplete for built-in values while still accepting arbitrary strings from extraViews.
|
|
5
|
-
type ViewMode = typeof BUILTIN_VIEWS[number] | (string & {});
|
|
5
|
+
type ViewMode = (typeof BUILTIN_VIEWS)[number] | (string & {});
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<script lang="ts">
|
|
@@ -177,6 +177,7 @@
|
|
|
177
177
|
class="pipeline-panel__run-popover"
|
|
178
178
|
bind:this={runPopoverEl}
|
|
179
179
|
role="menu"
|
|
180
|
+
tabindex="-1"
|
|
180
181
|
onkeydown={(e) => {
|
|
181
182
|
if (e.key === 'Escape') {
|
|
182
183
|
runDropdownOpen = false;
|
|
@@ -415,6 +416,9 @@
|
|
|
415
416
|
right: 0;
|
|
416
417
|
z-index: 50;
|
|
417
418
|
min-width: 160px;
|
|
419
|
+
max-width: 320px;
|
|
420
|
+
max-height: min(60vh, 420px);
|
|
421
|
+
overflow-y: auto;
|
|
418
422
|
padding: var(--fd-space-xs);
|
|
419
423
|
background-color: var(--fd-background);
|
|
420
424
|
border: 1px solid var(--fd-border);
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
statusData: NodeStatusData | undefined;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// svelte-ignore state_referenced_locally — endpointConfig is consumed once to build the API client; it must be stable
|
|
67
68
|
const fetcher = createPipelineDataFetcher(() => pipelineId, endpointConfig);
|
|
68
69
|
|
|
69
70
|
$effect(() => {
|
|
@@ -137,14 +138,24 @@
|
|
|
137
138
|
{#if expandable}
|
|
138
139
|
<Icon
|
|
139
140
|
icon="mdi:chevron-right"
|
|
140
|
-
class="pipeline-table__chevron {expanded
|
|
141
|
+
class="pipeline-table__chevron {expanded
|
|
142
|
+
? 'pipeline-table__chevron--open'
|
|
143
|
+
: ''}"
|
|
141
144
|
/>
|
|
142
145
|
{/if}
|
|
143
146
|
</td>
|
|
144
|
-
<td class="pipeline-table__td pipeline-table__td--label" title={row.node.data.label}
|
|
145
|
-
|
|
147
|
+
<td class="pipeline-table__td pipeline-table__td--label" title={row.node.data.label}
|
|
148
|
+
>{row.node.data.label}</td
|
|
149
|
+
>
|
|
150
|
+
<td
|
|
151
|
+
class="pipeline-table__td pipeline-table__td--muted"
|
|
152
|
+
title={row.node.data.metadata.id}>{row.node.data.metadata.id}</td
|
|
153
|
+
>
|
|
146
154
|
<td class="pipeline-table__td">
|
|
147
|
-
<span
|
|
155
|
+
<span
|
|
156
|
+
class="pipeline-table__status"
|
|
157
|
+
style="color: {getStatusTextColor(row.status)}"
|
|
158
|
+
>
|
|
148
159
|
<Icon
|
|
149
160
|
icon={STATUS_ICON[row.status] ?? 'mdi:circle-outline'}
|
|
150
161
|
class="pipeline-table__status-icon"
|
|
@@ -152,7 +163,9 @@
|
|
|
152
163
|
{row.status}
|
|
153
164
|
</span>
|
|
154
165
|
</td>
|
|
155
|
-
<td class="pipeline-table__td pipeline-table__td--id" title={row.node.id}
|
|
166
|
+
<td class="pipeline-table__td pipeline-table__td--id" title={row.node.id}
|
|
167
|
+
>{row.node.id}</td
|
|
168
|
+
>
|
|
156
169
|
</tr>
|
|
157
170
|
{#if expanded && expandable}
|
|
158
171
|
<tr class="pipeline-table__detail-row">
|
|
@@ -320,7 +333,8 @@
|
|
|
320
333
|
}
|
|
321
334
|
|
|
322
335
|
.pipeline-table__detail-cell {
|
|
323
|
-
padding: var(--fd-space-sm) var(--fd-space-md) var(--fd-space-sm)
|
|
336
|
+
padding: var(--fd-space-sm) var(--fd-space-md) var(--fd-space-sm)
|
|
337
|
+
calc(1.5rem + var(--fd-space-md));
|
|
324
338
|
border-bottom: 1px solid var(--fd-border);
|
|
325
339
|
}
|
|
326
340
|
|
|
@@ -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,8 +140,10 @@
|
|
|
133
140
|
const sessionId = getCurrentSession()?.id;
|
|
134
141
|
if (sessionId) {
|
|
135
142
|
void playgroundService
|
|
136
|
-
.getMessages(sessionId,
|
|
137
|
-
|
|
143
|
+
.getMessages(sessionId, {
|
|
144
|
+
since: playgroundService.getLastSequenceNumber() ?? undefined
|
|
145
|
+
})
|
|
146
|
+
.then((response) => applyServerResponse(response, sessionId))
|
|
138
147
|
.catch((err) => logger.error('[Playground] Visibility catchup failed:', err));
|
|
139
148
|
}
|
|
140
149
|
}
|
|
@@ -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
|
-
|
|
240
|
-
|
|
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();
|
|
260
|
+
applyServerResponse(response, sessionId);
|
|
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);
|
|
@@ -259,6 +321,11 @@
|
|
|
259
321
|
const sessionName = `Session ${getSessions().length + 1}`;
|
|
260
322
|
const session = await playgroundService.createSession(workflowId, sessionName);
|
|
261
323
|
|
|
324
|
+
// Stop polling the previous (possibly running) session before switching,
|
|
325
|
+
// mirroring handleSelectSession. Otherwise its next poll keeps the old
|
|
326
|
+
// 'running' status alive and the new session's chat input stays disabled.
|
|
327
|
+
playgroundService.stopPolling();
|
|
328
|
+
|
|
262
329
|
if (onSessionNavigate) {
|
|
263
330
|
onSessionNavigate(session.id);
|
|
264
331
|
return;
|
|
@@ -320,8 +387,10 @@
|
|
|
320
387
|
playgroundActions.addMessage(message);
|
|
321
388
|
// Only start polling if not already active — avoids resetting the cursor
|
|
322
389
|
// mid-session and re-fetching messages that are already in the store.
|
|
390
|
+
// Seed from the newest loaded message so polling tails live updates
|
|
391
|
+
// rather than crawling forward from the start of the conversation.
|
|
323
392
|
if (!playgroundService.isPolling()) {
|
|
324
|
-
startPolling(sessionId);
|
|
393
|
+
startPolling(sessionId, true);
|
|
325
394
|
}
|
|
326
395
|
} catch (err) {
|
|
327
396
|
const errorMessage = err instanceof Error ? err.message : 'Failed to send message';
|
|
@@ -358,7 +427,7 @@
|
|
|
358
427
|
|
|
359
428
|
playgroundService.startPolling(
|
|
360
429
|
sessionId,
|
|
361
|
-
(response) => applyServerResponse(response),
|
|
430
|
+
(response) => applyServerResponse(response, sessionId),
|
|
362
431
|
pollingInterval,
|
|
363
432
|
overrideShouldStopPolling ?? config.shouldStopPolling,
|
|
364
433
|
initialSequenceNumber
|
|
@@ -370,11 +439,10 @@
|
|
|
370
439
|
if (!sessionId || isRefreshing) return;
|
|
371
440
|
isRefreshing = true;
|
|
372
441
|
try {
|
|
373
|
-
const response = await playgroundService.getMessages(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
);
|
|
377
|
-
applyServerResponse(response);
|
|
442
|
+
const response = await playgroundService.getMessages(sessionId, {
|
|
443
|
+
since: playgroundService.getLastSequenceNumber() ?? undefined
|
|
444
|
+
});
|
|
445
|
+
applyServerResponse(response, sessionId);
|
|
378
446
|
if (response.sessionStatus === 'running' && !playgroundService.isPolling()) {
|
|
379
447
|
startPolling(sessionId, true);
|
|
380
448
|
}
|
|
@@ -392,11 +460,10 @@
|
|
|
392
460
|
try {
|
|
393
461
|
// Catch up immediately rather than waiting for the next poll interval.
|
|
394
462
|
// Use the service's sequence cursor so we only fetch new messages.
|
|
395
|
-
const response = await playgroundService.getMessages(
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
);
|
|
399
|
-
applyServerResponse(response);
|
|
463
|
+
const response = await playgroundService.getMessages(sessionId, {
|
|
464
|
+
since: playgroundService.getLastSequenceNumber() ?? undefined
|
|
465
|
+
});
|
|
466
|
+
applyServerResponse(response, sessionId);
|
|
400
467
|
} catch (err) {
|
|
401
468
|
logger.error('[Playground] Failed to refresh after interrupt:', err);
|
|
402
469
|
}
|
|
@@ -430,10 +497,7 @@
|
|
|
430
497
|
</div>
|
|
431
498
|
{/if}
|
|
432
499
|
|
|
433
|
-
<div
|
|
434
|
-
class="playground__content"
|
|
435
|
-
bind:this={playgroundContentEl}
|
|
436
|
-
>
|
|
500
|
+
<div class="playground__content" bind:this={playgroundContentEl}>
|
|
437
501
|
{#if getIsLoading() && !getCurrentSession()}
|
|
438
502
|
<div class="playground__loading">
|
|
439
503
|
<Icon icon="mdi:loading" class="playground__loading-icon" />
|
|
@@ -444,11 +508,14 @@
|
|
|
444
508
|
showTimestamps={config.showTimestamps ?? true}
|
|
445
509
|
autoScroll={config.autoScroll ?? true}
|
|
446
510
|
enableMarkdown={config.enableMarkdown ?? true}
|
|
447
|
-
showLogsInline={config.logDisplayMode === 'inline'}
|
|
448
511
|
onInterruptResolved={handleInterruptResolved}
|
|
449
512
|
onCreateSession={getSessions().length === 0 ? handleCreateSession : undefined}
|
|
513
|
+
onLoadOlder={loadOlderMessages}
|
|
450
514
|
/>
|
|
451
515
|
|
|
516
|
+
<!-- Focusable ARIA splitter: keyboard/pointer handlers drive the resize -->
|
|
517
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
518
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
452
519
|
<div
|
|
453
520
|
class="playground__vertical-resizer"
|
|
454
521
|
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,11 +156,15 @@
|
|
|
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;">
|
|
159
170
|
<button
|
|
@@ -178,6 +189,9 @@
|
|
|
178
189
|
/>
|
|
179
190
|
</div>
|
|
180
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 -->
|
|
181
195
|
<div
|
|
182
196
|
class="playground-studio__resizer"
|
|
183
197
|
class:playground-studio__resizer--active={isResizing}
|
|
@@ -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
|
}
|
|
@@ -19,8 +19,11 @@ export interface ProximityEdgeCandidate {
|
|
|
19
19
|
export declare class ProximityConnectHelper {
|
|
20
20
|
/**
|
|
21
21
|
* Get ALL ports (static + dynamic + gateway branches) for a node.
|
|
22
|
+
*
|
|
23
|
+
* Only reads `type` and `data`, so callers can pass a full node or a lighter
|
|
24
|
+
* slice (e.g. a renderer that has metadata + config but no position/measured).
|
|
22
25
|
*/
|
|
23
|
-
static getAllPorts(node: WorkflowNodeType, direction: 'input' | 'output'): NodePort[];
|
|
26
|
+
static getAllPorts(node: Pick<WorkflowNodeType, 'type' | 'data'>, direction: 'input' | 'output'): NodePort[];
|
|
24
27
|
/**
|
|
25
28
|
* Build handle ID in the standard format.
|
|
26
29
|
*/
|
|
@@ -13,12 +13,28 @@ const PROXIMITY_EDGE_CLASS = 'flowdrop--edge--proximity-preview';
|
|
|
13
13
|
export class ProximityConnectHelper {
|
|
14
14
|
/**
|
|
15
15
|
* Get ALL ports (static + dynamic + gateway branches) for a node.
|
|
16
|
+
*
|
|
17
|
+
* Only reads `type` and `data`, so callers can pass a full node or a lighter
|
|
18
|
+
* slice (e.g. a renderer that has metadata + config but no position/measured).
|
|
16
19
|
*/
|
|
17
20
|
static getAllPorts(node, direction) {
|
|
18
21
|
// Static ports from metadata
|
|
19
|
-
|
|
22
|
+
let staticPorts = direction === 'output'
|
|
20
23
|
? (node.data?.metadata?.outputs ?? [])
|
|
21
24
|
: (node.data?.metadata?.inputs ?? []);
|
|
25
|
+
// Atom value-type binding: the bound output port's dataType follows a config
|
|
26
|
+
// field (e.g. Constant's `valueType`), so connection validation matches the
|
|
27
|
+
// type the user actually picked. Derived on read — never stored redundantly.
|
|
28
|
+
if (direction === 'output') {
|
|
29
|
+
const atom = node.data?.metadata?.extensions?.ui?.atom;
|
|
30
|
+
const boundType = atom?.valueTypeKey
|
|
31
|
+
? node.data?.config?.[atom.valueTypeKey]
|
|
32
|
+
: undefined;
|
|
33
|
+
if (boundType) {
|
|
34
|
+
const portId = atom?.outputPortId ?? staticPorts[0]?.id;
|
|
35
|
+
staticPorts = staticPorts.map((p) => (p.id === portId ? { ...p, dataType: boundType } : p));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
22
38
|
// Dynamic ports from config
|
|
23
39
|
const dynamicKey = direction === 'output' ? 'dynamicOutputs' : 'dynamicInputs';
|
|
24
40
|
const rawDynamic = node.data?.config?.[dynamicKey] ?? [];
|