@classic-homes/agent-chat 0.10.0 → 0.12.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 +57 -2
- package/dist/components/AgentChat.svelte +61 -22
- package/dist/components/AgentChat.svelte.d.ts +11 -1
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/ArtifactPanel.svelte +4 -2
- package/dist/components/AssistantMessage.svelte +14 -4
- package/dist/components/AssistantMessage.svelte.d.ts +2 -0
- package/dist/components/AssistantMessage.svelte.d.ts.map +1 -1
- package/dist/components/ChatHeader.svelte +4 -1
- package/dist/components/ChatHeader.svelte.d.ts +2 -0
- package/dist/components/ChatHeader.svelte.d.ts.map +1 -1
- package/dist/components/ChatLauncher.svelte +157 -0
- package/dist/components/ChatLauncher.svelte.d.ts +23 -0
- package/dist/components/ChatLauncher.svelte.d.ts.map +1 -0
- package/dist/components/Composer.svelte +7 -3
- package/dist/components/Composer.svelte.d.ts.map +1 -1
- package/dist/components/FloatingWindow.svelte +239 -0
- package/dist/components/FloatingWindow.svelte.d.ts +26 -0
- package/dist/components/FloatingWindow.svelte.d.ts.map +1 -0
- package/dist/components/Markdown.svelte +4 -0
- package/dist/components/Markdown.svelte.d.ts.map +1 -1
- package/dist/components/MessageFeedback.svelte +11 -4
- package/dist/components/MessageFeedback.svelte.d.ts.map +1 -1
- package/dist/components/MessageFiles.svelte +133 -40
- package/dist/components/MessageFiles.svelte.d.ts +6 -0
- package/dist/components/MessageFiles.svelte.d.ts.map +1 -1
- package/dist/components/MessageItem.svelte +2 -0
- package/dist/components/MessageItem.svelte.d.ts +2 -0
- package/dist/components/MessageItem.svelte.d.ts.map +1 -1
- package/dist/components/MessageList.svelte +40 -7
- package/dist/components/MessageList.svelte.d.ts +3 -1
- package/dist/components/MessageList.svelte.d.ts.map +1 -1
- package/dist/components/ShortcutsDialog.svelte +10 -3
- package/dist/components/ShortcutsDialog.svelte.d.ts +2 -0
- package/dist/components/ShortcutsDialog.svelte.d.ts.map +1 -1
- package/dist/components/UiFrame.svelte +12 -4
- package/dist/components/UiFrame.svelte.d.ts.map +1 -1
- package/dist/components/UserMessage.svelte +10 -1
- package/dist/components/UserMessage.svelte.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +94 -61
- package/dist/reducer.d.ts +11 -0
- package/dist/reducer.d.ts.map +1 -1
- package/dist/reducer.js +18 -2
- package/dist/session.svelte.d.ts +46 -0
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +208 -13
- package/dist/shortcuts.d.ts +7 -0
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +9 -0
- package/dist/surface-geometry.d.ts +56 -0
- package/dist/surface-geometry.d.ts.map +1 -0
- package/dist/surface-geometry.js +99 -0
- package/dist/surface.svelte.d.ts +69 -0
- package/dist/surface.svelte.d.ts.map +1 -0
- package/dist/surface.svelte.js +156 -0
- package/dist/ui-frame-protocol.d.ts +6 -0
- package/dist/ui-frame-protocol.d.ts.map +1 -1
- package/dist/ui-frame-protocol.js +11 -0
- package/package.json +8 -7
- package/dist/artifact-pages.test.d.ts +0 -2
- package/dist/artifact-pages.test.d.ts.map +0 -1
- package/dist/artifact-pages.test.js +0 -102
- package/dist/components/a11y.svelte.test.d.ts +0 -2
- package/dist/components/a11y.svelte.test.d.ts.map +0 -1
- package/dist/components/a11y.svelte.test.js +0 -183
- package/dist/components/agent-picker.svelte.test.d.ts +0 -2
- package/dist/components/agent-picker.svelte.test.d.ts.map +0 -1
- package/dist/components/agent-picker.svelte.test.js +0 -84
- package/dist/components/composer.svelte.test.d.ts +0 -2
- package/dist/components/composer.svelte.test.d.ts.map +0 -1
- package/dist/components/composer.svelte.test.js +0 -162
- package/dist/components/conversation-chrome.svelte.test.d.ts +0 -2
- package/dist/components/conversation-chrome.svelte.test.d.ts.map +0 -1
- package/dist/components/conversation-chrome.svelte.test.js +0 -147
- package/dist/components/feedback.svelte.test.d.ts +0 -2
- package/dist/components/feedback.svelte.test.d.ts.map +0 -1
- package/dist/components/feedback.svelte.test.js +0 -99
- package/dist/components/live-fixes.svelte.test.d.ts +0 -2
- package/dist/components/live-fixes.svelte.test.d.ts.map +0 -1
- package/dist/components/live-fixes.svelte.test.js +0 -127
- package/dist/components/markdown-prose.svelte.test.d.ts +0 -2
- package/dist/components/markdown-prose.svelte.test.d.ts.map +0 -1
- package/dist/components/markdown-prose.svelte.test.js +0 -26
- package/dist/components/messages.svelte.test.d.ts +0 -2
- package/dist/components/messages.svelte.test.d.ts.map +0 -1
- package/dist/components/messages.svelte.test.js +0 -202
- package/dist/components/regenerate.svelte.test.d.ts +0 -2
- package/dist/components/regenerate.svelte.test.d.ts.map +0 -1
- package/dist/components/regenerate.svelte.test.js +0 -108
- package/dist/components/welcome.svelte.test.d.ts +0 -2
- package/dist/components/welcome.svelte.test.d.ts.map +0 -1
- package/dist/components/welcome.svelte.test.js +0 -119
- package/dist/composer-keys.test.d.ts +0 -2
- package/dist/composer-keys.test.d.ts.map +0 -1
- package/dist/composer-keys.test.js +0 -16
- package/dist/markdown.test.d.ts +0 -2
- package/dist/markdown.test.d.ts.map +0 -1
- package/dist/markdown.test.js +0 -35
- package/dist/reducer.test.d.ts +0 -2
- package/dist/reducer.test.d.ts.map +0 -1
- package/dist/reducer.test.js +0 -153
- package/dist/ui-frame-protocol.test.d.ts +0 -2
- package/dist/ui-frame-protocol.test.d.ts.map +0 -1
- package/dist/ui-frame-protocol.test.js +0 -48
- package/dist/ui-frame-relay.test.d.ts +0 -2
- package/dist/ui-frame-relay.test.d.ts.map +0 -1
- package/dist/ui-frame-relay.test.js +0 -36
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ artifacts and MCP-UI sandbox frames. The Classic App's `/agent` page and agent p
|
|
|
5
5
|
|
|
6
6
|
## Requirements
|
|
7
7
|
|
|
8
|
-
- `svelte` 5 and `@classic-homes/theme-svelte` 1.
|
|
8
|
+
- `svelte` `^5.31.0` and `@classic-homes/theme-svelte` `^1.42.2` (peer dependencies).
|
|
9
9
|
- Tailwind with `@classic-homes/theme-tailwind-preset`. The components are styled with Tailwind
|
|
10
10
|
utility classes and theme tokens, so the consumer's Tailwind build has to scan this package:
|
|
11
11
|
|
|
@@ -38,13 +38,68 @@ artifacts and MCP-UI sandbox frames. The Classic App's `/agent` page and agent p
|
|
|
38
38
|
`client` is an `AgentApiClient` from `@classic-homes/agent-client`. `sandboxOrigin` is the
|
|
39
39
|
agent-api origin that serves `/frame`; artifacts and MCP-UI resources only ever render there.
|
|
40
40
|
|
|
41
|
+
### `conversationId`
|
|
42
|
+
|
|
43
|
+
- `null` (or absent): a new chat. When the first message starts a conversation,
|
|
44
|
+
`onconversationstarted(id)` reports its id; the host may pass that id back as
|
|
45
|
+
`conversationId` (the chat stays mounted and does not reload) or keep passing `null` (the
|
|
46
|
+
chat carries on in that conversation).
|
|
47
|
+
- Another id: the chat loads that transcript, re-attaching to a turn still running there. A
|
|
48
|
+
stream of the conversation shown before is dropped (not cancelled on the server; it finishes
|
|
49
|
+
there) and does not fire `onturnend`. A load that fails shows Try again; it is not retried
|
|
50
|
+
automatically for the same id.
|
|
51
|
+
- From an id back to `null` (the host's "New chat"): the transcript and the conversation are
|
|
52
|
+
cleared, so the next message starts a new conversation.
|
|
53
|
+
|
|
54
|
+
`ChatSession` (used by `AgentChat`, exported for other layouts) has the matching methods:
|
|
55
|
+
`load(id)`, `reset()` (forget the conversation and drop a running stream without a server
|
|
56
|
+
cancel), `send`, `cancel` (asks the runtime to stop and keeps reading until it confirms, at
|
|
57
|
+
most `cancelGraceMs`, default 3 s, so the text up to the stop is kept), `retry`, `regenerate`,
|
|
58
|
+
`edit` and `continue(requestId)`.
|
|
59
|
+
|
|
60
|
+
### Continue
|
|
61
|
+
|
|
62
|
+
An answer cut short by the output limit (`finishReason: 'length'`) or stopped by the user
|
|
63
|
+
(`'cancelled'`) shows Continue: `ChatSession.continue` asks the runtime to carry on and streams
|
|
64
|
+
the rest into the same message. A connection that drops mid-answer is re-attached
|
|
65
|
+
automatically (twice); if that fails the answer shows a retryable error (Try again), not
|
|
66
|
+
Continue. When a continuation itself fails, the text so far stays and Continue is offered again.
|
|
67
|
+
Continue needs a runtime that supports `ChatRequest.continues`; an older one ignores it.
|
|
68
|
+
|
|
69
|
+
### Files
|
|
70
|
+
|
|
71
|
+
The file endpoint needs the bearer, which `<img src>` and download links cannot send. `AgentChat`
|
|
72
|
+
loads produced files with `client.fetchFile` (blob URLs, revoked when the message goes away);
|
|
73
|
+
`MessageFiles` takes a `loadFile(fileId): Promise<Blob>` prop and links to `fileUrl` without one
|
|
74
|
+
(only for cookie-authenticated hosts).
|
|
75
|
+
|
|
76
|
+
### Detached chat (launcher, floating window, dock)
|
|
77
|
+
|
|
78
|
+
A host can show the chat over its pages instead of on a route of its own:
|
|
79
|
+
|
|
80
|
+
- `createChatSurface({ storageKey })` holds which presentation is showing (`launcher`, `float`
|
|
81
|
+
or `dock`), the launcher's corner, the window's geometry and an `unread` flag; it persists to
|
|
82
|
+
`localStorage` under `storageKey` and keeps the window inside the viewport.
|
|
83
|
+
- `ChatLauncher` is the floating button (drag to another corner; shows answering, unseen answer,
|
|
84
|
+
error and a MOCK marker). `FloatingWindow` is a non-modal window around any content: drag by
|
|
85
|
+
the header, resize from the edges, drop on the right edge to dock, arrow keys on the move
|
|
86
|
+
handle, Escape to minimize. The dock itself is the host's (e.g. a theme `DataPanel`).
|
|
87
|
+
- Pass `AgentChat` a host-owned `session` (`new ChatSession({...})`) and `variant="float"` or
|
|
88
|
+
`"panel"`. The session then survives the view moving between presentations, including a
|
|
89
|
+
running answer. With `session` set, configure `mode`, `agent`, `clientKind`, `getContext` and
|
|
90
|
+
the turn callbacks on the session; those `AgentChat` props are ignored. Call
|
|
91
|
+
`surface.notifyTurnEnd()` from the session's `onTurnEnd` to flag an answer that finished while
|
|
92
|
+
collapsed.
|
|
93
|
+
|
|
41
94
|
## Components
|
|
42
95
|
|
|
43
96
|
`AgentChat` composes the rest; each is also exported for other layouts:
|
|
44
97
|
|
|
45
98
|
- `ChatHeader`, `ConversationSidebar`: title and conversation actions; the history rail.
|
|
46
99
|
`ChatHeader`'s menu opens `ShortcutsDialog` (the chat's `CHAT_SHORTCUTS` plus the host's
|
|
47
|
-
`shortcuts`, e.g. a panel toggle). Hosts implement their own app-level shortcuts.
|
|
100
|
+
`shortcuts`, e.g. a panel toggle). Hosts implement their own app-level shortcuts. Pass
|
|
101
|
+
`savedPrompts={false}` to `ChatHeader` when the chat has no `prompts`, so the `/` entry is
|
|
102
|
+
left out.
|
|
48
103
|
- `MessageList`: transcript with day separators and a jump-to-latest button.
|
|
49
104
|
- `MessageItem` → `UserMessage` / `AssistantMessage`, with `MessageActions` (copy, time,
|
|
50
105
|
`MessageFeedback` thumbs up/down with an optional note, Regenerate and Edit on the latest turn),
|
|
@@ -32,6 +32,14 @@
|
|
|
32
32
|
|
|
33
33
|
interface Props {
|
|
34
34
|
client: AgentApiClient;
|
|
35
|
+
/**
|
|
36
|
+
* A session the host owns, so it survives this component being unmounted and mounted
|
|
37
|
+
* elsewhere (launcher, floating window, dock) without dropping a running answer. The host
|
|
38
|
+
* then configures it itself: `mode`, `agent`, `clientKind`, `getContext`,
|
|
39
|
+
* `onconversationstarted`, `ontoolresult` and `onturnend` here are ignored. Without it the
|
|
40
|
+
* component creates its own, as before.
|
|
41
|
+
*/
|
|
42
|
+
session?: ChatSession;
|
|
35
43
|
/** Sandbox origin for artifacts and MCP-UI (agent-api `/frame`). */
|
|
36
44
|
sandboxOrigin: string;
|
|
37
45
|
conversationId?: string | null;
|
|
@@ -47,7 +55,8 @@
|
|
|
47
55
|
/** The agent of this conversation changed (picked, or learned from a loaded conversation). */
|
|
48
56
|
onagentchange?: (agent: AgentKey) => void;
|
|
49
57
|
clientKind?: ClientKind;
|
|
50
|
-
|
|
58
|
+
/** `float` is the detached window: laid out like the panel. */
|
|
59
|
+
variant?: 'page' | 'panel' | 'float';
|
|
51
60
|
getContext?: () => DocumentContext | null | undefined;
|
|
52
61
|
onconversationstarted?: (conversationId: string) => void;
|
|
53
62
|
ontoolresult?: (name: string, status: string) => void;
|
|
@@ -83,6 +92,7 @@
|
|
|
83
92
|
}
|
|
84
93
|
let {
|
|
85
94
|
client,
|
|
95
|
+
session: hostSession,
|
|
86
96
|
sandboxOrigin,
|
|
87
97
|
conversationId = null,
|
|
88
98
|
mode,
|
|
@@ -111,29 +121,54 @@
|
|
|
111
121
|
class: className,
|
|
112
122
|
}: Props = $props();
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
// One session for the component's lifetime: the host's, or our own.
|
|
125
|
+
// svelte-ignore state_referenced_locally
|
|
126
|
+
const session =
|
|
127
|
+
hostSession ??
|
|
128
|
+
new ChatSession({
|
|
129
|
+
client,
|
|
130
|
+
conversationId,
|
|
131
|
+
mode,
|
|
132
|
+
agent,
|
|
133
|
+
clientKind,
|
|
134
|
+
getContext,
|
|
135
|
+
onConversationStarted: (id) => onconversationstarted?.(id),
|
|
136
|
+
onToolResult: (name, status) => ontoolresult?.(name, status),
|
|
137
|
+
onTurnEnd: (info) => onturnend?.(info),
|
|
138
|
+
});
|
|
139
|
+
// Open the pane when the runtime starts an artifact, not by watching state later: a fast
|
|
140
|
+
// stream can open and close it before any effect sees `open`. Only beside the chat: a sheet
|
|
141
|
+
// (narrow screens, the panel, the floating window) would cover the answer mid-stream, so the
|
|
142
|
+
// chip opens it there.
|
|
143
|
+
$effect(() =>
|
|
144
|
+
session.subscribe((event) => {
|
|
145
|
+
if (event.type === 'artifact' && event.op === 'open' && sidePane) {
|
|
128
146
|
selectedArtifact = event.artifactId;
|
|
129
147
|
artifactsOpen = true;
|
|
130
148
|
}
|
|
131
|
-
},
|
|
132
|
-
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
const compact = $derived(variant !== 'page');
|
|
133
152
|
let attemptedId: string | null = null;
|
|
153
|
+
/** The prop as last seen, to tell the host's "New chat" (id → null) from a chat started here. */
|
|
154
|
+
let previousId: string | null = null;
|
|
134
155
|
// The constructor already knows `conversationId`; what decides a fetch is whether the
|
|
135
|
-
// transcript for it has been loaded (or started) in this session.
|
|
156
|
+
// transcript for it has been loaded (or started) in this session. `load` stops a stream of
|
|
157
|
+
// the conversation shown before.
|
|
136
158
|
$effect(() => {
|
|
159
|
+
const before = previousId;
|
|
160
|
+
previousId = conversationId;
|
|
161
|
+
if (!conversationId) {
|
|
162
|
+
// The host left a conversation for a new chat: forget it, or the next message would go
|
|
163
|
+
// to it. A chat started here while the prop stayed null keeps going (null → null).
|
|
164
|
+
if (before && session.conversationId) {
|
|
165
|
+
session.reset();
|
|
166
|
+
attemptedId = null;
|
|
167
|
+
selectedArtifact = null;
|
|
168
|
+
artifactsOpen = false;
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
137
172
|
if (
|
|
138
173
|
conversationId &&
|
|
139
174
|
conversationId !== session.loadedConversationId &&
|
|
@@ -146,7 +181,7 @@
|
|
|
146
181
|
}
|
|
147
182
|
});
|
|
148
183
|
$effect(() => {
|
|
149
|
-
if (mode && !session.mode) session.mode = mode;
|
|
184
|
+
if (mode && !hostSession && !session.mode) session.mode = mode;
|
|
150
185
|
});
|
|
151
186
|
|
|
152
187
|
let composer: Composer | undefined = $state();
|
|
@@ -174,7 +209,7 @@
|
|
|
174
209
|
}
|
|
175
210
|
function retry(): void {
|
|
176
211
|
const last = session.messages[session.messages.length - 1];
|
|
177
|
-
if (last?.role === 'assistant')
|
|
212
|
+
if (last?.role === 'assistant') thenFocus(session.retry(last.requestId));
|
|
178
213
|
}
|
|
179
214
|
const agentName = $derived(agents?.find((a) => a.key === session.agent)?.name);
|
|
180
215
|
const pickAgent = $derived(
|
|
@@ -259,7 +294,7 @@
|
|
|
259
294
|
title={welcomeTitle}
|
|
260
295
|
description={welcomeDescription}
|
|
261
296
|
{suggestions}
|
|
262
|
-
compact
|
|
297
|
+
{compact}
|
|
263
298
|
disabled={session.status !== 'idle'}
|
|
264
299
|
onselect={(text) => thenFocus(send(text))}
|
|
265
300
|
/>
|
|
@@ -292,6 +327,7 @@
|
|
|
292
327
|
streaming={session.streaming}
|
|
293
328
|
{sandboxOrigin}
|
|
294
329
|
fileUrl={(id) => client.fileUrl(id)}
|
|
330
|
+
loadFile={client.fetchFile ? (id) => client.fetchFile!(id) : undefined}
|
|
295
331
|
{toolLabels}
|
|
296
332
|
{onnavigate}
|
|
297
333
|
onopenartifact={(id) => {
|
|
@@ -310,7 +346,10 @@
|
|
|
310
346
|
.rate(requestId, rating, comment)
|
|
311
347
|
.catch(() => onnotify?.('Could not save your feedback. Try again.'))
|
|
312
348
|
: undefined}
|
|
313
|
-
oncontinue={() =>
|
|
349
|
+
oncontinue={() => {
|
|
350
|
+
const answer = session.current;
|
|
351
|
+
if (answer) thenFocus(session.continue(answer.requestId));
|
|
352
|
+
}}
|
|
314
353
|
empty={emptyState}
|
|
315
354
|
/>
|
|
316
355
|
<Composer
|
|
@@ -2,9 +2,18 @@ import type { AgentApiClient } from '@classic-homes/agent-client';
|
|
|
2
2
|
import type { AgentKey, AgentSummary, 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 { ChatSession } from '../session.svelte.js';
|
|
5
6
|
import type { WelcomeSuggestion } from '../welcome.js';
|
|
6
7
|
interface Props {
|
|
7
8
|
client: AgentApiClient;
|
|
9
|
+
/**
|
|
10
|
+
* A session the host owns, so it survives this component being unmounted and mounted
|
|
11
|
+
* elsewhere (launcher, floating window, dock) without dropping a running answer. The host
|
|
12
|
+
* then configures it itself: `mode`, `agent`, `clientKind`, `getContext`,
|
|
13
|
+
* `onconversationstarted`, `ontoolresult` and `onturnend` here are ignored. Without it the
|
|
14
|
+
* component creates its own, as before.
|
|
15
|
+
*/
|
|
16
|
+
session?: ChatSession;
|
|
8
17
|
/** Sandbox origin for artifacts and MCP-UI (agent-api `/frame`). */
|
|
9
18
|
sandboxOrigin: string;
|
|
10
19
|
conversationId?: string | null;
|
|
@@ -20,7 +29,8 @@ interface Props {
|
|
|
20
29
|
/** The agent of this conversation changed (picked, or learned from a loaded conversation). */
|
|
21
30
|
onagentchange?: (agent: AgentKey) => void;
|
|
22
31
|
clientKind?: ClientKind;
|
|
23
|
-
|
|
32
|
+
/** `float` is the detached window: laid out like the panel. */
|
|
33
|
+
variant?: 'page' | 'panel' | 'float';
|
|
24
34
|
getContext?: () => DocumentContext | null | undefined;
|
|
25
35
|
onconversationstarted?: (conversationId: string) => void;
|
|
26
36
|
ontoolresult?: (name: string, status: string) => void;
|
|
@@ -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,YAAY,EACZ,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAclE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"AgentChat.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AgentChat.svelte.ts"],"names":[],"mappings":"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"}
|
|
@@ -115,8 +115,10 @@
|
|
|
115
115
|
<div class="min-h-0 flex-1 overflow-auto p-3">
|
|
116
116
|
{#if selected.mediaType === 'text/markdown'}
|
|
117
117
|
<Markdown html={markdownHtml} />
|
|
118
|
-
{:else if selected.mediaType === 'application/vnd.ant.code' ||
|
|
119
|
-
<!--
|
|
118
|
+
{:else if selected.mediaType === 'application/vnd.ant.code' || selected.open}
|
|
119
|
+
<!-- Framed artifacts (html, svg, react, mermaid) render once complete: the source shows
|
|
120
|
+
while it streams, so the sandbox is not reloaded on every delta (and a partial
|
|
121
|
+
diagram is a parse error). -->
|
|
120
122
|
<pre class="overflow-auto whitespace-pre font-mono text-[13px] leading-relaxed"><code
|
|
121
123
|
>{selected.text}</code
|
|
122
124
|
></pre>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { Alert, Button, Icon } from '@classic-homes/theme-svelte';
|
|
8
8
|
import type { TurnRating } from '@classic-homes/agent-contracts';
|
|
9
9
|
import type { AssistantMessage } from '../reducer.js';
|
|
10
|
-
import { errorTitle } from '../reducer.js';
|
|
10
|
+
import { errorTitle, isContinuable } from '../reducer.js';
|
|
11
11
|
import { parseMarkdown } from '../markdown.js';
|
|
12
12
|
import ArtifactChip from './ArtifactChip.svelte';
|
|
13
13
|
import Markdown from './Markdown.svelte';
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
last?: boolean;
|
|
25
25
|
sandboxOrigin: string;
|
|
26
26
|
fileUrl: (fileId: string) => string;
|
|
27
|
+
/** Fetch a file with the host's credentials (blob URLs); without it files link to `fileUrl`. */
|
|
28
|
+
loadFile?: (fileId: string) => Promise<Blob>;
|
|
27
29
|
toolLabels?: Record<string, string>;
|
|
28
30
|
onnavigate?: (href: string) => void;
|
|
29
31
|
onopenartifact?: (artifactId: string) => void;
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
last = false,
|
|
45
47
|
sandboxOrigin,
|
|
46
48
|
fileUrl,
|
|
49
|
+
loadFile,
|
|
47
50
|
toolLabels,
|
|
48
51
|
onnavigate,
|
|
49
52
|
onopenartifact,
|
|
@@ -98,7 +101,9 @@
|
|
|
98
101
|
!message.tools.some((t) => !t.status) &&
|
|
99
102
|
!message.artifacts.some((a) => a.open),
|
|
100
103
|
);
|
|
101
|
-
|
|
104
|
+
// Continue carries on the text so far of an answer cut short or stopped; a reply stopped before
|
|
105
|
+
// any text, or one that failed or lost its connection, is retried instead.
|
|
106
|
+
const canContinue = $derived(last && !!oncontinue && isContinuable(message));
|
|
102
107
|
</script>
|
|
103
108
|
|
|
104
109
|
<div class="group/message flex gap-3 px-4 py-2" data-role="assistant" data-status={message.status}>
|
|
@@ -142,7 +147,7 @@
|
|
|
142
147
|
{#each message.ui as resource (resource.callId + resource.uri)}
|
|
143
148
|
<UiFrame {resource} {sandboxOrigin} {onprompt} {ontool} {onnotify} />
|
|
144
149
|
{/each}
|
|
145
|
-
{#if message.files.length}<MessageFiles files={message.files} {fileUrl} />{/if}
|
|
150
|
+
{#if message.files.length}<MessageFiles files={message.files} {fileUrl} {loadFile} />{/if}
|
|
146
151
|
{#if message.proposals.length}
|
|
147
152
|
<div class="flex flex-col gap-1.5">
|
|
148
153
|
{#each message.proposals as operation (operation.operationId)}
|
|
@@ -157,7 +162,10 @@
|
|
|
157
162
|
<strong class="font-semibold">{errorTitle(message.error.code)}.</strong>
|
|
158
163
|
{message.error.message}
|
|
159
164
|
</p>
|
|
160
|
-
{#if
|
|
165
|
+
{#if canContinue}
|
|
166
|
+
<!-- A continuation failed: the text so far is kept and can still be continued. -->
|
|
167
|
+
<Button variant="outline" size="sm" onclick={oncontinue}>Continue</Button>
|
|
168
|
+
{:else if message.error.retryable && onretry}
|
|
161
169
|
<Button variant="outline" size="sm" onclick={onretry}>
|
|
162
170
|
<Icon name="refresh" size={14} /> Try again
|
|
163
171
|
</Button>
|
|
@@ -169,6 +177,8 @@
|
|
|
169
177
|
<Icon name="stop" size={14} /> Stopped
|
|
170
178
|
{#if canContinue}
|
|
171
179
|
<Button variant="link" size="sm" class="h-auto p-0" onclick={oncontinue}>Continue</Button>
|
|
180
|
+
{:else if last && !message.text && onretry}
|
|
181
|
+
<Button variant="link" size="sm" class="h-auto p-0" onclick={onretry}>Try again</Button>
|
|
172
182
|
{/if}
|
|
173
183
|
</div>
|
|
174
184
|
{:else if message.status === 'incomplete'}
|
|
@@ -7,6 +7,8 @@ interface Props {
|
|
|
7
7
|
last?: boolean;
|
|
8
8
|
sandboxOrigin: string;
|
|
9
9
|
fileUrl: (fileId: string) => string;
|
|
10
|
+
/** Fetch a file with the host's credentials (blob URLs); without it files link to `fileUrl`. */
|
|
11
|
+
loadFile?: (fileId: string) => Promise<Blob>;
|
|
10
12
|
toolLabels?: Record<string, string>;
|
|
11
13
|
onnavigate?: (href: string) => void;
|
|
12
14
|
onopenartifact?: (artifactId: string) => void;
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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,gGAAgG;IAChG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,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;AAmKH;;;GAGG;AACH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
onerror?: (message: string) => void;
|
|
35
35
|
/** Host shortcuts for the "Keyboard shortcuts" dialog, e.g. opening the panel. */
|
|
36
36
|
shortcuts?: ChatShortcut[];
|
|
37
|
+
/** Whether the chat offers saved prompts (`AgentChat`'s `prompts`); false hides the `/` shortcut. */
|
|
38
|
+
savedPrompts?: boolean;
|
|
37
39
|
/** Extra controls before the menu. */
|
|
38
40
|
actions?: Snippet;
|
|
39
41
|
class?: string;
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
ondelete,
|
|
50
52
|
onerror,
|
|
51
53
|
shortcuts,
|
|
54
|
+
savedPrompts = true,
|
|
52
55
|
actions,
|
|
53
56
|
class: className,
|
|
54
57
|
}: Props = $props();
|
|
@@ -131,4 +134,4 @@
|
|
|
131
134
|
</header>
|
|
132
135
|
|
|
133
136
|
<ConversationDialogs bind:renaming bind:deleting {onrename} {ondelete} {onerror} {returnFocus} />
|
|
134
|
-
<ShortcutsDialog bind:open={showingShortcuts} {shortcuts} {returnFocus} />
|
|
137
|
+
<ShortcutsDialog bind:open={showingShortcuts} {shortcuts} {returnFocus} {savedPrompts} />
|
|
@@ -15,6 +15,8 @@ interface Props {
|
|
|
15
15
|
onerror?: (message: string) => void;
|
|
16
16
|
/** Host shortcuts for the "Keyboard shortcuts" dialog, e.g. opening the panel. */
|
|
17
17
|
shortcuts?: ChatShortcut[];
|
|
18
|
+
/** Whether the chat offers saved prompts (`AgentChat`'s `prompts`); false hides the `/` shortcut. */
|
|
19
|
+
savedPrompts?: boolean;
|
|
18
20
|
/** Extra controls before the menu. */
|
|
19
21
|
actions?: Snippet;
|
|
20
22
|
class?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatHeader.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ChatHeader.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKlD,UAAU,KAAK;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,kFAAkF;IAClF,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,sCAAsC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"ChatHeader.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ChatHeader.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKlD,UAAU,KAAK;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,kFAAkF;IAClF,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,qGAAqG;IACrG,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sCAAsC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAwFH;;;;;GAKG;AACH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The floating launcher: a round button pinned to a viewport corner that opens the chat. The
|
|
4
|
+
* user can drag it; on release it snaps to the nearest corner. It also reports what the chat
|
|
5
|
+
* is doing while collapsed (answering, an unseen answer, an error) and keeps the mock backend
|
|
6
|
+
* visible (AGENTS.md: mock must always be visible).
|
|
7
|
+
*/
|
|
8
|
+
import { cn, Icon } from '@classic-homes/theme-svelte';
|
|
9
|
+
import type { LauncherStatus } from '../surface.svelte.js';
|
|
10
|
+
import { DRAG_THRESHOLD, nearestCorner, type Corner, type Point } from '../surface-geometry.js';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
corner?: Corner;
|
|
14
|
+
status?: LauncherStatus;
|
|
15
|
+
/** The backend is the mock: show the marker on the button. */
|
|
16
|
+
mock?: boolean;
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Id of the surface this button opens, for `aria-controls`. */
|
|
19
|
+
controls?: string;
|
|
20
|
+
expanded?: boolean;
|
|
21
|
+
draggable?: boolean;
|
|
22
|
+
onclick?: () => void;
|
|
23
|
+
/** The user dropped the button in another corner. */
|
|
24
|
+
oncornerchange?: (corner: Corner) => void;
|
|
25
|
+
class?: string;
|
|
26
|
+
}
|
|
27
|
+
let {
|
|
28
|
+
corner = 'bottom-right',
|
|
29
|
+
status = 'idle',
|
|
30
|
+
mock = false,
|
|
31
|
+
label = 'Classic Agent',
|
|
32
|
+
controls,
|
|
33
|
+
expanded = false,
|
|
34
|
+
draggable = true,
|
|
35
|
+
onclick,
|
|
36
|
+
oncornerchange,
|
|
37
|
+
class: className,
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
let button: HTMLButtonElement | undefined = $state();
|
|
41
|
+
export function focus(): void {
|
|
42
|
+
button?.focus();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const STATUS_TEXT: Record<LauncherStatus, string> = {
|
|
46
|
+
idle: '',
|
|
47
|
+
streaming: 'answering',
|
|
48
|
+
unread: 'new answer',
|
|
49
|
+
error: 'something went wrong',
|
|
50
|
+
};
|
|
51
|
+
const name = $derived(
|
|
52
|
+
[label, STATUS_TEXT[status], mock ? 'mock backend' : ''].filter(Boolean).join(', '),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const CORNER_CLASS: Record<Corner, string> = {
|
|
56
|
+
'bottom-right': 'bottom-5 right-5',
|
|
57
|
+
'bottom-left': 'bottom-5 left-5',
|
|
58
|
+
'top-right': 'top-5 right-5',
|
|
59
|
+
'top-left': 'top-5 left-5',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let start: Point | null = null;
|
|
63
|
+
let offset = $state<Point>({ x: 0, y: 0 });
|
|
64
|
+
let dragging = $state(false);
|
|
65
|
+
// A drag ends in a click event on the same button; that click must not open the chat.
|
|
66
|
+
let suppressClick = false;
|
|
67
|
+
|
|
68
|
+
function onpointerdown(event: PointerEvent): void {
|
|
69
|
+
if (!draggable || event.button !== 0) return;
|
|
70
|
+
start = { x: event.clientX, y: event.clientY };
|
|
71
|
+
button?.setPointerCapture?.(event.pointerId);
|
|
72
|
+
}
|
|
73
|
+
function onpointermove(event: PointerEvent): void {
|
|
74
|
+
if (!start) return;
|
|
75
|
+
const dx = event.clientX - start.x;
|
|
76
|
+
const dy = event.clientY - start.y;
|
|
77
|
+
if (!dragging && Math.abs(dx) <= DRAG_THRESHOLD && Math.abs(dy) <= DRAG_THRESHOLD) return;
|
|
78
|
+
dragging = true;
|
|
79
|
+
offset = { x: dx, y: dy };
|
|
80
|
+
}
|
|
81
|
+
function onpointerup(event: PointerEvent): void {
|
|
82
|
+
if (!start) return;
|
|
83
|
+
start = null;
|
|
84
|
+
button?.releasePointerCapture?.(event.pointerId);
|
|
85
|
+
if (!dragging) return;
|
|
86
|
+
dragging = false;
|
|
87
|
+
suppressClick = true;
|
|
88
|
+
offset = { x: 0, y: 0 };
|
|
89
|
+
const next = nearestCorner(
|
|
90
|
+
{ x: event.clientX, y: event.clientY },
|
|
91
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
92
|
+
);
|
|
93
|
+
if (next !== corner) oncornerchange?.(next);
|
|
94
|
+
}
|
|
95
|
+
function onpointercancel(): void {
|
|
96
|
+
start = null;
|
|
97
|
+
dragging = false;
|
|
98
|
+
offset = { x: 0, y: 0 };
|
|
99
|
+
}
|
|
100
|
+
function handleClick(): void {
|
|
101
|
+
if (suppressClick) {
|
|
102
|
+
suppressClick = false;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
onclick?.();
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<button
|
|
110
|
+
bind:this={button}
|
|
111
|
+
type="button"
|
|
112
|
+
class={cn(
|
|
113
|
+
'fixed z-40 flex size-14 touch-none select-none items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg outline-none ring-offset-background transition-shadow hover:shadow-xl focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
114
|
+
CORNER_CLASS[corner],
|
|
115
|
+
dragging ? 'cursor-grabbing opacity-80' : 'cursor-pointer',
|
|
116
|
+
className,
|
|
117
|
+
)}
|
|
118
|
+
style:transform={dragging ? `translate(${offset.x}px, ${offset.y}px)` : undefined}
|
|
119
|
+
aria-label={name}
|
|
120
|
+
aria-expanded={expanded}
|
|
121
|
+
aria-controls={controls}
|
|
122
|
+
data-agent-launcher
|
|
123
|
+
data-corner={corner}
|
|
124
|
+
data-status={status}
|
|
125
|
+
onclick={handleClick}
|
|
126
|
+
{onpointerdown}
|
|
127
|
+
{onpointermove}
|
|
128
|
+
{onpointerup}
|
|
129
|
+
{onpointercancel}
|
|
130
|
+
>
|
|
131
|
+
{#if status === 'streaming'}
|
|
132
|
+
<span
|
|
133
|
+
class="pointer-events-none absolute inset-0 animate-ping rounded-full bg-primary opacity-30 motion-reduce:hidden"
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
></span>
|
|
136
|
+
{/if}
|
|
137
|
+
<Icon name="sparkles" size={24} />
|
|
138
|
+
{#if status === 'unread' || status === 'error'}
|
|
139
|
+
<span
|
|
140
|
+
class={cn(
|
|
141
|
+
'pointer-events-none absolute right-0.5 top-0.5 size-3.5 rounded-full border-2 border-background',
|
|
142
|
+
status === 'error' ? 'bg-destructive' : 'bg-success',
|
|
143
|
+
)}
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
data-testid="launcher-dot"
|
|
146
|
+
></span>
|
|
147
|
+
{/if}
|
|
148
|
+
{#if mock}
|
|
149
|
+
<span
|
|
150
|
+
class="pointer-events-none absolute -bottom-1 left-1/2 -translate-x-1/2 rounded bg-warning px-1 text-[10px] font-bold leading-4 tracking-wide text-warning-foreground"
|
|
151
|
+
aria-hidden="true"
|
|
152
|
+
data-testid="launcher-mock"
|
|
153
|
+
>
|
|
154
|
+
MOCK
|
|
155
|
+
</span>
|
|
156
|
+
{/if}
|
|
157
|
+
</button>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LauncherStatus } from '../surface.svelte.js';
|
|
2
|
+
import { type Corner } from '../surface-geometry.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
corner?: Corner;
|
|
5
|
+
status?: LauncherStatus;
|
|
6
|
+
/** The backend is the mock: show the marker on the button. */
|
|
7
|
+
mock?: boolean;
|
|
8
|
+
label?: string;
|
|
9
|
+
/** Id of the surface this button opens, for `aria-controls`. */
|
|
10
|
+
controls?: string;
|
|
11
|
+
expanded?: boolean;
|
|
12
|
+
draggable?: boolean;
|
|
13
|
+
onclick?: () => void;
|
|
14
|
+
/** The user dropped the button in another corner. */
|
|
15
|
+
oncornerchange?: (corner: Corner) => void;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const ChatLauncher: import("svelte").Component<Props, {
|
|
19
|
+
focus: () => void;
|
|
20
|
+
}, "">;
|
|
21
|
+
type ChatLauncher = ReturnType<typeof ChatLauncher>;
|
|
22
|
+
export default ChatLauncher;
|
|
23
|
+
//# sourceMappingURL=ChatLauncher.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatLauncher.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ChatLauncher.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAiC,KAAK,MAAM,EAAc,MAAM,wBAAwB,CAAC;AAG9F,UAAU,KAAK;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qDAAqD;IACrD,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkHH,QAAA,MAAM,YAAY;iBA7FG,IAAI;MA6FiC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -150,6 +150,8 @@
|
|
|
150
150
|
function onpaste(event: ClipboardEvent): void {
|
|
151
151
|
const pasted = [...(event.clipboardData?.files ?? [])];
|
|
152
152
|
if (!upload || pasted.length === 0) return;
|
|
153
|
+
// Excel and Word put both text and a picture of the cells on the clipboard: paste the text.
|
|
154
|
+
if (event.clipboardData?.getData?.('text/plain')) return;
|
|
153
155
|
// Pasting a screenshot or a copied file attaches it; plain text pastes as usual.
|
|
154
156
|
event.preventDefault();
|
|
155
157
|
void addFiles(pasted);
|
|
@@ -281,9 +283,11 @@
|
|
|
281
283
|
class={cn(
|
|
282
284
|
'text-xs tabular-nums text-muted-foreground',
|
|
283
285
|
chars >= LIMITS.messageChars && 'text-destructive',
|
|
284
|
-
)}
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
)}>{chars.toLocaleString()} / {LIMITS.messageChars.toLocaleString()}</span
|
|
287
|
+
>
|
|
288
|
+
<!-- Announced once when the limit is crossed, not on every keystroke. -->
|
|
289
|
+
<span class="sr-only" aria-live="polite"
|
|
290
|
+
>{chars > LIMITS.messageChars ? 'Message is over the length limit' : ''}</span
|
|
287
291
|
>
|
|
288
292
|
{/if}
|
|
289
293
|
{#if streaming}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Composer.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Composer.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAIvF,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,oEAAoE;IACpE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;
|
|
1
|
+
{"version":3,"file":"Composer.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Composer.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAIvF,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,oEAAoE;IACpE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;AAiQH;;;;GAIG;AACH,QAAA,MAAM,QAAQ;iBApOO,IAAI;qBAoO6B,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|