@aparte/svelte 0.23.0 → 0.25.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
package/README.md
CHANGED
|
@@ -4,6 +4,11 @@ The **Svelte AI chat component** of [aparté](https://github.com/apartejs/aparte
|
|
|
4
4
|
`<AparteChat>` (Svelte 4 and 5) plus stores (`createAparteChat`, `createAparteClient`, `createConversationManager`) over the
|
|
5
5
|
framework-agnostic web components in `@aparte/core`.
|
|
6
6
|
|
|
7
|
+
On Svelte 5, import from **`@aparte/svelte/runes`**: the same chat written in runes — snippets,
|
|
8
|
+
`bind:messages`, callback props, and `<AparteToolUI>` to draw a tool call with your own component.
|
|
9
|
+
Tested in CI with Svelte 4 and 5, and in a SvelteKit 2 page rendered on the server and hydrated
|
|
10
|
+
with no mismatch.
|
|
11
|
+
|
|
7
12
|
**Svelte 4 and Svelte 5**, from the same source. The package ships its `.svelte` files
|
|
8
13
|
under the `svelte` export condition, so **your** compiler builds them — how a Svelte
|
|
9
14
|
component library is meant to be published, and the only arrangement that can serve two
|
package/dist/AparteChat.svelte
CHANGED
|
@@ -1,232 +1,306 @@
|
|
|
1
|
-
<script
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export let
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
let
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export let
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export let
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, tick, createEventDispatcher } from 'svelte';
|
|
3
|
+
import { AparteChatHost, aparteGlobalConfig, isAwaitingReply, releaseDeferredHydration, type AparteChatHostBinding, type AparteConfig, type AparteChatImperativeApi, uuid } from '@aparte/core';
|
|
4
|
+
import type { AparteMessage, AparteSegment, AparteSendEventDetail, AparteActionEventDetail } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The host element's `id`, and therefore the `targetId` every event this chat
|
|
8
|
+
* dispatches carries. Generated when omitted — but it used to be generated and
|
|
9
|
+
* neither accepted nor exposed, which made `scopeToTargetId` unreachable from
|
|
10
|
+
* this component.
|
|
11
|
+
*/
|
|
12
|
+
export let id: string | undefined = undefined;
|
|
13
|
+
export let messages: AparteMessage[] = [];
|
|
14
|
+
export let placeholder = 'Type a message...';
|
|
15
|
+
export let disabled = false;
|
|
16
|
+
export let isTyping = false;
|
|
17
|
+
export let typingText: string | undefined = undefined;
|
|
18
|
+
/** When false, Shift+Enter submits and a bare Enter inserts a newline. */
|
|
19
|
+
export let submitOnEnter = true;
|
|
20
|
+
/** Freeze viewport spacer recalculation for this many ms after a conv swap. */
|
|
21
|
+
export let layoutTransitionMs = 0;
|
|
22
|
+
/**
|
|
23
|
+
* Opt in to the "centered composer when empty" layout: the composer sits
|
|
24
|
+
* vertically centered with the `empty-state` slot above it while the list is
|
|
25
|
+
* empty, then slides to the bottom on the first message (~0.3s). Off by
|
|
26
|
+
* default — additive.
|
|
27
|
+
*/
|
|
28
|
+
export let centerWhenEmpty = false;
|
|
29
|
+
/**
|
|
30
|
+
* The conversation is on its way: the viewport draws two skeleton turns, the empty
|
|
31
|
+
* state stays off and the composer is disabled until it lands. The conversation
|
|
32
|
+
* controller sets this itself while it fetches through your adapter's `loadFull()`;
|
|
33
|
+
* the prop is for a wait of your own. Off by default.
|
|
34
|
+
*/
|
|
35
|
+
export let loading = false;
|
|
36
|
+
// The controller's wait (a conversation fetched through the adapter) OR the consumer's.
|
|
37
|
+
let hostLoading = false;
|
|
38
|
+
$: waiting = loading || hostLoading;
|
|
39
|
+
// Bumped on every config change: the labels below are locale text, and `$:` does not see the
|
|
40
|
+
// config — a language switch left them in the old language. Passing the tick is what makes
|
|
41
|
+
// the two statements re-run.
|
|
42
|
+
let localeTick = 0;
|
|
43
|
+
let unsubscribeLocale: (() => void) | null = null;
|
|
44
|
+
const localeText = (_tick: number, cfg: AparteConfig | undefined, key: 'loadingConversation' | 'typing'): string =>
|
|
45
|
+
(cfg ?? aparteGlobalConfig).t(key);
|
|
46
|
+
$: loadingText = localeText(localeTick, config, 'loadingConversation');
|
|
47
|
+
// The status line's word, from the locale like the rest of the chat, unless the prop says.
|
|
48
|
+
$: typingLabel = typingText ?? localeText(localeTick, config, 'typing');
|
|
49
|
+
/** Overlay the composer on the transcript: full-column scroll surface, edge-to-edge scrollbar, floating composer. Read when the viewport mounts. */
|
|
50
|
+
export let overlayComposer = false;
|
|
51
|
+
/**
|
|
52
|
+
* Add the file picker + chips strip to the default composer shell. **Off by
|
|
53
|
+
* default**, because the capability needs a host that consumes the files: an
|
|
54
|
+
* `AparteClient` inlines them per its `rawFileInject` option, but if you drive
|
|
55
|
+
* your own loop you must read `event.files` on `messageSent` — otherwise the file
|
|
56
|
+
* the user deliberately attached is dropped in silence. No effect when you fill
|
|
57
|
+
* the `composer` slot (drop `<aparte-composer-add-attachment>` and
|
|
58
|
+
* `<aparte-composer-attachments>` in it yourself).
|
|
59
|
+
*/
|
|
60
|
+
export let attachments = false;
|
|
61
|
+
/**
|
|
62
|
+
* Render the `<aparte-elicitation>` presenter inside the host — on by default, as it
|
|
63
|
+
* is in `<aparte-chat>`: the built-in approval gate and `requestUserInput()` ask
|
|
64
|
+
* through it, and it renders nothing until something asks. Pass `false` when you
|
|
65
|
+
* mount a presenter of your own (`setElicitationPresenter`) or place the element
|
|
66
|
+
* yourself.
|
|
67
|
+
*/
|
|
68
|
+
export let elicitation = true;
|
|
69
|
+
/** Extra class names for the root element (`[data-aparte-chat]`), merged after core's own. */
|
|
70
|
+
let className = '';
|
|
71
|
+
export { className as class };
|
|
72
|
+
/** Inline style for the root element (`[data-aparte-chat]`). */
|
|
73
|
+
export let style: string | undefined = undefined;
|
|
74
|
+
/** Active conversation id (loads/persists via the registered AparteConversationManager). */
|
|
75
|
+
export let conversationId: string | null = null;
|
|
76
|
+
/**
|
|
77
|
+
* Instance {@link AparteConfig} for this chat. When set, aparté components
|
|
78
|
+
* inside resolve THIS config instead of `aparteGlobalConfig`, so
|
|
79
|
+
* several independently-configured chats can coexist on one page. Omit for the
|
|
80
|
+
* global config. Read once when the host mounts.
|
|
81
|
+
*/
|
|
82
|
+
export let config: AparteConfig | undefined = undefined;
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
* Callback props, called with the payload itself, alongside the events below (#47).
|
|
86
|
+
* Svelte 5 documents `createEventDispatcher` as deprecated and recommends callback
|
|
87
|
+
* props; a Svelte 4 consumer keeps `on:` and sees nothing change, a Svelte 5 one
|
|
88
|
+
* never writes `on:` on a component. Both routes fire for the same occurrence, in
|
|
89
|
+
* this order: the callback, then the event.
|
|
90
|
+
*/
|
|
91
|
+
/** User submitted a message from the composer — see `messageSent` below for the append rule. */
|
|
92
|
+
export let onmessageSent: ((detail: AparteSendEventDetail) => void) | undefined = undefined;
|
|
93
|
+
/** A custom bubble action (registerBubbleAction) was clicked. */
|
|
94
|
+
export let onaction: ((detail: AparteActionEventDetail) => void) | undefined = undefined;
|
|
95
|
+
/** Active path changed — the list to bind back to `messages`. */
|
|
96
|
+
export let onmessagesChange: ((messages: AparteMessage[]) => void) | undefined = undefined;
|
|
97
|
+
/** One message was appended. */
|
|
98
|
+
export let onmessageAppended: ((message: AparteMessage) => void) | undefined = undefined;
|
|
99
|
+
/** The typing/"thinking" indicator toggled. */
|
|
100
|
+
export let ontypingChange: ((typing: boolean) => void) | undefined = undefined;
|
|
101
|
+
/** The host created a conversation id (persistence). */
|
|
102
|
+
export let onconversationCreated: ((id: string) => void) | undefined = undefined;
|
|
103
|
+
|
|
104
|
+
const dispatch = createEventDispatcher<{
|
|
105
|
+
/**
|
|
106
|
+
* User submitted a message from the composer. It is **appended to the
|
|
107
|
+
* thread automatically** (optimistic UI) before this fires — do NOT add it
|
|
108
|
+
* again (uncontrolled → duplicates; controlled → mirror into your own
|
|
109
|
+
* `messages`). For side-effects: scroll, analytics, send.
|
|
110
|
+
*/
|
|
111
|
+
messageSent: AparteSendEventDetail;
|
|
112
|
+
/** A custom bubble action (registerBubbleAction) was clicked — typed aparte-action. */
|
|
113
|
+
action: AparteActionEventDetail;
|
|
114
|
+
/** Active path changed (branch nav/edit/retry/streaming) — bind back to `messages`. */
|
|
115
|
+
messagesChange: AparteMessage[];
|
|
116
|
+
messageAppended: AparteMessage;
|
|
117
|
+
/** The typing/"thinking" indicator toggled (the host flips it off on the first streamed token). */
|
|
118
|
+
typingChange: boolean;
|
|
119
|
+
conversationCreated: string;
|
|
120
|
+
}>();
|
|
121
|
+
|
|
122
|
+
// Generated client-side in onMount (below): during SSR this stays empty so the
|
|
123
|
+
// server and first client render agree on the id — no hydration mismatch.
|
|
124
|
+
let hostId = '';
|
|
125
|
+
|
|
126
|
+
let rootRef: HTMLElement;
|
|
127
|
+
// The server render carries `defer-hydration` on the root, so core's elements leave the
|
|
128
|
+
// server's DOM alone until Svelte has it; onMount releases them. The client never draws it:
|
|
129
|
+
// a client-only render holds nothing, so a slot's component — whose onMount runs before the
|
|
130
|
+
// chat's — talks to live elements. A hydration may take the server's attribute off as it
|
|
131
|
+
// claims the root; the release does not wait for that either.
|
|
132
|
+
const serverRender = typeof window === 'undefined';
|
|
133
|
+
let viewportRef: HTMLElement;
|
|
134
|
+
let composerRef: HTMLElement;
|
|
135
|
+
let internalMessages: AparteMessage[] = [...messages];
|
|
136
|
+
let typingActive = isTyping;
|
|
137
|
+
let host: AparteChatHost | null = null;
|
|
138
|
+
let teardown: (() => void) | null = null;
|
|
139
|
+
|
|
140
|
+
// Parent push → internal list, unless it is the host's own report coming back: a parent
|
|
141
|
+
// that lands late hands back an EARLIER one, and applying it rolled the list back under the
|
|
142
|
+
// host's next write. The host tells them apart (`takeEcho`), so a list the parent restores
|
|
143
|
+
// later (a reopened conversation) is not mistaken for one.
|
|
144
|
+
let lastProp = messages;
|
|
145
|
+
$: if (messages !== lastProp) {
|
|
146
|
+
lastProp = messages;
|
|
147
|
+
if (!host?.takeEcho(messages)) {
|
|
148
|
+
internalMessages = [...messages];
|
|
149
|
+
if (messages.length === 0) host?.clearRenderCache();
|
|
150
|
+
}
|
|
50
151
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
lastTyping = isTyping;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
let lastConv = conversationId;
|
|
58
|
-
$: if (conversationId !== lastConv) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
viewportRef
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
152
|
+
|
|
153
|
+
// Controlled typing indicator (host may flip it off on the first token).
|
|
154
|
+
let lastTyping = isTyping;
|
|
155
|
+
$: if (isTyping !== lastTyping) { lastTyping = isTyping; typingActive = isTyping; }
|
|
156
|
+
|
|
157
|
+
// Conversation id changes (initial value loaded by the host on bind).
|
|
158
|
+
let lastConv = conversationId;
|
|
159
|
+
$: if (conversationId !== lastConv) {
|
|
160
|
+
lastConv = conversationId;
|
|
161
|
+
void host?.setConversationId(conversationId ?? null);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Reconcile bubbles after the rendered list changes (host queries the DOM).
|
|
165
|
+
$: if (host && internalMessages) { void tick().then(() => host?.syncBubbles()); }
|
|
166
|
+
|
|
167
|
+
// Composer attributes set imperatively (like Angular's `[attr.x]`), because the
|
|
168
|
+
// attribute IS the state core's composer parts read. Svelte's custom-element
|
|
169
|
+
// binding assigns to *properties*; core gained the matching setters in 0.16.12
|
|
170
|
+
// (before that `placeholder`/`disabled` were getter-only and assigning threw), so
|
|
171
|
+
// this is now a direct write rather than a way around a crash.
|
|
172
|
+
function toggleAttr(el: HTMLElement, name: string, on: boolean, value: string) {
|
|
173
|
+
if (on) el.setAttribute(name, value); else el.removeAttribute(name);
|
|
174
|
+
}
|
|
175
|
+
// The viewport's `loading` too: Svelte 5 sets a template attribute as a PROPERTY when
|
|
176
|
+
// the element has one, and `''` through the setter reads as false — the attribute
|
|
177
|
+
// never appeared (Svelte 4 set the attribute; the svelte5 example caught the drift).
|
|
178
|
+
$: if (viewportRef) toggleAttr(viewportRef, 'loading', waiting, '');
|
|
179
|
+
$: if (composerRef) {
|
|
180
|
+
composerRef.setAttribute('target', hostId);
|
|
181
|
+
composerRef.setAttribute('placeholder', placeholder);
|
|
182
|
+
toggleAttr(composerRef, 'disabled', disabled || waiting, '');
|
|
183
|
+
toggleAttr(composerRef, 'submit-on-enter', !submitOnEnter, 'false');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function handleSend(event: Event) {
|
|
187
|
+
(viewportRef as unknown as { requestSmoothScroll?: () => void })?.requestSmoothScroll?.();
|
|
188
|
+
const detail = (event as CustomEvent<AparteSendEventDetail>).detail;
|
|
189
|
+
onmessageSent?.(detail);
|
|
190
|
+
dispatch('messageSent', detail);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Custom bubble actions bubble to the root as `aparte-action` — dispatch typed.
|
|
194
|
+
function handleAction(event: Event) {
|
|
195
|
+
const detail = (event as CustomEvent<AparteActionEventDetail>).detail;
|
|
196
|
+
onaction?.(detail);
|
|
197
|
+
dispatch('action', detail);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
onMount(() => {
|
|
201
|
+
// The elements a server render left waiting are upgraded first: the host talks to them.
|
|
202
|
+
releaseDeferredHydration(rootRef);
|
|
203
|
+
unsubscribeLocale = (config ?? aparteGlobalConfig).subscribe(() => { localeTick += 1; });
|
|
204
|
+
// A caller-supplied `id` wins, so `scopeToTargetId` has something to match.
|
|
205
|
+
hostId = id ?? `aparte-chat-${uuid()}`;
|
|
206
|
+
// Set the id imperatively (deterministic, like Angular's ngAfterViewInit) rather
|
|
207
|
+
// than waiting on a reactive re-render of `id={hostId}`; the composer target
|
|
208
|
+
// reactive block below picks up the same hostId.
|
|
209
|
+
if (rootRef) rootRef.id = hostId;
|
|
210
|
+
const binding: AparteChatHostBinding = {
|
|
211
|
+
hostId,
|
|
212
|
+
host: rootRef,
|
|
213
|
+
viewport: viewportRef,
|
|
214
|
+
getMessages: () => internalMessages,
|
|
215
|
+
setMessages: (m) => { internalMessages = m as AparteMessage[]; },
|
|
216
|
+
onMessagesChange: (m) => {
|
|
217
|
+
onmessagesChange?.(m as AparteMessage[]);
|
|
218
|
+
dispatch('messagesChange', m as AparteMessage[]);
|
|
219
|
+
},
|
|
220
|
+
onMessageAppended: (m) => { onmessageAppended?.(m as AparteMessage); dispatch('messageAppended', m as AparteMessage); },
|
|
221
|
+
onTypingChange: (t) => { typingActive = t; ontypingChange?.(t); dispatch('typingChange', t); },
|
|
222
|
+
onStreamingChange: () => { /* exposed via isStreaming() */ },
|
|
223
|
+
afterRender: (cb) => { void tick().then(cb); },
|
|
224
|
+
resetComposer: () => (composerRef as unknown as { reset?: () => void })?.reset?.(),
|
|
225
|
+
onLoadingChange: (on) => { hostLoading = on; },
|
|
226
|
+
};
|
|
227
|
+
host = new AparteChatHost(binding, {
|
|
228
|
+
layoutTransitionMs,
|
|
229
|
+
conversationId: conversationId ?? null,
|
|
230
|
+
onConversationCreated: (id) => { onconversationCreated?.(id); dispatch('conversationCreated', id); },
|
|
231
|
+
config,
|
|
232
|
+
});
|
|
233
|
+
teardown = host.bind();
|
|
234
|
+
host.syncBubbles();
|
|
235
|
+
rootRef?.addEventListener('aparte-action', handleAction);
|
|
90
236
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
internalMessages = m;
|
|
100
|
-
},
|
|
101
|
-
onMessagesChange: (m) => {
|
|
102
|
-
emitted.add(m);
|
|
103
|
-
onmessagesChange?.(m);
|
|
104
|
-
dispatch("messagesChange", m);
|
|
105
|
-
},
|
|
106
|
-
onMessageAppended: (m) => {
|
|
107
|
-
onmessageAppended?.(m);
|
|
108
|
-
dispatch("messageAppended", m);
|
|
109
|
-
},
|
|
110
|
-
onTypingChange: (t) => {
|
|
111
|
-
typingActive = t;
|
|
112
|
-
ontypingChange?.(t);
|
|
113
|
-
dispatch("typingChange", t);
|
|
114
|
-
},
|
|
115
|
-
onStreamingChange: () => {
|
|
116
|
-
},
|
|
117
|
-
afterRender: (cb) => {
|
|
118
|
-
void tick().then(cb);
|
|
119
|
-
},
|
|
120
|
-
resetComposer: () => composerRef?.reset?.(),
|
|
121
|
-
onLoadingChange: (on) => {
|
|
122
|
-
hostLoading = on;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
host = new AparteChatHost(binding, {
|
|
126
|
-
layoutTransitionMs,
|
|
127
|
-
conversationId: conversationId ?? null,
|
|
128
|
-
onConversationCreated: (id2) => {
|
|
129
|
-
onconversationCreated?.(id2);
|
|
130
|
-
dispatch("conversationCreated", id2);
|
|
131
|
-
},
|
|
132
|
-
config
|
|
237
|
+
|
|
238
|
+
onDestroy(() => {
|
|
239
|
+
unsubscribeLocale?.();
|
|
240
|
+
unsubscribeLocale = null;
|
|
241
|
+
rootRef?.removeEventListener('aparte-action', handleAction);
|
|
242
|
+
teardown?.();
|
|
243
|
+
teardown = null;
|
|
244
|
+
host = null;
|
|
133
245
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
export function
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
export function
|
|
150
|
-
host?.
|
|
151
|
-
}
|
|
152
|
-
export function
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
export function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
host?.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return host?.setConversationId(id2) ?? Promise.resolve();
|
|
193
|
-
}
|
|
194
|
-
export function scrollToBottom() {
|
|
195
|
-
viewportRef?.scrollToBottom?.();
|
|
196
|
-
}
|
|
197
|
-
export function getViewport() {
|
|
198
|
-
return viewportRef ?? null;
|
|
199
|
-
}
|
|
200
|
-
export function focusInput() {
|
|
201
|
-
composerRef?.focus?.();
|
|
202
|
-
}
|
|
203
|
-
export function isStreaming() {
|
|
204
|
-
return host?.isStreaming ?? false;
|
|
205
|
-
}
|
|
206
|
-
function _assertImperativeParity() {
|
|
207
|
-
return {
|
|
208
|
-
appendMessage,
|
|
209
|
-
updateMessage,
|
|
210
|
-
updateLastMessage,
|
|
211
|
-
addSegment,
|
|
212
|
-
updateSegment,
|
|
213
|
-
removeSegment,
|
|
214
|
-
appendToSegment,
|
|
215
|
-
getMessages,
|
|
216
|
-
clearMessages,
|
|
217
|
-
addBranch,
|
|
218
|
-
addSiblingOf,
|
|
219
|
-
truncateFrom,
|
|
220
|
-
truncateResponsesAfter,
|
|
221
|
-
injectTokenStream,
|
|
222
|
-
stopTokenStream,
|
|
223
|
-
setConversationId,
|
|
224
|
-
scrollToBottom,
|
|
225
|
-
focusInput,
|
|
226
|
-
isStreaming,
|
|
227
|
-
getViewport
|
|
228
|
-
};
|
|
229
|
-
}
|
|
246
|
+
|
|
247
|
+
// ── Imperative API (bind:this on the component) ──
|
|
248
|
+
export function appendMessage(m: AparteMessage, options?: { historical?: boolean }) { host?.appendMessage(m, options); }
|
|
249
|
+
export function updateMessage(id: string, u: Partial<AparteMessage>) { host?.updateMessage(id, u); }
|
|
250
|
+
export function updateLastMessage(content: string, options?: { append?: boolean }) {
|
|
251
|
+
host?.updateLastMessage(content, options);
|
|
252
|
+
}
|
|
253
|
+
export function addSegment(segment: AparteSegment) { host?.addSegment(segment); }
|
|
254
|
+
export function updateSegment(segmentId: string, updates: Partial<AparteSegment>) {
|
|
255
|
+
host?.updateSegment(segmentId, updates);
|
|
256
|
+
}
|
|
257
|
+
export function removeSegment(segmentId: string) { host?.removeSegment(segmentId); }
|
|
258
|
+
export function appendToSegment(segmentId: string, content: string) {
|
|
259
|
+
host?.appendToSegment(segmentId, content);
|
|
260
|
+
}
|
|
261
|
+
export function getMessages(): AparteMessage[] { return host?.getMessages() ?? internalMessages; }
|
|
262
|
+
export function clearMessages(options?: { revokeAttachments?: boolean }) { host?.clearMessages(options); }
|
|
263
|
+
export function addBranch(messageId: string): number { return host?.addBranch(messageId) ?? 0; }
|
|
264
|
+
export function addSiblingOf(existingId: string, message: AparteMessage): string | null {
|
|
265
|
+
return host?.addSiblingOf(existingId, message) ?? null;
|
|
266
|
+
}
|
|
267
|
+
export function truncateFrom(messageId: string) { host?.truncateFrom(messageId); }
|
|
268
|
+
export function truncateResponsesAfter(userMessageId: string) {
|
|
269
|
+
host?.truncateResponsesAfter(userMessageId);
|
|
270
|
+
}
|
|
271
|
+
export function injectTokenStream(messageId: string, tokens: AsyncIterable<string>): Promise<void> {
|
|
272
|
+
return host?.streamTokens(messageId, tokens) ?? Promise.resolve();
|
|
273
|
+
}
|
|
274
|
+
export function stopTokenStream() { host?.stopTokenStream(); }
|
|
275
|
+
export function setConversationId(id: string | null): Promise<void> {
|
|
276
|
+
return host?.setConversationId(id) ?? Promise.resolve();
|
|
277
|
+
}
|
|
278
|
+
export function scrollToBottom() {
|
|
279
|
+
(viewportRef as unknown as { scrollToBottom?: () => void })?.scrollToBottom?.();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The `<aparte-chat-viewport>` element — for custom scroll handling, an
|
|
283
|
+
* IntersectionObserver, etc. Same `getViewport()` accessor on all four
|
|
284
|
+
* wrappers.
|
|
285
|
+
*/
|
|
286
|
+
export function getViewport(): HTMLElement | null { return viewportRef ?? null; }
|
|
287
|
+
export function focusInput() {
|
|
288
|
+
(composerRef as unknown as { focus?: () => void })?.focus?.();
|
|
289
|
+
}
|
|
290
|
+
export function isStreaming(): boolean { return host?.isStreaming ?? false; }
|
|
291
|
+
|
|
292
|
+
// Compile-time parity check: Svelte 4 can't generic-annotate `export function`s,
|
|
293
|
+
// so this never-called factory type-checks that the exported surface matches the
|
|
294
|
+
// canonical AparteChatImperativeApi. A dropped/mistyped method is a build error.
|
|
295
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
296
|
+
function _assertImperativeParity(): AparteChatImperativeApi {
|
|
297
|
+
return {
|
|
298
|
+
appendMessage, updateMessage, updateLastMessage, addSegment, updateSegment, removeSegment,
|
|
299
|
+
appendToSegment, getMessages, clearMessages, addBranch, addSiblingOf, truncateFrom,
|
|
300
|
+
truncateResponsesAfter, injectTokenStream, stopTokenStream, setConversationId,
|
|
301
|
+
scrollToBottom, focusInput, isStreaming, getViewport,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
230
304
|
</script>
|
|
231
305
|
|
|
232
306
|
<div
|
|
@@ -234,6 +308,7 @@ function _assertImperativeParity() {
|
|
|
234
308
|
class:aparte-chat-container--auto-center={centerWhenEmpty}
|
|
235
309
|
{style}
|
|
236
310
|
data-aparte-chat
|
|
311
|
+
{...(serverRender ? { 'defer-hydration': '' } : {})}
|
|
237
312
|
{...(overlayComposer ? { 'overlay-composer': '' } : {})}
|
|
238
313
|
data-aparte-empty={centerWhenEmpty && internalMessages.length === 0 && !waiting ? '' : null}
|
|
239
314
|
id={hostId}
|