@classic-homes/agent-chat 0.5.0 → 0.7.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 +2 -1
- package/dist/components/AgentChat.svelte +8 -0
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/AssistantMessage.svelte +10 -0
- package/dist/components/AssistantMessage.svelte.d.ts +5 -0
- package/dist/components/AssistantMessage.svelte.d.ts.map +1 -1
- package/dist/components/MessageActions.svelte +47 -4
- package/dist/components/MessageActions.svelte.d.ts +10 -1
- package/dist/components/MessageActions.svelte.d.ts.map +1 -1
- package/dist/components/MessageFeedback.svelte +89 -0
- package/dist/components/MessageFeedback.svelte.d.ts +13 -0
- package/dist/components/MessageFeedback.svelte.d.ts.map +1 -0
- package/dist/components/MessageItem.svelte +7 -2
- package/dist/components/MessageItem.svelte.d.ts +5 -0
- package/dist/components/MessageItem.svelte.d.ts.map +1 -1
- package/dist/components/MessageList.svelte +35 -0
- package/dist/components/MessageList.svelte.d.ts +7 -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/feedback.svelte.test.d.ts +2 -0
- package/dist/components/feedback.svelte.test.d.ts.map +1 -0
- package/dist/components/feedback.svelte.test.js +99 -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/reducer.d.ts +11 -1
- package/dist/reducer.d.ts.map +1 -1
- package/dist/reducer.js +15 -0
- package/dist/session.svelte.d.ts +17 -4
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +61 -5
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -44,7 +44,8 @@ agent-api origin that serves `/frame`; artifacts and MCP-UI resources only ever
|
|
|
44
44
|
|
|
45
45
|
- `ChatHeader`, `ConversationSidebar`: title and conversation actions; the history rail.
|
|
46
46
|
- `MessageList`: transcript with day separators and a jump-to-latest button.
|
|
47
|
-
- `MessageItem` → `UserMessage` / `AssistantMessage`, with `MessageActions` (copy, time
|
|
47
|
+
- `MessageItem` → `UserMessage` / `AssistantMessage`, with `MessageActions` (copy, time,
|
|
48
|
+
`MessageFeedback` thumbs up/down with an optional note, Regenerate and Edit on the latest turn),
|
|
48
49
|
`ArtifactChip`, `MessageFiles` (image lightbox, downloads) and `ProposalCard` (read-only
|
|
49
50
|
document edits; applying stays in the Office add-in).
|
|
50
51
|
- `Composer`: attach by button, drop or paste; `prompts` turns on the `/` saved-prompt menu
|
|
@@ -254,6 +254,14 @@
|
|
|
254
254
|
ontool={(name, params) => ontoolrequest?.(name, params)}
|
|
255
255
|
{onnotify}
|
|
256
256
|
onretry={retry}
|
|
257
|
+
onregenerate={(requestId) => void session.regenerate(requestId)}
|
|
258
|
+
onedit={(requestId, text) => void session.edit(requestId, text)}
|
|
259
|
+
onrate={session.conversationId
|
|
260
|
+
? (requestId, rating, comment) =>
|
|
261
|
+
session
|
|
262
|
+
.rate(requestId, rating, comment)
|
|
263
|
+
.catch(() => onnotify?.('Could not save your feedback. Try again.'))
|
|
264
|
+
: undefined}
|
|
257
265
|
oncontinue={() => void session.send('Continue.')}
|
|
258
266
|
empty={emptyState}
|
|
259
267
|
/>
|
|
@@ -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;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;
|
|
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;AAmNH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
-->
|
|
6
6
|
<script lang="ts">
|
|
7
7
|
import { Alert, Button, Icon } from '@classic-homes/theme-svelte';
|
|
8
|
+
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
8
9
|
import type { AssistantMessage } from '../reducer.js';
|
|
9
10
|
import { errorTitle } from '../reducer.js';
|
|
10
11
|
import { parseMarkdown } from '../markdown.js';
|
|
@@ -30,6 +31,10 @@
|
|
|
30
31
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
31
32
|
onnotify?: (message: string) => void;
|
|
32
33
|
onretry?: () => void;
|
|
34
|
+
/** Ask for a new answer to the same question (the latest finished answer). */
|
|
35
|
+
onregenerate?: () => void;
|
|
36
|
+
/** Rate this answer; offered once it is done. */
|
|
37
|
+
onrate?: (rating: TurnRating | null, comment?: string) => void | Promise<void>;
|
|
33
38
|
/** Ask the agent to pick up a stopped or cut-short answer. */
|
|
34
39
|
oncontinue?: () => void;
|
|
35
40
|
}
|
|
@@ -46,6 +51,8 @@
|
|
|
46
51
|
ontool,
|
|
47
52
|
onnotify,
|
|
48
53
|
onretry,
|
|
54
|
+
onregenerate,
|
|
55
|
+
onrate,
|
|
49
56
|
oncontinue,
|
|
50
57
|
}: Props = $props();
|
|
51
58
|
|
|
@@ -179,6 +186,9 @@
|
|
|
179
186
|
text={message.text}
|
|
180
187
|
createdAt={message.createdAt}
|
|
181
188
|
visible={last}
|
|
189
|
+
{onregenerate}
|
|
190
|
+
rating={message.feedback}
|
|
191
|
+
onrate={message.status === 'done' ? onrate : undefined}
|
|
182
192
|
class="-ml-1.5"
|
|
183
193
|
/>
|
|
184
194
|
{/if}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
1
2
|
import type { AssistantMessage } from '../reducer.js';
|
|
2
3
|
interface Props {
|
|
3
4
|
message: AssistantMessage;
|
|
@@ -13,6 +14,10 @@ interface Props {
|
|
|
13
14
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
14
15
|
onnotify?: (message: string) => void;
|
|
15
16
|
onretry?: () => void;
|
|
17
|
+
/** Ask for a new answer to the same question (the latest finished answer). */
|
|
18
|
+
onregenerate?: () => void;
|
|
19
|
+
/** Rate this answer; offered once it is done. */
|
|
20
|
+
onrate?: (rating: TurnRating | null, comment?: string) => void | Promise<void>;
|
|
16
21
|
/** Ask the agent to pick up a stopped or cut-short answer. */
|
|
17
22
|
oncontinue?: () => void;
|
|
18
23
|
}
|
|
@@ -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,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,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,iDAAiD;IACjD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AA2JH;;;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,14 @@
|
|
|
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 {
|
|
9
|
+
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
10
|
+
import { Button, cn, CopyButton, Icon, Tooltip } from '@classic-homes/theme-svelte';
|
|
11
|
+
import MessageFeedback from './MessageFeedback.svelte';
|
|
9
12
|
import { formatDateTime, formatTime } from '../message-format.js';
|
|
10
13
|
|
|
11
14
|
interface Props {
|
|
@@ -14,14 +17,31 @@
|
|
|
14
17
|
/** Keep the bar visible without hover (the last message). */
|
|
15
18
|
visible?: boolean;
|
|
16
19
|
align?: 'start' | 'end';
|
|
20
|
+
/** Offer "Regenerate" (the latest answer). */
|
|
21
|
+
onregenerate?: () => void;
|
|
22
|
+
/** Offer "Edit" (the latest question). */
|
|
23
|
+
onedit?: () => void;
|
|
24
|
+
/** The answer's rating; with `onrate`, thumbs up/down are offered. */
|
|
25
|
+
rating?: TurnRating;
|
|
26
|
+
onrate?: (rating: TurnRating | null, comment?: string) => void | Promise<void>;
|
|
17
27
|
class?: string;
|
|
18
28
|
}
|
|
19
|
-
let {
|
|
29
|
+
let {
|
|
30
|
+
text,
|
|
31
|
+
createdAt,
|
|
32
|
+
visible = false,
|
|
33
|
+
align = 'start',
|
|
34
|
+
onregenerate,
|
|
35
|
+
onedit,
|
|
36
|
+
rating,
|
|
37
|
+
onrate,
|
|
38
|
+
class: className,
|
|
39
|
+
}: Props = $props();
|
|
20
40
|
</script>
|
|
21
41
|
|
|
22
42
|
<div
|
|
23
43
|
class={cn(
|
|
24
|
-
'flex items-center gap-0.5 text-xs text-muted-foreground transition-opacity',
|
|
44
|
+
'flex flex-wrap items-center gap-0.5 text-xs text-muted-foreground transition-opacity',
|
|
25
45
|
align === 'end' && 'justify-end',
|
|
26
46
|
!visible &&
|
|
27
47
|
'opacity-0 group-hover/message:opacity-100 group-focus-within/message:opacity-100 [@media(hover:none)]:opacity-100',
|
|
@@ -39,6 +59,29 @@
|
|
|
39
59
|
class="h-7 w-7"
|
|
40
60
|
/>
|
|
41
61
|
{/if}
|
|
62
|
+
{#if onrate}
|
|
63
|
+
<MessageFeedback {rating} {onrate} />
|
|
64
|
+
{/if}
|
|
65
|
+
{#if onedit}
|
|
66
|
+
<Button
|
|
67
|
+
variant="ghost"
|
|
68
|
+
size="icon"
|
|
69
|
+
class="h-7 w-7"
|
|
70
|
+
aria-label="Edit message"
|
|
71
|
+
title="Edit"
|
|
72
|
+
onclick={onedit}><Icon name="edit" size={14} /></Button
|
|
73
|
+
>
|
|
74
|
+
{/if}
|
|
75
|
+
{#if onregenerate}
|
|
76
|
+
<Button
|
|
77
|
+
variant="ghost"
|
|
78
|
+
size="icon"
|
|
79
|
+
class="h-7 w-7"
|
|
80
|
+
aria-label="Regenerate response"
|
|
81
|
+
title="Regenerate"
|
|
82
|
+
onclick={onregenerate}><Icon name="refresh" size={14} /></Button
|
|
83
|
+
>
|
|
84
|
+
{/if}
|
|
42
85
|
<Tooltip content={formatDateTime(createdAt)}>
|
|
43
86
|
{#snippet children({ props })}
|
|
44
87
|
<!-- Focusable so keyboard users can read the full date in the tooltip. -->
|
|
@@ -1,13 +1,22 @@
|
|
|
1
|
+
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
1
2
|
interface Props {
|
|
2
3
|
text: string;
|
|
3
4
|
createdAt: number;
|
|
4
5
|
/** Keep the bar visible without hover (the last message). */
|
|
5
6
|
visible?: boolean;
|
|
6
7
|
align?: 'start' | 'end';
|
|
8
|
+
/** Offer "Regenerate" (the latest answer). */
|
|
9
|
+
onregenerate?: () => void;
|
|
10
|
+
/** Offer "Edit" (the latest question). */
|
|
11
|
+
onedit?: () => void;
|
|
12
|
+
/** The answer's rating; with `onrate`, thumbs up/down are offered. */
|
|
13
|
+
rating?: TurnRating;
|
|
14
|
+
onrate?: (rating: TurnRating | null, comment?: string) => void | Promise<void>;
|
|
7
15
|
class?: string;
|
|
8
16
|
}
|
|
9
17
|
/**
|
|
10
|
-
* Per-message action bar: copy the raw text
|
|
18
|
+
* Per-message action bar: copy the raw text, regenerate or edit (latest turn only) and the time
|
|
19
|
+
* it was sent. Hidden until the message is
|
|
11
20
|
* hovered or holds focus, except where `visible` (the last message) or on touch screens, which
|
|
12
21
|
* have no hover.
|
|
13
22
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageActions.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageActions.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MessageActions.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageActions.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAM/D,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,sEAAsE;IACtE,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqDH;;;;;GAKG;AACH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Thumbs up / down for one answer. Choosing the current rating again withdraws it. After a thumbs
|
|
4
|
+
down, an optional note can follow ("What went wrong?").
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
8
|
+
import { Button, cn, Icon, Input } from '@classic-homes/theme-svelte';
|
|
9
|
+
import { tick } from 'svelte';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
rating?: TurnRating;
|
|
13
|
+
onrate: (rating: TurnRating | null, comment?: string) => void | Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
let { rating, onrate }: Props = $props();
|
|
16
|
+
|
|
17
|
+
let asking = $state(false);
|
|
18
|
+
let note = $state('');
|
|
19
|
+
const noteId = $props.id();
|
|
20
|
+
const NOTE_MAX = 1000;
|
|
21
|
+
|
|
22
|
+
async function choose(value: TurnRating): Promise<void> {
|
|
23
|
+
const next = rating === value ? null : value;
|
|
24
|
+
asking = next === 'down';
|
|
25
|
+
note = '';
|
|
26
|
+
await onrate(next);
|
|
27
|
+
if (asking) {
|
|
28
|
+
await tick();
|
|
29
|
+
document.getElementById(noteId)?.focus();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function sendNote(): Promise<void> {
|
|
33
|
+
const text = note.trim();
|
|
34
|
+
asking = false;
|
|
35
|
+
if (text) await onrate('down', text);
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<span class="inline-flex items-center" role="group" aria-label="Rate this answer">
|
|
40
|
+
<Button
|
|
41
|
+
variant="ghost"
|
|
42
|
+
size="icon"
|
|
43
|
+
class={cn('h-7 w-7', rating === 'up' && 'text-foreground')}
|
|
44
|
+
aria-label="Good answer"
|
|
45
|
+
aria-pressed={rating === 'up'}
|
|
46
|
+
title="Good answer"
|
|
47
|
+
onclick={() => void choose('up')}
|
|
48
|
+
>
|
|
49
|
+
<Icon name="thumbs-up" size={14} class={cn(rating === 'up' && 'fill-current')} />
|
|
50
|
+
</Button>
|
|
51
|
+
<Button
|
|
52
|
+
variant="ghost"
|
|
53
|
+
size="icon"
|
|
54
|
+
class={cn('h-7 w-7', rating === 'down' && 'text-foreground')}
|
|
55
|
+
aria-label="Bad answer"
|
|
56
|
+
aria-pressed={rating === 'down'}
|
|
57
|
+
title="Bad answer"
|
|
58
|
+
onclick={() => void choose('down')}
|
|
59
|
+
>
|
|
60
|
+
<Icon name="thumbs-down" size={14} class={cn(rating === 'down' && 'fill-current')} />
|
|
61
|
+
</Button>
|
|
62
|
+
</span>
|
|
63
|
+
{#if asking}
|
|
64
|
+
<form
|
|
65
|
+
class="flex basis-full items-center gap-2 pt-1"
|
|
66
|
+
aria-label="Feedback note"
|
|
67
|
+
onsubmit={(e) => {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
void sendNote();
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<Input
|
|
73
|
+
id={noteId}
|
|
74
|
+
bind:value={note}
|
|
75
|
+
maxlength={NOTE_MAX}
|
|
76
|
+
placeholder="What went wrong? (optional)"
|
|
77
|
+
aria-label="What went wrong?"
|
|
78
|
+
class="h-8 max-w-sm text-sm"
|
|
79
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
80
|
+
if (e.key === 'Escape') {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
asking = false;
|
|
83
|
+
}
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
<Button type="submit" size="sm" variant="outline" disabled={!note.trim()}>Send</Button>
|
|
87
|
+
<Button type="button" size="sm" variant="ghost" onclick={() => (asking = false)}>Skip</Button>
|
|
88
|
+
</form>
|
|
89
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
2
|
+
interface Props {
|
|
3
|
+
rating?: TurnRating;
|
|
4
|
+
onrate: (rating: TurnRating | null, comment?: string) => void | Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Thumbs up / down for one answer. Choosing the current rating again withdraws it. After a thumbs
|
|
8
|
+
* down, an optional note can follow ("What went wrong?").
|
|
9
|
+
*/
|
|
10
|
+
declare const MessageFeedback: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type MessageFeedback = ReturnType<typeof MessageFeedback>;
|
|
12
|
+
export default MessageFeedback;
|
|
13
|
+
//# sourceMappingURL=MessageFeedback.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MessageFeedback.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageFeedback.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAK/D,UAAU,KAAK;IACb,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,MAAM,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AAyDH;;;GAGG;AACH,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
One transcript entry: a `UserMessage` or an `AssistantMessage`.
|
|
4
4
|
-->
|
|
5
5
|
<script lang="ts">
|
|
6
|
+
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
6
7
|
import type { ChatMessage } from '../reducer.js';
|
|
7
8
|
import AssistantMessage from './AssistantMessage.svelte';
|
|
8
9
|
import UserMessage from './UserMessage.svelte';
|
|
@@ -20,13 +21,17 @@
|
|
|
20
21
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
21
22
|
onnotify?: (message: string) => void;
|
|
22
23
|
onretry?: () => void;
|
|
24
|
+
onregenerate?: () => void;
|
|
25
|
+
onrate?: (rating: TurnRating | null, comment?: string) => void | Promise<void>;
|
|
23
26
|
oncontinue?: () => void;
|
|
27
|
+
/** User messages only: edit and resend. */
|
|
28
|
+
onedit?: (text: string) => void;
|
|
24
29
|
}
|
|
25
|
-
let { message, last = false, ...rest }: Props = $props();
|
|
30
|
+
let { message, last = false, onedit, ...rest }: Props = $props();
|
|
26
31
|
</script>
|
|
27
32
|
|
|
28
33
|
{#if message.role === 'user'}
|
|
29
|
-
<UserMessage {message} {last} />
|
|
34
|
+
<UserMessage {message} {last} {onedit} />
|
|
30
35
|
{:else}
|
|
31
36
|
<AssistantMessage {message} {last} {...rest} />
|
|
32
37
|
{/if}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
1
2
|
import type { ChatMessage } from '../reducer.js';
|
|
2
3
|
interface Props {
|
|
3
4
|
message: ChatMessage;
|
|
@@ -12,7 +13,11 @@ interface Props {
|
|
|
12
13
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
13
14
|
onnotify?: (message: string) => void;
|
|
14
15
|
onretry?: () => void;
|
|
16
|
+
onregenerate?: () => void;
|
|
17
|
+
onrate?: (rating: TurnRating | null, comment?: string) => void | Promise<void>;
|
|
15
18
|
oncontinue?: () => void;
|
|
19
|
+
/** User messages only: edit and resend. */
|
|
20
|
+
onedit?: (text: string) => void;
|
|
16
21
|
}
|
|
17
22
|
/** One transcript entry: a `UserMessage` or an `AssistantMessage`. */
|
|
18
23
|
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,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,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,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAmBH,sEAAsE;AACtE,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tick } from 'svelte';
|
|
3
3
|
import { Button, Icon } from '@classic-homes/theme-svelte';
|
|
4
|
+
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
4
5
|
import type { ChatMessage } from '../reducer.js';
|
|
5
6
|
import { daySeparators, formatDay } from '../message-format.js';
|
|
6
7
|
import MessageItem from './MessageItem.svelte';
|
|
@@ -17,6 +18,16 @@
|
|
|
17
18
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
18
19
|
onnotify?: (message: string) => void;
|
|
19
20
|
onretry?: () => void;
|
|
21
|
+
/** Regenerate the latest answer, by its request id. */
|
|
22
|
+
onregenerate?: (requestId: string) => void;
|
|
23
|
+
/** Rate an answer, by its request id. */
|
|
24
|
+
onrate?: (
|
|
25
|
+
requestId: string,
|
|
26
|
+
rating: TurnRating | null,
|
|
27
|
+
comment?: string,
|
|
28
|
+
) => void | Promise<void>;
|
|
29
|
+
/** Edit the latest question, by its request id. */
|
|
30
|
+
onedit?: (requestId: string, text: string) => void;
|
|
20
31
|
oncontinue?: () => void;
|
|
21
32
|
empty?: import('svelte').Snippet;
|
|
22
33
|
}
|
|
@@ -32,10 +43,20 @@
|
|
|
32
43
|
ontool,
|
|
33
44
|
onnotify,
|
|
34
45
|
onretry,
|
|
46
|
+
onregenerate,
|
|
47
|
+
onrate,
|
|
48
|
+
onedit,
|
|
35
49
|
oncontinue,
|
|
36
50
|
empty,
|
|
37
51
|
}: Props = $props();
|
|
38
52
|
|
|
53
|
+
// The latest turn can be regenerated or edited once it has finished.
|
|
54
|
+
const latest = $derived.by(() => {
|
|
55
|
+
const last = messages[messages.length - 1];
|
|
56
|
+
if (streaming || last?.role !== 'assistant' || last.status === 'streaming') return undefined;
|
|
57
|
+
return messages[messages.length - 2]?.role === 'user' ? last.requestId : undefined;
|
|
58
|
+
});
|
|
59
|
+
|
|
39
60
|
let container: HTMLDivElement | undefined = $state();
|
|
40
61
|
/** Pinned to the bottom: new content scrolls into view. Off once the reader scrolls up. */
|
|
41
62
|
let follow = $state(true);
|
|
@@ -143,6 +164,20 @@
|
|
|
143
164
|
{onnotify}
|
|
144
165
|
onretry={i === messages.length - 1 ? onretry : undefined}
|
|
145
166
|
oncontinue={i === messages.length - 1 && !streaming ? oncontinue : undefined}
|
|
167
|
+
onregenerate={latest &&
|
|
168
|
+
onregenerate &&
|
|
169
|
+
i === messages.length - 1 &&
|
|
170
|
+
message.role === 'assistant' &&
|
|
171
|
+
message.status === 'done'
|
|
172
|
+
? () => onregenerate(latest)
|
|
173
|
+
: undefined}
|
|
174
|
+
onrate={onrate && message.role === 'assistant'
|
|
175
|
+
? (rating: TurnRating | null, comment?: string) =>
|
|
176
|
+
onrate(message.requestId, rating, comment)
|
|
177
|
+
: undefined}
|
|
178
|
+
onedit={latest && onedit && i === messages.length - 2
|
|
179
|
+
? (text: string) => onedit(latest, text)
|
|
180
|
+
: undefined}
|
|
146
181
|
/>
|
|
147
182
|
{/each}
|
|
148
183
|
</div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
1
2
|
import type { ChatMessage } from '../reducer.js';
|
|
2
3
|
interface Props {
|
|
3
4
|
messages: ChatMessage[];
|
|
@@ -11,6 +12,12 @@ interface Props {
|
|
|
11
12
|
ontool?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
12
13
|
onnotify?: (message: string) => void;
|
|
13
14
|
onretry?: () => void;
|
|
15
|
+
/** Regenerate the latest answer, by its request id. */
|
|
16
|
+
onregenerate?: (requestId: string) => void;
|
|
17
|
+
/** Rate an answer, by its request id. */
|
|
18
|
+
onrate?: (requestId: string, rating: TurnRating | null, comment?: string) => void | Promise<void>;
|
|
19
|
+
/** Edit the latest question, by its request id. */
|
|
20
|
+
onedit?: (requestId: string, text: string) => void;
|
|
14
21
|
oncontinue?: () => void;
|
|
15
22
|
empty?: import('svelte').Snippet;
|
|
16
23
|
}
|
|
@@ -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,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,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,yCAAyC;IACzC,MAAM,CAAC,EAAE,CACP,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,UAAU,GAAG,IAAI,EACzB,OAAO,CAAC,EAAE,MAAM,KACb,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,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;AAkJH,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 @@
|
|
|
1
|
+
{"version":3,"file":"feedback.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/feedback.svelte.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { ChatSession } from '../session.svelte.js';
|
|
5
|
+
import AgentChat from './AgentChat.svelte';
|
|
6
|
+
afterEach(() => cleanup());
|
|
7
|
+
function fakeClient(sendFeedback = vi.fn().mockResolvedValue(undefined)) {
|
|
8
|
+
return {
|
|
9
|
+
fileUrl: (id) => `#${id}`,
|
|
10
|
+
uploadFile: vi.fn(),
|
|
11
|
+
cancel: vi.fn(),
|
|
12
|
+
resumeTurn: vi.fn(),
|
|
13
|
+
sendFeedback,
|
|
14
|
+
getConversation: vi.fn().mockResolvedValue({
|
|
15
|
+
conversation: { conversationId: 'c-1' },
|
|
16
|
+
messages: [
|
|
17
|
+
{ id: 'u-r1', seq: 0, role: 'user', content: [{ type: 'text', text: 'Q' }], createdAt: 1 },
|
|
18
|
+
{
|
|
19
|
+
id: 'a-r1',
|
|
20
|
+
seq: 1,
|
|
21
|
+
role: 'assistant',
|
|
22
|
+
content: [{ type: 'text', text: 'A' }],
|
|
23
|
+
createdAt: 2,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
files: [],
|
|
27
|
+
feedback: { r1: 'up' },
|
|
28
|
+
}),
|
|
29
|
+
streamChat: vi.fn(async (req, opts) => {
|
|
30
|
+
[
|
|
31
|
+
{ type: 'start', conversationId: 'c-1', mode: 'live' },
|
|
32
|
+
{ type: 'delta', text: 'Answer' },
|
|
33
|
+
{ type: 'done', finishReason: 'stop' },
|
|
34
|
+
].forEach((e, seq) => opts.onEvent({ requestId: req.requestId, seq, ...e }));
|
|
35
|
+
return { kind: 'done' };
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
describe('ChatSession.rate', () => {
|
|
40
|
+
it('loads stored ratings and sends changes', async () => {
|
|
41
|
+
const client = fakeClient();
|
|
42
|
+
const session = new ChatSession({ client: client });
|
|
43
|
+
await session.load('c-1');
|
|
44
|
+
expect(session.messages[1]).toMatchObject({ feedback: 'up' });
|
|
45
|
+
await session.rate('r1', 'down', ' Wrong lot ');
|
|
46
|
+
expect(client.sendFeedback).toHaveBeenCalledWith('c-1', 'r1', {
|
|
47
|
+
rating: 'down',
|
|
48
|
+
comment: 'Wrong lot',
|
|
49
|
+
});
|
|
50
|
+
expect(session.messages[1]).toMatchObject({ feedback: 'down' });
|
|
51
|
+
await session.rate('r1', null);
|
|
52
|
+
expect(session.messages[1]).not.toHaveProperty('feedback');
|
|
53
|
+
});
|
|
54
|
+
it('puts the old rating back when the runtime refuses', async () => {
|
|
55
|
+
const client = fakeClient(vi.fn().mockRejectedValue(new Error('nope')));
|
|
56
|
+
const session = new ChatSession({ client: client });
|
|
57
|
+
await session.load('c-1');
|
|
58
|
+
await expect(session.rate('r1', 'down')).rejects.toThrow('nope');
|
|
59
|
+
expect(session.messages[1]).toMatchObject({ feedback: 'up' });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('AgentChat feedback', () => {
|
|
63
|
+
it('rates an answer, asks for a note after a thumbs down, and toggles off', async () => {
|
|
64
|
+
const user = userEvent.setup();
|
|
65
|
+
const client = fakeClient();
|
|
66
|
+
render(AgentChat, { props: { client: client, sandboxOrigin: 'https://frame.test' } });
|
|
67
|
+
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'hi{Enter}');
|
|
68
|
+
await screen.findAllByText('Answer');
|
|
69
|
+
const requestId = client.streamChat.mock.calls[0][0].requestId;
|
|
70
|
+
const down = screen.getByRole('button', { name: 'Bad answer' });
|
|
71
|
+
await user.click(down);
|
|
72
|
+
expect(client.sendFeedback).toHaveBeenLastCalledWith('c-1', requestId, { rating: 'down' });
|
|
73
|
+
expect(down.getAttribute('aria-pressed')).toBe('true');
|
|
74
|
+
const note = await screen.findByRole('textbox', { name: 'What went wrong?' });
|
|
75
|
+
await waitFor(() => expect(document.activeElement).toBe(note));
|
|
76
|
+
await user.type(note, 'Old prices{Enter}');
|
|
77
|
+
expect(client.sendFeedback).toHaveBeenLastCalledWith('c-1', requestId, {
|
|
78
|
+
rating: 'down',
|
|
79
|
+
comment: 'Old prices',
|
|
80
|
+
});
|
|
81
|
+
expect(screen.queryByRole('textbox', { name: 'What went wrong?' })).toBeNull();
|
|
82
|
+
await user.click(screen.getByRole('button', { name: 'Bad answer' }));
|
|
83
|
+
expect(client.sendFeedback).toHaveBeenLastCalledWith('c-1', requestId, { rating: null });
|
|
84
|
+
expect(screen.getByRole('button', { name: 'Bad answer' }).getAttribute('aria-pressed')).toBe('false');
|
|
85
|
+
});
|
|
86
|
+
it('tells the host when a rating could not be saved', async () => {
|
|
87
|
+
const user = userEvent.setup();
|
|
88
|
+
const onnotify = vi.fn();
|
|
89
|
+
const client = fakeClient(vi.fn().mockRejectedValue(new Error('down')));
|
|
90
|
+
render(AgentChat, {
|
|
91
|
+
props: { client: client, sandboxOrigin: 'https://frame.test', onnotify },
|
|
92
|
+
});
|
|
93
|
+
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'hi{Enter}');
|
|
94
|
+
await screen.findAllByText('Answer');
|
|
95
|
+
await user.click(screen.getByRole('button', { name: 'Good answer' }));
|
|
96
|
+
await waitFor(() => expect(onnotify).toHaveBeenCalledWith(expect.stringMatching(/feedback/)));
|
|
97
|
+
expect(screen.getByRole('button', { name: 'Good answer' }).getAttribute('aria-pressed')).toBe('false');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -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
|
+
});
|
package/dist/reducer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ArtifactMediaType, BackendMode, ChatEvent, ClientKind, DocumentContext, FileRef, ProposedOperation, ToolResultStatus } from '@classic-homes/agent-contracts';
|
|
1
|
+
import type { ArtifactMediaType, BackendMode, ChatEvent, ClientKind, DocumentContext, FileRef, ProposedOperation, ToolResultStatus, TurnRating } from '@classic-homes/agent-contracts';
|
|
2
2
|
import type { ClientErrorCode } from '@classic-homes/agent-client';
|
|
3
3
|
/**
|
|
4
4
|
* Pure state model for one conversation view. `applyEvent` folds stream events into the
|
|
@@ -78,6 +78,8 @@ export interface AssistantMessage {
|
|
|
78
78
|
};
|
|
79
79
|
finishReason?: 'stop' | 'length' | 'cancelled' | 'error';
|
|
80
80
|
mode?: BackendMode;
|
|
81
|
+
/** The user's rating of this answer, when given. */
|
|
82
|
+
feedback?: TurnRating;
|
|
81
83
|
createdAt: number;
|
|
82
84
|
}
|
|
83
85
|
export type ChatMessage = UserMessage | AssistantMessage;
|
|
@@ -100,6 +102,14 @@ export declare function turnForRetry(messages: ChatMessage[], requestId: string)
|
|
|
100
102
|
user: UserMessage;
|
|
101
103
|
rest: ChatMessage[];
|
|
102
104
|
} | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* The conversation's latest finished turn (any outcome), to regenerate or edit: its question
|
|
107
|
+
* and the messages before it. Undefined while it streams or when `requestId` is not the latest.
|
|
108
|
+
*/
|
|
109
|
+
export declare function latestTurn(messages: ChatMessage[], requestId: string): {
|
|
110
|
+
user: UserMessage;
|
|
111
|
+
rest: ChatMessage[];
|
|
112
|
+
} | undefined;
|
|
103
113
|
/** Human titles for client error codes (mirrors the task pane's wording). */
|
|
104
114
|
export declare function errorTitle(code: ClientErrorCode): string;
|
|
105
115
|
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":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,WAAW,EACX,SAAS,EACT,UAAU,EACV,eAAe,EACf,OAAO,EACP,iBAAiB,EACjB,gBAAgB,
|
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentApiClient } from '@classic-homes/agent-client';
|
|
2
|
-
import type { AgentKey, BackendMode, ChatEvent, ClientKind, DocumentContext, FileRef } from '@classic-homes/agent-contracts';
|
|
2
|
+
import type { AgentKey, BackendMode, ChatEvent, ClientKind, DocumentContext, FileRef, TurnRating } from '@classic-homes/agent-contracts';
|
|
3
3
|
import { type AssistantMessage, type ChatMessage } from './reducer.js';
|
|
4
4
|
export interface ChatSessionOptions {
|
|
5
5
|
client: AgentApiClient;
|
|
@@ -50,17 +50,30 @@ 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>;
|
|
72
|
+
/**
|
|
73
|
+
* Rate an answer (`null` withdraws the rating). Shown at once; put back and rethrown when the
|
|
74
|
+
* runtime refuses it.
|
|
75
|
+
*/
|
|
76
|
+
rate(requestId: string, rating: TurnRating | null, comment?: string): Promise<void>;
|
|
64
77
|
cancel(): void;
|
|
65
78
|
private run;
|
|
66
79
|
private toolName;
|
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"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"}
|
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.
|
|
@@ -47,6 +47,9 @@ export class ChatSession {
|
|
|
47
47
|
...newAssistantMessage(m.id.replace(/^a-/, ''), m.createdAt),
|
|
48
48
|
text: m.content.map((c) => c.text).join('\n'),
|
|
49
49
|
status: 'done',
|
|
50
|
+
...(detail.feedback?.[m.id.replace(/^a-/, '')]
|
|
51
|
+
? { feedback: detail.feedback[m.id.replace(/^a-/, '')] }
|
|
52
|
+
: {}),
|
|
50
53
|
});
|
|
51
54
|
const active = detail.conversation.activeRequestId;
|
|
52
55
|
this.status = 'idle';
|
|
@@ -58,6 +61,10 @@ export class ChatSession {
|
|
|
58
61
|
this.loadError = err instanceof Error ? err.message : 'Could not load the conversation';
|
|
59
62
|
}
|
|
60
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Send a message. `replaces` names the latest turn this one replaces (regenerate, retry,
|
|
66
|
+
* edit); the runtime drops that turn's question and answer before answering.
|
|
67
|
+
*/
|
|
61
68
|
async send(text, options = {}) {
|
|
62
69
|
const message = text.trim();
|
|
63
70
|
if (!message || this.status !== 'idle')
|
|
@@ -74,6 +81,7 @@ export class ChatSession {
|
|
|
74
81
|
...(context ? { context } : {}),
|
|
75
82
|
...(files.length ? { fileIds: files.map((f) => f.fileId) } : {}),
|
|
76
83
|
...(this.options.clientKind ? { client: this.options.clientKind } : {}),
|
|
84
|
+
...(options.replaces ? { replaces: options.replaces } : {}),
|
|
77
85
|
};
|
|
78
86
|
this.messages = [
|
|
79
87
|
...this.messages,
|
|
@@ -83,9 +91,8 @@ export class ChatSession {
|
|
|
83
91
|
await this.run(requestId, (signal, onEvent) => this.options.client.streamChat(request, { signal, onEvent }));
|
|
84
92
|
}
|
|
85
93
|
/**
|
|
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.)
|
|
94
|
+
* Resend the last turn after it failed, was stopped or was cut short. The turn is replaced,
|
|
95
|
+
* in the view and in the runtime's transcript, rather than duplicated.
|
|
89
96
|
*/
|
|
90
97
|
async retry(requestId) {
|
|
91
98
|
if (this.status !== 'idle')
|
|
@@ -94,7 +101,27 @@ export class ChatSession {
|
|
|
94
101
|
if (!turn)
|
|
95
102
|
return;
|
|
96
103
|
this.messages = turn.rest;
|
|
97
|
-
await this.send(turn.user.text, { files: turn.user.attachments });
|
|
104
|
+
await this.send(turn.user.text, { files: turn.user.attachments, replaces: requestId });
|
|
105
|
+
}
|
|
106
|
+
/** Ask again for a new answer to the latest question, replacing the current one. */
|
|
107
|
+
async regenerate(requestId) {
|
|
108
|
+
if (this.status !== 'idle')
|
|
109
|
+
return;
|
|
110
|
+
const turn = latestTurn(this.messages, requestId);
|
|
111
|
+
if (!turn)
|
|
112
|
+
return;
|
|
113
|
+
this.messages = turn.rest;
|
|
114
|
+
await this.send(turn.user.text, { files: turn.user.attachments, replaces: requestId });
|
|
115
|
+
}
|
|
116
|
+
/** Change the latest question and answer it again, replacing that turn. */
|
|
117
|
+
async edit(requestId, text) {
|
|
118
|
+
if (this.status !== 'idle' || !text.trim())
|
|
119
|
+
return;
|
|
120
|
+
const turn = latestTurn(this.messages, requestId);
|
|
121
|
+
if (!turn)
|
|
122
|
+
return;
|
|
123
|
+
this.messages = turn.rest;
|
|
124
|
+
await this.send(text, { files: turn.user.attachments, replaces: requestId });
|
|
98
125
|
}
|
|
99
126
|
/** Re-attach to a turn after reload: replays events after the last seq we hold (none here). */
|
|
100
127
|
async resume(requestId) {
|
|
@@ -109,6 +136,35 @@ export class ChatSession {
|
|
|
109
136
|
const conversationId = this.conversationId;
|
|
110
137
|
await this.run(requestId, (signal, onEvent) => this.options.client.resumeTurn(conversationId, requestId, -1, { signal, onEvent }));
|
|
111
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Rate an answer (`null` withdraws the rating). Shown at once; put back and rethrown when the
|
|
141
|
+
* runtime refuses it.
|
|
142
|
+
*/
|
|
143
|
+
async rate(requestId, rating, comment) {
|
|
144
|
+
const conversationId = this.conversationId;
|
|
145
|
+
if (!conversationId)
|
|
146
|
+
return;
|
|
147
|
+
const before = this.messages.find((m) => m.role === 'assistant' && m.requestId === requestId)?.feedback;
|
|
148
|
+
const set = (value) => this.update(requestId, (m) => {
|
|
149
|
+
const next = { ...m };
|
|
150
|
+
if (value)
|
|
151
|
+
next.feedback = value;
|
|
152
|
+
else
|
|
153
|
+
delete next.feedback;
|
|
154
|
+
return next;
|
|
155
|
+
});
|
|
156
|
+
set(rating ?? undefined);
|
|
157
|
+
try {
|
|
158
|
+
await this.options.client.sendFeedback(conversationId, requestId, {
|
|
159
|
+
rating,
|
|
160
|
+
...(comment?.trim() ? { comment: comment.trim() } : {}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
set(before);
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
112
168
|
cancel() {
|
|
113
169
|
const current = this.current;
|
|
114
170
|
if (!this.controller || !current)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/agent-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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,16 +17,16 @@
|
|
|
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.7.0",
|
|
21
|
+
"@classic-homes/agent-contracts": "0.7.0",
|
|
22
22
|
"dompurify": "3.4.17"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@classic-homes/theme-svelte": "^1.
|
|
26
|
-
"svelte": "^5.
|
|
25
|
+
"@classic-homes/theme-svelte": "^1.42.2",
|
|
26
|
+
"svelte": "^5.31.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@classic-homes/theme-svelte": "1.
|
|
29
|
+
"@classic-homes/theme-svelte": "1.42.2",
|
|
30
30
|
"@sveltejs/package": "3.0.2",
|
|
31
31
|
"@sveltejs/vite-plugin-svelte": "7.3.1",
|
|
32
32
|
"@testing-library/svelte": "5.4.2",
|