@classic-homes/agent-chat 0.4.1 → 0.6.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 +99 -6
- package/dist/components/AgentChat.svelte.d.ts +10 -0
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/AssistantMessage.svelte +4 -0
- package/dist/components/AssistantMessage.svelte.d.ts +2 -0
- package/dist/components/AssistantMessage.svelte.d.ts.map +1 -1
- package/dist/components/MessageActions.svelte +36 -3
- package/dist/components/MessageActions.svelte.d.ts +6 -1
- package/dist/components/MessageActions.svelte.d.ts.map +1 -1
- package/dist/components/MessageItem.svelte +5 -2
- package/dist/components/MessageItem.svelte.d.ts +3 -0
- package/dist/components/MessageItem.svelte.d.ts.map +1 -1
- package/dist/components/MessageList.svelte +23 -0
- package/dist/components/MessageList.svelte.d.ts +4 -0
- package/dist/components/MessageList.svelte.d.ts.map +1 -1
- package/dist/components/UserMessage.svelte +75 -8
- package/dist/components/UserMessage.svelte.d.ts +6 -1
- package/dist/components/UserMessage.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/regenerate.svelte.test.d.ts +2 -0
- package/dist/components/regenerate.svelte.test.d.ts.map +1 -0
- package/dist/components/regenerate.svelte.test.js +108 -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/reducer.d.ts +8 -0
- package/dist/reducer.d.ts.map +1 -1
- package/dist/reducer.js +15 -0
- package/dist/session.svelte.d.ts +11 -3
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +29 -5
- 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}
|
|
@@ -163,8 +254,10 @@
|
|
|
163
254
|
ontool={(name, params) => ontoolrequest?.(name, params)}
|
|
164
255
|
{onnotify}
|
|
165
256
|
onretry={retry}
|
|
257
|
+
onregenerate={(requestId) => void session.regenerate(requestId)}
|
|
258
|
+
onedit={(requestId, text) => void session.edit(requestId, text)}
|
|
166
259
|
oncontinue={() => void session.send('Continue.')}
|
|
167
|
-
{
|
|
260
|
+
empty={emptyState}
|
|
168
261
|
/>
|
|
169
262
|
<Composer
|
|
170
263
|
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"}
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
31
31
|
onnotify?: (message: string) => void;
|
|
32
32
|
onretry?: () => void;
|
|
33
|
+
/** Ask for a new answer to the same question (the latest finished answer). */
|
|
34
|
+
onregenerate?: () => void;
|
|
33
35
|
/** Ask the agent to pick up a stopped or cut-short answer. */
|
|
34
36
|
oncontinue?: () => void;
|
|
35
37
|
}
|
|
@@ -46,6 +48,7 @@
|
|
|
46
48
|
ontool,
|
|
47
49
|
onnotify,
|
|
48
50
|
onretry,
|
|
51
|
+
onregenerate,
|
|
49
52
|
oncontinue,
|
|
50
53
|
}: Props = $props();
|
|
51
54
|
|
|
@@ -179,6 +182,7 @@
|
|
|
179
182
|
text={message.text}
|
|
180
183
|
createdAt={message.createdAt}
|
|
181
184
|
visible={last}
|
|
185
|
+
{onregenerate}
|
|
182
186
|
class="-ml-1.5"
|
|
183
187
|
/>
|
|
184
188
|
{/if}
|
|
@@ -13,6 +13,8 @@ interface Props {
|
|
|
13
13
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
14
14
|
onnotify?: (message: string) => void;
|
|
15
15
|
onretry?: () => void;
|
|
16
|
+
/** Ask for a new answer to the same question (the latest finished answer). */
|
|
17
|
+
onregenerate?: () => void;
|
|
16
18
|
/** Ask the agent to pick up a stopped or cut-short answer. */
|
|
17
19
|
oncontinue?: () => void;
|
|
18
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssistantMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AssistantMessage.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAYpD,UAAU,KAAK;IACb,OAAO,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,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,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"AssistantMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AssistantMessage.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAYpD,UAAU,KAAK;IACb,OAAO,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,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,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAyJH;;;GAGG;AACH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
|
-
Per-message action bar: copy the raw text
|
|
3
|
+
Per-message action bar: copy the raw text, regenerate or edit (latest turn only) and the time
|
|
4
|
+
it was sent. Hidden until the message is
|
|
4
5
|
hovered or holds focus, except where `visible` (the last message) or on touch screens, which
|
|
5
6
|
have no hover.
|
|
6
7
|
-->
|
|
7
8
|
<script lang="ts">
|
|
8
|
-
import { cn, CopyButton, Tooltip } from '@classic-homes/theme-svelte';
|
|
9
|
+
import { Button, cn, CopyButton, Icon, Tooltip } from '@classic-homes/theme-svelte';
|
|
9
10
|
import { formatDateTime, formatTime } from '../message-format.js';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
@@ -14,9 +15,21 @@
|
|
|
14
15
|
/** Keep the bar visible without hover (the last message). */
|
|
15
16
|
visible?: boolean;
|
|
16
17
|
align?: 'start' | 'end';
|
|
18
|
+
/** Offer "Regenerate" (the latest answer). */
|
|
19
|
+
onregenerate?: () => void;
|
|
20
|
+
/** Offer "Edit" (the latest question). */
|
|
21
|
+
onedit?: () => void;
|
|
17
22
|
class?: string;
|
|
18
23
|
}
|
|
19
|
-
let {
|
|
24
|
+
let {
|
|
25
|
+
text,
|
|
26
|
+
createdAt,
|
|
27
|
+
visible = false,
|
|
28
|
+
align = 'start',
|
|
29
|
+
onregenerate,
|
|
30
|
+
onedit,
|
|
31
|
+
class: className,
|
|
32
|
+
}: Props = $props();
|
|
20
33
|
</script>
|
|
21
34
|
|
|
22
35
|
<div
|
|
@@ -39,6 +52,26 @@
|
|
|
39
52
|
class="h-7 w-7"
|
|
40
53
|
/>
|
|
41
54
|
{/if}
|
|
55
|
+
{#if onedit}
|
|
56
|
+
<Button
|
|
57
|
+
variant="ghost"
|
|
58
|
+
size="icon"
|
|
59
|
+
class="h-7 w-7"
|
|
60
|
+
aria-label="Edit message"
|
|
61
|
+
title="Edit"
|
|
62
|
+
onclick={onedit}><Icon name="edit" size={14} /></Button
|
|
63
|
+
>
|
|
64
|
+
{/if}
|
|
65
|
+
{#if onregenerate}
|
|
66
|
+
<Button
|
|
67
|
+
variant="ghost"
|
|
68
|
+
size="icon"
|
|
69
|
+
class="h-7 w-7"
|
|
70
|
+
aria-label="Regenerate response"
|
|
71
|
+
title="Regenerate"
|
|
72
|
+
onclick={onregenerate}><Icon name="refresh" size={14} /></Button
|
|
73
|
+
>
|
|
74
|
+
{/if}
|
|
42
75
|
<Tooltip content={formatDateTime(createdAt)}>
|
|
43
76
|
{#snippet children({ props })}
|
|
44
77
|
<!-- Focusable so keyboard users can read the full date in the tooltip. -->
|
|
@@ -4,10 +4,15 @@ interface Props {
|
|
|
4
4
|
/** Keep the bar visible without hover (the last message). */
|
|
5
5
|
visible?: boolean;
|
|
6
6
|
align?: 'start' | 'end';
|
|
7
|
+
/** Offer "Regenerate" (the latest answer). */
|
|
8
|
+
onregenerate?: () => void;
|
|
9
|
+
/** Offer "Edit" (the latest question). */
|
|
10
|
+
onedit?: () => void;
|
|
7
11
|
class?: string;
|
|
8
12
|
}
|
|
9
13
|
/**
|
|
10
|
-
* Per-message action bar: copy the raw text
|
|
14
|
+
* Per-message action bar: copy the raw text, regenerate or edit (latest turn only) and the time
|
|
15
|
+
* it was sent. Hidden until the message is
|
|
11
16
|
* hovered or holds focus, except where `visible` (the last message) or on touch screens, which
|
|
12
17
|
* have no hover.
|
|
13
18
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageActions.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageActions.svelte.ts"],"names":[],"mappings":"AAOE,UAAU,KAAK;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"MessageActions.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageActions.svelte.ts"],"names":[],"mappings":"AAOE,UAAU,KAAK;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IACxB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8CH;;;;;GAKG;AACH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -20,13 +20,16 @@
|
|
|
20
20
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
21
21
|
onnotify?: (message: string) => void;
|
|
22
22
|
onretry?: () => void;
|
|
23
|
+
onregenerate?: () => void;
|
|
23
24
|
oncontinue?: () => void;
|
|
25
|
+
/** User messages only: edit and resend. */
|
|
26
|
+
onedit?: (text: string) => void;
|
|
24
27
|
}
|
|
25
|
-
let { message, last = false, ...rest }: Props = $props();
|
|
28
|
+
let { message, last = false, onedit, ...rest }: Props = $props();
|
|
26
29
|
</script>
|
|
27
30
|
|
|
28
31
|
{#if message.role === 'user'}
|
|
29
|
-
<UserMessage {message} {last} />
|
|
32
|
+
<UserMessage {message} {last} {onedit} />
|
|
30
33
|
{:else}
|
|
31
34
|
<AssistantMessage {message} {last} {...rest} />
|
|
32
35
|
{/if}
|
|
@@ -12,7 +12,10 @@ interface Props {
|
|
|
12
12
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
13
13
|
onnotify?: (message: string) => void;
|
|
14
14
|
onretry?: () => void;
|
|
15
|
+
onregenerate?: () => void;
|
|
15
16
|
oncontinue?: () => void;
|
|
17
|
+
/** User messages only: edit and resend. */
|
|
18
|
+
onedit?: (text: string) => void;
|
|
16
19
|
}
|
|
17
20
|
/** One transcript entry: a `UserMessage` or an `AssistantMessage`. */
|
|
18
21
|
declare const MessageItem: import("svelte").Component<Props, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAkBH,sEAAsE;AACtE,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
18
18
|
onnotify?: (message: string) => void;
|
|
19
19
|
onretry?: () => void;
|
|
20
|
+
/** Regenerate the latest answer, by its request id. */
|
|
21
|
+
onregenerate?: (requestId: string) => void;
|
|
22
|
+
/** Edit the latest question, by its request id. */
|
|
23
|
+
onedit?: (requestId: string, text: string) => void;
|
|
20
24
|
oncontinue?: () => void;
|
|
21
25
|
empty?: import('svelte').Snippet;
|
|
22
26
|
}
|
|
@@ -32,10 +36,19 @@
|
|
|
32
36
|
ontool,
|
|
33
37
|
onnotify,
|
|
34
38
|
onretry,
|
|
39
|
+
onregenerate,
|
|
40
|
+
onedit,
|
|
35
41
|
oncontinue,
|
|
36
42
|
empty,
|
|
37
43
|
}: Props = $props();
|
|
38
44
|
|
|
45
|
+
// The latest turn can be regenerated or edited once it has finished.
|
|
46
|
+
const latest = $derived.by(() => {
|
|
47
|
+
const last = messages[messages.length - 1];
|
|
48
|
+
if (streaming || last?.role !== 'assistant' || last.status === 'streaming') return undefined;
|
|
49
|
+
return messages[messages.length - 2]?.role === 'user' ? last.requestId : undefined;
|
|
50
|
+
});
|
|
51
|
+
|
|
39
52
|
let container: HTMLDivElement | undefined = $state();
|
|
40
53
|
/** Pinned to the bottom: new content scrolls into view. Off once the reader scrolls up. */
|
|
41
54
|
let follow = $state(true);
|
|
@@ -143,6 +156,16 @@
|
|
|
143
156
|
{onnotify}
|
|
144
157
|
onretry={i === messages.length - 1 ? onretry : undefined}
|
|
145
158
|
oncontinue={i === messages.length - 1 && !streaming ? oncontinue : undefined}
|
|
159
|
+
onregenerate={latest &&
|
|
160
|
+
onregenerate &&
|
|
161
|
+
i === messages.length - 1 &&
|
|
162
|
+
message.role === 'assistant' &&
|
|
163
|
+
message.status === 'done'
|
|
164
|
+
? () => onregenerate(latest)
|
|
165
|
+
: undefined}
|
|
166
|
+
onedit={latest && onedit && i === messages.length - 2
|
|
167
|
+
? (text: string) => onedit(latest, text)
|
|
168
|
+
: undefined}
|
|
146
169
|
/>
|
|
147
170
|
{/each}
|
|
148
171
|
</div>
|
|
@@ -11,6 +11,10 @@ interface Props {
|
|
|
11
11
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
12
12
|
onnotify?: (message: string) => void;
|
|
13
13
|
onretry?: () => void;
|
|
14
|
+
/** Regenerate the latest answer, by its request id. */
|
|
15
|
+
onregenerate?: (requestId: string) => void;
|
|
16
|
+
/** Edit the latest question, by its request id. */
|
|
17
|
+
onedit?: (requestId: string, text: string) => void;
|
|
14
18
|
oncontinue?: () => void;
|
|
15
19
|
empty?: import('svelte').Snippet;
|
|
16
20
|
}
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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,uDAAuD;IACvD,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,mDAAmD;IACnD,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;AA6IH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
3
|
The user's turn: attachments and context above a right-aligned muted bubble, then the action bar.
|
|
4
|
+
With `onedit` (the latest question) the bubble can be edited in place and sent again.
|
|
4
5
|
-->
|
|
5
6
|
<script lang="ts">
|
|
6
|
-
import { Icon } from '@classic-homes/theme-svelte';
|
|
7
|
+
import { Button, Icon, Textarea } from '@classic-homes/theme-svelte';
|
|
8
|
+
import { LIMITS } from '@classic-homes/agent-contracts';
|
|
9
|
+
import { tick } from 'svelte';
|
|
7
10
|
import type { UserMessage } from '../reducer.js';
|
|
8
11
|
import { fileIcon, formatBytes } from '../message-format.js';
|
|
9
12
|
import MessageActions from './MessageActions.svelte';
|
|
@@ -11,8 +14,37 @@
|
|
|
11
14
|
interface Props {
|
|
12
15
|
message: UserMessage;
|
|
13
16
|
last?: boolean;
|
|
17
|
+
/** Replace this question with new text and answer it again. */
|
|
18
|
+
onedit?: (text: string) => void;
|
|
19
|
+
}
|
|
20
|
+
let { message, last = false, onedit }: Props = $props();
|
|
21
|
+
|
|
22
|
+
let editing = $state(false);
|
|
23
|
+
let draft = $state('');
|
|
24
|
+
let field = $state<HTMLTextAreaElement | null>(null);
|
|
25
|
+
async function startEdit(): Promise<void> {
|
|
26
|
+
draft = message.text;
|
|
27
|
+
editing = true;
|
|
28
|
+
await tick();
|
|
29
|
+
field?.focus();
|
|
30
|
+
field?.select();
|
|
31
|
+
}
|
|
32
|
+
function save(): void {
|
|
33
|
+
const text = draft.trim();
|
|
34
|
+
if (!text) return;
|
|
35
|
+
editing = false;
|
|
36
|
+
if (text !== message.text.trim()) onedit?.(text);
|
|
37
|
+
}
|
|
38
|
+
function onkeydown(event: KeyboardEvent): void {
|
|
39
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
40
|
+
if (event.key === 'Escape') {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
editing = false;
|
|
43
|
+
} else if (event.key === 'Enter' && !event.shiftKey) {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
save();
|
|
46
|
+
}
|
|
14
47
|
}
|
|
15
|
-
let { message, last = false }: Props = $props();
|
|
16
48
|
</script>
|
|
17
49
|
|
|
18
50
|
<div class="group/message flex flex-col items-end gap-1 px-4 py-2" data-role="user">
|
|
@@ -37,10 +69,45 @@
|
|
|
37
69
|
{/each}
|
|
38
70
|
</ul>
|
|
39
71
|
{/if}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
{#if editing}
|
|
73
|
+
<form
|
|
74
|
+
class="flex w-full max-w-[85%] flex-col gap-2 rounded-2xl border border-input bg-background p-2 focus-within:ring-2 focus-within:ring-ring/20"
|
|
75
|
+
aria-label="Edit message"
|
|
76
|
+
onsubmit={(e) => {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
save();
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<Textarea
|
|
82
|
+
bind:ref={field}
|
|
83
|
+
bind:value={draft}
|
|
84
|
+
autosize
|
|
85
|
+
rows={1}
|
|
86
|
+
maxRows={10}
|
|
87
|
+
maxlength={LIMITS.messageChars}
|
|
88
|
+
aria-label="Edited message"
|
|
89
|
+
class="border-0 bg-transparent shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
|
90
|
+
{onkeydown}
|
|
91
|
+
/>
|
|
92
|
+
<div class="flex justify-end gap-2">
|
|
93
|
+
<Button type="button" variant="ghost" size="sm" onclick={() => (editing = false)}
|
|
94
|
+
>Cancel</Button
|
|
95
|
+
>
|
|
96
|
+
<Button type="submit" size="sm" disabled={!draft.trim()}>Send</Button>
|
|
97
|
+
</div>
|
|
98
|
+
</form>
|
|
99
|
+
{:else}
|
|
100
|
+
<div
|
|
101
|
+
class="max-w-[85%] whitespace-pre-wrap rounded-2xl rounded-br-md bg-muted px-4 py-2.5 text-foreground [overflow-wrap:anywhere]"
|
|
102
|
+
>
|
|
103
|
+
{message.text}
|
|
104
|
+
</div>
|
|
105
|
+
<MessageActions
|
|
106
|
+
text={message.text}
|
|
107
|
+
createdAt={message.createdAt}
|
|
108
|
+
visible={last}
|
|
109
|
+
align="end"
|
|
110
|
+
onedit={onedit ? startEdit : undefined}
|
|
111
|
+
/>
|
|
112
|
+
{/if}
|
|
46
113
|
</div>
|
|
@@ -2,8 +2,13 @@ import type { UserMessage } from '../reducer.js';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
message: UserMessage;
|
|
4
4
|
last?: boolean;
|
|
5
|
+
/** Replace this question with new text and answer it again. */
|
|
6
|
+
onedit?: (text: string) => void;
|
|
5
7
|
}
|
|
6
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The user's turn: attachments and context above a right-aligned muted bubble, then the action bar.
|
|
10
|
+
* With `onedit` (the latest question) the bubble can be edited in place and sent again.
|
|
11
|
+
*/
|
|
7
12
|
declare const UserMessage: import("svelte").Component<Props, {}, "">;
|
|
8
13
|
type UserMessage = ReturnType<typeof UserMessage>;
|
|
9
14
|
export default UserMessage;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/UserMessage.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"UserMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/UserMessage.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,+DAA+D;IAC/D,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AA+EH;;;GAGG;AACH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,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":"regenerate.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/regenerate.svelte.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,108 @@
|
|
|
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 { latestTurn, newAssistantMessage, newUserMessage } from '../reducer.js';
|
|
5
|
+
import { ChatSession } from '../session.svelte.js';
|
|
6
|
+
import AgentChat from './AgentChat.svelte';
|
|
7
|
+
afterEach(() => cleanup());
|
|
8
|
+
/** Answers each request with "Answer N" (N counts requests). */
|
|
9
|
+
function fakeClient() {
|
|
10
|
+
let n = 0;
|
|
11
|
+
return {
|
|
12
|
+
fileUrl: (id) => `#${id}`,
|
|
13
|
+
uploadFile: vi.fn(),
|
|
14
|
+
cancel: vi.fn(),
|
|
15
|
+
getConversation: vi.fn(),
|
|
16
|
+
resumeTurn: vi.fn(),
|
|
17
|
+
streamChat: vi.fn(async (req, opts) => {
|
|
18
|
+
n += 1;
|
|
19
|
+
[
|
|
20
|
+
{ type: 'start', conversationId: 'c-1', mode: 'live' },
|
|
21
|
+
{ type: 'delta', text: `Answer ${n}` },
|
|
22
|
+
{ type: 'done', finishReason: 'stop' },
|
|
23
|
+
].forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
|
|
24
|
+
return { kind: 'done' };
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
describe('latestTurn', () => {
|
|
29
|
+
it('finds the finished latest turn only', () => {
|
|
30
|
+
const done = { ...newAssistantMessage('r2'), status: 'done' };
|
|
31
|
+
const messages = [
|
|
32
|
+
newUserMessage('r1', 'one'),
|
|
33
|
+
{ ...newAssistantMessage('r1'), status: 'done' },
|
|
34
|
+
newUserMessage('r2', 'two'),
|
|
35
|
+
done,
|
|
36
|
+
];
|
|
37
|
+
expect(latestTurn(messages, 'r2')?.user.text).toBe('two');
|
|
38
|
+
expect(latestTurn(messages, 'r2')?.rest).toHaveLength(2);
|
|
39
|
+
expect(latestTurn(messages, 'r1')).toBeUndefined();
|
|
40
|
+
expect(latestTurn([...messages.slice(0, 3), newAssistantMessage('r2')], 'r2')).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('ChatSession regenerate and edit', () => {
|
|
44
|
+
it('replaces the latest turn instead of adding one', async () => {
|
|
45
|
+
const client = fakeClient();
|
|
46
|
+
const session = new ChatSession({ client: client });
|
|
47
|
+
await session.send('first');
|
|
48
|
+
const firstId = client.streamChat.mock.calls[0][0].requestId;
|
|
49
|
+
await session.regenerate(firstId);
|
|
50
|
+
const second = client.streamChat.mock.calls[1][0];
|
|
51
|
+
expect(second).toMatchObject({ message: 'first', replaces: firstId, conversationId: 'c-1' });
|
|
52
|
+
expect(session.messages.map((m) => m.text)).toEqual(['first', 'Answer 2']);
|
|
53
|
+
await session.edit(second.requestId, 'first, but better');
|
|
54
|
+
expect(client.streamChat.mock.calls[2][0]).toMatchObject({
|
|
55
|
+
message: 'first, but better',
|
|
56
|
+
replaces: second.requestId,
|
|
57
|
+
});
|
|
58
|
+
expect(session.messages.map((m) => m.text)).toEqual(['first, but better', 'Answer 3']);
|
|
59
|
+
});
|
|
60
|
+
it('sends a normal message without replaces', async () => {
|
|
61
|
+
const client = fakeClient();
|
|
62
|
+
const session = new ChatSession({ client: client });
|
|
63
|
+
await session.send('hi');
|
|
64
|
+
expect(client.streamChat.mock.calls[0][0]).not.toHaveProperty('replaces');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('AgentChat regenerate and edit', () => {
|
|
68
|
+
it('offers Regenerate on the latest answer and Edit on the latest question', async () => {
|
|
69
|
+
const user = userEvent.setup();
|
|
70
|
+
const client = fakeClient();
|
|
71
|
+
render(AgentChat, { props: { client: client, sandboxOrigin: 'https://frame.test' } });
|
|
72
|
+
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'one{Enter}');
|
|
73
|
+
await screen.findAllByText('Answer 1');
|
|
74
|
+
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'two{Enter}');
|
|
75
|
+
await screen.findAllByText('Answer 2');
|
|
76
|
+
// Only the latest turn.
|
|
77
|
+
expect(screen.getAllByRole('button', { name: 'Regenerate response' })).toHaveLength(1);
|
|
78
|
+
expect(screen.getAllByRole('button', { name: 'Edit message' })).toHaveLength(1);
|
|
79
|
+
await user.click(screen.getByRole('button', { name: 'Regenerate response' }));
|
|
80
|
+
await waitFor(() => expect(client.streamChat).toHaveBeenCalledTimes(3));
|
|
81
|
+
expect(client.streamChat.mock.calls[2][0]).toMatchObject({ message: 'two' });
|
|
82
|
+
expect((await screen.findAllByText('Answer 3')).length).toBeGreaterThan(0);
|
|
83
|
+
expect(screen.queryByText('Answer 2')).toBeNull();
|
|
84
|
+
await user.click(screen.getByRole('button', { name: 'Edit message' }));
|
|
85
|
+
const field = screen.getByRole('textbox', { name: 'Edited message' });
|
|
86
|
+
await user.clear(field);
|
|
87
|
+
await user.type(field, 'two, edited{Enter}');
|
|
88
|
+
await waitFor(() => expect(client.streamChat).toHaveBeenCalledTimes(4));
|
|
89
|
+
expect(client.streamChat.mock.calls[3][0]).toMatchObject({
|
|
90
|
+
message: 'two, edited',
|
|
91
|
+
replaces: client.streamChat.mock.calls[2][0].requestId,
|
|
92
|
+
});
|
|
93
|
+
expect(await screen.findByText('two, edited')).toBeTruthy();
|
|
94
|
+
expect(screen.queryByText(/^two$/)).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
it('cancels an edit with Escape', async () => {
|
|
97
|
+
const user = userEvent.setup();
|
|
98
|
+
const client = fakeClient();
|
|
99
|
+
render(AgentChat, { props: { client: client, sandboxOrigin: 'https://frame.test' } });
|
|
100
|
+
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'one{Enter}');
|
|
101
|
+
await screen.findAllByText('Answer 1');
|
|
102
|
+
await user.click(screen.getByRole('button', { name: 'Edit message' }));
|
|
103
|
+
await user.type(screen.getByRole('textbox', { name: 'Edited message' }), ' more{Escape}');
|
|
104
|
+
expect(screen.queryByRole('textbox', { name: 'Edited message' })).toBeNull();
|
|
105
|
+
expect(screen.getByText('one')).toBeTruthy();
|
|
106
|
+
expect(client.streamChat).toHaveBeenCalledOnce();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -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';
|
package/dist/reducer.d.ts
CHANGED
|
@@ -100,6 +100,14 @@ export declare function turnForRetry(messages: ChatMessage[], requestId: string)
|
|
|
100
100
|
user: UserMessage;
|
|
101
101
|
rest: ChatMessage[];
|
|
102
102
|
} | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* The conversation's latest finished turn (any outcome), to regenerate or edit: its question
|
|
105
|
+
* and the messages before it. Undefined while it streams or when `requestId` is not the latest.
|
|
106
|
+
*/
|
|
107
|
+
export declare function latestTurn(messages: ChatMessage[], requestId: string): {
|
|
108
|
+
user: UserMessage;
|
|
109
|
+
rest: ChatMessage[];
|
|
110
|
+
} | undefined;
|
|
103
111
|
/** Human titles for client error codes (mirrors the task pane's wording). */
|
|
104
112
|
export declare function errorTitle(code: ClientErrorCode): string;
|
|
105
113
|
export type { ClientKind };
|
package/dist/reducer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/lib/reducer.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/lib/reducer.ts"],"names":[],"mappings":"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"}
|
package/dist/reducer.js
CHANGED
|
@@ -182,6 +182,21 @@ export function turnForRetry(messages, requestId) {
|
|
|
182
182
|
return undefined;
|
|
183
183
|
return { user, rest: messages.slice(0, -2) };
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* The conversation's latest finished turn (any outcome), to regenerate or edit: its question
|
|
187
|
+
* and the messages before it. Undefined while it streams or when `requestId` is not the latest.
|
|
188
|
+
*/
|
|
189
|
+
export function latestTurn(messages, requestId) {
|
|
190
|
+
const last = messages[messages.length - 1];
|
|
191
|
+
if (last?.role !== 'assistant' || last.requestId !== requestId)
|
|
192
|
+
return undefined;
|
|
193
|
+
if (last.status === 'streaming')
|
|
194
|
+
return undefined;
|
|
195
|
+
const user = messages[messages.length - 2];
|
|
196
|
+
if (user?.role !== 'user')
|
|
197
|
+
return undefined;
|
|
198
|
+
return { user, rest: messages.slice(0, -2) };
|
|
199
|
+
}
|
|
185
200
|
/** Human titles for client error codes (mirrors the task pane's wording). */
|
|
186
201
|
export function errorTitle(code) {
|
|
187
202
|
const titles = {
|
package/dist/session.svelte.d.ts
CHANGED
|
@@ -50,15 +50,23 @@ export declare class ChatSession {
|
|
|
50
50
|
get current(): AssistantMessage | undefined;
|
|
51
51
|
/** Load an existing conversation's transcript and re-attach to a running turn if any. */
|
|
52
52
|
load(conversationId: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Send a message. `replaces` names the latest turn this one replaces (regenerate, retry,
|
|
55
|
+
* edit); the runtime drops that turn's question and answer before answering.
|
|
56
|
+
*/
|
|
53
57
|
send(text: string, options?: {
|
|
54
58
|
files?: FileRef[];
|
|
59
|
+
replaces?: string;
|
|
55
60
|
}): Promise<void>;
|
|
56
61
|
/**
|
|
57
|
-
* Resend the last turn after it failed, was stopped or was cut short. The
|
|
58
|
-
*
|
|
59
|
-
* message when it had accepted the turn; a server-side regenerate is a separate change.)
|
|
62
|
+
* Resend the last turn after it failed, was stopped or was cut short. The turn is replaced,
|
|
63
|
+
* in the view and in the runtime's transcript, rather than duplicated.
|
|
60
64
|
*/
|
|
61
65
|
retry(requestId: string): Promise<void>;
|
|
66
|
+
/** Ask again for a new answer to the latest question, replacing the current one. */
|
|
67
|
+
regenerate(requestId: string): Promise<void>;
|
|
68
|
+
/** Change the latest question and answer it again, replacing that turn. */
|
|
69
|
+
edit(requestId: string, text: string): Promise<void>;
|
|
62
70
|
/** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
|
|
63
71
|
resume(requestId: string): Promise<void>;
|
|
64
72
|
cancel(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,SAAS,EAET,UAAU,EACV,eAAe,EACf,OAAO,EACR,MAAM,gCAAgC,CAAC;AACxC,OAAO,
|
|
1
|
+
{"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,SAAS,EAET,UAAU,EACV,eAAe,EACf,OAAO,EACR,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAML,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mGAAmG;IACnG,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kEAAkE;IAClE,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,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACrC;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAClG;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7D;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,gBAA6B;IACrC,MAAM,gBAAiC;IACvC,cAAc,gBAA+B;IAC7C,gGAAgG;IAChG,oBAAoB,gBAA+B;IACnD,IAAI,8BAA8C;IAClD,SAAS,qBAAyC;IAClD,YAAY;;;;;;QAAyB;IAErC,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,EAAE,kBAAkB;IAMvC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,gBAAgB,GAAG,SAAS,CAG1C;IAED,yFAAyF;IACnF,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BjD;;;OAGG;IACG,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA2B/F;;;OAGG;IACG,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ7C,oFAAoF;IAC9E,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQlD,2EAA2E;IACrE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ1D,+FAA+F;IACzF,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9C,MAAM,IAAI,IAAI;YAOA,GAAG;IA2DjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,MAAM;CAKf;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAE1E"}
|
package/dist/session.svelte.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ApiRequestError } from '@classic-homes/agent-client';
|
|
2
|
-
import { applyEvent, newAssistantMessage, newUserMessage, turnForRetry, } from './reducer.js';
|
|
2
|
+
import { applyEvent, newAssistantMessage, newUserMessage, latestTurn, turnForRetry, } from './reducer.js';
|
|
3
3
|
/**
|
|
4
4
|
* Reactive conversation state (Svelte 5 runes class). Owns the stream lifecycle: one turn at a
|
|
5
5
|
* time, cancel, resume after reload, history load. Rendering is left to the components.
|
|
@@ -58,6 +58,10 @@ export class ChatSession {
|
|
|
58
58
|
this.loadError = err instanceof Error ? err.message : 'Could not load the conversation';
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Send a message. `replaces` names the latest turn this one replaces (regenerate, retry,
|
|
63
|
+
* edit); the runtime drops that turn's question and answer before answering.
|
|
64
|
+
*/
|
|
61
65
|
async send(text, options = {}) {
|
|
62
66
|
const message = text.trim();
|
|
63
67
|
if (!message || this.status !== 'idle')
|
|
@@ -74,6 +78,7 @@ export class ChatSession {
|
|
|
74
78
|
...(context ? { context } : {}),
|
|
75
79
|
...(files.length ? { fileIds: files.map((f) => f.fileId) } : {}),
|
|
76
80
|
...(this.options.clientKind ? { client: this.options.clientKind } : {}),
|
|
81
|
+
...(options.replaces ? { replaces: options.replaces } : {}),
|
|
77
82
|
};
|
|
78
83
|
this.messages = [
|
|
79
84
|
...this.messages,
|
|
@@ -83,9 +88,8 @@ export class ChatSession {
|
|
|
83
88
|
await this.run(requestId, (signal, onEvent) => this.options.client.streamChat(request, { signal, onEvent }));
|
|
84
89
|
}
|
|
85
90
|
/**
|
|
86
|
-
* Resend the last turn after it failed, was stopped or was cut short. The
|
|
87
|
-
*
|
|
88
|
-
* message when it had accepted the turn; a server-side regenerate is a separate change.)
|
|
91
|
+
* Resend the last turn after it failed, was stopped or was cut short. The turn is replaced,
|
|
92
|
+
* in the view and in the runtime's transcript, rather than duplicated.
|
|
89
93
|
*/
|
|
90
94
|
async retry(requestId) {
|
|
91
95
|
if (this.status !== 'idle')
|
|
@@ -94,7 +98,27 @@ export class ChatSession {
|
|
|
94
98
|
if (!turn)
|
|
95
99
|
return;
|
|
96
100
|
this.messages = turn.rest;
|
|
97
|
-
await this.send(turn.user.text, { files: turn.user.attachments });
|
|
101
|
+
await this.send(turn.user.text, { files: turn.user.attachments, replaces: requestId });
|
|
102
|
+
}
|
|
103
|
+
/** Ask again for a new answer to the latest question, replacing the current one. */
|
|
104
|
+
async regenerate(requestId) {
|
|
105
|
+
if (this.status !== 'idle')
|
|
106
|
+
return;
|
|
107
|
+
const turn = latestTurn(this.messages, requestId);
|
|
108
|
+
if (!turn)
|
|
109
|
+
return;
|
|
110
|
+
this.messages = turn.rest;
|
|
111
|
+
await this.send(turn.user.text, { files: turn.user.attachments, replaces: requestId });
|
|
112
|
+
}
|
|
113
|
+
/** Change the latest question and answer it again, replacing that turn. */
|
|
114
|
+
async edit(requestId, text) {
|
|
115
|
+
if (this.status !== 'idle' || !text.trim())
|
|
116
|
+
return;
|
|
117
|
+
const turn = latestTurn(this.messages, requestId);
|
|
118
|
+
if (!turn)
|
|
119
|
+
return;
|
|
120
|
+
this.messages = turn.rest;
|
|
121
|
+
await this.send(text, { files: turn.user.attachments, replaces: requestId });
|
|
98
122
|
}
|
|
99
123
|
/** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
|
|
100
124
|
async resume(requestId) {
|
|
@@ -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.6.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.6.0",
|
|
21
|
+
"@classic-homes/agent-contracts": "0.6.0",
|
|
22
22
|
"dompurify": "3.4.17"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|