@aparte/core 0.7.1 → 0.9.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 +20 -6
- package/dist/client/aparte-client.d.ts +113 -47
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/lifecycle-events.d.ts +55 -0
- package/dist/client/lifecycle-events.d.ts.map +1 -0
- package/dist/client/stream-adapter.d.ts +11 -5
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/client/xml-artifact-feed.d.ts +76 -0
- package/dist/client/xml-artifact-feed.d.ts.map +1 -0
- package/dist/components/bubble/aparte-chat-bubble.d.ts +35 -8
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/bubble/bubble-sync.d.ts.map +1 -1
- package/dist/components/chat/aparte-chat.d.ts +0 -6
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-action.d.ts +18 -3
- package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-input.d.ts +0 -2
- package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-send.d.ts +5 -0
- package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-toolbar.d.ts +4 -0
- package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer.d.ts +64 -6
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/composer/index.d.ts +1 -1
- package/dist/components/composer/index.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +2 -5
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/conversation-list/index.d.ts +1 -1
- package/dist/components/conversation-list/index.d.ts.map +1 -1
- package/dist/components/elicitation/aparte-elicitation.d.ts +63 -3
- package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/status/aparte-chat-status.d.ts +8 -6
- package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +45 -9
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/action-provider.d.ts +4 -4
- package/dist/config/aparte-config.d.ts +124 -41
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/attachment-renderer.d.ts +1 -1
- package/dist/config/avatar-provider.d.ts +1 -1
- package/dist/config/config-context.d.ts +66 -11
- package/dist/config/config-context.d.ts.map +1 -1
- package/dist/config/elicitation-field-renderer.d.ts +73 -0
- package/dist/config/elicitation-field-renderer.d.ts.map +1 -0
- package/dist/config/error-renderer.d.ts +1 -1
- package/dist/config/icon-provider.d.ts +2 -2
- package/dist/config/icon-provider.d.ts.map +1 -1
- package/dist/config/index.d.ts +7 -5
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/locale.d.ts +43 -2
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/config/sanitize.d.ts +1 -7
- package/dist/config/sanitize.d.ts.map +1 -1
- package/dist/config/sibling-nav-renderer.d.ts +1 -1
- package/dist/config/skeleton-provider.d.ts +2 -2
- package/dist/config/skeleton-provider.d.ts.map +1 -1
- package/dist/config/status-renderer.d.ts +1 -1
- package/dist/conversations/conversation-controller.d.ts +19 -5
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/conversation-manager.d.ts +2 -2
- package/dist/conversations/conversation-manager.d.ts.map +1 -1
- package/dist/conversations/index.d.ts +1 -1
- package/dist/conversations/index.d.ts.map +1 -1
- package/dist/conversations/types.d.ts +1 -1
- package/dist/custom-elements.json +8418 -6295
- package/dist/elicitation/index.d.ts +1 -1
- package/dist/elicitation/panel.d.ts +32 -0
- package/dist/elicitation/panel.d.ts.map +1 -1
- package/dist/elicitation/types.d.ts +29 -2
- package/dist/elicitation/types.d.ts.map +1 -1
- package/dist/host/aparte-chat-host.d.ts +3 -3
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/{index-Dn7TH14G.js → index-hQQljwfX.js} +1890 -1222
- package/dist/index-hQQljwfX.js.map +1 -0
- package/dist/index.css +299 -21
- package/dist/index.d.ts +22 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +485 -210
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +23 -13
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +56 -46
- package/dist/index.node.js.map +1 -1
- package/dist/interop/element-props.d.ts +1 -1
- package/dist/interop/element-props.d.ts.map +1 -1
- package/dist/parsers/aparte-stream-parser.d.ts +25 -0
- package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
- package/dist/primitives/index.d.ts +1 -1
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/select/aparte-optgroup.d.ts +15 -4
- package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
- package/dist/primitives/select/aparte-option.d.ts +0 -5
- package/dist/primitives/select/aparte-option.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts +17 -5
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/primitives/select/index.d.ts +1 -1
- package/dist/primitives/select/index.d.ts.map +1 -1
- package/dist/renderers/markdown-stream.d.ts +42 -0
- package/dist/renderers/markdown-stream.d.ts.map +1 -0
- package/dist/renderers/segment-renderers.d.ts +20 -14
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/renderers/segments/artifact/binary-file.d.ts +45 -0
- package/dist/renderers/segments/artifact/binary-file.d.ts.map +1 -0
- package/dist/renderers/segments/artifact/card.d.ts +3 -0
- package/dist/renderers/segments/artifact/card.d.ts.map +1 -0
- package/dist/renderers/segments/artifact/preview-document.d.ts +27 -0
- package/dist/renderers/segments/artifact/preview-document.d.ts.map +1 -0
- package/dist/renderers/segments/artifact/shared.d.ts +12 -0
- package/dist/renderers/segments/artifact/shared.d.ts.map +1 -0
- package/dist/renderers/segments/code.d.ts +3 -0
- package/dist/renderers/segments/code.d.ts.map +1 -0
- package/dist/renderers/segments/error.d.ts +3 -0
- package/dist/renderers/segments/error.d.ts.map +1 -0
- package/dist/renderers/segments/file-tree.d.ts +3 -0
- package/dist/renderers/segments/file-tree.d.ts.map +1 -0
- package/dist/renderers/segments/pipeline-waiting.d.ts +3 -0
- package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -0
- package/dist/renderers/segments/progress.d.ts +3 -0
- package/dist/renderers/segments/progress.d.ts.map +1 -0
- package/dist/renderers/segments/terminal.d.ts +3 -0
- package/dist/renderers/segments/terminal.d.ts.map +1 -0
- package/dist/renderers/segments/text.d.ts +10 -0
- package/dist/renderers/segments/text.d.ts.map +1 -0
- package/dist/renderers/segments/thinking.d.ts +3 -0
- package/dist/renderers/segments/thinking.d.ts.map +1 -0
- package/dist/renderers/segments/tool-call.d.ts +3 -0
- package/dist/renderers/segments/tool-call.d.ts.map +1 -0
- package/dist/runtime/message-repository.d.ts +3 -3
- package/dist/runtime/message-repository.d.ts.map +1 -1
- package/dist/transport/backend-handler.d.ts +19 -7
- package/dist/transport/backend-handler.d.ts.map +1 -1
- package/dist/transport/backend-transport.d.ts +1 -1
- package/dist/transport/backend-transport.d.ts.map +1 -1
- package/dist/transport/direct-transport.d.ts +4 -4
- package/dist/transport/direct-transport.d.ts.map +1 -1
- package/dist/transport/index.d.ts +2 -2
- package/dist/transport/index.d.ts.map +1 -1
- package/dist/transport/types.d.ts +3 -3
- package/dist/types/element-map.d.ts +69 -0
- package/dist/types/element-map.d.ts.map +1 -0
- package/dist/types/event-map.d.ts +84 -23
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/events.d.ts +212 -64
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/model-provider.d.ts +100 -54
- package/dist/types/model-provider.d.ts.map +1 -1
- package/dist/types/models.d.ts +29 -7
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +33 -8
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/theming.d.ts +25 -37
- package/dist/types/theming.d.ts.map +1 -1
- package/dist/types/tools.d.ts +60 -15
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/escape.d.ts +46 -0
- package/dist/utils/escape.d.ts.map +1 -0
- package/dist/utils/files-to-attachments.d.ts +18 -1
- package/dist/utils/files-to-attachments.d.ts.map +1 -1
- package/dist/utils/segments.d.ts +174 -0
- package/dist/utils/segments.d.ts.map +1 -0
- package/dist/utils/uuid.d.ts +26 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/package.json +3 -5
- package/dist/index-Dn7TH14G.js.map +0 -1
- package/dist/types/providers.d.ts +0 -93
- package/dist/types/providers.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A, a, b, c, e, f, g, h, i,
|
|
1
|
+
import { H as escapeAttr, Y as resolveConfig, C as cssEscape, _ as runWithConfig, I as escapeHtml, L as getSegmentRenderer, M as installDefaultRenderersOnce, q as AparteMessageRepository, a2 as stampSegmentActivity, a3 as stampSegmentOnInsert, a4 as stampSegmentOnUpdate, a5 as renumberSegments, a6 as openSegmentIds, a7 as isTerminalStatus, N as isAwaitingReply, T as populateBubbleFromMessage, a1 as uuid, Z as revokeAttachmentUrls, v as buildElicitationPanel } from "./index-hQQljwfX.js";
|
|
2
|
+
import { A, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, r, s, t, u, w, x, y, z, B, D, E, F, G, J, K, O, P, Q, R, S, U, V, W, X, $, a0 } from "./index-hQQljwfX.js";
|
|
3
3
|
class AparteOption extends HTMLElement {
|
|
4
4
|
static get observedAttributes() {
|
|
5
5
|
return ["value", "disabled", "selected", "data-status"];
|
|
@@ -192,6 +192,22 @@ class AparteSelect extends HTMLElement {
|
|
|
192
192
|
_searchInput = null;
|
|
193
193
|
_observer = null;
|
|
194
194
|
// Bound handlers for cleanup
|
|
195
|
+
/**
|
|
196
|
+
* Bound, like its two neighbours below — because an inline arrow on `this` can
|
|
197
|
+
* never be removed.
|
|
198
|
+
*
|
|
199
|
+
* `_setupEventListeners()` runs on EVERY `connectedCallback`, and `_render()`'s
|
|
200
|
+
* idempotency guard means the host element survives a re-connect. So each move
|
|
201
|
+
* of the element (a portal, a Vue teleport, any framework re-parent) added
|
|
202
|
+
* another option-click listener: measured, one click fired the change handler
|
|
203
|
+
* FIVE times after five re-connects. `disconnectedCallback` removed the two
|
|
204
|
+
* document-level handlers and could not touch this one.
|
|
205
|
+
*
|
|
206
|
+
* This is verbatim the bug class `aparte-chat-viewport` documents having fixed
|
|
207
|
+
* for its own listeners; a repo sweep found this as the last inline-arrow
|
|
208
|
+
* listener on a persistent node.
|
|
209
|
+
*/
|
|
210
|
+
_boundHandleOptionClick = this._handleOptionClick.bind(this);
|
|
195
211
|
_boundHandleDocumentClick = this._handleDocumentClick.bind(this);
|
|
196
212
|
_boundHandleKeydown = this._handleKeydown.bind(this);
|
|
197
213
|
static get observedAttributes() {
|
|
@@ -205,6 +221,7 @@ class AparteSelect extends HTMLElement {
|
|
|
205
221
|
this._setupMutationObserver();
|
|
206
222
|
}
|
|
207
223
|
disconnectedCallback() {
|
|
224
|
+
this.removeEventListener("click", this._boundHandleOptionClick);
|
|
208
225
|
document.removeEventListener("click", this._boundHandleDocumentClick);
|
|
209
226
|
document.removeEventListener("keydown", this._boundHandleKeydown);
|
|
210
227
|
this._observer?.disconnect();
|
|
@@ -368,12 +385,8 @@ class AparteSelect extends HTMLElement {
|
|
|
368
385
|
this._openDropdown();
|
|
369
386
|
}
|
|
370
387
|
});
|
|
371
|
-
this.
|
|
372
|
-
|
|
373
|
-
if (option && !option.hasAttribute("disabled")) {
|
|
374
|
-
this._selectOption(option);
|
|
375
|
-
}
|
|
376
|
-
});
|
|
388
|
+
this.removeEventListener("click", this._boundHandleOptionClick);
|
|
389
|
+
this.addEventListener("click", this._boundHandleOptionClick);
|
|
377
390
|
this._searchInput?.addEventListener("input", (e2) => {
|
|
378
391
|
const query = e2.target.value.toLowerCase();
|
|
379
392
|
this._filterOptions(query);
|
|
@@ -381,6 +394,12 @@ class AparteSelect extends HTMLElement {
|
|
|
381
394
|
document.addEventListener("click", this._boundHandleDocumentClick);
|
|
382
395
|
document.addEventListener("keydown", this._boundHandleKeydown);
|
|
383
396
|
}
|
|
397
|
+
_handleOptionClick(e2) {
|
|
398
|
+
const option = e2.target.closest("aparte-option");
|
|
399
|
+
if (option && !option.hasAttribute("disabled")) {
|
|
400
|
+
this._selectOption(option);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
384
403
|
_handleDocumentClick(e2) {
|
|
385
404
|
if (!this.contains(e2.target)) {
|
|
386
405
|
this._closeDropdown();
|
|
@@ -660,7 +679,7 @@ class AparteChat extends HTMLElement {
|
|
|
660
679
|
if (this.hasAttribute("framework-managed")) return;
|
|
661
680
|
if (this.querySelector("aparte-chat-viewport")) return;
|
|
662
681
|
const placeholder = this.getAttribute("placeholder");
|
|
663
|
-
const composerAttrs = (placeholder !== null ? ` placeholder="${
|
|
682
|
+
const composerAttrs = (placeholder !== null ? ` placeholder="${escapeAttr(placeholder)}"` : "") + (this.hasAttribute("disabled") ? " disabled" : "");
|
|
664
683
|
const attachments = this.hasAttribute("attachments");
|
|
665
684
|
this.innerHTML = `
|
|
666
685
|
<aparte-chat-viewport></aparte-chat-viewport>
|
|
@@ -723,27 +742,21 @@ class AparteChat extends HTMLElement {
|
|
|
723
742
|
const empty = !viewport || !viewport.querySelector("aparte-chat-bubble");
|
|
724
743
|
this.toggleAttribute("data-empty", empty);
|
|
725
744
|
}
|
|
726
|
-
_escapeAttr(value) {
|
|
727
|
-
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
728
|
-
}
|
|
729
745
|
}
|
|
730
746
|
if (!customElements.get("aparte-chat")) {
|
|
731
747
|
customElements.define("aparte-chat", AparteChat);
|
|
732
748
|
}
|
|
733
|
-
function cssEscape(value) {
|
|
734
|
-
return value.replace(/["\\]/g, "\\$&");
|
|
735
|
-
}
|
|
736
749
|
let _warnedNoRenderer = false;
|
|
737
750
|
function warnMissingRenderer(type) {
|
|
738
751
|
if (_warnedNoRenderer) return;
|
|
739
752
|
_warnedNoRenderer = true;
|
|
740
753
|
console.warn(`[aparte] No renderer for segment "${type}". Register one with registerSegmentRenderer({ type: '${type}', render }) from @aparte/core — see https://apartejs.dev/guides/customization/#custom-segment-types`);
|
|
741
754
|
}
|
|
742
|
-
function resolveSegmentRenderer(type) {
|
|
743
|
-
const renderer = getSegmentRenderer(type);
|
|
755
|
+
function resolveSegmentRenderer(type, config) {
|
|
756
|
+
const renderer = getSegmentRenderer(type, config);
|
|
744
757
|
if (renderer) return renderer;
|
|
745
|
-
installDefaultRenderersOnce();
|
|
746
|
-
return getSegmentRenderer(type);
|
|
758
|
+
installDefaultRenderersOnce(config);
|
|
759
|
+
return getSegmentRenderer(type, config);
|
|
747
760
|
}
|
|
748
761
|
function segmentRenderResultToElement(result) {
|
|
749
762
|
if (result instanceof HTMLElement) return result;
|
|
@@ -820,9 +833,11 @@ class AparteChatBubble extends HTMLElement {
|
|
|
820
833
|
this._updateContent();
|
|
821
834
|
this._updateTimestamp(this.getAttribute("timestamp"));
|
|
822
835
|
window.addEventListener("aparte-config-change", this._onConfigChange);
|
|
836
|
+
this.addEventListener("click", this._onBranchPickerClick);
|
|
823
837
|
}
|
|
824
838
|
disconnectedCallback() {
|
|
825
839
|
window.removeEventListener("aparte-config-change", this._onConfigChange);
|
|
840
|
+
this.removeEventListener("click", this._onBranchPickerClick);
|
|
826
841
|
if (this._avatarCleanup) {
|
|
827
842
|
try {
|
|
828
843
|
this._avatarCleanup();
|
|
@@ -909,12 +924,26 @@ class AparteChatBubble extends HTMLElement {
|
|
|
909
924
|
return [...this._segments];
|
|
910
925
|
}
|
|
911
926
|
/** Remove a segment by id (e.g. to discard a transient waiting indicator) */
|
|
927
|
+
/**
|
|
928
|
+
* Scoped to DIRECT children on purpose.
|
|
929
|
+
*
|
|
930
|
+
* Segments are appended as direct children of the container, but a descendant
|
|
931
|
+
* query returns the first match in document order — and sanitized model
|
|
932
|
+
* markdown renders inside that same container, with `data-*` attributes
|
|
933
|
+
* deliberately preserved (they are inert). So a decoy `data-segment-id` planted
|
|
934
|
+
* in an earlier segment's prose used to win over the real segment element.
|
|
935
|
+
*
|
|
936
|
+
* Parser ids are unguessable UUIDs, but a tool segment is `tool-${toolCallId}`
|
|
937
|
+
* and the MODEL chooses that id — so this was reachable, and pointing an update
|
|
938
|
+
* at a decoy left a rejected tool rendering as still-running: a spoof against
|
|
939
|
+
* the human-in-the-loop control.
|
|
940
|
+
*/
|
|
912
941
|
removeSegment(segmentId) {
|
|
913
942
|
const index = this._segments.findIndex((s2) => s2.id === segmentId);
|
|
914
943
|
if (index !== -1) {
|
|
915
944
|
this._segments.splice(index, 1);
|
|
916
945
|
}
|
|
917
|
-
const el = this._segmentsEl?.querySelector(
|
|
946
|
+
const el = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
|
|
918
947
|
el?.remove();
|
|
919
948
|
this._updateWaiting();
|
|
920
949
|
}
|
|
@@ -928,7 +957,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
928
957
|
*
|
|
929
958
|
* This is the *precondition* for the info ("i") action, not the trigger: the
|
|
930
959
|
* button appears only if the app also declared it wants it —
|
|
931
|
-
* `
|
|
960
|
+
* `aparteGlobalConfig.setBubbleActions({ info: true })` — because the stats popover it
|
|
932
961
|
* opens (`aparte-message-info`) is the app's, and core has none. Without usage
|
|
933
962
|
* there is nothing to show, so the button never renders either way.
|
|
934
963
|
*/
|
|
@@ -983,7 +1012,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
983
1012
|
console.warn(`[AparteChatBubble] _appendSegmentEl ABORT: _segmentsEl is null`);
|
|
984
1013
|
return;
|
|
985
1014
|
}
|
|
986
|
-
const renderer = resolveSegmentRenderer(segment.type);
|
|
1015
|
+
const renderer = resolveSegmentRenderer(segment.type, this._cfg);
|
|
987
1016
|
if (renderer) {
|
|
988
1017
|
const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
|
|
989
1018
|
if (el) {
|
|
@@ -1001,12 +1030,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1001
1030
|
this._reflectError();
|
|
1002
1031
|
}
|
|
1003
1032
|
_applySegmentUpdate(segmentId, segment, updates) {
|
|
1004
|
-
const el = this._segmentsEl?.querySelector(
|
|
1033
|
+
const el = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
|
|
1005
1034
|
if (!el) {
|
|
1006
1035
|
this._renderSegments();
|
|
1007
1036
|
return;
|
|
1008
1037
|
}
|
|
1009
|
-
const renderer = resolveSegmentRenderer(segment.type);
|
|
1038
|
+
const renderer = resolveSegmentRenderer(segment.type, this._cfg);
|
|
1010
1039
|
if (!renderer) return;
|
|
1011
1040
|
if (renderer.update) {
|
|
1012
1041
|
runWithConfig(this._cfg, () => renderer.update(el, segment));
|
|
@@ -1056,11 +1085,11 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1056
1085
|
else this.innerHTML = out;
|
|
1057
1086
|
} else {
|
|
1058
1087
|
this.innerHTML = `
|
|
1059
|
-
<div class="aparte-message" data-role="${role}" role="article" aria-label="${this._getAriaLabel()}">
|
|
1060
|
-
<div class="aparte-avatar" data-role="${role}"></div>
|
|
1088
|
+
<div class="aparte-message" data-role="${escapeAttr(role)}" role="article" aria-label="${escapeAttr(this._getAriaLabel())}">
|
|
1089
|
+
<div class="aparte-avatar" data-role="${escapeAttr(role)}"></div>
|
|
1061
1090
|
<div class="aparte-body">
|
|
1062
1091
|
<div class="aparte-header">
|
|
1063
|
-
<span class="aparte-name">${
|
|
1092
|
+
<span class="aparte-name">${escapeHtml(displayName)}</span>
|
|
1064
1093
|
<span class="aparte-timestamp"></span>
|
|
1065
1094
|
</div>
|
|
1066
1095
|
<div class="aparte-attachments" hidden></div>
|
|
@@ -1074,11 +1103,11 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1074
1103
|
</div>
|
|
1075
1104
|
<div class="aparte-footer">
|
|
1076
1105
|
<div class="aparte-branch-picker" hidden>
|
|
1077
|
-
<button class="aparte-branch-prev" aria-label="${this._cfg.getLocale().previousResponse ?? "Previous response"}">‹</button>
|
|
1106
|
+
<button class="aparte-branch-prev" aria-label="${escapeAttr(this._cfg.getLocale().previousResponse ?? "Previous response")}">‹</button>
|
|
1078
1107
|
<span class="aparte-branch-label">1 / 1</span>
|
|
1079
|
-
<button class="aparte-branch-next" aria-label="${this._cfg.getLocale().nextResponse ?? "Next response"}">›</button>
|
|
1108
|
+
<button class="aparte-branch-next" aria-label="${escapeAttr(this._cfg.getLocale().nextResponse ?? "Next response")}">›</button>
|
|
1080
1109
|
</div>
|
|
1081
|
-
<div class="aparte-action-bar" role="toolbar" aria-label="${this._cfg.getLocale().messageActions ?? "Message actions"}"></div>
|
|
1110
|
+
<div class="aparte-action-bar" role="toolbar" aria-label="${escapeAttr(this._cfg.getLocale().messageActions ?? "Message actions")}"></div>
|
|
1082
1111
|
</div>
|
|
1083
1112
|
</div>
|
|
1084
1113
|
</div>
|
|
@@ -1090,7 +1119,6 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1090
1119
|
this._actionBarEl = this.querySelector(".aparte-action-bar");
|
|
1091
1120
|
this._branchPickerEl = this.querySelector(".aparte-branch-picker");
|
|
1092
1121
|
this._footerEl = this.querySelector(".aparte-footer");
|
|
1093
|
-
this._setupBranchPickerListeners();
|
|
1094
1122
|
this._updateActionBar();
|
|
1095
1123
|
this._renderAvatar();
|
|
1096
1124
|
if (this._streaming) this._updateStreaming(true);
|
|
@@ -1204,7 +1232,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1204
1232
|
if (!this._segmentsEl) return;
|
|
1205
1233
|
this._segmentsEl.innerHTML = "";
|
|
1206
1234
|
for (const segment of this._segments) {
|
|
1207
|
-
const renderer = resolveSegmentRenderer(segment.type);
|
|
1235
|
+
const renderer = resolveSegmentRenderer(segment.type, this._cfg);
|
|
1208
1236
|
if (renderer) {
|
|
1209
1237
|
const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
|
|
1210
1238
|
if (el) {
|
|
@@ -1276,11 +1304,11 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1276
1304
|
return;
|
|
1277
1305
|
}
|
|
1278
1306
|
this._attachmentsEl.innerHTML = this._attachments.map((a2) => {
|
|
1279
|
-
const name =
|
|
1307
|
+
const name = escapeHtml(a2.name);
|
|
1280
1308
|
if (a2.type.startsWith("image/")) {
|
|
1281
|
-
return `<div class="aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${
|
|
1309
|
+
return `<div class="aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${escapeHtml(a2.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
|
|
1282
1310
|
}
|
|
1283
|
-
return `<div class="aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${
|
|
1311
|
+
return `<div class="aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${escapeHtml(this._fileExt(a2.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
|
|
1284
1312
|
}).join("");
|
|
1285
1313
|
if (!this._cfg.getHostHandlers().attachmentPreview) return;
|
|
1286
1314
|
this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
|
|
@@ -1312,22 +1340,39 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1312
1340
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1313
1341
|
// Branch Picker
|
|
1314
1342
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1343
|
+
/**
|
|
1344
|
+
* The branch arrows are handled by DELEGATION, on this element, bound once.
|
|
1345
|
+
*
|
|
1346
|
+
* They used to get a fresh listener each, attached by `_render()` to the buttons
|
|
1347
|
+
* `_render()` had just created. So a click that landed while a re-render was
|
|
1348
|
+
* swapping those nodes hit an element about to be discarded, and did nothing at
|
|
1349
|
+
* all — not late, nothing. Invisible on a fast machine; reproducible on
|
|
1350
|
+
* WebKit-Linux in CI, where `‹` left the picker on "2 / 2" and a 20-second
|
|
1351
|
+
* assertion watched it stay there.
|
|
1352
|
+
*
|
|
1353
|
+
* Delegation makes `_render()` irrelevant to it: the listener lives on the host,
|
|
1354
|
+
* which is never replaced, and `closest()` finds whichever button exists at the
|
|
1355
|
+
* moment of the click. It is also less work — one listener per bubble instead of
|
|
1356
|
+
* two per bubble per render.
|
|
1357
|
+
*
|
|
1358
|
+
* Bound in `connectedCallback` and removed in `disconnectedCallback` as a stable
|
|
1359
|
+
* field, because an inline arrow re-added on every re-connect is how this repo has
|
|
1360
|
+
* stacked listeners twice before (the viewport, and `aparte-select`).
|
|
1361
|
+
*/
|
|
1362
|
+
_onBranchPickerClick = (event) => {
|
|
1363
|
+
const target = event.target;
|
|
1364
|
+
const button = target?.closest?.(".aparte-branch-prev, .aparte-branch-next");
|
|
1365
|
+
if (!button || !this.contains(button)) return;
|
|
1366
|
+
const direction = button.classList.contains("aparte-branch-prev") ? "prev" : "next";
|
|
1367
|
+
const messageId = this.getAttribute("message-id");
|
|
1368
|
+
if (!messageId) return;
|
|
1369
|
+
const detail = { messageId, direction };
|
|
1370
|
+
this.dispatchEvent(new CustomEvent("aparte-branch-navigate", {
|
|
1371
|
+
bubbles: true,
|
|
1372
|
+
composed: true,
|
|
1373
|
+
detail
|
|
1374
|
+
}));
|
|
1375
|
+
};
|
|
1331
1376
|
_updateBranchPicker() {
|
|
1332
1377
|
if (!this._branchPickerEl) return;
|
|
1333
1378
|
if (this._siblingCount <= 1 || this._role !== "assistant") {
|
|
@@ -1428,28 +1473,28 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1428
1473
|
switch (action) {
|
|
1429
1474
|
case "copy": {
|
|
1430
1475
|
const l2 = locale.copy ?? "Copy";
|
|
1431
|
-
return `<button class="aparte-action-btn aparte-action-copy" data-action="copy" aria-label="${l2}" title="${l2}">${icons.copy()}</button>`;
|
|
1476
|
+
return `<button class="aparte-action-btn aparte-action-copy" data-action="copy" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.copy()}</button>`;
|
|
1432
1477
|
}
|
|
1433
1478
|
case "edit": {
|
|
1434
1479
|
const l2 = locale.edit ?? "Edit message";
|
|
1435
|
-
return `<button class="aparte-action-btn aparte-action-edit" data-action="edit" aria-label="${l2}" title="${l2}">${icons.edit()}</button>`;
|
|
1480
|
+
return `<button class="aparte-action-btn aparte-action-edit" data-action="edit" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.edit()}</button>`;
|
|
1436
1481
|
}
|
|
1437
1482
|
case "retry": {
|
|
1438
1483
|
const l2 = locale.retry ?? "Retry";
|
|
1439
|
-
return `<button class="aparte-action-btn aparte-action-retry" data-action="retry" aria-label="${l2}" title="${l2}">${icons.retry()}</button>`;
|
|
1484
|
+
return `<button class="aparte-action-btn aparte-action-retry" data-action="retry" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.retry()}</button>`;
|
|
1440
1485
|
}
|
|
1441
1486
|
case "thumbUp": {
|
|
1442
1487
|
const l2 = locale.feedbackPositive ?? "Good response";
|
|
1443
|
-
return `<button class="aparte-action-btn aparte-action-feedback-pos" data-action="feedback-positive" aria-label="${l2}" title="${l2}">${icons.thumbUp()}</button>`;
|
|
1488
|
+
return `<button class="aparte-action-btn aparte-action-feedback-pos" data-action="feedback-positive" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.thumbUp()}</button>`;
|
|
1444
1489
|
}
|
|
1445
1490
|
case "thumbDown": {
|
|
1446
1491
|
const l2 = locale.feedbackNegative ?? "Bad response";
|
|
1447
|
-
return `<button class="aparte-action-btn aparte-action-feedback-neg" data-action="feedback-negative" aria-label="${l2}" title="${l2}">${icons.thumbDown()}</button>`;
|
|
1492
|
+
return `<button class="aparte-action-btn aparte-action-feedback-neg" data-action="feedback-negative" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.thumbDown()}</button>`;
|
|
1448
1493
|
}
|
|
1449
1494
|
case "info": {
|
|
1450
1495
|
if (!this._usage) return "";
|
|
1451
1496
|
const l2 = locale.messageInfo ?? "Details";
|
|
1452
|
-
return `<button class="aparte-action-btn aparte-action-info" data-action="info" aria-label="${l2}" title="${l2}">${INFO_ICON_SVG}</button>`;
|
|
1497
|
+
return `<button class="aparte-action-btn aparte-action-info" data-action="info" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${INFO_ICON_SVG}</button>`;
|
|
1453
1498
|
}
|
|
1454
1499
|
default:
|
|
1455
1500
|
return "";
|
|
@@ -1461,7 +1506,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1461
1506
|
const locale = this._cfg.getLocale();
|
|
1462
1507
|
const saveLabel = locale.editConfirm ?? "Save";
|
|
1463
1508
|
const cancelLabel = locale.editCancel ?? "Cancel";
|
|
1464
|
-
this._actionBarEl.innerHTML = `<button class="aparte-action-btn aparte-action-edit-save" data-action="edit-save" aria-label="${saveLabel}" title="${saveLabel}">${this._cfg.getIcon("check")}</button><button class="aparte-action-btn aparte-action-edit-cancel" data-action="edit-cancel" aria-label="${cancelLabel}" title="${cancelLabel}">${this._cfg.getIcon("close")}</button>`;
|
|
1509
|
+
this._actionBarEl.innerHTML = `<button class="aparte-action-btn aparte-action-edit-save" data-action="edit-save" aria-label="${escapeAttr(saveLabel)}" title="${escapeAttr(saveLabel)}">${this._cfg.getIcon("check")}</button><button class="aparte-action-btn aparte-action-edit-cancel" data-action="edit-cancel" aria-label="${escapeAttr(cancelLabel)}" title="${escapeAttr(cancelLabel)}">${this._cfg.getIcon("close")}</button>`;
|
|
1465
1510
|
this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
|
|
1466
1511
|
btn.addEventListener("click", (e2) => this._handleActionClick(e2));
|
|
1467
1512
|
});
|
|
@@ -1636,9 +1681,6 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1636
1681
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1637
1682
|
// Utilities
|
|
1638
1683
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1639
|
-
_escapeHtml(str) {
|
|
1640
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1641
|
-
}
|
|
1642
1684
|
_updateStreaming(streaming) {
|
|
1643
1685
|
const wasStreaming = this._streaming;
|
|
1644
1686
|
this._streaming = streaming;
|
|
@@ -1664,12 +1706,20 @@ class AparteChatStatus extends HTMLElement {
|
|
|
1664
1706
|
static get observedAttributes() {
|
|
1665
1707
|
return ["visible", "text"];
|
|
1666
1708
|
}
|
|
1667
|
-
|
|
1709
|
+
/**
|
|
1710
|
+
* Resolved LIVE, not cached. Caching it at connect made this element
|
|
1711
|
+
* permanently deaf to its own instance: `_onConfigChange` filters on
|
|
1712
|
+
* `detail.config !== this._cfg`, so once `_cfg` had latched the global config no
|
|
1713
|
+
* change for the real instance ever matched, and the filter meant to isolate
|
|
1714
|
+
* chats became the thing that silenced one.
|
|
1715
|
+
*/
|
|
1716
|
+
get _cfg() {
|
|
1717
|
+
return resolveConfig(this);
|
|
1718
|
+
}
|
|
1668
1719
|
constructor() {
|
|
1669
1720
|
super();
|
|
1670
1721
|
}
|
|
1671
1722
|
connectedCallback() {
|
|
1672
|
-
this._cfg = resolveConfig(this);
|
|
1673
1723
|
this._render();
|
|
1674
1724
|
window.addEventListener("aparte-config-change", this._onConfigChange);
|
|
1675
1725
|
}
|
|
@@ -1792,7 +1842,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1792
1842
|
_spacerRafId = null;
|
|
1793
1843
|
_spacerFrozenUntil = 0;
|
|
1794
1844
|
_layoutTransitionMs = 0;
|
|
1795
|
-
_repo = new
|
|
1845
|
+
_repo = new AparteMessageRepository();
|
|
1796
1846
|
_isAutoScrollEnabled = true;
|
|
1797
1847
|
_scrollThreshold = 50;
|
|
1798
1848
|
/** When true, the next _autoScroll() call uses smooth instead of instant, then resets. */
|
|
@@ -1928,7 +1978,13 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1928
1978
|
if (segment && "content" in segment) {
|
|
1929
1979
|
message.segments[index] = {
|
|
1930
1980
|
...segment,
|
|
1931
|
-
content: segment.content + chunk
|
|
1981
|
+
content: segment.content + chunk,
|
|
1982
|
+
// Content arriving IS the segment's activity, so this is what
|
|
1983
|
+
// `endedAt` measures. Without it a thinking block's end would be
|
|
1984
|
+
// whenever someone happened to notice it had stopped — the end of
|
|
1985
|
+
// the turn, or the start of the next segment — and both of those
|
|
1986
|
+
// silently fold the waiting that followed into the duration.
|
|
1987
|
+
...stampSegmentActivity(segment)
|
|
1932
1988
|
};
|
|
1933
1989
|
}
|
|
1934
1990
|
this.dispatchEvent(new CustomEvent("aparte-segment-update", {
|
|
@@ -1947,7 +2003,17 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1947
2003
|
* framework wrapper's host element.
|
|
1948
2004
|
*/
|
|
1949
2005
|
_activeMessageId() {
|
|
1950
|
-
|
|
2006
|
+
const head = this._repo.headId;
|
|
2007
|
+
const streaming = this._streamingMessageId();
|
|
2008
|
+
if (streaming !== null && streaming !== head) return null;
|
|
2009
|
+
return head;
|
|
2010
|
+
}
|
|
2011
|
+
/** The id of the message currently streaming, if any. */
|
|
2012
|
+
_streamingMessageId() {
|
|
2013
|
+
for (const message of this._repo.getMessages()) {
|
|
2014
|
+
if (message.isStreaming) return message.id;
|
|
2015
|
+
}
|
|
2016
|
+
return null;
|
|
1951
2017
|
}
|
|
1952
2018
|
addSegment(messageIdOrSegment, maybeSegment) {
|
|
1953
2019
|
const messageId = typeof messageIdOrSegment === "string" ? messageIdOrSegment : this._activeMessageId();
|
|
@@ -1957,8 +2023,9 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1957
2023
|
if (!message.segments) {
|
|
1958
2024
|
message.segments = [];
|
|
1959
2025
|
}
|
|
1960
|
-
message.segments
|
|
1961
|
-
|
|
2026
|
+
const stamped = stampSegmentOnInsert(message.segments, segment, messageId);
|
|
2027
|
+
message.segments.push(stamped);
|
|
2028
|
+
this._notifyBubble(messageId, "addSegment", stamped);
|
|
1962
2029
|
this._autoScroll();
|
|
1963
2030
|
}
|
|
1964
2031
|
updateSegment(a2, b2, c2) {
|
|
@@ -1971,11 +2038,13 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1971
2038
|
if (!message?.segments) return;
|
|
1972
2039
|
const segmentIndex = message.segments.findIndex((s2) => s2.id === segmentId);
|
|
1973
2040
|
if (segmentIndex !== -1) {
|
|
2041
|
+
const current = message.segments[segmentIndex];
|
|
2042
|
+
const stamped = stampSegmentOnUpdate(current, updates);
|
|
1974
2043
|
message.segments[segmentIndex] = {
|
|
1975
|
-
...
|
|
1976
|
-
...
|
|
2044
|
+
...current,
|
|
2045
|
+
...stamped
|
|
1977
2046
|
};
|
|
1978
|
-
this._notifyBubble(messageId, "updateSegment", { segmentId, updates });
|
|
2047
|
+
this._notifyBubble(messageId, "updateSegment", { segmentId, updates: stamped });
|
|
1979
2048
|
}
|
|
1980
2049
|
}
|
|
1981
2050
|
removeSegment(a2, b2) {
|
|
@@ -1986,7 +2055,10 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1986
2055
|
const message = this._repo.getMessageById(messageId);
|
|
1987
2056
|
if (message?.segments) {
|
|
1988
2057
|
const idx = message.segments.findIndex((s2) => s2.id === segmentId);
|
|
1989
|
-
if (idx !== -1)
|
|
2058
|
+
if (idx !== -1) {
|
|
2059
|
+
message.segments.splice(idx, 1);
|
|
2060
|
+
renumberSegments(message.segments);
|
|
2061
|
+
}
|
|
1990
2062
|
}
|
|
1991
2063
|
this._notifyBubble(messageId, "removeSegment", segmentId);
|
|
1992
2064
|
}
|
|
@@ -2007,7 +2079,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2007
2079
|
/**
|
|
2008
2080
|
* Persist token usage on a message and propagate to the live bubble, which is
|
|
2009
2081
|
* what allows the info ("i") action to render — provided the app declared it
|
|
2010
|
-
* with `
|
|
2082
|
+
* with `aparteGlobalConfig.setBubbleActions({ info: true })`; it is off by default,
|
|
2011
2083
|
* since the popover it opens belongs to the app.
|
|
2012
2084
|
*/
|
|
2013
2085
|
setUsage(messageId, usage) {
|
|
@@ -2026,13 +2098,26 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2026
2098
|
if (message) {
|
|
2027
2099
|
message.isStreaming = false;
|
|
2028
2100
|
message.status = "completed";
|
|
2029
|
-
|
|
2030
|
-
s2.isStreaming = false;
|
|
2031
|
-
});
|
|
2101
|
+
this._settleSegments(messageId, message);
|
|
2032
2102
|
this._notifyBubble(messageId, "complete", { status: "completed" });
|
|
2033
2103
|
this._recalculateSpacer();
|
|
2034
2104
|
}
|
|
2035
2105
|
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Close a finished message's still-open segments, one `updateSegment` each.
|
|
2108
|
+
*
|
|
2109
|
+
* Deliberately NOT a loop that writes `isStreaming` onto the objects: that path
|
|
2110
|
+
* stamps the model and tells the bubble nothing, so a renderer never learns its
|
|
2111
|
+
* segment settled — no `endedAt` in the rendered label, and no final Markdown
|
|
2112
|
+
* flush either. Going through `updateSegment` reuses the one path that does
|
|
2113
|
+
* both.
|
|
2114
|
+
*/
|
|
2115
|
+
_settleSegments(messageId, message) {
|
|
2116
|
+
if (!message.segments) return;
|
|
2117
|
+
for (const id of openSegmentIds(message.segments)) {
|
|
2118
|
+
this.updateSegment(messageId, id, { isStreaming: false });
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2036
2121
|
/**
|
|
2037
2122
|
* Atomic update for a message by ID
|
|
2038
2123
|
* Supports updating content, status, segments, and other metadata
|
|
@@ -2043,6 +2128,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2043
2128
|
Object.assign(message, updates);
|
|
2044
2129
|
if (updates.status) {
|
|
2045
2130
|
message.isStreaming = updates.status === "streaming" || updates.status === "pending";
|
|
2131
|
+
if (isTerminalStatus(updates.status)) this._settleSegments(messageId, message);
|
|
2046
2132
|
}
|
|
2047
2133
|
this._notifyBubble(messageId, "update", updates);
|
|
2048
2134
|
this._autoScroll();
|
|
@@ -2125,7 +2211,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2125
2211
|
const meta = this._repo.getMessage(messageId);
|
|
2126
2212
|
if (!meta) return 0;
|
|
2127
2213
|
const newMsg = {
|
|
2128
|
-
id:
|
|
2214
|
+
id: uuid(),
|
|
2129
2215
|
role: "assistant",
|
|
2130
2216
|
content: "",
|
|
2131
2217
|
status: "pending",
|
|
@@ -2235,7 +2321,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2235
2321
|
* branch arrows on already-rendered bubbles.
|
|
2236
2322
|
*/
|
|
2237
2323
|
importTree(tree) {
|
|
2238
|
-
this.clearAll();
|
|
2324
|
+
this.clearAll({ revokeAttachments: false });
|
|
2239
2325
|
this._repo.import(tree);
|
|
2240
2326
|
this._reRenderActivePath();
|
|
2241
2327
|
}
|
|
@@ -2248,7 +2334,12 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2248
2334
|
* so desynchronises the framework's view tree from the live DOM and the
|
|
2249
2335
|
* next change-detection pass throws `NotFoundError` on insertBefore.
|
|
2250
2336
|
*/
|
|
2251
|
-
clearAll() {
|
|
2337
|
+
clearAll(options) {
|
|
2338
|
+
if (options?.revokeAttachments !== false) {
|
|
2339
|
+
for (const message of this._repo.getMessages()) {
|
|
2340
|
+
revokeAttachmentUrls(message.attachments);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2252
2343
|
this._repo.clear();
|
|
2253
2344
|
if (!this._frameworkManagedDOM) {
|
|
2254
2345
|
const wrapper = this.querySelector(".aparte-messages-wrapper");
|
|
@@ -2277,7 +2368,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2277
2368
|
* build chat history).
|
|
2278
2369
|
*/
|
|
2279
2370
|
setMessages(messages) {
|
|
2280
|
-
this.clearAll();
|
|
2371
|
+
this.clearAll({ revokeAttachments: false });
|
|
2281
2372
|
for (const m2 of messages) {
|
|
2282
2373
|
this.appendMessage(m2);
|
|
2283
2374
|
}
|
|
@@ -2535,18 +2626,27 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2535
2626
|
this._bottomSpacer.style.height = `${px}px`;
|
|
2536
2627
|
}
|
|
2537
2628
|
}
|
|
2629
|
+
// Bound fields, not inline arrows: a custom element is re-connected every
|
|
2630
|
+
// time it is MOVED in the DOM (a portal, a dialog, a framework re-parenting),
|
|
2631
|
+
// so `_setupEventListeners` runs again each time. An inline arrow can never
|
|
2632
|
+
// be handed to `removeEventListener`, so it just accumulates — one branch
|
|
2633
|
+
// click then ran N handlers, N active-path re-renders and N storage writes
|
|
2634
|
+
// through the conversation controller. The window listeners next to these
|
|
2635
|
+
// were always removed properly; these two, attached to `this`, were not.
|
|
2636
|
+
_onScrollBtnClick = () => {
|
|
2637
|
+
this._isAutoScrollEnabled = true;
|
|
2638
|
+
this._smoothScrollToBottom();
|
|
2639
|
+
this._updateScrollButton();
|
|
2640
|
+
};
|
|
2641
|
+
_onBranchNavigate = (e2) => {
|
|
2642
|
+
const evt = e2;
|
|
2643
|
+
evt.stopPropagation();
|
|
2644
|
+
this.navigateBranch(evt.detail.messageId, evt.detail.direction);
|
|
2645
|
+
};
|
|
2538
2646
|
_setupEventListeners() {
|
|
2539
2647
|
this._container?.addEventListener("scroll", this._handleScroll, { passive: true });
|
|
2540
|
-
this._scrollBtn?.addEventListener("click",
|
|
2541
|
-
|
|
2542
|
-
this._smoothScrollToBottom();
|
|
2543
|
-
this._updateScrollButton();
|
|
2544
|
-
});
|
|
2545
|
-
this.addEventListener("aparte-branch-navigate", (e2) => {
|
|
2546
|
-
const evt = e2;
|
|
2547
|
-
evt.stopPropagation();
|
|
2548
|
-
this.navigateBranch(evt.detail.messageId, evt.detail.direction);
|
|
2549
|
-
});
|
|
2648
|
+
this._scrollBtn?.addEventListener("click", this._onScrollBtnClick);
|
|
2649
|
+
this.addEventListener("aparte-branch-navigate", this._onBranchNavigate);
|
|
2550
2650
|
}
|
|
2551
2651
|
_setupObservers() {
|
|
2552
2652
|
this._resizeObserver = new ResizeObserver(() => {
|
|
@@ -2592,6 +2692,41 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2592
2692
|
_scrollToBottom() {
|
|
2593
2693
|
if (!this._container) return;
|
|
2594
2694
|
this._container.scrollTop = this._container.scrollHeight;
|
|
2695
|
+
this._settleAtBottom(4);
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Confirm over the next few frames that we actually reached the bottom.
|
|
2699
|
+
*
|
|
2700
|
+
* One assignment is not enough, and the reason is measured rather than guessed.
|
|
2701
|
+
* A timeline of a streamed turn on Safari (framework mode) recorded the content
|
|
2702
|
+
* settling in TWO layout passes — 1118 → 1121 → 1152 px. `scrollTop =
|
|
2703
|
+
* scrollHeight` ran against the middle one, clamped to that layout's max (603),
|
|
2704
|
+
* and nothing ran afterwards: the last 31px never closed. Auto-follow stayed
|
|
2705
|
+
* armed the whole time, so the component was not disarmed — it was SATISFIED.
|
|
2706
|
+
* `_isAtBottom()` answers "yes" for any gap under `_scrollThreshold` (50), which
|
|
2707
|
+
* is the right rule for keeping auto-follow armed and the wrong one as a
|
|
2708
|
+
* definition of "anchored".
|
|
2709
|
+
*
|
|
2710
|
+
* Ruled out on the way here, so nobody pays for it twice: not a WebKit
|
|
2711
|
+
* padding-accounting difference (a probe writing `scrollTop = 1e7` reached
|
|
2712
|
+
* exactly `scrollHeight - clientHeight`), not a missing `characterData`
|
|
2713
|
+
* mutation, and not a child resize a ResizeObserver could see.
|
|
2714
|
+
*
|
|
2715
|
+
* A BOUNDED retry, not one corrective frame: a single frame lands on the same
|
|
2716
|
+
* stale layout and was measured leaving a wider gap than doing nothing. Bounded
|
|
2717
|
+
* so it always terminates; re-reads `_isAutoScrollEnabled` every frame so a
|
|
2718
|
+
* reader who scrolls away mid-settle is left alone; stops as soon as the gap is
|
|
2719
|
+
* closed, so the common case costs one frame that does nothing.
|
|
2720
|
+
*/
|
|
2721
|
+
_settleAtBottom(framesLeft) {
|
|
2722
|
+
if (framesLeft <= 0) return;
|
|
2723
|
+
requestAnimationFrame(() => {
|
|
2724
|
+
if (!this._container || !this._isAutoScrollEnabled) return;
|
|
2725
|
+
const max = this._container.scrollHeight - this._container.clientHeight;
|
|
2726
|
+
if (max - this._container.scrollTop <= 1) return;
|
|
2727
|
+
this._container.scrollTop = max;
|
|
2728
|
+
this._settleAtBottom(framesLeft - 1);
|
|
2729
|
+
});
|
|
2595
2730
|
}
|
|
2596
2731
|
_smoothScrollToBottom() {
|
|
2597
2732
|
if (!this._container) return;
|
|
@@ -2684,7 +2819,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2684
2819
|
* Cap the number of rendered bubbles in the DOM (perf ceiling only).
|
|
2685
2820
|
*
|
|
2686
2821
|
* Drops the oldest `<aparte-chat-bubble>` elements beyond `_maxRenderedBubbles`
|
|
2687
|
-
* from the DOM. It **never** touches the
|
|
2822
|
+
* from the DOM. It **never** touches the AparteMessageRepository — the conversation
|
|
2688
2823
|
* model and its persistence snapshot stay complete (retention/eviction is a
|
|
2689
2824
|
* consumer/persistence concern, not the viewport's). No-op when a framework
|
|
2690
2825
|
* owns the DOM.
|
|
@@ -2703,11 +2838,13 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2703
2838
|
if (this._warnedMaxMessagesDeprecation) return;
|
|
2704
2839
|
this._warnedMaxMessagesDeprecation = true;
|
|
2705
2840
|
console.warn(
|
|
2706
|
-
"[Aparte] `maxMessages` / `max-messages` on aparte-chat-viewport is deprecated: it used to silently evict messages from the conversation model. It now only caps rendered bubbles in the DOM — use `maxRenderedBubbles` / `max-rendered-bubbles`. For actual history retention, configure it on your
|
|
2841
|
+
"[Aparte] `maxMessages` / `max-messages` on aparte-chat-viewport is deprecated: it used to silently evict messages from the conversation model. It now only caps rendered bubbles in the DOM — use `maxRenderedBubbles` / `max-rendered-bubbles`. For actual history retention, configure it on your AparteConversationManager instead."
|
|
2707
2842
|
);
|
|
2708
2843
|
}
|
|
2709
2844
|
_cleanup() {
|
|
2710
2845
|
this._container?.removeEventListener("scroll", this._handleScroll);
|
|
2846
|
+
this._scrollBtn?.removeEventListener("click", this._onScrollBtnClick);
|
|
2847
|
+
this.removeEventListener("aparte-branch-navigate", this._onBranchNavigate);
|
|
2711
2848
|
this._resizeObserver?.disconnect();
|
|
2712
2849
|
this._mutationObserver?.disconnect();
|
|
2713
2850
|
this._resizeObserver = null;
|
|
@@ -2727,6 +2864,8 @@ class AparteComposer extends HTMLElement {
|
|
|
2727
2864
|
_attachments = [];
|
|
2728
2865
|
_listeners = /* @__PURE__ */ new Map();
|
|
2729
2866
|
_panelActive = false;
|
|
2867
|
+
/** What the send button means while a panel is up — see `showPanel`. */
|
|
2868
|
+
_panelMode = "submit";
|
|
2730
2869
|
_panelSubmitEnabled = false;
|
|
2731
2870
|
_panelOnSubmit = null;
|
|
2732
2871
|
// Internal bus events that represent an observable STATE change — these are
|
|
@@ -2743,8 +2882,25 @@ class AparteComposer extends HTMLElement {
|
|
|
2743
2882
|
_onMessageStart = this._handleMessageStart.bind(this);
|
|
2744
2883
|
_onMessageDone = this._handleMessageDone.bind(this);
|
|
2745
2884
|
/** Config governing THIS composer (nearest instance boundary, else global). */
|
|
2746
|
-
|
|
2747
|
-
|
|
2885
|
+
/**
|
|
2886
|
+
* Resolved LIVE, not cached at connect.
|
|
2887
|
+
*
|
|
2888
|
+
* A wrapper runs `AparteChatHost.bind()` — which calls `attachConfig` — from its
|
|
2889
|
+
* POST-mount hook, so this element connects BEFORE the boundary exists. Caching
|
|
2890
|
+
* here latched the global config forever: an instance `config` carrying an RTL
|
|
2891
|
+
* locale flipped the transcript and not the composer, and the
|
|
2892
|
+
* `requireModelSelection` gate was read off the wrong object. `aparte-chat-bubble`
|
|
2893
|
+
* has always resolved live, and its JSDoc names this exact race.
|
|
2894
|
+
*/
|
|
2895
|
+
get _cfg() {
|
|
2896
|
+
return resolveConfig(this);
|
|
2897
|
+
}
|
|
2898
|
+
/** Only OUR config: a change on another chat's instance must not touch us. */
|
|
2899
|
+
_onConfigChangeEvent = (e2) => {
|
|
2900
|
+
const detail = e2.detail;
|
|
2901
|
+
if (detail?.config && detail.config !== this._cfg) return;
|
|
2902
|
+
this._onConfigChange();
|
|
2903
|
+
};
|
|
2748
2904
|
/** True while `requireModelSelection` is on AND no model is selected — blocks send. */
|
|
2749
2905
|
_modelGated = false;
|
|
2750
2906
|
_onConfigChange = () => {
|
|
@@ -2776,8 +2932,7 @@ class AparteComposer extends HTMLElement {
|
|
|
2776
2932
|
window.addEventListener("aparte-message-done", this._onMessageDone);
|
|
2777
2933
|
window.addEventListener("aparte-message-error", this._onMessageDone);
|
|
2778
2934
|
window.addEventListener("aparte-message-aborted", this._onMessageDone);
|
|
2779
|
-
|
|
2780
|
-
this._configUnsub = this._cfg.subscribe(this._onConfigChange);
|
|
2935
|
+
window.addEventListener("aparte-config-change", this._onConfigChangeEvent);
|
|
2781
2936
|
this._evaluateModelGate();
|
|
2782
2937
|
this._applyDirection();
|
|
2783
2938
|
}
|
|
@@ -2786,8 +2941,7 @@ class AparteComposer extends HTMLElement {
|
|
|
2786
2941
|
window.removeEventListener("aparte-message-done", this._onMessageDone);
|
|
2787
2942
|
window.removeEventListener("aparte-message-error", this._onMessageDone);
|
|
2788
2943
|
window.removeEventListener("aparte-message-aborted", this._onMessageDone);
|
|
2789
|
-
this.
|
|
2790
|
-
this._configUnsub = null;
|
|
2944
|
+
window.removeEventListener("aparte-config-change", this._onConfigChangeEvent);
|
|
2791
2945
|
this._listeners.clear();
|
|
2792
2946
|
}
|
|
2793
2947
|
attributeChangedCallback(name, _old, value) {
|
|
@@ -2863,11 +3017,29 @@ class AparteComposer extends HTMLElement {
|
|
|
2863
3017
|
this._attachments = [];
|
|
2864
3018
|
this._emit("attachments-change", { attachments: [] });
|
|
2865
3019
|
}
|
|
2866
|
-
/**
|
|
3020
|
+
/**
|
|
3021
|
+
* Inject a panel into the composer. The send button calls `onSubmit` when clicked.
|
|
3022
|
+
*
|
|
3023
|
+
* While a panel is up, the composer is answering a QUESTION, not composing a
|
|
3024
|
+
* message — so the affordances that lead nowhere go away with the text input:
|
|
3025
|
+
* the attachment picker above all, which stayed clickable while the user was
|
|
3026
|
+
* being asked something ("on voyait encore l'icône de upload", reported from a
|
|
3027
|
+
* real session). Ratified decision #8: an affordance nothing can honour is not
|
|
3028
|
+
* rendered.
|
|
3029
|
+
*
|
|
3030
|
+
* What STAYS, deliberately: the attachments strip, because pending attachments
|
|
3031
|
+
* are the user's state and not an action to offer — hiding them would look like
|
|
3032
|
+
* losing them; and the toolbar, because switching model still does something.
|
|
3033
|
+
*
|
|
3034
|
+
* Declared with an attribute + CSS rather than the inline `style.display` this
|
|
3035
|
+
* used to set on a child: an attribute is themeable, is visible to a consumer's
|
|
3036
|
+
* own rules, and does not clobber a `display` the consumer had set (the restore
|
|
3037
|
+
* wrote `''`, not the previous value).
|
|
3038
|
+
*/
|
|
2867
3039
|
showPanel(panel, options) {
|
|
2868
3040
|
this.hidePanel();
|
|
2869
3041
|
const inputEl = this.querySelector("aparte-composer-input");
|
|
2870
|
-
|
|
3042
|
+
this.setAttribute("data-panel-active", "");
|
|
2871
3043
|
panel.dataset["apartePanel"] = "true";
|
|
2872
3044
|
if (inputEl) {
|
|
2873
3045
|
inputEl.insertAdjacentElement("afterend", panel);
|
|
@@ -2876,26 +3048,35 @@ class AparteComposer extends HTMLElement {
|
|
|
2876
3048
|
}
|
|
2877
3049
|
this._panelActive = true;
|
|
2878
3050
|
this._panelSubmitEnabled = options?.submitEnabled ?? false;
|
|
3051
|
+
this._panelMode = options?.mode ?? "submit";
|
|
2879
3052
|
this._panelOnSubmit = options?.onSubmit ?? null;
|
|
2880
|
-
this._emit("panel-change", { active: true, submitEnabled: this._panelSubmitEnabled });
|
|
3053
|
+
this._emit("panel-change", { active: true, submitEnabled: this._panelSubmitEnabled, mode: this._panelMode });
|
|
2881
3054
|
}
|
|
2882
|
-
/** Remove the panel and restore the
|
|
3055
|
+
/** Remove the panel and restore the composer's own controls. */
|
|
2883
3056
|
hidePanel() {
|
|
2884
3057
|
const existing = this.querySelector("[data-aparte-panel]");
|
|
2885
3058
|
if (existing) existing.remove();
|
|
2886
|
-
|
|
2887
|
-
if (inputEl) inputEl.style.display = "";
|
|
3059
|
+
this.removeAttribute("data-panel-active");
|
|
2888
3060
|
this._panelActive = false;
|
|
2889
3061
|
this._panelSubmitEnabled = false;
|
|
3062
|
+
this._panelMode = "submit";
|
|
2890
3063
|
this._panelOnSubmit = null;
|
|
2891
|
-
this._emit("panel-change", { active: false, submitEnabled: false });
|
|
3064
|
+
this._emit("panel-change", { active: false, submitEnabled: false, mode: "submit" });
|
|
2892
3065
|
this.focus();
|
|
2893
3066
|
}
|
|
2894
|
-
/**
|
|
2895
|
-
|
|
3067
|
+
/**
|
|
3068
|
+
* Update the send button's state while a panel is active.
|
|
3069
|
+
*
|
|
3070
|
+
* `mode` moves with it because both change on the same event — answering the
|
|
3071
|
+
* question you are on can enable the button AND turn it from "advance" into
|
|
3072
|
+
* "submit" (when it was the last one), and two separate calls would flash a
|
|
3073
|
+
* wrong icon between them.
|
|
3074
|
+
*/
|
|
3075
|
+
setPanelSubmitEnabled(enabled, mode) {
|
|
2896
3076
|
if (!this._panelActive) return;
|
|
2897
3077
|
this._panelSubmitEnabled = enabled;
|
|
2898
|
-
this.
|
|
3078
|
+
if (mode) this._panelMode = mode;
|
|
3079
|
+
this._emit("panel-change", { active: true, submitEnabled: enabled, mode: this._panelMode });
|
|
2899
3080
|
}
|
|
2900
3081
|
get panelActive() {
|
|
2901
3082
|
return this._panelActive;
|
|
@@ -2986,7 +3167,36 @@ class AparteComposer extends HTMLElement {
|
|
|
2986
3167
|
* Without this filter, streaming in one chat flips every composer's state. */
|
|
2987
3168
|
_isForThisComposer(e2) {
|
|
2988
3169
|
const evtTargetId = e2.detail?.targetId;
|
|
2989
|
-
return !evtTargetId || !this.
|
|
3170
|
+
return !evtTargetId || !this._ownTargetId() || evtTargetId === this._ownTargetId();
|
|
3171
|
+
}
|
|
3172
|
+
/**
|
|
3173
|
+
* Which chat this composer belongs to: its `target` attribute, or the id of the
|
|
3174
|
+
* chat host above it.
|
|
3175
|
+
*
|
|
3176
|
+
* All four wrappers set `target` themselves, so the attribute alone identified a
|
|
3177
|
+
* composer there. In RAW core — the documented quick start, where the markup is
|
|
3178
|
+
* hand-written — nothing sets it, so `!this.targetId` was true and this composer
|
|
3179
|
+
* accepted every chat's lifecycle events: on a two-chat page, one chat's Stop
|
|
3180
|
+
* tore down the other's open elicitation panel while its tool call kept waiting,
|
|
3181
|
+
* i.e. the question vanished and the turn hung.
|
|
3182
|
+
*
|
|
3183
|
+
* Found by a two-chat test written for the elicitation presenter, which is the
|
|
3184
|
+
* only reason it surfaced: raw core with two chats is a shape nothing exercised.
|
|
3185
|
+
* The hosts matched are the ones `aparte-chat-bubble._resolveTargetId()` matches,
|
|
3186
|
+
* for the reason written there — Angular's wrapper root IS `<aparte-chat>`, the
|
|
3187
|
+
* other three render a `[data-aparte-chat]` div.
|
|
3188
|
+
*/
|
|
3189
|
+
_ownTargetId() {
|
|
3190
|
+
const attr = this.targetId;
|
|
3191
|
+
if (attr) return attr;
|
|
3192
|
+
let el = this.parentElement;
|
|
3193
|
+
while (el) {
|
|
3194
|
+
const tag = el.tagName?.toLowerCase();
|
|
3195
|
+
const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
|
|
3196
|
+
if (isHost && el.id) return el.id;
|
|
3197
|
+
el = el.parentElement;
|
|
3198
|
+
}
|
|
3199
|
+
return void 0;
|
|
2990
3200
|
}
|
|
2991
3201
|
_handleMessageStart(e2) {
|
|
2992
3202
|
if (!this._isForThisComposer(e2)) return;
|
|
@@ -3093,7 +3303,7 @@ class AparteComposerInput extends HTMLElement {
|
|
|
3093
3303
|
_render() {
|
|
3094
3304
|
if (this.querySelector(".aparte-ci-editor")) return;
|
|
3095
3305
|
const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
|
|
3096
|
-
const placeholder =
|
|
3306
|
+
const placeholder = escapeAttr(this._getPlaceholder());
|
|
3097
3307
|
this.innerHTML = `<div
|
|
3098
3308
|
class="aparte-ci-editor"
|
|
3099
3309
|
contenteditable="${!disabled}"
|
|
@@ -3217,9 +3427,6 @@ class AparteComposerInput extends HTMLElement {
|
|
|
3217
3427
|
this._editor.setAttribute("aria-disabled", String(disabled));
|
|
3218
3428
|
}
|
|
3219
3429
|
/** Escape a value before it lands in a double-quoted HTML attribute. */
|
|
3220
|
-
_escapeAttr(str) {
|
|
3221
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3222
|
-
}
|
|
3223
3430
|
}
|
|
3224
3431
|
if (!customElements.get("aparte-composer-input")) {
|
|
3225
3432
|
customElements.define("aparte-composer-input", AparteComposerInput);
|
|
@@ -3250,8 +3457,8 @@ class AparteComposerSend extends HTMLElement {
|
|
|
3250
3457
|
const disabled = !root || root.disabled || root.value.trim() === "";
|
|
3251
3458
|
this.innerHTML = `<button
|
|
3252
3459
|
class="aparte-cs-button aparte-send-button"
|
|
3253
|
-
aria-label="${label}"
|
|
3254
|
-
title="${label}"
|
|
3460
|
+
aria-label="${escapeAttr(label)}"
|
|
3461
|
+
title="${escapeAttr(label)}"
|
|
3255
3462
|
${disabled ? "disabled" : ""}
|
|
3256
3463
|
>${icon}</button>`;
|
|
3257
3464
|
this._button = this.querySelector(".aparte-cs-button");
|
|
@@ -3276,12 +3483,14 @@ class AparteComposerSend extends HTMLElement {
|
|
|
3276
3483
|
root._on("attachments-change", () => this._syncState())
|
|
3277
3484
|
);
|
|
3278
3485
|
this._unsubscribes.push(
|
|
3279
|
-
root._on("panel-change", ({ active, submitEnabled }) => {
|
|
3486
|
+
root._on("panel-change", ({ active, submitEnabled, mode }) => {
|
|
3280
3487
|
if (!this._button) return;
|
|
3281
3488
|
if (active) {
|
|
3282
3489
|
this._button.disabled = !submitEnabled;
|
|
3283
|
-
|
|
3284
|
-
const
|
|
3490
|
+
const cfg = resolveConfig(this);
|
|
3491
|
+
const advancing = mode === "advance";
|
|
3492
|
+
this._button.innerHTML = advancing ? cfg.getIcon("nextBranch") : this._getSubmitIcon();
|
|
3493
|
+
const label = advancing ? cfg.t("elicitationNext") || "Next" : cfg.t("submitButton") || "Submit";
|
|
3285
3494
|
this._button.setAttribute("aria-label", label);
|
|
3286
3495
|
this._button.setAttribute("title", label);
|
|
3287
3496
|
this._button.classList.remove("is-streaming");
|
|
@@ -3324,6 +3533,13 @@ class AparteComposerSend extends HTMLElement {
|
|
|
3324
3533
|
this._syncState();
|
|
3325
3534
|
}
|
|
3326
3535
|
}
|
|
3536
|
+
/**
|
|
3537
|
+
* The icon for submitting an ANSWER, which is not the same act as sending a
|
|
3538
|
+
* message — one button, two meanings, and it has to say which one it is.
|
|
3539
|
+
*/
|
|
3540
|
+
_getSubmitIcon() {
|
|
3541
|
+
return resolveConfig(this).getIcon("check");
|
|
3542
|
+
}
|
|
3327
3543
|
_getSendIcon() {
|
|
3328
3544
|
return resolveConfig(this).getIcon("send") || "Send";
|
|
3329
3545
|
}
|
|
@@ -3358,8 +3574,8 @@ class AparteComposerCancel extends HTMLElement {
|
|
|
3358
3574
|
const icon = this._getStopIcon();
|
|
3359
3575
|
this.innerHTML = `<button
|
|
3360
3576
|
class="aparte-cc-button"
|
|
3361
|
-
aria-label="${label}"
|
|
3362
|
-
title="${label}"
|
|
3577
|
+
aria-label="${escapeAttr(label)}"
|
|
3578
|
+
title="${escapeAttr(label)}"
|
|
3363
3579
|
hidden
|
|
3364
3580
|
>${icon}</button>`;
|
|
3365
3581
|
this._button = this.querySelector(".aparte-cc-button");
|
|
@@ -3426,9 +3642,9 @@ class AparteComposerAttachments extends HTMLElement {
|
|
|
3426
3642
|
if (file.type.startsWith("image/")) {
|
|
3427
3643
|
const url = URL.createObjectURL(file);
|
|
3428
3644
|
this._objectUrls.push(url);
|
|
3429
|
-
return `<div class="aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${url}" alt="${name}" /><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
|
|
3645
|
+
return `<div class="aparte-thumb aparte-thumb--image" title="${escapeAttr(name)}"><img class="aparte-thumb__img" src="${escapeAttr(url)}" alt="${escapeAttr(name)}" /><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
|
|
3430
3646
|
}
|
|
3431
|
-
return `<div class="aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${this._escape(this._ext(file.name))}</span><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
|
|
3647
|
+
return `<div class="aparte-thumb aparte-thumb--file" title="${escapeAttr(name)}"><span class="aparte-thumb__ext">${this._escape(this._ext(file.name))}</span><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
|
|
3432
3648
|
}).join("");
|
|
3433
3649
|
this.querySelectorAll(".aparte-thumb__remove").forEach((btn, i2) => {
|
|
3434
3650
|
btn.addEventListener("click", (e2) => {
|
|
@@ -3507,8 +3723,8 @@ class AparteComposerAddAttachment extends HTMLElement {
|
|
|
3507
3723
|
const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
|
|
3508
3724
|
this.innerHTML = `<button
|
|
3509
3725
|
class="aparte-caa-button aparte-action-button"
|
|
3510
|
-
aria-label="${label}"
|
|
3511
|
-
title="${label}"
|
|
3726
|
+
aria-label="${escapeAttr(label)}"
|
|
3727
|
+
title="${escapeAttr(label)}"
|
|
3512
3728
|
type="button"
|
|
3513
3729
|
${disabled ? "disabled" : ""}
|
|
3514
3730
|
>${icon}</button>`;
|
|
@@ -3618,7 +3834,7 @@ class AparteComposerAction extends HTMLElement {
|
|
|
3618
3834
|
}
|
|
3619
3835
|
_render() {
|
|
3620
3836
|
if (this.querySelector(".aparte-cact-button")) return;
|
|
3621
|
-
const label =
|
|
3837
|
+
const label = escapeAttr(this.getAttribute("label") ?? "");
|
|
3622
3838
|
const icon = this._resolveIcon(this.getAttribute("icon") ?? "");
|
|
3623
3839
|
const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
|
|
3624
3840
|
this.innerHTML = `<button
|
|
@@ -3658,9 +3874,6 @@ class AparteComposerAction extends HTMLElement {
|
|
|
3658
3874
|
return resolveConfig(this).getIcon(icon) ?? icon;
|
|
3659
3875
|
}
|
|
3660
3876
|
/** Escape a value before it lands in a double-quoted HTML attribute. */
|
|
3661
|
-
_escapeAttr(str) {
|
|
3662
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3663
|
-
}
|
|
3664
3877
|
}
|
|
3665
3878
|
if (!customElements.get("aparte-composer-action")) {
|
|
3666
3879
|
customElements.define("aparte-composer-action", AparteComposerAction);
|
|
@@ -3776,8 +3989,8 @@ class AparteConversationList extends HTMLElement {
|
|
|
3776
3989
|
class="aparte-conv-item__archive"
|
|
3777
3990
|
type="button"
|
|
3778
3991
|
data-archive-id="${escapedId}"
|
|
3779
|
-
data-archive-action="${archiveAction}"
|
|
3780
|
-
aria-label="${archiveAriaLabel}"
|
|
3992
|
+
data-archive-action="${escapeAttr(archiveAction)}"
|
|
3993
|
+
aria-label="${escapeAttr(archiveAriaLabel)}"
|
|
3781
3994
|
tabindex="0"
|
|
3782
3995
|
>${archiveIcon}</button>
|
|
3783
3996
|
<button
|
|
@@ -3856,14 +4069,49 @@ class AparteConversationList extends HTMLElement {
|
|
|
3856
4069
|
if (!customElements.get("aparte-conversation-list")) customElements.define("aparte-conversation-list", AparteConversationList);
|
|
3857
4070
|
class AparteElicitation extends HTMLElement {
|
|
3858
4071
|
_pending = null;
|
|
3859
|
-
|
|
4072
|
+
/**
|
|
4073
|
+
* A turn ended — cancel the open question only if it was OUR turn.
|
|
4074
|
+
*
|
|
4075
|
+
* These two listeners sit on `window` and had no instance filter at all, so a
|
|
4076
|
+
* Stop (or an error) in one chat cancelled the question a DIFFERENT chat was
|
|
4077
|
+
* waiting on — and that chat's model was told the user had refused a question
|
|
4078
|
+
* the user was still looking at. Same defect as the `compact()` handler that
|
|
4079
|
+
* emptied both chats, which its four sibling handlers in `AparteClient` already
|
|
4080
|
+
* guarded against.
|
|
4081
|
+
*
|
|
4082
|
+
* The leniency rule is the composer's, deliberately: an event with no
|
|
4083
|
+
* `targetId` is for everyone (a single-chat app never sets one), and a chat we
|
|
4084
|
+
* cannot identify accepts everything rather than becoming deaf. Only a
|
|
4085
|
+
* MISMATCH is ignored.
|
|
4086
|
+
*/
|
|
4087
|
+
_onTurnEnd = (e2) => {
|
|
4088
|
+
const evtTargetId = e2.detail?.targetId;
|
|
4089
|
+
const own = this._pendingTargetId();
|
|
4090
|
+
if (evtTargetId && own && evtTargetId !== own) return;
|
|
4091
|
+
this._cancelPending();
|
|
4092
|
+
};
|
|
3860
4093
|
connectedCallback() {
|
|
3861
4094
|
this.style.display = "none";
|
|
3862
|
-
this._injectStyles();
|
|
3863
4095
|
resolveConfig(this).setElicitationPresenter(this._present);
|
|
3864
4096
|
window.addEventListener("aparte-message-aborted", this._onTurnEnd);
|
|
3865
4097
|
window.addEventListener("aparte-message-error", this._onTurnEnd);
|
|
3866
4098
|
}
|
|
4099
|
+
/**
|
|
4100
|
+
* The boundary above us appeared, changed, or went away — move the
|
|
4101
|
+
* registration with it.
|
|
4102
|
+
*
|
|
4103
|
+
* `connectedCallback` alone is not enough and cannot be: registering is a
|
|
4104
|
+
* WRITE, and under all four wrappers it happens before `attachConfig` runs, so
|
|
4105
|
+
* it lands on the global singleton. `requestUserInput()` then resolves the
|
|
4106
|
+
* instance config, finds nothing, and answers the model `cancel` — the model
|
|
4107
|
+
* hears the user refuse a question the user never saw.
|
|
4108
|
+
*
|
|
4109
|
+
* See {@link AparteConfigAware}.
|
|
4110
|
+
*/
|
|
4111
|
+
aparteConfigChanged(next, previous) {
|
|
4112
|
+
if (previous.getElicitationPresenter() === this._present) previous.setElicitationPresenter(null);
|
|
4113
|
+
next.setElicitationPresenter(this._present);
|
|
4114
|
+
}
|
|
3867
4115
|
disconnectedCallback() {
|
|
3868
4116
|
const cfg = resolveConfig(this);
|
|
3869
4117
|
if (cfg.getElicitationPresenter() === this._present) cfg.setElicitationPresenter(null);
|
|
@@ -3891,22 +4139,26 @@ class AparteElicitation extends HTMLElement {
|
|
|
3891
4139
|
}
|
|
3892
4140
|
request.signal.addEventListener("abort", () => settle({ action: "cancel" }), { once: true });
|
|
3893
4141
|
}
|
|
3894
|
-
const
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
footer.className = "aparte-elic-footer";
|
|
4142
|
+
const cfg = resolveConfig(this);
|
|
4143
|
+
const panel = runWithConfig(cfg, () => buildElicitationPanel(request.message, request.schema, () => {
|
|
4144
|
+
composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
|
|
4145
|
+
}));
|
|
3899
4146
|
const skip = document.createElement("button");
|
|
3900
4147
|
skip.type = "button";
|
|
3901
4148
|
skip.className = "aparte-elic-skip";
|
|
3902
|
-
skip.textContent = "
|
|
4149
|
+
skip.textContent = cfg.t("elicitationSkip");
|
|
3903
4150
|
skip.addEventListener("click", () => settle({ action: "decline" }));
|
|
3904
|
-
|
|
3905
|
-
panel.el.appendChild(footer);
|
|
4151
|
+
panel.dismiss.appendChild(skip);
|
|
3906
4152
|
this._pending = { settle, composer };
|
|
3907
4153
|
composer.showPanel(panel.el, {
|
|
3908
|
-
submitEnabled: panel.
|
|
4154
|
+
submitEnabled: panel.canProceed(),
|
|
4155
|
+
mode: panel.mode(),
|
|
3909
4156
|
onSubmit: () => {
|
|
4157
|
+
if (panel.mode() === "advance") {
|
|
4158
|
+
panel.proceed();
|
|
4159
|
+
composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
3910
4162
|
if (panel.isComplete()) settle({ action: "accept", content: panel.getContent() });
|
|
3911
4163
|
}
|
|
3912
4164
|
});
|
|
@@ -3916,43 +4168,56 @@ class AparteElicitation extends HTMLElement {
|
|
|
3916
4168
|
_cancelPending() {
|
|
3917
4169
|
this._pending?.settle({ action: "cancel" });
|
|
3918
4170
|
}
|
|
4171
|
+
/**
|
|
4172
|
+
* The host id of the chat whose composer holds the open panel.
|
|
4173
|
+
*
|
|
4174
|
+
* Walks up from the composer rather than from `this`, because the panel lives in
|
|
4175
|
+
* the composer and that is what the turn belongs to. Matches the hosts
|
|
4176
|
+
* `aparte-chat-bubble._resolveTargetId()` matches, for the reason written there:
|
|
4177
|
+
* Angular's wrapper root IS the `<aparte-chat>` element, while the plain-root
|
|
4178
|
+
* wrappers render a `[data-aparte-chat]` div instead, so matching only the tag
|
|
4179
|
+
* resolves `undefined` on three wrappers out of four.
|
|
4180
|
+
*/
|
|
4181
|
+
_pendingTargetId() {
|
|
4182
|
+
let el = this._pending?.composer ?? null;
|
|
4183
|
+
while (el) {
|
|
4184
|
+
const tag = el.tagName?.toLowerCase();
|
|
4185
|
+
const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
|
|
4186
|
+
if (isHost && el.id) return el.id;
|
|
4187
|
+
el = el.parentElement;
|
|
4188
|
+
}
|
|
4189
|
+
return void 0;
|
|
4190
|
+
}
|
|
4191
|
+
/**
|
|
4192
|
+
* The composer to present in: the nearest one in an ancestor subtree, and
|
|
4193
|
+
* nothing else.
|
|
4194
|
+
*
|
|
4195
|
+
* There used to be a `document.querySelector('aparte-composer')` fallback, which
|
|
4196
|
+
* is the "first chat on the page" bug this repo has now fixed in four other
|
|
4197
|
+
* places: on a page with two chats, an elicitation that could not find its own
|
|
4198
|
+
* composer mounted its panel in the OTHER chat's — so one conversation's question
|
|
4199
|
+
* appeared under the other conversation, and answering it resolved a tool call
|
|
4200
|
+
* belonging to a chat the user was not looking at.
|
|
4201
|
+
*
|
|
4202
|
+
* Returning `null` instead resolves `cancel`, which is honest: nothing was
|
|
4203
|
+
* shown, so nothing was answered. The warning names the fix, because this is a
|
|
4204
|
+
* setup mistake and only the developer can correct it — the guide's own example
|
|
4205
|
+
* puts `<aparte-elicitation>` inside `<aparte-chat>`.
|
|
4206
|
+
*/
|
|
3919
4207
|
_getComposer() {
|
|
3920
4208
|
let node = this.parentElement;
|
|
3921
4209
|
while (node) {
|
|
3922
4210
|
const composer = node.querySelector("aparte-composer");
|
|
3923
4211
|
if (composer && typeof composer.showPanel === "function") return composer;
|
|
4212
|
+
const tag = node.tagName?.toLowerCase();
|
|
4213
|
+
const isChatBoundary = tag === "aparte-chat" || tag === "aparte-chat-component" || node.hasAttribute?.("data-aparte-chat");
|
|
4214
|
+
if (isChatBoundary) break;
|
|
3924
4215
|
node = node.parentElement;
|
|
3925
4216
|
}
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
const id = "aparte-elicitation-styles";
|
|
3931
|
-
if (document.getElementById(id)) return;
|
|
3932
|
-
const style = document.createElement("style");
|
|
3933
|
-
style.id = id;
|
|
3934
|
-
style.textContent = `
|
|
3935
|
-
.aparte-elic-panel { display:flex; flex-direction:column; gap:6px; padding:6px 4px; width:100%; box-sizing:border-box; max-height:50vh; overflow-y:auto; }
|
|
3936
|
-
.aparte-elic-message { margin:0 6px 6px; font-size:.82rem; font-weight:600; color:var(--aparte-text); line-height:1.4; }
|
|
3937
|
-
.aparte-elic-field + .aparte-elic-field { margin-top:8px; padding-top:8px; border-top:1px solid var(--aparte-border, rgba(128,128,128,0.12)); }
|
|
3938
|
-
.aparte-elic-title { margin:0 6px 4px; font-size:.8rem; font-weight:600; color:var(--aparte-text); }
|
|
3939
|
-
.aparte-elic-desc { margin:0 6px 6px; font-size:.76rem; color:var(--aparte-text-muted, rgba(128,128,128,0.75)); }
|
|
3940
|
-
.aparte-elic-options { display:flex; flex-direction:column; gap:1px; }
|
|
3941
|
-
.aparte-elic-option { display:flex; align-items:flex-start; gap:10px; padding:7px 10px; border-radius:8px; cursor:pointer; border:1px solid transparent; transition:background .12s,border-color .12s; }
|
|
3942
|
-
.aparte-elic-option:hover { background:var(--aparte-surface-2, rgba(128,128,128,0.08)); border-color:var(--aparte-border, rgba(128,128,128,0.15)); }
|
|
3943
|
-
.aparte-elic-control { margin-top:3px; flex-shrink:0; accent-color:var(--aparte-primary, #6366f1); width:15px; height:15px; cursor:pointer; }
|
|
3944
|
-
.aparte-elic-option-body { display:flex; flex-direction:column; gap:2px; flex:1; }
|
|
3945
|
-
.aparte-elic-option-title { font-size:.875rem; font-weight:500; color:var(--aparte-text, inherit); line-height:1.4; }
|
|
3946
|
-
.aparte-elic-option-desc { font-size:.78rem; color:var(--aparte-text-muted, rgba(128,128,128,0.75)); line-height:1.4; }
|
|
3947
|
-
.aparte-elic-other-input, .aparte-elic-text { margin-top:4px; width:100%; padding:6px 8px; border:1px solid var(--aparte-border, rgba(128,128,128,0.2)); border-radius:6px; font-size:.875rem; background:var(--aparte-surface-2, rgba(128,128,128,0.06)); color:var(--aparte-text, inherit); outline:none; box-sizing:border-box; font-family:inherit; }
|
|
3948
|
-
textarea.aparte-elic-text { min-height:64px; resize:vertical; }
|
|
3949
|
-
.aparte-elic-other-input:focus, .aparte-elic-text:focus { border-color:var(--aparte-primary, #6366f1); }
|
|
3950
|
-
.aparte-elic-option--recommended { border-color:var(--aparte-primary, #6366f1) !important; background:color-mix(in srgb, var(--aparte-primary, #6366f1) 6%, transparent); }
|
|
3951
|
-
.aparte-elic-footer { display:flex; justify-content:flex-end; margin-top:6px; }
|
|
3952
|
-
.aparte-elic-skip { border:none; background:transparent; color:var(--aparte-text-muted, rgba(128,128,128,0.75)); font-size:.8rem; cursor:pointer; padding:4px 8px; border-radius:6px; font-family:inherit; }
|
|
3953
|
-
.aparte-elic-skip:hover { background:var(--aparte-surface-2, rgba(128,128,128,0.08)); color:var(--aparte-text); }
|
|
3954
|
-
`;
|
|
3955
|
-
document.head.appendChild(style);
|
|
4217
|
+
console.warn(
|
|
4218
|
+
"[aparte-elicitation] No <aparte-composer> in this element's subtree, so the request was cancelled — the model will read that as a refusal. Move <aparte-elicitation> inside the <aparte-chat> it belongs to. It is deliberately NOT borrowing another chat's composer: on a page with two chats that put the question under the wrong one."
|
|
4219
|
+
);
|
|
4220
|
+
return null;
|
|
3956
4221
|
}
|
|
3957
4222
|
}
|
|
3958
4223
|
if (typeof customElements !== "undefined" && !customElements.get("aparte-elicitation")) {
|
|
@@ -3969,13 +4234,20 @@ function registerAllComponents() {
|
|
|
3969
4234
|
}
|
|
3970
4235
|
export {
|
|
3971
4236
|
A as APARTE_CONVERSATION_SCHEMA_VERSION,
|
|
3972
|
-
a as
|
|
4237
|
+
a as APARTE_DEFAULT_BUBBLE_ACTIONS,
|
|
4238
|
+
b as APARTE_DEFAULT_HOST_HANDLERS,
|
|
4239
|
+
c as APARTE_DEFAULT_ICON_FALLBACKS,
|
|
4240
|
+
d as APARTE_DEFAULT_LOCALE,
|
|
4241
|
+
e as APARTE_DEFAULT_SKELETON_FALLBACKS,
|
|
4242
|
+
f as APARTE_DEFAULT_UI_EVENTS,
|
|
4243
|
+
g as APARTE_HOST_ATTR,
|
|
4244
|
+
h as AparteBackendTransport,
|
|
3973
4245
|
AparteChat,
|
|
3974
4246
|
AparteChatBubble,
|
|
3975
|
-
|
|
4247
|
+
i as AparteChatHost,
|
|
3976
4248
|
AparteChatStatus,
|
|
3977
4249
|
AparteChatViewport,
|
|
3978
|
-
|
|
4250
|
+
j as AparteClient,
|
|
3979
4251
|
AparteComposer,
|
|
3980
4252
|
AparteComposerAction,
|
|
3981
4253
|
AparteComposerAddAttachment,
|
|
@@ -3984,54 +4256,57 @@ export {
|
|
|
3984
4256
|
AparteComposerInput,
|
|
3985
4257
|
AparteComposerSend,
|
|
3986
4258
|
AparteComposerToolbar,
|
|
3987
|
-
AparteConfig,
|
|
3988
|
-
|
|
3989
|
-
f as AparteConversationController,
|
|
4259
|
+
k as AparteConfig,
|
|
4260
|
+
l as AparteConversationController,
|
|
3990
4261
|
AparteConversationList,
|
|
4262
|
+
m as AparteConversationManager,
|
|
4263
|
+
n as AparteDirectTransport,
|
|
3991
4264
|
AparteElicitation,
|
|
3992
|
-
|
|
3993
|
-
|
|
4265
|
+
o as AparteError,
|
|
4266
|
+
p as AparteErrorCode,
|
|
4267
|
+
AparteMessageRepository,
|
|
3994
4268
|
AparteOptgroup,
|
|
3995
4269
|
AparteOption,
|
|
3996
4270
|
AparteProgressSpinner,
|
|
3997
4271
|
AparteSelect,
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
j as DEFAULT_HOST_HANDLERS,
|
|
4003
|
-
k as DEFAULT_ICON_FALLBACKS,
|
|
4004
|
-
l as DEFAULT_LOCALE,
|
|
4005
|
-
m as DEFAULT_SKELETON_FALLBACKS,
|
|
4006
|
-
n as DEFAULT_UI_EVENTS,
|
|
4007
|
-
o as DirectTransport,
|
|
4008
|
-
MessageRepository,
|
|
4009
|
-
p as applyElementProps,
|
|
4010
|
-
q as attachConfig,
|
|
4272
|
+
r as AparteStreamParser,
|
|
4273
|
+
s as aparteGlobalConfig,
|
|
4274
|
+
t as applyElementProps,
|
|
4275
|
+
u as attachConfig,
|
|
4011
4276
|
buildElicitationPanel,
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4277
|
+
w as collectRendererStyles,
|
|
4278
|
+
x as contentToText,
|
|
4279
|
+
y as contextConfig,
|
|
4280
|
+
z as createAparteChatHandler,
|
|
4281
|
+
B as createStreamAdapter,
|
|
4282
|
+
cssEscape,
|
|
4283
|
+
D as declineDefaultRenderers,
|
|
4284
|
+
E as defaultSanitizer,
|
|
4285
|
+
F as deriveArtifactKind,
|
|
4286
|
+
G as detachConfig,
|
|
4287
|
+
escapeAttr,
|
|
4288
|
+
escapeHtml,
|
|
4289
|
+
J as filesToAttachments,
|
|
4290
|
+
K as getAllRenderers,
|
|
4021
4291
|
getSegmentRenderer,
|
|
4292
|
+
installDefaultRenderersOnce,
|
|
4022
4293
|
isAwaitingReply,
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4294
|
+
O as isFormatAdapter,
|
|
4295
|
+
P as isSafeUrl,
|
|
4296
|
+
Q as isSegmentSettled,
|
|
4297
|
+
R as parseAparteEventStream,
|
|
4298
|
+
S as parseMarkdownToSegments,
|
|
4027
4299
|
populateBubbleFromMessage,
|
|
4028
|
-
|
|
4300
|
+
U as readableToAsyncIterable,
|
|
4029
4301
|
registerAllComponents,
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4302
|
+
V as registerDefaultRenderers,
|
|
4303
|
+
W as registerSegmentRenderer,
|
|
4304
|
+
X as requestUserInput,
|
|
4033
4305
|
resolveConfig,
|
|
4306
|
+
revokeAttachmentUrls,
|
|
4034
4307
|
runWithConfig,
|
|
4035
|
-
|
|
4308
|
+
$ as segmentDuration,
|
|
4309
|
+
a0 as unregisterSegmentRenderer,
|
|
4310
|
+
uuid
|
|
4036
4311
|
};
|
|
4037
4312
|
//# sourceMappingURL=index.js.map
|