@d34dman/flowdrop 0.0.37 → 0.0.39
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/LICENSE +21 -0
- package/dist/components/NodeSidebar.svelte +1 -0
- package/dist/components/form/FormCodeEditor.svelte +6 -1
- package/dist/components/interrupt/ChoicePrompt.svelte +389 -0
- package/dist/components/interrupt/ChoicePrompt.svelte.d.ts +21 -0
- package/dist/components/interrupt/ConfirmationPrompt.svelte +280 -0
- package/dist/components/interrupt/ConfirmationPrompt.svelte.d.ts +23 -0
- package/dist/components/interrupt/FormPrompt.svelte +223 -0
- package/dist/components/interrupt/FormPrompt.svelte.d.ts +21 -0
- package/dist/components/interrupt/InterruptBubble.svelte +621 -0
- package/dist/components/interrupt/InterruptBubble.svelte.d.ts +16 -0
- package/dist/components/interrupt/TextInputPrompt.svelte +333 -0
- package/dist/components/interrupt/TextInputPrompt.svelte.d.ts +21 -0
- package/dist/components/interrupt/index.d.ts +13 -0
- package/dist/components/interrupt/index.js +15 -0
- package/dist/components/nodes/GatewayNode.svelte +1 -3
- package/dist/components/nodes/IdeaNode.svelte +30 -35
- package/dist/components/nodes/IdeaNode.svelte.d.ts +1 -1
- package/dist/components/nodes/SimpleNode.svelte +1 -3
- package/dist/components/nodes/TerminalNode.svelte +1 -3
- package/dist/components/nodes/ToolNode.svelte +2 -2
- package/dist/components/nodes/WorkflowNode.svelte +1 -3
- package/dist/components/playground/ChatPanel.svelte +144 -7
- package/dist/components/playground/ChatPanel.svelte.d.ts +2 -0
- package/dist/components/playground/MessageBubble.svelte +1 -3
- package/dist/components/playground/Playground.svelte +50 -5
- package/dist/components/playground/PlaygroundModal.svelte +8 -7
- package/dist/components/playground/PlaygroundModal.svelte.d.ts +3 -3
- package/dist/config/endpoints.d.ts +12 -0
- package/dist/config/endpoints.js +7 -0
- package/dist/playground/index.d.ts +5 -0
- package/dist/playground/index.js +21 -0
- package/dist/playground/mount.d.ts +3 -3
- package/dist/playground/mount.js +30 -22
- package/dist/services/interruptService.d.ts +133 -0
- package/dist/services/interruptService.js +279 -0
- package/dist/stores/interruptStore.d.ts +200 -0
- package/dist/stores/interruptStore.js +424 -0
- package/dist/stores/playgroundStore.d.ts +11 -1
- package/dist/stores/playgroundStore.js +34 -0
- package/dist/styles/base.css +89 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/interrupt.d.ts +305 -0
- package/dist/types/interrupt.js +126 -0
- package/dist/types/interruptState.d.ts +211 -0
- package/dist/types/interruptState.js +308 -0
- package/dist/utils/colors.js +1 -0
- package/dist/utils/connections.js +2 -2
- package/dist/utils/icons.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
InterruptBubble Component
|
|
3
|
+
|
|
4
|
+
Container component for rendering interrupt prompts inline in the chat flow.
|
|
5
|
+
Displays the appropriate prompt component based on interrupt type.
|
|
6
|
+
Handles resolve/cancel actions using state machine for safe transitions.
|
|
7
|
+
Styled with BEM syntax similar to MessageBubble.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import Icon from '@iconify/svelte';
|
|
12
|
+
import ConfirmationPrompt from './ConfirmationPrompt.svelte';
|
|
13
|
+
import ChoicePrompt from './ChoicePrompt.svelte';
|
|
14
|
+
import TextInputPrompt from './TextInputPrompt.svelte';
|
|
15
|
+
import FormPrompt from './FormPrompt.svelte';
|
|
16
|
+
import type {
|
|
17
|
+
Interrupt,
|
|
18
|
+
InterruptType,
|
|
19
|
+
ConfirmationConfig,
|
|
20
|
+
ChoiceConfig,
|
|
21
|
+
TextConfig,
|
|
22
|
+
FormConfig
|
|
23
|
+
} from '../../types/interrupt.js';
|
|
24
|
+
import {
|
|
25
|
+
isTerminalState,
|
|
26
|
+
isSubmitting as checkIsSubmitting,
|
|
27
|
+
getErrorMessage,
|
|
28
|
+
getResolvedValue
|
|
29
|
+
} from '../../types/interruptState.js';
|
|
30
|
+
import {
|
|
31
|
+
interrupts,
|
|
32
|
+
interruptActions,
|
|
33
|
+
type InterruptWithState
|
|
34
|
+
} from '../../stores/interruptStore.js';
|
|
35
|
+
import { interruptService } from '../../services/interruptService.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Component props
|
|
39
|
+
*/
|
|
40
|
+
interface Props {
|
|
41
|
+
/** The interrupt to display (initial data, used for ID lookup) */
|
|
42
|
+
interrupt: Interrupt | InterruptWithState;
|
|
43
|
+
/** Whether to show the timestamp */
|
|
44
|
+
showTimestamp?: boolean;
|
|
45
|
+
/** Callback to refresh messages after interrupt resolution */
|
|
46
|
+
onResolved?: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let { interrupt: initialInterrupt, showTimestamp = true, onResolved }: Props = $props();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the current interrupt state from the store.
|
|
53
|
+
* This ensures we react to store updates (like status changes).
|
|
54
|
+
*/
|
|
55
|
+
const currentInterrupt = $derived(
|
|
56
|
+
$interrupts.get(initialInterrupt.id) ?? addMachineState(initialInterrupt)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Helper to ensure interrupt has machine state
|
|
61
|
+
*/
|
|
62
|
+
function addMachineState(interrupt: Interrupt | InterruptWithState): InterruptWithState {
|
|
63
|
+
if ('machineState' in interrupt) {
|
|
64
|
+
return interrupt;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...interrupt,
|
|
68
|
+
machineState: { status: 'idle' }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether this interrupt is in a terminal state (resolved or cancelled) */
|
|
73
|
+
const isResolved = $derived(isTerminalState(currentInterrupt.machineState));
|
|
74
|
+
|
|
75
|
+
/** Whether this interrupt is currently submitting */
|
|
76
|
+
const isSubmitting = $derived(checkIsSubmitting(currentInterrupt.machineState));
|
|
77
|
+
|
|
78
|
+
/** Error message for this interrupt */
|
|
79
|
+
const error = $derived(getErrorMessage(currentInterrupt.machineState));
|
|
80
|
+
|
|
81
|
+
/** Resolved value for display */
|
|
82
|
+
const resolvedValue = $derived(getResolvedValue(currentInterrupt.machineState));
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get the icon for the interrupt type
|
|
86
|
+
*/
|
|
87
|
+
function getTypeIcon(type: InterruptType): string {
|
|
88
|
+
switch (type) {
|
|
89
|
+
case 'confirmation':
|
|
90
|
+
return 'mdi:help-circle';
|
|
91
|
+
case 'choice':
|
|
92
|
+
return 'mdi:format-list-bulleted';
|
|
93
|
+
case 'text':
|
|
94
|
+
return 'mdi:text-box';
|
|
95
|
+
case 'form':
|
|
96
|
+
return 'mdi:form-select';
|
|
97
|
+
default:
|
|
98
|
+
return 'mdi:bell';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the label for the interrupt type
|
|
104
|
+
*/
|
|
105
|
+
function getTypeLabel(type: InterruptType): string {
|
|
106
|
+
switch (type) {
|
|
107
|
+
case 'confirmation':
|
|
108
|
+
return 'Confirmation Required';
|
|
109
|
+
case 'choice':
|
|
110
|
+
return 'Selection Required';
|
|
111
|
+
case 'text':
|
|
112
|
+
return 'Input Required';
|
|
113
|
+
case 'form':
|
|
114
|
+
return 'Form Required';
|
|
115
|
+
default:
|
|
116
|
+
return 'Action Required';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Get resolved label for the header when resolved */
|
|
121
|
+
function getResolvedLabel(type: InterruptType): string {
|
|
122
|
+
switch (type) {
|
|
123
|
+
case 'confirmation':
|
|
124
|
+
return 'Confirmation Submitted';
|
|
125
|
+
case 'choice':
|
|
126
|
+
return 'Selection Made';
|
|
127
|
+
case 'text':
|
|
128
|
+
return 'Input Submitted';
|
|
129
|
+
case 'form':
|
|
130
|
+
return 'Form Submitted';
|
|
131
|
+
default:
|
|
132
|
+
return 'Response Submitted';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Format timestamp for display
|
|
138
|
+
*/
|
|
139
|
+
function formatTimestamp(timestamp: string): string {
|
|
140
|
+
const date = new Date(timestamp);
|
|
141
|
+
return date.toLocaleTimeString('en-US', {
|
|
142
|
+
hour12: false,
|
|
143
|
+
hour: '2-digit',
|
|
144
|
+
minute: '2-digit',
|
|
145
|
+
second: '2-digit'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Handle resolve action using state machine
|
|
151
|
+
*/
|
|
152
|
+
async function handleResolve(value: unknown): Promise<void> {
|
|
153
|
+
// Start the submission - state machine validates this transition
|
|
154
|
+
const startResult = interruptActions.startSubmit(currentInterrupt.id, value);
|
|
155
|
+
if (!startResult.valid) {
|
|
156
|
+
console.warn('[InterruptBubble] Cannot submit:', startResult.error);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// Call API if service is configured
|
|
162
|
+
if (interruptService.isConfigured()) {
|
|
163
|
+
await interruptService.resolveInterrupt(currentInterrupt.id, value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Mark as successful - transitions to resolved state
|
|
167
|
+
interruptActions.submitSuccess(currentInterrupt.id);
|
|
168
|
+
|
|
169
|
+
// Notify parent to refresh messages
|
|
170
|
+
onResolved?.();
|
|
171
|
+
} catch (err) {
|
|
172
|
+
// Mark as failed - transitions to error state (can retry)
|
|
173
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to submit response';
|
|
174
|
+
interruptActions.submitFailure(currentInterrupt.id, errorMessage);
|
|
175
|
+
console.error('[InterruptBubble] Resolve error:', err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Handle cancel action using state machine
|
|
181
|
+
*/
|
|
182
|
+
async function handleCancel(): Promise<void> {
|
|
183
|
+
// Start the cancel - state machine validates this transition
|
|
184
|
+
const startResult = interruptActions.startCancel(currentInterrupt.id);
|
|
185
|
+
if (!startResult.valid) {
|
|
186
|
+
console.warn('[InterruptBubble] Cannot cancel:', startResult.error);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// Call API if service is configured
|
|
192
|
+
if (interruptService.isConfigured()) {
|
|
193
|
+
await interruptService.cancelInterrupt(currentInterrupt.id);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Mark as successful - transitions to cancelled state
|
|
197
|
+
interruptActions.submitSuccess(currentInterrupt.id);
|
|
198
|
+
|
|
199
|
+
// Notify parent to refresh messages
|
|
200
|
+
onResolved?.();
|
|
201
|
+
} catch (err) {
|
|
202
|
+
// Mark as failed - transitions to error state (can retry)
|
|
203
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to cancel';
|
|
204
|
+
interruptActions.submitFailure(currentInterrupt.id, errorMessage);
|
|
205
|
+
console.error('[InterruptBubble] Cancel error:', err);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Handle retry after error
|
|
211
|
+
*/
|
|
212
|
+
function handleRetry(): void {
|
|
213
|
+
interruptActions.retry(currentInterrupt.id);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Typed config getters for each prompt type
|
|
217
|
+
const confirmationConfig = $derived(currentInterrupt.config as ConfirmationConfig);
|
|
218
|
+
const choiceConfig = $derived(currentInterrupt.config as ChoiceConfig);
|
|
219
|
+
const textConfig = $derived(currentInterrupt.config as TextConfig);
|
|
220
|
+
const formConfig = $derived(currentInterrupt.config as FormConfig);
|
|
221
|
+
|
|
222
|
+
// Determine the actual resolved value to pass to prompt components
|
|
223
|
+
const displayResolvedValue = $derived(resolvedValue ?? currentInterrupt.responseValue);
|
|
224
|
+
</script>
|
|
225
|
+
|
|
226
|
+
<div
|
|
227
|
+
class="interrupt-bubble"
|
|
228
|
+
class:interrupt-bubble--completed={currentInterrupt.machineState.status === 'resolved'}
|
|
229
|
+
class:interrupt-bubble--cancelled={currentInterrupt.machineState.status === 'cancelled'}
|
|
230
|
+
class:interrupt-bubble--submitting={isSubmitting}
|
|
231
|
+
class:interrupt-bubble--error={currentInterrupt.machineState.status === 'error'}
|
|
232
|
+
>
|
|
233
|
+
<!-- Avatar / Icon -->
|
|
234
|
+
<div class="interrupt-bubble__avatar">
|
|
235
|
+
{#if currentInterrupt.machineState.status === 'cancelled'}
|
|
236
|
+
<Icon icon="mdi:close-circle" />
|
|
237
|
+
{:else if currentInterrupt.machineState.status === 'resolved'}
|
|
238
|
+
<Icon icon="mdi:check-circle" />
|
|
239
|
+
{:else if currentInterrupt.machineState.status === 'error'}
|
|
240
|
+
<Icon icon="mdi:alert-circle" />
|
|
241
|
+
{:else}
|
|
242
|
+
<Icon icon="mdi:bell-ring" />
|
|
243
|
+
{/if}
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<!-- Content -->
|
|
247
|
+
<div class="interrupt-bubble__content">
|
|
248
|
+
<!-- Header -->
|
|
249
|
+
<div class="interrupt-bubble__header">
|
|
250
|
+
<span class="interrupt-bubble__type">
|
|
251
|
+
<Icon icon={getTypeIcon(currentInterrupt.type)} />
|
|
252
|
+
{#if isResolved}
|
|
253
|
+
{currentInterrupt.machineState.status === 'cancelled'
|
|
254
|
+
? 'Cancelled'
|
|
255
|
+
: getResolvedLabel(currentInterrupt.type)}
|
|
256
|
+
{:else if currentInterrupt.machineState.status === 'error'}
|
|
257
|
+
Error - Click to Retry
|
|
258
|
+
{:else}
|
|
259
|
+
{getTypeLabel(currentInterrupt.type)}
|
|
260
|
+
{/if}
|
|
261
|
+
</span>
|
|
262
|
+
{#if showTimestamp}
|
|
263
|
+
<span class="interrupt-bubble__timestamp">
|
|
264
|
+
{formatTimestamp(currentInterrupt.resolvedAt ?? currentInterrupt.createdAt)}
|
|
265
|
+
</span>
|
|
266
|
+
{/if}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<!-- Error message with retry button -->
|
|
270
|
+
{#if currentInterrupt.machineState.status === 'error'}
|
|
271
|
+
<div class="interrupt-bubble__error">
|
|
272
|
+
<Icon icon="mdi:alert-circle" />
|
|
273
|
+
<span>{error}</span>
|
|
274
|
+
<button type="button" class="interrupt-bubble__retry-btn" onclick={handleRetry}>
|
|
275
|
+
<Icon icon="mdi:refresh" />
|
|
276
|
+
Retry
|
|
277
|
+
</button>
|
|
278
|
+
</div>
|
|
279
|
+
{/if}
|
|
280
|
+
|
|
281
|
+
<!-- Prompt content based on type -->
|
|
282
|
+
<div class="interrupt-bubble__prompt">
|
|
283
|
+
{#if currentInterrupt.type === 'confirmation'}
|
|
284
|
+
<ConfirmationPrompt
|
|
285
|
+
config={confirmationConfig}
|
|
286
|
+
{isResolved}
|
|
287
|
+
resolvedValue={displayResolvedValue as boolean | undefined}
|
|
288
|
+
{isSubmitting}
|
|
289
|
+
{error}
|
|
290
|
+
onConfirm={() => handleResolve(true)}
|
|
291
|
+
onDecline={() => handleResolve(false)}
|
|
292
|
+
/>
|
|
293
|
+
{:else if currentInterrupt.type === 'choice'}
|
|
294
|
+
<ChoicePrompt
|
|
295
|
+
config={choiceConfig}
|
|
296
|
+
{isResolved}
|
|
297
|
+
resolvedValue={displayResolvedValue as string | string[] | undefined}
|
|
298
|
+
{isSubmitting}
|
|
299
|
+
{error}
|
|
300
|
+
onSubmit={(value) => handleResolve(value)}
|
|
301
|
+
/>
|
|
302
|
+
{:else if currentInterrupt.type === 'text'}
|
|
303
|
+
<TextInputPrompt
|
|
304
|
+
config={textConfig}
|
|
305
|
+
{isResolved}
|
|
306
|
+
resolvedValue={displayResolvedValue as string | undefined}
|
|
307
|
+
{isSubmitting}
|
|
308
|
+
{error}
|
|
309
|
+
onSubmit={(value) => handleResolve(value)}
|
|
310
|
+
/>
|
|
311
|
+
{:else if currentInterrupt.type === 'form'}
|
|
312
|
+
<FormPrompt
|
|
313
|
+
config={formConfig}
|
|
314
|
+
{isResolved}
|
|
315
|
+
resolvedValue={displayResolvedValue as Record<string, unknown> | undefined}
|
|
316
|
+
{isSubmitting}
|
|
317
|
+
{error}
|
|
318
|
+
onSubmit={(value) => handleResolve(value)}
|
|
319
|
+
/>
|
|
320
|
+
{/if}
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<!-- Cancel button (if allowed and not in terminal state) -->
|
|
324
|
+
{#if currentInterrupt.allowCancel && !isResolved && currentInterrupt.type !== 'confirmation'}
|
|
325
|
+
<div class="interrupt-bubble__cancel-wrapper">
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
class="interrupt-bubble__cancel-btn"
|
|
329
|
+
onclick={handleCancel}
|
|
330
|
+
disabled={isSubmitting}
|
|
331
|
+
>
|
|
332
|
+
<Icon icon="mdi:close" />
|
|
333
|
+
<span>Cancel</span>
|
|
334
|
+
</button>
|
|
335
|
+
</div>
|
|
336
|
+
{/if}
|
|
337
|
+
|
|
338
|
+
<!-- Node info footer -->
|
|
339
|
+
{#if currentInterrupt.nodeId}
|
|
340
|
+
<div class="interrupt-bubble__footer">
|
|
341
|
+
<span class="interrupt-bubble__node" title="Node ID: {currentInterrupt.nodeId}">
|
|
342
|
+
<Icon icon="mdi:graph" />
|
|
343
|
+
<span>From workflow node</span>
|
|
344
|
+
</span>
|
|
345
|
+
</div>
|
|
346
|
+
{/if}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<style>
|
|
351
|
+
/* Uses design tokens from base.css: --flowdrop-interrupt-* */
|
|
352
|
+
.interrupt-bubble {
|
|
353
|
+
display: flex;
|
|
354
|
+
gap: 0.75rem;
|
|
355
|
+
padding: 1rem 1.25rem;
|
|
356
|
+
margin: 0.75rem 1rem;
|
|
357
|
+
border-radius: 0.75rem;
|
|
358
|
+
background: var(--flowdrop-interrupt-pending-bg);
|
|
359
|
+
border: 1px solid var(--flowdrop-interrupt-pending-border);
|
|
360
|
+
box-shadow: 0 2px 8px var(--flowdrop-interrupt-pending-shadow);
|
|
361
|
+
animation: interruptSlideIn 0.3s ease-out;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@keyframes interruptSlideIn {
|
|
365
|
+
from {
|
|
366
|
+
opacity: 0;
|
|
367
|
+
transform: translateY(12px);
|
|
368
|
+
}
|
|
369
|
+
to {
|
|
370
|
+
opacity: 1;
|
|
371
|
+
transform: translateY(0);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* Completed state - neutral blue to indicate response received without implying good/bad */
|
|
376
|
+
.interrupt-bubble--completed {
|
|
377
|
+
background: var(--flowdrop-interrupt-completed-bg);
|
|
378
|
+
border-color: var(--flowdrop-interrupt-completed-border);
|
|
379
|
+
box-shadow: 0 2px 8px var(--flowdrop-interrupt-completed-shadow);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.interrupt-bubble--cancelled {
|
|
383
|
+
background: var(--flowdrop-interrupt-cancelled-bg);
|
|
384
|
+
border-color: var(--flowdrop-interrupt-cancelled-border);
|
|
385
|
+
box-shadow: 0 2px 8px var(--flowdrop-interrupt-cancelled-shadow);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.interrupt-bubble--error {
|
|
389
|
+
background: var(--flowdrop-interrupt-error-bg);
|
|
390
|
+
border-color: var(--flowdrop-interrupt-error-border);
|
|
391
|
+
box-shadow: 0 2px 8px var(--flowdrop-interrupt-error-shadow);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.interrupt-bubble--submitting {
|
|
395
|
+
opacity: 0.9;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* Avatar */
|
|
399
|
+
.interrupt-bubble__avatar {
|
|
400
|
+
flex-shrink: 0;
|
|
401
|
+
width: 2.25rem;
|
|
402
|
+
height: 2.25rem;
|
|
403
|
+
display: flex;
|
|
404
|
+
align-items: center;
|
|
405
|
+
justify-content: center;
|
|
406
|
+
border-radius: 50%;
|
|
407
|
+
background-color: var(--flowdrop-interrupt-pending-avatar);
|
|
408
|
+
color: #ffffff;
|
|
409
|
+
font-size: 1.125rem;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.interrupt-bubble--completed .interrupt-bubble__avatar {
|
|
413
|
+
background-color: var(--flowdrop-interrupt-completed-avatar);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.interrupt-bubble--cancelled .interrupt-bubble__avatar {
|
|
417
|
+
background-color: var(--flowdrop-interrupt-cancelled-avatar);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.interrupt-bubble--error .interrupt-bubble__avatar {
|
|
421
|
+
background-color: var(--flowdrop-interrupt-error-avatar);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* Content */
|
|
425
|
+
.interrupt-bubble__content {
|
|
426
|
+
flex: 1;
|
|
427
|
+
min-width: 0;
|
|
428
|
+
display: flex;
|
|
429
|
+
flex-direction: column;
|
|
430
|
+
gap: 0.75rem;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* Header */
|
|
434
|
+
.interrupt-bubble__header {
|
|
435
|
+
display: flex;
|
|
436
|
+
align-items: center;
|
|
437
|
+
justify-content: space-between;
|
|
438
|
+
gap: 0.5rem;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.interrupt-bubble__type {
|
|
442
|
+
display: flex;
|
|
443
|
+
align-items: center;
|
|
444
|
+
gap: 0.375rem;
|
|
445
|
+
font-weight: 600;
|
|
446
|
+
font-size: 0.875rem;
|
|
447
|
+
color: var(--flowdrop-interrupt-pending-text);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.interrupt-bubble--completed .interrupt-bubble__type {
|
|
451
|
+
color: var(--flowdrop-interrupt-completed-text);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.interrupt-bubble--cancelled .interrupt-bubble__type {
|
|
455
|
+
color: var(--flowdrop-interrupt-cancelled-text);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.interrupt-bubble--error .interrupt-bubble__type {
|
|
459
|
+
color: var(--flowdrop-interrupt-error-text);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.interrupt-bubble__timestamp {
|
|
463
|
+
font-size: 0.6875rem;
|
|
464
|
+
color: var(--flowdrop-interrupt-pending-text-light);
|
|
465
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.interrupt-bubble--completed .interrupt-bubble__timestamp {
|
|
469
|
+
color: var(--flowdrop-interrupt-completed-text-light);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.interrupt-bubble--cancelled .interrupt-bubble__timestamp {
|
|
473
|
+
color: var(--flowdrop-interrupt-cancelled-text-light);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.interrupt-bubble--error .interrupt-bubble__timestamp {
|
|
477
|
+
color: var(--flowdrop-interrupt-error-text-light);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* Error message */
|
|
481
|
+
.interrupt-bubble__error {
|
|
482
|
+
display: flex;
|
|
483
|
+
align-items: center;
|
|
484
|
+
gap: 0.5rem;
|
|
485
|
+
padding: 0.5rem 0.75rem;
|
|
486
|
+
background-color: var(--color-ref-red-50, rgba(239, 68, 68, 0.1));
|
|
487
|
+
border-radius: 0.375rem;
|
|
488
|
+
color: var(--flowdrop-interrupt-error-text);
|
|
489
|
+
font-size: 0.8125rem;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.interrupt-bubble__retry-btn {
|
|
493
|
+
display: inline-flex;
|
|
494
|
+
align-items: center;
|
|
495
|
+
gap: 0.25rem;
|
|
496
|
+
margin-left: auto;
|
|
497
|
+
padding: 0.25rem 0.5rem;
|
|
498
|
+
font-size: 0.75rem;
|
|
499
|
+
font-weight: 500;
|
|
500
|
+
font-family: inherit;
|
|
501
|
+
color: #ffffff;
|
|
502
|
+
background-color: var(--flowdrop-interrupt-error-avatar);
|
|
503
|
+
border: none;
|
|
504
|
+
border-radius: 0.25rem;
|
|
505
|
+
cursor: pointer;
|
|
506
|
+
transition: background-color 0.15s ease;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.interrupt-bubble__retry-btn:hover {
|
|
510
|
+
background-color: var(--color-ref-red-600, #dc2626);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/* Prompt */
|
|
514
|
+
.interrupt-bubble__prompt {
|
|
515
|
+
background-color: var(--flowdrop-interrupt-prompt-bg);
|
|
516
|
+
border-radius: 0.5rem;
|
|
517
|
+
padding: 1rem;
|
|
518
|
+
border: 1px solid var(--flowdrop-interrupt-prompt-border-pending);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.interrupt-bubble--completed .interrupt-bubble__prompt {
|
|
522
|
+
border-color: var(--flowdrop-interrupt-prompt-border-completed);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.interrupt-bubble--cancelled .interrupt-bubble__prompt {
|
|
526
|
+
border-color: var(--flowdrop-interrupt-prompt-border-cancelled);
|
|
527
|
+
opacity: 0.75;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.interrupt-bubble--error .interrupt-bubble__prompt {
|
|
531
|
+
border-color: var(--flowdrop-interrupt-prompt-border-error);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/* Cancel button wrapper */
|
|
535
|
+
.interrupt-bubble__cancel-wrapper {
|
|
536
|
+
display: flex;
|
|
537
|
+
justify-content: flex-end;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.interrupt-bubble__cancel-btn {
|
|
541
|
+
display: inline-flex;
|
|
542
|
+
align-items: center;
|
|
543
|
+
gap: 0.375rem;
|
|
544
|
+
padding: 0.375rem 0.75rem;
|
|
545
|
+
font-size: 0.75rem;
|
|
546
|
+
font-weight: 500;
|
|
547
|
+
font-family: inherit;
|
|
548
|
+
color: #6b7280;
|
|
549
|
+
background-color: transparent;
|
|
550
|
+
border: 1px solid #d1d5db;
|
|
551
|
+
border-radius: 0.375rem;
|
|
552
|
+
cursor: pointer;
|
|
553
|
+
transition: all 0.15s ease;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.interrupt-bubble__cancel-btn:hover:not(:disabled) {
|
|
557
|
+
color: #dc2626;
|
|
558
|
+
border-color: #fca5a5;
|
|
559
|
+
background-color: #fef2f2;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.interrupt-bubble__cancel-btn:disabled {
|
|
563
|
+
opacity: 0.5;
|
|
564
|
+
cursor: not-allowed;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/* Footer */
|
|
568
|
+
.interrupt-bubble__footer {
|
|
569
|
+
display: flex;
|
|
570
|
+
align-items: center;
|
|
571
|
+
gap: 0.5rem;
|
|
572
|
+
padding-top: 0.5rem;
|
|
573
|
+
border-top: 1px solid var(--flowdrop-interrupt-prompt-border-pending);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.interrupt-bubble--completed .interrupt-bubble__footer {
|
|
577
|
+
border-color: var(--flowdrop-interrupt-prompt-border-completed);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.interrupt-bubble--cancelled .interrupt-bubble__footer {
|
|
581
|
+
border-color: var(--flowdrop-interrupt-prompt-border-cancelled);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.interrupt-bubble--error .interrupt-bubble__footer {
|
|
585
|
+
border-color: var(--flowdrop-interrupt-prompt-border-error);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.interrupt-bubble__node {
|
|
589
|
+
display: flex;
|
|
590
|
+
align-items: center;
|
|
591
|
+
gap: 0.25rem;
|
|
592
|
+
font-size: 0.6875rem;
|
|
593
|
+
color: var(--flowdrop-interrupt-pending-text);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.interrupt-bubble--completed .interrupt-bubble__node {
|
|
597
|
+
color: var(--flowdrop-interrupt-completed-text);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.interrupt-bubble--cancelled .interrupt-bubble__node {
|
|
601
|
+
color: var(--flowdrop-interrupt-cancelled-text);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.interrupt-bubble--error .interrupt-bubble__node {
|
|
605
|
+
color: var(--flowdrop-interrupt-error-text);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/* Responsive */
|
|
609
|
+
@media (max-width: 640px) {
|
|
610
|
+
.interrupt-bubble {
|
|
611
|
+
margin: 0.5rem;
|
|
612
|
+
padding: 0.875rem 1rem;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.interrupt-bubble__avatar {
|
|
616
|
+
width: 2rem;
|
|
617
|
+
height: 2rem;
|
|
618
|
+
font-size: 1rem;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Interrupt } from '../../types/interrupt.js';
|
|
2
|
+
import { type InterruptWithState } from '../../stores/interruptStore.js';
|
|
3
|
+
/**
|
|
4
|
+
* Component props
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
/** The interrupt to display (initial data, used for ID lookup) */
|
|
8
|
+
interrupt: Interrupt | InterruptWithState;
|
|
9
|
+
/** Whether to show the timestamp */
|
|
10
|
+
showTimestamp?: boolean;
|
|
11
|
+
/** Callback to refresh messages after interrupt resolution */
|
|
12
|
+
onResolved?: () => void;
|
|
13
|
+
}
|
|
14
|
+
declare const InterruptBubble: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type InterruptBubble = ReturnType<typeof InterruptBubble>;
|
|
16
|
+
export default InterruptBubble;
|