@urbicon-ui/blocks 6.35.0 → 6.37.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 (124) hide show
  1. package/dist/components/Chat/Chat/Chat.svelte +53 -0
  2. package/dist/components/Chat/Chat/Chat.svelte.d.ts +4 -0
  3. package/dist/components/Chat/Chat/chat.variants.d.ts +20 -0
  4. package/dist/components/Chat/Chat/chat.variants.js +15 -0
  5. package/dist/components/Chat/Chat/index.d.ts +47 -0
  6. package/dist/components/Chat/Chat/index.js +2 -0
  7. package/dist/components/Chat/ChatMessage/ChatMessage.svelte +342 -0
  8. package/dist/components/Chat/ChatMessage/ChatMessage.svelte.d.ts +4 -0
  9. package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +138 -0
  10. package/dist/components/Chat/ChatMessage/chat-message.variants.js +118 -0
  11. package/dist/components/Chat/ChatMessage/index.d.ts +102 -0
  12. package/dist/components/Chat/ChatMessage/index.js +2 -0
  13. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte +301 -0
  14. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte.d.ts +6 -0
  15. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.d.ts +23 -0
  16. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.js +32 -0
  17. package/dist/components/Chat/ChatMessageList/chat-scroll.d.ts +45 -0
  18. package/dist/components/Chat/ChatMessageList/chat-scroll.js +52 -0
  19. package/dist/components/Chat/ChatMessageList/index.d.ts +93 -0
  20. package/dist/components/Chat/ChatMessageList/index.js +3 -0
  21. package/dist/components/Chat/CitationChip/CitationChip.svelte +107 -0
  22. package/dist/components/Chat/CitationChip/CitationChip.svelte.d.ts +4 -0
  23. package/dist/components/Chat/CitationChip/citation-chip.variants.d.ts +40 -0
  24. package/dist/components/Chat/CitationChip/citation-chip.variants.js +42 -0
  25. package/dist/components/Chat/CitationChip/index.d.ts +69 -0
  26. package/dist/components/Chat/CitationChip/index.js +2 -0
  27. package/dist/components/Chat/CodeBlock/CodeBlock.svelte +120 -0
  28. package/dist/components/Chat/CodeBlock/CodeBlock.svelte.d.ts +4 -0
  29. package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +40 -0
  30. package/dist/components/Chat/CodeBlock/code-block.variants.js +51 -0
  31. package/dist/components/Chat/CodeBlock/index.d.ts +55 -0
  32. package/dist/components/Chat/CodeBlock/index.js +2 -0
  33. package/dist/components/Chat/PromptInput/PromptInput.svelte +432 -0
  34. package/dist/components/Chat/PromptInput/PromptInput.svelte.d.ts +4 -0
  35. package/dist/components/Chat/PromptInput/index.d.ts +165 -0
  36. package/dist/components/Chat/PromptInput/index.js +2 -0
  37. package/dist/components/Chat/PromptInput/prompt-input.variants.d.ts +90 -0
  38. package/dist/components/Chat/PromptInput/prompt-input.variants.js +102 -0
  39. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte +107 -0
  40. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte.d.ts +4 -0
  41. package/dist/components/Chat/ReasoningDisclosure/index.d.ts +85 -0
  42. package/dist/components/Chat/ReasoningDisclosure/index.js +2 -0
  43. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.d.ts +20 -0
  44. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.js +31 -0
  45. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte +99 -0
  46. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte.d.ts +9 -0
  47. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte +82 -0
  48. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte.d.ts +9 -0
  49. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte +118 -0
  50. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte.d.ts +4 -0
  51. package/dist/components/Chat/StreamingMarkdown/index.d.ts +86 -0
  52. package/dist/components/Chat/StreamingMarkdown/index.js +2 -0
  53. package/dist/components/Chat/StreamingMarkdown/md-context.d.ts +58 -0
  54. package/dist/components/Chat/StreamingMarkdown/md-context.js +1 -0
  55. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.d.ts +145 -0
  56. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.js +73 -0
  57. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +174 -0
  58. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte.d.ts +4 -0
  59. package/dist/components/Chat/ToolCallCard/index.d.ts +84 -0
  60. package/dist/components/Chat/ToolCallCard/index.js +2 -0
  61. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +38 -0
  62. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +38 -0
  63. package/dist/components/Chat/chat.types.d.ts +82 -0
  64. package/dist/components/Chat/chat.types.js +7 -0
  65. package/dist/components/Chat/index.d.ts +22 -0
  66. package/dist/components/Chat/index.js +14 -0
  67. package/dist/components/Chat/markdown/blocks.d.ts +54 -0
  68. package/dist/components/Chat/markdown/blocks.js +462 -0
  69. package/dist/components/Chat/markdown/inline.d.ts +31 -0
  70. package/dist/components/Chat/markdown/inline.js +446 -0
  71. package/dist/components/Chat/markdown/repair.d.ts +23 -0
  72. package/dist/components/Chat/markdown/repair.js +337 -0
  73. package/dist/components/Chat/markdown/streaming-harness.d.ts +23 -0
  74. package/dist/components/Chat/markdown/streaming-harness.js +62 -0
  75. package/dist/components/Chat/markdown/types.d.ts +139 -0
  76. package/dist/components/Chat/markdown/types.js +13 -0
  77. package/dist/components/Chat/markdown/url-policy.d.ts +33 -0
  78. package/dist/components/Chat/markdown/url-policy.js +109 -0
  79. package/dist/components/FileUpload/FileUpload.svelte +35 -123
  80. package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
  81. package/dist/components/FileUpload/index.d.ts +2 -28
  82. package/dist/components/PinInput/PinInput.svelte +321 -0
  83. package/dist/components/PinInput/PinInput.svelte.d.ts +4 -0
  84. package/dist/components/PinInput/index.d.ts +98 -0
  85. package/dist/components/PinInput/index.js +2 -0
  86. package/dist/components/PinInput/pin-input.variants.d.ts +96 -0
  87. package/dist/components/PinInput/pin-input.variants.js +111 -0
  88. package/dist/components/QRCode/QRCode.svelte +107 -0
  89. package/dist/components/QRCode/QRCode.svelte.d.ts +4 -0
  90. package/dist/components/QRCode/index.d.ts +74 -0
  91. package/dist/components/QRCode/index.js +3 -0
  92. package/dist/components/QRCode/qr-code.variants.d.ts +25 -0
  93. package/dist/components/QRCode/qr-code.variants.js +24 -0
  94. package/dist/components/QRCode/qr-encode.d.ts +22 -0
  95. package/dist/components/QRCode/qr-encode.js +528 -0
  96. package/dist/components/TimeInput/TimeInput.svelte +521 -0
  97. package/dist/components/TimeInput/TimeInput.svelte.d.ts +4 -0
  98. package/dist/components/TimeInput/index.d.ts +93 -0
  99. package/dist/components/TimeInput/index.js +2 -0
  100. package/dist/components/TimeInput/time-input.variants.d.ts +131 -0
  101. package/dist/components/TimeInput/time-input.variants.js +120 -0
  102. package/dist/components/index.d.ts +7 -0
  103. package/dist/components/index.js +4 -0
  104. package/dist/i18n/index.d.ts +18 -0
  105. package/dist/primitives/Collapsible/Collapsible.svelte +5 -0
  106. package/dist/primitives/SplitPane/SplitPane.svelte +293 -0
  107. package/dist/primitives/SplitPane/SplitPane.svelte.d.ts +4 -0
  108. package/dist/primitives/SplitPane/index.d.ts +88 -0
  109. package/dist/primitives/SplitPane/index.js +2 -0
  110. package/dist/primitives/SplitPane/split-pane.utils.d.ts +73 -0
  111. package/dist/primitives/SplitPane/split-pane.utils.js +94 -0
  112. package/dist/primitives/SplitPane/split-pane.variants.d.ts +40 -0
  113. package/dist/primitives/SplitPane/split-pane.variants.js +56 -0
  114. package/dist/primitives/index.d.ts +2 -0
  115. package/dist/primitives/index.js +1 -0
  116. package/dist/translations/de.d.ts +6 -0
  117. package/dist/translations/de.js +6 -0
  118. package/dist/translations/en.d.ts +6 -0
  119. package/dist/translations/en.js +6 -0
  120. package/dist/utils/file-intake.d.ts +78 -0
  121. package/dist/utils/file-intake.js +144 -0
  122. package/dist/utils/index.d.ts +1 -0
  123. package/dist/utils/index.js +1 -0
  124. package/package.json +3 -3
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import { getBlocksConfig, resolveSlotClasses } from '../../../provider';
3
+ import { chatVariants } from './chat.variants';
4
+ import type { ChatProps } from './index';
5
+
6
+ let {
7
+ children,
8
+ header,
9
+ composer,
10
+ class: className,
11
+ unstyled: unstyledProp = false,
12
+ slotClasses: slotClassesProp = {},
13
+ preset,
14
+ ...restProps
15
+ }: ChatProps = $props();
16
+
17
+ const blocksConfig = getBlocksConfig();
18
+ const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
19
+
20
+ const styles = chatVariants();
21
+ const slotClasses = $derived(
22
+ resolveSlotClasses(blocksConfig, 'Chat', preset, {}, slotClassesProp)
23
+ );
24
+
25
+ function cls(name: keyof typeof slotClasses, extra?: string | (string | undefined)[]) {
26
+ if (unstyled) {
27
+ const own = slotClasses?.[name];
28
+ return [own, ...(Array.isArray(extra) ? extra : [extra])].filter(Boolean).join(' ');
29
+ }
30
+ const slotFns = styles as Record<string, (args: { class?: unknown }) => string>;
31
+ return slotFns[name]({
32
+ class: [slotClasses?.[name], ...(Array.isArray(extra) ? extra : [extra])]
33
+ });
34
+ }
35
+ </script>
36
+
37
+ <div class={cls('root', className)} {...restProps}>
38
+ {#if header}
39
+ <div class={cls('header')}>
40
+ {@render header()}
41
+ </div>
42
+ {/if}
43
+
44
+ <div class={cls('body')}>
45
+ {@render children?.()}
46
+ </div>
47
+
48
+ {#if composer}
49
+ <div class={cls('composer')}>
50
+ {@render composer()}
51
+ </div>
52
+ {/if}
53
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { ChatProps } from './index.js';
2
+ declare const Chat: import("svelte").Component<ChatProps, {}, "">;
3
+ type Chat = ReturnType<typeof Chat>;
4
+ export default Chat;
@@ -0,0 +1,20 @@
1
+ import { type SlotNames, type VariantProps } from '../../../utils/variants.js';
2
+ export declare const chatVariants: ((props?: {} | undefined) => {
3
+ root: (props?: ({} & {
4
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
5
+ }) | undefined) => string;
6
+ header: (props?: ({} & {
7
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
8
+ }) | undefined) => string;
9
+ body: (props?: ({} & {
10
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
11
+ }) | undefined) => string;
12
+ composer: (props?: ({} & {
13
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
14
+ }) | undefined) => string;
15
+ }) & {
16
+ readonly config: import("../../../utils/variants.js").TVConfig;
17
+ };
18
+ export type ChatVariants = VariantProps<typeof chatVariants>;
19
+ /** Slot names derived from the tv() config — single source of truth for slotClasses. */
20
+ export type ChatSlots = SlotNames<typeof chatVariants>;
@@ -0,0 +1,15 @@
1
+ import { tv } from '../../../utils/variants.js';
2
+ export const chatVariants = tv({
3
+ slots: {
4
+ // Fills its parent's height as a flex column. `min-h-0` is the discipline
5
+ // that lets the body child own the scroll instead of overflowing the page.
6
+ root: ['flex h-full min-h-0 w-full flex-col bg-surface-base'],
7
+ // Pinned header — never scrolls.
8
+ header: ['shrink-0 border-b border-border-subtle'],
9
+ // The conversation area. Flexes to fill and clamps to min-h-0 so its own
10
+ // scrollable child (ChatMessageList) can scroll; the container never does.
11
+ body: ['min-h-0 min-w-0 flex-1'],
12
+ // Pinned composer at the bottom.
13
+ composer: ['shrink-0 border-t border-border-subtle bg-surface-base']
14
+ }
15
+ });
@@ -0,0 +1,47 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { ChatSlots, ChatVariants } from './chat.variants.js';
4
+ /**
5
+ * @description Thin full-height layout shell for a chat surface: an optional pinned
6
+ * `header` (border-b), the scrollable conversation area as `children` (the consumer
7
+ * drops a ChatMessageList here — the shell itself never scrolls, the body child does),
8
+ * and an optional pinned `composer` at the bottom (border-t, e.g. PromptInput). No
9
+ * state, no context, no provider — pure structure with `min-h-0` flex discipline so it
10
+ * fills its parent and hands scrolling to its body.
11
+ *
12
+ * @tag ai
13
+ * @tag layout
14
+ * @related ChatMessageList
15
+ * @related PromptInput
16
+ * @stability experimental
17
+ *
18
+ * @example
19
+ * ```svelte
20
+ * <Chat>
21
+ * {#snippet header()}<Toolbar>…</Toolbar>{/snippet}
22
+ * <ChatMessageList {messages} />
23
+ * {#snippet composer()}<PromptInput onSubmit={send} />{/snippet}
24
+ * </Chat>
25
+ * ```
26
+ */
27
+ export interface ChatProps extends ChatVariants, Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class'> {
28
+ /** The scrollable conversation area — typically a ChatMessageList. */
29
+ children?: Snippet;
30
+ /** Optional pinned header rendered above the conversation (border-b). */
31
+ header?: Snippet;
32
+ /** Optional pinned composer rendered below the conversation (border-t) — e.g. PromptInput. */
33
+ composer?: Snippet;
34
+ /** Extra classes merged onto the root element. */
35
+ class?: string;
36
+ /** Remove all default tv classes. */
37
+ unstyled?: boolean;
38
+ /** Per-slot class overrides. Slots: root | header | body | composer */
39
+ slotClasses?: Partial<Record<ChatSlots, string>>;
40
+ /**
41
+ * Apply a named preset registered via `<BlocksProvider presets={{ Chat: {...} }}>`.
42
+ * Prefer this over `class` overrides for reusable custom looks.
43
+ */
44
+ preset?: string;
45
+ }
46
+ export { default as Chat } from './Chat.svelte';
47
+ export { type ChatSlots, type ChatVariants, chatVariants } from './chat.variants.js';
@@ -0,0 +1,2 @@
1
+ export { default as Chat } from './Chat.svelte';
2
+ export { chatVariants } from './chat.variants.js';
@@ -0,0 +1,342 @@
1
+ <script lang="ts">
2
+ import { getBlocksConfig, resolveSlotClasses } from '../../../provider';
3
+ import { Alert, Avatar, Button, Skeleton, Tooltip } from '../../../primitives';
4
+ import { resolveIcon } from '../../../icons';
5
+ import SparklesIconDefault from '../../../icons/SparklesIcon.svelte';
6
+ import UserIconDefault from '../../../icons/UserIcon.svelte';
7
+ import InfoIconDefault from '../../../icons/InfoCircleIcon.svelte';
8
+ import CopyIconDefault from '../../../icons/CopyIcon.svelte';
9
+ import CheckIconDefault from '../../../icons/CheckIcon.svelte';
10
+ import RefreshIconDefault from '../../../icons/RefreshIcon.svelte';
11
+ import FileIconDefault from '../../../icons/FileIcon.svelte';
12
+ import { formatFileSize } from '../../../utils/file-intake';
13
+ import StreamingMarkdown from '../StreamingMarkdown/StreamingMarkdown.svelte';
14
+ import CitationChip from '../CitationChip/CitationChip.svelte';
15
+ import ReasoningDisclosure from '../ReasoningDisclosure/ReasoningDisclosure.svelte';
16
+ import ToolCallCard from '../ToolCallCard/ToolCallCard.svelte';
17
+ import type { CitationSource } from '../CitationChip';
18
+ import { checkLinkUrl } from '../markdown/url-policy.js';
19
+ import type { ChatMessagePart, ChatRole } from '../chat.types';
20
+ import { chatMessageVariants, type ChatMessageVariants } from './chat-message.variants';
21
+ import type { ChatMessageProps } from './index';
22
+
23
+ type AttachmentPart = Extract<ChatMessagePart, { type: 'attachment' }>;
24
+
25
+ let {
26
+ message,
27
+ urlPolicy,
28
+ onRegenerate,
29
+ onRetry,
30
+ avatar,
31
+ actions,
32
+ metadata,
33
+ partRenderers,
34
+ layout = 'bubble',
35
+ density = 'comfortable',
36
+ roleLabels,
37
+ copyLabel = 'Copy message',
38
+ copiedLabel = 'Copied',
39
+ regenerateLabel = 'Regenerate',
40
+ retryLabel = 'Retry',
41
+ errorLabel = 'Something went wrong',
42
+ abortedLabel = 'Generation stopped',
43
+ class: className,
44
+ unstyled: unstyledProp = false,
45
+ slotClasses: slotClassesProp = {},
46
+ preset,
47
+ ...restProps
48
+ }: ChatMessageProps = $props();
49
+
50
+ const blocksConfig = getBlocksConfig();
51
+ const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
52
+
53
+ const SparklesIcon = resolveIcon('sparkles', SparklesIconDefault);
54
+ const UserIcon = resolveIcon('user', UserIconDefault);
55
+ const InfoIcon = resolveIcon('info', InfoIconDefault);
56
+ const CopyIcon = resolveIcon('copy', CopyIconDefault);
57
+ const CheckIcon = resolveIcon('check', CheckIconDefault);
58
+ const RefreshIcon = resolveIcon('refresh', RefreshIconDefault);
59
+ const FileIcon = resolveIcon('file', FileIconDefault);
60
+
61
+ const variantProps: ChatMessageVariants = $derived({ layout, density, role: message.role });
62
+ const styles = $derived(chatMessageVariants(variantProps));
63
+ const slotClasses = $derived(
64
+ resolveSlotClasses(blocksConfig, 'ChatMessage', preset, variantProps, slotClassesProp)
65
+ );
66
+
67
+ function cls(name: keyof typeof slotClasses, extra?: string | (string | undefined)[]) {
68
+ if (unstyled) {
69
+ const own = slotClasses?.[name];
70
+ return [own, ...(Array.isArray(extra) ? extra : [extra])].filter(Boolean).join(' ');
71
+ }
72
+ const slotFns = styles as Record<string, (args: { class?: unknown }) => string>;
73
+ return slotFns[name]({
74
+ class: [slotClasses?.[name], ...(Array.isArray(extra) ? extra : [extra])]
75
+ });
76
+ }
77
+
78
+ const status = $derived(message.status ?? 'complete');
79
+ const lastIndex = $derived(message.parts.length - 1);
80
+
81
+ // Sources are collected out of the flow into a single numbered footer row and
82
+ // also feed StreamingMarkdown so `[id]` markers resolve to chips. Deduped by
83
+ // id (first occurrence wins): models routinely cite one source repeatedly,
84
+ // and a duplicate id would crash the keyed footer each (review finding).
85
+ const sources = $derived.by<CitationSource[]>(() => {
86
+ const byId = new Map<string, CitationSource>();
87
+ for (const p of message.parts) {
88
+ if (p.type === 'source' && !byId.has(p.id)) {
89
+ byId.set(p.id, { id: p.id, title: p.title, url: p.url, snippet: p.snippet });
90
+ }
91
+ }
92
+ return [...byId.values()];
93
+ });
94
+
95
+ // Concatenated text parts — what the copy action writes to the clipboard.
96
+ const copyText = $derived(
97
+ message.parts.flatMap((p) => (p.type === 'text' ? [p.text] : [])).join('\n\n')
98
+ );
99
+
100
+ // Zero-parts streaming placeholder. An empty *trailing* text part already gets
101
+ // StreamingMarkdown's cursor, so only the genuinely empty message needs this.
102
+ const showPlaceholder = $derived(message.status === 'streaming' && message.parts.length === 0);
103
+
104
+ const DEFAULT_ROLE_LABELS: Record<ChatRole, string> = {
105
+ user: 'You',
106
+ assistant: 'Assistant',
107
+ system: 'System'
108
+ };
109
+ const roleLabel = $derived(roleLabels?.[message.role] ?? DEFAULT_ROLE_LABELS[message.role]);
110
+
111
+ const createdAtIso = $derived(message.createdAt?.toISOString());
112
+ const createdAtTime = $derived(
113
+ message.createdAt?.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
114
+ );
115
+
116
+ // ── Copy interaction ─────────────────────────────────────────────────────
117
+ let copied = $state(false);
118
+ let resetTimer: ReturnType<typeof setTimeout> | undefined;
119
+
120
+ async function copyMessage() {
121
+ try {
122
+ await navigator.clipboard.writeText(copyText);
123
+ copied = true;
124
+ if (resetTimer) clearTimeout(resetTimer);
125
+ resetTimer = setTimeout(() => {
126
+ copied = false;
127
+ resetTimer = undefined;
128
+ }, 2000);
129
+ } catch (err) {
130
+ // Never confirm a copy that failed (e.g. denied clipboard permission).
131
+ console.error('ChatMessage: failed to copy message', err);
132
+ }
133
+ }
134
+
135
+ $effect(() => () => {
136
+ if (resetTimer) clearTimeout(resetTimer);
137
+ });
138
+ </script>
139
+
140
+ {#snippet defaultAvatar(role: ChatRole)}
141
+ <Avatar size="sm" class={cls('avatar')}>
142
+ {#if role === 'assistant'}
143
+ <SparklesIcon size={16} />
144
+ {:else if role === 'user'}
145
+ <UserIcon size={16} />
146
+ {:else}
147
+ <InfoIcon size={16} />
148
+ {/if}
149
+ </Avatar>
150
+ {/snippet}
151
+
152
+ {#snippet attachmentChip(part: AttachmentPart)}
153
+ {@const linkCheck =
154
+ part.url != null ? checkLinkUrl(part.url, urlPolicy) : ({ ok: false } as const)}
155
+ {#if linkCheck.ok}
156
+ <a
157
+ class={cls('attachment')}
158
+ href={linkCheck.href}
159
+ target="_blank"
160
+ rel="noopener noreferrer"
161
+ download
162
+ >
163
+ <FileIcon size={16} class={cls('attachmentIcon')} />
164
+ <span class={cls('attachmentName')}>{part.name}</span>
165
+ {#if part.size != null}<span class={cls('attachmentSize')}>{formatFileSize(part.size)}</span
166
+ >{/if}
167
+ </a>
168
+ {:else}
169
+ <span class={cls('attachment')}>
170
+ <FileIcon size={16} class={cls('attachmentIcon')} />
171
+ <span class={cls('attachmentName')}>{part.name}</span>
172
+ {#if part.size != null}<span class={cls('attachmentSize')}>{formatFileSize(part.size)}</span
173
+ >{/if}
174
+ </span>
175
+ {/if}
176
+ {/snippet}
177
+
178
+ {#snippet body()}
179
+ <div class={cls('partsFlow')}>
180
+ {#each message.parts as part, index (index)}
181
+ <!--
182
+ Parts are append-only during streaming: array position IS identity, so
183
+ the index key is stable here (the documented exception to the
184
+ no-index-as-key rule).
185
+ -->
186
+ {#if part.type === 'text'}
187
+ {#if partRenderers?.text}
188
+ {@render partRenderers.text(part)}
189
+ {:else}
190
+ <StreamingMarkdown
191
+ content={part.text}
192
+ streaming={message.status === 'streaming' && index === lastIndex}
193
+ sources={sources.length ? sources : undefined}
194
+ {urlPolicy}
195
+ size="sm"
196
+ />
197
+ {/if}
198
+ {:else if part.type === 'reasoning'}
199
+ {#if partRenderers?.reasoning}
200
+ {@render partRenderers.reasoning(part)}
201
+ {:else}
202
+ <!-- Streaming while it is the trailing part: once the answer text
203
+ starts flowing behind it, the reasoning is settled. -->
204
+ <ReasoningDisclosure
205
+ reasoning={part}
206
+ streaming={status === 'streaming' && index === lastIndex}
207
+ {urlPolicy}
208
+ />
209
+ {/if}
210
+ {:else if part.type === 'tool-call'}
211
+ {#if partRenderers?.['tool-call']}
212
+ {@render partRenderers['tool-call'](part)}
213
+ {:else}
214
+ <ToolCallCard toolCall={part} />
215
+ {/if}
216
+ {:else if part.type === 'attachment'}
217
+ {#if partRenderers?.attachment}
218
+ {@render partRenderers.attachment(part)}
219
+ {:else}
220
+ {@render attachmentChip(part)}
221
+ {/if}
222
+ {:else if part.type === 'a2ui'}
223
+ <!-- default: render nothing. A2UIView lands in P4 via partRenderers. -->
224
+ {#if partRenderers?.a2ui}{@render partRenderers.a2ui(part)}{/if}
225
+ {/if}
226
+ <!-- part.type === 'source' is intentionally skipped: sources render in the
227
+ citation footer, never inline. -->
228
+ {/each}
229
+
230
+ {#if showPlaceholder}
231
+ <div class={cls('placeholder')}>
232
+ <Skeleton variant="text" width="40%" />
233
+ </div>
234
+ {/if}
235
+ </div>
236
+ {/snippet}
237
+
238
+ {#snippet retryAction()}
239
+ <Button size="sm" onclick={onRetry}>
240
+ <RefreshIcon size={14} />
241
+ {retryLabel}
242
+ </Button>
243
+ {/snippet}
244
+
245
+ {#snippet defaultActions()}
246
+ <div class={cls('actions')}>
247
+ <!-- No copy for text-less messages (tool-call/reasoning-only): copying an
248
+ empty string and confirming "Copied" would be a lie (review finding). -->
249
+ {#if copyText}
250
+ <Tooltip label={copyLabel}>
251
+ <button
252
+ type="button"
253
+ class={cls('actionButton')}
254
+ onclick={copyMessage}
255
+ aria-label={copied ? copiedLabel : copyLabel}
256
+ >
257
+ {#if copied}
258
+ <CheckIcon size={16} />
259
+ {:else}
260
+ <CopyIcon size={16} />
261
+ {/if}
262
+ </button>
263
+ </Tooltip>
264
+ {/if}
265
+ {#if onRegenerate}
266
+ <Tooltip label={regenerateLabel}>
267
+ <button
268
+ type="button"
269
+ class={cls('actionButton')}
270
+ onclick={onRegenerate}
271
+ aria-label={regenerateLabel}
272
+ >
273
+ <RefreshIcon size={16} />
274
+ </button>
275
+ </Tooltip>
276
+ {/if}
277
+ </div>
278
+ {/snippet}
279
+
280
+ <div class={cls('root', className)} data-role={message.role} data-status={status} {...restProps}>
281
+ {#if layout === 'plain'}
282
+ <div class={cls('header')}>
283
+ {#if avatar}
284
+ {@render avatar({ role: message.role })}
285
+ {:else}
286
+ {@render defaultAvatar(message.role)}
287
+ {/if}
288
+ <span class={cls('roleName')}>{roleLabel}</span>
289
+ </div>
290
+ <div class={cls('bubble')}>
291
+ {@render body()}
292
+ </div>
293
+ {:else}
294
+ <div class={cls('container')}>
295
+ {#if message.role !== 'user'}
296
+ {#if avatar}
297
+ {@render avatar({ role: message.role })}
298
+ {:else}
299
+ {@render defaultAvatar(message.role)}
300
+ {/if}
301
+ {/if}
302
+ <div class={cls('bubble')}>
303
+ {@render body()}
304
+ </div>
305
+ </div>
306
+ {/if}
307
+
308
+ {#if sources.length}
309
+ <div class={cls('sourcesFooter')}>
310
+ {#each sources as source, i (source.id)}
311
+ <CitationChip {source} index={i + 1} {urlPolicy} />
312
+ {/each}
313
+ </div>
314
+ {/if}
315
+
316
+ {#if status === 'error' || status === 'aborted'}
317
+ <div class={cls('statusAlert')}>
318
+ <Alert
319
+ intent={status === 'error' ? 'danger' : 'warning'}
320
+ title={status === 'error' ? errorLabel : abortedLabel}
321
+ actions={onRetry ? retryAction : undefined}
322
+ />
323
+ </div>
324
+ {/if}
325
+
326
+ <div class={cls('footer')}>
327
+ {#if actions}
328
+ {@render actions({ message })}
329
+ {:else}
330
+ {@render defaultActions()}
331
+ {/if}
332
+ {#if metadata}
333
+ {@render metadata({ message })}
334
+ {:else if createdAtTime}
335
+ <time class={cls('metadata')} datetime={createdAtIso}>{createdAtTime}</time>
336
+ {/if}
337
+ </div>
338
+
339
+ <!-- Copy confirmation for screen readers — a live status region, present
340
+ before `copied` flips so it always announces the change. -->
341
+ <span class="sr-only" role="status">{copied ? copiedLabel : ''}</span>
342
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { ChatMessageProps } from './index.js';
2
+ declare const ChatMessage: import("svelte").Component<ChatMessageProps, {}, "">;
3
+ type ChatMessage = ReturnType<typeof ChatMessage>;
4
+ export default ChatMessage;