@aparte/core 0.3.0-alpha.0 → 0.5.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/dist/client/aparte-client.d.ts +7 -1
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +26 -2
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/chat/aparte-chat.d.ts +13 -0
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts +3 -2
- package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +17 -4
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +64 -7
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/icon-provider.d.ts +21 -21
- package/dist/config/icon-provider.d.ts.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/custom-elements.json +3435 -3000
- package/dist/host/aparte-chat-host.d.ts +58 -1
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/{index-CW2BCAqn.js → index-Dn7TH14G.js} +282 -81
- package/dist/index-Dn7TH14G.js.map +1 -0
- package/dist/index.css +75 -8
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +216 -62
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +3 -1
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +34 -30
- package/dist/index.node.js.map +1 -1
- package/dist/primitives/select/aparte-optgroup.d.ts +2 -0
- package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts +2 -0
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/renderers/index.d.ts +1 -1
- package/dist/renderers/index.d.ts.map +1 -1
- package/dist/renderers/segment-renderers.d.ts +23 -0
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/types/imperative-api.d.ts +13 -0
- package/dist/types/imperative-api.d.ts.map +1 -1
- package/dist/types/models.d.ts +56 -9
- package/dist/types/models.d.ts.map +1 -1
- package/dist/utils/files-to-attachments.d.ts +27 -0
- package/dist/utils/files-to-attachments.d.ts.map +1 -0
- package/dist/utils/is-awaiting-reply.d.ts +22 -0
- package/dist/utils/is-awaiting-reply.d.ts.map +1 -0
- package/package.json +8 -1
- package/dist/index-CW2BCAqn.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A, a, b, c, e, f, g, h, i, B, C, D, j, k, l, m, n, o,
|
|
1
|
+
import { R as resolveConfig, S as runWithConfig, F as getSegmentRenderer, U as installDefaultRenderersOnce, M as MessageRepository, G as isAwaitingReply, L as populateBubbleFromMessage, d as AparteConfig, r as buildElicitationPanel } from "./index-Dn7TH14G.js";
|
|
2
|
+
import { A, a, b, c, e, f, g, h, i, B, C, D, j, k, l, m, n, o, p, q, s, t, u, v, w, x, y, z, E, H, I, J, K, N, O, P, Q, T } from "./index-Dn7TH14G.js";
|
|
3
3
|
class AparteOption extends HTMLElement {
|
|
4
4
|
static get observedAttributes() {
|
|
5
5
|
return ["value", "disabled", "selected", "data-status"];
|
|
@@ -73,6 +73,8 @@ if (!customElements.get("aparte-option")) {
|
|
|
73
73
|
customElements.define("aparte-option", AparteOption);
|
|
74
74
|
}
|
|
75
75
|
class AparteOptgroup extends HTMLElement {
|
|
76
|
+
/** Ids for the label span the group points `aria-labelledby` at. */
|
|
77
|
+
static _labelIdSeq = 0;
|
|
76
78
|
static get observedAttributes() {
|
|
77
79
|
return ["label", "collapsible", "collapsed", "loading"];
|
|
78
80
|
}
|
|
@@ -121,10 +123,11 @@ class AparteOptgroup extends HTMLElement {
|
|
|
121
123
|
if (!existingHeader) {
|
|
122
124
|
const header = document.createElement("div");
|
|
123
125
|
header.className = "aparte-optgroup-header";
|
|
124
|
-
header.setAttribute("aria-label", this.label);
|
|
125
126
|
const labelSpan = document.createElement("span");
|
|
126
127
|
labelSpan.className = "aparte-optgroup-label";
|
|
128
|
+
labelSpan.id = `aparte-optgroup-label-${++AparteOptgroup._labelIdSeq}`;
|
|
127
129
|
labelSpan.textContent = this.label;
|
|
130
|
+
this.setAttribute("aria-labelledby", labelSpan.id);
|
|
128
131
|
header.appendChild(labelSpan);
|
|
129
132
|
if (this.collapsible) {
|
|
130
133
|
const chevron = document.createElement("span");
|
|
@@ -179,6 +182,8 @@ if (!customElements.get("aparte-optgroup")) {
|
|
|
179
182
|
}
|
|
180
183
|
class AparteSelect extends HTMLElement {
|
|
181
184
|
static _optIdSeq = 0;
|
|
185
|
+
/** Fallback ids for the listbox `aria-controls` target when the host has no id. */
|
|
186
|
+
static _listboxSeq = 0;
|
|
182
187
|
_value = "";
|
|
183
188
|
_isOpen = false;
|
|
184
189
|
_activeIndex = -1;
|
|
@@ -271,17 +276,21 @@ class AparteSelect extends HTMLElement {
|
|
|
271
276
|
trigger.append(labelSpan, chevronSpan);
|
|
272
277
|
const dropdown = document.createElement("div");
|
|
273
278
|
dropdown.className = "aparte-select-dropdown";
|
|
274
|
-
dropdown.setAttribute("role", "listbox");
|
|
275
279
|
dropdown.hidden = !this._isOpen;
|
|
276
280
|
if (searchable) {
|
|
277
281
|
const searchInput = document.createElement("input");
|
|
278
282
|
searchInput.type = "text";
|
|
279
283
|
searchInput.className = "aparte-select-search";
|
|
280
284
|
searchInput.placeholder = "Search...";
|
|
285
|
+
searchInput.setAttribute("aria-label", "Search options");
|
|
281
286
|
dropdown.appendChild(searchInput);
|
|
282
287
|
}
|
|
283
288
|
const optionsContainer = document.createElement("div");
|
|
284
289
|
optionsContainer.className = "aparte-select-options";
|
|
290
|
+
optionsContainer.setAttribute("role", "listbox");
|
|
291
|
+
optionsContainer.setAttribute("aria-label", trigger.getAttribute("aria-label") ?? placeholder);
|
|
292
|
+
optionsContainer.id = this.id ? `${this.id}-listbox` : `aparte-listbox-${++AparteSelect._listboxSeq}`;
|
|
293
|
+
trigger.setAttribute("aria-controls", optionsContainer.id);
|
|
285
294
|
slottedChildren.forEach((child) => {
|
|
286
295
|
if (child.tagName === "APARTE-OPTION" || child.tagName === "APARTE-OPTGROUP") {
|
|
287
296
|
optionsContainer.appendChild(child);
|
|
@@ -589,9 +598,15 @@ if (!customElements.get("aparte-progress-spinner")) {
|
|
|
589
598
|
}
|
|
590
599
|
class AparteChat extends HTMLElement {
|
|
591
600
|
static get observedAttributes() {
|
|
592
|
-
return ["placeholder", "disabled", "center-empty"];
|
|
601
|
+
return ["placeholder", "disabled", "center-empty", "attachments"];
|
|
593
602
|
}
|
|
594
603
|
_observer = null;
|
|
604
|
+
/**
|
|
605
|
+
* True only for the composition THIS element injected. An author-provided
|
|
606
|
+
* composer (or a `framework-managed` host) is never edited by the attachments
|
|
607
|
+
* toggle below — those own their own markup.
|
|
608
|
+
*/
|
|
609
|
+
_ownsShell = false;
|
|
595
610
|
connectedCallback() {
|
|
596
611
|
this._render();
|
|
597
612
|
this._forwardAttr("placeholder");
|
|
@@ -608,6 +623,10 @@ class AparteChat extends HTMLElement {
|
|
|
608
623
|
this._syncEmptyWatch();
|
|
609
624
|
return;
|
|
610
625
|
}
|
|
626
|
+
if (name === "attachments") {
|
|
627
|
+
this._syncAttachments();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
611
630
|
const composer = this.querySelector("aparte-composer");
|
|
612
631
|
if (!composer) return;
|
|
613
632
|
if (newValue !== null) composer.setAttribute(name, newValue);
|
|
@@ -626,17 +645,43 @@ class AparteChat extends HTMLElement {
|
|
|
626
645
|
if (this.querySelector("aparte-chat-viewport")) return;
|
|
627
646
|
const placeholder = this.getAttribute("placeholder");
|
|
628
647
|
const composerAttrs = (placeholder !== null ? ` placeholder="${this._escapeAttr(placeholder)}"` : "") + (this.hasAttribute("disabled") ? " disabled" : "");
|
|
648
|
+
const attachments = this.hasAttribute("attachments");
|
|
629
649
|
this.innerHTML = `
|
|
630
650
|
<aparte-chat-viewport></aparte-chat-viewport>
|
|
631
651
|
<aparte-composer${composerAttrs}>
|
|
632
652
|
<div class="aparte-composer-shell">
|
|
653
|
+
${attachments ? "<aparte-composer-attachments></aparte-composer-attachments>" : ""}
|
|
633
654
|
<div class="aparte-composer-row">
|
|
655
|
+
${attachments ? "<aparte-composer-add-attachment></aparte-composer-add-attachment>" : ""}
|
|
634
656
|
<aparte-composer-input></aparte-composer-input>
|
|
635
657
|
<aparte-composer-send></aparte-composer-send>
|
|
636
658
|
</div>
|
|
637
659
|
</div>
|
|
638
660
|
</aparte-composer>
|
|
639
661
|
`;
|
|
662
|
+
this._ownsShell = true;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Add/remove the two attachment primitives on the composition we injected, so
|
|
666
|
+
* toggling the attribute after mount works like the wrappers' reactive prop
|
|
667
|
+
* (there, a re-render does it). Author-provided markup is left alone.
|
|
668
|
+
*/
|
|
669
|
+
_syncAttachments() {
|
|
670
|
+
if (!this._ownsShell) return;
|
|
671
|
+
const composer = this.composer;
|
|
672
|
+
const shell = composer?.querySelector(".aparte-composer-shell");
|
|
673
|
+
const row = shell?.querySelector(".aparte-composer-row");
|
|
674
|
+
if (!composer || !shell || !row) return;
|
|
675
|
+
const strip = shell.querySelector("aparte-composer-attachments");
|
|
676
|
+
const picker = row.querySelector("aparte-composer-add-attachment");
|
|
677
|
+
if (this.hasAttribute("attachments")) {
|
|
678
|
+
if (!strip) shell.insertBefore(document.createElement("aparte-composer-attachments"), row);
|
|
679
|
+
if (!picker) row.insertBefore(document.createElement("aparte-composer-add-attachment"), row.firstChild);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
strip?.remove();
|
|
683
|
+
picker?.remove();
|
|
684
|
+
composer.clearAttachments();
|
|
640
685
|
}
|
|
641
686
|
/** Set an attribute on the inner composer only when the shell carries it. */
|
|
642
687
|
_forwardAttr(name) {
|
|
@@ -676,7 +721,13 @@ let _warnedNoRenderer = false;
|
|
|
676
721
|
function warnMissingRenderer(type) {
|
|
677
722
|
if (_warnedNoRenderer) return;
|
|
678
723
|
_warnedNoRenderer = true;
|
|
679
|
-
console.warn(`[aparte] No renderer for segment "${type}".
|
|
724
|
+
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`);
|
|
725
|
+
}
|
|
726
|
+
function resolveSegmentRenderer(type) {
|
|
727
|
+
const renderer = getSegmentRenderer(type);
|
|
728
|
+
if (renderer) return renderer;
|
|
729
|
+
installDefaultRenderersOnce();
|
|
730
|
+
return getSegmentRenderer(type);
|
|
680
731
|
}
|
|
681
732
|
function segmentRenderResultToElement(result) {
|
|
682
733
|
if (result instanceof HTMLElement) return result;
|
|
@@ -691,6 +742,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
691
742
|
_attachmentsEl = null;
|
|
692
743
|
_actionBarEl = null;
|
|
693
744
|
_branchPickerEl = null;
|
|
745
|
+
_footerEl = null;
|
|
694
746
|
_content = "";
|
|
695
747
|
_streaming = false;
|
|
696
748
|
_segments = [];
|
|
@@ -795,11 +847,13 @@ class AparteChatBubble extends HTMLElement {
|
|
|
795
847
|
setSegments(segments) {
|
|
796
848
|
this._segments = segments;
|
|
797
849
|
this._renderSegments();
|
|
850
|
+
this._updateWaiting();
|
|
798
851
|
}
|
|
799
852
|
/** Add a segment */
|
|
800
853
|
addSegment(segment) {
|
|
801
854
|
this._segments.push(segment);
|
|
802
855
|
this._appendSegmentEl(segment);
|
|
856
|
+
this._updateWaiting();
|
|
803
857
|
}
|
|
804
858
|
/** Update a specific segment */
|
|
805
859
|
updateSegment(segmentId, updates) {
|
|
@@ -830,6 +884,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
830
884
|
}
|
|
831
885
|
const el = this._segmentsEl?.querySelector(`[data-segment-id="${cssEscape(segmentId)}"]`);
|
|
832
886
|
el?.remove();
|
|
887
|
+
this._updateWaiting();
|
|
833
888
|
}
|
|
834
889
|
/** Set attachments (chips shown above message content, user role only) */
|
|
835
890
|
setAttachments(attachments) {
|
|
@@ -838,8 +893,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
838
893
|
}
|
|
839
894
|
/**
|
|
840
895
|
* Set token usage + timing for this message (assistant only).
|
|
841
|
-
*
|
|
842
|
-
* the
|
|
896
|
+
*
|
|
897
|
+
* This is the *precondition* for the info ("i") action, not the trigger: the
|
|
898
|
+
* button appears only if the app also declared it wants it —
|
|
899
|
+
* `AparteConfig.setBubbleActions({ info: true })` — because the stats popover it
|
|
900
|
+
* opens (`aparte-message-info`) is the app's, and core has none. Without usage
|
|
901
|
+
* there is nothing to show, so the button never renders either way.
|
|
843
902
|
*/
|
|
844
903
|
setUsage(usage) {
|
|
845
904
|
this._usage = usage ?? null;
|
|
@@ -892,7 +951,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
892
951
|
console.warn(`[AparteChatBubble] _appendSegmentEl ABORT: _segmentsEl is null`);
|
|
893
952
|
return;
|
|
894
953
|
}
|
|
895
|
-
const renderer =
|
|
954
|
+
const renderer = resolveSegmentRenderer(segment.type);
|
|
896
955
|
if (renderer) {
|
|
897
956
|
const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
|
|
898
957
|
if (el) {
|
|
@@ -915,7 +974,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
915
974
|
this._renderSegments();
|
|
916
975
|
return;
|
|
917
976
|
}
|
|
918
|
-
const renderer =
|
|
977
|
+
const renderer = resolveSegmentRenderer(segment.type);
|
|
919
978
|
if (!renderer) return;
|
|
920
979
|
if (renderer.update) {
|
|
921
980
|
runWithConfig(this._cfg, () => renderer.update(el, segment));
|
|
@@ -976,6 +1035,10 @@ class AparteChatBubble extends HTMLElement {
|
|
|
976
1035
|
<div class="aparte-message-content">
|
|
977
1036
|
<div class="aparte-segments"></div>
|
|
978
1037
|
<div class="aparte-content"></div>
|
|
1038
|
+
<div class="aparte-waiting" hidden>
|
|
1039
|
+
<span class="aparte-dots" aria-hidden="true"><span class="aparte-dot"></span><span class="aparte-dot"></span><span class="aparte-dot"></span></span>
|
|
1040
|
+
<span class="aparte-sr-only"></span>
|
|
1041
|
+
</div>
|
|
979
1042
|
</div>
|
|
980
1043
|
<div class="aparte-footer">
|
|
981
1044
|
<div class="aparte-branch-picker" hidden>
|
|
@@ -994,9 +1057,32 @@ class AparteChatBubble extends HTMLElement {
|
|
|
994
1057
|
this._attachmentsEl = this.querySelector(".aparte-attachments");
|
|
995
1058
|
this._actionBarEl = this.querySelector(".aparte-action-bar");
|
|
996
1059
|
this._branchPickerEl = this.querySelector(".aparte-branch-picker");
|
|
1060
|
+
this._footerEl = this.querySelector(".aparte-footer");
|
|
997
1061
|
this._setupBranchPickerListeners();
|
|
998
1062
|
this._updateActionBar();
|
|
999
1063
|
this._renderAvatar();
|
|
1064
|
+
if (this._streaming) this._updateStreaming(true);
|
|
1065
|
+
this._updateWaiting();
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Show the built-in waiting indicator while this bubble is in flight and has
|
|
1069
|
+
* nothing to show yet — the gap between "user sends" and the first token, which
|
|
1070
|
+
* used to be a bubble with a name and an empty body.
|
|
1071
|
+
*
|
|
1072
|
+
* The dots are CSS (no per-token work, themable, honours reduced-motion); the
|
|
1073
|
+
* accessible name is `locale.typing`, next to the `aria-busy` the streaming state
|
|
1074
|
+
* already sets. A custom bubble shell without the region simply has no indicator
|
|
1075
|
+
* (same null-guarded degradation as the other region hooks).
|
|
1076
|
+
*/
|
|
1077
|
+
_updateWaiting() {
|
|
1078
|
+
const el = this.querySelector(".aparte-waiting");
|
|
1079
|
+
if (!el) return;
|
|
1080
|
+
const waiting = this._streaming && this._role !== "user" && this._segments.length === 0 && !this._content.trim();
|
|
1081
|
+
el.hidden = !waiting;
|
|
1082
|
+
if (!waiting) return;
|
|
1083
|
+
const label = this._cfg.getLocale().typing;
|
|
1084
|
+
const sr = el.querySelector(".aparte-sr-only");
|
|
1085
|
+
if (sr && sr.textContent !== label) sr.textContent = label;
|
|
1000
1086
|
}
|
|
1001
1087
|
/**
|
|
1002
1088
|
* Hand the avatar host element off to the registered AvatarProvider, if
|
|
@@ -1047,10 +1133,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1047
1133
|
if (!this._contentEl) return;
|
|
1048
1134
|
if (this._segments.length > 0) {
|
|
1049
1135
|
this._contentEl.style.display = "none";
|
|
1136
|
+
this._updateWaiting();
|
|
1050
1137
|
return;
|
|
1051
1138
|
}
|
|
1052
1139
|
this._contentEl.style.display = "";
|
|
1053
1140
|
this._contentEl.innerHTML = this._cfg.renderMarkdown(this._content);
|
|
1141
|
+
this._updateWaiting();
|
|
1054
1142
|
if (!this._streaming) this._highlightContentCode();
|
|
1055
1143
|
}
|
|
1056
1144
|
/**
|
|
@@ -1084,7 +1172,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1084
1172
|
if (!this._segmentsEl) return;
|
|
1085
1173
|
this._segmentsEl.innerHTML = "";
|
|
1086
1174
|
for (const segment of this._segments) {
|
|
1087
|
-
const renderer =
|
|
1175
|
+
const renderer = resolveSegmentRenderer(segment.type);
|
|
1088
1176
|
if (renderer) {
|
|
1089
1177
|
const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => renderer.render(segment)));
|
|
1090
1178
|
if (el) {
|
|
@@ -1162,8 +1250,11 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1162
1250
|
}
|
|
1163
1251
|
return `<div class="aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${this._escapeHtml(this._fileExt(a2.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
|
|
1164
1252
|
}).join("");
|
|
1253
|
+
if (!this._cfg.getHostHandlers().attachmentPreview) return;
|
|
1165
1254
|
this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
|
|
1166
|
-
tile.
|
|
1255
|
+
tile.setAttribute("role", "button");
|
|
1256
|
+
tile.setAttribute("tabindex", "0");
|
|
1257
|
+
const open = () => {
|
|
1167
1258
|
const img = tile.querySelector(".aparte-thumb__img");
|
|
1168
1259
|
if (!img) return;
|
|
1169
1260
|
this.dispatchEvent(new CustomEvent("aparte-attachment-preview", {
|
|
@@ -1171,6 +1262,13 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1171
1262
|
composed: true,
|
|
1172
1263
|
detail: { url: img.src, name: tile.getAttribute("title") ?? "" }
|
|
1173
1264
|
}));
|
|
1265
|
+
};
|
|
1266
|
+
tile.addEventListener("click", open);
|
|
1267
|
+
tile.addEventListener("keydown", (e2) => {
|
|
1268
|
+
const key = e2.key;
|
|
1269
|
+
if (key !== "Enter" && key !== " ") return;
|
|
1270
|
+
e2.preventDefault();
|
|
1271
|
+
open();
|
|
1174
1272
|
});
|
|
1175
1273
|
});
|
|
1176
1274
|
}
|
|
@@ -1202,9 +1300,11 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1202
1300
|
if (!this._branchPickerEl) return;
|
|
1203
1301
|
if (this._siblingCount <= 1 || this._role !== "assistant") {
|
|
1204
1302
|
this._branchPickerEl.hidden = true;
|
|
1303
|
+
this._syncFooterVisibility();
|
|
1205
1304
|
return;
|
|
1206
1305
|
}
|
|
1207
1306
|
this._branchPickerEl.hidden = false;
|
|
1307
|
+
this._syncFooterVisibility();
|
|
1208
1308
|
const label = this._branchPickerEl.querySelector(".aparte-branch-label");
|
|
1209
1309
|
if (label) {
|
|
1210
1310
|
const customNav = this._cfg.getSiblingNavRenderer?.();
|
|
@@ -1251,12 +1351,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1251
1351
|
buttons.push(this._actionButtonHtml("thumbUp", icons, locale));
|
|
1252
1352
|
buttons.push(this._actionButtonHtml("thumbDown", icons, locale));
|
|
1253
1353
|
}
|
|
1254
|
-
if (this.
|
|
1255
|
-
const infoLabel = locale.messageInfo ?? "Details";
|
|
1256
|
-
buttons.push(`<button class="aparte-action-btn aparte-action-info" data-action="info" aria-label="${infoLabel}" title="${infoLabel}">
|
|
1257
|
-
${INFO_ICON_SVG}
|
|
1258
|
-
</button>`);
|
|
1259
|
-
}
|
|
1354
|
+
if (config.info) buttons.push(this._actionButtonHtml("info", icons, locale));
|
|
1260
1355
|
}
|
|
1261
1356
|
}
|
|
1262
1357
|
this._actionBarEl.innerHTML = buttons.join("");
|
|
@@ -1264,6 +1359,21 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1264
1359
|
this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
|
|
1265
1360
|
btn.addEventListener("click", (e2) => this._handleActionClick(e2));
|
|
1266
1361
|
});
|
|
1362
|
+
this._syncFooterVisibility();
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* An empty action bar is not a bar: with every action off it was still a
|
|
1366
|
+
* `role="toolbar"` with nothing in it (announced as such), and it still reserved
|
|
1367
|
+
* its fixed height plus the footer's under every bubble. So both follow their
|
|
1368
|
+
* contents — the footer stays as long as the branch picker or the bar has
|
|
1369
|
+
* something to show.
|
|
1370
|
+
*/
|
|
1371
|
+
_syncFooterVisibility() {
|
|
1372
|
+
if (this._actionBarEl) this._actionBarEl.hidden = this._actionBarEl.children.length === 0;
|
|
1373
|
+
if (!this._footerEl) return;
|
|
1374
|
+
const barEmpty = !this._actionBarEl || this._actionBarEl.hidden;
|
|
1375
|
+
const pickerHidden = !this._branchPickerEl || this._branchPickerEl.hidden;
|
|
1376
|
+
this._footerEl.hidden = barEmpty && pickerHidden;
|
|
1267
1377
|
}
|
|
1268
1378
|
/** Append the registered custom action buttons for this bubble's role. */
|
|
1269
1379
|
_appendCustomActions(icons) {
|
|
@@ -1304,6 +1414,11 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1304
1414
|
const l2 = locale.feedbackNegative ?? "Bad response";
|
|
1305
1415
|
return `<button class="aparte-action-btn aparte-action-feedback-neg" data-action="feedback-negative" aria-label="${l2}" title="${l2}">${icons.thumbDown()}</button>`;
|
|
1306
1416
|
}
|
|
1417
|
+
case "info": {
|
|
1418
|
+
if (!this._usage) return "";
|
|
1419
|
+
const l2 = locale.messageInfo ?? "Details";
|
|
1420
|
+
return `<button class="aparte-action-btn aparte-action-info" data-action="info" aria-label="${l2}" title="${l2}">${INFO_ICON_SVG}</button>`;
|
|
1421
|
+
}
|
|
1307
1422
|
default:
|
|
1308
1423
|
return "";
|
|
1309
1424
|
}
|
|
@@ -1318,6 +1433,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1318
1433
|
this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
|
|
1319
1434
|
btn.addEventListener("click", (e2) => this._handleActionClick(e2));
|
|
1320
1435
|
});
|
|
1436
|
+
this._syncFooterVisibility();
|
|
1321
1437
|
}
|
|
1322
1438
|
_handleActionClick(e2) {
|
|
1323
1439
|
const btn = e2.currentTarget;
|
|
@@ -1505,6 +1621,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1505
1621
|
message.classList.remove("aparte-message-streaming");
|
|
1506
1622
|
}
|
|
1507
1623
|
}
|
|
1624
|
+
this._updateWaiting();
|
|
1508
1625
|
if (wasStreaming && !streaming) this._highlightContentCode();
|
|
1509
1626
|
}
|
|
1510
1627
|
}
|
|
@@ -1754,9 +1871,13 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1754
1871
|
if (!message.segments) {
|
|
1755
1872
|
message.segments = [];
|
|
1756
1873
|
}
|
|
1757
|
-
const
|
|
1874
|
+
const index = message.segments.findIndex((s2) => s2.id === segmentId);
|
|
1875
|
+
const segment = index === -1 ? void 0 : message.segments[index];
|
|
1758
1876
|
if (segment && "content" in segment) {
|
|
1759
|
-
|
|
1877
|
+
message.segments[index] = {
|
|
1878
|
+
...segment,
|
|
1879
|
+
content: segment.content + chunk
|
|
1880
|
+
};
|
|
1760
1881
|
}
|
|
1761
1882
|
this.dispatchEvent(new CustomEvent("aparte-segment-update", {
|
|
1762
1883
|
bubbles: true,
|
|
@@ -1832,8 +1953,10 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1832
1953
|
this.updateSegment(messageId, segmentId, { isStreaming: false });
|
|
1833
1954
|
}
|
|
1834
1955
|
/**
|
|
1835
|
-
* Persist token usage on a message and propagate to the live bubble
|
|
1836
|
-
*
|
|
1956
|
+
* Persist token usage on a message and propagate to the live bubble, which is
|
|
1957
|
+
* what allows the info ("i") action to render — provided the app declared it
|
|
1958
|
+
* with `AparteConfig.setBubbleActions({ info: true })`; it is off by default,
|
|
1959
|
+
* since the popover it opens belongs to the app.
|
|
1837
1960
|
*/
|
|
1838
1961
|
setUsage(messageId, usage) {
|
|
1839
1962
|
const message = this._repo.getMessageById(messageId);
|
|
@@ -1903,7 +2026,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1903
2026
|
bubble.setAttribute("role", message.role);
|
|
1904
2027
|
if (message.timestamp) bubble.setAttribute("timestamp", String(message.timestamp));
|
|
1905
2028
|
if (message.content) bubble.setAttribute("content", message.content);
|
|
1906
|
-
if (message
|
|
2029
|
+
if (isAwaitingReply(message)) {
|
|
1907
2030
|
bubble.setAttribute("streaming", "");
|
|
1908
2031
|
}
|
|
1909
2032
|
if (this._bottomSpacer && this._bottomSpacer.parentNode === wrapper) {
|
|
@@ -1911,6 +2034,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1911
2034
|
} else {
|
|
1912
2035
|
wrapper.appendChild(bubble);
|
|
1913
2036
|
}
|
|
2037
|
+
populateBubbleFromMessage(bubble, message);
|
|
1914
2038
|
}
|
|
1915
2039
|
}
|
|
1916
2040
|
this._pruneRenderedBubbles();
|
|
@@ -1991,7 +2115,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1991
2115
|
if (currentIdx === -1) return;
|
|
1992
2116
|
const targetIdx = direction === "prev" ? currentIdx - 1 : currentIdx + 1;
|
|
1993
2117
|
if (targetIdx < 0 || targetIdx >= siblings.length) return;
|
|
1994
|
-
this._isAutoScrollEnabled =
|
|
2118
|
+
this._isAutoScrollEnabled = this._isAtBottom();
|
|
1995
2119
|
this._updateScrollButton();
|
|
1996
2120
|
this._repo.switchToBranch(siblings[targetIdx]);
|
|
1997
2121
|
this._reRenderActivePath();
|
|
@@ -2181,11 +2305,14 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2181
2305
|
case "setUsage":
|
|
2182
2306
|
bubble.setUsage?.(payload);
|
|
2183
2307
|
break;
|
|
2184
|
-
case "update":
|
|
2185
|
-
|
|
2308
|
+
case "update": {
|
|
2309
|
+
const updates = payload;
|
|
2310
|
+
const renderable = ["status", "segments", "content", "attachments", "usage"];
|
|
2311
|
+
if (renderable.some((key) => key in updates)) {
|
|
2186
2312
|
bubble.updateMessage?.(payload);
|
|
2187
2313
|
}
|
|
2188
2314
|
break;
|
|
2315
|
+
}
|
|
2189
2316
|
case "complete":
|
|
2190
2317
|
bubble.updateMessage?.(payload);
|
|
2191
2318
|
break;
|
|
@@ -2222,7 +2349,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2222
2349
|
bubble.setAttribute("message-id", message.id);
|
|
2223
2350
|
bubble.setAttribute("role", message.role);
|
|
2224
2351
|
if (message.timestamp) bubble.setAttribute("timestamp", String(message.timestamp));
|
|
2225
|
-
if (message
|
|
2352
|
+
if (isAwaitingReply(message)) {
|
|
2226
2353
|
bubble.setAttribute("streaming", "");
|
|
2227
2354
|
}
|
|
2228
2355
|
wrapper.appendChild(bubble);
|
|
@@ -2230,7 +2357,6 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2230
2357
|
}
|
|
2231
2358
|
this._dispatchPathChanged(activeMessages, siblingsInfo);
|
|
2232
2359
|
this._recalculateSpacer();
|
|
2233
|
-
requestAnimationFrame(() => this._handleScroll());
|
|
2234
2360
|
}
|
|
2235
2361
|
_dispatchPathChanged(messages, siblings) {
|
|
2236
2362
|
const detail = { messages, siblings };
|
|
@@ -2400,11 +2526,15 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2400
2526
|
});
|
|
2401
2527
|
}
|
|
2402
2528
|
}
|
|
2529
|
+
/** Is the scroll surface within `_scrollThreshold` of its bottom, right now? */
|
|
2530
|
+
_isAtBottom() {
|
|
2531
|
+
if (!this._container) return true;
|
|
2532
|
+
const { scrollTop, scrollHeight, clientHeight } = this._container;
|
|
2533
|
+
return scrollHeight - scrollTop - clientHeight <= this._scrollThreshold;
|
|
2534
|
+
}
|
|
2403
2535
|
_handleScroll() {
|
|
2404
2536
|
if (!this._container) return;
|
|
2405
|
-
|
|
2406
|
-
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
2407
|
-
this._isAutoScrollEnabled = distanceFromBottom <= this._scrollThreshold;
|
|
2537
|
+
this._isAutoScrollEnabled = this._isAtBottom();
|
|
2408
2538
|
this._updateScrollButton();
|
|
2409
2539
|
}
|
|
2410
2540
|
_scrollToBottom() {
|
|
@@ -2423,11 +2553,20 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2423
2553
|
return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2424
2554
|
}
|
|
2425
2555
|
/**
|
|
2426
|
-
* Show/hide the scroll-to-bottom button
|
|
2427
|
-
*
|
|
2556
|
+
* Show/hide the scroll-to-bottom button from the **current geometry**, not from
|
|
2557
|
+
* `_isAutoScrollEnabled`.
|
|
2558
|
+
*
|
|
2559
|
+
* The two answer different questions: the flag is intent ("should new content
|
|
2560
|
+
* pull the view down"), the button is a fact ("is there anything below the
|
|
2561
|
+
* fold"). Mirroring the flag made the button lie whenever the two diverged —
|
|
2562
|
+
* `navigateBranch` deliberately disarms auto-follow, so swapping a branch while
|
|
2563
|
+
* already at the bottom of a scrollable transcript left the button offering to
|
|
2564
|
+
* scroll nowhere (reported from bonaparte, React). Re-derived on scroll, on the
|
|
2565
|
+
* post-mutation frame and after a path swap, so it converges to the truth
|
|
2566
|
+
* whatever a framework's render timing does in between.
|
|
2428
2567
|
*/
|
|
2429
2568
|
_updateScrollButton() {
|
|
2430
|
-
this._scrollBtn?.classList.toggle("aparte-scroll-btn--hidden", this.
|
|
2569
|
+
this._scrollBtn?.classList.toggle("aparte-scroll-btn--hidden", this._isAtBottom());
|
|
2431
2570
|
}
|
|
2432
2571
|
/**
|
|
2433
2572
|
* Recalculate the bottom spacer height so the last user message is always
|
|
@@ -2486,6 +2625,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2486
2625
|
this._spacerRafId = requestAnimationFrame(() => {
|
|
2487
2626
|
this._spacerRafId = null;
|
|
2488
2627
|
this._recalculateSpacer();
|
|
2628
|
+
this._updateScrollButton();
|
|
2489
2629
|
});
|
|
2490
2630
|
}
|
|
2491
2631
|
/**
|
|
@@ -2993,9 +3133,9 @@ class AparteComposerInput extends HTMLElement {
|
|
|
2993
3133
|
}
|
|
2994
3134
|
_updatePlaceholder() {
|
|
2995
3135
|
if (this._editor) {
|
|
2996
|
-
const
|
|
2997
|
-
this._editor.setAttribute("data-placeholder",
|
|
2998
|
-
this._editor.setAttribute("aria-label",
|
|
3136
|
+
const p2 = this._getPlaceholder();
|
|
3137
|
+
this._editor.setAttribute("data-placeholder", p2);
|
|
3138
|
+
this._editor.setAttribute("aria-label", p2);
|
|
2999
3139
|
}
|
|
3000
3140
|
}
|
|
3001
3141
|
_updatePlaceholderVisibility() {
|
|
@@ -3226,8 +3366,11 @@ class AparteComposerAttachments extends HTMLElement {
|
|
|
3226
3366
|
if (root) root.removeAttachment(root.attachments[i2]);
|
|
3227
3367
|
});
|
|
3228
3368
|
});
|
|
3369
|
+
if (!resolveConfig(this).getHostHandlers().attachmentPreview) return;
|
|
3229
3370
|
this.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
|
|
3230
|
-
tile.
|
|
3371
|
+
tile.setAttribute("role", "button");
|
|
3372
|
+
tile.setAttribute("tabindex", "0");
|
|
3373
|
+
const open = () => {
|
|
3231
3374
|
const img = tile.querySelector(".aparte-thumb__img");
|
|
3232
3375
|
if (!img) return;
|
|
3233
3376
|
this.dispatchEvent(new CustomEvent("aparte-attachment-preview", {
|
|
@@ -3235,6 +3378,13 @@ class AparteComposerAttachments extends HTMLElement {
|
|
|
3235
3378
|
composed: true,
|
|
3236
3379
|
detail: { url: img.src, name: tile.getAttribute("title") ?? "" }
|
|
3237
3380
|
}));
|
|
3381
|
+
};
|
|
3382
|
+
tile.addEventListener("click", open);
|
|
3383
|
+
tile.addEventListener("keydown", (e2) => {
|
|
3384
|
+
const key = e2.key;
|
|
3385
|
+
if (key !== "Enter" && key !== " ") return;
|
|
3386
|
+
e2.preventDefault();
|
|
3387
|
+
open();
|
|
3238
3388
|
});
|
|
3239
3389
|
});
|
|
3240
3390
|
}
|
|
@@ -3729,36 +3879,40 @@ export {
|
|
|
3729
3879
|
i as AparteStreamParser,
|
|
3730
3880
|
B as BackendTransport,
|
|
3731
3881
|
C as ConversationManager,
|
|
3732
|
-
D as
|
|
3733
|
-
j as
|
|
3734
|
-
k as
|
|
3735
|
-
l as
|
|
3736
|
-
m as
|
|
3882
|
+
D as DEFAULT_BUBBLE_ACTIONS,
|
|
3883
|
+
j as DEFAULT_HOST_HANDLERS,
|
|
3884
|
+
k as DEFAULT_ICON_FALLBACKS,
|
|
3885
|
+
l as DEFAULT_LOCALE,
|
|
3886
|
+
m as DEFAULT_SKELETON_FALLBACKS,
|
|
3887
|
+
n as DEFAULT_UI_EVENTS,
|
|
3888
|
+
o as DirectTransport,
|
|
3737
3889
|
MessageRepository,
|
|
3738
|
-
|
|
3739
|
-
|
|
3890
|
+
p as applyElementProps,
|
|
3891
|
+
q as attachConfig,
|
|
3740
3892
|
buildElicitationPanel,
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3893
|
+
s as collectRendererStyles,
|
|
3894
|
+
t as contentToText,
|
|
3895
|
+
u as contextConfig,
|
|
3896
|
+
v as createAparteChatHandler,
|
|
3897
|
+
w as createStreamAdapter,
|
|
3898
|
+
x as defaultSanitizer,
|
|
3899
|
+
y as deriveArtifactKind,
|
|
3900
|
+
z as detachConfig,
|
|
3901
|
+
E as filesToAttachments,
|
|
3749
3902
|
getSegmentRenderer,
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3903
|
+
isAwaitingReply,
|
|
3904
|
+
H as isFormatAdapter,
|
|
3905
|
+
I as isSafeUrl,
|
|
3906
|
+
J as parseAparteEventStream,
|
|
3907
|
+
K as parseMarkdownToSegments,
|
|
3754
3908
|
populateBubbleFromMessage,
|
|
3755
|
-
|
|
3909
|
+
N as readableToAsyncIterable,
|
|
3756
3910
|
registerAllComponents,
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3911
|
+
O as registerDefaultRenderers,
|
|
3912
|
+
P as registerSegmentRenderer,
|
|
3913
|
+
Q as requestUserInput,
|
|
3760
3914
|
resolveConfig,
|
|
3761
3915
|
runWithConfig,
|
|
3762
|
-
|
|
3916
|
+
T as unregisterSegmentRenderer
|
|
3763
3917
|
};
|
|
3764
3918
|
//# sourceMappingURL=index.js.map
|