@d34dman/flowdrop 0.0.33 → 0.0.35
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/components/WorkflowEditor.svelte +22 -0
- package/dist/components/playground/ChatPanel.svelte +6 -32
- package/dist/components/playground/Playground.svelte +3 -0
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -0
- package/dist/helpers/workflowEditorHelper.d.ts +32 -3
- package/dist/helpers/workflowEditorHelper.js +57 -6
- package/dist/stores/playgroundStore.d.ts +3 -0
- package/dist/stores/playgroundStore.js +41 -3
- package/dist/styles/base.css +9 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/playground.d.ts +16 -2
- package/dist/utils/connections.d.ts +46 -0
- package/dist/utils/connections.js +91 -0
- package/package.json +1 -1
|
@@ -620,4 +620,26 @@
|
|
|
620
620
|
stroke: var(--flowdrop-edge-data-color-selected);
|
|
621
621
|
stroke-width: 2;
|
|
622
622
|
}
|
|
623
|
+
|
|
624
|
+
/* Loopback Edge: Dashed gray line for loop iteration connections */
|
|
625
|
+
:global(.flowdrop--edge--loopback path.svelte-flow__edge-path) {
|
|
626
|
+
stroke: var(--flowdrop-edge-loopback-color);
|
|
627
|
+
stroke-width: var(--flowdrop-edge-loopback-width);
|
|
628
|
+
stroke-dasharray: var(--flowdrop-edge-loopback-dasharray);
|
|
629
|
+
opacity: var(--flowdrop-edge-loopback-opacity);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
:global(.flowdrop--edge--loopback:hover path.svelte-flow__edge-path) {
|
|
633
|
+
stroke: var(--flowdrop-edge-loopback-color-hover);
|
|
634
|
+
stroke-width: var(--flowdrop-edge-loopback-width-hover);
|
|
635
|
+
opacity: 1;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
:global(.flowdrop--edge--loopback.selected path.svelte-flow__edge-path) {
|
|
639
|
+
stroke: var(--flowdrop-edge-loopback-color-selected);
|
|
640
|
+
stroke-width: var(--flowdrop-edge-loopback-width-hover);
|
|
641
|
+
stroke-dasharray: var(--flowdrop-edge-loopback-dasharray);
|
|
642
|
+
filter: drop-shadow(0 0 3px rgba(139, 92, 246, 0.4));
|
|
643
|
+
opacity: 1;
|
|
644
|
+
}
|
|
623
645
|
</style>
|
|
@@ -240,11 +240,6 @@
|
|
|
240
240
|
onkeydown={handleKeydown}
|
|
241
241
|
oninput={handleInput}
|
|
242
242
|
></textarea>
|
|
243
|
-
|
|
244
|
-
<!-- Attachment button placeholder -->
|
|
245
|
-
<button type="button" class="chat-panel__attachment-btn" title="Attach file" disabled>
|
|
246
|
-
<Icon icon="mdi:image-outline" />
|
|
247
|
-
</button>
|
|
248
243
|
</div>
|
|
249
244
|
|
|
250
245
|
{#if $sessionStatus === 'running' || $isExecuting}
|
|
@@ -277,12 +272,14 @@
|
|
|
277
272
|
display: flex;
|
|
278
273
|
flex-direction: column;
|
|
279
274
|
height: 100%;
|
|
275
|
+
min-height: 0; /* Critical: allows flexbox to shrink properly */
|
|
280
276
|
background-color: #ffffff;
|
|
281
277
|
}
|
|
282
278
|
|
|
283
|
-
/* Messages Container */
|
|
279
|
+
/* Messages Container - Scrollable area that takes remaining space */
|
|
284
280
|
.chat-panel__messages {
|
|
285
281
|
flex: 1;
|
|
282
|
+
min-height: 0; /* Critical: allows overflow to work in flex container */
|
|
286
283
|
overflow-y: auto;
|
|
287
284
|
padding: 1.5rem;
|
|
288
285
|
scroll-behavior: smooth;
|
|
@@ -380,10 +377,12 @@
|
|
|
380
377
|
color: #6b7280;
|
|
381
378
|
}
|
|
382
379
|
|
|
383
|
-
/* Input Area */
|
|
380
|
+
/* Input Area - Always stays at bottom, never shrinks */
|
|
384
381
|
.chat-panel__input-area {
|
|
382
|
+
flex-shrink: 0;
|
|
385
383
|
padding: 1rem 1.5rem 1.5rem;
|
|
386
384
|
background-color: #ffffff;
|
|
385
|
+
border-top: 1px solid #f3f4f6;
|
|
387
386
|
}
|
|
388
387
|
|
|
389
388
|
.chat-panel__input-container {
|
|
@@ -434,31 +433,6 @@
|
|
|
434
433
|
opacity: 0.6;
|
|
435
434
|
}
|
|
436
435
|
|
|
437
|
-
.chat-panel__attachment-btn {
|
|
438
|
-
display: flex;
|
|
439
|
-
align-items: center;
|
|
440
|
-
justify-content: center;
|
|
441
|
-
width: 2rem;
|
|
442
|
-
height: 2rem;
|
|
443
|
-
border: none;
|
|
444
|
-
border-radius: 0.375rem;
|
|
445
|
-
background: transparent;
|
|
446
|
-
color: #9ca3af;
|
|
447
|
-
cursor: pointer;
|
|
448
|
-
transition: all 0.15s ease;
|
|
449
|
-
flex-shrink: 0;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
.chat-panel__attachment-btn:hover:not(:disabled) {
|
|
453
|
-
color: #6b7280;
|
|
454
|
-
background-color: #f3f4f6;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
.chat-panel__attachment-btn:disabled {
|
|
458
|
-
opacity: 0.5;
|
|
459
|
-
cursor: not-allowed;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
436
|
.chat-panel__send-btn {
|
|
463
437
|
display: flex;
|
|
464
438
|
align-items: center;
|
|
@@ -533,6 +533,7 @@
|
|
|
533
533
|
display: flex;
|
|
534
534
|
flex-direction: column;
|
|
535
535
|
height: 100%;
|
|
536
|
+
overflow: hidden; /* Prevent playground-level scrolling */
|
|
536
537
|
background-color: #f8fafc;
|
|
537
538
|
font-family:
|
|
538
539
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
@@ -744,6 +745,8 @@
|
|
|
744
745
|
display: flex;
|
|
745
746
|
flex-direction: column;
|
|
746
747
|
min-width: 0;
|
|
748
|
+
min-height: 0; /* Allow proper flex shrinking */
|
|
749
|
+
overflow: hidden; /* Prevent scrolling - ChatPanel handles it */
|
|
747
750
|
background-color: #ffffff;
|
|
748
751
|
}
|
|
749
752
|
|
package/dist/core/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* import { getStatusColor, createDefaultExecutionInfo } from "@d34dman/flowdrop/core";
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
|
-
export type { NodeCategory, NodeDataType, NodePort, DynamicPort, Branch, NodeMetadata, NodeExtensions, NodeUIExtensions, ConfigValues, WorkflowNode, WorkflowEdge, Workflow, ApiResponse, NodesResponse, WorkflowResponse, WorkflowsResponse, ExecutionStatus, ExecutionResult, FlowDropConfig, WorkflowEvents, BuiltinNodeType, PortConfig, PortCompatibilityRule, ConfigSchema, ConfigProperty, HttpMethod, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditOptions } from '../types/index.js';
|
|
17
|
+
export type { NodeCategory, NodeDataType, NodePort, DynamicPort, Branch, NodeMetadata, NodeExtensions, NodeUIExtensions, ConfigValues, WorkflowNode, WorkflowEdge, Workflow, ApiResponse, NodesResponse, WorkflowResponse, WorkflowsResponse, ExecutionStatus, ExecutionResult, FlowDropConfig, WorkflowEvents, BuiltinNodeType, PortConfig, PortCompatibilityRule, ConfigSchema, ConfigProperty, HttpMethod, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditOptions, EdgeCategory } from '../types/index.js';
|
|
18
18
|
export type { WorkflowEditorConfig, EditorFeatures, UIConfig, APIConfig, ExecutionConfig, StorageConfig } from '../types/config.js';
|
|
19
19
|
export type { AuthProvider, StaticAuthConfig, CallbackAuthConfig } from '../types/auth.js';
|
|
20
20
|
export type { WorkflowChangeType, FlowDropEventHandlers, FlowDropFeatures } from '../types/events.js';
|
|
@@ -35,6 +35,7 @@ export * from '../utils/colors.js';
|
|
|
35
35
|
export * from '../utils/icons.js';
|
|
36
36
|
export * from '../utils/config.js';
|
|
37
37
|
export * from '../utils/nodeTypes.js';
|
|
38
|
+
export { isLoopbackEdge, isValidLoopbackCycle, hasCycles, hasInvalidCycles } from '../utils/connections.js';
|
|
38
39
|
export { isFieldOptionArray, normalizeOptions } from '../components/form/types.js';
|
|
39
40
|
export { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
|
|
40
41
|
export { defaultEndpointConfig, createEndpointConfig } from '../config/endpoints.js';
|
package/dist/core/index.js
CHANGED
|
@@ -38,6 +38,8 @@ export * from '../utils/icons.js';
|
|
|
38
38
|
export * from '../utils/config.js';
|
|
39
39
|
// Node type utilities
|
|
40
40
|
export * from '../utils/nodeTypes.js';
|
|
41
|
+
// Connection utilities (including loopback edge detection)
|
|
42
|
+
export { isLoopbackEdge, isValidLoopbackCycle, hasCycles, hasInvalidCycles } from '../utils/connections.js';
|
|
41
43
|
// Form type utilities
|
|
42
44
|
export { isFieldOptionArray, normalizeOptions } from '../components/form/types.js';
|
|
43
45
|
// ============================================================================
|
|
@@ -14,9 +14,10 @@ export declare function generateNodeId(nodeTypeId: string, existingNodes: Workfl
|
|
|
14
14
|
* Edge category type for styling purposes
|
|
15
15
|
* - trigger: For control flow connections (dataType: "trigger")
|
|
16
16
|
* - tool: Dashed amber line for tool connections (dataType: "tool")
|
|
17
|
+
* - loopback: Dashed gray line for loop iteration connections (targets loop_back port)
|
|
17
18
|
* - data: Normal gray line for all other data connections
|
|
18
19
|
*/
|
|
19
|
-
export type EdgeCategory = 'trigger' | 'tool' | 'data';
|
|
20
|
+
export type EdgeCategory = 'trigger' | 'tool' | 'loopback' | 'data';
|
|
20
21
|
/**
|
|
21
22
|
* Edge styling configuration based on source port data type
|
|
22
23
|
*/
|
|
@@ -49,12 +50,25 @@ export declare class EdgeStylingHelper {
|
|
|
49
50
|
static getPortDataType(node: WorkflowNodeType, portId: string, portType: 'input' | 'output'): string | null;
|
|
50
51
|
/**
|
|
51
52
|
* Determine the edge category based on source port data type
|
|
53
|
+
* Note: This method does not check for loopback edges.
|
|
54
|
+
* Use getEdgeCategoryWithLoopback() for full edge categorization.
|
|
55
|
+
*
|
|
52
56
|
* @param sourcePortDataType - The data type of the source output port
|
|
53
57
|
* @returns The edge category for styling
|
|
54
58
|
*/
|
|
55
59
|
static getEdgeCategory(sourcePortDataType: string | null): EdgeCategory;
|
|
56
60
|
/**
|
|
57
|
-
*
|
|
61
|
+
* Determine the full edge category including loopback detection
|
|
62
|
+
* Loopback edges take precedence over source port data type
|
|
63
|
+
*
|
|
64
|
+
* @param edge - The edge to categorize
|
|
65
|
+
* @param sourcePortDataType - The data type of the source output port
|
|
66
|
+
* @returns The edge category for styling
|
|
67
|
+
*/
|
|
68
|
+
static getEdgeCategoryWithLoopback(edge: WorkflowEdge, sourcePortDataType: string | null): EdgeCategory;
|
|
69
|
+
/**
|
|
70
|
+
* Apply custom styling to connection edges based on edge type:
|
|
71
|
+
* - Loopback: Dashed gray line for loop iteration (targets loop_back port)
|
|
58
72
|
* - Trigger ports: Solid black line with arrow
|
|
59
73
|
* - Tool ports: Dashed amber line with arrow
|
|
60
74
|
* - Data ports: Normal gray line with arrow
|
|
@@ -118,9 +132,24 @@ export declare class WorkflowOperationsHelper {
|
|
|
118
132
|
*/
|
|
119
133
|
static exportWorkflow(workflow: Workflow | null): void;
|
|
120
134
|
/**
|
|
121
|
-
* Check if workflow has cycles
|
|
135
|
+
* Check if workflow has invalid cycles (excludes valid loopback cycles)
|
|
136
|
+
* Valid loopback cycles are used for ForEach node iteration and should not
|
|
137
|
+
* trigger a warning.
|
|
138
|
+
*
|
|
139
|
+
* @param nodes - Array of workflow nodes
|
|
140
|
+
* @param edges - Array of workflow edges
|
|
141
|
+
* @returns True if there are invalid (non-loopback) cycles
|
|
122
142
|
*/
|
|
123
143
|
static checkWorkflowCycles(nodes: WorkflowNodeType[], edges: WorkflowEdge[]): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Check if workflow has any cycles (including valid loopback cycles)
|
|
146
|
+
* Use this when you need to detect ALL cycles regardless of type.
|
|
147
|
+
*
|
|
148
|
+
* @param nodes - Array of workflow nodes
|
|
149
|
+
* @param edges - Array of workflow edges
|
|
150
|
+
* @returns True if any cycle exists
|
|
151
|
+
*/
|
|
152
|
+
static checkWorkflowHasAnyCycles(nodes: WorkflowNodeType[], edges: WorkflowEdge[]): boolean;
|
|
124
153
|
}
|
|
125
154
|
/**
|
|
126
155
|
* Configuration helper
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Contains business logic for workflow operations
|
|
4
4
|
*/
|
|
5
5
|
import { MarkerType } from '@xyflow/svelte';
|
|
6
|
-
import { hasCycles } from '../utils/connections.js';
|
|
6
|
+
import { hasCycles, hasInvalidCycles, isLoopbackEdge } from '../utils/connections.js';
|
|
7
7
|
import { workflowApi, nodeApi, setEndpointConfig } from '../services/api.js';
|
|
8
8
|
import { v4 as uuidv4 } from 'uuid';
|
|
9
9
|
import { workflowActions } from '../stores/workflowStore.js';
|
|
@@ -104,6 +104,9 @@ export class EdgeStylingHelper {
|
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
106
|
* Determine the edge category based on source port data type
|
|
107
|
+
* Note: This method does not check for loopback edges.
|
|
108
|
+
* Use getEdgeCategoryWithLoopback() for full edge categorization.
|
|
109
|
+
*
|
|
107
110
|
* @param sourcePortDataType - The data type of the source output port
|
|
108
111
|
* @returns The edge category for styling
|
|
109
112
|
*/
|
|
@@ -118,7 +121,25 @@ export class EdgeStylingHelper {
|
|
|
118
121
|
return 'data';
|
|
119
122
|
}
|
|
120
123
|
/**
|
|
121
|
-
*
|
|
124
|
+
* Determine the full edge category including loopback detection
|
|
125
|
+
* Loopback edges take precedence over source port data type
|
|
126
|
+
*
|
|
127
|
+
* @param edge - The edge to categorize
|
|
128
|
+
* @param sourcePortDataType - The data type of the source output port
|
|
129
|
+
* @returns The edge category for styling
|
|
130
|
+
*/
|
|
131
|
+
static getEdgeCategoryWithLoopback(edge, sourcePortDataType) {
|
|
132
|
+
// Loopback edges are identified by their target handle
|
|
133
|
+
// Check this first as it takes precedence
|
|
134
|
+
if (isLoopbackEdge(edge)) {
|
|
135
|
+
return 'loopback';
|
|
136
|
+
}
|
|
137
|
+
// Fall back to source port data type categorization
|
|
138
|
+
return this.getEdgeCategory(sourcePortDataType);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Apply custom styling to connection edges based on edge type:
|
|
142
|
+
* - Loopback: Dashed gray line for loop iteration (targets loop_back port)
|
|
122
143
|
* - Trigger ports: Solid black line with arrow
|
|
123
144
|
* - Tool ports: Dashed amber line with arrow
|
|
124
145
|
* - Data ports: Normal gray line with arrow
|
|
@@ -130,17 +151,30 @@ export class EdgeStylingHelper {
|
|
|
130
151
|
const sourcePortDataType = sourcePortId
|
|
131
152
|
? this.getPortDataType(sourceNode, sourcePortId, 'output')
|
|
132
153
|
: null;
|
|
133
|
-
// Determine edge category
|
|
134
|
-
const edgeCategory = this.
|
|
154
|
+
// Determine edge category (loopback takes precedence)
|
|
155
|
+
const edgeCategory = this.getEdgeCategoryWithLoopback(edge, sourcePortDataType);
|
|
135
156
|
// Edge color constants (matching CSS tokens in base.css)
|
|
136
157
|
const EDGE_COLORS = {
|
|
137
158
|
trigger: '#111827', // --color-ref-gray-900
|
|
138
159
|
tool: '#f59e0b', // --color-ref-amber-500
|
|
160
|
+
loopback: '#6b7280', // --color-ref-gray-500
|
|
139
161
|
data: '#9ca3af' // --color-ref-gray-400
|
|
140
162
|
};
|
|
141
163
|
// Apply styling based on edge category
|
|
142
164
|
// CSS classes handle styling via tokens; inline styles are fallback
|
|
143
165
|
switch (edgeCategory) {
|
|
166
|
+
case 'loopback':
|
|
167
|
+
// Loopback edges: dashed gray line for loop iteration
|
|
168
|
+
edge.style =
|
|
169
|
+
'stroke: var(--flowdrop-edge-loopback-color); stroke-dasharray: 5 5; stroke-width: var(--flowdrop-edge-loopback-width);';
|
|
170
|
+
edge.class = 'flowdrop--edge--loopback';
|
|
171
|
+
edge.markerEnd = {
|
|
172
|
+
type: MarkerType.ArrowClosed,
|
|
173
|
+
width: 14,
|
|
174
|
+
height: 14,
|
|
175
|
+
color: EDGE_COLORS.loopback
|
|
176
|
+
};
|
|
177
|
+
break;
|
|
144
178
|
case 'trigger':
|
|
145
179
|
// Trigger edges: solid dark line for control flow
|
|
146
180
|
edge.style =
|
|
@@ -183,7 +217,7 @@ export class EdgeStylingHelper {
|
|
|
183
217
|
metadata: {
|
|
184
218
|
...(edge.data?.metadata || {}),
|
|
185
219
|
edgeType: edgeCategory,
|
|
186
|
-
sourcePortDataType: sourcePortDataType
|
|
220
|
+
sourcePortDataType: sourcePortDataType ?? undefined
|
|
187
221
|
},
|
|
188
222
|
targetNodeType: targetNode.type,
|
|
189
223
|
targetCategory: targetNode.data.metadata.category
|
|
@@ -484,9 +518,26 @@ export class WorkflowOperationsHelper {
|
|
|
484
518
|
URL.revokeObjectURL(url);
|
|
485
519
|
}
|
|
486
520
|
/**
|
|
487
|
-
* Check if workflow has cycles
|
|
521
|
+
* Check if workflow has invalid cycles (excludes valid loopback cycles)
|
|
522
|
+
* Valid loopback cycles are used for ForEach node iteration and should not
|
|
523
|
+
* trigger a warning.
|
|
524
|
+
*
|
|
525
|
+
* @param nodes - Array of workflow nodes
|
|
526
|
+
* @param edges - Array of workflow edges
|
|
527
|
+
* @returns True if there are invalid (non-loopback) cycles
|
|
488
528
|
*/
|
|
489
529
|
static checkWorkflowCycles(nodes, edges) {
|
|
530
|
+
return hasInvalidCycles(nodes, edges);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Check if workflow has any cycles (including valid loopback cycles)
|
|
534
|
+
* Use this when you need to detect ALL cycles regardless of type.
|
|
535
|
+
*
|
|
536
|
+
* @param nodes - Array of workflow nodes
|
|
537
|
+
* @param edges - Array of workflow edges
|
|
538
|
+
* @returns True if any cycle exists
|
|
539
|
+
*/
|
|
540
|
+
static checkWorkflowHasAnyCycles(nodes, edges) {
|
|
490
541
|
return hasCycles(nodes, edges);
|
|
491
542
|
}
|
|
492
543
|
}
|
|
@@ -117,18 +117,21 @@ export declare const playgroundActions: {
|
|
|
117
117
|
removeSession: (sessionId: string) => void;
|
|
118
118
|
/**
|
|
119
119
|
* Set messages for the current session
|
|
120
|
+
* Messages are automatically sorted chronologically
|
|
120
121
|
*
|
|
121
122
|
* @param messageList - Array of messages
|
|
122
123
|
*/
|
|
123
124
|
setMessages: (messageList: PlaygroundMessage[]) => void;
|
|
124
125
|
/**
|
|
125
126
|
* Add a message to the current session
|
|
127
|
+
* Messages are automatically sorted chronologically after adding
|
|
126
128
|
*
|
|
127
129
|
* @param message - The message to add
|
|
128
130
|
*/
|
|
129
131
|
addMessage: (message: PlaygroundMessage) => void;
|
|
130
132
|
/**
|
|
131
133
|
* Add multiple messages to the current session
|
|
134
|
+
* Messages are deduplicated and automatically sorted chronologically
|
|
132
135
|
*
|
|
133
136
|
* @param newMessages - Array of messages to add
|
|
134
137
|
*/
|
|
@@ -138,6 +138,40 @@ export const hasChatInput = derived(inputFields, ($fields) => $fields.some((fiel
|
|
|
138
138
|
*/
|
|
139
139
|
export const sessionCount = derived(sessions, ($sessions) => $sessions.length);
|
|
140
140
|
// =========================================================================
|
|
141
|
+
// Helper Functions
|
|
142
|
+
// =========================================================================
|
|
143
|
+
/**
|
|
144
|
+
* Sort messages chronologically by sequenceNumber
|
|
145
|
+
*
|
|
146
|
+
* All messages (user, assistant, log) have incrementing sequenceNumbers (1, 2, 3, ...).
|
|
147
|
+
* This provides a simple, reliable sort order for displaying messages.
|
|
148
|
+
*
|
|
149
|
+
* Sort order:
|
|
150
|
+
* 1. Primary: sequenceNumber (incrementing for all messages)
|
|
151
|
+
* 2. Secondary: timestamp (fallback for messages without sequenceNumber)
|
|
152
|
+
* 3. Tertiary: id as final tiebreaker
|
|
153
|
+
*
|
|
154
|
+
* @param messageList - Array of messages to sort
|
|
155
|
+
* @returns Sorted array of messages
|
|
156
|
+
*/
|
|
157
|
+
function sortMessagesChronologically(messageList) {
|
|
158
|
+
return [...messageList].sort((a, b) => {
|
|
159
|
+
// Primary: Sort by sequenceNumber
|
|
160
|
+
const seqA = a.sequenceNumber ?? 0;
|
|
161
|
+
const seqB = b.sequenceNumber ?? 0;
|
|
162
|
+
if (seqA !== seqB) {
|
|
163
|
+
return seqA - seqB;
|
|
164
|
+
}
|
|
165
|
+
// Secondary: Sort by timestamp for messages without sequenceNumber
|
|
166
|
+
const timestampCompare = a.timestamp.localeCompare(b.timestamp);
|
|
167
|
+
if (timestampCompare !== 0) {
|
|
168
|
+
return timestampCompare;
|
|
169
|
+
}
|
|
170
|
+
// Tertiary: Sort by ID as final tiebreaker
|
|
171
|
+
return a.id.localeCompare(b.id);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// =========================================================================
|
|
141
175
|
// Actions
|
|
142
176
|
// =========================================================================
|
|
143
177
|
/**
|
|
@@ -213,22 +247,25 @@ export const playgroundActions = {
|
|
|
213
247
|
},
|
|
214
248
|
/**
|
|
215
249
|
* Set messages for the current session
|
|
250
|
+
* Messages are automatically sorted chronologically
|
|
216
251
|
*
|
|
217
252
|
* @param messageList - Array of messages
|
|
218
253
|
*/
|
|
219
254
|
setMessages: (messageList) => {
|
|
220
|
-
messages.set(messageList);
|
|
255
|
+
messages.set(sortMessagesChronologically(messageList));
|
|
221
256
|
},
|
|
222
257
|
/**
|
|
223
258
|
* Add a message to the current session
|
|
259
|
+
* Messages are automatically sorted chronologically after adding
|
|
224
260
|
*
|
|
225
261
|
* @param message - The message to add
|
|
226
262
|
*/
|
|
227
263
|
addMessage: (message) => {
|
|
228
|
-
messages.update(($messages) => [...$messages, message]);
|
|
264
|
+
messages.update(($messages) => sortMessagesChronologically([...$messages, message]));
|
|
229
265
|
},
|
|
230
266
|
/**
|
|
231
267
|
* Add multiple messages to the current session
|
|
268
|
+
* Messages are deduplicated and automatically sorted chronologically
|
|
232
269
|
*
|
|
233
270
|
* @param newMessages - Array of messages to add
|
|
234
271
|
*/
|
|
@@ -239,7 +276,8 @@ export const playgroundActions = {
|
|
|
239
276
|
// Deduplicate by message ID
|
|
240
277
|
const existingIds = new Set($messages.map((m) => m.id));
|
|
241
278
|
const uniqueNewMessages = newMessages.filter((m) => !existingIds.has(m.id));
|
|
242
|
-
|
|
279
|
+
// Sort the combined messages chronologically
|
|
280
|
+
return sortMessagesChronologically([...$messages, ...uniqueNewMessages]);
|
|
243
281
|
});
|
|
244
282
|
},
|
|
245
283
|
/**
|
package/dist/styles/base.css
CHANGED
|
@@ -1157,6 +1157,15 @@
|
|
|
1157
1157
|
--flowdrop-edge-data-color-hover: var(--color-ref-gray-500);
|
|
1158
1158
|
--flowdrop-edge-data-color-selected: var(--color-ref-violet-600);
|
|
1159
1159
|
|
|
1160
|
+
/* Loopback edge styling tokens */
|
|
1161
|
+
--flowdrop-edge-loopback-color: var(--color-ref-gray-500);
|
|
1162
|
+
--flowdrop-edge-loopback-color-hover: var(--color-ref-gray-600);
|
|
1163
|
+
--flowdrop-edge-loopback-color-selected: var(--color-ref-violet-600);
|
|
1164
|
+
--flowdrop-edge-loopback-width: 1.5px;
|
|
1165
|
+
--flowdrop-edge-loopback-width-hover: 2.5px;
|
|
1166
|
+
--flowdrop-edge-loopback-dasharray: 5 5;
|
|
1167
|
+
--flowdrop-edge-loopback-opacity: 0.85;
|
|
1168
|
+
|
|
1160
1169
|
/* Tool node theming tokens */
|
|
1161
1170
|
--flowdrop-tool-node-color: var(--color-ref-amber-500);
|
|
1162
1171
|
--flowdrop-tool-node-color-light: var(--color-ref-amber-50);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -656,13 +656,14 @@ export interface WorkflowNode extends Node {
|
|
|
656
656
|
};
|
|
657
657
|
}
|
|
658
658
|
/**
|
|
659
|
-
* Edge category types based on source port data type
|
|
659
|
+
* Edge category types based on source port data type or target handle
|
|
660
660
|
* Used for visual styling of edges on the canvas
|
|
661
661
|
* - trigger: For control flow connections (dataType: "trigger")
|
|
662
662
|
* - tool: Dashed amber line for tool connections (dataType: "tool")
|
|
663
|
+
* - loopback: Dashed gray line for loop iteration (targets loop_back port)
|
|
663
664
|
* - data: Normal gray line for all other data connections
|
|
664
665
|
*/
|
|
665
|
-
export type EdgeCategory = 'trigger' | 'tool' | 'data';
|
|
666
|
+
export type EdgeCategory = 'trigger' | 'tool' | 'loopback' | 'data';
|
|
666
667
|
/**
|
|
667
668
|
* Extended edge type for workflows
|
|
668
669
|
*/
|
|
@@ -24,6 +24,10 @@ export type PlaygroundMessageRole = 'user' | 'assistant' | 'system' | 'log';
|
|
|
24
24
|
* Log level for log-type messages
|
|
25
25
|
*/
|
|
26
26
|
export type PlaygroundMessageLevel = 'info' | 'warning' | 'error' | 'debug';
|
|
27
|
+
/**
|
|
28
|
+
* Status of a playground message
|
|
29
|
+
*/
|
|
30
|
+
export type PlaygroundMessageStatus = 'pending' | 'processing' | 'completed' | 'failed';
|
|
27
31
|
/**
|
|
28
32
|
* Playground session representing a test conversation
|
|
29
33
|
*
|
|
@@ -104,8 +108,18 @@ export interface PlaygroundMessage {
|
|
|
104
108
|
content: string;
|
|
105
109
|
/** Message timestamp (ISO 8601) */
|
|
106
110
|
timestamp: string;
|
|
107
|
-
/**
|
|
108
|
-
|
|
111
|
+
/** Message status */
|
|
112
|
+
status?: PlaygroundMessageStatus;
|
|
113
|
+
/**
|
|
114
|
+
* Sequence number for ordering messages
|
|
115
|
+
* - User messages: incrementing numbers (1, 2, 3, ...)
|
|
116
|
+
* - Assistant/system responses: 0 (sorted after parent via parentMessageId)
|
|
117
|
+
*/
|
|
118
|
+
sequenceNumber?: number;
|
|
119
|
+
/** Parent message ID (for assistant responses linked to user messages) */
|
|
120
|
+
parentMessageId?: string;
|
|
121
|
+
/** Associated node ID (for log/assistant messages) */
|
|
122
|
+
nodeId?: string | null;
|
|
109
123
|
/** Additional message metadata */
|
|
110
124
|
metadata?: PlaygroundMessageMetadata;
|
|
111
125
|
}
|
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
* Connection validation utilities for FlowDrop
|
|
3
3
|
*/
|
|
4
4
|
import type { NodeMetadata, NodePort, NodeDataType, WorkflowNode, WorkflowEdge, PortConfig, PortDataTypeConfig } from '../types/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Determines if an edge is a loopback edge.
|
|
7
|
+
* Loopback edges target the special `loop_back` input port on ForEach nodes.
|
|
8
|
+
* These edges are used to trigger the next iteration in a loop construct.
|
|
9
|
+
*
|
|
10
|
+
* @param edge - The edge to check
|
|
11
|
+
* @returns True if the edge is a loopback edge
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const edge = { targetHandle: "foreach.1-input-loop_back", ... };
|
|
16
|
+
* const isLoop = isLoopbackEdge(edge); // true
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function isLoopbackEdge(edge: WorkflowEdge): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a cycle consists entirely of loopback edges.
|
|
22
|
+
* A valid loopback cycle only contains edges that target loop_back ports.
|
|
23
|
+
*
|
|
24
|
+
* @param cycleEdges - Array of edges that form a cycle
|
|
25
|
+
* @returns True if all edges in the cycle are loopback edges
|
|
26
|
+
*/
|
|
27
|
+
export declare function isValidLoopbackCycle(cycleEdges: WorkflowEdge[]): boolean;
|
|
5
28
|
/**
|
|
6
29
|
* Configurable port compatibility checker
|
|
7
30
|
*/
|
|
@@ -71,8 +94,31 @@ export declare function getConnectionSuggestions(nodeId: string, nodes: Workflow
|
|
|
71
94
|
}>;
|
|
72
95
|
/**
|
|
73
96
|
* Check if a workflow has any cycles (prevent infinite loops)
|
|
97
|
+
* Note: This function detects ALL cycles, including valid loopback cycles.
|
|
98
|
+
* Use `hasInvalidCycles` to check only for cycles that could cause infinite execution.
|
|
99
|
+
*
|
|
100
|
+
* @param nodes - Array of workflow nodes
|
|
101
|
+
* @param edges - Array of workflow edges
|
|
102
|
+
* @returns True if any cycle exists in the workflow
|
|
74
103
|
*/
|
|
75
104
|
export declare function hasCycles(nodes: WorkflowNode[], edges: WorkflowEdge[]): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Check if a workflow has any invalid cycles (non-loopback cycles).
|
|
107
|
+
* This excludes valid loopback cycles used for ForEach iteration.
|
|
108
|
+
* Only cycles that could cause infinite execution are detected.
|
|
109
|
+
*
|
|
110
|
+
* @param nodes - Array of workflow nodes
|
|
111
|
+
* @param edges - Array of workflow edges
|
|
112
|
+
* @returns True if any invalid (non-loopback) cycle exists
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* // A cycle through a loopback edge is valid (returns false)
|
|
117
|
+
* // A cycle through regular data edges is invalid (returns true)
|
|
118
|
+
* const hasInvalid = hasInvalidCycles(nodes, edges);
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export declare function hasInvalidCycles(nodes: WorkflowNode[], edges: WorkflowEdge[]): boolean;
|
|
76
122
|
/**
|
|
77
123
|
* Get the execution order for a workflow (topological sort)
|
|
78
124
|
*/
|
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Connection validation utilities for FlowDrop
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Loopback port name constant
|
|
6
|
+
* This is the standard input port name used for loop iteration triggers
|
|
7
|
+
*/
|
|
8
|
+
const LOOPBACK_PORT_NAME = "loop_back";
|
|
9
|
+
/**
|
|
10
|
+
* Determines if an edge is a loopback edge.
|
|
11
|
+
* Loopback edges target the special `loop_back` input port on ForEach nodes.
|
|
12
|
+
* These edges are used to trigger the next iteration in a loop construct.
|
|
13
|
+
*
|
|
14
|
+
* @param edge - The edge to check
|
|
15
|
+
* @returns True if the edge is a loopback edge
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const edge = { targetHandle: "foreach.1-input-loop_back", ... };
|
|
20
|
+
* const isLoop = isLoopbackEdge(edge); // true
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function isLoopbackEdge(edge) {
|
|
24
|
+
const targetHandle = edge.targetHandle ?? "";
|
|
25
|
+
return targetHandle.includes(`-input-${LOOPBACK_PORT_NAME}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Checks if a cycle consists entirely of loopback edges.
|
|
29
|
+
* A valid loopback cycle only contains edges that target loop_back ports.
|
|
30
|
+
*
|
|
31
|
+
* @param cycleEdges - Array of edges that form a cycle
|
|
32
|
+
* @returns True if all edges in the cycle are loopback edges
|
|
33
|
+
*/
|
|
34
|
+
export function isValidLoopbackCycle(cycleEdges) {
|
|
35
|
+
return cycleEdges.every((edge) => isLoopbackEdge(edge));
|
|
36
|
+
}
|
|
4
37
|
/**
|
|
5
38
|
* Configurable port compatibility checker
|
|
6
39
|
*/
|
|
@@ -225,6 +258,12 @@ export function getConnectionSuggestions(nodeId, nodes, nodeTypes) {
|
|
|
225
258
|
}
|
|
226
259
|
/**
|
|
227
260
|
* Check if a workflow has any cycles (prevent infinite loops)
|
|
261
|
+
* Note: This function detects ALL cycles, including valid loopback cycles.
|
|
262
|
+
* Use `hasInvalidCycles` to check only for cycles that could cause infinite execution.
|
|
263
|
+
*
|
|
264
|
+
* @param nodes - Array of workflow nodes
|
|
265
|
+
* @param edges - Array of workflow edges
|
|
266
|
+
* @returns True if any cycle exists in the workflow
|
|
228
267
|
*/
|
|
229
268
|
export function hasCycles(nodes, edges) {
|
|
230
269
|
const visited = new Set();
|
|
@@ -254,6 +293,58 @@ export function hasCycles(nodes, edges) {
|
|
|
254
293
|
}
|
|
255
294
|
return false;
|
|
256
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Check if a workflow has any invalid cycles (non-loopback cycles).
|
|
298
|
+
* This excludes valid loopback cycles used for ForEach iteration.
|
|
299
|
+
* Only cycles that could cause infinite execution are detected.
|
|
300
|
+
*
|
|
301
|
+
* @param nodes - Array of workflow nodes
|
|
302
|
+
* @param edges - Array of workflow edges
|
|
303
|
+
* @returns True if any invalid (non-loopback) cycle exists
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```typescript
|
|
307
|
+
* // A cycle through a loopback edge is valid (returns false)
|
|
308
|
+
* // A cycle through regular data edges is invalid (returns true)
|
|
309
|
+
* const hasInvalid = hasInvalidCycles(nodes, edges);
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
export function hasInvalidCycles(nodes, edges) {
|
|
313
|
+
// Filter out loopback edges - these create valid cycles for loop iteration
|
|
314
|
+
const nonLoopbackEdges = edges.filter((edge) => !isLoopbackEdge(edge));
|
|
315
|
+
// Check for cycles using only non-loopback edges
|
|
316
|
+
const visited = new Set();
|
|
317
|
+
const recursionStack = new Set();
|
|
318
|
+
/**
|
|
319
|
+
* DFS utility to detect cycles in the graph
|
|
320
|
+
* @param nodeId - Current node being visited
|
|
321
|
+
* @returns True if a cycle is found from this node
|
|
322
|
+
*/
|
|
323
|
+
function hasCycleUtil(nodeId) {
|
|
324
|
+
if (recursionStack.has(nodeId))
|
|
325
|
+
return true;
|
|
326
|
+
if (visited.has(nodeId))
|
|
327
|
+
return false;
|
|
328
|
+
visited.add(nodeId);
|
|
329
|
+
recursionStack.add(nodeId);
|
|
330
|
+
// Get all outgoing non-loopback edges from this node
|
|
331
|
+
const outgoingEdges = nonLoopbackEdges.filter((e) => e.source === nodeId);
|
|
332
|
+
for (const edge of outgoingEdges) {
|
|
333
|
+
if (hasCycleUtil(edge.target))
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
recursionStack.delete(nodeId);
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
// Check each node for cycles
|
|
340
|
+
for (const node of nodes) {
|
|
341
|
+
if (!visited.has(node.id)) {
|
|
342
|
+
if (hasCycleUtil(node.id))
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
257
348
|
/**
|
|
258
349
|
* Get the execution order for a workflow (topological sort)
|
|
259
350
|
*/
|