@flowdrop/flowdrop 1.12.0 → 1.13.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/dist/components/ConfigForm.svelte +1 -0
- package/dist/components/SchemaForm.svelte +1 -0
- package/dist/components/form/FormAutocomplete.svelte +67 -10
- package/dist/components/form/FormField.svelte +21 -0
- package/dist/components/form/FormFieldLight.svelte +1 -0
- package/dist/components/interrupt/InterruptBubble.svelte +76 -17
- package/dist/components/interrupt/InterruptBubble.svelte.d.ts +11 -0
- package/dist/components/playground/ChatBubble.svelte +289 -0
- package/dist/components/playground/ChatBubble.svelte.d.ts +10 -0
- package/dist/components/playground/HierarchyTrail.svelte +88 -0
- package/dist/components/playground/HierarchyTrail.svelte.d.ts +7 -0
- package/dist/components/playground/LogRow.svelte +178 -0
- package/dist/components/playground/LogRow.svelte.d.ts +8 -0
- package/dist/components/playground/MessageBubble.stories.svelte +89 -0
- package/dist/components/playground/MessageBubble.svelte +25 -737
- package/dist/components/playground/MessageBubble.svelte.d.ts +3 -11
- package/dist/components/playground/MessageCard.svelte +106 -0
- package/dist/components/playground/MessageCard.svelte.d.ts +10 -0
- package/dist/components/playground/MessageMarkdown.svelte +160 -0
- package/dist/components/playground/MessageMarkdown.svelte.d.ts +7 -0
- package/dist/components/playground/MessageNotice.svelte +120 -0
- package/dist/components/playground/MessageNotice.svelte.d.ts +9 -0
- package/dist/components/playground/MessageStream.svelte +85 -1
- package/dist/components/playground/MessageTagChip.svelte +99 -0
- package/dist/components/playground/MessageTagChip.svelte.d.ts +7 -0
- package/dist/components/playground/MessageTagStrip.svelte +37 -0
- package/dist/components/playground/MessageTagStrip.svelte.d.ts +7 -0
- package/dist/components/playground/PlaygroundStudio.svelte +78 -0
- package/dist/components/playground/messageDisplay.d.ts +19 -0
- package/dist/components/playground/messageDisplay.js +62 -0
- package/dist/form/autocomplete.d.ts +1 -0
- package/dist/form/autocomplete.js +1 -0
- package/dist/form/index.d.ts +17 -0
- package/dist/form/index.js +19 -0
- package/dist/messages/defaults.d.ts +5 -0
- package/dist/messages/defaults.js +6 -0
- package/dist/schemas/v1/workflow.schema.json +10 -1
- package/dist/services/categoriesApi.d.ts +2 -1
- package/dist/services/categoriesApi.js +5 -3
- package/dist/services/portConfigApi.d.ts +2 -1
- package/dist/services/portConfigApi.js +5 -3
- package/dist/svelte-app.d.ts +1 -0
- package/dist/svelte-app.js +5 -5
- package/dist/types/index.d.ts +13 -0
- package/dist/types/playground.d.ts +76 -0
- package/dist/types/playground.js +14 -0
- package/package.json +6 -1
|
@@ -191,6 +191,7 @@
|
|
|
191
191
|
* This fixes the Svelte 5 reactivity warnings
|
|
192
192
|
*/
|
|
193
193
|
let configValues = $state<Record<string, unknown>>({});
|
|
194
|
+
setContext<() => Record<string, unknown>>('flowdrop:getFormValues', () => configValues);
|
|
194
195
|
|
|
195
196
|
/**
|
|
196
197
|
* UI Extension values for display settings
|
|
@@ -197,6 +197,7 @@
|
|
|
197
197
|
* Internal reactive state for form values
|
|
198
198
|
*/
|
|
199
199
|
let formValues = $state<Record<string, unknown>>({});
|
|
200
|
+
setContext<() => Record<string, unknown>>('flowdrop:getFormValues', () => formValues);
|
|
200
201
|
|
|
201
202
|
/**
|
|
202
203
|
* Initialize form values when schema or values change
|
|
@@ -68,6 +68,9 @@
|
|
|
68
68
|
'flowdrop:getAuthProvider'
|
|
69
69
|
);
|
|
70
70
|
const getBaseUrl = getContext<(() => string) | undefined>('flowdrop:getBaseUrl');
|
|
71
|
+
const getFormValues = getContext<(() => Record<string, unknown>) | undefined>(
|
|
72
|
+
'flowdrop:getFormValues'
|
|
73
|
+
);
|
|
71
74
|
|
|
72
75
|
// Hoist the autocomplete branch — seven reads in the template, one inside
|
|
73
76
|
// an {#each tag} loop. One getter walk per render.
|
|
@@ -82,6 +85,7 @@
|
|
|
82
85
|
const valueField = $derived(autocomplete.valueField ?? 'value');
|
|
83
86
|
const allowFreeText = $derived(autocomplete.allowFreeText ?? false);
|
|
84
87
|
const multiple = $derived(autocomplete.multiple ?? false);
|
|
88
|
+
const params = $derived(autocomplete.params ?? {});
|
|
85
89
|
|
|
86
90
|
// Component state
|
|
87
91
|
let inputElement: HTMLInputElement | undefined = $state(undefined);
|
|
@@ -102,6 +106,33 @@
|
|
|
102
106
|
// Cache of value-to-label mappings for selected items
|
|
103
107
|
let labelCache = $state<Map<string, string>>(new Map());
|
|
104
108
|
|
|
109
|
+
// Resolved values for each param dependency: { paramName -> currentFieldValue }
|
|
110
|
+
// Used both in buildUrl (appending to query) and in the dep-change effect.
|
|
111
|
+
const depParamValues = $derived.by(() => {
|
|
112
|
+
const all = getFormValues?.() ?? {};
|
|
113
|
+
const result: Record<string, string> = {};
|
|
114
|
+
for (const [paramName, fieldName] of Object.entries(params)) {
|
|
115
|
+
const val = all[fieldName];
|
|
116
|
+
if (val !== undefined && val !== '') result[paramName] = String(val);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Stable fingerprint — any change triggers selection clearing.
|
|
122
|
+
// JSON.stringify gives a canonical string without null-byte ambiguity.
|
|
123
|
+
const depFingerprint = $derived(JSON.stringify(depParamValues));
|
|
124
|
+
let prevDepFingerprint = depFingerprint;
|
|
125
|
+
|
|
126
|
+
$effect(() => {
|
|
127
|
+
if (Object.keys(params).length > 0 && !getFormValues) {
|
|
128
|
+
logger.warn(
|
|
129
|
+
'[FormAutocomplete] `params` is configured but no flowdrop:getFormValues context ' +
|
|
130
|
+
'was found. Dependent field values will not be passed to the autocomplete URL. ' +
|
|
131
|
+
'Ensure this component is rendered inside ConfigForm or SchemaForm.'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
105
136
|
// Generate unique IDs for accessibility
|
|
106
137
|
// svelte-ignore state_referenced_locally — id prop never changes
|
|
107
138
|
const listboxId = `${id}-listbox`;
|
|
@@ -160,7 +191,13 @@
|
|
|
160
191
|
: `${baseUrl}${autocomplete.url}`;
|
|
161
192
|
|
|
162
193
|
const separator = url.includes('?') ? '&' : '?';
|
|
163
|
-
|
|
194
|
+
let fullUrl = `${url}${separator}${encodeURIComponent(queryParam)}=${encodeURIComponent(query)}`;
|
|
195
|
+
|
|
196
|
+
for (const [paramName, val] of Object.entries(depParamValues)) {
|
|
197
|
+
fullUrl += `&${encodeURIComponent(paramName)}=${encodeURIComponent(val)}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return fullUrl;
|
|
164
201
|
}
|
|
165
202
|
|
|
166
203
|
/**
|
|
@@ -198,25 +235,25 @@
|
|
|
198
235
|
|
|
199
236
|
isLoading = true;
|
|
200
237
|
error = null;
|
|
201
|
-
|
|
238
|
+
// Capture in a local const so the timeout closure and fetch signal
|
|
239
|
+
// always reference the same controller, even if a new request starts
|
|
240
|
+
// before the 5-second timeout fires.
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
abortController = controller;
|
|
202
243
|
|
|
244
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
203
245
|
try {
|
|
204
246
|
// Build headers with authentication (call getter to get current value)
|
|
205
247
|
const headers = await buildFetchHeaders(getAuthProvider?.());
|
|
206
248
|
|
|
207
|
-
|
|
208
|
-
const timeoutId = setTimeout(() => {
|
|
209
|
-
abortController?.abort();
|
|
210
|
-
}, 5000);
|
|
249
|
+
timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
211
250
|
|
|
212
251
|
const response = await fetch(buildUrl(query), {
|
|
213
252
|
method: 'GET',
|
|
214
253
|
headers,
|
|
215
|
-
signal:
|
|
254
|
+
signal: controller.signal
|
|
216
255
|
});
|
|
217
256
|
|
|
218
|
-
clearTimeout(timeoutId);
|
|
219
|
-
|
|
220
257
|
if (!response.ok) {
|
|
221
258
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
222
259
|
}
|
|
@@ -240,6 +277,7 @@
|
|
|
240
277
|
error = err instanceof Error ? err.message : 'Failed to fetch suggestions';
|
|
241
278
|
suggestions = [];
|
|
242
279
|
} finally {
|
|
280
|
+
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
243
281
|
isLoading = false;
|
|
244
282
|
abortController = null;
|
|
245
283
|
}
|
|
@@ -297,6 +335,13 @@
|
|
|
297
335
|
setTimeout(() => {
|
|
298
336
|
hideDropdown();
|
|
299
337
|
|
|
338
|
+
// When dependent params are configured, the suggestion list is bound to the
|
|
339
|
+
// current values of sibling fields. Clear it on blur so that fetchOnFocus
|
|
340
|
+
// always re-fetches with the current dep values rather than showing stale results.
|
|
341
|
+
if (Object.keys(params).length > 0) {
|
|
342
|
+
suggestions = [];
|
|
343
|
+
}
|
|
344
|
+
|
|
300
345
|
// If not allowFreeText and single mode, validate the input
|
|
301
346
|
if (!allowFreeText && !multiple && inputValue !== '') {
|
|
302
347
|
const currentVal = selectedValues;
|
|
@@ -542,6 +587,18 @@
|
|
|
542
587
|
}
|
|
543
588
|
});
|
|
544
589
|
|
|
590
|
+
$effect(() => {
|
|
591
|
+
const current = depFingerprint;
|
|
592
|
+
if (current === prevDepFingerprint) return;
|
|
593
|
+
prevDepFingerprint = current;
|
|
594
|
+
// A dependency field changed — abort any in-flight fetch, clear state
|
|
595
|
+
abortController?.abort();
|
|
596
|
+
suggestions = [];
|
|
597
|
+
labelCache = new Map();
|
|
598
|
+
if (multiple) onChange([]);
|
|
599
|
+
else onChange('');
|
|
600
|
+
});
|
|
601
|
+
|
|
545
602
|
/**
|
|
546
603
|
* Cleanup on unmount
|
|
547
604
|
*/
|
|
@@ -567,11 +624,11 @@
|
|
|
567
624
|
class:form-autocomplete--has-value={selectedValues.length > 0}
|
|
568
625
|
>
|
|
569
626
|
<!-- Main input container styled like a textfield/textarea -->
|
|
627
|
+
<!-- svelte-ignore a11y_no_static_element_interactions — role="presentation"; keyboard interaction is handled by the <input> inside -->
|
|
570
628
|
<div
|
|
571
629
|
class="form-autocomplete__field"
|
|
572
630
|
class:form-autocomplete__field--focused={isOpen}
|
|
573
631
|
onclick={() => inputElement?.focus()}
|
|
574
|
-
onkeydown={() => {}}
|
|
575
632
|
role="presentation"
|
|
576
633
|
>
|
|
577
634
|
<!-- Selected tags -->
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
import { getSchemaOptions } from './types.js';
|
|
46
46
|
import type { WorkflowNode, WorkflowEdge, AuthProvider } from '../../types/index.js';
|
|
47
47
|
import { getResolvedTheme } from '../../stores/settingsStore.svelte.js';
|
|
48
|
+
import { fieldComponentRegistry } from '../../form/fieldRegistry.js';
|
|
48
49
|
|
|
49
50
|
interface Props {
|
|
50
51
|
/** Unique key/id for the field */
|
|
@@ -224,6 +225,10 @@
|
|
|
224
225
|
return [];
|
|
225
226
|
});
|
|
226
227
|
|
|
228
|
+
const registeredAutocompleteComponent = $derived(
|
|
229
|
+
fieldType === 'autocomplete' ? fieldComponentRegistry.resolveFieldComponent(schema) : null
|
|
230
|
+
);
|
|
231
|
+
|
|
227
232
|
/**
|
|
228
233
|
* Get autocomplete value - can be string or string[] based on multiple setting
|
|
229
234
|
*/
|
|
@@ -387,6 +392,22 @@
|
|
|
387
392
|
{authProvider}
|
|
388
393
|
onChange={(val) => onChange(val)}
|
|
389
394
|
/>
|
|
395
|
+
{:else if fieldType === 'autocomplete' && schema.autocomplete && registeredAutocompleteComponent}
|
|
396
|
+
<registeredAutocompleteComponent.component
|
|
397
|
+
id={fieldKey}
|
|
398
|
+
value={autocompleteValue}
|
|
399
|
+
placeholder={schema.placeholder ?? ''}
|
|
400
|
+
{required}
|
|
401
|
+
{schema}
|
|
402
|
+
ariaDescribedBy={descriptionId}
|
|
403
|
+
disabled={isReadOnly}
|
|
404
|
+
{node}
|
|
405
|
+
{nodes}
|
|
406
|
+
{edges}
|
|
407
|
+
{workflowId}
|
|
408
|
+
{authProvider}
|
|
409
|
+
onChange={(val: unknown) => onChange(val)}
|
|
410
|
+
/>
|
|
390
411
|
{:else if fieldType === 'autocomplete' && schema.autocomplete}
|
|
391
412
|
<FormAutocomplete
|
|
392
413
|
id={fieldKey}
|
|
@@ -243,6 +243,7 @@
|
|
|
243
243
|
spellChecker={schema.spellChecker as boolean | undefined}
|
|
244
244
|
variables={schema.variables}
|
|
245
245
|
placeholderExample={schema.placeholderExample as string | undefined}
|
|
246
|
+
autocomplete={schema.autocomplete}
|
|
246
247
|
onChange={(val: unknown) => onChange(val)}
|
|
247
248
|
/>
|
|
248
249
|
{:else if fieldType === 'checkbox-group'}
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
import TextInputPrompt from './TextInputPrompt.svelte';
|
|
15
15
|
import FormPrompt from './FormPrompt.svelte';
|
|
16
16
|
import ReviewPrompt from './ReviewPrompt.svelte';
|
|
17
|
+
import MessageTagStrip from '../playground/MessageTagStrip.svelte';
|
|
18
|
+
import HierarchyTrail from '../playground/HierarchyTrail.svelte';
|
|
19
|
+
import type {
|
|
20
|
+
MessageHierarchyItem,
|
|
21
|
+
MessageTag
|
|
22
|
+
} from '../../types/playground.js';
|
|
17
23
|
import type {
|
|
18
24
|
Interrupt,
|
|
19
25
|
InterruptType,
|
|
@@ -49,9 +55,25 @@
|
|
|
49
55
|
showTimestamp?: boolean;
|
|
50
56
|
/** Callback to refresh messages after interrupt resolution */
|
|
51
57
|
onResolved?: () => void;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Hierarchy items forwarded from the parent playground message. Rendered
|
|
60
|
+
* as a chevron-separated trail in the footer.
|
|
61
|
+
*/
|
|
62
|
+
hierarchy?: MessageHierarchyItem[];
|
|
63
|
+
/**
|
|
64
|
+
* Server-emitted tags forwarded from the parent playground message.
|
|
65
|
+
* Rendered as chips in the footer.
|
|
66
|
+
*/
|
|
67
|
+
tags?: MessageTag[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let {
|
|
71
|
+
interrupt: initialInterrupt,
|
|
72
|
+
showTimestamp = true,
|
|
73
|
+
onResolved,
|
|
74
|
+
hierarchy,
|
|
75
|
+
tags
|
|
76
|
+
}: Props = $props();
|
|
55
77
|
|
|
56
78
|
/**
|
|
57
79
|
* Get the current interrupt state from the store.
|
|
@@ -61,6 +83,11 @@
|
|
|
61
83
|
getInterruptsMap().get(initialInterrupt.id) ?? addMachineState(initialInterrupt)
|
|
62
84
|
);
|
|
63
85
|
|
|
86
|
+
const hierarchyItems = $derived(hierarchy ?? []);
|
|
87
|
+
const tagItems = $derived(tags ?? []);
|
|
88
|
+
const hasHierarchy = $derived(hierarchyItems.length > 0);
|
|
89
|
+
const hasTags = $derived(tagItems.length > 0);
|
|
90
|
+
|
|
64
91
|
/**
|
|
65
92
|
* Helper to ensure interrupt has machine state
|
|
66
93
|
*/
|
|
@@ -262,7 +289,7 @@
|
|
|
262
289
|
<!-- Header -->
|
|
263
290
|
<div class="interrupt-bubble__header">
|
|
264
291
|
<span class="interrupt-bubble__type">
|
|
265
|
-
<Icon icon={getTypeIcon(currentInterrupt.type)} />
|
|
292
|
+
<Icon icon={getTypeIcon(currentInterrupt.type)} aria-hidden="true" />
|
|
266
293
|
{#if isResolved}
|
|
267
294
|
{currentInterrupt.machineState.status === 'cancelled'
|
|
268
295
|
? t.cancelled
|
|
@@ -274,9 +301,13 @@
|
|
|
274
301
|
{/if}
|
|
275
302
|
</span>
|
|
276
303
|
{#if showTimestamp}
|
|
277
|
-
<
|
|
304
|
+
<time
|
|
305
|
+
class="interrupt-bubble__timestamp"
|
|
306
|
+
datetime={currentInterrupt.resolvedAt ?? currentInterrupt.createdAt}
|
|
307
|
+
aria-label="sent at {formatTimestamp(currentInterrupt.resolvedAt ?? currentInterrupt.createdAt)}"
|
|
308
|
+
>
|
|
278
309
|
{formatTimestamp(currentInterrupt.resolvedAt ?? currentInterrupt.createdAt)}
|
|
279
|
-
</
|
|
310
|
+
</time>
|
|
280
311
|
{/if}
|
|
281
312
|
</div>
|
|
282
313
|
|
|
@@ -349,17 +380,21 @@
|
|
|
349
380
|
</div>
|
|
350
381
|
|
|
351
382
|
<!-- Footer -->
|
|
352
|
-
{#if currentInterrupt.nodeId || (currentInterrupt.allowCancel && !isResolved && currentInterrupt.type !== 'confirmation')}
|
|
383
|
+
{#if currentInterrupt.nodeId || hasHierarchy || hasTags || (currentInterrupt.allowCancel && !isResolved && currentInterrupt.type !== 'confirmation')}
|
|
353
384
|
<div class="interrupt-bubble__footer">
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
385
|
+
<div class="interrupt-bubble__attribution">
|
|
386
|
+
{#if currentInterrupt.nodeId}
|
|
387
|
+
<span
|
|
388
|
+
class="interrupt-bubble__node"
|
|
389
|
+
title={t.nodeIdTooltip({ id: currentInterrupt.nodeId })}
|
|
390
|
+
>
|
|
391
|
+
<Icon icon="mdi:graph" aria-hidden="true" />
|
|
392
|
+
<span>{t.fromWorkflow}</span>
|
|
393
|
+
</span>
|
|
394
|
+
{/if}
|
|
395
|
+
<HierarchyTrail items={hierarchyItems} />
|
|
396
|
+
<MessageTagStrip tags={tagItems} />
|
|
397
|
+
</div>
|
|
363
398
|
{#if currentInterrupt.allowCancel && !isResolved && currentInterrupt.type !== 'confirmation'}
|
|
364
399
|
<button
|
|
365
400
|
type="button"
|
|
@@ -367,7 +402,7 @@
|
|
|
367
402
|
onclick={handleCancel}
|
|
368
403
|
disabled={isSubmitting}
|
|
369
404
|
>
|
|
370
|
-
<Icon icon="mdi:close" />
|
|
405
|
+
<Icon icon="mdi:close" aria-hidden="true" />
|
|
371
406
|
<span>{t.cancel}</span>
|
|
372
407
|
</button>
|
|
373
408
|
{/if}
|
|
@@ -542,6 +577,7 @@
|
|
|
542
577
|
/* Footer */
|
|
543
578
|
.interrupt-bubble__footer {
|
|
544
579
|
display: flex;
|
|
580
|
+
flex-wrap: wrap;
|
|
545
581
|
align-items: center;
|
|
546
582
|
justify-content: space-between;
|
|
547
583
|
gap: var(--fd-space-xs);
|
|
@@ -565,6 +601,15 @@
|
|
|
565
601
|
border-top-color: var(--fd-interrupt-prompt-border-error);
|
|
566
602
|
}
|
|
567
603
|
|
|
604
|
+
.interrupt-bubble__attribution {
|
|
605
|
+
display: flex;
|
|
606
|
+
flex-wrap: wrap;
|
|
607
|
+
align-items: center;
|
|
608
|
+
gap: var(--fd-space-xs);
|
|
609
|
+
min-width: 0;
|
|
610
|
+
flex: 1 1 auto;
|
|
611
|
+
}
|
|
612
|
+
|
|
568
613
|
.interrupt-bubble__node {
|
|
569
614
|
display: flex;
|
|
570
615
|
align-items: center;
|
|
@@ -618,5 +663,19 @@
|
|
|
618
663
|
padding-left: var(--fd-space-lg);
|
|
619
664
|
padding-right: var(--fd-space-lg);
|
|
620
665
|
}
|
|
666
|
+
|
|
667
|
+
.interrupt-bubble__cancel-btn {
|
|
668
|
+
min-height: 2.5rem;
|
|
669
|
+
padding: var(--fd-space-xs) var(--fd-space-md);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
@media (prefers-reduced-motion: reduce) {
|
|
674
|
+
.interrupt-bubble {
|
|
675
|
+
animation: none;
|
|
676
|
+
}
|
|
677
|
+
.interrupt-bubble__cancel-btn {
|
|
678
|
+
transition: none;
|
|
679
|
+
}
|
|
621
680
|
}
|
|
622
681
|
</style>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MessageHierarchyItem, MessageTag } from '../../types/playground.js';
|
|
1
2
|
import type { Interrupt } from '../../types/interrupt.js';
|
|
2
3
|
import { type InterruptWithState } from '../../stores/interruptStore.svelte.js';
|
|
3
4
|
/**
|
|
@@ -10,6 +11,16 @@ interface Props {
|
|
|
10
11
|
showTimestamp?: boolean;
|
|
11
12
|
/** Callback to refresh messages after interrupt resolution */
|
|
12
13
|
onResolved?: () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Hierarchy items forwarded from the parent playground message. Rendered
|
|
16
|
+
* as a chevron-separated trail in the footer.
|
|
17
|
+
*/
|
|
18
|
+
hierarchy?: MessageHierarchyItem[];
|
|
19
|
+
/**
|
|
20
|
+
* Server-emitted tags forwarded from the parent playground message.
|
|
21
|
+
* Rendered as chips in the footer.
|
|
22
|
+
*/
|
|
23
|
+
tags?: MessageTag[];
|
|
13
24
|
}
|
|
14
25
|
declare const InterruptBubble: import("svelte").Component<Props, {}, "">;
|
|
15
26
|
type InterruptBubble = ReturnType<typeof InterruptBubble>;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
ChatBubble — avatar + bubble layout for user/assistant/system messages.
|
|
3
|
+
Markdown typography comes from MessageMarkdown; user-bubble overrides
|
|
4
|
+
(primary-bg) are scoped here.
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import Icon from '@iconify/svelte';
|
|
9
|
+
import type { PlaygroundMessage } from '../../types/playground.js';
|
|
10
|
+
import HierarchyTrail from './HierarchyTrail.svelte';
|
|
11
|
+
import MessageTagStrip from './MessageTagStrip.svelte';
|
|
12
|
+
import MessageMarkdown from './MessageMarkdown.svelte';
|
|
13
|
+
import {
|
|
14
|
+
formatDuration,
|
|
15
|
+
formatTimestamp,
|
|
16
|
+
getRoleIcon,
|
|
17
|
+
getRoleLabel
|
|
18
|
+
} from './messageDisplay.js';
|
|
19
|
+
import { m } from '../../messages/index.js';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
message: PlaygroundMessage;
|
|
23
|
+
showTimestamp?: boolean;
|
|
24
|
+
isLast?: boolean;
|
|
25
|
+
enableMarkdown?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let { message, showTimestamp = true, isLast = false, enableMarkdown = true }: Props = $props();
|
|
29
|
+
|
|
30
|
+
const hierarchy = $derived(message.hierarchy ?? []);
|
|
31
|
+
const tags = $derived(message.tags ?? []);
|
|
32
|
+
const roleLabel = $derived(getRoleLabel(message, m().playground.roles));
|
|
33
|
+
const hasFooter = $derived(
|
|
34
|
+
message.metadata?.duration !== undefined || !!message.nodeId || tags.length > 0
|
|
35
|
+
);
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<article
|
|
39
|
+
class="message-bubble"
|
|
40
|
+
class:message-bubble--user={message.role === 'user'}
|
|
41
|
+
class:message-bubble--assistant={message.role === 'assistant'}
|
|
42
|
+
class:message-bubble--system={message.role === 'system'}
|
|
43
|
+
class:message-bubble--last={isLast}
|
|
44
|
+
aria-label="{roleLabel} message"
|
|
45
|
+
>
|
|
46
|
+
<div class="message-bubble__avatar" aria-hidden="true">
|
|
47
|
+
<Icon icon={getRoleIcon(message.role)} />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="message-bubble__content">
|
|
51
|
+
<div class="message-bubble__header">
|
|
52
|
+
<span class="message-bubble__role">{roleLabel}</span>
|
|
53
|
+
{#if showTimestamp}
|
|
54
|
+
<time
|
|
55
|
+
class="message-bubble__timestamp"
|
|
56
|
+
datetime={message.timestamp}
|
|
57
|
+
aria-label="sent at {formatTimestamp(message.timestamp)}"
|
|
58
|
+
>{formatTimestamp(message.timestamp)}</time>
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{#if hierarchy.length > 0}
|
|
63
|
+
<div class="message-bubble__hierarchy">
|
|
64
|
+
<HierarchyTrail items={hierarchy} />
|
|
65
|
+
</div>
|
|
66
|
+
{/if}
|
|
67
|
+
|
|
68
|
+
<MessageMarkdown content={message.content} {enableMarkdown} />
|
|
69
|
+
|
|
70
|
+
{#if hasFooter}
|
|
71
|
+
<div class="message-bubble__footer">
|
|
72
|
+
{#if message.nodeId}
|
|
73
|
+
<span
|
|
74
|
+
class="message-bubble__node"
|
|
75
|
+
title={m().playground.messageTooltips.nodeId({ id: message.nodeId })}
|
|
76
|
+
>
|
|
77
|
+
<Icon icon="mdi:vector-square" aria-hidden="true" />
|
|
78
|
+
via {message.metadata?.nodeLabel ?? message.nodeId}
|
|
79
|
+
</span>
|
|
80
|
+
{/if}
|
|
81
|
+
{#if message.metadata?.duration !== undefined}
|
|
82
|
+
<span
|
|
83
|
+
class="message-bubble__duration"
|
|
84
|
+
title={m().playground.messageTooltips.executionDuration}
|
|
85
|
+
aria-label="execution duration {formatDuration(message.metadata.duration)}"
|
|
86
|
+
>
|
|
87
|
+
<Icon icon="mdi:timer-outline" aria-hidden="true" />
|
|
88
|
+
{formatDuration(message.metadata.duration)}
|
|
89
|
+
</span>
|
|
90
|
+
{/if}
|
|
91
|
+
<MessageTagStrip {tags} />
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
</article>
|
|
96
|
+
|
|
97
|
+
<style>
|
|
98
|
+
.message-bubble {
|
|
99
|
+
display: flex;
|
|
100
|
+
gap: var(--fd-space-sm);
|
|
101
|
+
padding: 2px var(--fd-space-xl);
|
|
102
|
+
margin-bottom: 2px;
|
|
103
|
+
align-items: flex-end;
|
|
104
|
+
/* fd-fade-in + reduced-motion guard live in MessageStream.svelte */
|
|
105
|
+
animation: fd-fade-in 0.18s ease-out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.message-bubble--user {
|
|
109
|
+
flex-direction: row-reverse;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.message-bubble--last {
|
|
113
|
+
margin-bottom: var(--fd-space-xl);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.message-bubble__avatar {
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
width: 1.875rem;
|
|
119
|
+
height: 1.875rem;
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
border-radius: var(--fd-radius-full);
|
|
124
|
+
font-size: 1rem;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.message-bubble--user .message-bubble__avatar {
|
|
128
|
+
background-color: var(--fd-primary);
|
|
129
|
+
color: var(--fd-primary-foreground);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.message-bubble--assistant .message-bubble__avatar {
|
|
133
|
+
background-color: var(--fd-secondary);
|
|
134
|
+
color: var(--fd-secondary-foreground);
|
|
135
|
+
border: 1px solid var(--fd-border);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.message-bubble--system .message-bubble__avatar {
|
|
139
|
+
background-color: var(--fd-muted);
|
|
140
|
+
color: var(--fd-muted-foreground);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.message-bubble__content {
|
|
144
|
+
min-width: 0;
|
|
145
|
+
max-width: 78%;
|
|
146
|
+
padding: var(--fd-space-sm) var(--fd-space-md);
|
|
147
|
+
border-radius: var(--fd-radius-2xl);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.message-bubble--user .message-bubble__content {
|
|
151
|
+
background-color: var(--fd-primary);
|
|
152
|
+
color: var(--fd-primary-foreground);
|
|
153
|
+
border-bottom-right-radius: var(--fd-radius-sm);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.message-bubble--assistant .message-bubble__content {
|
|
157
|
+
background-color: var(--fd-card);
|
|
158
|
+
border: 1px solid var(--fd-border);
|
|
159
|
+
color: var(--fd-card-foreground);
|
|
160
|
+
box-shadow: 0 1px 3px 0 oklch(0% 0 0 / 0.06), 0 1px 2px -1px oklch(0% 0 0 / 0.04);
|
|
161
|
+
border-bottom-left-radius: var(--fd-radius-sm);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.message-bubble--system .message-bubble__content {
|
|
165
|
+
background-color: var(--fd-muted);
|
|
166
|
+
border: 1px solid var(--fd-border);
|
|
167
|
+
color: var(--fd-muted-foreground);
|
|
168
|
+
font-size: var(--fd-text-sm);
|
|
169
|
+
max-width: 88%;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.message-bubble__header {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: var(--fd-space-xs);
|
|
176
|
+
margin-bottom: var(--fd-space-3xs);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.message-bubble--user .message-bubble__header {
|
|
180
|
+
flex-direction: row-reverse;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.message-bubble__role {
|
|
184
|
+
font-weight: 600;
|
|
185
|
+
font-size: var(--fd-text-xs);
|
|
186
|
+
text-transform: uppercase;
|
|
187
|
+
letter-spacing: 0.05em;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.message-bubble--user .message-bubble__role {
|
|
191
|
+
color: var(--fd-primary-foreground);
|
|
192
|
+
opacity: 0.75;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.message-bubble--assistant .message-bubble__role,
|
|
196
|
+
.message-bubble--system .message-bubble__role {
|
|
197
|
+
color: var(--fd-muted-foreground);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.message-bubble__timestamp {
|
|
201
|
+
font-size: 0.6875rem;
|
|
202
|
+
font-family: var(--fd-font-mono);
|
|
203
|
+
opacity: 0.55;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.message-bubble--user .message-bubble__timestamp {
|
|
207
|
+
color: var(--fd-primary-foreground);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.message-bubble--assistant .message-bubble__timestamp {
|
|
211
|
+
color: var(--fd-muted-foreground);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.message-bubble__hierarchy {
|
|
215
|
+
margin: var(--fd-space-3xs) 0 var(--fd-space-xs);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Override markdown styling on the primary-bg user bubble */
|
|
219
|
+
.message-bubble--user :global(.message-markdown code) {
|
|
220
|
+
background-color: color-mix(in srgb, var(--fd-primary-foreground) 18%, transparent);
|
|
221
|
+
color: var(--fd-primary-foreground);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.message-bubble--user :global(.message-markdown pre) {
|
|
225
|
+
background-color: rgb(0 0 0 / 0.25);
|
|
226
|
+
color: var(--fd-primary-foreground);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.message-bubble--user :global(.message-markdown a) {
|
|
230
|
+
color: var(--fd-primary-foreground);
|
|
231
|
+
text-decoration: underline;
|
|
232
|
+
opacity: 0.85;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.message-bubble--user :global(.message-markdown blockquote) {
|
|
236
|
+
border-left-color: color-mix(in srgb, var(--fd-primary-foreground) 40%, transparent);
|
|
237
|
+
color: var(--fd-primary-foreground);
|
|
238
|
+
opacity: 0.8;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.message-bubble__footer {
|
|
242
|
+
display: flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
flex-wrap: wrap;
|
|
245
|
+
gap: var(--fd-space-md);
|
|
246
|
+
margin-top: var(--fd-space-xs);
|
|
247
|
+
padding-top: var(--fd-space-3xs);
|
|
248
|
+
border-top: 1px solid var(--fd-border);
|
|
249
|
+
font-size: var(--fd-text-xs);
|
|
250
|
+
color: var(--fd-muted-foreground);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.message-bubble--user .message-bubble__footer {
|
|
254
|
+
justify-content: flex-end;
|
|
255
|
+
border-top-color: color-mix(in srgb, var(--fd-primary-foreground) 20%, transparent);
|
|
256
|
+
color: var(--fd-primary-foreground);
|
|
257
|
+
opacity: 0.75;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.message-bubble__node,
|
|
261
|
+
.message-bubble__duration {
|
|
262
|
+
display: flex;
|
|
263
|
+
align-items: center;
|
|
264
|
+
gap: var(--fd-space-3xs);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@media (max-width: 640px) {
|
|
268
|
+
.message-bubble {
|
|
269
|
+
padding: 2px var(--fd-space-md);
|
|
270
|
+
gap: var(--fd-space-xs);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.message-bubble__content {
|
|
274
|
+
max-width: calc(100% - 2.5rem);
|
|
275
|
+
padding: var(--fd-space-xs) var(--fd-space-sm);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.message-bubble__avatar {
|
|
279
|
+
width: 1.625rem;
|
|
280
|
+
height: 1.625rem;
|
|
281
|
+
font-size: var(--fd-text-sm);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.message-bubble__footer {
|
|
285
|
+
gap: var(--fd-space-xs);
|
|
286
|
+
font-size: var(--fd-text-2xs);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
</style>
|