@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.
Files changed (47) hide show
  1. package/dist/components/ConfigForm.svelte +1 -0
  2. package/dist/components/SchemaForm.svelte +1 -0
  3. package/dist/components/form/FormAutocomplete.svelte +67 -10
  4. package/dist/components/form/FormField.svelte +21 -0
  5. package/dist/components/form/FormFieldLight.svelte +1 -0
  6. package/dist/components/interrupt/InterruptBubble.svelte +76 -17
  7. package/dist/components/interrupt/InterruptBubble.svelte.d.ts +11 -0
  8. package/dist/components/playground/ChatBubble.svelte +289 -0
  9. package/dist/components/playground/ChatBubble.svelte.d.ts +10 -0
  10. package/dist/components/playground/HierarchyTrail.svelte +88 -0
  11. package/dist/components/playground/HierarchyTrail.svelte.d.ts +7 -0
  12. package/dist/components/playground/LogRow.svelte +178 -0
  13. package/dist/components/playground/LogRow.svelte.d.ts +8 -0
  14. package/dist/components/playground/MessageBubble.stories.svelte +89 -0
  15. package/dist/components/playground/MessageBubble.svelte +25 -737
  16. package/dist/components/playground/MessageBubble.svelte.d.ts +3 -11
  17. package/dist/components/playground/MessageCard.svelte +106 -0
  18. package/dist/components/playground/MessageCard.svelte.d.ts +10 -0
  19. package/dist/components/playground/MessageMarkdown.svelte +160 -0
  20. package/dist/components/playground/MessageMarkdown.svelte.d.ts +7 -0
  21. package/dist/components/playground/MessageNotice.svelte +120 -0
  22. package/dist/components/playground/MessageNotice.svelte.d.ts +9 -0
  23. package/dist/components/playground/MessageStream.svelte +85 -1
  24. package/dist/components/playground/MessageTagChip.svelte +99 -0
  25. package/dist/components/playground/MessageTagChip.svelte.d.ts +7 -0
  26. package/dist/components/playground/MessageTagStrip.svelte +37 -0
  27. package/dist/components/playground/MessageTagStrip.svelte.d.ts +7 -0
  28. package/dist/components/playground/PlaygroundStudio.svelte +78 -0
  29. package/dist/components/playground/messageDisplay.d.ts +19 -0
  30. package/dist/components/playground/messageDisplay.js +62 -0
  31. package/dist/form/autocomplete.d.ts +1 -0
  32. package/dist/form/autocomplete.js +1 -0
  33. package/dist/form/index.d.ts +17 -0
  34. package/dist/form/index.js +19 -0
  35. package/dist/messages/defaults.d.ts +5 -0
  36. package/dist/messages/defaults.js +6 -0
  37. package/dist/schemas/v1/workflow.schema.json +10 -1
  38. package/dist/services/categoriesApi.d.ts +2 -1
  39. package/dist/services/categoriesApi.js +5 -3
  40. package/dist/services/portConfigApi.d.ts +2 -1
  41. package/dist/services/portConfigApi.js +5 -3
  42. package/dist/svelte-app.d.ts +1 -0
  43. package/dist/svelte-app.js +5 -5
  44. package/dist/types/index.d.ts +13 -0
  45. package/dist/types/playground.d.ts +76 -0
  46. package/dist/types/playground.js +14 -0
  47. 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
- return `${url}${separator}${encodeURIComponent(queryParam)}=${encodeURIComponent(query)}`;
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
- abortController = new AbortController();
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
- // Fetch with timeout
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: abortController.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
- let { interrupt: initialInterrupt, showTimestamp = true, onResolved }: Props = $props();
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
- <span class="interrupt-bubble__timestamp">
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
- </span>
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
- {#if currentInterrupt.nodeId}
355
- <span
356
- class="interrupt-bubble__node"
357
- title={t.nodeIdTooltip({ id: currentInterrupt.nodeId })}
358
- >
359
- <Icon icon="mdi:graph" />
360
- <span>{t.fromWorkflow}</span>
361
- </span>
362
- {/if}
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>