@aparte/core 0.3.0-alpha.0 → 0.5.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 +26 -2
- package/dist/client/aparte-client.d.ts +7 -1
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +26 -2
- 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/composer/aparte-composer-attachments.d.ts +3 -2
- package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +17 -4
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +64 -7
- 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/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/custom-elements.json +3435 -3000
- package/dist/host/aparte-chat-host.d.ts +58 -1
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/{index-CW2BCAqn.js → index-Dn7TH14G.js} +282 -81
- package/dist/index-Dn7TH14G.js.map +1 -0
- package/dist/index.css +75 -8
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +216 -62
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +3 -1
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +34 -30
- 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/renderers/index.d.ts +1 -1
- package/dist/renderers/index.d.ts.map +1 -1
- package/dist/renderers/segment-renderers.d.ts +23 -0
- package/dist/renderers/segment-renderers.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/types/models.d.ts +56 -9
- package/dist/types/models.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
|
@@ -939,6 +939,18 @@ const defaultSanitizer = (html) => {
|
|
|
939
939
|
sanitizeChildren(doc.body, container, doc);
|
|
940
940
|
return container.innerHTML;
|
|
941
941
|
};
|
|
942
|
+
const DEFAULT_BUBBLE_ACTIONS = {
|
|
943
|
+
copy: true,
|
|
944
|
+
retry: false,
|
|
945
|
+
edit: false,
|
|
946
|
+
feedback: false,
|
|
947
|
+
info: false
|
|
948
|
+
};
|
|
949
|
+
const DEFAULT_HOST_HANDLERS = {
|
|
950
|
+
attachmentPreview: false,
|
|
951
|
+
terminalRun: false,
|
|
952
|
+
artifactRedownload: false
|
|
953
|
+
};
|
|
942
954
|
class AparteConfigClass {
|
|
943
955
|
_markdownProvider;
|
|
944
956
|
_streamingMarkdownProvider;
|
|
@@ -978,8 +990,11 @@ class AparteConfigClass {
|
|
|
978
990
|
// Tool Registry
|
|
979
991
|
_tools = /* @__PURE__ */ new Map();
|
|
980
992
|
_toolRenderers = /* @__PURE__ */ new Map();
|
|
981
|
-
//
|
|
982
|
-
|
|
993
|
+
// Host handlers — what the app declares it can actually complete.
|
|
994
|
+
_hostHandlers = { ...DEFAULT_HOST_HANDLERS };
|
|
995
|
+
// Bubble Actions — DEFAULT_BUBBLE_ACTIONS is the single source of truth
|
|
996
|
+
// (init here, restored by reset(), and the fallback in getBubbleActions()).
|
|
997
|
+
_bubbleActionsConfig = { ...DEFAULT_BUBBLE_ACTIONS };
|
|
983
998
|
// ─────────────────────────────────────────────────────────────────────────
|
|
984
999
|
// Provider Setters (Dependency Injection)
|
|
985
1000
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1021,12 +1036,13 @@ class AparteConfigClass {
|
|
|
1021
1036
|
}
|
|
1022
1037
|
/**
|
|
1023
1038
|
* Configure which action buttons appear in message bubbles.
|
|
1024
|
-
* Unset keys keep their defaults
|
|
1039
|
+
* Unset keys keep their defaults — see {@link DEFAULT_BUBBLE_ACTIONS}: `copy`
|
|
1040
|
+
* only, because every other button needs a host to honor it.
|
|
1025
1041
|
*
|
|
1026
1042
|
* @example
|
|
1027
|
-
* AparteConfig.setBubbleActions({
|
|
1028
|
-
* AparteConfig.setBubbleActions({
|
|
1029
|
-
* AparteConfig.setBubbleActions({ copy: false
|
|
1043
|
+
* AparteConfig.setBubbleActions({ retry: true, edit: true }) // you run AparteClient
|
|
1044
|
+
* AparteConfig.setBubbleActions({ feedback: true }) // you listen for aparte-feedback
|
|
1045
|
+
* AparteConfig.setBubbleActions({ copy: false }) // hide everything
|
|
1030
1046
|
* // Explicit per-role ordered sets (replace the flag defaults for that role):
|
|
1031
1047
|
* AparteConfig.setBubbleActions({ user: ['edit', 'copy'], assistant: ['copy', 'thumbUp', 'thumbDown', 'retry'] })
|
|
1032
1048
|
*/
|
|
@@ -1034,13 +1050,33 @@ class AparteConfigClass {
|
|
|
1034
1050
|
this._bubbleActionsConfig = { ...this._bubbleActionsConfig, ...config };
|
|
1035
1051
|
this._notify();
|
|
1036
1052
|
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Declare which host-dependent affordances your app handles. Core renders the
|
|
1055
|
+
* trigger only for the ones you claim — see {@link AparteHostHandlersConfig}.
|
|
1056
|
+
*
|
|
1057
|
+
* @example
|
|
1058
|
+
* AparteConfig.setHostHandlers({ attachmentPreview: true, terminalRun: true });
|
|
1059
|
+
*/
|
|
1060
|
+
setHostHandlers(config) {
|
|
1061
|
+
this._hostHandlers = { ...this._hostHandlers, ...config };
|
|
1062
|
+
this._notify();
|
|
1063
|
+
}
|
|
1064
|
+
/** Returns the resolved host-handler declarations (undeclared = false). */
|
|
1065
|
+
getHostHandlers() {
|
|
1066
|
+
return {
|
|
1067
|
+
attachmentPreview: this._hostHandlers.attachmentPreview ?? DEFAULT_HOST_HANDLERS.attachmentPreview,
|
|
1068
|
+
terminalRun: this._hostHandlers.terminalRun ?? DEFAULT_HOST_HANDLERS.terminalRun,
|
|
1069
|
+
artifactRedownload: this._hostHandlers.artifactRedownload ?? DEFAULT_HOST_HANDLERS.artifactRedownload
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1037
1072
|
/** Returns the resolved bubble actions config (flag defaults applied; per-role sets passed through). */
|
|
1038
1073
|
getBubbleActions() {
|
|
1039
1074
|
return {
|
|
1040
|
-
copy: this._bubbleActionsConfig.copy ??
|
|
1041
|
-
retry: this._bubbleActionsConfig.retry ??
|
|
1042
|
-
edit: this._bubbleActionsConfig.edit ??
|
|
1043
|
-
feedback: this._bubbleActionsConfig.feedback ??
|
|
1075
|
+
copy: this._bubbleActionsConfig.copy ?? DEFAULT_BUBBLE_ACTIONS.copy,
|
|
1076
|
+
retry: this._bubbleActionsConfig.retry ?? DEFAULT_BUBBLE_ACTIONS.retry,
|
|
1077
|
+
edit: this._bubbleActionsConfig.edit ?? DEFAULT_BUBBLE_ACTIONS.edit,
|
|
1078
|
+
feedback: this._bubbleActionsConfig.feedback ?? DEFAULT_BUBBLE_ACTIONS.feedback,
|
|
1079
|
+
info: this._bubbleActionsConfig.info ?? DEFAULT_BUBBLE_ACTIONS.info,
|
|
1044
1080
|
user: this._bubbleActionsConfig.user,
|
|
1045
1081
|
assistant: this._bubbleActionsConfig.assistant
|
|
1046
1082
|
};
|
|
@@ -1165,13 +1201,24 @@ class AparteConfigClass {
|
|
|
1165
1201
|
return this._artifactPreviewBuilder;
|
|
1166
1202
|
}
|
|
1167
1203
|
/**
|
|
1168
|
-
*
|
|
1204
|
+
* The icon set as a **complete** provider: every name resolves, falling back
|
|
1205
|
+
* to `DEFAULT_ICON_FALLBACKS` for anything the registered provider doesn't
|
|
1206
|
+
* implement. Callers (bubble action bar, composer controls) can therefore
|
|
1207
|
+
* invoke `icons.copy()` unconditionally.
|
|
1208
|
+
*
|
|
1209
|
+
* It used to hand back the registered provider verbatim, so a provider that
|
|
1210
|
+
* implemented a subset — which `getIcon()` has always supported, and which
|
|
1211
|
+
* the type now states — crashed every other icon with
|
|
1212
|
+
* "icons.retry is not a function".
|
|
1169
1213
|
*/
|
|
1170
1214
|
getIconProvider() {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1215
|
+
const registered = this._iconProvider;
|
|
1216
|
+
const complete = {};
|
|
1217
|
+
for (const name of Object.keys(DEFAULT_ICON_FALLBACKS)) {
|
|
1218
|
+
const fn = registered?.[name];
|
|
1219
|
+
complete[name] = fn ?? (() => DEFAULT_ICON_FALLBACKS[name]);
|
|
1220
|
+
}
|
|
1221
|
+
return complete;
|
|
1175
1222
|
}
|
|
1176
1223
|
/**
|
|
1177
1224
|
* Set a custom avatar renderer. Lets framework consumers (Angular,
|
|
@@ -1296,9 +1343,8 @@ class AparteConfigClass {
|
|
|
1296
1343
|
* Falls back to textual representation if no provider is set
|
|
1297
1344
|
*/
|
|
1298
1345
|
getIcon(name) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1346
|
+
const icon = this._iconProvider?.[name];
|
|
1347
|
+
if (icon) return icon();
|
|
1302
1348
|
return DEFAULT_ICON_FALLBACKS[name];
|
|
1303
1349
|
}
|
|
1304
1350
|
/**
|
|
@@ -1671,7 +1717,8 @@ class AparteConfigClass {
|
|
|
1671
1717
|
this._modelConfig = {};
|
|
1672
1718
|
this._requireModelSelection = false;
|
|
1673
1719
|
this._modelPreferenceProvider = void 0;
|
|
1674
|
-
this._bubbleActionsConfig = {
|
|
1720
|
+
this._bubbleActionsConfig = { ...DEFAULT_BUBBLE_ACTIONS };
|
|
1721
|
+
this._hostHandlers = { ...DEFAULT_HOST_HANDLERS };
|
|
1675
1722
|
this._notify();
|
|
1676
1723
|
}
|
|
1677
1724
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1808,10 +1855,23 @@ function findClosingFence(s) {
|
|
|
1808
1855
|
}
|
|
1809
1856
|
const renderers = /* @__PURE__ */ new Map();
|
|
1810
1857
|
let styleElement = null;
|
|
1858
|
+
let defaultsInstalled = false;
|
|
1859
|
+
let defaultsDeclined = false;
|
|
1811
1860
|
function registerSegmentRenderer(renderer) {
|
|
1812
1861
|
renderers.set(renderer.type, renderer);
|
|
1813
1862
|
injectRendererStyles();
|
|
1814
1863
|
}
|
|
1864
|
+
function declineDefaultRenderers() {
|
|
1865
|
+
defaultsDeclined = true;
|
|
1866
|
+
}
|
|
1867
|
+
function installDefaultRenderersOnce() {
|
|
1868
|
+
if (defaultsInstalled || defaultsDeclined) return;
|
|
1869
|
+
defaultsInstalled = true;
|
|
1870
|
+
for (const renderer of DEFAULT_RENDERERS) {
|
|
1871
|
+
if (!renderers.has(renderer.type)) renderers.set(renderer.type, renderer);
|
|
1872
|
+
}
|
|
1873
|
+
injectRendererStyles();
|
|
1874
|
+
}
|
|
1815
1875
|
function unregisterSegmentRenderer(type) {
|
|
1816
1876
|
renderers.delete(type);
|
|
1817
1877
|
}
|
|
@@ -1943,7 +2003,7 @@ const terminalRenderer = {
|
|
|
1943
2003
|
</div>
|
|
1944
2004
|
<code class="terminal-command">${escapeHtml(segment.command || "")}</code>
|
|
1945
2005
|
<div class="terminal-actions">
|
|
1946
|
-
${segment.isRunning ? `<span class="terminal-running"><span class="spinner"></span>${contextConfig().t("running")}</span>` : `<button class="terminal-run-btn" data-action="run" aria-label="${contextConfig().t("run")}" title="${contextConfig().t("run")}">${contextConfig().t("run")}</button>`}
|
|
2006
|
+
${segment.isRunning ? `<span class="terminal-running"><span class="spinner"></span>${contextConfig().t("running")}</span>` : contextConfig().getHostHandlers().terminalRun ? `<button class="terminal-run-btn" data-action="run" aria-label="${contextConfig().t("run")}" title="${contextConfig().t("run")}">${contextConfig().t("run")}</button>` : ""}
|
|
1947
2007
|
<button class="terminal-copy-btn" data-action="copy" aria-label="${contextConfig().t("copy")}" title="${contextConfig().t("copy")}">
|
|
1948
2008
|
${contextConfig().getIcon("copy")}
|
|
1949
2009
|
</button>
|
|
@@ -2201,16 +2261,8 @@ const pipelineWaitingRenderer = {
|
|
|
2201
2261
|
`
|
|
2202
2262
|
};
|
|
2203
2263
|
function registerDefaultRenderers() {
|
|
2204
|
-
|
|
2205
|
-
registerSegmentRenderer(
|
|
2206
|
-
registerSegmentRenderer(codeRenderer);
|
|
2207
|
-
registerSegmentRenderer(terminalRenderer);
|
|
2208
|
-
registerSegmentRenderer(errorRenderer);
|
|
2209
|
-
registerSegmentRenderer(progressRenderer);
|
|
2210
|
-
registerSegmentRenderer(fileTreeRenderer);
|
|
2211
|
-
registerSegmentRenderer(toolCallRenderer);
|
|
2212
|
-
registerSegmentRenderer(artifactRenderer);
|
|
2213
|
-
registerSegmentRenderer(pipelineWaitingRenderer);
|
|
2264
|
+
defaultsInstalled = true;
|
|
2265
|
+
for (const renderer of DEFAULT_RENDERERS) registerSegmentRenderer(renderer);
|
|
2214
2266
|
}
|
|
2215
2267
|
const PREVIEWABLE_KINDS = /* @__PURE__ */ new Set(["react", "html", "svg", "js", "css"]);
|
|
2216
2268
|
const BINARY_FILE_KINDS = /* @__PURE__ */ new Set(["pdf", "xlsx", "docx"]);
|
|
@@ -2695,6 +2747,7 @@ function renderBinaryFileArtifact(segment, kind) {
|
|
|
2695
2747
|
const title = segment.title?.trim() || labelForKind(kind);
|
|
2696
2748
|
const iconLabel = FILE_ICON_LABEL[kind] ?? kind.toUpperCase();
|
|
2697
2749
|
const isStreaming = !!segment.isStreaming;
|
|
2750
|
+
const canRedownload = contextConfig().getHostHandlers().artifactRedownload;
|
|
2698
2751
|
const cached = _binaryArtifactCache.get(segment.id);
|
|
2699
2752
|
if (cached && !isStreaming) {
|
|
2700
2753
|
const previewBody = cached.previewHtml ? contextConfig().sanitizeHtml(cached.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
|
|
@@ -2710,7 +2763,7 @@ function renderBinaryFileArtifact(segment, kind) {
|
|
|
2710
2763
|
<div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(formatBytes(cached.bytes))} · ${escapeHtml(kind.toUpperCase())}</div>
|
|
2711
2764
|
</div>
|
|
2712
2765
|
<div class="aparte-art-file__actions">
|
|
2713
|
-
<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">Download</button>
|
|
2766
|
+
${canRedownload ? '<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download">Download</button>' : ""}
|
|
2714
2767
|
</div>
|
|
2715
2768
|
</div>
|
|
2716
2769
|
<div class="aparte-art-file__body">
|
|
@@ -2736,7 +2789,7 @@ function renderBinaryFileArtifact(segment, kind) {
|
|
|
2736
2789
|
<div class="aparte-art-file__meta-sub" data-role="file-sub">${escapeHtml(subText)}</div>
|
|
2737
2790
|
</div>
|
|
2738
2791
|
<div class="aparte-art-file__actions">
|
|
2739
|
-
<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>Download</button>
|
|
2792
|
+
${canRedownload ? '<button type="button" class="aparte-art-file__btn aparte-art-file__btn--primary" data-action="download" disabled>Download</button>' : ""}
|
|
2740
2793
|
</div>
|
|
2741
2794
|
</div>
|
|
2742
2795
|
<div class="aparte-art-file__body">
|
|
@@ -3068,6 +3121,18 @@ function escapeClosingScriptTag(body) {
|
|
|
3068
3121
|
}
|
|
3069
3122
|
return out;
|
|
3070
3123
|
}
|
|
3124
|
+
const DEFAULT_RENDERERS = [
|
|
3125
|
+
textRenderer,
|
|
3126
|
+
thinkingRenderer,
|
|
3127
|
+
codeRenderer,
|
|
3128
|
+
terminalRenderer,
|
|
3129
|
+
errorRenderer,
|
|
3130
|
+
progressRenderer,
|
|
3131
|
+
fileTreeRenderer,
|
|
3132
|
+
toolCallRenderer,
|
|
3133
|
+
artifactRenderer,
|
|
3134
|
+
pipelineWaitingRenderer
|
|
3135
|
+
];
|
|
3071
3136
|
function populateBubbleFromMessage(bubble, message, siblingInfo) {
|
|
3072
3137
|
if (message.segments && message.segments.length > 0) {
|
|
3073
3138
|
const existing = bubble.getSegments?.() ?? [];
|
|
@@ -3319,6 +3384,19 @@ class ConversationManager {
|
|
|
3319
3384
|
return text.trim() || "New Chat";
|
|
3320
3385
|
}
|
|
3321
3386
|
}
|
|
3387
|
+
function attachmentId() {
|
|
3388
|
+
return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3389
|
+
}
|
|
3390
|
+
function filesToAttachments(files) {
|
|
3391
|
+
return files.map((file) => ({
|
|
3392
|
+
id: attachmentId(),
|
|
3393
|
+
name: file.name,
|
|
3394
|
+
type: file.type || "application/octet-stream",
|
|
3395
|
+
url: URL.createObjectURL(file),
|
|
3396
|
+
size: file.size,
|
|
3397
|
+
blob: file
|
|
3398
|
+
}));
|
|
3399
|
+
}
|
|
3322
3400
|
class AparteConversationController {
|
|
3323
3401
|
_binding;
|
|
3324
3402
|
_options;
|
|
@@ -3363,16 +3441,7 @@ class AparteConversationController {
|
|
|
3363
3441
|
if (targetId && targetId !== this._binding.hostId) return;
|
|
3364
3442
|
const content = evt.detail?.content ?? "";
|
|
3365
3443
|
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;
|
|
3444
|
+
const attachments = files?.length ? filesToAttachments(files) : void 0;
|
|
3376
3445
|
const userMsg = {
|
|
3377
3446
|
id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`,
|
|
3378
3447
|
role: "user",
|
|
@@ -3622,6 +3691,23 @@ class AparteChatHost {
|
|
|
3622
3691
|
_pendingSiblings;
|
|
3623
3692
|
/** Abort handle for an in-flight {@link streamTokens} loop. */
|
|
3624
3693
|
_streamAbort;
|
|
3694
|
+
/** Segment chunks written to the bubble, awaiting one coalesced state sync. */
|
|
3695
|
+
/**
|
|
3696
|
+
* Buffered segment text, per segment id, as `base + text` — an **absolute**
|
|
3697
|
+
* target, never a delta.
|
|
3698
|
+
*
|
|
3699
|
+
* Why absolute: the immediate paint (`appendToSegment` below) writes through the
|
|
3700
|
+
* viewport into the very segment object the framework list holds — they are the
|
|
3701
|
+
* same object, handed to both by `addSegment`. A delta added at flush time would
|
|
3702
|
+
* therefore land on content that already contains it, and every chunk came out
|
|
3703
|
+
* doubled. `base` is captured before the paint, so whoever mutates the shared
|
|
3704
|
+
* object in between cannot change the result.
|
|
3705
|
+
*/
|
|
3706
|
+
_segmentBuffer = /* @__PURE__ */ new Map();
|
|
3707
|
+
/** {@link streamTokens} text written to the bubble, awaiting the same sync. */
|
|
3708
|
+
_tokenBuffer = null;
|
|
3709
|
+
/** Handle of the scheduled coalesced sync, or null when none is pending. */
|
|
3710
|
+
_flushHandle = null;
|
|
3625
3711
|
_conversationId = null;
|
|
3626
3712
|
_controller;
|
|
3627
3713
|
_teardown = [];
|
|
@@ -3734,6 +3820,9 @@ class AparteChatHost {
|
|
|
3734
3820
|
}
|
|
3735
3821
|
/** Drop transient streaming state so a new conversation starts clean. */
|
|
3736
3822
|
_beginConversationSwap() {
|
|
3823
|
+
this._segmentBuffer.clear();
|
|
3824
|
+
this._tokenBuffer = null;
|
|
3825
|
+
this._flushStreamState();
|
|
3737
3826
|
this.stopTokenStream();
|
|
3738
3827
|
this._setStreamingId(null);
|
|
3739
3828
|
this.binding.onTypingChange?.(false);
|
|
@@ -3755,12 +3844,14 @@ class AparteChatHost {
|
|
|
3755
3844
|
* is the append-specific signal instead.
|
|
3756
3845
|
*/
|
|
3757
3846
|
appendMessage(message) {
|
|
3847
|
+
this._flushStreamState();
|
|
3758
3848
|
if (message.role === "user") this._vp()?.setAutoScroll?.(true);
|
|
3759
3849
|
this.binding.setMessages([...this.binding.getMessages(), message]);
|
|
3760
3850
|
this.binding.onMessageAppended?.(message);
|
|
3761
3851
|
}
|
|
3762
3852
|
/** Atomic partial update of a message by id. */
|
|
3763
3853
|
updateMessage(messageId, updates) {
|
|
3854
|
+
this._flushStreamState();
|
|
3764
3855
|
const msgs = this.binding.getMessages();
|
|
3765
3856
|
const index = msgs.findIndex((m) => m.id === messageId);
|
|
3766
3857
|
if (index === -1) return;
|
|
@@ -3786,6 +3877,7 @@ class AparteChatHost {
|
|
|
3786
3877
|
}
|
|
3787
3878
|
/** Add a segment to the last message (immediate bubble + state sync). */
|
|
3788
3879
|
addSegment(segment) {
|
|
3880
|
+
this._flushStreamState();
|
|
3789
3881
|
const msgs = this.binding.getMessages();
|
|
3790
3882
|
const last = msgs[msgs.length - 1];
|
|
3791
3883
|
if (last && this._isOrphan(last.id)) return;
|
|
@@ -3799,6 +3891,7 @@ class AparteChatHost {
|
|
|
3799
3891
|
}
|
|
3800
3892
|
/** Update a segment in the last message in place. */
|
|
3801
3893
|
updateSegment(segmentId, updates) {
|
|
3894
|
+
this._flushStreamState();
|
|
3802
3895
|
const msgs = this.binding.getMessages();
|
|
3803
3896
|
const last = msgs[msgs.length - 1];
|
|
3804
3897
|
if (last && this._isOrphan(last.id)) return;
|
|
@@ -3813,6 +3906,7 @@ class AparteChatHost {
|
|
|
3813
3906
|
}
|
|
3814
3907
|
/** Remove a transient segment (e.g. pipeline-waiting indicator). */
|
|
3815
3908
|
removeSegment(segmentId) {
|
|
3909
|
+
this._flushStreamState();
|
|
3816
3910
|
this._lastBubble()?.removeSegment?.(segmentId);
|
|
3817
3911
|
const msgs = this.binding.getMessages();
|
|
3818
3912
|
const last = msgs[msgs.length - 1];
|
|
@@ -3823,18 +3917,92 @@ class AparteChatHost {
|
|
|
3823
3917
|
this.binding.setMessages(next);
|
|
3824
3918
|
this.binding.onMessagesChange?.(next);
|
|
3825
3919
|
}
|
|
3826
|
-
/**
|
|
3920
|
+
/**
|
|
3921
|
+
* Append text to a segment's content in the last message — the streaming path
|
|
3922
|
+
* for thinking blocks, tool pills and any segment that grows token by token.
|
|
3923
|
+
*
|
|
3924
|
+
* Two writes, deliberately asymmetric:
|
|
3925
|
+
* 1. straight into the bubble, every chunk, so the text appears immediately —
|
|
3926
|
+
* the same direct path `appendToken` uses;
|
|
3927
|
+
* 2. into the framework's message list, **coalesced to once per frame**.
|
|
3928
|
+
*
|
|
3929
|
+
* It used to do only (2), synchronously: a full list rebuild plus
|
|
3930
|
+
* `setMessages` + `onMessagesChange` for every chunk, i.e. one complete
|
|
3931
|
+
* framework render per token. At 60 tok/s from a local model that is unusable,
|
|
3932
|
+
* and nothing in the imperative API hinted that `appendToSegment` cost so much
|
|
3933
|
+
* more than `injectTokenStream`. Consumers had to write their own rAF batcher.
|
|
3934
|
+
*
|
|
3935
|
+
* Any structural change flushes the buffer first (see
|
|
3936
|
+
* {@link _flushStreamState}), so ordering is never observable.
|
|
3937
|
+
*/
|
|
3827
3938
|
appendToSegment(segmentId, content) {
|
|
3828
3939
|
const msgs = this.binding.getMessages();
|
|
3829
3940
|
const last = msgs[msgs.length - 1];
|
|
3830
3941
|
if (!last || !last.segments) return;
|
|
3831
3942
|
if (this._isOrphan(last.id)) return;
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3943
|
+
let buffered = this._segmentBuffer.get(segmentId);
|
|
3944
|
+
if (!buffered) {
|
|
3945
|
+
const segment = last.segments.find((s) => s.id === segmentId);
|
|
3946
|
+
const base = segment && "content" in segment ? segment.content ?? "" : "";
|
|
3947
|
+
buffered = { base, text: "" };
|
|
3948
|
+
this._segmentBuffer.set(segmentId, buffered);
|
|
3949
|
+
}
|
|
3950
|
+
buffered.text += content;
|
|
3951
|
+
this._vp()?.appendToSegment?.(last.id, segmentId, content);
|
|
3952
|
+
this._scheduleStreamFlush();
|
|
3953
|
+
}
|
|
3954
|
+
/** Schedule the coalesced segment-state sync (idempotent within a frame). */
|
|
3955
|
+
_scheduleStreamFlush() {
|
|
3956
|
+
if (this._flushHandle !== null) return;
|
|
3957
|
+
const run = () => {
|
|
3958
|
+
this._flushHandle = null;
|
|
3959
|
+
this._flushStreamState();
|
|
3960
|
+
};
|
|
3961
|
+
this._flushHandle = typeof requestAnimationFrame === "function" ? requestAnimationFrame(run) : setTimeout(run, 0);
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* Write every buffered stream chunk — segment chunks and {@link streamTokens}
|
|
3965
|
+
* text alike — into the framework's message list in **one** update. Called by
|
|
3966
|
+
* the scheduled frame, and eagerly by any mutation that would otherwise observe
|
|
3967
|
+
* a stale list (a new segment, a new message, a conversation swap): flushing
|
|
3968
|
+
* first keeps the emitted order identical to the call order.
|
|
3969
|
+
*/
|
|
3970
|
+
_flushStreamState() {
|
|
3971
|
+
if (this._flushHandle !== null) {
|
|
3972
|
+
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(this._flushHandle);
|
|
3973
|
+
else clearTimeout(this._flushHandle);
|
|
3974
|
+
this._flushHandle = null;
|
|
3975
|
+
}
|
|
3976
|
+
const tokens = this._tokenBuffer;
|
|
3977
|
+
const pending = this._segmentBuffer.size > 0 ? new Map(this._segmentBuffer) : null;
|
|
3978
|
+
if (!tokens && !pending) return;
|
|
3979
|
+
this._tokenBuffer = null;
|
|
3980
|
+
this._segmentBuffer.clear();
|
|
3981
|
+
let msgs = this.binding.getMessages();
|
|
3982
|
+
let changed = false;
|
|
3983
|
+
if (tokens) {
|
|
3984
|
+
const index = msgs.findIndex((m) => m.id === tokens.messageId);
|
|
3985
|
+
if (index !== -1) {
|
|
3986
|
+
const next = [...msgs];
|
|
3987
|
+
next[index] = { ...next[index], content: (next[index].content ?? "") + tokens.text };
|
|
3988
|
+
msgs = next;
|
|
3989
|
+
changed = true;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
if (pending) {
|
|
3993
|
+
const last = msgs[msgs.length - 1];
|
|
3994
|
+
if (last?.segments) {
|
|
3995
|
+
const segments = last.segments.map((s) => {
|
|
3996
|
+
const buffered = pending.get(s.id);
|
|
3997
|
+
return buffered !== void 0 && "content" in s ? { ...s, content: buffered.base + buffered.text } : s;
|
|
3998
|
+
});
|
|
3999
|
+
msgs = [...msgs.slice(0, -1), { ...last, segments }];
|
|
4000
|
+
changed = true;
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
if (!changed) return;
|
|
4004
|
+
this.binding.setMessages(msgs);
|
|
4005
|
+
this.binding.onMessagesChange?.(msgs);
|
|
3838
4006
|
}
|
|
3839
4007
|
/** Read the current message list. */
|
|
3840
4008
|
getMessages() {
|
|
@@ -3880,6 +4048,13 @@ class AparteChatHost {
|
|
|
3880
4048
|
* appends each token, completes the message at the end. Per-framework
|
|
3881
4049
|
* wrappers adapt their native stream type (RxJS Observable, AsyncIterable,
|
|
3882
4050
|
* ReadableStream) into this.
|
|
4051
|
+
*
|
|
4052
|
+
* Each token reaches the bubble immediately and the framework's message list
|
|
4053
|
+
* is synced **once per frame** (same discipline as {@link appendToSegment}),
|
|
4054
|
+
* with a guaranteed flush before completion and on abort. Before that sync
|
|
4055
|
+
* existed the two disagreed: the DOM had the reply, the framework list still
|
|
4056
|
+
* held `content: ''` — so any re-render from state wiped it, and a custom
|
|
4057
|
+
* bubble (which renders from that state) showed nothing at all.
|
|
3883
4058
|
*/
|
|
3884
4059
|
async streamTokens(messageId, tokens) {
|
|
3885
4060
|
this.stopTokenStream();
|
|
@@ -3899,10 +4074,13 @@ class AparteChatHost {
|
|
|
3899
4074
|
const res = await Promise.race([iterator.next(), aborted]);
|
|
3900
4075
|
if (res === "aborted" || ac.signal.aborted || res.done) break;
|
|
3901
4076
|
this._vp()?.appendToken?.(messageId, res.value);
|
|
4077
|
+
this._bufferToken(messageId, res.value);
|
|
3902
4078
|
}
|
|
4079
|
+
this._flushStreamState();
|
|
3903
4080
|
if (!ac.signal.aborted) this._vp()?.completeMessage?.(messageId);
|
|
3904
4081
|
this._setStreamingId(null);
|
|
3905
4082
|
} catch (err) {
|
|
4083
|
+
this._flushStreamState();
|
|
3906
4084
|
this._setStreamingId(null);
|
|
3907
4085
|
throw err;
|
|
3908
4086
|
} finally {
|
|
@@ -3915,11 +4093,22 @@ class AparteChatHost {
|
|
|
3915
4093
|
if (this._streamAbort === ac) this._streamAbort = void 0;
|
|
3916
4094
|
}
|
|
3917
4095
|
}
|
|
4096
|
+
/**
|
|
4097
|
+
* Buffer a streamed token for the coalesced state sync. A different message id
|
|
4098
|
+
* (a second stream started without stopping the first) flushes the previous
|
|
4099
|
+
* one, so text never migrates between messages.
|
|
4100
|
+
*/
|
|
4101
|
+
_bufferToken(messageId, chunk) {
|
|
4102
|
+
if (this._tokenBuffer && this._tokenBuffer.messageId !== messageId) this._flushStreamState();
|
|
4103
|
+
this._tokenBuffer = this._tokenBuffer ? { messageId, text: this._tokenBuffer.text + chunk } : { messageId, text: chunk };
|
|
4104
|
+
this._scheduleStreamFlush();
|
|
4105
|
+
}
|
|
3918
4106
|
/** Abort an in-flight {@link streamTokens} loop. */
|
|
3919
4107
|
stopTokenStream() {
|
|
3920
4108
|
if (this._streamAbort) {
|
|
3921
4109
|
this._streamAbort.abort();
|
|
3922
4110
|
this._streamAbort = void 0;
|
|
4111
|
+
this._flushStreamState();
|
|
3923
4112
|
this._setStreamingId(null);
|
|
3924
4113
|
}
|
|
3925
4114
|
}
|
|
@@ -4407,6 +4596,8 @@ class AparteClient {
|
|
|
4407
4596
|
this._config = options.config ?? AparteConfig;
|
|
4408
4597
|
if (this.options.autoRegister) {
|
|
4409
4598
|
registerDefaultRenderers();
|
|
4599
|
+
} else {
|
|
4600
|
+
declineDefaultRenderers();
|
|
4410
4601
|
}
|
|
4411
4602
|
this._setupListeners();
|
|
4412
4603
|
}
|
|
@@ -4864,8 +5055,9 @@ ${summary}`,
|
|
|
4864
5055
|
const targetId = event.detail?.targetId;
|
|
4865
5056
|
if (targetId) {
|
|
4866
5057
|
const byId = document.getElementById(targetId);
|
|
4867
|
-
|
|
4868
|
-
|
|
5058
|
+
const resolved = this._asRenderTarget(byId);
|
|
5059
|
+
if (resolved) {
|
|
5060
|
+
targetElement = resolved;
|
|
4869
5061
|
} else {
|
|
4870
5062
|
console.warn("[AparteClient] ⚠️ targetId present but element not found or missing appendMessage:", targetId);
|
|
4871
5063
|
}
|
|
@@ -6093,6 +6285,10 @@ function applyElementProps(el2, props, transformValue = (value) => value) {
|
|
|
6093
6285
|
}
|
|
6094
6286
|
}
|
|
6095
6287
|
}
|
|
6288
|
+
function isAwaitingReply(message) {
|
|
6289
|
+
if (message.status === "streaming" || message.status === "pending") return true;
|
|
6290
|
+
return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
|
|
6291
|
+
}
|
|
6096
6292
|
function el(tag, className, text) {
|
|
6097
6293
|
const node = document.createElement(tag);
|
|
6098
6294
|
if (className) node.className = className;
|
|
@@ -6278,19 +6474,24 @@ export {
|
|
|
6278
6474
|
APARTE_CONVERSATION_SCHEMA_VERSION as A,
|
|
6279
6475
|
BackendTransport as B,
|
|
6280
6476
|
ConversationManager as C,
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6477
|
+
DEFAULT_BUBBLE_ACTIONS as D,
|
|
6478
|
+
filesToAttachments as E,
|
|
6479
|
+
getSegmentRenderer as F,
|
|
6480
|
+
isAwaitingReply as G,
|
|
6481
|
+
isFormatAdapter as H,
|
|
6482
|
+
isSafeUrl as I,
|
|
6483
|
+
parseAparteEventStream as J,
|
|
6484
|
+
parseMarkdownToSegments as K,
|
|
6485
|
+
populateBubbleFromMessage as L,
|
|
6290
6486
|
MessageRepository as M,
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6487
|
+
readableToAsyncIterable as N,
|
|
6488
|
+
registerDefaultRenderers as O,
|
|
6489
|
+
registerSegmentRenderer as P,
|
|
6490
|
+
requestUserInput as Q,
|
|
6491
|
+
resolveConfig as R,
|
|
6492
|
+
runWithConfig as S,
|
|
6493
|
+
unregisterSegmentRenderer as T,
|
|
6494
|
+
installDefaultRenderersOnce as U,
|
|
6294
6495
|
APARTE_HOST_ATTR as a,
|
|
6295
6496
|
AparteChatHost as b,
|
|
6296
6497
|
AparteClient as c,
|
|
@@ -6300,22 +6501,22 @@ export {
|
|
|
6300
6501
|
AparteError as g,
|
|
6301
6502
|
AparteErrorCode as h,
|
|
6302
6503
|
AparteStreamParser as i,
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6504
|
+
DEFAULT_HOST_HANDLERS as j,
|
|
6505
|
+
DEFAULT_ICON_FALLBACKS as k,
|
|
6506
|
+
DEFAULT_LOCALE as l,
|
|
6507
|
+
DEFAULT_SKELETON_FALLBACKS as m,
|
|
6508
|
+
DEFAULT_UI_EVENTS as n,
|
|
6509
|
+
DirectTransport as o,
|
|
6510
|
+
applyElementProps as p,
|
|
6511
|
+
attachConfig as q,
|
|
6512
|
+
buildElicitationPanel as r,
|
|
6513
|
+
collectRendererStyles as s,
|
|
6514
|
+
contentToText as t,
|
|
6515
|
+
contextConfig as u,
|
|
6516
|
+
createAparteChatHandler as v,
|
|
6517
|
+
createStreamAdapter as w,
|
|
6518
|
+
defaultSanitizer as x,
|
|
6519
|
+
deriveArtifactKind as y,
|
|
6520
|
+
detachConfig as z
|
|
6320
6521
|
};
|
|
6321
|
-
//# sourceMappingURL=index-
|
|
6522
|
+
//# sourceMappingURL=index-Dn7TH14G.js.map
|