@classic-homes/agent-chat 0.4.1 → 0.5.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 +4 -0
- package/dist/components/AgentChat.svelte +97 -6
- package/dist/components/AgentChat.svelte.d.ts +10 -0
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/Welcome.svelte +99 -0
- package/dist/components/Welcome.svelte.d.ts +22 -0
- package/dist/components/Welcome.svelte.d.ts.map +1 -0
- package/dist/components/welcome.svelte.test.d.ts +2 -0
- package/dist/components/welcome.svelte.test.d.ts.map +1 -0
- package/dist/components/welcome.svelte.test.js +119 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/welcome.d.ts +21 -0
- package/dist/welcome.d.ts.map +1 -0
- package/dist/welcome.js +11 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -51,6 +51,10 @@ agent-api origin that serves `/frame`; artifacts and MCP-UI resources only ever
|
|
|
51
51
|
(`ComposerPrompt.text` may be a loader, e.g. fetching the template).
|
|
52
52
|
- `ToolTimeline`: tool calls with status and duration, delegations nested; `toolLabels` maps
|
|
53
53
|
tool names to readable ones (`AgentChat` passes both through).
|
|
54
|
+
- `Welcome`: the empty conversation, with a greeting (`userName`), a capability line and up to
|
|
55
|
+
four `suggestions` that send when chosen. `AgentChat` shows it unless the host passes an
|
|
56
|
+
`empty` snippet, a loading skeleton while an existing conversation's transcript arrives, and
|
|
57
|
+
an error with Try again when that load fails.
|
|
54
58
|
- `ArtifactPanel`, `UiFrame`, `ModeBanner`.
|
|
55
59
|
|
|
56
60
|
Publishing: `docs/operations/package-publishing.md`.
|
|
@@ -7,15 +7,25 @@
|
|
|
7
7
|
FileRef,
|
|
8
8
|
} from '@classic-homes/agent-contracts';
|
|
9
9
|
import type { BackendMode } from '@classic-homes/agent-contracts';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Alert,
|
|
12
|
+
Button,
|
|
13
|
+
cn,
|
|
14
|
+
Icon,
|
|
15
|
+
ResizablePanes,
|
|
16
|
+
Sheet,
|
|
17
|
+
Skeleton,
|
|
18
|
+
} from '@classic-homes/theme-svelte';
|
|
11
19
|
import { MediaQuery } from 'svelte/reactivity';
|
|
12
20
|
import { releaseStuckBodyLock } from '../body-lock.js';
|
|
13
21
|
import type { ComposerPrompt } from '../composer-prompts.js';
|
|
14
22
|
import { ChatSession } from '../session.svelte.js';
|
|
23
|
+
import type { WelcomeSuggestion } from '../welcome.js';
|
|
15
24
|
import ArtifactPanel from './ArtifactPanel.svelte';
|
|
16
25
|
import Composer from './Composer.svelte';
|
|
17
26
|
import MessageList from './MessageList.svelte';
|
|
18
27
|
import ModeBanner from './ModeBanner.svelte';
|
|
28
|
+
import Welcome from './Welcome.svelte';
|
|
19
29
|
|
|
20
30
|
interface Props {
|
|
21
31
|
client: AgentApiClient;
|
|
@@ -45,9 +55,18 @@
|
|
|
45
55
|
prompts?: ComposerPrompt[];
|
|
46
56
|
/** Readable tool names for the timeline, by tool name (`server/name` or `name`). */
|
|
47
57
|
toolLabels?: Record<string, string>;
|
|
58
|
+
/** The signed-in user's name for the welcome greeting (first word shown). */
|
|
59
|
+
userName?: string | null;
|
|
60
|
+
/** Starters on an empty conversation (up to four); choosing one sends it. */
|
|
61
|
+
suggestions?: WelcomeSuggestion[];
|
|
62
|
+
/** Replaces the welcome's time-of-day greeting. */
|
|
63
|
+
welcomeTitle?: string;
|
|
64
|
+
/** Replaces the welcome's capability line. */
|
|
65
|
+
welcomeDescription?: string;
|
|
48
66
|
/** Rendered above the transcript, e.g. a `ChatHeader`. */
|
|
49
67
|
header?: import('svelte').Snippet;
|
|
50
68
|
context?: import('svelte').Snippet;
|
|
69
|
+
/** Replaces the default `Welcome` on an empty conversation. */
|
|
51
70
|
empty?: import('svelte').Snippet;
|
|
52
71
|
class?: string;
|
|
53
72
|
}
|
|
@@ -69,6 +88,10 @@
|
|
|
69
88
|
placeholder,
|
|
70
89
|
prompts,
|
|
71
90
|
toolLabels,
|
|
91
|
+
userName,
|
|
92
|
+
suggestions,
|
|
93
|
+
welcomeTitle,
|
|
94
|
+
welcomeDescription,
|
|
72
95
|
header,
|
|
73
96
|
context,
|
|
74
97
|
empty,
|
|
@@ -94,15 +117,20 @@
|
|
|
94
117
|
}
|
|
95
118
|
},
|
|
96
119
|
});
|
|
120
|
+
let attemptedId: string | null = null;
|
|
97
121
|
// The constructor already knows `conversationId`; what decides a fetch is whether the
|
|
98
122
|
// transcript for it has been loaded (or started) in this session.
|
|
99
123
|
$effect(() => {
|
|
100
124
|
if (
|
|
101
125
|
conversationId &&
|
|
102
126
|
conversationId !== session.loadedConversationId &&
|
|
127
|
+
conversationId !== attemptedId &&
|
|
103
128
|
session.status !== 'loading'
|
|
104
|
-
)
|
|
129
|
+
) {
|
|
130
|
+
// Once per id: a failed load waits for Try again instead of retrying in a loop.
|
|
131
|
+
attemptedId = conversationId;
|
|
105
132
|
void session.load(conversationId);
|
|
133
|
+
}
|
|
106
134
|
});
|
|
107
135
|
$effect(() => {
|
|
108
136
|
if (mode && !session.mode) session.mode = mode;
|
|
@@ -135,19 +163,82 @@
|
|
|
135
163
|
const last = session.messages[session.messages.length - 1];
|
|
136
164
|
if (last?.role === 'assistant') void session.retry(last.requestId);
|
|
137
165
|
}
|
|
166
|
+
// An existing conversation whose transcript has not arrived yet: show a skeleton, not the
|
|
167
|
+
// welcome (which would flash before the messages).
|
|
168
|
+
const pending = $derived(
|
|
169
|
+
session.status === 'loading' ||
|
|
170
|
+
(!!conversationId &&
|
|
171
|
+
conversationId !== session.loadedConversationId &&
|
|
172
|
+
!session.loadError &&
|
|
173
|
+
session.messages.length === 0),
|
|
174
|
+
);
|
|
175
|
+
function reload(): void {
|
|
176
|
+
if (conversationId) void session.load(conversationId);
|
|
177
|
+
}
|
|
138
178
|
function prompt(text: string): void {
|
|
139
179
|
draft = text;
|
|
140
180
|
composer?.focus();
|
|
141
181
|
}
|
|
142
182
|
</script>
|
|
143
183
|
|
|
184
|
+
{#snippet emptyState()}
|
|
185
|
+
{#if pending}
|
|
186
|
+
<div
|
|
187
|
+
class="mx-auto flex w-full max-w-3xl flex-col gap-6 px-4 py-6"
|
|
188
|
+
aria-busy="true"
|
|
189
|
+
data-agent-loading
|
|
190
|
+
>
|
|
191
|
+
<span class="sr-only" role="status">Loading conversation…</span>
|
|
192
|
+
<Skeleton class="ml-auto h-10 w-2/5 rounded-2xl" />
|
|
193
|
+
<div class="flex flex-col gap-2">
|
|
194
|
+
<Skeleton variant="text" class="w-11/12" />
|
|
195
|
+
<Skeleton variant="text" class="w-4/5" />
|
|
196
|
+
<Skeleton variant="text" class="w-3/5" />
|
|
197
|
+
</div>
|
|
198
|
+
<Skeleton class="ml-auto h-10 w-1/3 rounded-2xl" />
|
|
199
|
+
<div class="flex flex-col gap-2">
|
|
200
|
+
<Skeleton variant="text" class="w-10/12" />
|
|
201
|
+
<Skeleton variant="text" class="w-2/3" />
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
{:else if session.loadError}
|
|
205
|
+
<!-- The alert above explains it; no welcome under a failed load. -->
|
|
206
|
+
{:else if empty}
|
|
207
|
+
{@render empty()}
|
|
208
|
+
{:else}
|
|
209
|
+
<Welcome
|
|
210
|
+
name={userName}
|
|
211
|
+
title={welcomeTitle}
|
|
212
|
+
description={welcomeDescription}
|
|
213
|
+
{suggestions}
|
|
214
|
+
compact={variant === 'panel'}
|
|
215
|
+
disabled={session.status !== 'idle'}
|
|
216
|
+
onselect={(text) => void send(text)}
|
|
217
|
+
/>
|
|
218
|
+
{/if}
|
|
219
|
+
{/snippet}
|
|
220
|
+
|
|
144
221
|
{#snippet chat()}
|
|
145
222
|
<div class="flex h-full min-w-0 flex-1 flex-col">
|
|
146
223
|
{#if header}{@render header()}{/if}
|
|
147
224
|
<ModeBanner mode={session.mode} />
|
|
148
|
-
{#if session.loadError}
|
|
149
|
-
|
|
150
|
-
|
|
225
|
+
{#if session.loadError}
|
|
226
|
+
<div class="mx-auto w-full max-w-3xl px-4 pt-3">
|
|
227
|
+
<Alert variant="error">
|
|
228
|
+
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
229
|
+
<p class="min-w-0 flex-1">
|
|
230
|
+
<strong class="font-semibold">Could not load this conversation.</strong>
|
|
231
|
+
{session.loadError}
|
|
232
|
+
</p>
|
|
233
|
+
{#if conversationId}
|
|
234
|
+
<Button variant="outline" size="sm" onclick={reload}>
|
|
235
|
+
<Icon name="refresh" size={14} /> Try again
|
|
236
|
+
</Button>
|
|
237
|
+
{/if}
|
|
238
|
+
</div>
|
|
239
|
+
</Alert>
|
|
240
|
+
</div>
|
|
241
|
+
{/if}
|
|
151
242
|
<MessageList
|
|
152
243
|
messages={session.messages}
|
|
153
244
|
streaming={session.streaming}
|
|
@@ -164,7 +255,7 @@
|
|
|
164
255
|
{onnotify}
|
|
165
256
|
onretry={retry}
|
|
166
257
|
oncontinue={() => void session.send('Continue.')}
|
|
167
|
-
{
|
|
258
|
+
empty={emptyState}
|
|
168
259
|
/>
|
|
169
260
|
<Composer
|
|
170
261
|
bind:this={composer}
|
|
@@ -2,6 +2,7 @@ import type { AgentApiClient } from '@classic-homes/agent-client';
|
|
|
2
2
|
import type { AgentKey, ClientKind, DocumentContext } from '@classic-homes/agent-contracts';
|
|
3
3
|
import type { BackendMode } from '@classic-homes/agent-contracts';
|
|
4
4
|
import type { ComposerPrompt } from '../composer-prompts.js';
|
|
5
|
+
import type { WelcomeSuggestion } from '../welcome.js';
|
|
5
6
|
interface Props {
|
|
6
7
|
client: AgentApiClient;
|
|
7
8
|
/** Sandbox origin for artifacts and MCP-UI (agent-api `/frame`). */
|
|
@@ -30,9 +31,18 @@ interface Props {
|
|
|
30
31
|
prompts?: ComposerPrompt[];
|
|
31
32
|
/** Readable tool names for the timeline, by tool name (`server/name` or `name`). */
|
|
32
33
|
toolLabels?: Record<string, string>;
|
|
34
|
+
/** The signed-in user's name for the welcome greeting (first word shown). */
|
|
35
|
+
userName?: string | null;
|
|
36
|
+
/** Starters on an empty conversation (up to four); choosing one sends it. */
|
|
37
|
+
suggestions?: WelcomeSuggestion[];
|
|
38
|
+
/** Replaces the welcome's time-of-day greeting. */
|
|
39
|
+
welcomeTitle?: string;
|
|
40
|
+
/** Replaces the welcome's capability line. */
|
|
41
|
+
welcomeDescription?: string;
|
|
33
42
|
/** Rendered above the transcript, e.g. a `ChatHeader`. */
|
|
34
43
|
header?: import('svelte').Snippet;
|
|
35
44
|
context?: import('svelte').Snippet;
|
|
45
|
+
/** Replaces the default `Welcome` on an empty conversation. */
|
|
36
46
|
empty?: import('svelte').Snippet;
|
|
37
47
|
class?: string;
|
|
38
48
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentChat.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AgentChat.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"AgentChat.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AgentChat.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAYlE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAQrD,UAAU,KAAK;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IACtD,qBAAqB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,gFAAgF;IAChF,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,IAAI,CAAC;IACX,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,iGAAiG;IACjG,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8MH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The default empty conversation: a greeting, a capability line and starter suggestions.
|
|
4
|
+
`AgentChat` renders it unless the host passes its own `empty` snippet.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { cn, Icon } from '@classic-homes/theme-svelte';
|
|
8
|
+
import { greeting, suggestionText, type WelcomeSuggestion } from '../welcome.js';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** The user's name; only the first word is shown. */
|
|
12
|
+
name?: string | null;
|
|
13
|
+
/** Replaces the time-of-day greeting. */
|
|
14
|
+
title?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
suggestions?: WelcomeSuggestion[];
|
|
17
|
+
/** Compact single-column layout for the side panel. */
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
onselect?: (text: string, suggestion: WelcomeSuggestion) => void;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
let {
|
|
24
|
+
name,
|
|
25
|
+
title,
|
|
26
|
+
description = 'Ask about homes, lots, documents and company data, or attach a file to work from it.',
|
|
27
|
+
suggestions = [],
|
|
28
|
+
compact = false,
|
|
29
|
+
disabled = false,
|
|
30
|
+
onselect,
|
|
31
|
+
class: className,
|
|
32
|
+
}: Props = $props();
|
|
33
|
+
|
|
34
|
+
const headingId = $props.id();
|
|
35
|
+
const heading = $derived(title ?? greeting(name));
|
|
36
|
+
const shown = $derived(suggestions.filter((s) => suggestionText(s)).slice(0, 4));
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<section
|
|
40
|
+
class={cn(
|
|
41
|
+
'm-auto flex w-full flex-col items-center px-4 text-center motion-safe:animate-fade-in',
|
|
42
|
+
compact ? 'max-w-sm gap-4 py-6' : 'max-w-2xl gap-6 py-10',
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
aria-labelledby={headingId}
|
|
46
|
+
data-agent-welcome
|
|
47
|
+
>
|
|
48
|
+
<div class="flex flex-col items-center gap-3">
|
|
49
|
+
<span
|
|
50
|
+
class={cn(
|
|
51
|
+
'inline-flex items-center justify-center rounded-full bg-primary/10 text-primary',
|
|
52
|
+
compact ? 'size-10' : 'size-12',
|
|
53
|
+
)}
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
>
|
|
56
|
+
<Icon name="sparkles" size={compact ? 20 : 24} />
|
|
57
|
+
</span>
|
|
58
|
+
<h2
|
|
59
|
+
id={headingId}
|
|
60
|
+
class={cn('font-semibold tracking-tight text-foreground', compact ? 'text-lg' : 'text-2xl')}
|
|
61
|
+
>
|
|
62
|
+
{heading}
|
|
63
|
+
</h2>
|
|
64
|
+
{#if description}
|
|
65
|
+
<p class="max-w-md text-sm text-muted-foreground">{description}</p>
|
|
66
|
+
{/if}
|
|
67
|
+
</div>
|
|
68
|
+
{#if shown.length}
|
|
69
|
+
<ul
|
|
70
|
+
class={cn('grid w-full gap-2 text-left', !compact && 'sm:grid-cols-2')}
|
|
71
|
+
aria-label="Suggestions"
|
|
72
|
+
>
|
|
73
|
+
{#each shown as suggestion (suggestion.id ?? suggestion.label)}
|
|
74
|
+
<li class="flex">
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
class="group flex w-full items-start gap-3 rounded-lg border border-border bg-background p-3 text-left text-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
78
|
+
{disabled}
|
|
79
|
+
onclick={() => onselect?.(suggestionText(suggestion), suggestion)}
|
|
80
|
+
>
|
|
81
|
+
<Icon
|
|
82
|
+
name={suggestion.icon ?? 'message-square'}
|
|
83
|
+
size={16}
|
|
84
|
+
class="mt-0.5 shrink-0 text-muted-foreground group-hover:text-foreground"
|
|
85
|
+
/>
|
|
86
|
+
<span class="min-w-0">
|
|
87
|
+
<span class="block font-medium text-foreground">{suggestion.label}</span>
|
|
88
|
+
{#if suggestion.description}
|
|
89
|
+
<span class="mt-0.5 block text-xs text-muted-foreground"
|
|
90
|
+
>{suggestion.description}</span
|
|
91
|
+
>
|
|
92
|
+
{/if}
|
|
93
|
+
</span>
|
|
94
|
+
</button>
|
|
95
|
+
</li>
|
|
96
|
+
{/each}
|
|
97
|
+
</ul>
|
|
98
|
+
{/if}
|
|
99
|
+
</section>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type WelcomeSuggestion } from '../welcome.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The user's name; only the first word is shown. */
|
|
4
|
+
name?: string | null;
|
|
5
|
+
/** Replaces the time-of-day greeting. */
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
suggestions?: WelcomeSuggestion[];
|
|
9
|
+
/** Compact single-column layout for the side panel. */
|
|
10
|
+
compact?: boolean;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
onselect?: (text: string, suggestion: WelcomeSuggestion) => void;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The default empty conversation: a greeting, a capability line and starter suggestions.
|
|
17
|
+
* `AgentChat` renders it unless the host passes its own `empty` snippet.
|
|
18
|
+
*/
|
|
19
|
+
declare const Welcome: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type Welcome = ReturnType<typeof Welcome>;
|
|
21
|
+
export default Welcome;
|
|
22
|
+
//# sourceMappingURL=Welcome.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Welcome.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Welcome.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAA4B,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAG/E,UAAU,KAAK;IACb,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,uDAAuD;IACvD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8DH;;;GAGG;AACH,QAAA,MAAM,OAAO,2CAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"welcome.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/welcome.svelte.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/svelte';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { greeting, suggestionText } from '../welcome.js';
|
|
5
|
+
import AgentChat from './AgentChat.svelte';
|
|
6
|
+
import Welcome from './Welcome.svelte';
|
|
7
|
+
afterEach(() => cleanup());
|
|
8
|
+
function fakeClient(getConversation = vi.fn()) {
|
|
9
|
+
return {
|
|
10
|
+
fileUrl: (id) => `#${id}`,
|
|
11
|
+
uploadFile: vi.fn(),
|
|
12
|
+
cancel: vi.fn(),
|
|
13
|
+
getConversation,
|
|
14
|
+
resumeTurn: vi.fn(),
|
|
15
|
+
streamChat: vi.fn(async (req, opts) => {
|
|
16
|
+
[
|
|
17
|
+
{ type: 'start', conversationId: 'c-1', mode: 'live' },
|
|
18
|
+
{ type: 'delta', text: 'Sure.' },
|
|
19
|
+
{ type: 'done', finishReason: 'stop' },
|
|
20
|
+
].forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
|
|
21
|
+
return { kind: 'done' };
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const base = { sandboxOrigin: 'https://frame.test' };
|
|
26
|
+
describe('welcome helpers', () => {
|
|
27
|
+
it('greets by the time of day and the first name only', () => {
|
|
28
|
+
expect(greeting('Pat Lee', new Date(2026, 10, 4, 9))).toBe('Good morning, Pat');
|
|
29
|
+
expect(greeting(undefined, new Date(2026, 10, 4, 14))).toBe('Good afternoon');
|
|
30
|
+
expect(greeting(' ', new Date(2026, 10, 4, 20))).toBe('Good evening');
|
|
31
|
+
expect(greeting('Sam', new Date(2026, 10, 4, 2))).toBe('Good evening, Sam');
|
|
32
|
+
});
|
|
33
|
+
it('sends the text, or the label without one', () => {
|
|
34
|
+
expect(suggestionText({ label: 'Open lots' })).toBe('Open lots');
|
|
35
|
+
expect(suggestionText({ label: 'Lots', text: ' List open lots ' })).toBe('List open lots');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('Welcome', () => {
|
|
39
|
+
it('shows at most four suggestions and reports the chosen one', async () => {
|
|
40
|
+
const onselect = vi.fn();
|
|
41
|
+
render(Welcome, {
|
|
42
|
+
props: {
|
|
43
|
+
name: 'Pat',
|
|
44
|
+
title: 'Hi Pat',
|
|
45
|
+
onselect,
|
|
46
|
+
suggestions: ['A', 'B', 'C', 'D', 'E'].map((label) => ({ label, text: `Do ${label}` })),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
expect(screen.getByRole('heading', { name: 'Hi Pat' })).toBeTruthy();
|
|
50
|
+
const list = screen.getByRole('list', { name: 'Suggestions' });
|
|
51
|
+
expect(list.querySelectorAll('button')).toHaveLength(4);
|
|
52
|
+
await userEvent.click(screen.getByRole('button', { name: 'B' }));
|
|
53
|
+
expect(onselect).toHaveBeenCalledWith('Do B', expect.objectContaining({ label: 'B' }));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('AgentChat empty states', () => {
|
|
57
|
+
it('welcomes a new chat and sends a chosen suggestion', async () => {
|
|
58
|
+
const client = fakeClient();
|
|
59
|
+
render(AgentChat, {
|
|
60
|
+
props: {
|
|
61
|
+
...base,
|
|
62
|
+
client: client,
|
|
63
|
+
userName: 'Pat Lee',
|
|
64
|
+
suggestions: [{ label: 'Open lots', text: 'List the open lots' }],
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
expect(screen.getByRole('heading', { name: /, Pat$/ })).toBeTruthy();
|
|
68
|
+
await userEvent.click(screen.getByRole('button', { name: 'Open lots' }));
|
|
69
|
+
await waitFor(() => expect(client.streamChat).toHaveBeenCalledOnce());
|
|
70
|
+
expect(client.streamChat.mock.calls[0][0]).toMatchObject({ message: 'List the open lots' });
|
|
71
|
+
expect((await screen.findAllByText('Sure.')).length).toBeGreaterThan(0);
|
|
72
|
+
expect(document.querySelector('[data-agent-welcome]')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
it('shows a skeleton, not the welcome, while a conversation loads', async () => {
|
|
75
|
+
let resolve;
|
|
76
|
+
const client = fakeClient(vi.fn(() => new Promise((r) => (resolve = r))));
|
|
77
|
+
render(AgentChat, { props: { ...base, client: client, conversationId: 'c-9' } });
|
|
78
|
+
expect(document.querySelector('[data-agent-loading]')).toBeTruthy();
|
|
79
|
+
expect(document.querySelector('[data-agent-welcome]')).toBeNull();
|
|
80
|
+
resolve({
|
|
81
|
+
conversation: { conversationId: 'c-9', activeRequestId: null },
|
|
82
|
+
messages: [
|
|
83
|
+
{
|
|
84
|
+
id: 'u-1',
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: [{ type: 'text', text: 'Earlier question' }],
|
|
87
|
+
createdAt: Date.now(),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
expect(await screen.findByText('Earlier question')).toBeTruthy();
|
|
92
|
+
expect(document.querySelector('[data-agent-loading]')).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
it('explains a failed load and retries it', async () => {
|
|
95
|
+
const getConversation = vi
|
|
96
|
+
.fn()
|
|
97
|
+
.mockRejectedValueOnce(new Error('Network down'))
|
|
98
|
+
.mockResolvedValueOnce({
|
|
99
|
+
conversation: { conversationId: 'c-9', activeRequestId: null },
|
|
100
|
+
messages: [],
|
|
101
|
+
});
|
|
102
|
+
render(AgentChat, {
|
|
103
|
+
props: { ...base, client: fakeClient(getConversation), conversationId: 'c-9' },
|
|
104
|
+
});
|
|
105
|
+
expect(await screen.findByText('Could not load this conversation.')).toBeTruthy();
|
|
106
|
+
expect(document.querySelector('[data-agent-welcome]')).toBeNull();
|
|
107
|
+
// No automatic retry loop.
|
|
108
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
109
|
+
expect(getConversation).toHaveBeenCalledOnce();
|
|
110
|
+
await userEvent.click(screen.getByRole('button', { name: /Try again/ }));
|
|
111
|
+
await waitFor(() => expect(getConversation).toHaveBeenCalledTimes(2));
|
|
112
|
+
await waitFor(() => expect(screen.queryByText('Could not load this conversation.')).toBeNull());
|
|
113
|
+
expect(document.querySelector('[data-agent-welcome]')).toBeTruthy();
|
|
114
|
+
});
|
|
115
|
+
it('renders the welcome when the host gives no empty snippet', () => {
|
|
116
|
+
render(AgentChat, { props: { ...base, client: fakeClient() } });
|
|
117
|
+
expect(document.querySelector('[data-agent-welcome]')).toBeTruthy();
|
|
118
|
+
});
|
|
119
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './message-format.js';
|
|
|
6
6
|
export * from './reducer.js';
|
|
7
7
|
export * from './session.svelte.js';
|
|
8
8
|
export * from './ui-frame-protocol.js';
|
|
9
|
+
export * from './welcome.js';
|
|
9
10
|
export { default as AgentChat } from './components/AgentChat.svelte';
|
|
10
11
|
export { default as ArtifactPanel } from './components/ArtifactPanel.svelte';
|
|
11
12
|
export { default as Composer } from './components/Composer.svelte';
|
|
@@ -22,4 +23,5 @@ export { default as MessageActions } from './components/MessageActions.svelte';
|
|
|
22
23
|
export { default as ArtifactChip } from './components/ArtifactChip.svelte';
|
|
23
24
|
export { default as MessageFiles } from './components/MessageFiles.svelte';
|
|
24
25
|
export { default as ProposalCard } from './components/ProposalCard.svelte';
|
|
26
|
+
export { default as Welcome } from './components/Welcome.svelte';
|
|
25
27
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AACzF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AACzF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from './message-format.js';
|
|
|
6
6
|
export * from './reducer.js';
|
|
7
7
|
export * from './session.svelte.js';
|
|
8
8
|
export * from './ui-frame-protocol.js';
|
|
9
|
+
export * from './welcome.js';
|
|
9
10
|
export { default as AgentChat } from './components/AgentChat.svelte';
|
|
10
11
|
export { default as ArtifactPanel } from './components/ArtifactPanel.svelte';
|
|
11
12
|
export { default as Composer } from './components/Composer.svelte';
|
|
@@ -22,3 +23,4 @@ export { default as MessageActions } from './components/MessageActions.svelte';
|
|
|
22
23
|
export { default as ArtifactChip } from './components/ArtifactChip.svelte';
|
|
23
24
|
export { default as MessageFiles } from './components/MessageFiles.svelte';
|
|
24
25
|
export { default as ProposalCard } from './components/ProposalCard.svelte';
|
|
26
|
+
export { default as Welcome } from './components/Welcome.svelte';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for the empty conversation: greeting and suggestion types.
|
|
3
|
+
*/
|
|
4
|
+
import type { IconName } from '@classic-homes/theme-svelte';
|
|
5
|
+
/** A starter offered on an empty conversation. */
|
|
6
|
+
export interface WelcomeSuggestion {
|
|
7
|
+
/** Stable key; defaults to the label. */
|
|
8
|
+
id?: string;
|
|
9
|
+
/** Short card title, e.g. "Summarize this lot". */
|
|
10
|
+
label: string;
|
|
11
|
+
/** One line under the label. */
|
|
12
|
+
description?: string;
|
|
13
|
+
/** Message sent when the card is chosen (defaults to the label). */
|
|
14
|
+
text?: string;
|
|
15
|
+
icon?: IconName;
|
|
16
|
+
}
|
|
17
|
+
/** The text a suggestion sends. */
|
|
18
|
+
export declare function suggestionText(s: WelcomeSuggestion): string;
|
|
19
|
+
/** "Good morning, Pat" by the local hour; without a name, "Good morning". */
|
|
20
|
+
export declare function greeting(name?: string | null, now?: Date): string;
|
|
21
|
+
//# sourceMappingURL=welcome.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"welcome.d.ts","sourceRoot":"","sources":["../src/lib/welcome.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAE5D,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,yCAAyC;IACzC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AAED,mCAAmC;AACnC,wBAAgB,cAAc,CAAC,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAE3D;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAK7E"}
|
package/dist/welcome.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The text a suggestion sends. */
|
|
2
|
+
export function suggestionText(s) {
|
|
3
|
+
return (s.text ?? s.label).trim();
|
|
4
|
+
}
|
|
5
|
+
/** "Good morning, Pat" by the local hour; without a name, "Good morning". */
|
|
6
|
+
export function greeting(name, now = new Date()) {
|
|
7
|
+
const hour = now.getHours();
|
|
8
|
+
const part = hour < 5 ? 'evening' : hour < 12 ? 'morning' : hour < 18 ? 'afternoon' : 'evening';
|
|
9
|
+
const first = name?.trim().split(/\s+/)[0];
|
|
10
|
+
return first ? `Good ${part}, ${first}` : `Good ${part}`;
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/agent-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Svelte 5 chat UI for Classic Agent: session state, message list, composer, tool timeline, artifacts and MCP-UI sandbox frames. Consumed by the Classic App.",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"dist"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@classic-homes/agent-client": "0.
|
|
21
|
-
"@classic-homes/agent-contracts": "0.
|
|
20
|
+
"@classic-homes/agent-client": "0.5.0",
|
|
21
|
+
"@classic-homes/agent-contracts": "0.5.0",
|
|
22
22
|
"dompurify": "3.4.17"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|