@aparte/core 0.7.1 → 0.8.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/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 +35 -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 +6664 -4877
- 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-BieFbKeW.js} +1795 -1238
- package/dist/index-BieFbKeW.js.map +1 -0
- package/dist/index.css +299 -21
- package/dist/index.d.ts +21 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +447 -200
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +22 -13
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +54 -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.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 +206 -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 +1 -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/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, X as resolveConfig, C as cssEscape, Z as runWithConfig, I as escapeHtml, L as getSegmentRenderer, M as installDefaultRenderersOnce, q as AparteMessageRepository, N as isAwaitingReply, S as populateBubbleFromMessage, $ as uuid, Y as revokeAttachmentUrls, v as buildElicitationPanel } from "./index-BieFbKeW.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, T, U, V, W, _ } from "./index-BieFbKeW.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. */
|
|
@@ -1947,7 +1997,17 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1947
1997
|
* framework wrapper's host element.
|
|
1948
1998
|
*/
|
|
1949
1999
|
_activeMessageId() {
|
|
1950
|
-
|
|
2000
|
+
const head = this._repo.headId;
|
|
2001
|
+
const streaming = this._streamingMessageId();
|
|
2002
|
+
if (streaming !== null && streaming !== head) return null;
|
|
2003
|
+
return head;
|
|
2004
|
+
}
|
|
2005
|
+
/** The id of the message currently streaming, if any. */
|
|
2006
|
+
_streamingMessageId() {
|
|
2007
|
+
for (const message of this._repo.getMessages()) {
|
|
2008
|
+
if (message.isStreaming) return message.id;
|
|
2009
|
+
}
|
|
2010
|
+
return null;
|
|
1951
2011
|
}
|
|
1952
2012
|
addSegment(messageIdOrSegment, maybeSegment) {
|
|
1953
2013
|
const messageId = typeof messageIdOrSegment === "string" ? messageIdOrSegment : this._activeMessageId();
|
|
@@ -2007,7 +2067,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2007
2067
|
/**
|
|
2008
2068
|
* Persist token usage on a message and propagate to the live bubble, which is
|
|
2009
2069
|
* what allows the info ("i") action to render — provided the app declared it
|
|
2010
|
-
* with `
|
|
2070
|
+
* with `aparteGlobalConfig.setBubbleActions({ info: true })`; it is off by default,
|
|
2011
2071
|
* since the popover it opens belongs to the app.
|
|
2012
2072
|
*/
|
|
2013
2073
|
setUsage(messageId, usage) {
|
|
@@ -2125,7 +2185,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2125
2185
|
const meta = this._repo.getMessage(messageId);
|
|
2126
2186
|
if (!meta) return 0;
|
|
2127
2187
|
const newMsg = {
|
|
2128
|
-
id:
|
|
2188
|
+
id: uuid(),
|
|
2129
2189
|
role: "assistant",
|
|
2130
2190
|
content: "",
|
|
2131
2191
|
status: "pending",
|
|
@@ -2235,7 +2295,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2235
2295
|
* branch arrows on already-rendered bubbles.
|
|
2236
2296
|
*/
|
|
2237
2297
|
importTree(tree) {
|
|
2238
|
-
this.clearAll();
|
|
2298
|
+
this.clearAll({ revokeAttachments: false });
|
|
2239
2299
|
this._repo.import(tree);
|
|
2240
2300
|
this._reRenderActivePath();
|
|
2241
2301
|
}
|
|
@@ -2248,7 +2308,12 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2248
2308
|
* so desynchronises the framework's view tree from the live DOM and the
|
|
2249
2309
|
* next change-detection pass throws `NotFoundError` on insertBefore.
|
|
2250
2310
|
*/
|
|
2251
|
-
clearAll() {
|
|
2311
|
+
clearAll(options) {
|
|
2312
|
+
if (options?.revokeAttachments !== false) {
|
|
2313
|
+
for (const message of this._repo.getMessages()) {
|
|
2314
|
+
revokeAttachmentUrls(message.attachments);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2252
2317
|
this._repo.clear();
|
|
2253
2318
|
if (!this._frameworkManagedDOM) {
|
|
2254
2319
|
const wrapper = this.querySelector(".aparte-messages-wrapper");
|
|
@@ -2277,7 +2342,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2277
2342
|
* build chat history).
|
|
2278
2343
|
*/
|
|
2279
2344
|
setMessages(messages) {
|
|
2280
|
-
this.clearAll();
|
|
2345
|
+
this.clearAll({ revokeAttachments: false });
|
|
2281
2346
|
for (const m2 of messages) {
|
|
2282
2347
|
this.appendMessage(m2);
|
|
2283
2348
|
}
|
|
@@ -2535,18 +2600,27 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2535
2600
|
this._bottomSpacer.style.height = `${px}px`;
|
|
2536
2601
|
}
|
|
2537
2602
|
}
|
|
2603
|
+
// Bound fields, not inline arrows: a custom element is re-connected every
|
|
2604
|
+
// time it is MOVED in the DOM (a portal, a dialog, a framework re-parenting),
|
|
2605
|
+
// so `_setupEventListeners` runs again each time. An inline arrow can never
|
|
2606
|
+
// be handed to `removeEventListener`, so it just accumulates — one branch
|
|
2607
|
+
// click then ran N handlers, N active-path re-renders and N storage writes
|
|
2608
|
+
// through the conversation controller. The window listeners next to these
|
|
2609
|
+
// were always removed properly; these two, attached to `this`, were not.
|
|
2610
|
+
_onScrollBtnClick = () => {
|
|
2611
|
+
this._isAutoScrollEnabled = true;
|
|
2612
|
+
this._smoothScrollToBottom();
|
|
2613
|
+
this._updateScrollButton();
|
|
2614
|
+
};
|
|
2615
|
+
_onBranchNavigate = (e2) => {
|
|
2616
|
+
const evt = e2;
|
|
2617
|
+
evt.stopPropagation();
|
|
2618
|
+
this.navigateBranch(evt.detail.messageId, evt.detail.direction);
|
|
2619
|
+
};
|
|
2538
2620
|
_setupEventListeners() {
|
|
2539
2621
|
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
|
-
});
|
|
2622
|
+
this._scrollBtn?.addEventListener("click", this._onScrollBtnClick);
|
|
2623
|
+
this.addEventListener("aparte-branch-navigate", this._onBranchNavigate);
|
|
2550
2624
|
}
|
|
2551
2625
|
_setupObservers() {
|
|
2552
2626
|
this._resizeObserver = new ResizeObserver(() => {
|
|
@@ -2592,6 +2666,41 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2592
2666
|
_scrollToBottom() {
|
|
2593
2667
|
if (!this._container) return;
|
|
2594
2668
|
this._container.scrollTop = this._container.scrollHeight;
|
|
2669
|
+
this._settleAtBottom(4);
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Confirm over the next few frames that we actually reached the bottom.
|
|
2673
|
+
*
|
|
2674
|
+
* One assignment is not enough, and the reason is measured rather than guessed.
|
|
2675
|
+
* A timeline of a streamed turn on Safari (framework mode) recorded the content
|
|
2676
|
+
* settling in TWO layout passes — 1118 → 1121 → 1152 px. `scrollTop =
|
|
2677
|
+
* scrollHeight` ran against the middle one, clamped to that layout's max (603),
|
|
2678
|
+
* and nothing ran afterwards: the last 31px never closed. Auto-follow stayed
|
|
2679
|
+
* armed the whole time, so the component was not disarmed — it was SATISFIED.
|
|
2680
|
+
* `_isAtBottom()` answers "yes" for any gap under `_scrollThreshold` (50), which
|
|
2681
|
+
* is the right rule for keeping auto-follow armed and the wrong one as a
|
|
2682
|
+
* definition of "anchored".
|
|
2683
|
+
*
|
|
2684
|
+
* Ruled out on the way here, so nobody pays for it twice: not a WebKit
|
|
2685
|
+
* padding-accounting difference (a probe writing `scrollTop = 1e7` reached
|
|
2686
|
+
* exactly `scrollHeight - clientHeight`), not a missing `characterData`
|
|
2687
|
+
* mutation, and not a child resize a ResizeObserver could see.
|
|
2688
|
+
*
|
|
2689
|
+
* A BOUNDED retry, not one corrective frame: a single frame lands on the same
|
|
2690
|
+
* stale layout and was measured leaving a wider gap than doing nothing. Bounded
|
|
2691
|
+
* so it always terminates; re-reads `_isAutoScrollEnabled` every frame so a
|
|
2692
|
+
* reader who scrolls away mid-settle is left alone; stops as soon as the gap is
|
|
2693
|
+
* closed, so the common case costs one frame that does nothing.
|
|
2694
|
+
*/
|
|
2695
|
+
_settleAtBottom(framesLeft) {
|
|
2696
|
+
if (framesLeft <= 0) return;
|
|
2697
|
+
requestAnimationFrame(() => {
|
|
2698
|
+
if (!this._container || !this._isAutoScrollEnabled) return;
|
|
2699
|
+
const max = this._container.scrollHeight - this._container.clientHeight;
|
|
2700
|
+
if (max - this._container.scrollTop <= 1) return;
|
|
2701
|
+
this._container.scrollTop = max;
|
|
2702
|
+
this._settleAtBottom(framesLeft - 1);
|
|
2703
|
+
});
|
|
2595
2704
|
}
|
|
2596
2705
|
_smoothScrollToBottom() {
|
|
2597
2706
|
if (!this._container) return;
|
|
@@ -2684,7 +2793,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2684
2793
|
* Cap the number of rendered bubbles in the DOM (perf ceiling only).
|
|
2685
2794
|
*
|
|
2686
2795
|
* Drops the oldest `<aparte-chat-bubble>` elements beyond `_maxRenderedBubbles`
|
|
2687
|
-
* from the DOM. It **never** touches the
|
|
2796
|
+
* from the DOM. It **never** touches the AparteMessageRepository — the conversation
|
|
2688
2797
|
* model and its persistence snapshot stay complete (retention/eviction is a
|
|
2689
2798
|
* consumer/persistence concern, not the viewport's). No-op when a framework
|
|
2690
2799
|
* owns the DOM.
|
|
@@ -2703,11 +2812,13 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2703
2812
|
if (this._warnedMaxMessagesDeprecation) return;
|
|
2704
2813
|
this._warnedMaxMessagesDeprecation = true;
|
|
2705
2814
|
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
|
|
2815
|
+
"[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
2816
|
);
|
|
2708
2817
|
}
|
|
2709
2818
|
_cleanup() {
|
|
2710
2819
|
this._container?.removeEventListener("scroll", this._handleScroll);
|
|
2820
|
+
this._scrollBtn?.removeEventListener("click", this._onScrollBtnClick);
|
|
2821
|
+
this.removeEventListener("aparte-branch-navigate", this._onBranchNavigate);
|
|
2711
2822
|
this._resizeObserver?.disconnect();
|
|
2712
2823
|
this._mutationObserver?.disconnect();
|
|
2713
2824
|
this._resizeObserver = null;
|
|
@@ -2727,6 +2838,8 @@ class AparteComposer extends HTMLElement {
|
|
|
2727
2838
|
_attachments = [];
|
|
2728
2839
|
_listeners = /* @__PURE__ */ new Map();
|
|
2729
2840
|
_panelActive = false;
|
|
2841
|
+
/** What the send button means while a panel is up — see `showPanel`. */
|
|
2842
|
+
_panelMode = "submit";
|
|
2730
2843
|
_panelSubmitEnabled = false;
|
|
2731
2844
|
_panelOnSubmit = null;
|
|
2732
2845
|
// Internal bus events that represent an observable STATE change — these are
|
|
@@ -2743,8 +2856,25 @@ class AparteComposer extends HTMLElement {
|
|
|
2743
2856
|
_onMessageStart = this._handleMessageStart.bind(this);
|
|
2744
2857
|
_onMessageDone = this._handleMessageDone.bind(this);
|
|
2745
2858
|
/** Config governing THIS composer (nearest instance boundary, else global). */
|
|
2746
|
-
|
|
2747
|
-
|
|
2859
|
+
/**
|
|
2860
|
+
* Resolved LIVE, not cached at connect.
|
|
2861
|
+
*
|
|
2862
|
+
* A wrapper runs `AparteChatHost.bind()` — which calls `attachConfig` — from its
|
|
2863
|
+
* POST-mount hook, so this element connects BEFORE the boundary exists. Caching
|
|
2864
|
+
* here latched the global config forever: an instance `config` carrying an RTL
|
|
2865
|
+
* locale flipped the transcript and not the composer, and the
|
|
2866
|
+
* `requireModelSelection` gate was read off the wrong object. `aparte-chat-bubble`
|
|
2867
|
+
* has always resolved live, and its JSDoc names this exact race.
|
|
2868
|
+
*/
|
|
2869
|
+
get _cfg() {
|
|
2870
|
+
return resolveConfig(this);
|
|
2871
|
+
}
|
|
2872
|
+
/** Only OUR config: a change on another chat's instance must not touch us. */
|
|
2873
|
+
_onConfigChangeEvent = (e2) => {
|
|
2874
|
+
const detail = e2.detail;
|
|
2875
|
+
if (detail?.config && detail.config !== this._cfg) return;
|
|
2876
|
+
this._onConfigChange();
|
|
2877
|
+
};
|
|
2748
2878
|
/** True while `requireModelSelection` is on AND no model is selected — blocks send. */
|
|
2749
2879
|
_modelGated = false;
|
|
2750
2880
|
_onConfigChange = () => {
|
|
@@ -2776,8 +2906,7 @@ class AparteComposer extends HTMLElement {
|
|
|
2776
2906
|
window.addEventListener("aparte-message-done", this._onMessageDone);
|
|
2777
2907
|
window.addEventListener("aparte-message-error", this._onMessageDone);
|
|
2778
2908
|
window.addEventListener("aparte-message-aborted", this._onMessageDone);
|
|
2779
|
-
|
|
2780
|
-
this._configUnsub = this._cfg.subscribe(this._onConfigChange);
|
|
2909
|
+
window.addEventListener("aparte-config-change", this._onConfigChangeEvent);
|
|
2781
2910
|
this._evaluateModelGate();
|
|
2782
2911
|
this._applyDirection();
|
|
2783
2912
|
}
|
|
@@ -2786,8 +2915,7 @@ class AparteComposer extends HTMLElement {
|
|
|
2786
2915
|
window.removeEventListener("aparte-message-done", this._onMessageDone);
|
|
2787
2916
|
window.removeEventListener("aparte-message-error", this._onMessageDone);
|
|
2788
2917
|
window.removeEventListener("aparte-message-aborted", this._onMessageDone);
|
|
2789
|
-
this.
|
|
2790
|
-
this._configUnsub = null;
|
|
2918
|
+
window.removeEventListener("aparte-config-change", this._onConfigChangeEvent);
|
|
2791
2919
|
this._listeners.clear();
|
|
2792
2920
|
}
|
|
2793
2921
|
attributeChangedCallback(name, _old, value) {
|
|
@@ -2863,11 +2991,29 @@ class AparteComposer extends HTMLElement {
|
|
|
2863
2991
|
this._attachments = [];
|
|
2864
2992
|
this._emit("attachments-change", { attachments: [] });
|
|
2865
2993
|
}
|
|
2866
|
-
/**
|
|
2994
|
+
/**
|
|
2995
|
+
* Inject a panel into the composer. The send button calls `onSubmit` when clicked.
|
|
2996
|
+
*
|
|
2997
|
+
* While a panel is up, the composer is answering a QUESTION, not composing a
|
|
2998
|
+
* message — so the affordances that lead nowhere go away with the text input:
|
|
2999
|
+
* the attachment picker above all, which stayed clickable while the user was
|
|
3000
|
+
* being asked something ("on voyait encore l'icône de upload", reported from a
|
|
3001
|
+
* real session). Ratified decision #8: an affordance nothing can honour is not
|
|
3002
|
+
* rendered.
|
|
3003
|
+
*
|
|
3004
|
+
* What STAYS, deliberately: the attachments strip, because pending attachments
|
|
3005
|
+
* are the user's state and not an action to offer — hiding them would look like
|
|
3006
|
+
* losing them; and the toolbar, because switching model still does something.
|
|
3007
|
+
*
|
|
3008
|
+
* Declared with an attribute + CSS rather than the inline `style.display` this
|
|
3009
|
+
* used to set on a child: an attribute is themeable, is visible to a consumer's
|
|
3010
|
+
* own rules, and does not clobber a `display` the consumer had set (the restore
|
|
3011
|
+
* wrote `''`, not the previous value).
|
|
3012
|
+
*/
|
|
2867
3013
|
showPanel(panel, options) {
|
|
2868
3014
|
this.hidePanel();
|
|
2869
3015
|
const inputEl = this.querySelector("aparte-composer-input");
|
|
2870
|
-
|
|
3016
|
+
this.setAttribute("data-panel-active", "");
|
|
2871
3017
|
panel.dataset["apartePanel"] = "true";
|
|
2872
3018
|
if (inputEl) {
|
|
2873
3019
|
inputEl.insertAdjacentElement("afterend", panel);
|
|
@@ -2876,26 +3022,35 @@ class AparteComposer extends HTMLElement {
|
|
|
2876
3022
|
}
|
|
2877
3023
|
this._panelActive = true;
|
|
2878
3024
|
this._panelSubmitEnabled = options?.submitEnabled ?? false;
|
|
3025
|
+
this._panelMode = options?.mode ?? "submit";
|
|
2879
3026
|
this._panelOnSubmit = options?.onSubmit ?? null;
|
|
2880
|
-
this._emit("panel-change", { active: true, submitEnabled: this._panelSubmitEnabled });
|
|
3027
|
+
this._emit("panel-change", { active: true, submitEnabled: this._panelSubmitEnabled, mode: this._panelMode });
|
|
2881
3028
|
}
|
|
2882
|
-
/** Remove the panel and restore the
|
|
3029
|
+
/** Remove the panel and restore the composer's own controls. */
|
|
2883
3030
|
hidePanel() {
|
|
2884
3031
|
const existing = this.querySelector("[data-aparte-panel]");
|
|
2885
3032
|
if (existing) existing.remove();
|
|
2886
|
-
|
|
2887
|
-
if (inputEl) inputEl.style.display = "";
|
|
3033
|
+
this.removeAttribute("data-panel-active");
|
|
2888
3034
|
this._panelActive = false;
|
|
2889
3035
|
this._panelSubmitEnabled = false;
|
|
3036
|
+
this._panelMode = "submit";
|
|
2890
3037
|
this._panelOnSubmit = null;
|
|
2891
|
-
this._emit("panel-change", { active: false, submitEnabled: false });
|
|
3038
|
+
this._emit("panel-change", { active: false, submitEnabled: false, mode: "submit" });
|
|
2892
3039
|
this.focus();
|
|
2893
3040
|
}
|
|
2894
|
-
/**
|
|
2895
|
-
|
|
3041
|
+
/**
|
|
3042
|
+
* Update the send button's state while a panel is active.
|
|
3043
|
+
*
|
|
3044
|
+
* `mode` moves with it because both change on the same event — answering the
|
|
3045
|
+
* question you are on can enable the button AND turn it from "advance" into
|
|
3046
|
+
* "submit" (when it was the last one), and two separate calls would flash a
|
|
3047
|
+
* wrong icon between them.
|
|
3048
|
+
*/
|
|
3049
|
+
setPanelSubmitEnabled(enabled, mode) {
|
|
2896
3050
|
if (!this._panelActive) return;
|
|
2897
3051
|
this._panelSubmitEnabled = enabled;
|
|
2898
|
-
this.
|
|
3052
|
+
if (mode) this._panelMode = mode;
|
|
3053
|
+
this._emit("panel-change", { active: true, submitEnabled: enabled, mode: this._panelMode });
|
|
2899
3054
|
}
|
|
2900
3055
|
get panelActive() {
|
|
2901
3056
|
return this._panelActive;
|
|
@@ -2986,7 +3141,36 @@ class AparteComposer extends HTMLElement {
|
|
|
2986
3141
|
* Without this filter, streaming in one chat flips every composer's state. */
|
|
2987
3142
|
_isForThisComposer(e2) {
|
|
2988
3143
|
const evtTargetId = e2.detail?.targetId;
|
|
2989
|
-
return !evtTargetId || !this.
|
|
3144
|
+
return !evtTargetId || !this._ownTargetId() || evtTargetId === this._ownTargetId();
|
|
3145
|
+
}
|
|
3146
|
+
/**
|
|
3147
|
+
* Which chat this composer belongs to: its `target` attribute, or the id of the
|
|
3148
|
+
* chat host above it.
|
|
3149
|
+
*
|
|
3150
|
+
* All four wrappers set `target` themselves, so the attribute alone identified a
|
|
3151
|
+
* composer there. In RAW core — the documented quick start, where the markup is
|
|
3152
|
+
* hand-written — nothing sets it, so `!this.targetId` was true and this composer
|
|
3153
|
+
* accepted every chat's lifecycle events: on a two-chat page, one chat's Stop
|
|
3154
|
+
* tore down the other's open elicitation panel while its tool call kept waiting,
|
|
3155
|
+
* i.e. the question vanished and the turn hung.
|
|
3156
|
+
*
|
|
3157
|
+
* Found by a two-chat test written for the elicitation presenter, which is the
|
|
3158
|
+
* only reason it surfaced: raw core with two chats is a shape nothing exercised.
|
|
3159
|
+
* The hosts matched are the ones `aparte-chat-bubble._resolveTargetId()` matches,
|
|
3160
|
+
* for the reason written there — Angular's wrapper root IS `<aparte-chat>`, the
|
|
3161
|
+
* other three render a `[data-aparte-chat]` div.
|
|
3162
|
+
*/
|
|
3163
|
+
_ownTargetId() {
|
|
3164
|
+
const attr = this.targetId;
|
|
3165
|
+
if (attr) return attr;
|
|
3166
|
+
let el = this.parentElement;
|
|
3167
|
+
while (el) {
|
|
3168
|
+
const tag = el.tagName?.toLowerCase();
|
|
3169
|
+
const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
|
|
3170
|
+
if (isHost && el.id) return el.id;
|
|
3171
|
+
el = el.parentElement;
|
|
3172
|
+
}
|
|
3173
|
+
return void 0;
|
|
2990
3174
|
}
|
|
2991
3175
|
_handleMessageStart(e2) {
|
|
2992
3176
|
if (!this._isForThisComposer(e2)) return;
|
|
@@ -3093,7 +3277,7 @@ class AparteComposerInput extends HTMLElement {
|
|
|
3093
3277
|
_render() {
|
|
3094
3278
|
if (this.querySelector(".aparte-ci-editor")) return;
|
|
3095
3279
|
const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
|
|
3096
|
-
const placeholder =
|
|
3280
|
+
const placeholder = escapeAttr(this._getPlaceholder());
|
|
3097
3281
|
this.innerHTML = `<div
|
|
3098
3282
|
class="aparte-ci-editor"
|
|
3099
3283
|
contenteditable="${!disabled}"
|
|
@@ -3217,9 +3401,6 @@ class AparteComposerInput extends HTMLElement {
|
|
|
3217
3401
|
this._editor.setAttribute("aria-disabled", String(disabled));
|
|
3218
3402
|
}
|
|
3219
3403
|
/** 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
3404
|
}
|
|
3224
3405
|
if (!customElements.get("aparte-composer-input")) {
|
|
3225
3406
|
customElements.define("aparte-composer-input", AparteComposerInput);
|
|
@@ -3250,8 +3431,8 @@ class AparteComposerSend extends HTMLElement {
|
|
|
3250
3431
|
const disabled = !root || root.disabled || root.value.trim() === "";
|
|
3251
3432
|
this.innerHTML = `<button
|
|
3252
3433
|
class="aparte-cs-button aparte-send-button"
|
|
3253
|
-
aria-label="${label}"
|
|
3254
|
-
title="${label}"
|
|
3434
|
+
aria-label="${escapeAttr(label)}"
|
|
3435
|
+
title="${escapeAttr(label)}"
|
|
3255
3436
|
${disabled ? "disabled" : ""}
|
|
3256
3437
|
>${icon}</button>`;
|
|
3257
3438
|
this._button = this.querySelector(".aparte-cs-button");
|
|
@@ -3276,12 +3457,14 @@ class AparteComposerSend extends HTMLElement {
|
|
|
3276
3457
|
root._on("attachments-change", () => this._syncState())
|
|
3277
3458
|
);
|
|
3278
3459
|
this._unsubscribes.push(
|
|
3279
|
-
root._on("panel-change", ({ active, submitEnabled }) => {
|
|
3460
|
+
root._on("panel-change", ({ active, submitEnabled, mode }) => {
|
|
3280
3461
|
if (!this._button) return;
|
|
3281
3462
|
if (active) {
|
|
3282
3463
|
this._button.disabled = !submitEnabled;
|
|
3283
|
-
|
|
3284
|
-
const
|
|
3464
|
+
const cfg = resolveConfig(this);
|
|
3465
|
+
const advancing = mode === "advance";
|
|
3466
|
+
this._button.innerHTML = advancing ? cfg.getIcon("nextBranch") : this._getSubmitIcon();
|
|
3467
|
+
const label = advancing ? cfg.t("elicitationNext") || "Next" : cfg.t("submitButton") || "Submit";
|
|
3285
3468
|
this._button.setAttribute("aria-label", label);
|
|
3286
3469
|
this._button.setAttribute("title", label);
|
|
3287
3470
|
this._button.classList.remove("is-streaming");
|
|
@@ -3324,6 +3507,13 @@ class AparteComposerSend extends HTMLElement {
|
|
|
3324
3507
|
this._syncState();
|
|
3325
3508
|
}
|
|
3326
3509
|
}
|
|
3510
|
+
/**
|
|
3511
|
+
* The icon for submitting an ANSWER, which is not the same act as sending a
|
|
3512
|
+
* message — one button, two meanings, and it has to say which one it is.
|
|
3513
|
+
*/
|
|
3514
|
+
_getSubmitIcon() {
|
|
3515
|
+
return resolveConfig(this).getIcon("check");
|
|
3516
|
+
}
|
|
3327
3517
|
_getSendIcon() {
|
|
3328
3518
|
return resolveConfig(this).getIcon("send") || "Send";
|
|
3329
3519
|
}
|
|
@@ -3358,8 +3548,8 @@ class AparteComposerCancel extends HTMLElement {
|
|
|
3358
3548
|
const icon = this._getStopIcon();
|
|
3359
3549
|
this.innerHTML = `<button
|
|
3360
3550
|
class="aparte-cc-button"
|
|
3361
|
-
aria-label="${label}"
|
|
3362
|
-
title="${label}"
|
|
3551
|
+
aria-label="${escapeAttr(label)}"
|
|
3552
|
+
title="${escapeAttr(label)}"
|
|
3363
3553
|
hidden
|
|
3364
3554
|
>${icon}</button>`;
|
|
3365
3555
|
this._button = this.querySelector(".aparte-cc-button");
|
|
@@ -3426,9 +3616,9 @@ class AparteComposerAttachments extends HTMLElement {
|
|
|
3426
3616
|
if (file.type.startsWith("image/")) {
|
|
3427
3617
|
const url = URL.createObjectURL(file);
|
|
3428
3618
|
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>`;
|
|
3619
|
+
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
3620
|
}
|
|
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>`;
|
|
3621
|
+
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
3622
|
}).join("");
|
|
3433
3623
|
this.querySelectorAll(".aparte-thumb__remove").forEach((btn, i2) => {
|
|
3434
3624
|
btn.addEventListener("click", (e2) => {
|
|
@@ -3507,8 +3697,8 @@ class AparteComposerAddAttachment extends HTMLElement {
|
|
|
3507
3697
|
const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
|
|
3508
3698
|
this.innerHTML = `<button
|
|
3509
3699
|
class="aparte-caa-button aparte-action-button"
|
|
3510
|
-
aria-label="${label}"
|
|
3511
|
-
title="${label}"
|
|
3700
|
+
aria-label="${escapeAttr(label)}"
|
|
3701
|
+
title="${escapeAttr(label)}"
|
|
3512
3702
|
type="button"
|
|
3513
3703
|
${disabled ? "disabled" : ""}
|
|
3514
3704
|
>${icon}</button>`;
|
|
@@ -3618,7 +3808,7 @@ class AparteComposerAction extends HTMLElement {
|
|
|
3618
3808
|
}
|
|
3619
3809
|
_render() {
|
|
3620
3810
|
if (this.querySelector(".aparte-cact-button")) return;
|
|
3621
|
-
const label =
|
|
3811
|
+
const label = escapeAttr(this.getAttribute("label") ?? "");
|
|
3622
3812
|
const icon = this._resolveIcon(this.getAttribute("icon") ?? "");
|
|
3623
3813
|
const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
|
|
3624
3814
|
this.innerHTML = `<button
|
|
@@ -3658,9 +3848,6 @@ class AparteComposerAction extends HTMLElement {
|
|
|
3658
3848
|
return resolveConfig(this).getIcon(icon) ?? icon;
|
|
3659
3849
|
}
|
|
3660
3850
|
/** 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
3851
|
}
|
|
3665
3852
|
if (!customElements.get("aparte-composer-action")) {
|
|
3666
3853
|
customElements.define("aparte-composer-action", AparteComposerAction);
|
|
@@ -3776,8 +3963,8 @@ class AparteConversationList extends HTMLElement {
|
|
|
3776
3963
|
class="aparte-conv-item__archive"
|
|
3777
3964
|
type="button"
|
|
3778
3965
|
data-archive-id="${escapedId}"
|
|
3779
|
-
data-archive-action="${archiveAction}"
|
|
3780
|
-
aria-label="${archiveAriaLabel}"
|
|
3966
|
+
data-archive-action="${escapeAttr(archiveAction)}"
|
|
3967
|
+
aria-label="${escapeAttr(archiveAriaLabel)}"
|
|
3781
3968
|
tabindex="0"
|
|
3782
3969
|
>${archiveIcon}</button>
|
|
3783
3970
|
<button
|
|
@@ -3856,14 +4043,49 @@ class AparteConversationList extends HTMLElement {
|
|
|
3856
4043
|
if (!customElements.get("aparte-conversation-list")) customElements.define("aparte-conversation-list", AparteConversationList);
|
|
3857
4044
|
class AparteElicitation extends HTMLElement {
|
|
3858
4045
|
_pending = null;
|
|
3859
|
-
|
|
4046
|
+
/**
|
|
4047
|
+
* A turn ended — cancel the open question only if it was OUR turn.
|
|
4048
|
+
*
|
|
4049
|
+
* These two listeners sit on `window` and had no instance filter at all, so a
|
|
4050
|
+
* Stop (or an error) in one chat cancelled the question a DIFFERENT chat was
|
|
4051
|
+
* waiting on — and that chat's model was told the user had refused a question
|
|
4052
|
+
* the user was still looking at. Same defect as the `compact()` handler that
|
|
4053
|
+
* emptied both chats, which its four sibling handlers in `AparteClient` already
|
|
4054
|
+
* guarded against.
|
|
4055
|
+
*
|
|
4056
|
+
* The leniency rule is the composer's, deliberately: an event with no
|
|
4057
|
+
* `targetId` is for everyone (a single-chat app never sets one), and a chat we
|
|
4058
|
+
* cannot identify accepts everything rather than becoming deaf. Only a
|
|
4059
|
+
* MISMATCH is ignored.
|
|
4060
|
+
*/
|
|
4061
|
+
_onTurnEnd = (e2) => {
|
|
4062
|
+
const evtTargetId = e2.detail?.targetId;
|
|
4063
|
+
const own = this._pendingTargetId();
|
|
4064
|
+
if (evtTargetId && own && evtTargetId !== own) return;
|
|
4065
|
+
this._cancelPending();
|
|
4066
|
+
};
|
|
3860
4067
|
connectedCallback() {
|
|
3861
4068
|
this.style.display = "none";
|
|
3862
|
-
this._injectStyles();
|
|
3863
4069
|
resolveConfig(this).setElicitationPresenter(this._present);
|
|
3864
4070
|
window.addEventListener("aparte-message-aborted", this._onTurnEnd);
|
|
3865
4071
|
window.addEventListener("aparte-message-error", this._onTurnEnd);
|
|
3866
4072
|
}
|
|
4073
|
+
/**
|
|
4074
|
+
* The boundary above us appeared, changed, or went away — move the
|
|
4075
|
+
* registration with it.
|
|
4076
|
+
*
|
|
4077
|
+
* `connectedCallback` alone is not enough and cannot be: registering is a
|
|
4078
|
+
* WRITE, and under all four wrappers it happens before `attachConfig` runs, so
|
|
4079
|
+
* it lands on the global singleton. `requestUserInput()` then resolves the
|
|
4080
|
+
* instance config, finds nothing, and answers the model `cancel` — the model
|
|
4081
|
+
* hears the user refuse a question the user never saw.
|
|
4082
|
+
*
|
|
4083
|
+
* See {@link AparteConfigAware}.
|
|
4084
|
+
*/
|
|
4085
|
+
aparteConfigChanged(next, previous) {
|
|
4086
|
+
if (previous.getElicitationPresenter() === this._present) previous.setElicitationPresenter(null);
|
|
4087
|
+
next.setElicitationPresenter(this._present);
|
|
4088
|
+
}
|
|
3867
4089
|
disconnectedCallback() {
|
|
3868
4090
|
const cfg = resolveConfig(this);
|
|
3869
4091
|
if (cfg.getElicitationPresenter() === this._present) cfg.setElicitationPresenter(null);
|
|
@@ -3891,22 +4113,26 @@ class AparteElicitation extends HTMLElement {
|
|
|
3891
4113
|
}
|
|
3892
4114
|
request.signal.addEventListener("abort", () => settle({ action: "cancel" }), { once: true });
|
|
3893
4115
|
}
|
|
3894
|
-
const
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
footer.className = "aparte-elic-footer";
|
|
4116
|
+
const cfg = resolveConfig(this);
|
|
4117
|
+
const panel = runWithConfig(cfg, () => buildElicitationPanel(request.message, request.schema, () => {
|
|
4118
|
+
composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
|
|
4119
|
+
}));
|
|
3899
4120
|
const skip = document.createElement("button");
|
|
3900
4121
|
skip.type = "button";
|
|
3901
4122
|
skip.className = "aparte-elic-skip";
|
|
3902
|
-
skip.textContent = "
|
|
4123
|
+
skip.textContent = cfg.t("elicitationSkip");
|
|
3903
4124
|
skip.addEventListener("click", () => settle({ action: "decline" }));
|
|
3904
|
-
|
|
3905
|
-
panel.el.appendChild(footer);
|
|
4125
|
+
panel.dismiss.appendChild(skip);
|
|
3906
4126
|
this._pending = { settle, composer };
|
|
3907
4127
|
composer.showPanel(panel.el, {
|
|
3908
|
-
submitEnabled: panel.
|
|
4128
|
+
submitEnabled: panel.canProceed(),
|
|
4129
|
+
mode: panel.mode(),
|
|
3909
4130
|
onSubmit: () => {
|
|
4131
|
+
if (panel.mode() === "advance") {
|
|
4132
|
+
panel.proceed();
|
|
4133
|
+
composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
|
|
4134
|
+
return;
|
|
4135
|
+
}
|
|
3910
4136
|
if (panel.isComplete()) settle({ action: "accept", content: panel.getContent() });
|
|
3911
4137
|
}
|
|
3912
4138
|
});
|
|
@@ -3916,43 +4142,56 @@ class AparteElicitation extends HTMLElement {
|
|
|
3916
4142
|
_cancelPending() {
|
|
3917
4143
|
this._pending?.settle({ action: "cancel" });
|
|
3918
4144
|
}
|
|
4145
|
+
/**
|
|
4146
|
+
* The host id of the chat whose composer holds the open panel.
|
|
4147
|
+
*
|
|
4148
|
+
* Walks up from the composer rather than from `this`, because the panel lives in
|
|
4149
|
+
* the composer and that is what the turn belongs to. Matches the hosts
|
|
4150
|
+
* `aparte-chat-bubble._resolveTargetId()` matches, for the reason written there:
|
|
4151
|
+
* Angular's wrapper root IS the `<aparte-chat>` element, while the plain-root
|
|
4152
|
+
* wrappers render a `[data-aparte-chat]` div instead, so matching only the tag
|
|
4153
|
+
* resolves `undefined` on three wrappers out of four.
|
|
4154
|
+
*/
|
|
4155
|
+
_pendingTargetId() {
|
|
4156
|
+
let el = this._pending?.composer ?? null;
|
|
4157
|
+
while (el) {
|
|
4158
|
+
const tag = el.tagName?.toLowerCase();
|
|
4159
|
+
const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
|
|
4160
|
+
if (isHost && el.id) return el.id;
|
|
4161
|
+
el = el.parentElement;
|
|
4162
|
+
}
|
|
4163
|
+
return void 0;
|
|
4164
|
+
}
|
|
4165
|
+
/**
|
|
4166
|
+
* The composer to present in: the nearest one in an ancestor subtree, and
|
|
4167
|
+
* nothing else.
|
|
4168
|
+
*
|
|
4169
|
+
* There used to be a `document.querySelector('aparte-composer')` fallback, which
|
|
4170
|
+
* is the "first chat on the page" bug this repo has now fixed in four other
|
|
4171
|
+
* places: on a page with two chats, an elicitation that could not find its own
|
|
4172
|
+
* composer mounted its panel in the OTHER chat's — so one conversation's question
|
|
4173
|
+
* appeared under the other conversation, and answering it resolved a tool call
|
|
4174
|
+
* belonging to a chat the user was not looking at.
|
|
4175
|
+
*
|
|
4176
|
+
* Returning `null` instead resolves `cancel`, which is honest: nothing was
|
|
4177
|
+
* shown, so nothing was answered. The warning names the fix, because this is a
|
|
4178
|
+
* setup mistake and only the developer can correct it — the guide's own example
|
|
4179
|
+
* puts `<aparte-elicitation>` inside `<aparte-chat>`.
|
|
4180
|
+
*/
|
|
3919
4181
|
_getComposer() {
|
|
3920
4182
|
let node = this.parentElement;
|
|
3921
4183
|
while (node) {
|
|
3922
4184
|
const composer = node.querySelector("aparte-composer");
|
|
3923
4185
|
if (composer && typeof composer.showPanel === "function") return composer;
|
|
4186
|
+
const tag = node.tagName?.toLowerCase();
|
|
4187
|
+
const isChatBoundary = tag === "aparte-chat" || tag === "aparte-chat-component" || node.hasAttribute?.("data-aparte-chat");
|
|
4188
|
+
if (isChatBoundary) break;
|
|
3924
4189
|
node = node.parentElement;
|
|
3925
4190
|
}
|
|
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);
|
|
4191
|
+
console.warn(
|
|
4192
|
+
"[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."
|
|
4193
|
+
);
|
|
4194
|
+
return null;
|
|
3956
4195
|
}
|
|
3957
4196
|
}
|
|
3958
4197
|
if (typeof customElements !== "undefined" && !customElements.get("aparte-elicitation")) {
|
|
@@ -3969,13 +4208,20 @@ function registerAllComponents() {
|
|
|
3969
4208
|
}
|
|
3970
4209
|
export {
|
|
3971
4210
|
A as APARTE_CONVERSATION_SCHEMA_VERSION,
|
|
3972
|
-
a as
|
|
4211
|
+
a as APARTE_DEFAULT_BUBBLE_ACTIONS,
|
|
4212
|
+
b as APARTE_DEFAULT_HOST_HANDLERS,
|
|
4213
|
+
c as APARTE_DEFAULT_ICON_FALLBACKS,
|
|
4214
|
+
d as APARTE_DEFAULT_LOCALE,
|
|
4215
|
+
e as APARTE_DEFAULT_SKELETON_FALLBACKS,
|
|
4216
|
+
f as APARTE_DEFAULT_UI_EVENTS,
|
|
4217
|
+
g as APARTE_HOST_ATTR,
|
|
4218
|
+
h as AparteBackendTransport,
|
|
3973
4219
|
AparteChat,
|
|
3974
4220
|
AparteChatBubble,
|
|
3975
|
-
|
|
4221
|
+
i as AparteChatHost,
|
|
3976
4222
|
AparteChatStatus,
|
|
3977
4223
|
AparteChatViewport,
|
|
3978
|
-
|
|
4224
|
+
j as AparteClient,
|
|
3979
4225
|
AparteComposer,
|
|
3980
4226
|
AparteComposerAction,
|
|
3981
4227
|
AparteComposerAddAttachment,
|
|
@@ -3984,54 +4230,55 @@ export {
|
|
|
3984
4230
|
AparteComposerInput,
|
|
3985
4231
|
AparteComposerSend,
|
|
3986
4232
|
AparteComposerToolbar,
|
|
3987
|
-
AparteConfig,
|
|
3988
|
-
|
|
3989
|
-
f as AparteConversationController,
|
|
4233
|
+
k as AparteConfig,
|
|
4234
|
+
l as AparteConversationController,
|
|
3990
4235
|
AparteConversationList,
|
|
4236
|
+
m as AparteConversationManager,
|
|
4237
|
+
n as AparteDirectTransport,
|
|
3991
4238
|
AparteElicitation,
|
|
3992
|
-
|
|
3993
|
-
|
|
4239
|
+
o as AparteError,
|
|
4240
|
+
p as AparteErrorCode,
|
|
4241
|
+
AparteMessageRepository,
|
|
3994
4242
|
AparteOptgroup,
|
|
3995
4243
|
AparteOption,
|
|
3996
4244
|
AparteProgressSpinner,
|
|
3997
4245
|
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,
|
|
4246
|
+
r as AparteStreamParser,
|
|
4247
|
+
s as aparteGlobalConfig,
|
|
4248
|
+
t as applyElementProps,
|
|
4249
|
+
u as attachConfig,
|
|
4011
4250
|
buildElicitationPanel,
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4251
|
+
w as collectRendererStyles,
|
|
4252
|
+
x as contentToText,
|
|
4253
|
+
y as contextConfig,
|
|
4254
|
+
z as createAparteChatHandler,
|
|
4255
|
+
B as createStreamAdapter,
|
|
4256
|
+
cssEscape,
|
|
4257
|
+
D as declineDefaultRenderers,
|
|
4258
|
+
E as defaultSanitizer,
|
|
4259
|
+
F as deriveArtifactKind,
|
|
4260
|
+
G as detachConfig,
|
|
4261
|
+
escapeAttr,
|
|
4262
|
+
escapeHtml,
|
|
4263
|
+
J as filesToAttachments,
|
|
4264
|
+
K as getAllRenderers,
|
|
4021
4265
|
getSegmentRenderer,
|
|
4266
|
+
installDefaultRenderersOnce,
|
|
4022
4267
|
isAwaitingReply,
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4268
|
+
O as isFormatAdapter,
|
|
4269
|
+
P as isSafeUrl,
|
|
4270
|
+
Q as parseAparteEventStream,
|
|
4271
|
+
R as parseMarkdownToSegments,
|
|
4027
4272
|
populateBubbleFromMessage,
|
|
4028
|
-
|
|
4273
|
+
T as readableToAsyncIterable,
|
|
4029
4274
|
registerAllComponents,
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4275
|
+
U as registerDefaultRenderers,
|
|
4276
|
+
V as registerSegmentRenderer,
|
|
4277
|
+
W as requestUserInput,
|
|
4033
4278
|
resolveConfig,
|
|
4279
|
+
revokeAttachmentUrls,
|
|
4034
4280
|
runWithConfig,
|
|
4035
|
-
|
|
4281
|
+
_ as unregisterSegmentRenderer,
|
|
4282
|
+
uuid
|
|
4036
4283
|
};
|
|
4037
4284
|
//# sourceMappingURL=index.js.map
|