@d34dman/flowdrop 0.0.40 → 0.0.42
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/interrupt/ChoicePrompt.svelte +7 -2
- package/dist/components/interrupt/ChoicePrompt.svelte.d.ts +2 -0
- package/dist/components/interrupt/ConfirmationPrompt.svelte +15 -3
- package/dist/components/interrupt/ConfirmationPrompt.svelte.d.ts +2 -0
- package/dist/components/interrupt/FormPrompt.svelte +7 -2
- package/dist/components/interrupt/FormPrompt.svelte.d.ts +2 -0
- package/dist/components/interrupt/InterruptBubble.svelte +14 -0
- package/dist/components/interrupt/TextInputPrompt.svelte +7 -2
- package/dist/components/interrupt/TextInputPrompt.svelte.d.ts +2 -0
- package/dist/components/playground/ChatPanel.svelte +96 -8
- package/dist/components/playground/ChatPanel.svelte.d.ts +7 -0
- package/dist/components/playground/MessageBubble.svelte +146 -60
- package/dist/components/playground/MessageBubble.svelte.d.ts +7 -0
- package/dist/types/interrupt.d.ts +2 -0
- package/dist/types/interrupt.js +2 -1
- package/dist/types/playground.d.ts +2 -0
- package/package.json +1 -1
|
@@ -25,11 +25,14 @@
|
|
|
25
25
|
isSubmitting: boolean;
|
|
26
26
|
/** Error message if submission failed */
|
|
27
27
|
error?: string;
|
|
28
|
+
/** Username of the person who resolved the interrupt */
|
|
29
|
+
resolvedByUserName?: string;
|
|
28
30
|
/** Callback when user submits selection */
|
|
29
31
|
onSubmit: (value: string | string[]) => void;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
let { config, isResolved, resolvedValue, isSubmitting, error, onSubmit }: Props =
|
|
34
|
+
let { config, isResolved, resolvedValue, isSubmitting, error, resolvedByUserName, onSubmit }: Props =
|
|
35
|
+
$props();
|
|
33
36
|
|
|
34
37
|
/** Local state for selected values */
|
|
35
38
|
let selectedValues = $state<Set<string>>(new Set());
|
|
@@ -189,7 +192,9 @@
|
|
|
189
192
|
{#if isResolved}
|
|
190
193
|
<div class="choice-prompt__resolved-badge">
|
|
191
194
|
<Icon icon="mdi:check-circle" />
|
|
192
|
-
<span>
|
|
195
|
+
<span>
|
|
196
|
+
{resolvedByUserName ? `Response submitted by ${resolvedByUserName}` : 'Response submitted'}
|
|
197
|
+
</span>
|
|
193
198
|
</div>
|
|
194
199
|
{/if}
|
|
195
200
|
</div>
|
|
@@ -13,6 +13,8 @@ interface Props {
|
|
|
13
13
|
isSubmitting: boolean;
|
|
14
14
|
/** Error message if submission failed */
|
|
15
15
|
error?: string;
|
|
16
|
+
/** Username of the person who resolved the interrupt */
|
|
17
|
+
resolvedByUserName?: string;
|
|
16
18
|
/** Callback when user submits selection */
|
|
17
19
|
onSubmit: (value: string | string[]) => void;
|
|
18
20
|
}
|
|
@@ -25,14 +25,24 @@
|
|
|
25
25
|
isSubmitting: boolean;
|
|
26
26
|
/** Error message if submission failed */
|
|
27
27
|
error?: string;
|
|
28
|
+
/** Username of the person who resolved the interrupt */
|
|
29
|
+
resolvedByUserName?: string;
|
|
28
30
|
/** Callback when user confirms (Yes) */
|
|
29
31
|
onConfirm: () => void;
|
|
30
32
|
/** Callback when user declines (No) */
|
|
31
33
|
onDecline: () => void;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
let {
|
|
35
|
-
|
|
36
|
+
let {
|
|
37
|
+
config,
|
|
38
|
+
isResolved,
|
|
39
|
+
resolvedValue,
|
|
40
|
+
isSubmitting,
|
|
41
|
+
error,
|
|
42
|
+
resolvedByUserName,
|
|
43
|
+
onConfirm,
|
|
44
|
+
onDecline
|
|
45
|
+
}: Props = $props();
|
|
36
46
|
|
|
37
47
|
/** Computed label for confirm button */
|
|
38
48
|
const confirmLabel = $derived(config.confirmLabel ?? 'Yes');
|
|
@@ -108,7 +118,9 @@
|
|
|
108
118
|
{#if isResolved}
|
|
109
119
|
<div class="confirmation-prompt__resolved-badge">
|
|
110
120
|
<Icon icon="mdi:check-circle" />
|
|
111
|
-
<span>
|
|
121
|
+
<span>
|
|
122
|
+
{resolvedByUserName ? `Response submitted by ${resolvedByUserName}` : 'Response submitted'}
|
|
123
|
+
</span>
|
|
112
124
|
</div>
|
|
113
125
|
{/if}
|
|
114
126
|
</div>
|
|
@@ -13,6 +13,8 @@ interface Props {
|
|
|
13
13
|
isSubmitting: boolean;
|
|
14
14
|
/** Error message if submission failed */
|
|
15
15
|
error?: string;
|
|
16
|
+
/** Username of the person who resolved the interrupt */
|
|
17
|
+
resolvedByUserName?: string;
|
|
16
18
|
/** Callback when user confirms (Yes) */
|
|
17
19
|
onConfirm: () => void;
|
|
18
20
|
/** Callback when user declines (No) */
|
|
@@ -26,11 +26,14 @@
|
|
|
26
26
|
isSubmitting: boolean;
|
|
27
27
|
/** Error message if submission failed */
|
|
28
28
|
error?: string;
|
|
29
|
+
/** Username of the person who resolved the interrupt */
|
|
30
|
+
resolvedByUserName?: string;
|
|
29
31
|
/** Callback when user submits form */
|
|
30
32
|
onSubmit: (value: Record<string, unknown>) => void;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
let { config, isResolved, resolvedValue, isSubmitting, error, onSubmit }: Props =
|
|
35
|
+
let { config, isResolved, resolvedValue, isSubmitting, error, resolvedByUserName, onSubmit }: Props =
|
|
36
|
+
$props();
|
|
34
37
|
|
|
35
38
|
/** Local state for form values */
|
|
36
39
|
let formValues = $state<Record<string, unknown>>(config.defaultValues ?? {});
|
|
@@ -119,7 +122,9 @@
|
|
|
119
122
|
{#if isResolved}
|
|
120
123
|
<div class="form-prompt__resolved-badge">
|
|
121
124
|
<Icon icon="mdi:check-circle" />
|
|
122
|
-
<span>
|
|
125
|
+
<span>
|
|
126
|
+
{resolvedByUserName ? `Response submitted by ${resolvedByUserName}` : 'Response submitted'}
|
|
127
|
+
</span>
|
|
123
128
|
</div>
|
|
124
129
|
{/if}
|
|
125
130
|
</div>
|
|
@@ -13,6 +13,8 @@ interface Props {
|
|
|
13
13
|
isSubmitting: boolean;
|
|
14
14
|
/** Error message if submission failed */
|
|
15
15
|
error?: string;
|
|
16
|
+
/** Username of the person who resolved the interrupt */
|
|
17
|
+
resolvedByUserName?: string;
|
|
16
18
|
/** Callback when user submits form */
|
|
17
19
|
onSubmit: (value: Record<string, unknown>) => void;
|
|
18
20
|
}
|
|
@@ -221,6 +221,16 @@
|
|
|
221
221
|
|
|
222
222
|
// Determine the actual resolved value to pass to prompt components
|
|
223
223
|
const displayResolvedValue = $derived(resolvedValue ?? currentInterrupt.responseValue);
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Extract the username of who resolved the interrupt from metadata.
|
|
227
|
+
* This is provided by the backend when the interrupt is resolved.
|
|
228
|
+
*/
|
|
229
|
+
const resolvedByUserName = $derived(
|
|
230
|
+
typeof currentInterrupt.metadata?.resolvedByUserName === 'string'
|
|
231
|
+
? currentInterrupt.metadata.resolvedByUserName
|
|
232
|
+
: undefined
|
|
233
|
+
);
|
|
224
234
|
</script>
|
|
225
235
|
|
|
226
236
|
<div
|
|
@@ -287,6 +297,7 @@
|
|
|
287
297
|
resolvedValue={displayResolvedValue as boolean | undefined}
|
|
288
298
|
{isSubmitting}
|
|
289
299
|
{error}
|
|
300
|
+
{resolvedByUserName}
|
|
290
301
|
onConfirm={() => handleResolve(true)}
|
|
291
302
|
onDecline={() => handleResolve(false)}
|
|
292
303
|
/>
|
|
@@ -297,6 +308,7 @@
|
|
|
297
308
|
resolvedValue={displayResolvedValue as string | string[] | undefined}
|
|
298
309
|
{isSubmitting}
|
|
299
310
|
{error}
|
|
311
|
+
{resolvedByUserName}
|
|
300
312
|
onSubmit={(value) => handleResolve(value)}
|
|
301
313
|
/>
|
|
302
314
|
{:else if currentInterrupt.type === 'text'}
|
|
@@ -306,6 +318,7 @@
|
|
|
306
318
|
resolvedValue={displayResolvedValue as string | undefined}
|
|
307
319
|
{isSubmitting}
|
|
308
320
|
{error}
|
|
321
|
+
{resolvedByUserName}
|
|
309
322
|
onSubmit={(value) => handleResolve(value)}
|
|
310
323
|
/>
|
|
311
324
|
{:else if currentInterrupt.type === 'form'}
|
|
@@ -315,6 +328,7 @@
|
|
|
315
328
|
resolvedValue={displayResolvedValue as Record<string, unknown> | undefined}
|
|
316
329
|
{isSubmitting}
|
|
317
330
|
{error}
|
|
331
|
+
{resolvedByUserName}
|
|
318
332
|
onSubmit={(value) => handleResolve(value)}
|
|
319
333
|
/>
|
|
320
334
|
{/if}
|
|
@@ -25,11 +25,14 @@
|
|
|
25
25
|
isSubmitting: boolean;
|
|
26
26
|
/** Error message if submission failed */
|
|
27
27
|
error?: string;
|
|
28
|
+
/** Username of the person who resolved the interrupt */
|
|
29
|
+
resolvedByUserName?: string;
|
|
28
30
|
/** Callback when user submits text */
|
|
29
31
|
onSubmit: (value: string) => void;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
let { config, isResolved, resolvedValue, isSubmitting, error, onSubmit }: Props =
|
|
34
|
+
let { config, isResolved, resolvedValue, isSubmitting, error, resolvedByUserName, onSubmit }: Props =
|
|
35
|
+
$props();
|
|
33
36
|
|
|
34
37
|
/** Local state for input value */
|
|
35
38
|
let inputValue = $state(config.defaultValue ?? '');
|
|
@@ -166,7 +169,9 @@
|
|
|
166
169
|
{#if isResolved}
|
|
167
170
|
<div class="text-prompt__resolved-badge">
|
|
168
171
|
<Icon icon="mdi:check-circle" />
|
|
169
|
-
<span>
|
|
172
|
+
<span>
|
|
173
|
+
{resolvedByUserName ? `Response submitted by ${resolvedByUserName}` : 'Response submitted'}
|
|
174
|
+
</span>
|
|
170
175
|
</div>
|
|
171
176
|
{/if}
|
|
172
177
|
</div>
|
|
@@ -13,6 +13,8 @@ interface Props {
|
|
|
13
13
|
isSubmitting: boolean;
|
|
14
14
|
/** Error message if submission failed */
|
|
15
15
|
error?: string;
|
|
16
|
+
/** Username of the person who resolved the interrupt */
|
|
17
|
+
resolvedByUserName?: string;
|
|
16
18
|
/** Callback when user submits text */
|
|
17
19
|
onSubmit: (value: string) => void;
|
|
18
20
|
}
|
|
@@ -66,6 +66,13 @@
|
|
|
66
66
|
* Used when showChatInput is false.
|
|
67
67
|
*/
|
|
68
68
|
predefinedMessage?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Whether to display system messages in compact mode.
|
|
71
|
+
* When true, system messages appear as minimal inline text
|
|
72
|
+
* instead of full chat bubbles to reduce visual noise.
|
|
73
|
+
* @default true
|
|
74
|
+
*/
|
|
75
|
+
compactSystemMessages?: boolean;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
let {
|
|
@@ -79,7 +86,8 @@
|
|
|
79
86
|
onInterruptResolved,
|
|
80
87
|
showChatInput = true,
|
|
81
88
|
showRunButton = true,
|
|
82
|
-
predefinedMessage = 'Run workflow'
|
|
89
|
+
predefinedMessage = 'Run workflow',
|
|
90
|
+
compactSystemMessages = true
|
|
83
91
|
}: Props = $props();
|
|
84
92
|
|
|
85
93
|
/**
|
|
@@ -109,6 +117,46 @@
|
|
|
109
117
|
*/
|
|
110
118
|
const displayMessages = $derived(showLogsInline ? $messages : $chatMessages);
|
|
111
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Track previous message count for detecting new messages.
|
|
122
|
+
* We only want to auto-scroll when NEW messages are added,
|
|
123
|
+
* not when existing messages are updated.
|
|
124
|
+
*/
|
|
125
|
+
let previousMessageCount = $state(0);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if user is near the bottom of the scroll container.
|
|
129
|
+
* Used to determine if we should auto-scroll when new messages arrive.
|
|
130
|
+
* If user has scrolled up to read previous messages, we don't interrupt them.
|
|
131
|
+
*
|
|
132
|
+
* @param threshold - Pixels from bottom to consider "near bottom"
|
|
133
|
+
* @returns True if user is within threshold of the bottom
|
|
134
|
+
*/
|
|
135
|
+
function isNearBottom(threshold: number = 100): boolean {
|
|
136
|
+
if (!messagesContainer) return true;
|
|
137
|
+
const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
|
|
138
|
+
return scrollHeight - scrollTop - clientHeight <= threshold;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if a form element inside the messages container has focus.
|
|
143
|
+
* When user is interacting with a form (e.g., interrupt prompt),
|
|
144
|
+
* we should not auto-scroll as it disrupts their input.
|
|
145
|
+
*/
|
|
146
|
+
function isFormFocused(): boolean {
|
|
147
|
+
if (!messagesContainer) return false;
|
|
148
|
+
const activeElement = document.activeElement;
|
|
149
|
+
if (!activeElement) return false;
|
|
150
|
+
// Check if active element is a form control inside the messages container
|
|
151
|
+
const isFormControl =
|
|
152
|
+
activeElement.tagName === 'INPUT' ||
|
|
153
|
+
activeElement.tagName === 'TEXTAREA' ||
|
|
154
|
+
activeElement.tagName === 'SELECT' ||
|
|
155
|
+
activeElement.tagName === 'BUTTON' ||
|
|
156
|
+
activeElement.getAttribute('contenteditable') === 'true';
|
|
157
|
+
return isFormControl && messagesContainer.contains(activeElement);
|
|
158
|
+
}
|
|
159
|
+
|
|
112
160
|
/**
|
|
113
161
|
* Check if a message is an interrupt request
|
|
114
162
|
*/
|
|
@@ -280,16 +328,55 @@
|
|
|
280
328
|
});
|
|
281
329
|
|
|
282
330
|
/**
|
|
283
|
-
*
|
|
331
|
+
* Smart auto-scroll to bottom when NEW messages are added.
|
|
332
|
+
*
|
|
333
|
+
* Only scrolls if:
|
|
334
|
+
* 1. autoScroll prop is enabled
|
|
335
|
+
* 2. New messages were actually added (not just updates)
|
|
336
|
+
* 3. User is already near the bottom (hasn't scrolled up to read)
|
|
337
|
+
* 4. User is not interacting with a form inside the chat
|
|
338
|
+
*
|
|
339
|
+
* This prevents disruptive scrolling when:
|
|
340
|
+
* - User is reading previous messages
|
|
341
|
+
* - User is filling out an interrupt form
|
|
342
|
+
* - Messages are being updated (e.g., status changes)
|
|
284
343
|
*/
|
|
285
344
|
$effect(() => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
345
|
+
const currentCount = displayMessages.length;
|
|
346
|
+
|
|
347
|
+
// Skip if auto-scroll is disabled or no container
|
|
348
|
+
if (!autoScroll || !messagesContainer) {
|
|
349
|
+
previousMessageCount = currentCount;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Check if this is a NEW message (count increased)
|
|
354
|
+
const hasNewMessage = currentCount > previousMessageCount;
|
|
355
|
+
|
|
356
|
+
// Update the tracked count
|
|
357
|
+
previousMessageCount = currentCount;
|
|
358
|
+
|
|
359
|
+
// Only scroll if there's a new message
|
|
360
|
+
if (!hasNewMessage) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Don't scroll if user has scrolled up to read previous messages
|
|
365
|
+
if (!isNearBottom()) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Don't scroll if user is interacting with a form
|
|
370
|
+
if (isFormFocused()) {
|
|
371
|
+
return;
|
|
292
372
|
}
|
|
373
|
+
|
|
374
|
+
// Safe to scroll to bottom
|
|
375
|
+
tick().then(() => {
|
|
376
|
+
if (messagesContainer) {
|
|
377
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
293
380
|
});
|
|
294
381
|
|
|
295
382
|
/**
|
|
@@ -453,6 +540,7 @@
|
|
|
453
540
|
showTimestamp={showTimestamps}
|
|
454
541
|
isLast={index === displayMessages.length - 1}
|
|
455
542
|
{enableMarkdown}
|
|
543
|
+
{compactSystemMessages}
|
|
456
544
|
/>
|
|
457
545
|
{/if}
|
|
458
546
|
{/each}
|
|
@@ -33,6 +33,13 @@ interface Props {
|
|
|
33
33
|
* Used when showChatInput is false.
|
|
34
34
|
*/
|
|
35
35
|
predefinedMessage?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Whether to display system messages in compact mode.
|
|
38
|
+
* When true, system messages appear as minimal inline text
|
|
39
|
+
* instead of full chat bubbles to reduce visual noise.
|
|
40
|
+
* @default true
|
|
41
|
+
*/
|
|
42
|
+
compactSystemMessages?: boolean;
|
|
36
43
|
}
|
|
37
44
|
declare const ChatPanel: import("svelte").Component<Props, {}, "">;
|
|
38
45
|
type ChatPanel = ReturnType<typeof ChatPanel>;
|
|
@@ -4,13 +4,18 @@
|
|
|
4
4
|
Renders individual messages in the playground chat interface.
|
|
5
5
|
Supports different message roles with distinct styling.
|
|
6
6
|
Supports markdown rendering for message content.
|
|
7
|
+
Supports compact mode for system messages to reduce visual noise.
|
|
7
8
|
Styled with BEM syntax.
|
|
8
9
|
-->
|
|
9
10
|
|
|
10
11
|
<script lang="ts">
|
|
11
12
|
import Icon from '@iconify/svelte';
|
|
12
13
|
import { marked } from 'marked';
|
|
13
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
PlaygroundMessage,
|
|
16
|
+
PlaygroundMessageMetadata,
|
|
17
|
+
PlaygroundMessageRole
|
|
18
|
+
} from '../../types/playground.js';
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* Component props
|
|
@@ -24,9 +29,28 @@
|
|
|
24
29
|
isLast?: boolean;
|
|
25
30
|
/** Whether to render markdown content */
|
|
26
31
|
enableMarkdown?: boolean;
|
|
27
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Use compact display mode for system messages.
|
|
34
|
+
* When true, system messages are rendered as minimal inline text
|
|
35
|
+
* instead of full chat bubbles to reduce visual noise.
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
compactSystemMessages?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let {
|
|
42
|
+
message,
|
|
43
|
+
showTimestamp = true,
|
|
44
|
+
isLast = false,
|
|
45
|
+
enableMarkdown = true,
|
|
46
|
+
compactSystemMessages = true
|
|
47
|
+
}: Props = $props();
|
|
28
48
|
|
|
29
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Determine if this message should render in compact mode.
|
|
51
|
+
* Only system messages use compact mode when enabled.
|
|
52
|
+
*/
|
|
53
|
+
const useCompactMode = $derived(message.role === 'system' && compactSystemMessages);
|
|
30
54
|
|
|
31
55
|
/**
|
|
32
56
|
* Render content as markdown or plain text
|
|
@@ -60,18 +84,22 @@
|
|
|
60
84
|
* Get the display label for the message role
|
|
61
85
|
*
|
|
62
86
|
* @param role - The message role
|
|
87
|
+
* @param metadata - Optional message metadata containing userName for user messages
|
|
63
88
|
* @returns Display label
|
|
64
89
|
*/
|
|
65
|
-
function getRoleLabel(
|
|
90
|
+
function getRoleLabel(
|
|
91
|
+
role: PlaygroundMessageRole,
|
|
92
|
+
metadata?: PlaygroundMessageMetadata
|
|
93
|
+
): string {
|
|
66
94
|
switch (role) {
|
|
67
95
|
case 'user':
|
|
68
|
-
return 'You';
|
|
96
|
+
return metadata?.userName ?? 'You';
|
|
69
97
|
case 'assistant':
|
|
70
98
|
return 'Assistant';
|
|
71
99
|
case 'system':
|
|
72
100
|
return 'System';
|
|
73
101
|
case 'log':
|
|
74
|
-
return
|
|
102
|
+
return metadata?.nodeLabel ?? 'Log';
|
|
75
103
|
default:
|
|
76
104
|
return 'Message';
|
|
77
105
|
}
|
|
@@ -121,68 +149,79 @@
|
|
|
121
149
|
}
|
|
122
150
|
</script>
|
|
123
151
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
message.metadata?.level === 'warning'}
|
|
133
|
-
class:message-bubble--last={isLast}
|
|
134
|
-
>
|
|
135
|
-
<!-- Avatar / Icon -->
|
|
136
|
-
<div class="message-bubble__avatar">
|
|
137
|
-
<Icon icon={getRoleIcon(message.role)} />
|
|
152
|
+
{#if useCompactMode}
|
|
153
|
+
<!-- Compact system message: minimal inline text without bubble -->
|
|
154
|
+
<div class="system-notice" class:system-notice--last={isLast}>
|
|
155
|
+
<Icon icon="mdi:information-outline" class="system-notice__icon" />
|
|
156
|
+
<span class="system-notice__text">{message.content}</span>
|
|
157
|
+
{#if showTimestamp}
|
|
158
|
+
<span class="system-notice__timestamp">{formatTimestamp(message.timestamp)}</span>
|
|
159
|
+
{/if}
|
|
138
160
|
</div>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
{
|
|
161
|
+
{:else}
|
|
162
|
+
<div
|
|
163
|
+
class="message-bubble"
|
|
164
|
+
class:message-bubble--user={message.role === 'user'}
|
|
165
|
+
class:message-bubble--assistant={message.role === 'assistant'}
|
|
166
|
+
class:message-bubble--system={message.role === 'system'}
|
|
167
|
+
class:message-bubble--log={message.role === 'log'}
|
|
168
|
+
class:message-bubble--log-error={message.role === 'log' && message.metadata?.level === 'error'}
|
|
169
|
+
class:message-bubble--log-warning={message.role === 'log' &&
|
|
170
|
+
message.metadata?.level === 'warning'}
|
|
171
|
+
class:message-bubble--last={isLast}
|
|
172
|
+
>
|
|
173
|
+
<!-- Avatar / Icon -->
|
|
174
|
+
<div class="message-bubble__avatar">
|
|
175
|
+
<Icon icon={getRoleIcon(message.role)} />
|
|
154
176
|
</div>
|
|
155
177
|
|
|
156
|
-
<!--
|
|
157
|
-
<div class="message-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
<!-- Metadata Footer -->
|
|
168
|
-
{#if message.metadata?.duration !== undefined || message.nodeId}
|
|
169
|
-
<div class="message-bubble__footer">
|
|
170
|
-
{#if message.nodeId}
|
|
171
|
-
<span class="message-bubble__node" title="Node ID: {message.nodeId}">
|
|
172
|
-
<Icon icon="mdi:graph" />
|
|
173
|
-
{message.metadata?.nodeLabel ?? message.nodeId}
|
|
178
|
+
<!-- Content -->
|
|
179
|
+
<div class="message-bubble__content">
|
|
180
|
+
<!-- Header -->
|
|
181
|
+
<div class="message-bubble__header">
|
|
182
|
+
<span class="message-bubble__role">{getRoleLabel(message.role, message.metadata)}</span>
|
|
183
|
+
{#if message.role === 'log' && message.metadata?.level}
|
|
184
|
+
<span class="message-bubble__log-level message-bubble__log-level--{message.metadata.level}">
|
|
185
|
+
<Icon icon={getLogLevelIcon()} />
|
|
186
|
+
{message.metadata.level.toUpperCase()}
|
|
174
187
|
</span>
|
|
175
188
|
{/if}
|
|
176
|
-
{#if
|
|
177
|
-
<span class="message-
|
|
178
|
-
<Icon icon="mdi:timer-outline" />
|
|
179
|
-
{formatDuration(message.metadata.duration)}
|
|
180
|
-
</span>
|
|
189
|
+
{#if showTimestamp}
|
|
190
|
+
<span class="message-bubble__timestamp">{formatTimestamp(message.timestamp)}</span>
|
|
181
191
|
{/if}
|
|
182
192
|
</div>
|
|
183
|
-
|
|
193
|
+
|
|
194
|
+
<!-- Message Text -->
|
|
195
|
+
<div class="message-bubble__text">
|
|
196
|
+
{#if enableMarkdown && message.role !== 'log'}
|
|
197
|
+
<!-- Markdown content - marked.js sanitizes content by default -->
|
|
198
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
199
|
+
{@html renderedContent}
|
|
200
|
+
{:else}
|
|
201
|
+
{message.content}
|
|
202
|
+
{/if}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<!-- Metadata Footer -->
|
|
206
|
+
{#if message.metadata?.duration !== undefined || message.nodeId}
|
|
207
|
+
<div class="message-bubble__footer">
|
|
208
|
+
{#if message.nodeId}
|
|
209
|
+
<span class="message-bubble__node" title="Node ID: {message.nodeId}">
|
|
210
|
+
<Icon icon="mdi:graph" />
|
|
211
|
+
{message.metadata?.nodeLabel ?? message.nodeId}
|
|
212
|
+
</span>
|
|
213
|
+
{/if}
|
|
214
|
+
{#if message.metadata?.duration !== undefined}
|
|
215
|
+
<span class="message-bubble__duration" title="Execution duration">
|
|
216
|
+
<Icon icon="mdi:timer-outline" />
|
|
217
|
+
{formatDuration(message.metadata.duration)}
|
|
218
|
+
</span>
|
|
219
|
+
{/if}
|
|
220
|
+
</div>
|
|
221
|
+
{/if}
|
|
222
|
+
</div>
|
|
184
223
|
</div>
|
|
185
|
-
|
|
224
|
+
{/if}
|
|
186
225
|
|
|
187
226
|
<style>
|
|
188
227
|
.message-bubble {
|
|
@@ -542,4 +581,51 @@
|
|
|
542
581
|
font-size: 1rem;
|
|
543
582
|
}
|
|
544
583
|
}
|
|
584
|
+
|
|
585
|
+
/* ========================================
|
|
586
|
+
Compact System Notice Styles
|
|
587
|
+
Minimal inline display for system messages
|
|
588
|
+
======================================== */
|
|
589
|
+
|
|
590
|
+
.system-notice {
|
|
591
|
+
display: flex;
|
|
592
|
+
align-items: center;
|
|
593
|
+
justify-content: center;
|
|
594
|
+
gap: 0.375rem;
|
|
595
|
+
padding: 0.375rem 0.75rem;
|
|
596
|
+
margin: 0.25rem 0;
|
|
597
|
+
font-size: 0.75rem;
|
|
598
|
+
color: #9ca3af;
|
|
599
|
+
text-align: center;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.system-notice--last {
|
|
603
|
+
margin-bottom: 0.75rem;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* Icon styling - using :global for Iconify component */
|
|
607
|
+
.system-notice :global(.system-notice__icon) {
|
|
608
|
+
flex-shrink: 0;
|
|
609
|
+
font-size: 0.875rem;
|
|
610
|
+
color: #d1d5db;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.system-notice__text {
|
|
614
|
+
color: #6b7280;
|
|
615
|
+
line-height: 1.4;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.system-notice__timestamp {
|
|
619
|
+
flex-shrink: 0;
|
|
620
|
+
font-size: 0.625rem;
|
|
621
|
+
color: #d1d5db;
|
|
622
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/* Responsive: hide timestamp on small screens for compactness */
|
|
626
|
+
@media (max-width: 640px) {
|
|
627
|
+
.system-notice__timestamp {
|
|
628
|
+
display: none;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
545
631
|
</style>
|
|
@@ -11,6 +11,13 @@ interface Props {
|
|
|
11
11
|
isLast?: boolean;
|
|
12
12
|
/** Whether to render markdown content */
|
|
13
13
|
enableMarkdown?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Use compact display mode for system messages.
|
|
16
|
+
* When true, system messages are rendered as minimal inline text
|
|
17
|
+
* instead of full chat bubbles to reduce visual noise.
|
|
18
|
+
* @default true
|
|
19
|
+
*/
|
|
20
|
+
compactSystemMessages?: boolean;
|
|
14
21
|
}
|
|
15
22
|
declare const MessageBubble: import("svelte").Component<Props, {}, "">;
|
|
16
23
|
type MessageBubble = ReturnType<typeof MessageBubble>;
|
|
@@ -264,6 +264,8 @@ export interface InterruptMessageMetadata {
|
|
|
264
264
|
multiple?: boolean;
|
|
265
265
|
min_selections?: number;
|
|
266
266
|
max_selections?: number;
|
|
267
|
+
/** Username of the person who resolved the interrupt */
|
|
268
|
+
resolvedByUserName?: string;
|
|
267
269
|
}
|
|
268
270
|
/**
|
|
269
271
|
* Type guard to check if message metadata indicates an interrupt
|
package/dist/types/interrupt.js
CHANGED
|
@@ -48,7 +48,8 @@ export function extractInterruptMetadata(metadata) {
|
|
|
48
48
|
max_length: metadata.max_length,
|
|
49
49
|
multiple: metadata.multiple,
|
|
50
50
|
min_selections: metadata.min_selections,
|
|
51
|
-
max_selections: metadata.max_selections
|
|
51
|
+
max_selections: metadata.max_selections,
|
|
52
|
+
resolvedByUserName: metadata.resolvedByUserName
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
/**
|
|
@@ -74,6 +74,8 @@ export interface PlaygroundMessageMetadata {
|
|
|
74
74
|
nodeLabel?: string;
|
|
75
75
|
/** Node output data */
|
|
76
76
|
outputs?: Record<string, unknown>;
|
|
77
|
+
/** User's display name for user-role messages (from backend) */
|
|
78
|
+
userName?: string;
|
|
77
79
|
/** Allow additional properties */
|
|
78
80
|
[key: string]: unknown;
|
|
79
81
|
}
|