@classic-homes/agent-chat 0.0.0-stage → 0.1.1
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/dist/components/AgentChat.svelte +188 -0
- package/dist/components/AgentChat.svelte.d.ts +29 -0
- package/dist/components/AgentChat.svelte.d.ts.map +1 -0
- package/dist/components/ArtifactPanel.svelte +161 -0
- package/dist/components/ArtifactPanel.svelte.d.ts +11 -0
- package/dist/components/ArtifactPanel.svelte.d.ts.map +1 -0
- package/dist/components/Composer.svelte +211 -0
- package/dist/components/Composer.svelte.d.ts +21 -0
- package/dist/components/Composer.svelte.d.ts.map +1 -0
- package/dist/components/ConversationSidebar.svelte +188 -0
- package/dist/components/ConversationSidebar.svelte.d.ts +18 -0
- package/dist/components/ConversationSidebar.svelte.d.ts.map +1 -0
- package/dist/components/MessageItem.svelte +253 -0
- package/dist/components/MessageItem.svelte.d.ts +17 -0
- package/dist/components/MessageItem.svelte.d.ts.map +1 -0
- package/dist/components/MessageList.svelte +104 -0
- package/dist/components/MessageList.svelte.d.ts +18 -0
- package/dist/components/MessageList.svelte.d.ts.map +1 -0
- package/dist/components/ModeBanner.svelte +23 -0
- package/dist/components/ModeBanner.svelte.d.ts +8 -0
- package/dist/components/ModeBanner.svelte.d.ts.map +1 -0
- package/dist/components/ToolTimeline.svelte +113 -0
- package/dist/components/ToolTimeline.svelte.d.ts +10 -0
- package/dist/components/ToolTimeline.svelte.d.ts.map +1 -0
- package/dist/components/UiFrame.svelte +95 -0
- package/dist/components/UiFrame.svelte.d.ts +13 -0
- package/dist/components/UiFrame.svelte.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/markdown.d.ts +13 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +325 -0
- package/dist/markdown.test.d.ts +2 -0
- package/dist/markdown.test.d.ts.map +1 -0
- package/dist/markdown.test.js +35 -0
- package/dist/reducer.d.ts +96 -0
- package/dist/reducer.d.ts.map +1 -0
- package/dist/reducer.js +186 -0
- package/dist/reducer.test.d.ts +2 -0
- package/dist/reducer.test.d.ts.map +1 -0
- package/dist/reducer.test.js +125 -0
- package/dist/session.svelte.d.ts +55 -0
- package/dist/session.svelte.d.ts.map +1 -0
- package/dist/session.svelte.js +157 -0
- package/dist/ui-frame-protocol.d.ts +42 -0
- package/dist/ui-frame-protocol.d.ts.map +1 -0
- package/dist/ui-frame-protocol.js +73 -0
- package/dist/ui-frame-protocol.test.d.ts +2 -0
- package/dist/ui-frame-protocol.test.d.ts.map +1 -0
- package/dist/ui-frame-protocol.test.js +34 -0
- package/package.json +47 -3
- package/README.md +0 -3
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AgentApiClient } from '@classic-homes/agent-client';
|
|
3
|
+
import type {
|
|
4
|
+
AgentKey,
|
|
5
|
+
ClientKind,
|
|
6
|
+
DocumentContext,
|
|
7
|
+
FileRef,
|
|
8
|
+
} from '@classic-homes/agent-contracts';
|
|
9
|
+
import type { BackendMode } from '@classic-homes/agent-contracts';
|
|
10
|
+
import { ChatSession } from '../session.svelte.js';
|
|
11
|
+
import ArtifactPanel from './ArtifactPanel.svelte';
|
|
12
|
+
import Composer from './Composer.svelte';
|
|
13
|
+
import MessageList from './MessageList.svelte';
|
|
14
|
+
import ModeBanner from './ModeBanner.svelte';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
client: AgentApiClient;
|
|
18
|
+
/** Sandbox origin for artifacts and MCP-UI (agent-api `/frame`). */
|
|
19
|
+
sandboxOrigin: string;
|
|
20
|
+
conversationId?: string | null;
|
|
21
|
+
/** Backend mode from `/api/me`; shows the mock banner before any turn runs. */
|
|
22
|
+
mode?: BackendMode;
|
|
23
|
+
agent?: AgentKey;
|
|
24
|
+
clientKind?: ClientKind;
|
|
25
|
+
variant?: 'page' | 'panel';
|
|
26
|
+
getContext?: () => DocumentContext | null | undefined;
|
|
27
|
+
onconversationstarted?: (conversationId: string) => void;
|
|
28
|
+
ontoolresult?: (name: string, status: string) => void;
|
|
29
|
+
onnavigate?: (href: string) => void;
|
|
30
|
+
onnotify?: (message: string) => void;
|
|
31
|
+
/** A UI resource asked to run a tool; the host confirms with the user and may send a message. */
|
|
32
|
+
ontoolrequest?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
33
|
+
placeholder?: string;
|
|
34
|
+
context?: import('svelte').Snippet;
|
|
35
|
+
empty?: import('svelte').Snippet;
|
|
36
|
+
class?: string;
|
|
37
|
+
}
|
|
38
|
+
let {
|
|
39
|
+
client,
|
|
40
|
+
sandboxOrigin,
|
|
41
|
+
conversationId = null,
|
|
42
|
+
mode,
|
|
43
|
+
agent = 'classic-agent',
|
|
44
|
+
clientKind,
|
|
45
|
+
variant = 'page',
|
|
46
|
+
getContext,
|
|
47
|
+
onconversationstarted,
|
|
48
|
+
ontoolresult,
|
|
49
|
+
onnavigate,
|
|
50
|
+
onnotify,
|
|
51
|
+
ontoolrequest,
|
|
52
|
+
placeholder,
|
|
53
|
+
context,
|
|
54
|
+
empty,
|
|
55
|
+
class: className = '',
|
|
56
|
+
}: Props = $props();
|
|
57
|
+
|
|
58
|
+
const session = new ChatSession({
|
|
59
|
+
client,
|
|
60
|
+
conversationId,
|
|
61
|
+
mode,
|
|
62
|
+
agent,
|
|
63
|
+
clientKind,
|
|
64
|
+
getContext,
|
|
65
|
+
onConversationStarted: (id) => onconversationstarted?.(id),
|
|
66
|
+
onToolResult: (name, status) => ontoolresult?.(name, status),
|
|
67
|
+
});
|
|
68
|
+
// The constructor already knows `conversationId`; what decides a fetch is whether the
|
|
69
|
+
// transcript for it has been loaded (or started) in this session.
|
|
70
|
+
$effect(() => {
|
|
71
|
+
if (
|
|
72
|
+
conversationId &&
|
|
73
|
+
conversationId !== session.loadedConversationId &&
|
|
74
|
+
session.status !== 'loading'
|
|
75
|
+
)
|
|
76
|
+
void session.load(conversationId);
|
|
77
|
+
});
|
|
78
|
+
$effect(() => {
|
|
79
|
+
if (mode && !session.mode) session.mode = mode;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let composer: Composer | undefined = $state();
|
|
83
|
+
let draft = $state('');
|
|
84
|
+
let files = $state<FileRef[]>([]);
|
|
85
|
+
let selectedArtifact = $state<string | null>(null);
|
|
86
|
+
let artifactsOpen = $state(false);
|
|
87
|
+
const artifacts = $derived(
|
|
88
|
+
session.messages.flatMap((m) => (m.role === 'assistant' ? m.artifacts : [])),
|
|
89
|
+
);
|
|
90
|
+
$effect(() => {
|
|
91
|
+
// A newly opened artifact pops the panel.
|
|
92
|
+
const last = artifacts[artifacts.length - 1];
|
|
93
|
+
if (last && last.open && selectedArtifact !== last.artifactId) {
|
|
94
|
+
selectedArtifact = last.artifactId;
|
|
95
|
+
artifactsOpen = true;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
async function send(text: string): Promise<void> {
|
|
100
|
+
const attached = files;
|
|
101
|
+
files = [];
|
|
102
|
+
await session.send(text, { files: attached });
|
|
103
|
+
}
|
|
104
|
+
function retry(): void {
|
|
105
|
+
const lastUser = [...session.messages].reverse().find((m) => m.role === 'user');
|
|
106
|
+
if (lastUser && lastUser.role === 'user')
|
|
107
|
+
void session.send(lastUser.text, { files: lastUser.attachments });
|
|
108
|
+
}
|
|
109
|
+
function prompt(text: string): void {
|
|
110
|
+
draft = text;
|
|
111
|
+
composer?.focus();
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<div class={`agent-chat agent-chat--${variant} ${className}`} data-variant={variant}>
|
|
116
|
+
<div class="agent-chat__main">
|
|
117
|
+
<ModeBanner mode={session.mode} />
|
|
118
|
+
{#if session.loadError}<div class="agent-chat__load-error" role="alert">
|
|
119
|
+
{session.loadError}
|
|
120
|
+
</div>{/if}
|
|
121
|
+
<MessageList
|
|
122
|
+
messages={session.messages}
|
|
123
|
+
streaming={session.streaming}
|
|
124
|
+
{sandboxOrigin}
|
|
125
|
+
fileUrl={(id) => client.fileUrl(id)}
|
|
126
|
+
{onnavigate}
|
|
127
|
+
onopenartifact={(id) => {
|
|
128
|
+
selectedArtifact = id;
|
|
129
|
+
artifactsOpen = true;
|
|
130
|
+
}}
|
|
131
|
+
onprompt={prompt}
|
|
132
|
+
ontool={(name, params) => ontoolrequest?.(name, params)}
|
|
133
|
+
{onnotify}
|
|
134
|
+
onretry={retry}
|
|
135
|
+
{empty}
|
|
136
|
+
/>
|
|
137
|
+
<Composer
|
|
138
|
+
bind:this={composer}
|
|
139
|
+
bind:value={draft}
|
|
140
|
+
bind:files
|
|
141
|
+
disabled={session.status === 'loading'}
|
|
142
|
+
streaming={session.streaming}
|
|
143
|
+
{placeholder}
|
|
144
|
+
upload={(file) => client.uploadFile(file, file.name)}
|
|
145
|
+
onsend={send}
|
|
146
|
+
oncancel={() => session.cancel()}
|
|
147
|
+
{context}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
{#if artifactsOpen && artifacts.length}
|
|
151
|
+
<div class="agent-chat__side">
|
|
152
|
+
<ArtifactPanel
|
|
153
|
+
{artifacts}
|
|
154
|
+
{sandboxOrigin}
|
|
155
|
+
bind:selectedId={selectedArtifact}
|
|
156
|
+
onclose={() => (artifactsOpen = false)}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
{/if}
|
|
160
|
+
</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>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentApiClient } from '@classic-homes/agent-client';
|
|
2
|
+
import type { AgentKey, ClientKind, DocumentContext } from '@classic-homes/agent-contracts';
|
|
3
|
+
import type { BackendMode } from '@classic-homes/agent-contracts';
|
|
4
|
+
interface Props {
|
|
5
|
+
client: AgentApiClient;
|
|
6
|
+
/** Sandbox origin for artifacts and MCP-UI (agent-api `/frame`). */
|
|
7
|
+
sandboxOrigin: string;
|
|
8
|
+
conversationId?: string | null;
|
|
9
|
+
/** Backend mode from `/api/me`; shows the mock banner before any turn runs. */
|
|
10
|
+
mode?: BackendMode;
|
|
11
|
+
agent?: AgentKey;
|
|
12
|
+
clientKind?: ClientKind;
|
|
13
|
+
variant?: 'page' | 'panel';
|
|
14
|
+
getContext?: () => DocumentContext | null | undefined;
|
|
15
|
+
onconversationstarted?: (conversationId: string) => void;
|
|
16
|
+
ontoolresult?: (name: string, status: string) => void;
|
|
17
|
+
onnavigate?: (href: string) => void;
|
|
18
|
+
onnotify?: (message: string) => void;
|
|
19
|
+
/** A UI resource asked to run a tool; the host confirms with the user and may send a message. */
|
|
20
|
+
ontoolrequest?: (toolName: string, params?: Record<string, unknown>) => void;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
context?: import('svelte').Snippet;
|
|
23
|
+
empty?: import('svelte').Snippet;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
declare const AgentChat: import("svelte").Component<Props, {}, "">;
|
|
27
|
+
type AgentChat = ReturnType<typeof AgentChat>;
|
|
28
|
+
export default AgentChat;
|
|
29
|
+
//# sourceMappingURL=AgentChat.svelte.d.ts.map
|
|
@@ -0,0 +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;AAQhE,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,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+GH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Icon } from '@classic-homes/theme-svelte';
|
|
3
|
+
import type { Artifact } from '../reducer.js';
|
|
4
|
+
import { parseMarkdown } from '../markdown.js';
|
|
5
|
+
import { UI_FRAME_RENDER } from '../ui-frame-protocol.js';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
artifacts: Artifact[];
|
|
9
|
+
sandboxOrigin: string;
|
|
10
|
+
selectedId?: string | null;
|
|
11
|
+
onclose?: () => void;
|
|
12
|
+
}
|
|
13
|
+
let { artifacts, sandboxOrigin, selectedId = $bindable(null), onclose }: Props = $props();
|
|
14
|
+
const selected = $derived(
|
|
15
|
+
artifacts.find((a) => a.artifactId === selectedId) ?? artifacts[artifacts.length - 1],
|
|
16
|
+
);
|
|
17
|
+
let frame: HTMLIFrameElement | null = $state(null);
|
|
18
|
+
|
|
19
|
+
function sendHtml(): void {
|
|
20
|
+
if (!selected || !frame?.contentWindow) return;
|
|
21
|
+
const html =
|
|
22
|
+
selected.mediaType === 'application/vnd.ant.mermaid'
|
|
23
|
+
? mermaidPage(selected.text)
|
|
24
|
+
: selected.text;
|
|
25
|
+
frame.contentWindow.postMessage(
|
|
26
|
+
{ type: UI_FRAME_RENDER, html, uri: `artifact://${selected.artifactId}` },
|
|
27
|
+
sandboxOrigin,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
$effect(() => {
|
|
31
|
+
void selected?.text;
|
|
32
|
+
if (
|
|
33
|
+
selected &&
|
|
34
|
+
(selected.mediaType === 'text/html' ||
|
|
35
|
+
selected.mediaType === 'application/vnd.ant.mermaid' ||
|
|
36
|
+
selected.mediaType === 'image/svg+xml')
|
|
37
|
+
)
|
|
38
|
+
sendHtml();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function mermaidPage(source: string): string {
|
|
42
|
+
const escaped = source.replace(/</g, '<');
|
|
43
|
+
return `<!doctype html><html><body><pre class="mermaid">${escaped}</pre><script>import m from "https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.esm.min.mjs";m.initialize({startOnLoad:true});<\/script></body></html>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function copy(): Promise<void> {
|
|
47
|
+
if (selected) await navigator.clipboard.writeText(selected.text);
|
|
48
|
+
}
|
|
49
|
+
function download(): void {
|
|
50
|
+
if (!selected) return;
|
|
51
|
+
const ext =
|
|
52
|
+
selected.mediaType === 'text/html'
|
|
53
|
+
? 'html'
|
|
54
|
+
: selected.mediaType === 'image/svg+xml'
|
|
55
|
+
? 'svg'
|
|
56
|
+
: selected.mediaType === 'application/vnd.ant.mermaid'
|
|
57
|
+
? 'mmd'
|
|
58
|
+
: selected.language
|
|
59
|
+
? selected.language
|
|
60
|
+
: 'md';
|
|
61
|
+
const blob = new Blob([selected.text], { type: 'text/plain' });
|
|
62
|
+
const url = URL.createObjectURL(blob);
|
|
63
|
+
const a = document.createElement('a');
|
|
64
|
+
a.href = url;
|
|
65
|
+
a.download = `${(selected.title ?? selected.artifactId).replace(/[^\w.-]+/g, '-')}.${ext}`;
|
|
66
|
+
a.click();
|
|
67
|
+
URL.revokeObjectURL(url);
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
{#if selected}
|
|
72
|
+
<aside class="agent-artifacts" aria-label="Artifacts">
|
|
73
|
+
<header class="agent-artifacts__header">
|
|
74
|
+
<select class="agent-artifacts__select" bind:value={selectedId} aria-label="Artifact">
|
|
75
|
+
{#each artifacts as a (a.artifactId)}
|
|
76
|
+
<option value={a.artifactId}>{a.title ?? a.artifactId}</option>
|
|
77
|
+
{/each}
|
|
78
|
+
</select>
|
|
79
|
+
<div class="agent-artifacts__actions">
|
|
80
|
+
<Button variant="ghost" size="icon" aria-label="Copy" onclick={copy}
|
|
81
|
+
><Icon name="copy" size={16} /></Button
|
|
82
|
+
>
|
|
83
|
+
<Button variant="ghost" size="icon" aria-label="Download" onclick={download}
|
|
84
|
+
><Icon name="download" size={16} /></Button
|
|
85
|
+
>
|
|
86
|
+
{#if onclose}<Button variant="ghost" size="icon" aria-label="Close" onclick={onclose}
|
|
87
|
+
><Icon name="close" size={16} /></Button
|
|
88
|
+
>{/if}
|
|
89
|
+
</div>
|
|
90
|
+
</header>
|
|
91
|
+
<div class="agent-artifacts__body">
|
|
92
|
+
{#if selected.mediaType === 'text/markdown'}
|
|
93
|
+
<div class="agent-markdown">{@html parseMarkdown(selected.text)}</div>
|
|
94
|
+
{:else if selected.mediaType === 'application/vnd.ant.code'}
|
|
95
|
+
<pre class="agent-code"><code>{selected.text}</code></pre>
|
|
96
|
+
{:else}
|
|
97
|
+
<!-- html, mermaid, svg, react: only ever inside the sandbox origin -->
|
|
98
|
+
<iframe
|
|
99
|
+
bind:this={frame}
|
|
100
|
+
title={selected.title ?? 'Artifact'}
|
|
101
|
+
src={`${sandboxOrigin}/frame`}
|
|
102
|
+
sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms"
|
|
103
|
+
referrerpolicy="no-referrer"
|
|
104
|
+
onload={sendHtml}
|
|
105
|
+
></iframe>
|
|
106
|
+
{/if}
|
|
107
|
+
{#if selected.open}<div class="agent-artifacts__streaming">Writing…</div>{/if}
|
|
108
|
+
</div>
|
|
109
|
+
</aside>
|
|
110
|
+
{/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>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Artifact } from '../reducer.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
artifacts: Artifact[];
|
|
4
|
+
sandboxOrigin: string;
|
|
5
|
+
selectedId?: string | null;
|
|
6
|
+
onclose?: () => void;
|
|
7
|
+
}
|
|
8
|
+
declare const ArtifactPanel: import("svelte").Component<Props, {}, "selectedId">;
|
|
9
|
+
type ArtifactPanel = ReturnType<typeof ArtifactPanel>;
|
|
10
|
+
export default ArtifactPanel;
|
|
11
|
+
//# sourceMappingURL=ArtifactPanel.svelte.d.ts.map
|
|
@@ -0,0 +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;AAK5C,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;CACtB;AAiGH,QAAA,MAAM,aAAa,qDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Icon } from '@classic-homes/theme-svelte';
|
|
3
|
+
import type { FileRef } from '@classic-homes/agent-contracts';
|
|
4
|
+
import { LIMITS } from '@classic-homes/agent-contracts';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
streaming?: boolean;
|
|
9
|
+
value?: string;
|
|
10
|
+
files?: FileRef[];
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
/** Upload a file; the composer shows it as pending until the message is sent. */
|
|
13
|
+
upload?: (file: File) => Promise<FileRef>;
|
|
14
|
+
onsend?: (text: string) => void;
|
|
15
|
+
oncancel?: () => void;
|
|
16
|
+
onremovefile?: (fileId: string) => void;
|
|
17
|
+
/** Slot for a host-provided context chip (page context toggle). */
|
|
18
|
+
context?: import('svelte').Snippet;
|
|
19
|
+
}
|
|
20
|
+
let {
|
|
21
|
+
disabled = false,
|
|
22
|
+
streaming = false,
|
|
23
|
+
value = $bindable(''),
|
|
24
|
+
files = $bindable([]),
|
|
25
|
+
placeholder = 'Ask Classic Agent…',
|
|
26
|
+
upload,
|
|
27
|
+
onsend,
|
|
28
|
+
oncancel,
|
|
29
|
+
onremovefile,
|
|
30
|
+
context,
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
|
|
33
|
+
let textarea: HTMLTextAreaElement | undefined = $state();
|
|
34
|
+
let fileInput: HTMLInputElement | undefined = $state();
|
|
35
|
+
let uploading = $state(0);
|
|
36
|
+
let uploadError = $state<string | undefined>();
|
|
37
|
+
|
|
38
|
+
export function focus(): void {
|
|
39
|
+
textarea?.focus({ preventScroll: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function submit(): void {
|
|
43
|
+
const text = value.trim();
|
|
44
|
+
if (!text || disabled || streaming || uploading > 0) return;
|
|
45
|
+
value = '';
|
|
46
|
+
if (textarea) textarea.style.height = 'auto';
|
|
47
|
+
onsend?.(text);
|
|
48
|
+
}
|
|
49
|
+
function onkeydown(event: KeyboardEvent): void {
|
|
50
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
submit();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function autosize(): void {
|
|
56
|
+
if (!textarea) return;
|
|
57
|
+
textarea.style.height = 'auto';
|
|
58
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
|
|
59
|
+
}
|
|
60
|
+
async function onfiles(event: Event): Promise<void> {
|
|
61
|
+
const input = event.currentTarget as HTMLInputElement;
|
|
62
|
+
const chosen = [...(input.files ?? [])];
|
|
63
|
+
input.value = '';
|
|
64
|
+
if (!upload || chosen.length === 0) return;
|
|
65
|
+
uploadError = undefined;
|
|
66
|
+
for (const file of chosen) {
|
|
67
|
+
if (file.size > LIMITS.fileBytes) {
|
|
68
|
+
uploadError = `${file.name} is larger than ${Math.round(LIMITS.fileBytes / 1048576)} MB`;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
uploading += 1;
|
|
72
|
+
try {
|
|
73
|
+
files = [...files, await upload(file)];
|
|
74
|
+
} catch (err) {
|
|
75
|
+
uploadError = err instanceof Error ? err.message : `Could not upload ${file.name}`;
|
|
76
|
+
} finally {
|
|
77
|
+
uploading -= 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<form
|
|
84
|
+
class="agent-composer"
|
|
85
|
+
onsubmit={(e) => {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
submit();
|
|
88
|
+
}}
|
|
89
|
+
aria-label="Send a message"
|
|
90
|
+
>
|
|
91
|
+
{#if context}<div class="agent-composer__context">{@render context()}</div>{/if}
|
|
92
|
+
{#if files.length || uploadError}
|
|
93
|
+
<div class="agent-composer__files">
|
|
94
|
+
{#each files as file (file.fileId)}
|
|
95
|
+
<span class="agent-composer__file">
|
|
96
|
+
<Icon name="file" size={12} />
|
|
97
|
+
{file.name}
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
aria-label={`Remove ${file.name}`}
|
|
101
|
+
onclick={() => {
|
|
102
|
+
files = files.filter((f) => f.fileId !== file.fileId);
|
|
103
|
+
onremovefile?.(file.fileId);
|
|
104
|
+
}}>×</button
|
|
105
|
+
>
|
|
106
|
+
</span>
|
|
107
|
+
{/each}
|
|
108
|
+
{#if uploadError}<span class="agent-composer__error" role="alert">{uploadError}</span>{/if}
|
|
109
|
+
</div>
|
|
110
|
+
{/if}
|
|
111
|
+
<div class="agent-composer__row">
|
|
112
|
+
{#if upload}
|
|
113
|
+
<input bind:this={fileInput} type="file" multiple hidden onchange={onfiles} />
|
|
114
|
+
<Button
|
|
115
|
+
type="button"
|
|
116
|
+
variant="ghost"
|
|
117
|
+
size="icon"
|
|
118
|
+
aria-label="Attach files"
|
|
119
|
+
disabled={disabled || uploading > 0}
|
|
120
|
+
loading={uploading > 0}
|
|
121
|
+
onclick={() => fileInput?.click()}
|
|
122
|
+
>
|
|
123
|
+
{#if uploading === 0}<Icon name="upload" size={18} />{/if}
|
|
124
|
+
</Button>
|
|
125
|
+
{/if}
|
|
126
|
+
<textarea
|
|
127
|
+
bind:this={textarea}
|
|
128
|
+
bind:value
|
|
129
|
+
class="agent-composer__field"
|
|
130
|
+
rows="1"
|
|
131
|
+
{placeholder}
|
|
132
|
+
aria-label="Message"
|
|
133
|
+
maxlength={LIMITS.messageChars}
|
|
134
|
+
{disabled}
|
|
135
|
+
{onkeydown}
|
|
136
|
+
oninput={autosize}></textarea>
|
|
137
|
+
{#if streaming}
|
|
138
|
+
<Button
|
|
139
|
+
type="button"
|
|
140
|
+
variant="outline"
|
|
141
|
+
size="icon"
|
|
142
|
+
aria-label="Stop"
|
|
143
|
+
onclick={() => oncancel?.()}><Icon name="square" size={16} /></Button
|
|
144
|
+
>
|
|
145
|
+
{:else}
|
|
146
|
+
<Button
|
|
147
|
+
type="submit"
|
|
148
|
+
size="icon"
|
|
149
|
+
aria-label="Send"
|
|
150
|
+
disabled={disabled || !value.trim() || uploading > 0}><Icon name="send" size={18} /></Button
|
|
151
|
+
>
|
|
152
|
+
{/if}
|
|
153
|
+
</div>
|
|
154
|
+
</form>
|
|
155
|
+
|
|
156
|
+
<style>
|
|
157
|
+
.agent-composer {
|
|
158
|
+
padding: 0.75rem 1rem max(0.75rem, env(safe-area-inset-bottom));
|
|
159
|
+
border-top: 1px solid hsl(var(--border));
|
|
160
|
+
background: hsl(var(--background));
|
|
161
|
+
display: grid;
|
|
162
|
+
gap: 0.5rem;
|
|
163
|
+
}
|
|
164
|
+
.agent-composer__row {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: flex-end;
|
|
167
|
+
gap: 0.5rem;
|
|
168
|
+
}
|
|
169
|
+
.agent-composer__field {
|
|
170
|
+
flex: 1;
|
|
171
|
+
resize: none;
|
|
172
|
+
min-height: 2.75rem;
|
|
173
|
+
max-height: 200px;
|
|
174
|
+
padding: 0.625rem 0.875rem;
|
|
175
|
+
border: 1px solid hsl(var(--border));
|
|
176
|
+
border-radius: 1rem;
|
|
177
|
+
background: hsl(var(--background));
|
|
178
|
+
color: hsl(var(--foreground));
|
|
179
|
+
font: inherit;
|
|
180
|
+
font-size: 16px;
|
|
181
|
+
line-height: 1.5;
|
|
182
|
+
}
|
|
183
|
+
.agent-composer__field:focus {
|
|
184
|
+
outline: none;
|
|
185
|
+
border-color: hsl(var(--primary));
|
|
186
|
+
box-shadow: 0 0 0 3px hsl(var(--primary) / 0.15);
|
|
187
|
+
}
|
|
188
|
+
.agent-composer__files {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-wrap: wrap;
|
|
191
|
+
gap: 0.375rem;
|
|
192
|
+
font-size: 0.75rem;
|
|
193
|
+
}
|
|
194
|
+
.agent-composer__file {
|
|
195
|
+
display: inline-flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
gap: 0.25rem;
|
|
198
|
+
padding: 0.125rem 0.5rem;
|
|
199
|
+
border-radius: 9999px;
|
|
200
|
+
background: hsl(var(--muted));
|
|
201
|
+
}
|
|
202
|
+
.agent-composer__file button {
|
|
203
|
+
border: 0;
|
|
204
|
+
background: none;
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
font: inherit;
|
|
207
|
+
}
|
|
208
|
+
.agent-composer__error {
|
|
209
|
+
color: hsl(var(--destructive));
|
|
210
|
+
}
|
|
211
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FileRef } from '@classic-homes/agent-contracts';
|
|
2
|
+
interface Props {
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
streaming?: boolean;
|
|
5
|
+
value?: string;
|
|
6
|
+
files?: FileRef[];
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
/** Upload a file; the composer shows it as pending until the message is sent. */
|
|
9
|
+
upload?: (file: File) => Promise<FileRef>;
|
|
10
|
+
onsend?: (text: string) => void;
|
|
11
|
+
oncancel?: () => void;
|
|
12
|
+
onremovefile?: (fileId: string) => void;
|
|
13
|
+
/** Slot for a host-provided context chip (page context toggle). */
|
|
14
|
+
context?: import('svelte').Snippet;
|
|
15
|
+
}
|
|
16
|
+
declare const Composer: import("svelte").Component<Props, {
|
|
17
|
+
focus: () => void;
|
|
18
|
+
}, "value" | "files">;
|
|
19
|
+
type Composer = ReturnType<typeof Composer>;
|
|
20
|
+
export default Composer;
|
|
21
|
+
//# sourceMappingURL=Composer.svelte.d.ts.map
|
|
@@ -0,0 +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;AAI5D,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,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;CACpC;AA6GH,QAAA,MAAM,QAAQ;iBArFO,IAAI;qBAqF6B,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|