@classic-homes/agent-chat 0.2.1 → 0.4.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/README.md +18 -2
- package/dist/components/AgentChat.svelte +10 -0
- package/dist/components/AgentChat.svelte.d.ts +5 -0
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/ArtifactChip.svelte +45 -0
- package/dist/components/ArtifactChip.svelte.d.ts +14 -0
- package/dist/components/ArtifactChip.svelte.d.ts.map +1 -0
- package/dist/components/AssistantMessage.svelte +186 -0
- package/dist/components/AssistantMessage.svelte.d.ts +26 -0
- package/dist/components/AssistantMessage.svelte.d.ts.map +1 -0
- package/dist/components/Composer.svelte +221 -30
- package/dist/components/Composer.svelte.d.ts +8 -0
- package/dist/components/Composer.svelte.d.ts.map +1 -1
- package/dist/components/Markdown.svelte +87 -5
- package/dist/components/Markdown.svelte.d.ts +9 -1
- package/dist/components/Markdown.svelte.d.ts.map +1 -1
- package/dist/components/MessageActions.svelte +56 -0
- package/dist/components/MessageActions.svelte.d.ts +17 -0
- package/dist/components/MessageActions.svelte.d.ts.map +1 -0
- package/dist/components/MessageFiles.svelte +83 -0
- package/dist/components/MessageFiles.svelte.d.ts +14 -0
- package/dist/components/MessageFiles.svelte.d.ts.map +1 -0
- package/dist/components/MessageItem.svelte +12 -144
- package/dist/components/MessageItem.svelte.d.ts +4 -0
- package/dist/components/MessageItem.svelte.d.ts.map +1 -1
- package/dist/components/MessageList.svelte +102 -30
- package/dist/components/MessageList.svelte.d.ts +2 -0
- package/dist/components/MessageList.svelte.d.ts.map +1 -1
- package/dist/components/ProposalCard.svelte +39 -0
- package/dist/components/ProposalCard.svelte.d.ts +13 -0
- package/dist/components/ProposalCard.svelte.d.ts.map +1 -0
- package/dist/components/ToolTimeline.svelte +138 -54
- package/dist/components/ToolTimeline.svelte.d.ts +11 -3
- package/dist/components/ToolTimeline.svelte.d.ts.map +1 -1
- package/dist/components/UserMessage.svelte +46 -0
- package/dist/components/UserMessage.svelte.d.ts +10 -0
- package/dist/components/UserMessage.svelte.d.ts.map +1 -0
- package/dist/components/composer.svelte.test.d.ts +2 -0
- package/dist/components/composer.svelte.test.d.ts.map +1 -0
- package/dist/components/composer.svelte.test.js +162 -0
- package/dist/components/messages.svelte.test.d.ts +2 -0
- package/dist/components/messages.svelte.test.d.ts.map +1 -0
- package/dist/components/messages.svelte.test.js +202 -0
- package/dist/composer-keys.d.ts +3 -0
- package/dist/composer-keys.d.ts.map +1 -1
- package/dist/composer-keys.js +18 -0
- package/dist/composer-prompts.d.ts +16 -0
- package/dist/composer-prompts.d.ts.map +1 -0
- package/dist/composer-prompts.js +27 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +12 -4
- package/dist/message-format.d.ts +58 -0
- package/dist/message-format.d.ts.map +1 -0
- package/dist/message-format.js +149 -0
- package/package.json +3 -3
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Files the agent produced in a turn: images as thumbnails that open in a lightbox, everything
|
|
4
|
+
else as a download card with a type icon and size.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { cn, Icon, Lightbox } from '@classic-homes/theme-svelte';
|
|
8
|
+
import type { FileState } from '../reducer.js';
|
|
9
|
+
import { fileIcon, formatBytes } from '../message-format.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
files: FileState[];
|
|
13
|
+
fileUrl: (fileId: string) => string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
let { files, fileUrl, class: className }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const images = $derived(files.filter((f) => f.mimeType.startsWith('image/')));
|
|
19
|
+
const others = $derived(files.filter((f) => !f.mimeType.startsWith('image/')));
|
|
20
|
+
const items = $derived(
|
|
21
|
+
images.map((f) => ({
|
|
22
|
+
id: f.fileId,
|
|
23
|
+
kind: 'image' as const,
|
|
24
|
+
src: fileUrl(f.fileId),
|
|
25
|
+
alt: f.name,
|
|
26
|
+
title: f.name,
|
|
27
|
+
})),
|
|
28
|
+
);
|
|
29
|
+
let open = $state(false);
|
|
30
|
+
let index = $state(0);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class={cn('flex flex-col gap-2', className)}>
|
|
34
|
+
{#if images.length}
|
|
35
|
+
<ul class="flex flex-wrap gap-2" aria-label="Images">
|
|
36
|
+
{#each images as file, i (file.fileId)}
|
|
37
|
+
<li>
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
class="block overflow-hidden rounded-lg border border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
41
|
+
aria-label={`View ${file.name}`}
|
|
42
|
+
onclick={() => {
|
|
43
|
+
index = i;
|
|
44
|
+
open = true;
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<img
|
|
48
|
+
class="block h-40 max-w-xs object-cover"
|
|
49
|
+
src={fileUrl(file.fileId)}
|
|
50
|
+
alt={file.name}
|
|
51
|
+
loading="lazy"
|
|
52
|
+
/>
|
|
53
|
+
</button>
|
|
54
|
+
</li>
|
|
55
|
+
{/each}
|
|
56
|
+
</ul>
|
|
57
|
+
<Lightbox {items} bind:open bind:index downloadable showCaption />
|
|
58
|
+
{/if}
|
|
59
|
+
{#if others.length}
|
|
60
|
+
<ul class="flex flex-wrap gap-2" aria-label="Files">
|
|
61
|
+
{#each others as file (file.fileId)}
|
|
62
|
+
<li>
|
|
63
|
+
<a
|
|
64
|
+
class="flex max-w-xs items-center gap-3 rounded-lg border border-border bg-background px-3 py-2 text-sm shadow-sm transition hover:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
65
|
+
href={fileUrl(file.fileId)}
|
|
66
|
+
download={file.name}
|
|
67
|
+
>
|
|
68
|
+
<span
|
|
69
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground"
|
|
70
|
+
>
|
|
71
|
+
<Icon name={fileIcon(file.mimeType)} size={18} />
|
|
72
|
+
</span>
|
|
73
|
+
<span class="min-w-0 flex-1">
|
|
74
|
+
<span class="block truncate font-medium text-foreground">{file.name}</span>
|
|
75
|
+
<span class="block text-xs text-muted-foreground">{formatBytes(file.bytes)}</span>
|
|
76
|
+
</span>
|
|
77
|
+
<Icon name="download" size={16} class="text-muted-foreground" />
|
|
78
|
+
</a>
|
|
79
|
+
</li>
|
|
80
|
+
{/each}
|
|
81
|
+
</ul>
|
|
82
|
+
{/if}
|
|
83
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FileState } from '../reducer.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
files: FileState[];
|
|
4
|
+
fileUrl: (fileId: string) => string;
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Files the agent produced in a turn: images as thumbnails that open in a lightbox, everything
|
|
9
|
+
* else as a download card with a type icon and size.
|
|
10
|
+
*/
|
|
11
|
+
declare const MessageFiles: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type MessageFiles = ReturnType<typeof MessageFiles>;
|
|
13
|
+
export default MessageFiles;
|
|
14
|
+
//# sourceMappingURL=MessageFiles.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MessageFiles.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageFiles.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAI7C,UAAU,KAAK;IACb,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8DH;;;GAGG;AACH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -1,164 +1,32 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
One transcript entry: a `UserMessage` or an `AssistantMessage`.
|
|
4
|
+
-->
|
|
1
5
|
<script lang="ts">
|
|
2
|
-
import { Button, Icon } from '@classic-homes/theme-svelte';
|
|
3
|
-
import Markdown from './Markdown.svelte';
|
|
4
6
|
import type { ChatMessage } from '../reducer.js';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import ToolTimeline from './ToolTimeline.svelte';
|
|
8
|
-
import UiFrame from './UiFrame.svelte';
|
|
7
|
+
import AssistantMessage from './AssistantMessage.svelte';
|
|
8
|
+
import UserMessage from './UserMessage.svelte';
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
11
|
message: ChatMessage;
|
|
12
12
|
streaming?: boolean;
|
|
13
|
+
last?: boolean;
|
|
13
14
|
sandboxOrigin: string;
|
|
14
15
|
fileUrl: (fileId: string) => string;
|
|
16
|
+
toolLabels?: Record<string, string>;
|
|
15
17
|
onnavigate?: (href: string) => void;
|
|
16
18
|
onopenartifact?: (artifactId: string) => void;
|
|
17
19
|
onprompt?: (text: string) => void;
|
|
18
20
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
19
21
|
onnotify?: (message: string) => void;
|
|
20
22
|
onretry?: () => void;
|
|
23
|
+
oncontinue?: () => void;
|
|
21
24
|
}
|
|
22
|
-
let {
|
|
23
|
-
message,
|
|
24
|
-
streaming = false,
|
|
25
|
-
sandboxOrigin,
|
|
26
|
-
fileUrl,
|
|
27
|
-
onnavigate,
|
|
28
|
-
onopenartifact,
|
|
29
|
-
onprompt,
|
|
30
|
-
ontool,
|
|
31
|
-
onnotify,
|
|
32
|
-
onretry,
|
|
33
|
-
}: Props = $props();
|
|
34
|
-
|
|
35
|
-
// Re-parsing markdown on every token is the dominant streaming cost; throttle it.
|
|
36
|
-
const PARSE_THROTTLE_MS = 120;
|
|
37
|
-
let html = $state('');
|
|
38
|
-
let lastParse = 0;
|
|
39
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
40
|
-
$effect(() => {
|
|
41
|
-
if (message.role !== 'assistant') return;
|
|
42
|
-
const text = message.text;
|
|
43
|
-
const now = Date.now();
|
|
44
|
-
if (!streaming || now - lastParse >= PARSE_THROTTLE_MS) {
|
|
45
|
-
if (timer) clearTimeout(timer);
|
|
46
|
-
timer = null;
|
|
47
|
-
lastParse = now;
|
|
48
|
-
html = parseMarkdown(text);
|
|
49
|
-
} else if (!timer) {
|
|
50
|
-
timer = setTimeout(
|
|
51
|
-
() => {
|
|
52
|
-
timer = null;
|
|
53
|
-
lastParse = Date.now();
|
|
54
|
-
html = parseMarkdown(message.role === 'assistant' ? message.text : '');
|
|
55
|
-
},
|
|
56
|
-
PARSE_THROTTLE_MS - (now - lastParse),
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
$effect(() => () => {
|
|
61
|
-
if (timer) clearTimeout(timer);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
function handleClick(event: MouseEvent): void {
|
|
65
|
-
if (!onnavigate) return;
|
|
66
|
-
const anchor = (event.target as HTMLElement | null)?.closest?.('a.agent-internal-link');
|
|
67
|
-
if (!(anchor instanceof HTMLAnchorElement)) return;
|
|
68
|
-
event.preventDefault();
|
|
69
|
-
onnavigate(anchor.getAttribute('href') ?? anchor.href);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function formatBytes(n: number): string {
|
|
73
|
-
return n < 1024
|
|
74
|
-
? `${n} B`
|
|
75
|
-
: n < 1048576
|
|
76
|
-
? `${(n / 1024).toFixed(0)} KB`
|
|
77
|
-
: `${(n / 1048576).toFixed(1)} MB`;
|
|
78
|
-
}
|
|
25
|
+
let { message, last = false, ...rest }: Props = $props();
|
|
79
26
|
</script>
|
|
80
27
|
|
|
81
28
|
{#if message.role === 'user'}
|
|
82
|
-
<
|
|
83
|
-
{#if message.contextLabel}<div class="text-xs text-muted-foreground">
|
|
84
|
-
Context: {message.contextLabel}
|
|
85
|
-
</div>{/if}
|
|
86
|
-
<div
|
|
87
|
-
class="whitespace-pre-wrap rounded-2xl rounded-br-md bg-primary px-3.5 py-2 text-primary-foreground [overflow-wrap:anywhere]"
|
|
88
|
-
>
|
|
89
|
-
{message.text}
|
|
90
|
-
</div>
|
|
91
|
-
{#if message.attachments.length}
|
|
92
|
-
<ul class="flex flex-wrap justify-end gap-2 text-xs text-muted-foreground">
|
|
93
|
-
{#each message.attachments as file (file.fileId)}<li class="inline-flex items-center gap-1">
|
|
94
|
-
<Icon name="file" size={12} />
|
|
95
|
-
{file.name}
|
|
96
|
-
</li>{/each}
|
|
97
|
-
</ul>
|
|
98
|
-
{/if}
|
|
99
|
-
</div>
|
|
29
|
+
<UserMessage {message} {last} />
|
|
100
30
|
{:else}
|
|
101
|
-
<
|
|
102
|
-
<ToolTimeline tools={message.tools} delegations={message.delegations} {streaming} />
|
|
103
|
-
{#if html}<Markdown {html} onclick={handleClick} />{/if}
|
|
104
|
-
{#if streaming && !message.text}<span class="italic text-muted-foreground">Thinking…</span>{/if}
|
|
105
|
-
{#if message.artifacts.length}
|
|
106
|
-
<div class="my-2 flex flex-wrap gap-1.5">
|
|
107
|
-
{#each message.artifacts as a (a.artifactId)}
|
|
108
|
-
<Button variant="outline" size="sm" onclick={() => onopenartifact?.(a.artifactId)}>
|
|
109
|
-
<Icon name="file-text" size={14} />
|
|
110
|
-
{a.title ?? a.artifactId}{a.open ? '…' : ''}
|
|
111
|
-
</Button>
|
|
112
|
-
{/each}
|
|
113
|
-
</div>
|
|
114
|
-
{/if}
|
|
115
|
-
{#each message.ui as resource (resource.callId + resource.uri)}
|
|
116
|
-
<UiFrame {resource} {sandboxOrigin} {onprompt} {ontool} {onnotify} />
|
|
117
|
-
{/each}
|
|
118
|
-
{#if message.files.length}
|
|
119
|
-
<ul class="mt-1 flex flex-wrap gap-2 text-sm">
|
|
120
|
-
{#each message.files as file (file.fileId)}
|
|
121
|
-
<li>
|
|
122
|
-
{#if file.mimeType.startsWith('image/')}
|
|
123
|
-
<img
|
|
124
|
-
class="mb-1 block max-h-96 max-w-full rounded-lg"
|
|
125
|
-
src={fileUrl(file.fileId)}
|
|
126
|
-
alt={file.name}
|
|
127
|
-
loading="lazy"
|
|
128
|
-
/>
|
|
129
|
-
{/if}
|
|
130
|
-
<a
|
|
131
|
-
class="inline-flex items-center gap-1 text-primary underline-offset-2 hover:underline"
|
|
132
|
-
href={fileUrl(file.fileId)}
|
|
133
|
-
download={file.name}
|
|
134
|
-
><Icon name="download" size={14} /> {file.name} ({formatBytes(file.bytes)})</a
|
|
135
|
-
>
|
|
136
|
-
</li>
|
|
137
|
-
{/each}
|
|
138
|
-
</ul>
|
|
139
|
-
{/if}
|
|
140
|
-
{#if message.proposals.length}
|
|
141
|
-
<div class="mt-1 text-xs text-muted-foreground">
|
|
142
|
-
Proposed {message.proposals.length} document edit{message.proposals.length === 1
|
|
143
|
-
? ''
|
|
144
|
-
: 's'}; apply them from the Excel or Word add-in.
|
|
145
|
-
</div>
|
|
146
|
-
{/if}
|
|
147
|
-
{#if message.error}
|
|
148
|
-
<div
|
|
149
|
-
class="mt-2 rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
|
150
|
-
role="alert"
|
|
151
|
-
>
|
|
152
|
-
<strong>{errorTitle(message.error.code)}.</strong>
|
|
153
|
-
{message.error.message}
|
|
154
|
-
{#if message.error.retryable && onretry}<Button variant="link" size="sm" onclick={onretry}
|
|
155
|
-
>Try again</Button
|
|
156
|
-
>{/if}
|
|
157
|
-
</div>
|
|
158
|
-
{:else if message.status === 'cancelled'}
|
|
159
|
-
<div class="mt-1 text-xs text-muted-foreground">Stopped.</div>
|
|
160
|
-
{:else if message.status === 'incomplete'}
|
|
161
|
-
<div class="mt-1 text-xs text-muted-foreground">The answer was cut short.</div>
|
|
162
|
-
{/if}
|
|
163
|
-
</div>
|
|
31
|
+
<AssistantMessage {message} {last} {...rest} />
|
|
164
32
|
{/if}
|
|
@@ -2,15 +2,19 @@ import type { ChatMessage } from '../reducer.js';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
message: ChatMessage;
|
|
4
4
|
streaming?: boolean;
|
|
5
|
+
last?: boolean;
|
|
5
6
|
sandboxOrigin: string;
|
|
6
7
|
fileUrl: (fileId: string) => string;
|
|
8
|
+
toolLabels?: Record<string, string>;
|
|
7
9
|
onnavigate?: (href: string) => void;
|
|
8
10
|
onopenartifact?: (artifactId: string) => void;
|
|
9
11
|
onprompt?: (text: string) => void;
|
|
10
12
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
11
13
|
onnotify?: (message: string) => void;
|
|
12
14
|
onretry?: () => void;
|
|
15
|
+
oncontinue?: () => void;
|
|
13
16
|
}
|
|
17
|
+
/** One transcript entry: a `UserMessage` or an `AssistantMessage`. */
|
|
14
18
|
declare const MessageItem: import("svelte").Component<Props, {}, "">;
|
|
15
19
|
type MessageItem = ReturnType<typeof MessageItem>;
|
|
16
20
|
export default MessageItem;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageItem.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageItem.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MessageItem.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageItem.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAkBH,sEAAsE;AACtE,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tick } from 'svelte';
|
|
3
|
+
import { Button, Icon } from '@classic-homes/theme-svelte';
|
|
3
4
|
import type { ChatMessage } from '../reducer.js';
|
|
5
|
+
import { daySeparators, formatDay } from '../message-format.js';
|
|
4
6
|
import MessageItem from './MessageItem.svelte';
|
|
5
7
|
|
|
6
8
|
interface Props {
|
|
@@ -8,12 +10,14 @@
|
|
|
8
10
|
streaming: boolean;
|
|
9
11
|
sandboxOrigin: string;
|
|
10
12
|
fileUrl: (fileId: string) => string;
|
|
13
|
+
toolLabels?: Record<string, string>;
|
|
11
14
|
onnavigate?: (href: string) => void;
|
|
12
15
|
onopenartifact?: (artifactId: string) => void;
|
|
13
16
|
onprompt?: (text: string) => void;
|
|
14
17
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
15
18
|
onnotify?: (message: string) => void;
|
|
16
19
|
onretry?: () => void;
|
|
20
|
+
oncontinue?: () => void;
|
|
17
21
|
empty?: import('svelte').Snippet;
|
|
18
22
|
}
|
|
19
23
|
let {
|
|
@@ -21,39 +25,77 @@
|
|
|
21
25
|
streaming,
|
|
22
26
|
sandboxOrigin,
|
|
23
27
|
fileUrl,
|
|
28
|
+
toolLabels,
|
|
24
29
|
onnavigate,
|
|
25
30
|
onopenartifact,
|
|
26
31
|
onprompt,
|
|
27
32
|
ontool,
|
|
28
33
|
onnotify,
|
|
29
34
|
onretry,
|
|
35
|
+
oncontinue,
|
|
30
36
|
empty,
|
|
31
37
|
}: Props = $props();
|
|
32
38
|
|
|
33
39
|
let container: HTMLDivElement | undefined = $state();
|
|
34
|
-
|
|
40
|
+
/** Pinned to the bottom: new content scrolls into view. Off once the reader scrolls up. */
|
|
41
|
+
let follow = $state(true);
|
|
42
|
+
/** Content arrived while the reader was scrolled up. */
|
|
43
|
+
let unseen = $state(false);
|
|
35
44
|
const FOLLOW_THRESHOLD_PX = 80;
|
|
45
|
+
function atBottom(): boolean {
|
|
46
|
+
if (!container) return true;
|
|
47
|
+
return (
|
|
48
|
+
container.scrollTop + container.clientHeight >= container.scrollHeight - FOLLOW_THRESHOLD_PX
|
|
49
|
+
);
|
|
50
|
+
}
|
|
36
51
|
function onscroll(): void {
|
|
52
|
+
follow = atBottom();
|
|
53
|
+
if (follow) unseen = false;
|
|
54
|
+
}
|
|
55
|
+
function scrollToBottom(smooth = false): void {
|
|
37
56
|
if (!container) return;
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
const reduce =
|
|
58
|
+
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
59
|
+
container.scrollTo({
|
|
60
|
+
top: container.scrollHeight,
|
|
61
|
+
behavior: smooth && !reduce ? 'smooth' : 'auto',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function jumpToLatest(): void {
|
|
65
|
+
follow = true;
|
|
66
|
+
unseen = false;
|
|
67
|
+
scrollToBottom(true);
|
|
40
68
|
}
|
|
69
|
+
|
|
41
70
|
const count = $derived(messages.length);
|
|
42
71
|
const lastRole = $derived(messages[messages.length - 1]?.role);
|
|
43
72
|
let prevCount = 0;
|
|
44
73
|
$effect(() => {
|
|
45
74
|
void messages;
|
|
46
|
-
|
|
75
|
+
// Also when streaming stops: the turn's footer (outcome, actions) appears then.
|
|
76
|
+
void streaming;
|
|
77
|
+
if (!container) return;
|
|
78
|
+
if (follow) container.scrollTop = container.scrollHeight;
|
|
79
|
+
else if (streaming) unseen = true;
|
|
47
80
|
});
|
|
48
81
|
$effect(() => {
|
|
49
82
|
void count;
|
|
50
83
|
void lastRole;
|
|
51
|
-
|
|
84
|
+
// Sending always brings the reader back to the newest turn.
|
|
85
|
+
if (count > prevCount && lastRole === 'user') {
|
|
86
|
+
follow = true;
|
|
87
|
+
unseen = false;
|
|
88
|
+
}
|
|
52
89
|
prevCount = count;
|
|
53
90
|
void tick().then(() => {
|
|
54
91
|
if (container && follow) container.scrollTop = container.scrollHeight;
|
|
55
92
|
});
|
|
56
93
|
});
|
|
94
|
+
|
|
95
|
+
const separators = $derived(daySeparators(messages));
|
|
96
|
+
// Only show day labels when the transcript spans more than one day.
|
|
97
|
+
const multiDay = $derived(separators.size > 1);
|
|
98
|
+
|
|
57
99
|
// Announce the finished answer once, not every token.
|
|
58
100
|
const announcement = $derived.by(() => {
|
|
59
101
|
if (streaming) return '';
|
|
@@ -63,31 +105,61 @@
|
|
|
63
105
|
</script>
|
|
64
106
|
|
|
65
107
|
<div class="sr-only" aria-live="polite" aria-atomic="true">{announcement}</div>
|
|
66
|
-
<div
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{#if
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
<div class="relative flex min-h-0 flex-1 flex-col">
|
|
109
|
+
<div
|
|
110
|
+
class="flex min-h-0 flex-1 flex-col overflow-y-auto py-2"
|
|
111
|
+
bind:this={container}
|
|
112
|
+
{onscroll}
|
|
113
|
+
role="log"
|
|
114
|
+
aria-label="Conversation"
|
|
115
|
+
>
|
|
116
|
+
{#if messages.length === 0}
|
|
117
|
+
{#if empty}{@render empty()}{/if}
|
|
118
|
+
{:else}
|
|
119
|
+
<div class="mx-auto flex w-full max-w-3xl flex-col">
|
|
120
|
+
{#each messages as message, i (message.id)}
|
|
121
|
+
{#if multiDay && separators.has(i)}
|
|
122
|
+
<div
|
|
123
|
+
class="flex items-center gap-3 px-4 py-3 text-xs font-medium text-muted-foreground"
|
|
124
|
+
role="separator"
|
|
125
|
+
aria-label={formatDay(message.createdAt)}
|
|
126
|
+
>
|
|
127
|
+
<span class="h-px flex-1 bg-border"></span>
|
|
128
|
+
{formatDay(message.createdAt)}
|
|
129
|
+
<span class="h-px flex-1 bg-border"></span>
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
132
|
+
<MessageItem
|
|
133
|
+
{message}
|
|
134
|
+
last={i === messages.length - 1}
|
|
135
|
+
streaming={streaming && i === messages.length - 1 && message.role === 'assistant'}
|
|
136
|
+
{sandboxOrigin}
|
|
137
|
+
{fileUrl}
|
|
138
|
+
{toolLabels}
|
|
139
|
+
{onnavigate}
|
|
140
|
+
{onopenartifact}
|
|
141
|
+
{onprompt}
|
|
142
|
+
{ontool}
|
|
143
|
+
{onnotify}
|
|
144
|
+
onretry={i === messages.length - 1 ? onretry : undefined}
|
|
145
|
+
oncontinue={i === messages.length - 1 && !streaming ? oncontinue : undefined}
|
|
146
|
+
/>
|
|
147
|
+
{/each}
|
|
148
|
+
</div>
|
|
149
|
+
{/if}
|
|
150
|
+
</div>
|
|
151
|
+
{#if !follow && messages.length > 0}
|
|
152
|
+
<div class="pointer-events-none absolute inset-x-0 bottom-3 flex justify-center">
|
|
153
|
+
<Button
|
|
154
|
+
variant={unseen ? 'default' : 'outline'}
|
|
155
|
+
size="sm"
|
|
156
|
+
class="pointer-events-auto rounded-full shadow-md"
|
|
157
|
+
onclick={jumpToLatest}
|
|
158
|
+
aria-label={unseen ? 'New messages; jump to the latest' : 'Jump to the latest message'}
|
|
159
|
+
>
|
|
160
|
+
<Icon name="arrow-down" size={14} />
|
|
161
|
+
{unseen ? 'New messages' : 'Latest'}
|
|
162
|
+
</Button>
|
|
91
163
|
</div>
|
|
92
164
|
{/if}
|
|
93
165
|
</div>
|
|
@@ -4,12 +4,14 @@ interface Props {
|
|
|
4
4
|
streaming: boolean;
|
|
5
5
|
sandboxOrigin: string;
|
|
6
6
|
fileUrl: (fileId: string) => string;
|
|
7
|
+
toolLabels?: Record<string, string>;
|
|
7
8
|
onnavigate?: (href: string) => void;
|
|
8
9
|
onopenartifact?: (artifactId: string) => void;
|
|
9
10
|
onprompt?: (text: string) => void;
|
|
10
11
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
11
12
|
onnotify?: (message: string) => void;
|
|
12
13
|
onretry?: () => void;
|
|
14
|
+
oncontinue?: () => void;
|
|
13
15
|
empty?: import('svelte').Snippet;
|
|
14
16
|
}
|
|
15
17
|
declare const MessageList: import("svelte").Component<Props, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageList.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MessageList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageList.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;AA4HH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Read-only view of a document edit the agent proposed. The chat never applies edits: they are
|
|
4
|
+
reviewed and applied from the Excel or Word add-in, which re-reads the target first.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import type { ProposedOperation } from '@classic-homes/agent-contracts';
|
|
8
|
+
import { cn, Icon } from '@classic-homes/theme-svelte';
|
|
9
|
+
import { describeProposal } from '../message-format.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
operation: ProposedOperation;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
let { operation, class: className }: Props = $props();
|
|
16
|
+
const info = $derived(describeProposal(operation));
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div
|
|
20
|
+
class={cn(
|
|
21
|
+
'flex max-w-md gap-3 rounded-lg border border-dashed border-border px-3 py-2',
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
data-operation-id={operation.operationId}
|
|
25
|
+
>
|
|
26
|
+
<span
|
|
27
|
+
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground"
|
|
28
|
+
>
|
|
29
|
+
<Icon name={info.host === 'Excel' ? 'table' : 'file-text'} size={16} />
|
|
30
|
+
</span>
|
|
31
|
+
<div class="min-w-0 flex-1 text-sm">
|
|
32
|
+
<p class="font-medium text-foreground">
|
|
33
|
+
{info.action}{#if info.target}
|
|
34
|
+
<span class="font-mono text-xs text-muted-foreground"> · {info.target}</span>{/if}
|
|
35
|
+
</p>
|
|
36
|
+
<p class="text-muted-foreground [overflow-wrap:anywhere]">{operation.summary}</p>
|
|
37
|
+
<p class="mt-1 text-xs text-muted-foreground">Apply from the {info.host} add-in.</p>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProposedOperation } from '@classic-homes/agent-contracts';
|
|
2
|
+
interface Props {
|
|
3
|
+
operation: ProposedOperation;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Read-only view of a document edit the agent proposed. The chat never applies edits: they are
|
|
8
|
+
* reviewed and applied from the Excel or Word add-in, which re-reads the target first.
|
|
9
|
+
*/
|
|
10
|
+
declare const ProposalCard: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type ProposalCard = ReturnType<typeof ProposalCard>;
|
|
12
|
+
export default ProposalCard;
|
|
13
|
+
//# sourceMappingURL=ProposalCard.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProposalCard.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ProposalCard.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAKtE,UAAU,KAAK;IACb,SAAS,EAAE,iBAAiB,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8BH;;;GAGG;AACH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|