@aparte/core 0.2.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 +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 +16 -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 +3658 -3385
- package/dist/host/aparte-chat-host.d.ts +47 -1
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/{index-D89k-n2Q.js → index-BBg6dsYx.js} +193 -43
- 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 +123 -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 +27 -0
- package/dist/types/imperative-api.d.ts.map +1 -1
- package/dist/types/model-provider.d.ts +9 -3
- package/dist/types/model-provider.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-D89k-n2Q.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,
|
|
@@ -1267,6 +1278,15 @@ class AparteConfigClass {
|
|
|
1267
1278
|
this._locale = locale;
|
|
1268
1279
|
this._notify();
|
|
1269
1280
|
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Restore the built-in English locale (the same `DEFAULT_LOCALE` core ships
|
|
1283
|
+
* with). Counterpart of {@link setLocale} for language toggles — avoids
|
|
1284
|
+
* having to import `DEFAULT_LOCALE` yourself.
|
|
1285
|
+
*/
|
|
1286
|
+
resetLocale() {
|
|
1287
|
+
this._locale = DEFAULT_LOCALE;
|
|
1288
|
+
this._notify();
|
|
1289
|
+
}
|
|
1270
1290
|
/**
|
|
1271
1291
|
* Get the current locale
|
|
1272
1292
|
*/
|
|
@@ -1287,9 +1307,8 @@ class AparteConfigClass {
|
|
|
1287
1307
|
* Falls back to textual representation if no provider is set
|
|
1288
1308
|
*/
|
|
1289
1309
|
getIcon(name) {
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
}
|
|
1310
|
+
const icon = this._iconProvider?.[name];
|
|
1311
|
+
if (icon) return icon();
|
|
1293
1312
|
return DEFAULT_ICON_FALLBACKS[name];
|
|
1294
1313
|
}
|
|
1295
1314
|
/**
|
|
@@ -1480,7 +1499,12 @@ class AparteConfigClass {
|
|
|
1480
1499
|
const provider = this._aiProviders.get(defaultProvider);
|
|
1481
1500
|
if (!provider) return void 0;
|
|
1482
1501
|
const models = provider.getModels();
|
|
1483
|
-
if (models instanceof Promise)
|
|
1502
|
+
if (models instanceof Promise) {
|
|
1503
|
+
console.warn(
|
|
1504
|
+
`[AparteConfig] Provider "${defaultProvider}".getModels() returned a Promise — it is ignored here, so model capabilities (e.g. function_calling) resolve to none and tools are disabled. getModels() must return the list synchronously; implement fetchModels() for async fetching.`
|
|
1505
|
+
);
|
|
1506
|
+
return void 0;
|
|
1507
|
+
}
|
|
1484
1508
|
return models.find((m) => m.id === defaultModel);
|
|
1485
1509
|
}
|
|
1486
1510
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -3305,6 +3329,19 @@ class ConversationManager {
|
|
|
3305
3329
|
return text.trim() || "New Chat";
|
|
3306
3330
|
}
|
|
3307
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
|
+
}
|
|
3308
3345
|
class AparteConversationController {
|
|
3309
3346
|
_binding;
|
|
3310
3347
|
_options;
|
|
@@ -3349,16 +3386,7 @@ class AparteConversationController {
|
|
|
3349
3386
|
if (targetId && targetId !== this._binding.hostId) return;
|
|
3350
3387
|
const content = evt.detail?.content ?? "";
|
|
3351
3388
|
const files = evt.detail?.files;
|
|
3352
|
-
const attachments = files?.length ? files
|
|
3353
|
-
id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`,
|
|
3354
|
-
name: f.name,
|
|
3355
|
-
type: f.type || "application/octet-stream",
|
|
3356
|
-
url: URL.createObjectURL(f),
|
|
3357
|
-
size: f.size,
|
|
3358
|
-
// Carry the raw File so the storage adapter can persist it
|
|
3359
|
-
// to its attachments table; reload reconstructs `url`.
|
|
3360
|
-
blob: f
|
|
3361
|
-
})) : void 0;
|
|
3389
|
+
const attachments = files?.length ? filesToAttachments(files) : void 0;
|
|
3362
3390
|
const userMsg = {
|
|
3363
3391
|
id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`,
|
|
3364
3392
|
role: "user",
|
|
@@ -3608,6 +3636,12 @@ class AparteChatHost {
|
|
|
3608
3636
|
_pendingSiblings;
|
|
3609
3637
|
/** Abort handle for an in-flight {@link streamTokens} loop. */
|
|
3610
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;
|
|
3611
3645
|
_conversationId = null;
|
|
3612
3646
|
_controller;
|
|
3613
3647
|
_teardown = [];
|
|
@@ -3720,6 +3754,9 @@ class AparteChatHost {
|
|
|
3720
3754
|
}
|
|
3721
3755
|
/** Drop transient streaming state so a new conversation starts clean. */
|
|
3722
3756
|
_beginConversationSwap() {
|
|
3757
|
+
this._segmentBuffer.clear();
|
|
3758
|
+
this._tokenBuffer = null;
|
|
3759
|
+
this._flushStreamState();
|
|
3723
3760
|
this.stopTokenStream();
|
|
3724
3761
|
this._setStreamingId(null);
|
|
3725
3762
|
this.binding.onTypingChange?.(false);
|
|
@@ -3741,12 +3778,14 @@ class AparteChatHost {
|
|
|
3741
3778
|
* is the append-specific signal instead.
|
|
3742
3779
|
*/
|
|
3743
3780
|
appendMessage(message) {
|
|
3781
|
+
this._flushStreamState();
|
|
3744
3782
|
if (message.role === "user") this._vp()?.setAutoScroll?.(true);
|
|
3745
3783
|
this.binding.setMessages([...this.binding.getMessages(), message]);
|
|
3746
3784
|
this.binding.onMessageAppended?.(message);
|
|
3747
3785
|
}
|
|
3748
3786
|
/** Atomic partial update of a message by id. */
|
|
3749
3787
|
updateMessage(messageId, updates) {
|
|
3788
|
+
this._flushStreamState();
|
|
3750
3789
|
const msgs = this.binding.getMessages();
|
|
3751
3790
|
const index = msgs.findIndex((m) => m.id === messageId);
|
|
3752
3791
|
if (index === -1) return;
|
|
@@ -3772,6 +3811,7 @@ class AparteChatHost {
|
|
|
3772
3811
|
}
|
|
3773
3812
|
/** Add a segment to the last message (immediate bubble + state sync). */
|
|
3774
3813
|
addSegment(segment) {
|
|
3814
|
+
this._flushStreamState();
|
|
3775
3815
|
const msgs = this.binding.getMessages();
|
|
3776
3816
|
const last = msgs[msgs.length - 1];
|
|
3777
3817
|
if (last && this._isOrphan(last.id)) return;
|
|
@@ -3785,6 +3825,7 @@ class AparteChatHost {
|
|
|
3785
3825
|
}
|
|
3786
3826
|
/** Update a segment in the last message in place. */
|
|
3787
3827
|
updateSegment(segmentId, updates) {
|
|
3828
|
+
this._flushStreamState();
|
|
3788
3829
|
const msgs = this.binding.getMessages();
|
|
3789
3830
|
const last = msgs[msgs.length - 1];
|
|
3790
3831
|
if (last && this._isOrphan(last.id)) return;
|
|
@@ -3799,6 +3840,7 @@ class AparteChatHost {
|
|
|
3799
3840
|
}
|
|
3800
3841
|
/** Remove a transient segment (e.g. pipeline-waiting indicator). */
|
|
3801
3842
|
removeSegment(segmentId) {
|
|
3843
|
+
this._flushStreamState();
|
|
3802
3844
|
this._lastBubble()?.removeSegment?.(segmentId);
|
|
3803
3845
|
const msgs = this.binding.getMessages();
|
|
3804
3846
|
const last = msgs[msgs.length - 1];
|
|
@@ -3809,18 +3851,85 @@ class AparteChatHost {
|
|
|
3809
3851
|
this.binding.setMessages(next);
|
|
3810
3852
|
this.binding.onMessagesChange?.(next);
|
|
3811
3853
|
}
|
|
3812
|
-
/**
|
|
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
|
+
*/
|
|
3813
3872
|
appendToSegment(segmentId, content) {
|
|
3814
3873
|
const msgs = this.binding.getMessages();
|
|
3815
3874
|
const last = msgs[msgs.length - 1];
|
|
3816
3875
|
if (!last || !last.segments) return;
|
|
3817
3876
|
if (this._isOrphan(last.id)) return;
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
);
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
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);
|
|
3824
3933
|
}
|
|
3825
3934
|
/** Read the current message list. */
|
|
3826
3935
|
getMessages() {
|
|
@@ -3866,6 +3975,13 @@ class AparteChatHost {
|
|
|
3866
3975
|
* appends each token, completes the message at the end. Per-framework
|
|
3867
3976
|
* wrappers adapt their native stream type (RxJS Observable, AsyncIterable,
|
|
3868
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.
|
|
3869
3985
|
*/
|
|
3870
3986
|
async streamTokens(messageId, tokens) {
|
|
3871
3987
|
this.stopTokenStream();
|
|
@@ -3885,10 +4001,13 @@ class AparteChatHost {
|
|
|
3885
4001
|
const res = await Promise.race([iterator.next(), aborted]);
|
|
3886
4002
|
if (res === "aborted" || ac.signal.aborted || res.done) break;
|
|
3887
4003
|
this._vp()?.appendToken?.(messageId, res.value);
|
|
4004
|
+
this._bufferToken(messageId, res.value);
|
|
3888
4005
|
}
|
|
4006
|
+
this._flushStreamState();
|
|
3889
4007
|
if (!ac.signal.aborted) this._vp()?.completeMessage?.(messageId);
|
|
3890
4008
|
this._setStreamingId(null);
|
|
3891
4009
|
} catch (err) {
|
|
4010
|
+
this._flushStreamState();
|
|
3892
4011
|
this._setStreamingId(null);
|
|
3893
4012
|
throw err;
|
|
3894
4013
|
} finally {
|
|
@@ -3901,11 +4020,22 @@ class AparteChatHost {
|
|
|
3901
4020
|
if (this._streamAbort === ac) this._streamAbort = void 0;
|
|
3902
4021
|
}
|
|
3903
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
|
+
}
|
|
3904
4033
|
/** Abort an in-flight {@link streamTokens} loop. */
|
|
3905
4034
|
stopTokenStream() {
|
|
3906
4035
|
if (this._streamAbort) {
|
|
3907
4036
|
this._streamAbort.abort();
|
|
3908
4037
|
this._streamAbort = void 0;
|
|
4038
|
+
this._flushStreamState();
|
|
3909
4039
|
this._setStreamingId(null);
|
|
3910
4040
|
}
|
|
3911
4041
|
}
|
|
@@ -4850,8 +4980,9 @@ ${summary}`,
|
|
|
4850
4980
|
const targetId = event.detail?.targetId;
|
|
4851
4981
|
if (targetId) {
|
|
4852
4982
|
const byId = document.getElementById(targetId);
|
|
4853
|
-
|
|
4854
|
-
|
|
4983
|
+
const resolved = this._asRenderTarget(byId);
|
|
4984
|
+
if (resolved) {
|
|
4985
|
+
targetElement = resolved;
|
|
4855
4986
|
} else {
|
|
4856
4987
|
console.warn("[AparteClient] ⚠️ targetId present but element not found or missing appendMessage:", targetId);
|
|
4857
4988
|
}
|
|
@@ -4918,7 +5049,7 @@ ${summary}`,
|
|
|
4918
5049
|
try {
|
|
4919
5050
|
authConfig = await this._resolveAuth(providerId);
|
|
4920
5051
|
const rawFiles = Array.isArray(event.detail?.files) ? event.detail.files : [];
|
|
4921
|
-
const filesToInject = this.
|
|
5052
|
+
const filesToInject = this._selectFilesToInject(rawFiles);
|
|
4922
5053
|
const contentParts = filesToInject.length > 0 ? await this._filesToContentParts(filesToInject) : [];
|
|
4923
5054
|
messages = this._buildMessages(content, targetElement, contentParts.length > 0 ? contentParts : void 0);
|
|
4924
5055
|
} catch (error) {
|
|
@@ -4976,6 +5107,19 @@ ${summary}`,
|
|
|
4976
5107
|
if (!message.segments) return "";
|
|
4977
5108
|
return message.segments.filter((s) => s.type === "text" || s.type === "code").map((s) => s.content ?? "").join("\n").trim();
|
|
4978
5109
|
}
|
|
5110
|
+
/**
|
|
5111
|
+
* Which of the send's pending files get inlined into the request: the
|
|
5112
|
+
* `rawFileInject` mode first —
|
|
5113
|
+
* 'none' → nothing inline (RAG handles all file types),
|
|
5114
|
+
* 'images-only' → images inline, text/docs to the app layer,
|
|
5115
|
+
* 'all' (default) → images + text files inline —
|
|
5116
|
+
* then the optional per-file `fileInjectFilter` veto.
|
|
5117
|
+
*/
|
|
5118
|
+
_selectFilesToInject(rawFiles) {
|
|
5119
|
+
const byMode = this.options.rawFileInject === "none" ? [] : this.options.rawFileInject === "images-only" ? rawFiles.filter((f) => f.type.startsWith("image/")) : rawFiles;
|
|
5120
|
+
const filter = this.options.fileInjectFilter;
|
|
5121
|
+
return filter ? byMode.filter((f) => filter(f)) : byMode;
|
|
5122
|
+
}
|
|
4979
5123
|
/**
|
|
4980
5124
|
* Convert an array of File objects to AparteContentPart[].
|
|
4981
5125
|
* - Images → AparteImagePart (base64 data URL)
|
|
@@ -6066,6 +6210,10 @@ function applyElementProps(el2, props, transformValue = (value) => value) {
|
|
|
6066
6210
|
}
|
|
6067
6211
|
}
|
|
6068
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
|
+
}
|
|
6069
6217
|
function el(tag, className, text) {
|
|
6070
6218
|
const node = document.createElement(tag);
|
|
6071
6219
|
if (className) node.className = className;
|
|
@@ -6252,18 +6400,20 @@ export {
|
|
|
6252
6400
|
BackendTransport as B,
|
|
6253
6401
|
ConversationManager as C,
|
|
6254
6402
|
DEFAULT_ICON_FALLBACKS as D,
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
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,
|
|
6263
6411
|
MessageRepository as M,
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6412
|
+
registerSegmentRenderer as N,
|
|
6413
|
+
requestUserInput as O,
|
|
6414
|
+
resolveConfig as P,
|
|
6415
|
+
runWithConfig as Q,
|
|
6416
|
+
unregisterSegmentRenderer as R,
|
|
6267
6417
|
APARTE_HOST_ATTR as a,
|
|
6268
6418
|
AparteChatHost as b,
|
|
6269
6419
|
AparteClient as c,
|
|
@@ -6288,7 +6438,7 @@ export {
|
|
|
6288
6438
|
defaultSanitizer as v,
|
|
6289
6439
|
deriveArtifactKind as w,
|
|
6290
6440
|
detachConfig as x,
|
|
6291
|
-
|
|
6292
|
-
|
|
6441
|
+
filesToAttachments as y,
|
|
6442
|
+
getSegmentRenderer as z
|
|
6293
6443
|
};
|
|
6294
|
-
//# sourceMappingURL=index-
|
|
6444
|
+
//# sourceMappingURL=index-BBg6dsYx.js.map
|