@urbicon-ui/blocks 6.35.0 → 6.36.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/Chat/Chat/Chat.svelte +53 -0
- package/dist/components/Chat/Chat/Chat.svelte.d.ts +4 -0
- package/dist/components/Chat/Chat/chat.variants.d.ts +20 -0
- package/dist/components/Chat/Chat/chat.variants.js +15 -0
- package/dist/components/Chat/Chat/index.d.ts +47 -0
- package/dist/components/Chat/Chat/index.js +2 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte +342 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte.d.ts +4 -0
- package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +138 -0
- package/dist/components/Chat/ChatMessage/chat-message.variants.js +118 -0
- package/dist/components/Chat/ChatMessage/index.d.ts +102 -0
- package/dist/components/Chat/ChatMessage/index.js +2 -0
- package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte +301 -0
- package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte.d.ts +6 -0
- package/dist/components/Chat/ChatMessageList/chat-message-list.variants.d.ts +23 -0
- package/dist/components/Chat/ChatMessageList/chat-message-list.variants.js +32 -0
- package/dist/components/Chat/ChatMessageList/chat-scroll.d.ts +45 -0
- package/dist/components/Chat/ChatMessageList/chat-scroll.js +52 -0
- package/dist/components/Chat/ChatMessageList/index.d.ts +93 -0
- package/dist/components/Chat/ChatMessageList/index.js +3 -0
- package/dist/components/Chat/CitationChip/CitationChip.svelte +107 -0
- package/dist/components/Chat/CitationChip/CitationChip.svelte.d.ts +4 -0
- package/dist/components/Chat/CitationChip/citation-chip.variants.d.ts +40 -0
- package/dist/components/Chat/CitationChip/citation-chip.variants.js +42 -0
- package/dist/components/Chat/CitationChip/index.d.ts +69 -0
- package/dist/components/Chat/CitationChip/index.js +2 -0
- package/dist/components/Chat/CodeBlock/CodeBlock.svelte +120 -0
- package/dist/components/Chat/CodeBlock/CodeBlock.svelte.d.ts +4 -0
- package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +40 -0
- package/dist/components/Chat/CodeBlock/code-block.variants.js +51 -0
- package/dist/components/Chat/CodeBlock/index.d.ts +55 -0
- package/dist/components/Chat/CodeBlock/index.js +2 -0
- package/dist/components/Chat/PromptInput/PromptInput.svelte +432 -0
- package/dist/components/Chat/PromptInput/PromptInput.svelte.d.ts +4 -0
- package/dist/components/Chat/PromptInput/index.d.ts +165 -0
- package/dist/components/Chat/PromptInput/index.js +2 -0
- package/dist/components/Chat/PromptInput/prompt-input.variants.d.ts +90 -0
- package/dist/components/Chat/PromptInput/prompt-input.variants.js +102 -0
- package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte +107 -0
- package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte.d.ts +4 -0
- package/dist/components/Chat/ReasoningDisclosure/index.d.ts +85 -0
- package/dist/components/Chat/ReasoningDisclosure/index.js +2 -0
- package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.d.ts +20 -0
- package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.js +31 -0
- package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte +99 -0
- package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte.d.ts +9 -0
- package/dist/components/Chat/StreamingMarkdown/MdInline.svelte +82 -0
- package/dist/components/Chat/StreamingMarkdown/MdInline.svelte.d.ts +9 -0
- package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte +118 -0
- package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte.d.ts +4 -0
- package/dist/components/Chat/StreamingMarkdown/index.d.ts +86 -0
- package/dist/components/Chat/StreamingMarkdown/index.js +2 -0
- package/dist/components/Chat/StreamingMarkdown/md-context.d.ts +58 -0
- package/dist/components/Chat/StreamingMarkdown/md-context.js +1 -0
- package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.d.ts +145 -0
- package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.js +73 -0
- package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +174 -0
- package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte.d.ts +4 -0
- package/dist/components/Chat/ToolCallCard/index.d.ts +84 -0
- package/dist/components/Chat/ToolCallCard/index.js +2 -0
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +38 -0
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +38 -0
- package/dist/components/Chat/chat.types.d.ts +82 -0
- package/dist/components/Chat/chat.types.js +7 -0
- package/dist/components/Chat/index.d.ts +22 -0
- package/dist/components/Chat/index.js +14 -0
- package/dist/components/Chat/markdown/blocks.d.ts +54 -0
- package/dist/components/Chat/markdown/blocks.js +462 -0
- package/dist/components/Chat/markdown/inline.d.ts +31 -0
- package/dist/components/Chat/markdown/inline.js +446 -0
- package/dist/components/Chat/markdown/repair.d.ts +23 -0
- package/dist/components/Chat/markdown/repair.js +337 -0
- package/dist/components/Chat/markdown/streaming-harness.d.ts +23 -0
- package/dist/components/Chat/markdown/streaming-harness.js +62 -0
- package/dist/components/Chat/markdown/types.d.ts +139 -0
- package/dist/components/Chat/markdown/types.js +13 -0
- package/dist/components/Chat/markdown/url-policy.d.ts +33 -0
- package/dist/components/Chat/markdown/url-policy.js +109 -0
- package/dist/components/FileUpload/FileUpload.svelte +35 -123
- package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
- package/dist/components/FileUpload/index.d.ts +2 -28
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/primitives/Collapsible/Collapsible.svelte +5 -0
- package/dist/primitives/SplitPane/SplitPane.svelte +293 -0
- package/dist/primitives/SplitPane/SplitPane.svelte.d.ts +4 -0
- package/dist/primitives/SplitPane/index.d.ts +88 -0
- package/dist/primitives/SplitPane/index.js +2 -0
- package/dist/primitives/SplitPane/split-pane.utils.d.ts +73 -0
- package/dist/primitives/SplitPane/split-pane.utils.js +94 -0
- package/dist/primitives/SplitPane/split-pane.variants.d.ts +40 -0
- package/dist/primitives/SplitPane/split-pane.variants.js +56 -0
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/utils/file-intake.d.ts +78 -0
- package/dist/utils/file-intake.js +144 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- 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,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,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>
|