@aparte/svelte 0.24.0 → 0.26.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 +5 -0
- package/dist/AparteChat.svelte +301 -226
- package/dist/AparteChat.svelte.d.ts +136 -133
- package/dist/AparteUi.svelte +82 -52
- package/dist/AparteUi.svelte.d.ts +36 -33
- package/dist/elements.d.ts +44 -0
- package/dist/elements.d.ts.map +1 -0
- package/dist/elements.js +1 -0
- package/dist/index.d.ts +1 -38
- package/dist/index.d.ts.map +1 -1
- package/dist/runes/AparteChat.svelte +304 -0
- package/dist/runes/AparteChat.svelte.d.ts +32 -0
- package/dist/runes/AparteSegmentUI.svelte +35 -0
- package/dist/runes/AparteSegmentUI.svelte.d.ts +38 -0
- package/dist/runes/AparteToolUI.svelte +44 -0
- package/dist/runes/AparteToolUI.svelte.d.ts +44 -0
- package/dist/runes/AparteUi.svelte +68 -0
- package/dist/runes/AparteUi.svelte.d.ts +16 -0
- package/dist/runes/SnippetHost.svelte +11 -0
- package/dist/runes/SnippetHost.svelte.d.ts +32 -0
- package/dist/runes/conversations.svelte.d.ts +42 -0
- package/dist/runes/conversations.svelte.d.ts.map +1 -0
- package/dist/runes/conversations.svelte.js +66 -0
- package/dist/runes/index.d.ts +18 -0
- package/dist/runes/index.d.ts.map +1 -0
- package/dist/runes/index.js +12 -0
- package/dist/runes/mount-snippet.svelte.d.ts +10 -0
- package/dist/runes/mount-snippet.svelte.d.ts.map +1 -0
- package/dist/runes/mount-snippet.svelte.js +24 -0
- package/dist/runes/types.d.ts +68 -0
- package/dist/runes/types.d.ts.map +1 -0
- package/dist/runes/types.js +1 -0
- package/dist/stores/aparteChat.d.ts +4 -0
- package/dist/stores/aparteChat.d.ts.map +1 -1
- package/dist/stores/aparteChat.js +4 -0
- package/dist/stores/aparteClient.d.ts +5 -0
- package/dist/stores/aparteClient.d.ts.map +1 -1
- package/dist/stores/aparteClient.js +5 -0
- package/dist/stores/conversationManager.d.ts +4 -0
- package/dist/stores/conversationManager.d.ts.map +1 -1
- package/dist/stores/conversationManager.js +4 -0
- package/package.json +19 -8
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, tick, untrack } from 'svelte';
|
|
3
|
+
import { AparteChatHost, aparteGlobalConfig, isAwaitingReply, releaseDeferredHydration, type AparteChatHostBinding, type AparteChatImperativeApi } from '@aparte/core';
|
|
4
|
+
import type { AparteMessage, AparteSegment, AparteSendEventDetail, AparteActionEventDetail } from '../types.js';
|
|
5
|
+
import type { AparteChatProps } from './types.js';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* The Svelte 5 chat: the same host, the same markup and the same rules as the
|
|
9
|
+
* Svelte-4-syntax component, written the way a Svelte 5 app writes components —
|
|
10
|
+
* `$props`, a bindable `messages`, snippets for the five places for your own markup,
|
|
11
|
+
* callback props for every event. The orchestration (streaming, branches, the
|
|
12
|
+
* conversation controller) is core's `AparteChatHost`, bound once on mount.
|
|
13
|
+
*/
|
|
14
|
+
let {
|
|
15
|
+
id,
|
|
16
|
+
messages = $bindable([]),
|
|
17
|
+
placeholder = 'Type a message...',
|
|
18
|
+
disabled = false,
|
|
19
|
+
isTyping = false,
|
|
20
|
+
typingText,
|
|
21
|
+
submitOnEnter = true,
|
|
22
|
+
layoutTransitionMs = 0,
|
|
23
|
+
centerWhenEmpty = false,
|
|
24
|
+
loading = false,
|
|
25
|
+
overlayComposer = false,
|
|
26
|
+
attachments = false,
|
|
27
|
+
elicitation = true,
|
|
28
|
+
class: className = '',
|
|
29
|
+
style,
|
|
30
|
+
conversationId = null,
|
|
31
|
+
config,
|
|
32
|
+
emptyState,
|
|
33
|
+
aboveComposer,
|
|
34
|
+
composer,
|
|
35
|
+
toolbar,
|
|
36
|
+
bubble,
|
|
37
|
+
onmessageSent,
|
|
38
|
+
onaction,
|
|
39
|
+
onmessagesChange,
|
|
40
|
+
onmessageAppended,
|
|
41
|
+
ontypingChange,
|
|
42
|
+
onconversationCreated,
|
|
43
|
+
}: AparteChatProps = $props();
|
|
44
|
+
|
|
45
|
+
// A caller-supplied id wins, so `scopeToTargetId` has something to match; otherwise one
|
|
46
|
+
// that the server render and the browser agree on.
|
|
47
|
+
const uid = $props.id();
|
|
48
|
+
const hostId = untrack(() => id) ?? `aparte-chat-${uid}`;
|
|
49
|
+
|
|
50
|
+
let rootRef: HTMLElement | undefined = $state();
|
|
51
|
+
// The server render carries `defer-hydration` on the root, so core's elements leave the
|
|
52
|
+
// server's DOM alone until Svelte has it; onMount releases them. The client never draws it:
|
|
53
|
+
// a client-only render holds nothing, so a slot's component — whose onMount runs before the
|
|
54
|
+
// chat's — talks to live elements. A hydration may take the server's attribute off as it
|
|
55
|
+
// claims the root; the release does not wait for that either.
|
|
56
|
+
const serverRender = typeof window === 'undefined';
|
|
57
|
+
let viewportRef: HTMLElement | undefined = $state();
|
|
58
|
+
let composerRef: HTMLElement | undefined = $state();
|
|
59
|
+
let host: AparteChatHost | null = null;
|
|
60
|
+
|
|
61
|
+
// The list the transcript draws. Raw, not deep state: the host replaces it whole, and a
|
|
62
|
+
// proxy around the messages would be handed to core on every read.
|
|
63
|
+
let internalMessages = $state.raw<AparteMessage[]>(untrack(() => [...messages]));
|
|
64
|
+
let typingActive = $state(untrack(() => isTyping));
|
|
65
|
+
// The controller's wait (a conversation fetched through the adapter), or the caller's.
|
|
66
|
+
let hostLoading = $state(false);
|
|
67
|
+
const waiting = $derived(loading || hostLoading);
|
|
68
|
+
|
|
69
|
+
// Bumped on every config change: the two labels below are locale text.
|
|
70
|
+
let localeTick = $state(0);
|
|
71
|
+
const loadingText = $derived.by(() => { void localeTick; return (config ?? aparteGlobalConfig).t('loadingConversation'); });
|
|
72
|
+
const typingLabel = $derived.by(() => { void localeTick; return typingText ?? (config ?? aparteGlobalConfig).t('typing'); });
|
|
73
|
+
|
|
74
|
+
// A parent write to `messages` reaches the transcript — a reassignment, or a push / splice on
|
|
75
|
+
// a bound $state array — unless it is the host's own emit coming back, current or stale:
|
|
76
|
+
// applying it rolled back what a non-emitting call (appendMessage) had added since.
|
|
77
|
+
//
|
|
78
|
+
// The reports the parent has not handed back yet, oldest first — the rule core's host applies
|
|
79
|
+
// for the other wrappers (`takeEcho`), here by value AND ids: what comes back is a proxy of the
|
|
80
|
+
// emitted list — the chat's own in a bare chat, the parent's under `bind:` or when the parent
|
|
81
|
+
// writes it back into a deep $state (a new proxy each time: Svelte keeps no cache) — and a
|
|
82
|
+
// proxy the parent mutated in place keeps its identity, not its ids. A report that comes back,
|
|
83
|
+
// however late, is taken with every older one; any other value is the parent's word and clears
|
|
84
|
+
// them, so a list the parent restores later (a reopened conversation, an undo) is applied.
|
|
85
|
+
let pendingEchoes: Array<{ values: object[]; ids: string[] }> = [];
|
|
86
|
+
const sameIds = (a: readonly (string | undefined)[], b: readonly (string | undefined)[]) =>
|
|
87
|
+
a.length === b.length && a.every((id, i) => id === b[i]);
|
|
88
|
+
function takeEcho(value: object, ids: readonly (string | undefined)[]): boolean {
|
|
89
|
+
const i = pendingEchoes.findIndex((p) => p.values.includes(value) && sameIds(ids, p.ids));
|
|
90
|
+
if (i < 0) { pendingEchoes = []; return false; }
|
|
91
|
+
pendingEchoes.splice(0, i + 1);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
let lastProp = untrack(() => messages);
|
|
95
|
+
$effect.pre(() => {
|
|
96
|
+
const next = messages;
|
|
97
|
+
// Read every index: a push, a splice or an index write re-runs this, not only a reassignment.
|
|
98
|
+
const items = [...next];
|
|
99
|
+
untrack(() => {
|
|
100
|
+
const ids = items.map((m) => m?.id);
|
|
101
|
+
if (takeEcho(next, ids)) { lastProp = next; return; }
|
|
102
|
+
if (next === lastProp && sameIds(ids, internalMessages.map((m) => m.id))) return;
|
|
103
|
+
lastProp = next;
|
|
104
|
+
internalMessages = items;
|
|
105
|
+
if (items.length === 0) host?.clearRenderCache();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// A waiting line the caller drives (the host may turn it off on the first token).
|
|
110
|
+
let lastTyping = untrack(() => isTyping);
|
|
111
|
+
$effect.pre(() => {
|
|
112
|
+
if (isTyping === lastTyping) return;
|
|
113
|
+
lastTyping = isTyping;
|
|
114
|
+
typingActive = isTyping;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// The conversation id drives the host; the initial one is handed over at bind.
|
|
118
|
+
let lastConv = untrack(() => conversationId);
|
|
119
|
+
$effect(() => {
|
|
120
|
+
const next = conversationId;
|
|
121
|
+
if (next === lastConv) return;
|
|
122
|
+
lastConv = next;
|
|
123
|
+
untrack(() => { void host?.setConversationId(next ?? null); });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// The bubbles are rendered by this template; the host reconciles them after each change.
|
|
127
|
+
$effect(() => {
|
|
128
|
+
void internalMessages;
|
|
129
|
+
untrack(() => { if (host) void tick().then(() => host?.syncBubbles()); });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// The composer's and the viewport's state are attributes core's elements read. Svelte 5
|
|
133
|
+
// sets a template attribute as a PROPERTY when the element has one, and `''` through a
|
|
134
|
+
// boolean setter reads as false, so presence attributes are written here explicitly.
|
|
135
|
+
function toggleAttr(el: HTMLElement, name: string, on: boolean, value: string) {
|
|
136
|
+
if (on) el.setAttribute(name, value); else el.removeAttribute(name);
|
|
137
|
+
}
|
|
138
|
+
$effect(() => { if (viewportRef) toggleAttr(viewportRef, 'loading', waiting, ''); });
|
|
139
|
+
$effect(() => {
|
|
140
|
+
if (!composerRef) return;
|
|
141
|
+
composerRef.setAttribute('target', hostId);
|
|
142
|
+
composerRef.setAttribute('placeholder', placeholder);
|
|
143
|
+
toggleAttr(composerRef, 'disabled', disabled || waiting, '');
|
|
144
|
+
toggleAttr(composerRef, 'submit-on-enter', !submitOnEnter, 'false');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
function handleSend(event: Event) {
|
|
148
|
+
(viewportRef as unknown as { requestSmoothScroll?: () => void })?.requestSmoothScroll?.();
|
|
149
|
+
onmessageSent?.((event as CustomEvent<AparteSendEventDetail>).detail);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Custom bubble actions bubble to the root as `aparte-action`.
|
|
153
|
+
function handleAction(event: Event) {
|
|
154
|
+
onaction?.((event as CustomEvent<AparteActionEventDetail>).detail);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
onMount(() => {
|
|
158
|
+
// The elements a server render left waiting are upgraded first: the host talks to them.
|
|
159
|
+
releaseDeferredHydration(rootRef);
|
|
160
|
+
const unsubscribeLocale = (config ?? aparteGlobalConfig).subscribe(() => { localeTick += 1; });
|
|
161
|
+
const binding: AparteChatHostBinding = {
|
|
162
|
+
hostId,
|
|
163
|
+
host: rootRef!,
|
|
164
|
+
viewport: viewportRef ?? null,
|
|
165
|
+
getMessages: () => internalMessages,
|
|
166
|
+
setMessages: (m) => { internalMessages = m as AparteMessage[]; },
|
|
167
|
+
onMessagesChange: (m) => {
|
|
168
|
+
const list = m as AparteMessage[];
|
|
169
|
+
const report = { values: [list as object], ids: list.map((x) => x.id) };
|
|
170
|
+
pendingEchoes.push(report);
|
|
171
|
+
if (pendingEchoes.length > 64) pendingEchoes.shift();
|
|
172
|
+
messages = list;
|
|
173
|
+
onmessagesChange?.(list);
|
|
174
|
+
// What the effect above will read back — read AFTER the callback, so a write-back made
|
|
175
|
+
// in it (`onmessagesChange={(m) => (msgs = m)}`) is the proxy recorded, not the one it
|
|
176
|
+
// replaced. A write-back after an await lands later, as the parent's own word.
|
|
177
|
+
const stored = untrack(() => messages);
|
|
178
|
+
report.values.push(stored);
|
|
179
|
+
lastProp = stored;
|
|
180
|
+
},
|
|
181
|
+
onMessageAppended: (m) => { onmessageAppended?.(m as AparteMessage); },
|
|
182
|
+
onTypingChange: (t) => { typingActive = t; ontypingChange?.(t); },
|
|
183
|
+
onStreamingChange: () => { /* exposed via isStreaming() */ },
|
|
184
|
+
afterRender: (cb) => { void tick().then(cb); },
|
|
185
|
+
resetComposer: () => (composerRef as unknown as { reset?: () => void })?.reset?.(),
|
|
186
|
+
onLoadingChange: (on) => { hostLoading = on; },
|
|
187
|
+
};
|
|
188
|
+
host = new AparteChatHost(binding, {
|
|
189
|
+
layoutTransitionMs,
|
|
190
|
+
conversationId: conversationId ?? null,
|
|
191
|
+
onConversationCreated: (createdId) => { onconversationCreated?.(createdId); },
|
|
192
|
+
config,
|
|
193
|
+
});
|
|
194
|
+
const unbind = host.bind();
|
|
195
|
+
host.syncBubbles();
|
|
196
|
+
rootRef?.addEventListener('aparte-action', handleAction);
|
|
197
|
+
return () => {
|
|
198
|
+
unsubscribeLocale();
|
|
199
|
+
rootRef?.removeEventListener('aparte-action', handleAction);
|
|
200
|
+
unbind();
|
|
201
|
+
host = null;
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ── Imperative API (bind:this) ──
|
|
206
|
+
export function appendMessage(m: AparteMessage, options?: { historical?: boolean }) { host?.appendMessage(m, options); }
|
|
207
|
+
export function updateMessage(messageId: string, u: Partial<AparteMessage>) { host?.updateMessage(messageId, u); }
|
|
208
|
+
export function updateLastMessage(content: string, options?: { append?: boolean }) { host?.updateLastMessage(content, options); }
|
|
209
|
+
export function addSegment(segment: AparteSegment) { host?.addSegment(segment); }
|
|
210
|
+
export function updateSegment(segmentId: string, updates: Partial<AparteSegment>) { host?.updateSegment(segmentId, updates); }
|
|
211
|
+
export function removeSegment(segmentId: string) { host?.removeSegment(segmentId); }
|
|
212
|
+
export function appendToSegment(segmentId: string, content: string) { host?.appendToSegment(segmentId, content); }
|
|
213
|
+
export function getMessages(): AparteMessage[] { return host?.getMessages() ?? internalMessages; }
|
|
214
|
+
export function clearMessages(options?: { revokeAttachments?: boolean }) { host?.clearMessages(options); }
|
|
215
|
+
export function addBranch(messageId: string): number { return host?.addBranch(messageId) ?? 0; }
|
|
216
|
+
export function addSiblingOf(existingId: string, message: AparteMessage): string | null { return host?.addSiblingOf(existingId, message) ?? null; }
|
|
217
|
+
export function truncateFrom(messageId: string) { host?.truncateFrom(messageId); }
|
|
218
|
+
export function truncateResponsesAfter(userMessageId: string) { host?.truncateResponsesAfter(userMessageId); }
|
|
219
|
+
export function injectTokenStream(messageId: string, tokens: AsyncIterable<string>): Promise<void> {
|
|
220
|
+
return host?.streamTokens(messageId, tokens) ?? Promise.resolve();
|
|
221
|
+
}
|
|
222
|
+
export function stopTokenStream() { host?.stopTokenStream(); }
|
|
223
|
+
export function setConversationId(next: string | null): Promise<void> { return host?.setConversationId(next) ?? Promise.resolve(); }
|
|
224
|
+
export function scrollToBottom() { (viewportRef as unknown as { scrollToBottom?: () => void })?.scrollToBottom?.(); }
|
|
225
|
+
/** The `<aparte-chat-viewport>` element — the same accessor on all four wrappers. */
|
|
226
|
+
export function getViewport(): HTMLElement | null { return viewportRef ?? null; }
|
|
227
|
+
export function focusInput() { (composerRef as unknown as { focus?: () => void })?.focus?.(); }
|
|
228
|
+
export function isStreaming(): boolean { return host?.isStreaming ?? false; }
|
|
229
|
+
|
|
230
|
+
// Compile-time parity with the canonical API: a dropped or mistyped method is a build error.
|
|
231
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
232
|
+
function _assertImperativeParity(): AparteChatImperativeApi {
|
|
233
|
+
return {
|
|
234
|
+
appendMessage, updateMessage, updateLastMessage, addSegment, updateSegment, removeSegment,
|
|
235
|
+
appendToSegment, getMessages, clearMessages, addBranch, addSiblingOf, truncateFrom,
|
|
236
|
+
truncateResponsesAfter, injectTokenStream, stopTokenStream, setConversationId,
|
|
237
|
+
scrollToBottom, focusInput, isStreaming, getViewport,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
<div
|
|
243
|
+
class="aparte-chat-container {className}"
|
|
244
|
+
class:aparte-chat-container--auto-center={centerWhenEmpty}
|
|
245
|
+
{style}
|
|
246
|
+
data-aparte-chat
|
|
247
|
+
{...(serverRender ? { 'defer-hydration': '' } : {})}
|
|
248
|
+
{...(overlayComposer ? { 'overlay-composer': '' } : {})}
|
|
249
|
+
data-aparte-empty={centerWhenEmpty && internalMessages.length === 0 && !waiting ? '' : null}
|
|
250
|
+
id={hostId}
|
|
251
|
+
bind:this={rootRef}
|
|
252
|
+
>
|
|
253
|
+
<aparte-chat-viewport bind:this={viewportRef} framework-managed="">
|
|
254
|
+
{#if waiting}
|
|
255
|
+
<div class="aparte-viewport-loading" aria-hidden="true">
|
|
256
|
+
<span class="aparte-skeleton aparte-skeleton--rect aparte-viewport-loading__user"></span>
|
|
257
|
+
<span class="aparte-skeleton aparte-skeleton--text"></span>
|
|
258
|
+
<span class="aparte-skeleton aparte-skeleton--text"></span>
|
|
259
|
+
<span class="aparte-skeleton aparte-skeleton--text"></span>
|
|
260
|
+
<span class="aparte-skeleton aparte-skeleton--text aparte-viewport-loading__last"></span>
|
|
261
|
+
</div>
|
|
262
|
+
<span class="aparte-viewport-loading-status aparte-sr-only" role="status">{loadingText}</span>
|
|
263
|
+
{/if}
|
|
264
|
+
{#if internalMessages.length === 0 && !waiting}
|
|
265
|
+
{@render emptyState?.()}
|
|
266
|
+
{/if}
|
|
267
|
+
{#each internalMessages as m (m.id)}
|
|
268
|
+
{#if bubble}
|
|
269
|
+
{@render bubble(m)}
|
|
270
|
+
{:else}
|
|
271
|
+
<aparte-chat-bubble
|
|
272
|
+
message-id={m.id}
|
|
273
|
+
data-role={m.role}
|
|
274
|
+
data-kind={m.compaction ? 'compaction' : undefined}
|
|
275
|
+
timestamp={m.timestamp}
|
|
276
|
+
content={m.content}
|
|
277
|
+
streaming={isAwaitingReply(m) ? '' : null}></aparte-chat-bubble>
|
|
278
|
+
{/if}
|
|
279
|
+
{/each}
|
|
280
|
+
<aparte-chat-status visible={typingActive ? '' : null} text={typingLabel}></aparte-chat-status>
|
|
281
|
+
</aparte-chat-viewport>
|
|
282
|
+
|
|
283
|
+
{#if elicitation}<aparte-elicitation></aparte-elicitation>{/if}
|
|
284
|
+
|
|
285
|
+
{@render aboveComposer?.()}
|
|
286
|
+
|
|
287
|
+
<aparte-composer bind:this={composerRef} onaparte-send={handleSend}>
|
|
288
|
+
{#if composer}
|
|
289
|
+
{@render composer()}
|
|
290
|
+
{:else}
|
|
291
|
+
<div class="aparte-composer-shell">
|
|
292
|
+
{#if attachments}<aparte-composer-attachments></aparte-composer-attachments>{/if}
|
|
293
|
+
<div class="aparte-composer-row">
|
|
294
|
+
{#if attachments}<aparte-composer-add-attachment></aparte-composer-add-attachment>{/if}
|
|
295
|
+
<aparte-composer-input></aparte-composer-input>
|
|
296
|
+
<aparte-composer-send></aparte-composer-send>
|
|
297
|
+
</div>
|
|
298
|
+
{#if toolbar}
|
|
299
|
+
<aparte-composer-toolbar>{@render toolbar()}</aparte-composer-toolbar>
|
|
300
|
+
{/if}
|
|
301
|
+
</div>
|
|
302
|
+
{/if}
|
|
303
|
+
</aparte-composer>
|
|
304
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AparteMessage, AparteSegment } from '../types.js';
|
|
2
|
+
import type { AparteChatProps } from './types.js';
|
|
3
|
+
declare const AparteChat: import("svelte").Component<AparteChatProps, {
|
|
4
|
+
appendMessage: (m: AparteMessage, options?: {
|
|
5
|
+
historical?: boolean;
|
|
6
|
+
}) => void;
|
|
7
|
+
updateMessage: (messageId: string, u: Partial<AparteMessage>) => void;
|
|
8
|
+
updateLastMessage: (content: string, options?: {
|
|
9
|
+
append?: boolean;
|
|
10
|
+
}) => void;
|
|
11
|
+
addSegment: (segment: AparteSegment) => void;
|
|
12
|
+
updateSegment: (segmentId: string, updates: Partial<AparteSegment>) => void;
|
|
13
|
+
removeSegment: (segmentId: string) => void;
|
|
14
|
+
appendToSegment: (segmentId: string, content: string) => void;
|
|
15
|
+
getMessages: () => AparteMessage[];
|
|
16
|
+
clearMessages: (options?: {
|
|
17
|
+
revokeAttachments?: boolean;
|
|
18
|
+
}) => void;
|
|
19
|
+
addBranch: (messageId: string) => number;
|
|
20
|
+
addSiblingOf: (existingId: string, message: AparteMessage) => string | null;
|
|
21
|
+
truncateFrom: (messageId: string) => void;
|
|
22
|
+
truncateResponsesAfter: (userMessageId: string) => void;
|
|
23
|
+
injectTokenStream: (messageId: string, tokens: AsyncIterable<string>) => Promise<void>;
|
|
24
|
+
stopTokenStream: () => void;
|
|
25
|
+
setConversationId: (next: string | null) => Promise<void>;
|
|
26
|
+
scrollToBottom: () => void;
|
|
27
|
+
getViewport: () => HTMLElement | null;
|
|
28
|
+
focusInput: () => void;
|
|
29
|
+
isStreaming: () => boolean;
|
|
30
|
+
}, "messages">;
|
|
31
|
+
type AparteChat = ReturnType<typeof AparteChat>;
|
|
32
|
+
export default AparteChat;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends AparteSegmentBase">
|
|
2
|
+
/**
|
|
3
|
+
* Your own Svelte component for a segment type — one your stream emits, or a built-in such
|
|
4
|
+
* as `code`. Mounted into each segment like `<AparteToolUI>`; taking the last declaration off
|
|
5
|
+
* a built-in type gives it back to core's renderer.
|
|
6
|
+
*/
|
|
7
|
+
import { getAllContexts, untrack, type Snippet } from 'svelte';
|
|
8
|
+
import { defineMountedSegmentRenderer, overrideSegmentRenderer } from '@aparte/core';
|
|
9
|
+
import type { AparteConfig, AparteSegmentBase } from '@aparte/core';
|
|
10
|
+
import { mountSnippet } from './mount-snippet.svelte.js';
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
type,
|
|
14
|
+
config,
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
/** The segment `type` this draws. */
|
|
18
|
+
type: T['type'];
|
|
19
|
+
/** The config to declare it on. Defaults to the global one. */
|
|
20
|
+
config?: AparteConfig;
|
|
21
|
+
/** What to draw for one segment, handed its latest state. */
|
|
22
|
+
children: Snippet<[T]>;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
const context = getAllContexts();
|
|
26
|
+
|
|
27
|
+
$effect(() => {
|
|
28
|
+
const segmentType = type;
|
|
29
|
+
const target = config;
|
|
30
|
+
return untrack(() => {
|
|
31
|
+
const renderer = defineMountedSegmentRenderer<T>(segmentType, (host, segment) => mountSnippet(host, () => children, segment, context));
|
|
32
|
+
return target ? overrideSegmentRenderer(renderer, target) : overrideSegmentRenderer(renderer);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Your own Svelte component for a segment type — one your stream emits, or a built-in such
|
|
3
|
+
* as `code`. Mounted into each segment like `<AparteToolUI>`; taking the last declaration off
|
|
4
|
+
* a built-in type gives it back to core's renderer.
|
|
5
|
+
*/
|
|
6
|
+
import { type Snippet } from 'svelte';
|
|
7
|
+
import type { AparteConfig, AparteSegmentBase } from '@aparte/core';
|
|
8
|
+
declare function $$render<T extends AparteSegmentBase>(): {
|
|
9
|
+
props: {
|
|
10
|
+
/** The segment `type` this draws. */
|
|
11
|
+
type: T["type"];
|
|
12
|
+
/** The config to declare it on. Defaults to the global one. */
|
|
13
|
+
config?: AparteConfig;
|
|
14
|
+
/** What to draw for one segment, handed its latest state. */
|
|
15
|
+
children: Snippet<[T]>;
|
|
16
|
+
};
|
|
17
|
+
exports: {};
|
|
18
|
+
bindings: "";
|
|
19
|
+
slots: {};
|
|
20
|
+
events: {};
|
|
21
|
+
};
|
|
22
|
+
declare class __sveltets_Render<T extends AparteSegmentBase> {
|
|
23
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
24
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
25
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
26
|
+
bindings(): "";
|
|
27
|
+
exports(): {};
|
|
28
|
+
}
|
|
29
|
+
interface $$IsomorphicComponent {
|
|
30
|
+
new <T extends AparteSegmentBase>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
31
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
32
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
33
|
+
<T extends AparteSegmentBase>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
34
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
35
|
+
}
|
|
36
|
+
declare const AparteSegmentUI: $$IsomorphicComponent;
|
|
37
|
+
type AparteSegmentUI<T extends AparteSegmentBase> = InstanceType<typeof AparteSegmentUI<T>>;
|
|
38
|
+
export default AparteSegmentUI;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts" generics="I = unknown, R = unknown">
|
|
2
|
+
/**
|
|
3
|
+
* Your own Svelte component in a tool call.
|
|
4
|
+
*
|
|
5
|
+
* `<AparteToolUI name="get_weather">{#snippet children(tool)}<WeatherCard {tool} />{/snippet}</AparteToolUI>`
|
|
6
|
+
* draws every `get_weather` call with your snippet for as long as it is mounted, and gives the
|
|
7
|
+
* calls back to core's row when it unmounts. The snippet is mounted with the contexts of where
|
|
8
|
+
* you put the declaration, so `getContext` reads what your app set there.
|
|
9
|
+
*
|
|
10
|
+
* Underneath it is core's `overrideToolRenderer` + `defineMountedToolRenderer`. Nothing is
|
|
11
|
+
* declared in a server render (`$effect` runs in the browser only); the cards are drawn there.
|
|
12
|
+
*/
|
|
13
|
+
import { getAllContexts, untrack, type Snippet } from 'svelte';
|
|
14
|
+
import { aparteGlobalConfig, defineMountedToolRenderer } from '@aparte/core';
|
|
15
|
+
import type { AparteConfig, AparteToolCallView, AparteToolMatcher } from '@aparte/core';
|
|
16
|
+
import { mountSnippet } from './mount-snippet.svelte.js';
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
name,
|
|
20
|
+
config,
|
|
21
|
+
children,
|
|
22
|
+
}: {
|
|
23
|
+
/** The tool this draws: a name, a `RegExp`, or `'*'` for every tool. */
|
|
24
|
+
name: AparteToolMatcher;
|
|
25
|
+
/** The config to declare it on. Defaults to the global one. */
|
|
26
|
+
config?: AparteConfig;
|
|
27
|
+
/** What to draw for one call: its id, name, input, status, result and the rest. */
|
|
28
|
+
children: Snippet<[AparteToolCallView<I, R>]>;
|
|
29
|
+
} = $props();
|
|
30
|
+
|
|
31
|
+
// Readable only while the component initialises.
|
|
32
|
+
const context = getAllContexts();
|
|
33
|
+
// The matcher by its text: a `RegExp` literal in a template is a new object on every render.
|
|
34
|
+
const key = $derived(typeof name === 'string' ? name : String(name));
|
|
35
|
+
|
|
36
|
+
$effect(() => {
|
|
37
|
+
void key;
|
|
38
|
+
const target = config ?? aparteGlobalConfig;
|
|
39
|
+
return untrack(() => target.overrideToolRenderer(
|
|
40
|
+
name,
|
|
41
|
+
defineMountedToolRenderer((host, view) => mountSnippet(host, () => children as Snippet<[AparteToolCallView]>, view, context)),
|
|
42
|
+
));
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Your own Svelte component in a tool call.
|
|
3
|
+
*
|
|
4
|
+
* `<AparteToolUI name="get_weather">{#snippet children(tool)}<WeatherCard {tool} />{/snippet}</AparteToolUI>`
|
|
5
|
+
* draws every `get_weather` call with your snippet for as long as it is mounted, and gives the
|
|
6
|
+
* calls back to core's row when it unmounts. The snippet is mounted with the contexts of where
|
|
7
|
+
* you put the declaration, so `getContext` reads what your app set there.
|
|
8
|
+
*
|
|
9
|
+
* Underneath it is core's `overrideToolRenderer` + `defineMountedToolRenderer`. Nothing is
|
|
10
|
+
* declared in a server render (`$effect` runs in the browser only); the cards are drawn there.
|
|
11
|
+
*/
|
|
12
|
+
import { type Snippet } from 'svelte';
|
|
13
|
+
import type { AparteConfig, AparteToolCallView, AparteToolMatcher } from '@aparte/core';
|
|
14
|
+
declare function $$render<I = unknown, R = unknown>(): {
|
|
15
|
+
props: {
|
|
16
|
+
/** The tool this draws: a name, a `RegExp`, or `'*'` for every tool. */
|
|
17
|
+
name: AparteToolMatcher;
|
|
18
|
+
/** The config to declare it on. Defaults to the global one. */
|
|
19
|
+
config?: AparteConfig;
|
|
20
|
+
/** What to draw for one call: its id, name, input, status, result and the rest. */
|
|
21
|
+
children: Snippet<[AparteToolCallView<I, R>]>;
|
|
22
|
+
};
|
|
23
|
+
exports: {};
|
|
24
|
+
bindings: "";
|
|
25
|
+
slots: {};
|
|
26
|
+
events: {};
|
|
27
|
+
};
|
|
28
|
+
declare class __sveltets_Render<I = unknown, R = unknown> {
|
|
29
|
+
props(): ReturnType<typeof $$render<I, R>>['props'];
|
|
30
|
+
events(): ReturnType<typeof $$render<I, R>>['events'];
|
|
31
|
+
slots(): ReturnType<typeof $$render<I, R>>['slots'];
|
|
32
|
+
bindings(): "";
|
|
33
|
+
exports(): {};
|
|
34
|
+
}
|
|
35
|
+
interface $$IsomorphicComponent {
|
|
36
|
+
new <I = unknown, R = unknown>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<I, R>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<I, R>['props']>, ReturnType<__sveltets_Render<I, R>['events']>, ReturnType<__sveltets_Render<I, R>['slots']>> & {
|
|
37
|
+
$$bindings?: ReturnType<__sveltets_Render<I, R>['bindings']>;
|
|
38
|
+
} & ReturnType<__sveltets_Render<I, R>['exports']>;
|
|
39
|
+
<I = unknown, R = unknown>(internal: unknown, props: ReturnType<__sveltets_Render<I, R>['props']> & {}): ReturnType<__sveltets_Render<I, R>['exports']>;
|
|
40
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
|
|
41
|
+
}
|
|
42
|
+
declare const AparteToolUI: $$IsomorphicComponent;
|
|
43
|
+
type AparteToolUI<I = unknown, R = unknown> = InstanceType<typeof AparteToolUI<I, R>>;
|
|
44
|
+
export default AparteToolUI;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, untrack } from 'svelte';
|
|
3
|
+
import { applyElementProps, APARTE_DEFAULT_UI_EVENTS } from '@aparte/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mounts any element by tag name, sets its props and forwards its events — for an element
|
|
7
|
+
* aparté does not wrap (a plugin's, or one of yours). Svelte 5: props and a callback prop.
|
|
8
|
+
*/
|
|
9
|
+
let {
|
|
10
|
+
name,
|
|
11
|
+
props = {},
|
|
12
|
+
events,
|
|
13
|
+
onelementEvent,
|
|
14
|
+
}: {
|
|
15
|
+
/** The custom element tag name (e.g. 'aparte-model-selector'). */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Props to apply. Keys starting with `--` become CSS variables. */
|
|
18
|
+
props?: Record<string, unknown>;
|
|
19
|
+
/** Which custom events to forward. Defaults to `APARTE_DEFAULT_UI_EVENTS`. */
|
|
20
|
+
events?: string[];
|
|
21
|
+
/** Called with the element's own CustomEvent for every forwarded event. */
|
|
22
|
+
onelementEvent?: (event: CustomEvent) => void;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
let hostEl: HTMLElement | undefined = $state();
|
|
26
|
+
let el = $state.raw<HTMLElement | null>(null);
|
|
27
|
+
|
|
28
|
+
// The element is created anew when the tag or the forwarded event set changes; a fresh
|
|
29
|
+
// inline `events` array with the same names does not recreate it.
|
|
30
|
+
const eventsKey = $derived((events ?? APARTE_DEFAULT_UI_EVENTS).join('|'));
|
|
31
|
+
|
|
32
|
+
let mounted = $state(false);
|
|
33
|
+
onMount(() => { mounted = true; });
|
|
34
|
+
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (!mounted || !hostEl) return;
|
|
37
|
+
const tag = name;
|
|
38
|
+
const names = eventsKey.split('|');
|
|
39
|
+
const created = document.createElement(tag);
|
|
40
|
+
const listener = (e: Event) => onelementEvent?.(e as CustomEvent);
|
|
41
|
+
for (const ev of names) created.addEventListener(ev, listener);
|
|
42
|
+
untrack(() => applyElementProps(created, props));
|
|
43
|
+
hostEl.appendChild(created);
|
|
44
|
+
el = created;
|
|
45
|
+
return () => {
|
|
46
|
+
for (const ev of names) created.removeEventListener(ev, listener);
|
|
47
|
+
created.remove();
|
|
48
|
+
if (el === created) el = null;
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
$effect(() => {
|
|
53
|
+
const current = el;
|
|
54
|
+
const next = props;
|
|
55
|
+
if (current) applyElementProps(current, next);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/** The mounted element — the same contract as `AparteUiHandle` on all four wrappers. */
|
|
59
|
+
export function getElement<T extends HTMLElement = HTMLElement>(): T | null {
|
|
60
|
+
return el as T | null;
|
|
61
|
+
}
|
|
62
|
+
export function callMethod<T = unknown>(methodName: string, ...args: unknown[]): T | undefined {
|
|
63
|
+
const fn = (el as unknown as Record<string, unknown>)?.[methodName];
|
|
64
|
+
return (typeof fn === 'function' ? (fn as (...a: unknown[]) => unknown).apply(el, args) : undefined) as T | undefined;
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<span bind:this={hostEl} style="display: contents"></span>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
/** The custom element tag name (e.g. 'aparte-model-selector'). */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Props to apply. Keys starting with `--` become CSS variables. */
|
|
5
|
+
props?: Record<string, unknown>;
|
|
6
|
+
/** Which custom events to forward. Defaults to `APARTE_DEFAULT_UI_EVENTS`. */
|
|
7
|
+
events?: string[];
|
|
8
|
+
/** Called with the element's own CustomEvent for every forwarded event. */
|
|
9
|
+
onelementEvent?: (event: CustomEvent) => void;
|
|
10
|
+
};
|
|
11
|
+
declare const AparteUi: import("svelte").Component<$$ComponentProps, {
|
|
12
|
+
getElement: <T extends HTMLElement = HTMLElement>() => T | null;
|
|
13
|
+
callMethod: <T = unknown>(methodName: string, ...args: unknown[]) => T | undefined;
|
|
14
|
+
}, "">;
|
|
15
|
+
type AparteUi = ReturnType<typeof AparteUi>;
|
|
16
|
+
export default AparteUi;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script lang="ts" generics="V">
|
|
2
|
+
/**
|
|
3
|
+
* One snippet, drawn with one value — what `<AparteToolUI>` and `<AparteSegmentUI>` mount
|
|
4
|
+
* into each element core hands them. Internal: not exported.
|
|
5
|
+
*/
|
|
6
|
+
import type { Snippet } from 'svelte';
|
|
7
|
+
|
|
8
|
+
let { snippet, value }: { snippet: Snippet<[V]>; value: V } = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
{@render snippet(value)}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One snippet, drawn with one value — what `<AparteToolUI>` and `<AparteSegmentUI>` mount
|
|
3
|
+
* into each element core hands them. Internal: not exported.
|
|
4
|
+
*/
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
declare function $$render<V>(): {
|
|
7
|
+
props: {
|
|
8
|
+
snippet: Snippet<[V]>;
|
|
9
|
+
value: V;
|
|
10
|
+
};
|
|
11
|
+
exports: {};
|
|
12
|
+
bindings: "";
|
|
13
|
+
slots: {};
|
|
14
|
+
events: {};
|
|
15
|
+
};
|
|
16
|
+
declare class __sveltets_Render<V> {
|
|
17
|
+
props(): ReturnType<typeof $$render<V>>['props'];
|
|
18
|
+
events(): ReturnType<typeof $$render<V>>['events'];
|
|
19
|
+
slots(): ReturnType<typeof $$render<V>>['slots'];
|
|
20
|
+
bindings(): "";
|
|
21
|
+
exports(): {};
|
|
22
|
+
}
|
|
23
|
+
interface $$IsomorphicComponent {
|
|
24
|
+
new <V>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<V>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<V>['props']>, ReturnType<__sveltets_Render<V>['events']>, ReturnType<__sveltets_Render<V>['slots']>> & {
|
|
25
|
+
$$bindings?: ReturnType<__sveltets_Render<V>['bindings']>;
|
|
26
|
+
} & ReturnType<__sveltets_Render<V>['exports']>;
|
|
27
|
+
<V>(internal: unknown, props: ReturnType<__sveltets_Render<V>['props']> & {}): ReturnType<__sveltets_Render<V>['exports']>;
|
|
28
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
29
|
+
}
|
|
30
|
+
declare const SnippetHost: $$IsomorphicComponent;
|
|
31
|
+
type SnippetHost<V> = InstanceType<typeof SnippetHost<V>>;
|
|
32
|
+
export default SnippetHost;
|