@classic-homes/agent-chat 0.1.3 → 0.2.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 +40 -0
- package/dist/components/AgentChat.svelte +65 -39
- package/dist/components/AgentChat.svelte.d.ts +2 -0
- package/dist/components/AgentChat.svelte.d.ts.map +1 -1
- package/dist/components/ArtifactPanel.svelte +79 -96
- package/dist/components/ArtifactPanel.svelte.d.ts +2 -0
- package/dist/components/ArtifactPanel.svelte.d.ts.map +1 -1
- package/dist/components/ChatHeader.svelte +122 -0
- package/dist/components/ChatHeader.svelte.d.ts +27 -0
- package/dist/components/ChatHeader.svelte.d.ts.map +1 -0
- package/dist/components/Composer.svelte +78 -121
- package/dist/components/Composer.svelte.d.ts.map +1 -1
- package/dist/components/ConversationDialogs.svelte +134 -0
- package/dist/components/ConversationDialogs.svelte.d.ts +25 -0
- package/dist/components/ConversationDialogs.svelte.d.ts.map +1 -0
- package/dist/components/ConversationSidebar.svelte +135 -114
- package/dist/components/ConversationSidebar.svelte.d.ts +8 -2
- package/dist/components/ConversationSidebar.svelte.d.ts.map +1 -1
- package/dist/components/Markdown.svelte +48 -0
- package/dist/components/Markdown.svelte.d.ts +16 -0
- package/dist/components/Markdown.svelte.d.ts.map +1 -0
- package/dist/components/MessageItem.svelte +27 -116
- package/dist/components/MessageItem.svelte.d.ts.map +1 -1
- package/dist/components/MessageList.svelte +23 -34
- package/dist/components/ModeBanner.svelte +7 -13
- package/dist/components/ModeBanner.svelte.d.ts.map +1 -1
- package/dist/components/SandboxFrame.svelte +43 -0
- package/dist/components/SandboxFrame.svelte.d.ts +13 -0
- package/dist/components/SandboxFrame.svelte.d.ts.map +1 -0
- package/dist/components/ToolTimeline.svelte +17 -68
- package/dist/components/ToolTimeline.svelte.d.ts.map +1 -1
- package/dist/components/UiFrame.svelte +9 -55
- package/dist/components/UiFrame.svelte.d.ts.map +1 -1
- package/dist/components/conversation-chrome.svelte.test.d.ts +2 -0
- package/dist/components/conversation-chrome.svelte.test.d.ts.map +1 -0
- package/dist/components/conversation-chrome.svelte.test.js +127 -0
- package/dist/components/markdown-prose.svelte.test.d.ts +2 -0
- package/dist/components/markdown-prose.svelte.test.d.ts.map +1 -0
- package/dist/components/markdown-prose.svelte.test.js +26 -0
- package/dist/composer-keys.d.ts +11 -0
- package/dist/composer-keys.d.ts.map +1 -0
- package/dist/composer-keys.js +10 -0
- package/dist/composer-keys.test.d.ts +2 -0
- package/dist/composer-keys.test.d.ts.map +1 -0
- package/dist/composer-keys.test.js +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/reducer.d.ts +10 -0
- package/dist/reducer.d.ts.map +1 -1
- package/dist/reducer.js +19 -1
- package/dist/reducer.test.js +29 -1
- package/dist/session.svelte.d.ts +6 -0
- package/dist/session.svelte.d.ts.map +1 -1
- package/dist/session.svelte.js +15 -1
- package/dist/ui-frame-protocol.d.ts +2 -2
- package/dist/ui-frame-protocol.js +2 -2
- package/dist/ui-frame-relay.d.ts +16 -0
- package/dist/ui-frame-relay.d.ts.map +1 -0
- package/dist/ui-frame-relay.js +22 -0
- package/dist/ui-frame-relay.test.d.ts +2 -0
- package/dist/ui-frame-relay.test.d.ts.map +1 -0
- package/dist/ui-frame-relay.test.js +36 -0
- package/package.json +8 -5
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @classic-homes/agent-chat
|
|
2
|
+
|
|
3
|
+
Svelte 5 chat UI for Classic Agent: session state, message list, composer, tool timeline,
|
|
4
|
+
artifacts and MCP-UI sandbox frames. The Classic App's `/agent` page and agent panel use it.
|
|
5
|
+
|
|
6
|
+
## Requirements
|
|
7
|
+
|
|
8
|
+
- `svelte` 5 and `@classic-homes/theme-svelte` 1.39 or later (peer dependencies).
|
|
9
|
+
- Tailwind with `@classic-homes/theme-tailwind-preset`. The components are styled with Tailwind
|
|
10
|
+
utility classes and theme tokens, so the consumer's Tailwind build has to scan this package:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
// tailwind.config.js
|
|
14
|
+
export default {
|
|
15
|
+
content: [
|
|
16
|
+
'./src/**/*.{html,js,svelte,ts}',
|
|
17
|
+
'./node_modules/@classic-homes/theme-svelte/**/*.{html,js,svelte,ts}',
|
|
18
|
+
'./node_modules/@classic-homes/agent-chat/**/*.{js,svelte}',
|
|
19
|
+
],
|
|
20
|
+
presets: [require('@classic-homes/theme-tailwind-preset')],
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Without that entry the chat renders unstyled. Markdown prose (rendered from model text) is the
|
|
25
|
+
one part styled with plain CSS, shipped inside the `Markdown` component.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```svelte
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import { AgentChat } from '@classic-homes/agent-chat';
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<AgentChat {client} sandboxOrigin={AGENT_SANDBOX_ORIGIN} {conversationId} mode={me.mode} />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`client` is an `AgentApiClient` from `@classic-homes/agent-client`. `sandboxOrigin` is the
|
|
38
|
+
agent-api origin that serves `/frame`; artifacts and MCP-UI resources only ever render there.
|
|
39
|
+
|
|
40
|
+
Publishing: `docs/operations/package-publishing.md`.
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
FileRef,
|
|
8
8
|
} from '@classic-homes/agent-contracts';
|
|
9
9
|
import type { BackendMode } from '@classic-homes/agent-contracts';
|
|
10
|
+
import { cn, ResizablePanes, Sheet } from '@classic-homes/theme-svelte';
|
|
11
|
+
import { MediaQuery } from 'svelte/reactivity';
|
|
10
12
|
import { ChatSession } from '../session.svelte.js';
|
|
11
13
|
import ArtifactPanel from './ArtifactPanel.svelte';
|
|
12
14
|
import Composer from './Composer.svelte';
|
|
@@ -31,6 +33,8 @@
|
|
|
31
33
|
/** A UI resource asked to run a tool; the host confirms with the user and may send a message. */
|
|
32
34
|
ontoolrequest?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
33
35
|
placeholder?: string;
|
|
36
|
+
/** Rendered above the transcript, e.g. a `ChatHeader`. */
|
|
37
|
+
header?: import('svelte').Snippet;
|
|
34
38
|
context?: import('svelte').Snippet;
|
|
35
39
|
empty?: import('svelte').Snippet;
|
|
36
40
|
class?: string;
|
|
@@ -50,9 +54,10 @@
|
|
|
50
54
|
onnotify,
|
|
51
55
|
ontoolrequest,
|
|
52
56
|
placeholder,
|
|
57
|
+
header,
|
|
53
58
|
context,
|
|
54
59
|
empty,
|
|
55
|
-
class: className
|
|
60
|
+
class: className,
|
|
56
61
|
}: Props = $props();
|
|
57
62
|
|
|
58
63
|
const session = new ChatSession({
|
|
@@ -96,15 +101,23 @@
|
|
|
96
101
|
}
|
|
97
102
|
});
|
|
98
103
|
|
|
104
|
+
// The page shows artifacts beside the chat on wide screens; narrow screens and the panel use a
|
|
105
|
+
// sheet so the transcript keeps its width.
|
|
106
|
+
const wide = new MediaQuery('(min-width: 1024px)', false);
|
|
107
|
+
const sidePane = $derived(variant === 'page' && wide.current);
|
|
108
|
+
const showArtifacts = $derived(artifactsOpen && artifacts.length > 0);
|
|
109
|
+
const selectedTitle = $derived(
|
|
110
|
+
artifacts.find((a) => a.artifactId === selectedArtifact)?.title ?? 'Artifact',
|
|
111
|
+
);
|
|
112
|
+
|
|
99
113
|
async function send(text: string): Promise<void> {
|
|
100
114
|
const attached = files;
|
|
101
115
|
files = [];
|
|
102
116
|
await session.send(text, { files: attached });
|
|
103
117
|
}
|
|
104
118
|
function retry(): void {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
void session.send(lastUser.text, { files: lastUser.attachments });
|
|
119
|
+
const last = session.messages[session.messages.length - 1];
|
|
120
|
+
if (last?.role === 'assistant') void session.retry(last.requestId);
|
|
108
121
|
}
|
|
109
122
|
function prompt(text: string): void {
|
|
110
123
|
draft = text;
|
|
@@ -112,10 +125,11 @@
|
|
|
112
125
|
}
|
|
113
126
|
</script>
|
|
114
127
|
|
|
115
|
-
|
|
116
|
-
<div class="
|
|
128
|
+
{#snippet chat()}
|
|
129
|
+
<div class="flex h-full min-w-0 flex-1 flex-col">
|
|
130
|
+
{#if header}{@render header()}{/if}
|
|
117
131
|
<ModeBanner mode={session.mode} />
|
|
118
|
-
{#if session.loadError}<div class="
|
|
132
|
+
{#if session.loadError}<div class="px-4 py-2 text-sm text-destructive" role="alert">
|
|
119
133
|
{session.loadError}
|
|
120
134
|
</div>{/if}
|
|
121
135
|
<MessageList
|
|
@@ -147,42 +161,54 @@
|
|
|
147
161
|
{context}
|
|
148
162
|
/>
|
|
149
163
|
</div>
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
{/snippet}
|
|
165
|
+
|
|
166
|
+
<div
|
|
167
|
+
class={cn('flex h-full min-h-0 bg-background text-foreground', className)}
|
|
168
|
+
data-variant={variant}
|
|
169
|
+
>
|
|
170
|
+
{#if sidePane}
|
|
171
|
+
<!-- The chat stays mounted while the pane opens and closes (iframes, scroll position): a
|
|
172
|
+
closed pane hides the end side and the handle and gives the chat the full width. -->
|
|
173
|
+
<ResizablePanes
|
|
174
|
+
size={60}
|
|
175
|
+
min={35}
|
|
176
|
+
max={75}
|
|
177
|
+
persistKey="agent-chat:artifacts"
|
|
178
|
+
handleLabel="Resize artifact pane"
|
|
179
|
+
class={cn(!showArtifacts && '[&>[role=separator]]:hidden')}
|
|
180
|
+
startClass={cn('flex', !showArtifacts && '!basis-full')}
|
|
181
|
+
endClass={cn(!showArtifacts && 'hidden')}
|
|
182
|
+
>
|
|
183
|
+
{#snippet start()}{@render chat()}{/snippet}
|
|
184
|
+
{#snippet end()}
|
|
185
|
+
{#if showArtifacts}
|
|
186
|
+
<ArtifactPanel
|
|
187
|
+
{artifacts}
|
|
188
|
+
{sandboxOrigin}
|
|
189
|
+
bind:selectedId={selectedArtifact}
|
|
190
|
+
onclose={() => (artifactsOpen = false)}
|
|
191
|
+
/>
|
|
192
|
+
{/if}
|
|
193
|
+
{/snippet}
|
|
194
|
+
</ResizablePanes>
|
|
195
|
+
{:else}
|
|
196
|
+
{@render chat()}
|
|
197
|
+
<Sheet
|
|
198
|
+
open={showArtifacts}
|
|
199
|
+
onOpenChange={(open) => (artifactsOpen = open)}
|
|
200
|
+
side="right"
|
|
201
|
+
size="lg"
|
|
202
|
+
title={selectedTitle}
|
|
203
|
+
hideTitle
|
|
204
|
+
>
|
|
152
205
|
<ArtifactPanel
|
|
153
206
|
{artifacts}
|
|
154
207
|
{sandboxOrigin}
|
|
155
208
|
bind:selectedId={selectedArtifact}
|
|
156
|
-
|
|
209
|
+
class="border-l-0"
|
|
210
|
+
headerClass="pr-12"
|
|
157
211
|
/>
|
|
158
|
-
</
|
|
212
|
+
</Sheet>
|
|
159
213
|
{/if}
|
|
160
214
|
</div>
|
|
161
|
-
|
|
162
|
-
<style>
|
|
163
|
-
.agent-chat {
|
|
164
|
-
display: flex;
|
|
165
|
-
height: 100%;
|
|
166
|
-
min-height: 0;
|
|
167
|
-
background: hsl(var(--background));
|
|
168
|
-
color: hsl(var(--foreground));
|
|
169
|
-
}
|
|
170
|
-
.agent-chat__main {
|
|
171
|
-
flex: 1;
|
|
172
|
-
min-width: 0;
|
|
173
|
-
display: flex;
|
|
174
|
-
flex-direction: column;
|
|
175
|
-
}
|
|
176
|
-
.agent-chat__side {
|
|
177
|
-
width: min(45%, 40rem);
|
|
178
|
-
min-width: 20rem;
|
|
179
|
-
}
|
|
180
|
-
.agent-chat--panel .agent-chat__side {
|
|
181
|
-
display: none;
|
|
182
|
-
}
|
|
183
|
-
.agent-chat__load-error {
|
|
184
|
-
padding: 0.5rem 1rem;
|
|
185
|
-
color: hsl(var(--destructive));
|
|
186
|
-
font-size: 0.875rem;
|
|
187
|
-
}
|
|
188
|
-
</style>
|
|
@@ -19,6 +19,8 @@ interface Props {
|
|
|
19
19
|
/** A UI resource asked to run a tool; the host confirms with the user and may send a message. */
|
|
20
20
|
ontoolrequest?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
21
21
|
placeholder?: string;
|
|
22
|
+
/** Rendered above the transcript, e.g. a `ChatHeader`. */
|
|
23
|
+
header?: import('svelte').Snippet;
|
|
22
24
|
context?: import('svelte').Snippet;
|
|
23
25
|
empty?: import('svelte').Snippet;
|
|
24
26
|
class?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentChat.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AgentChat.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"AgentChat.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AgentChat.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAUhE,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,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,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuIH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -1,41 +1,58 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { Button, Icon } from '@classic-homes/theme-svelte';
|
|
2
|
+
import { Button, cn, Icon } from '@classic-homes/theme-svelte';
|
|
3
3
|
import type { Artifact } from '../reducer.js';
|
|
4
4
|
import { parseMarkdown } from '../markdown.js';
|
|
5
|
-
import
|
|
5
|
+
import Markdown from './Markdown.svelte';
|
|
6
|
+
import SandboxFrame from './SandboxFrame.svelte';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
artifacts: Artifact[];
|
|
9
10
|
sandboxOrigin: string;
|
|
10
11
|
selectedId?: string | null;
|
|
11
12
|
onclose?: () => void;
|
|
13
|
+
class?: string;
|
|
14
|
+
headerClass?: string;
|
|
12
15
|
}
|
|
13
|
-
let {
|
|
16
|
+
let {
|
|
17
|
+
artifacts,
|
|
18
|
+
sandboxOrigin,
|
|
19
|
+
selectedId = $bindable(null),
|
|
20
|
+
onclose,
|
|
21
|
+
class: className,
|
|
22
|
+
headerClass,
|
|
23
|
+
}: Props = $props();
|
|
14
24
|
const selected = $derived(
|
|
15
25
|
artifacts.find((a) => a.artifactId === selectedId) ?? artifacts[artifacts.length - 1],
|
|
16
26
|
);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
? mermaidPage(selected.text)
|
|
24
|
-
: selected.text;
|
|
25
|
-
frame.contentWindow.postMessage(
|
|
26
|
-
{ type: UI_FRAME_RENDER, html, uri: `artifact://${selected.artifactId}` },
|
|
27
|
-
FRAME_TARGET_ORIGIN,
|
|
28
|
-
);
|
|
29
|
-
}
|
|
27
|
+
// Markdown artifacts stream like messages; re-parsing on every token is the dominant cost, so
|
|
28
|
+
// throttle while the artifact is still open (same budget as MessageItem).
|
|
29
|
+
const PARSE_THROTTLE_MS = 120;
|
|
30
|
+
let markdownHtml = $state('');
|
|
31
|
+
let lastParse = 0;
|
|
32
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
30
33
|
$effect(() => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
if (selected?.mediaType !== 'text/markdown') return;
|
|
35
|
+
const text = selected.text;
|
|
36
|
+
const streaming = selected.open;
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
if (!streaming || now - lastParse >= PARSE_THROTTLE_MS) {
|
|
39
|
+
if (timer) clearTimeout(timer);
|
|
40
|
+
timer = null;
|
|
41
|
+
lastParse = now;
|
|
42
|
+
markdownHtml = parseMarkdown(text);
|
|
43
|
+
} else if (!timer) {
|
|
44
|
+
timer = setTimeout(
|
|
45
|
+
() => {
|
|
46
|
+
timer = null;
|
|
47
|
+
lastParse = Date.now();
|
|
48
|
+
markdownHtml = parseMarkdown(selected?.text ?? '');
|
|
49
|
+
},
|
|
50
|
+
PARSE_THROTTLE_MS - (now - lastParse),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
$effect(() => () => {
|
|
55
|
+
if (timer) clearTimeout(timer);
|
|
39
56
|
});
|
|
40
57
|
|
|
41
58
|
function mermaidPage(source: string): string {
|
|
@@ -69,93 +86,59 @@
|
|
|
69
86
|
</script>
|
|
70
87
|
|
|
71
88
|
{#if selected}
|
|
72
|
-
<aside
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
<aside
|
|
90
|
+
class={cn('flex h-full flex-col border-l border-border bg-card', className)}
|
|
91
|
+
aria-label="Artifacts"
|
|
92
|
+
>
|
|
93
|
+
<header class={cn('flex items-center gap-2 border-b border-border px-3 py-2', headerClass)}>
|
|
94
|
+
<select
|
|
95
|
+
class="min-w-0 flex-1 truncate rounded-md bg-transparent py-1 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
96
|
+
bind:value={selectedId}
|
|
97
|
+
aria-label="Artifact"
|
|
98
|
+
>
|
|
75
99
|
{#each artifacts as a (a.artifactId)}
|
|
76
100
|
<option value={a.artifactId}>{a.title ?? a.artifactId}</option>
|
|
77
101
|
{/each}
|
|
78
102
|
</select>
|
|
79
|
-
<div class="
|
|
80
|
-
<Button variant="ghost" size="icon" aria-label="Copy" onclick={copy}
|
|
103
|
+
<div class="flex gap-0.5">
|
|
104
|
+
<Button variant="ghost" size="icon" class="size-8" aria-label="Copy" onclick={copy}
|
|
81
105
|
><Icon name="copy" size={16} /></Button
|
|
82
106
|
>
|
|
83
|
-
<Button variant="ghost" size="icon" aria-label="Download" onclick={download}
|
|
107
|
+
<Button variant="ghost" size="icon" class="size-8" aria-label="Download" onclick={download}
|
|
84
108
|
><Icon name="download" size={16} /></Button
|
|
85
109
|
>
|
|
86
|
-
{#if onclose}<Button
|
|
87
|
-
|
|
110
|
+
{#if onclose}<Button
|
|
111
|
+
variant="ghost"
|
|
112
|
+
size="icon"
|
|
113
|
+
class="size-8"
|
|
114
|
+
aria-label="Close"
|
|
115
|
+
onclick={onclose}><Icon name="close" size={16} /></Button
|
|
88
116
|
>{/if}
|
|
89
117
|
</div>
|
|
90
118
|
</header>
|
|
91
|
-
<div class="
|
|
119
|
+
<div class="min-h-0 flex-1 overflow-auto p-3">
|
|
92
120
|
{#if selected.mediaType === 'text/markdown'}
|
|
93
|
-
<
|
|
121
|
+
<Markdown html={markdownHtml} />
|
|
94
122
|
{:else if selected.mediaType === 'application/vnd.ant.code'}
|
|
95
|
-
<pre class="
|
|
123
|
+
<pre class="overflow-auto whitespace-pre font-mono text-[13px] leading-relaxed"><code
|
|
124
|
+
>{selected.text}</code
|
|
125
|
+
></pre>
|
|
96
126
|
{:else}
|
|
97
|
-
<!-- html, mermaid, svg, react: only ever inside the sandbox origin
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
<!-- html, mermaid, svg, react: only ever inside the sandbox origin. The sandbox page is
|
|
128
|
+
transparent and model HTML assumes a white page, so it gets a white surface. -->
|
|
129
|
+
<div class="h-full min-h-96 overflow-hidden rounded-md border border-border bg-white">
|
|
130
|
+
<SandboxFrame
|
|
131
|
+
{sandboxOrigin}
|
|
132
|
+
html={selected.mediaType === 'application/vnd.ant.mermaid'
|
|
133
|
+
? mermaidPage(selected.text)
|
|
134
|
+
: selected.text}
|
|
135
|
+
uri={`artifact://${selected.artifactId}`}
|
|
136
|
+
title={selected.title ?? 'Artifact'}
|
|
137
|
+
style="height:100%"
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
106
140
|
{/if}
|
|
107
|
-
{#if selected.open}<div class="
|
|
141
|
+
{#if selected.open}<div class="mt-2 text-xs text-muted-foreground">Writing…</div>{/if}
|
|
108
142
|
</div>
|
|
109
143
|
</aside>
|
|
110
144
|
{/if}
|
|
111
|
-
|
|
112
|
-
<style>
|
|
113
|
-
.agent-artifacts {
|
|
114
|
-
display: flex;
|
|
115
|
-
flex-direction: column;
|
|
116
|
-
height: 100%;
|
|
117
|
-
border-left: 1px solid hsl(var(--border));
|
|
118
|
-
background: hsl(var(--card));
|
|
119
|
-
}
|
|
120
|
-
.agent-artifacts__header {
|
|
121
|
-
display: flex;
|
|
122
|
-
align-items: center;
|
|
123
|
-
gap: 0.5rem;
|
|
124
|
-
padding: 0.5rem 0.75rem;
|
|
125
|
-
border-bottom: 1px solid hsl(var(--border));
|
|
126
|
-
}
|
|
127
|
-
.agent-artifacts__select {
|
|
128
|
-
flex: 1;
|
|
129
|
-
min-width: 0;
|
|
130
|
-
font: inherit;
|
|
131
|
-
background: transparent;
|
|
132
|
-
color: inherit;
|
|
133
|
-
border: 0;
|
|
134
|
-
}
|
|
135
|
-
.agent-artifacts__actions {
|
|
136
|
-
display: flex;
|
|
137
|
-
gap: 0.125rem;
|
|
138
|
-
}
|
|
139
|
-
.agent-artifacts__body {
|
|
140
|
-
flex: 1;
|
|
141
|
-
min-height: 0;
|
|
142
|
-
overflow: auto;
|
|
143
|
-
padding: 0.75rem;
|
|
144
|
-
}
|
|
145
|
-
iframe {
|
|
146
|
-
width: 100%;
|
|
147
|
-
height: 100%;
|
|
148
|
-
min-height: 24rem;
|
|
149
|
-
border: 0;
|
|
150
|
-
background: white;
|
|
151
|
-
}
|
|
152
|
-
.agent-code {
|
|
153
|
-
white-space: pre;
|
|
154
|
-
overflow: auto;
|
|
155
|
-
font-size: 0.8125rem;
|
|
156
|
-
}
|
|
157
|
-
.agent-artifacts__streaming {
|
|
158
|
-
font-size: 0.75rem;
|
|
159
|
-
color: hsl(var(--muted-foreground));
|
|
160
|
-
}
|
|
161
|
-
</style>
|
|
@@ -4,6 +4,8 @@ interface Props {
|
|
|
4
4
|
sandboxOrigin: string;
|
|
5
5
|
selectedId?: string | null;
|
|
6
6
|
onclose?: () => void;
|
|
7
|
+
class?: string;
|
|
8
|
+
headerClass?: string;
|
|
7
9
|
}
|
|
8
10
|
declare const ArtifactPanel: import("svelte").Component<Props, {}, "selectedId">;
|
|
9
11
|
type ArtifactPanel = ReturnType<typeof ArtifactPanel>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArtifactPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ArtifactPanel.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"ArtifactPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ArtifactPanel.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAM5C,UAAU,KAAK;IACb,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAkHH,QAAA,MAAM,aAAa,qDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Header for a chat: conversation title, agent badge and the conversation menu (rename,
|
|
4
|
+
open full page, delete). Pass it to `AgentChat` through the `header` snippet. Actions only
|
|
5
|
+
show when their handler is set; rename and delete also need a `conversationId`.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import {
|
|
9
|
+
Badge,
|
|
10
|
+
Button,
|
|
11
|
+
cn,
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
Icon,
|
|
14
|
+
type DropdownMenuItem,
|
|
15
|
+
} from '@classic-homes/theme-svelte';
|
|
16
|
+
import type { Snippet } from 'svelte';
|
|
17
|
+
import ConversationDialogs, { type ConversationRef } from './ConversationDialogs.svelte';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
title?: string;
|
|
21
|
+
conversationId?: string | null;
|
|
22
|
+
/** Agent name shown as a badge, e.g. "Classic Agent". */
|
|
23
|
+
agentLabel?: string;
|
|
24
|
+
/** Opens the conversation list; the button only shows below `lg`. */
|
|
25
|
+
onopenrail?: () => void;
|
|
26
|
+
onnew?: () => void;
|
|
27
|
+
/** Opens this conversation as a full page (panel variant). */
|
|
28
|
+
onexpand?: () => void;
|
|
29
|
+
onrename?: (conversationId: string, title: string) => void | Promise<void>;
|
|
30
|
+
ondelete?: (conversationId: string) => void | Promise<void>;
|
|
31
|
+
onerror?: (message: string) => void;
|
|
32
|
+
/** Extra controls before the menu. */
|
|
33
|
+
actions?: Snippet;
|
|
34
|
+
class?: string;
|
|
35
|
+
}
|
|
36
|
+
let {
|
|
37
|
+
title,
|
|
38
|
+
conversationId = null,
|
|
39
|
+
agentLabel,
|
|
40
|
+
onopenrail,
|
|
41
|
+
onnew,
|
|
42
|
+
onexpand,
|
|
43
|
+
onrename,
|
|
44
|
+
ondelete,
|
|
45
|
+
onerror,
|
|
46
|
+
actions,
|
|
47
|
+
class: className,
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
let renaming = $state<ConversationRef | null>(null);
|
|
51
|
+
let deleting = $state<ConversationRef | null>(null);
|
|
52
|
+
let headerEl = $state<HTMLElement | null>(null);
|
|
53
|
+
let returnFocus = $state<HTMLElement | null>(null);
|
|
54
|
+
/** Remember the menu trigger before a dialog takes focus from the closing menu. */
|
|
55
|
+
function open(set: () => void): void {
|
|
56
|
+
returnFocus = headerEl?.querySelector<HTMLElement>('[aria-haspopup="menu"]') ?? null;
|
|
57
|
+
set();
|
|
58
|
+
}
|
|
59
|
+
const displayTitle = $derived(title?.trim() || 'New chat');
|
|
60
|
+
const current = $derived(conversationId ? { conversationId, title: displayTitle } : null);
|
|
61
|
+
|
|
62
|
+
const items = $derived.by<DropdownMenuItem[]>(() => {
|
|
63
|
+
const list: DropdownMenuItem[] = [];
|
|
64
|
+
if (current && onrename)
|
|
65
|
+
list.push({
|
|
66
|
+
label: 'Rename',
|
|
67
|
+
icon: 'edit',
|
|
68
|
+
onSelect: () => open(() => (renaming = current)),
|
|
69
|
+
});
|
|
70
|
+
if (onexpand) list.push({ label: 'Open full page', icon: 'expand', onSelect: onexpand });
|
|
71
|
+
if (current && ondelete) {
|
|
72
|
+
if (list.length) list.push({ type: 'separator' });
|
|
73
|
+
list.push({
|
|
74
|
+
label: 'Delete',
|
|
75
|
+
icon: 'trash',
|
|
76
|
+
destructive: true,
|
|
77
|
+
onSelect: () => open(() => (deleting = current)),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return list;
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<header
|
|
85
|
+
bind:this={headerEl}
|
|
86
|
+
class={cn('flex min-h-12 items-center gap-2 border-b border-border px-3 py-2', className)}
|
|
87
|
+
>
|
|
88
|
+
{#if onopenrail}
|
|
89
|
+
<Button
|
|
90
|
+
variant="ghost"
|
|
91
|
+
size="icon"
|
|
92
|
+
class="size-8 lg:hidden"
|
|
93
|
+
aria-label="Show conversations"
|
|
94
|
+
onclick={onopenrail}><Icon name="sidebar" size={18} /></Button
|
|
95
|
+
>
|
|
96
|
+
{/if}
|
|
97
|
+
<div class="flex min-w-0 flex-1 items-center gap-2">
|
|
98
|
+
<h2 class="truncate text-sm font-semibold" title={displayTitle}>{displayTitle}</h2>
|
|
99
|
+
{#if agentLabel}<Badge variant="outline" size="sm" class="shrink-0">{agentLabel}</Badge>{/if}
|
|
100
|
+
</div>
|
|
101
|
+
{#if actions}{@render actions()}{/if}
|
|
102
|
+
{#if onnew}
|
|
103
|
+
<Button variant="ghost" size="icon" class="size-8" aria-label="New chat" onclick={onnew}
|
|
104
|
+
><Icon name="plus" size={18} /></Button
|
|
105
|
+
>
|
|
106
|
+
{/if}
|
|
107
|
+
{#if items.length}
|
|
108
|
+
<DropdownMenu
|
|
109
|
+
{items}
|
|
110
|
+
align="end"
|
|
111
|
+
triggerVariant="ghost"
|
|
112
|
+
triggerSize="icon"
|
|
113
|
+
triggerClass="size-8"
|
|
114
|
+
triggerLabel="Conversation options"
|
|
115
|
+
showChevron={false}
|
|
116
|
+
>
|
|
117
|
+
{#snippet trigger()}<Icon name="more-horizontal" size={18} />{/snippet}
|
|
118
|
+
</DropdownMenu>
|
|
119
|
+
{/if}
|
|
120
|
+
</header>
|
|
121
|
+
|
|
122
|
+
<ConversationDialogs bind:renaming bind:deleting {onrename} {ondelete} {onerror} {returnFocus} />
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
title?: string;
|
|
4
|
+
conversationId?: string | null;
|
|
5
|
+
/** Agent name shown as a badge, e.g. "Classic Agent". */
|
|
6
|
+
agentLabel?: string;
|
|
7
|
+
/** Opens the conversation list; the button only shows below `lg`. */
|
|
8
|
+
onopenrail?: () => void;
|
|
9
|
+
onnew?: () => void;
|
|
10
|
+
/** Opens this conversation as a full page (panel variant). */
|
|
11
|
+
onexpand?: () => void;
|
|
12
|
+
onrename?: (conversationId: string, title: string) => void | Promise<void>;
|
|
13
|
+
ondelete?: (conversationId: string) => void | Promise<void>;
|
|
14
|
+
onerror?: (message: string) => void;
|
|
15
|
+
/** Extra controls before the menu. */
|
|
16
|
+
actions?: Snippet;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Header for a chat: conversation title, agent badge and the conversation menu (rename,
|
|
21
|
+
* open full page, delete). Pass it to `AgentChat` through the `header` snippet. Actions only
|
|
22
|
+
* show when their handler is set; rename and delete also need a `conversationId`.
|
|
23
|
+
*/
|
|
24
|
+
declare const ChatHeader: import("svelte").Component<Props, {}, "">;
|
|
25
|
+
type ChatHeader = ReturnType<typeof ChatHeader>;
|
|
26
|
+
export default ChatHeader;
|
|
27
|
+
//# sourceMappingURL=ChatHeader.svelte.d.ts.map
|
|
@@ -0,0 +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;AAIpC,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,sCAAsC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8EH;;;;GAIG;AACH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|