@aparte/core 0.3.0-alpha.0 → 0.4.0-alpha.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 +21 -0
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +11 -0
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/chat/aparte-chat.d.ts +13 -0
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +13 -2
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +10 -2
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/icon-provider.d.ts +21 -21
- package/dist/config/icon-provider.d.ts.map +1 -1
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/custom-elements.json +7604 -7360
- package/dist/host/aparte-chat-host.d.ts +47 -1
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/{index-CW2BCAqn.js → index-BBg6dsYx.js} +164 -41
- package/dist/index-BBg6dsYx.js.map +1 -0
- package/dist/index.css +60 -6
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +122 -26
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +2 -0
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +16 -14
- package/dist/index.node.js.map +1 -1
- package/dist/primitives/select/aparte-optgroup.d.ts +2 -0
- package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts +2 -0
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/types/imperative-api.d.ts +13 -0
- package/dist/types/imperative-api.d.ts.map +1 -1
- package/dist/utils/files-to-attachments.d.ts +27 -0
- package/dist/utils/files-to-attachments.d.ts.map +1 -0
- package/dist/utils/is-awaiting-reply.d.ts +22 -0
- package/dist/utils/is-awaiting-reply.d.ts.map +1 -0
- package/package.json +8 -1
- package/dist/index-CW2BCAqn.js.map +0 -1
|
@@ -78,6 +78,12 @@ export declare class AparteChatHost {
|
|
|
78
78
|
private _pendingSiblings?;
|
|
79
79
|
/** Abort handle for an in-flight {@link streamTokens} loop. */
|
|
80
80
|
private _streamAbort?;
|
|
81
|
+
/** Segment chunks written to the bubble, awaiting one coalesced state sync. */
|
|
82
|
+
private readonly _segmentBuffer;
|
|
83
|
+
/** {@link streamTokens} text written to the bubble, awaiting the same sync. */
|
|
84
|
+
private _tokenBuffer;
|
|
85
|
+
/** Handle of the scheduled coalesced sync, or null when none is pending. */
|
|
86
|
+
private _flushHandle;
|
|
81
87
|
private _conversationId;
|
|
82
88
|
private _controller?;
|
|
83
89
|
private _teardown;
|
|
@@ -129,8 +135,35 @@ export declare class AparteChatHost {
|
|
|
129
135
|
updateSegment(segmentId: string, updates: Partial<AparteSegment>): void;
|
|
130
136
|
/** Remove a transient segment (e.g. pipeline-waiting indicator). */
|
|
131
137
|
removeSegment(segmentId: string): void;
|
|
132
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* Append text to a segment's content in the last message — the streaming path
|
|
140
|
+
* for thinking blocks, tool pills and any segment that grows token by token.
|
|
141
|
+
*
|
|
142
|
+
* Two writes, deliberately asymmetric:
|
|
143
|
+
* 1. straight into the bubble, every chunk, so the text appears immediately —
|
|
144
|
+
* the same direct path `appendToken` uses;
|
|
145
|
+
* 2. into the framework's message list, **coalesced to once per frame**.
|
|
146
|
+
*
|
|
147
|
+
* It used to do only (2), synchronously: a full list rebuild plus
|
|
148
|
+
* `setMessages` + `onMessagesChange` for every chunk, i.e. one complete
|
|
149
|
+
* framework render per token. At 60 tok/s from a local model that is unusable,
|
|
150
|
+
* and nothing in the imperative API hinted that `appendToSegment` cost so much
|
|
151
|
+
* more than `injectTokenStream`. Consumers had to write their own rAF batcher.
|
|
152
|
+
*
|
|
153
|
+
* Any structural change flushes the buffer first (see
|
|
154
|
+
* {@link _flushStreamState}), so ordering is never observable.
|
|
155
|
+
*/
|
|
133
156
|
appendToSegment(segmentId: string, content: string): void;
|
|
157
|
+
/** Schedule the coalesced segment-state sync (idempotent within a frame). */
|
|
158
|
+
private _scheduleStreamFlush;
|
|
159
|
+
/**
|
|
160
|
+
* Write every buffered stream chunk — segment chunks and {@link streamTokens}
|
|
161
|
+
* text alike — into the framework's message list in **one** update. Called by
|
|
162
|
+
* the scheduled frame, and eagerly by any mutation that would otherwise observe
|
|
163
|
+
* a stale list (a new segment, a new message, a conversation swap): flushing
|
|
164
|
+
* first keeps the emitted order identical to the call order.
|
|
165
|
+
*/
|
|
166
|
+
private _flushStreamState;
|
|
134
167
|
/** Read the current message list. */
|
|
135
168
|
getMessages(): AparteMessage[];
|
|
136
169
|
/** Clear all messages + reset viewport state. */
|
|
@@ -148,8 +181,21 @@ export declare class AparteChatHost {
|
|
|
148
181
|
* appends each token, completes the message at the end. Per-framework
|
|
149
182
|
* wrappers adapt their native stream type (RxJS Observable, AsyncIterable,
|
|
150
183
|
* ReadableStream) into this.
|
|
184
|
+
*
|
|
185
|
+
* Each token reaches the bubble immediately and the framework's message list
|
|
186
|
+
* is synced **once per frame** (same discipline as {@link appendToSegment}),
|
|
187
|
+
* with a guaranteed flush before completion and on abort. Before that sync
|
|
188
|
+
* existed the two disagreed: the DOM had the reply, the framework list still
|
|
189
|
+
* held `content: ''` — so any re-render from state wiped it, and a custom
|
|
190
|
+
* bubble (which renders from that state) showed nothing at all.
|
|
151
191
|
*/
|
|
152
192
|
streamTokens(messageId: string, tokens: AsyncIterable<string>): Promise<void>;
|
|
193
|
+
/**
|
|
194
|
+
* Buffer a streamed token for the coalesced state sync. A different message id
|
|
195
|
+
* (a second stream started without stopping the first) flushes the previous
|
|
196
|
+
* one, so text never migrates between messages.
|
|
197
|
+
*/
|
|
198
|
+
private _bufferToken;
|
|
153
199
|
/** Abort an in-flight {@link streamTokens} loop. */
|
|
154
200
|
stopTokenStream(): void;
|
|
155
201
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aparte-chat-host.d.ts","sourceRoot":"","sources":["../../src/host/aparte-chat-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,aAAa,EACb,aAAa,EAIhB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EACH,4BAA4B,EAE/B,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"aparte-chat-host.d.ts","sourceRoot":"","sources":["../../src/host/aparte-chat-host.ts"],"names":[],"mappings":"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"}
|
|
@@ -1165,13 +1165,24 @@ class AparteConfigClass {
|
|
|
1165
1165
|
return this._artifactPreviewBuilder;
|
|
1166
1166
|
}
|
|
1167
1167
|
/**
|
|
1168
|
-
*
|
|
1168
|
+
* The icon set as a **complete** provider: every name resolves, falling back
|
|
1169
|
+
* to `DEFAULT_ICON_FALLBACKS` for anything the registered provider doesn't
|
|
1170
|
+
* implement. Callers (bubble action bar, composer controls) can therefore
|
|
1171
|
+
* invoke `icons.copy()` unconditionally.
|
|
1172
|
+
*
|
|
1173
|
+
* It used to hand back the registered provider verbatim, so a provider that
|
|
1174
|
+
* implemented a subset — which `getIcon()` has always supported, and which
|
|
1175
|
+
* the type now states — crashed every other icon with
|
|
1176
|
+
* "icons.retry is not a function".
|
|
1169
1177
|
*/
|
|
1170
1178
|
getIconProvider() {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1179
|
+
const registered = this._iconProvider;
|
|
1180
|
+
const complete = {};
|
|
1181
|
+
for (const name of Object.keys(DEFAULT_ICON_FALLBACKS)) {
|
|
1182
|
+
const fn = registered?.[name];
|
|
1183
|
+
complete[name] = fn ?? (() => DEFAULT_ICON_FALLBACKS[name]);
|
|
1184
|
+
}
|
|
1185
|
+
return complete;
|
|
1175
1186
|
}
|
|
1176
1187
|
/**
|
|
1177
1188
|
* Set a custom avatar renderer. Lets framework consumers (Angular,
|
|
@@ -1296,9 +1307,8 @@ class AparteConfigClass {
|
|
|
1296
1307
|
* Falls back to textual representation if no provider is set
|
|
1297
1308
|
*/
|
|
1298
1309
|
getIcon(name) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1310
|
+
const icon = this._iconProvider?.[name];
|
|
1311
|
+
if (icon) return icon();
|
|
1302
1312
|
return DEFAULT_ICON_FALLBACKS[name];
|
|
1303
1313
|
}
|
|
1304
1314
|
/**
|
|
@@ -3319,6 +3329,19 @@ class ConversationManager {
|
|
|
3319
3329
|
return text.trim() || "New Chat";
|
|
3320
3330
|
}
|
|
3321
3331
|
}
|
|
3332
|
+
function attachmentId() {
|
|
3333
|
+
return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3334
|
+
}
|
|
3335
|
+
function filesToAttachments(files) {
|
|
3336
|
+
return files.map((file) => ({
|
|
3337
|
+
id: attachmentId(),
|
|
3338
|
+
name: file.name,
|
|
3339
|
+
type: file.type || "application/octet-stream",
|
|
3340
|
+
url: URL.createObjectURL(file),
|
|
3341
|
+
size: file.size,
|
|
3342
|
+
blob: file
|
|
3343
|
+
}));
|
|
3344
|
+
}
|
|
3322
3345
|
class AparteConversationController {
|
|
3323
3346
|
_binding;
|
|
3324
3347
|
_options;
|
|
@@ -3363,16 +3386,7 @@ class AparteConversationController {
|
|
|
3363
3386
|
if (targetId && targetId !== this._binding.hostId) return;
|
|
3364
3387
|
const content = evt.detail?.content ?? "";
|
|
3365
3388
|
const files = evt.detail?.files;
|
|
3366
|
-
const attachments = files?.length ? files
|
|
3367
|
-
id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`,
|
|
3368
|
-
name: f.name,
|
|
3369
|
-
type: f.type || "application/octet-stream",
|
|
3370
|
-
url: URL.createObjectURL(f),
|
|
3371
|
-
size: f.size,
|
|
3372
|
-
// Carry the raw File so the storage adapter can persist it
|
|
3373
|
-
// to its attachments table; reload reconstructs `url`.
|
|
3374
|
-
blob: f
|
|
3375
|
-
})) : void 0;
|
|
3389
|
+
const attachments = files?.length ? filesToAttachments(files) : void 0;
|
|
3376
3390
|
const userMsg = {
|
|
3377
3391
|
id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`,
|
|
3378
3392
|
role: "user",
|
|
@@ -3622,6 +3636,12 @@ class AparteChatHost {
|
|
|
3622
3636
|
_pendingSiblings;
|
|
3623
3637
|
/** Abort handle for an in-flight {@link streamTokens} loop. */
|
|
3624
3638
|
_streamAbort;
|
|
3639
|
+
/** Segment chunks written to the bubble, awaiting one coalesced state sync. */
|
|
3640
|
+
_segmentBuffer = /* @__PURE__ */ new Map();
|
|
3641
|
+
/** {@link streamTokens} text written to the bubble, awaiting the same sync. */
|
|
3642
|
+
_tokenBuffer = null;
|
|
3643
|
+
/** Handle of the scheduled coalesced sync, or null when none is pending. */
|
|
3644
|
+
_flushHandle = null;
|
|
3625
3645
|
_conversationId = null;
|
|
3626
3646
|
_controller;
|
|
3627
3647
|
_teardown = [];
|
|
@@ -3734,6 +3754,9 @@ class AparteChatHost {
|
|
|
3734
3754
|
}
|
|
3735
3755
|
/** Drop transient streaming state so a new conversation starts clean. */
|
|
3736
3756
|
_beginConversationSwap() {
|
|
3757
|
+
this._segmentBuffer.clear();
|
|
3758
|
+
this._tokenBuffer = null;
|
|
3759
|
+
this._flushStreamState();
|
|
3737
3760
|
this.stopTokenStream();
|
|
3738
3761
|
this._setStreamingId(null);
|
|
3739
3762
|
this.binding.onTypingChange?.(false);
|
|
@@ -3755,12 +3778,14 @@ class AparteChatHost {
|
|
|
3755
3778
|
* is the append-specific signal instead.
|
|
3756
3779
|
*/
|
|
3757
3780
|
appendMessage(message) {
|
|
3781
|
+
this._flushStreamState();
|
|
3758
3782
|
if (message.role === "user") this._vp()?.setAutoScroll?.(true);
|
|
3759
3783
|
this.binding.setMessages([...this.binding.getMessages(), message]);
|
|
3760
3784
|
this.binding.onMessageAppended?.(message);
|
|
3761
3785
|
}
|
|
3762
3786
|
/** Atomic partial update of a message by id. */
|
|
3763
3787
|
updateMessage(messageId, updates) {
|
|
3788
|
+
this._flushStreamState();
|
|
3764
3789
|
const msgs = this.binding.getMessages();
|
|
3765
3790
|
const index = msgs.findIndex((m) => m.id === messageId);
|
|
3766
3791
|
if (index === -1) return;
|
|
@@ -3786,6 +3811,7 @@ class AparteChatHost {
|
|
|
3786
3811
|
}
|
|
3787
3812
|
/** Add a segment to the last message (immediate bubble + state sync). */
|
|
3788
3813
|
addSegment(segment) {
|
|
3814
|
+
this._flushStreamState();
|
|
3789
3815
|
const msgs = this.binding.getMessages();
|
|
3790
3816
|
const last = msgs[msgs.length - 1];
|
|
3791
3817
|
if (last && this._isOrphan(last.id)) return;
|
|
@@ -3799,6 +3825,7 @@ class AparteChatHost {
|
|
|
3799
3825
|
}
|
|
3800
3826
|
/** Update a segment in the last message in place. */
|
|
3801
3827
|
updateSegment(segmentId, updates) {
|
|
3828
|
+
this._flushStreamState();
|
|
3802
3829
|
const msgs = this.binding.getMessages();
|
|
3803
3830
|
const last = msgs[msgs.length - 1];
|
|
3804
3831
|
if (last && this._isOrphan(last.id)) return;
|
|
@@ -3813,6 +3840,7 @@ class AparteChatHost {
|
|
|
3813
3840
|
}
|
|
3814
3841
|
/** Remove a transient segment (e.g. pipeline-waiting indicator). */
|
|
3815
3842
|
removeSegment(segmentId) {
|
|
3843
|
+
this._flushStreamState();
|
|
3816
3844
|
this._lastBubble()?.removeSegment?.(segmentId);
|
|
3817
3845
|
const msgs = this.binding.getMessages();
|
|
3818
3846
|
const last = msgs[msgs.length - 1];
|
|
@@ -3823,18 +3851,85 @@ class AparteChatHost {
|
|
|
3823
3851
|
this.binding.setMessages(next);
|
|
3824
3852
|
this.binding.onMessagesChange?.(next);
|
|
3825
3853
|
}
|
|
3826
|
-
/**
|
|
3854
|
+
/**
|
|
3855
|
+
* Append text to a segment's content in the last message — the streaming path
|
|
3856
|
+
* for thinking blocks, tool pills and any segment that grows token by token.
|
|
3857
|
+
*
|
|
3858
|
+
* Two writes, deliberately asymmetric:
|
|
3859
|
+
* 1. straight into the bubble, every chunk, so the text appears immediately —
|
|
3860
|
+
* the same direct path `appendToken` uses;
|
|
3861
|
+
* 2. into the framework's message list, **coalesced to once per frame**.
|
|
3862
|
+
*
|
|
3863
|
+
* It used to do only (2), synchronously: a full list rebuild plus
|
|
3864
|
+
* `setMessages` + `onMessagesChange` for every chunk, i.e. one complete
|
|
3865
|
+
* framework render per token. At 60 tok/s from a local model that is unusable,
|
|
3866
|
+
* and nothing in the imperative API hinted that `appendToSegment` cost so much
|
|
3867
|
+
* more than `injectTokenStream`. Consumers had to write their own rAF batcher.
|
|
3868
|
+
*
|
|
3869
|
+
* Any structural change flushes the buffer first (see
|
|
3870
|
+
* {@link _flushStreamState}), so ordering is never observable.
|
|
3871
|
+
*/
|
|
3827
3872
|
appendToSegment(segmentId, content) {
|
|
3828
3873
|
const msgs = this.binding.getMessages();
|
|
3829
3874
|
const last = msgs[msgs.length - 1];
|
|
3830
3875
|
if (!last || !last.segments) return;
|
|
3831
3876
|
if (this._isOrphan(last.id)) return;
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
);
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3877
|
+
this._vp()?.appendToSegment?.(last.id, segmentId, content);
|
|
3878
|
+
this._segmentBuffer.set(segmentId, (this._segmentBuffer.get(segmentId) ?? "") + content);
|
|
3879
|
+
this._scheduleStreamFlush();
|
|
3880
|
+
}
|
|
3881
|
+
/** Schedule the coalesced segment-state sync (idempotent within a frame). */
|
|
3882
|
+
_scheduleStreamFlush() {
|
|
3883
|
+
if (this._flushHandle !== null) return;
|
|
3884
|
+
const run = () => {
|
|
3885
|
+
this._flushHandle = null;
|
|
3886
|
+
this._flushStreamState();
|
|
3887
|
+
};
|
|
3888
|
+
this._flushHandle = typeof requestAnimationFrame === "function" ? requestAnimationFrame(run) : setTimeout(run, 0);
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Write every buffered stream chunk — segment chunks and {@link streamTokens}
|
|
3892
|
+
* text alike — into the framework's message list in **one** update. Called by
|
|
3893
|
+
* the scheduled frame, and eagerly by any mutation that would otherwise observe
|
|
3894
|
+
* a stale list (a new segment, a new message, a conversation swap): flushing
|
|
3895
|
+
* first keeps the emitted order identical to the call order.
|
|
3896
|
+
*/
|
|
3897
|
+
_flushStreamState() {
|
|
3898
|
+
if (this._flushHandle !== null) {
|
|
3899
|
+
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(this._flushHandle);
|
|
3900
|
+
else clearTimeout(this._flushHandle);
|
|
3901
|
+
this._flushHandle = null;
|
|
3902
|
+
}
|
|
3903
|
+
const tokens = this._tokenBuffer;
|
|
3904
|
+
const pending = this._segmentBuffer.size > 0 ? new Map(this._segmentBuffer) : null;
|
|
3905
|
+
if (!tokens && !pending) return;
|
|
3906
|
+
this._tokenBuffer = null;
|
|
3907
|
+
this._segmentBuffer.clear();
|
|
3908
|
+
let msgs = this.binding.getMessages();
|
|
3909
|
+
let changed = false;
|
|
3910
|
+
if (tokens) {
|
|
3911
|
+
const index = msgs.findIndex((m) => m.id === tokens.messageId);
|
|
3912
|
+
if (index !== -1) {
|
|
3913
|
+
const next = [...msgs];
|
|
3914
|
+
next[index] = { ...next[index], content: (next[index].content ?? "") + tokens.text };
|
|
3915
|
+
msgs = next;
|
|
3916
|
+
changed = true;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
if (pending) {
|
|
3920
|
+
const last = msgs[msgs.length - 1];
|
|
3921
|
+
if (last?.segments) {
|
|
3922
|
+
const segments = last.segments.map((s) => {
|
|
3923
|
+
const chunk = pending.get(s.id);
|
|
3924
|
+
return chunk !== void 0 && "content" in s ? { ...s, content: s.content + chunk } : s;
|
|
3925
|
+
});
|
|
3926
|
+
msgs = [...msgs.slice(0, -1), { ...last, segments }];
|
|
3927
|
+
changed = true;
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
if (!changed) return;
|
|
3931
|
+
this.binding.setMessages(msgs);
|
|
3932
|
+
this.binding.onMessagesChange?.(msgs);
|
|
3838
3933
|
}
|
|
3839
3934
|
/** Read the current message list. */
|
|
3840
3935
|
getMessages() {
|
|
@@ -3880,6 +3975,13 @@ class AparteChatHost {
|
|
|
3880
3975
|
* appends each token, completes the message at the end. Per-framework
|
|
3881
3976
|
* wrappers adapt their native stream type (RxJS Observable, AsyncIterable,
|
|
3882
3977
|
* ReadableStream) into this.
|
|
3978
|
+
*
|
|
3979
|
+
* Each token reaches the bubble immediately and the framework's message list
|
|
3980
|
+
* is synced **once per frame** (same discipline as {@link appendToSegment}),
|
|
3981
|
+
* with a guaranteed flush before completion and on abort. Before that sync
|
|
3982
|
+
* existed the two disagreed: the DOM had the reply, the framework list still
|
|
3983
|
+
* held `content: ''` — so any re-render from state wiped it, and a custom
|
|
3984
|
+
* bubble (which renders from that state) showed nothing at all.
|
|
3883
3985
|
*/
|
|
3884
3986
|
async streamTokens(messageId, tokens) {
|
|
3885
3987
|
this.stopTokenStream();
|
|
@@ -3899,10 +4001,13 @@ class AparteChatHost {
|
|
|
3899
4001
|
const res = await Promise.race([iterator.next(), aborted]);
|
|
3900
4002
|
if (res === "aborted" || ac.signal.aborted || res.done) break;
|
|
3901
4003
|
this._vp()?.appendToken?.(messageId, res.value);
|
|
4004
|
+
this._bufferToken(messageId, res.value);
|
|
3902
4005
|
}
|
|
4006
|
+
this._flushStreamState();
|
|
3903
4007
|
if (!ac.signal.aborted) this._vp()?.completeMessage?.(messageId);
|
|
3904
4008
|
this._setStreamingId(null);
|
|
3905
4009
|
} catch (err) {
|
|
4010
|
+
this._flushStreamState();
|
|
3906
4011
|
this._setStreamingId(null);
|
|
3907
4012
|
throw err;
|
|
3908
4013
|
} finally {
|
|
@@ -3915,11 +4020,22 @@ class AparteChatHost {
|
|
|
3915
4020
|
if (this._streamAbort === ac) this._streamAbort = void 0;
|
|
3916
4021
|
}
|
|
3917
4022
|
}
|
|
4023
|
+
/**
|
|
4024
|
+
* Buffer a streamed token for the coalesced state sync. A different message id
|
|
4025
|
+
* (a second stream started without stopping the first) flushes the previous
|
|
4026
|
+
* one, so text never migrates between messages.
|
|
4027
|
+
*/
|
|
4028
|
+
_bufferToken(messageId, chunk) {
|
|
4029
|
+
if (this._tokenBuffer && this._tokenBuffer.messageId !== messageId) this._flushStreamState();
|
|
4030
|
+
this._tokenBuffer = this._tokenBuffer ? { messageId, text: this._tokenBuffer.text + chunk } : { messageId, text: chunk };
|
|
4031
|
+
this._scheduleStreamFlush();
|
|
4032
|
+
}
|
|
3918
4033
|
/** Abort an in-flight {@link streamTokens} loop. */
|
|
3919
4034
|
stopTokenStream() {
|
|
3920
4035
|
if (this._streamAbort) {
|
|
3921
4036
|
this._streamAbort.abort();
|
|
3922
4037
|
this._streamAbort = void 0;
|
|
4038
|
+
this._flushStreamState();
|
|
3923
4039
|
this._setStreamingId(null);
|
|
3924
4040
|
}
|
|
3925
4041
|
}
|
|
@@ -4864,8 +4980,9 @@ ${summary}`,
|
|
|
4864
4980
|
const targetId = event.detail?.targetId;
|
|
4865
4981
|
if (targetId) {
|
|
4866
4982
|
const byId = document.getElementById(targetId);
|
|
4867
|
-
|
|
4868
|
-
|
|
4983
|
+
const resolved = this._asRenderTarget(byId);
|
|
4984
|
+
if (resolved) {
|
|
4985
|
+
targetElement = resolved;
|
|
4869
4986
|
} else {
|
|
4870
4987
|
console.warn("[AparteClient] ⚠️ targetId present but element not found or missing appendMessage:", targetId);
|
|
4871
4988
|
}
|
|
@@ -6093,6 +6210,10 @@ function applyElementProps(el2, props, transformValue = (value) => value) {
|
|
|
6093
6210
|
}
|
|
6094
6211
|
}
|
|
6095
6212
|
}
|
|
6213
|
+
function isAwaitingReply(message) {
|
|
6214
|
+
if (message.status === "streaming" || message.status === "pending") return true;
|
|
6215
|
+
return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
|
|
6216
|
+
}
|
|
6096
6217
|
function el(tag, className, text) {
|
|
6097
6218
|
const node = document.createElement(tag);
|
|
6098
6219
|
if (className) node.className = className;
|
|
@@ -6279,18 +6400,20 @@ export {
|
|
|
6279
6400
|
BackendTransport as B,
|
|
6280
6401
|
ConversationManager as C,
|
|
6281
6402
|
DEFAULT_ICON_FALLBACKS as D,
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6403
|
+
isAwaitingReply as E,
|
|
6404
|
+
isFormatAdapter as F,
|
|
6405
|
+
isSafeUrl as G,
|
|
6406
|
+
parseAparteEventStream as H,
|
|
6407
|
+
parseMarkdownToSegments as I,
|
|
6408
|
+
populateBubbleFromMessage as J,
|
|
6409
|
+
readableToAsyncIterable as K,
|
|
6410
|
+
registerDefaultRenderers as L,
|
|
6290
6411
|
MessageRepository as M,
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6412
|
+
registerSegmentRenderer as N,
|
|
6413
|
+
requestUserInput as O,
|
|
6414
|
+
resolveConfig as P,
|
|
6415
|
+
runWithConfig as Q,
|
|
6416
|
+
unregisterSegmentRenderer as R,
|
|
6294
6417
|
APARTE_HOST_ATTR as a,
|
|
6295
6418
|
AparteChatHost as b,
|
|
6296
6419
|
AparteClient as c,
|
|
@@ -6315,7 +6438,7 @@ export {
|
|
|
6315
6438
|
defaultSanitizer as v,
|
|
6316
6439
|
deriveArtifactKind as w,
|
|
6317
6440
|
detachConfig as x,
|
|
6318
|
-
|
|
6319
|
-
|
|
6441
|
+
filesToAttachments as y,
|
|
6442
|
+
getSegmentRenderer as z
|
|
6320
6443
|
};
|
|
6321
|
-
//# sourceMappingURL=index-
|
|
6444
|
+
//# sourceMappingURL=index-BBg6dsYx.js.map
|