@aparte/core 0.9.0 → 0.11.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/dist/client/aparte-client.d.ts +54 -33
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/stream-adapter.d.ts +6 -1
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +49 -7
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/chat/aparte-chat.d.ts +8 -4
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-action.d.ts +5 -5
- package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-add-attachment.d.ts +16 -4
- package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts +5 -3
- package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-cancel.d.ts +10 -2
- package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-input.d.ts +8 -5
- package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-send.d.ts +34 -2
- package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-toolbar.d.ts +3 -3
- package/dist/components/composer/aparte-composer.d.ts +54 -10
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +19 -12
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/elicitation/aparte-elicitation.d.ts +22 -0
- package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
- package/dist/components/status/aparte-chat-status.d.ts +9 -0
- package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +18 -5
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +54 -2
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/config-subscribe.d.ts +4 -0
- package/dist/config/config-subscribe.d.ts.map +1 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/locale.d.ts +141 -0
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/config/sanitize.d.ts.map +1 -1
- package/dist/custom-elements.json +5905 -4694
- package/dist/elicitation/approval-panel.d.ts +42 -0
- package/dist/elicitation/approval-panel.d.ts.map +1 -0
- package/dist/elicitation/index.d.ts +4 -1
- package/dist/elicitation/index.d.ts.map +1 -1
- package/dist/elicitation/panel.d.ts +14 -1
- package/dist/elicitation/panel.d.ts.map +1 -1
- package/dist/elicitation/types.d.ts +116 -9
- package/dist/elicitation/types.d.ts.map +1 -1
- package/dist/generated/element-attributes.d.ts +173 -0
- package/dist/generated/element-attributes.d.ts.map +1 -0
- package/dist/host/aparte-chat-host.d.ts +3 -1
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/index.css +752 -392
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +538 -290
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +5 -4
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +62 -57
- package/dist/index.node.js.map +1 -1
- package/dist/interop/element-props.d.ts +26 -1
- package/dist/interop/element-props.d.ts.map +1 -1
- package/dist/{index-hQQljwfX.js → is-awaiting-reply-Cb7H3YfN.js} +1159 -972
- package/dist/is-awaiting-reply-Cb7H3YfN.js.map +1 -0
- package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
- package/dist/primitives/select/aparte-optgroup.d.ts +3 -0
- package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
- package/dist/primitives/select/aparte-option.d.ts +1 -0
- package/dist/primitives/select/aparte-option.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts +22 -18
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/renderers/highlight-stream.d.ts +17 -0
- package/dist/renderers/highlight-stream.d.ts.map +1 -0
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/renderers/segments/artifact/binary-file.d.ts.map +1 -1
- package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
- package/dist/renderers/segments/artifact/shared.d.ts +19 -2
- package/dist/renderers/segments/artifact/shared.d.ts.map +1 -1
- package/dist/renderers/segments/code.d.ts.map +1 -1
- package/dist/renderers/segments/error.d.ts.map +1 -1
- package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -1
- package/dist/renderers/segments/thinking.d.ts.map +1 -1
- package/dist/renderers/segments/tool-call.d.ts.map +1 -1
- package/dist/types/element-attributes.d.ts +42 -0
- package/dist/types/element-attributes.d.ts.map +1 -0
- package/dist/types/event-map.d.ts +2 -4
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/events.d.ts +0 -22
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/models.d.ts +1 -6
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +87 -74
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/tools.d.ts +15 -21
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/segments.d.ts +83 -5
- package/dist/utils/segments.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/index-hQQljwfX.js.map +0 -1
- package/dist/renderers/segments/file-tree.d.ts +0 -3
- package/dist/renderers/segments/file-tree.d.ts.map +0 -1
- package/dist/renderers/segments/progress.d.ts +0 -3
- package/dist/renderers/segments/progress.d.ts.map +0 -1
- package/dist/renderers/segments/terminal.d.ts +0 -3
- package/dist/renderers/segments/terminal.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o,
|
|
1
|
+
import { a4 as subscribeConfigChange, $ as resolveConfig, a1 as runWithConfig, p as AparteElicitationAbortError, x as buildApprovalPanel, y as buildElicitationPanel, K as escapeAttr, F as cssEscape, a7 as mergeSegmentUpdate, L as escapeHtml, O as getSegmentRenderer, P as installDefaultRenderersOnce, s as AparteMessageRepository, a8 as stampSegmentActivity, a9 as stampSegmentOnInsert, aa as stampSegmentOnUpdate, ab as renumberSegments, ac as openSegmentIds, ad as isTerminalStatus, ae as adoptMessageSegments, Q as isAwaitingReply, W as populateBubbleFromMessage, a6 as uuid, a0 as revokeAttachmentUrls } from "./is-awaiting-reply-Cb7H3YfN.js";
|
|
2
|
+
import { A, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, q, r, t, u, v, w, z, B, C, D, E, G, H, I, J, M, N, R, S, T, U, V, X, Y, Z, _, a2, a3, a5 } from "./is-awaiting-reply-Cb7H3YfN.js";
|
|
3
3
|
class AparteOption extends HTMLElement {
|
|
4
4
|
static get observedAttributes() {
|
|
5
5
|
return ["value", "disabled", "selected", "data-status"];
|
|
@@ -631,6 +631,222 @@ class AparteProgressSpinner extends HTMLElement {
|
|
|
631
631
|
if (!customElements.get("aparte-progress-spinner")) {
|
|
632
632
|
customElements.define("aparte-progress-spinner", AparteProgressSpinner);
|
|
633
633
|
}
|
|
634
|
+
class AparteElicitation extends HTMLElement {
|
|
635
|
+
_pending = null;
|
|
636
|
+
/**
|
|
637
|
+
* A turn ended — cancel the open question only if it was OUR turn.
|
|
638
|
+
*
|
|
639
|
+
* These two listeners sit on `window` and had no instance filter at all, so a
|
|
640
|
+
* Stop (or an error) in one chat cancelled the question a DIFFERENT chat was
|
|
641
|
+
* waiting on — and that chat's model was told the user had refused a question
|
|
642
|
+
* the user was still looking at. Same defect as the `compact()` handler that
|
|
643
|
+
* emptied both chats, which its four sibling handlers in `AparteClient` already
|
|
644
|
+
* guarded against.
|
|
645
|
+
*
|
|
646
|
+
* The leniency rule is the composer's, deliberately: an event with no
|
|
647
|
+
* `targetId` is for everyone (a single-chat app never sets one), and a chat we
|
|
648
|
+
* cannot identify accepts everything rather than becoming deaf. Only a
|
|
649
|
+
* MISMATCH is ignored.
|
|
650
|
+
*/
|
|
651
|
+
_onTurnEnd = (e2) => {
|
|
652
|
+
const evtTargetId = e2.detail?.targetId;
|
|
653
|
+
const own = this._pendingTargetId();
|
|
654
|
+
if (evtTargetId && own && evtTargetId !== own) return;
|
|
655
|
+
this._cancelPending();
|
|
656
|
+
};
|
|
657
|
+
_unsubscribeConfig = null;
|
|
658
|
+
connectedCallback() {
|
|
659
|
+
this.style.display = "none";
|
|
660
|
+
this._unsubscribeConfig = subscribeConfigChange(this, () => this._relabelPending());
|
|
661
|
+
resolveConfig(this).setElicitationPresenter(this._present);
|
|
662
|
+
window.addEventListener("aparte-message-aborted", this._onTurnEnd);
|
|
663
|
+
window.addEventListener("aparte-message-error", this._onTurnEnd);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* The boundary above us appeared, changed, or went away — move the
|
|
667
|
+
* registration with it.
|
|
668
|
+
*
|
|
669
|
+
* `connectedCallback` alone is not enough and cannot be: registering is a
|
|
670
|
+
* WRITE, and under all four wrappers it happens before `attachConfig` runs, so
|
|
671
|
+
* it lands on the global singleton. `requestUserInput()` then resolves the
|
|
672
|
+
* instance config, finds nothing, and answers the model `cancel` — the model
|
|
673
|
+
* hears the user refuse a question the user never saw.
|
|
674
|
+
*
|
|
675
|
+
* See {@link AparteConfigAware}.
|
|
676
|
+
*/
|
|
677
|
+
aparteConfigChanged(next, previous) {
|
|
678
|
+
if (previous.getElicitationPresenter() === this._present) previous.setElicitationPresenter(null);
|
|
679
|
+
next.setElicitationPresenter(this._present);
|
|
680
|
+
}
|
|
681
|
+
disconnectedCallback() {
|
|
682
|
+
const cfg = resolveConfig(this);
|
|
683
|
+
if (cfg.getElicitationPresenter() === this._present) cfg.setElicitationPresenter(null);
|
|
684
|
+
window.removeEventListener("aparte-message-aborted", this._onTurnEnd);
|
|
685
|
+
window.removeEventListener("aparte-message-error", this._onTurnEnd);
|
|
686
|
+
this._unsubscribeConfig?.();
|
|
687
|
+
this._unsubscribeConfig = null;
|
|
688
|
+
this._cancelPending();
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Re-apply the open question's strings, in place.
|
|
692
|
+
*
|
|
693
|
+
* Two owners, and both have to move or the panel goes bilingual: the panel's own
|
|
694
|
+
* defaults (`relabel`), and the Skip button, which this file builds and this file
|
|
695
|
+
* therefore has to re-text. The composer's one button is a third, and it already
|
|
696
|
+
* follows — `aparte-composer-send` remembers the panel state it was given.
|
|
697
|
+
*/
|
|
698
|
+
_relabelPending() {
|
|
699
|
+
if (!this._pending) return;
|
|
700
|
+
const cfg = resolveConfig(this);
|
|
701
|
+
runWithConfig(cfg, () => this._pending.relabel());
|
|
702
|
+
}
|
|
703
|
+
_present = (request) => {
|
|
704
|
+
const composer = this._getComposer();
|
|
705
|
+
if (!composer) return Promise.reject(new AparteElicitationAbortError("no-presenter"));
|
|
706
|
+
return new Promise((resolve, reject) => {
|
|
707
|
+
let done = false;
|
|
708
|
+
const slot = {};
|
|
709
|
+
const close = () => {
|
|
710
|
+
if (done) return false;
|
|
711
|
+
done = true;
|
|
712
|
+
this._pending = null;
|
|
713
|
+
composer.hidePanel(slot.token);
|
|
714
|
+
return true;
|
|
715
|
+
};
|
|
716
|
+
const settle = (result) => {
|
|
717
|
+
if (close()) resolve(result);
|
|
718
|
+
};
|
|
719
|
+
const fail = (reason = "aborted") => {
|
|
720
|
+
if (close()) reject(new AparteElicitationAbortError(reason));
|
|
721
|
+
};
|
|
722
|
+
if (request.signal) {
|
|
723
|
+
if (request.signal.aborted) {
|
|
724
|
+
fail();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
request.signal.addEventListener("abort", () => fail(), { once: true });
|
|
728
|
+
}
|
|
729
|
+
const cfg = resolveConfig(this);
|
|
730
|
+
if (request.kind === "approval") {
|
|
731
|
+
const panel2 = runWithConfig(cfg, () => buildApprovalPanel(request.message, request.options ?? [], () => {
|
|
732
|
+
composer.setPanelSubmitEnabled(panel2.isComplete(), "submit");
|
|
733
|
+
}));
|
|
734
|
+
panel2.onSettle((answer) => settle({ action: "accept", content: answer }));
|
|
735
|
+
this._pending = { abort: () => fail(), composer, relabel: () => panel2.relabel() };
|
|
736
|
+
slot.token = composer.showPanel(panel2.el, {
|
|
737
|
+
submitEnabled: panel2.isComplete(),
|
|
738
|
+
mode: "submit",
|
|
739
|
+
// The composer's button carries the INSTRUCTION, which is written
|
|
740
|
+
// text — exactly the act that button already means. The options
|
|
741
|
+
// never route through it: a decision is its own click.
|
|
742
|
+
onSubmit: () => {
|
|
743
|
+
if (panel2.isComplete()) settle({ action: "accept", content: panel2.getContent() });
|
|
744
|
+
},
|
|
745
|
+
onEvict: () => fail()
|
|
746
|
+
});
|
|
747
|
+
panel2.focus();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const schema = request.schema ?? { type: "string" };
|
|
751
|
+
const panel = runWithConfig(cfg, () => buildElicitationPanel(request.message, schema, () => {
|
|
752
|
+
composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
|
|
753
|
+
}));
|
|
754
|
+
const skip = document.createElement("button");
|
|
755
|
+
skip.type = "button";
|
|
756
|
+
skip.className = "aparte-elic-skip";
|
|
757
|
+
skip.textContent = cfg.t("elicitationSkip");
|
|
758
|
+
skip.addEventListener("click", () => settle({ action: "decline" }));
|
|
759
|
+
panel.dismiss.appendChild(skip);
|
|
760
|
+
this._pending = {
|
|
761
|
+
abort: () => fail(),
|
|
762
|
+
composer,
|
|
763
|
+
relabel: () => {
|
|
764
|
+
panel.relabel();
|
|
765
|
+
skip.textContent = resolveConfig(this).t("elicitationSkip");
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
slot.token = composer.showPanel(panel.el, {
|
|
769
|
+
submitEnabled: panel.canProceed(),
|
|
770
|
+
mode: panel.mode(),
|
|
771
|
+
/*
|
|
772
|
+
* Something else took the slot — another request, a conversation switch,
|
|
773
|
+
* or a turn ending. The composer tears the panel down either way; only
|
|
774
|
+
* this callback can settle the promise, and without it the request hung
|
|
775
|
+
* AND `_pending` stayed set, so every later question was short-circuited
|
|
776
|
+
* for the life of the page. `cancel`, not `decline`: nobody declined
|
|
777
|
+
* anything, the question was taken away.
|
|
778
|
+
*/
|
|
779
|
+
onEvict: () => fail(),
|
|
780
|
+
onSubmit: () => {
|
|
781
|
+
if (panel.mode() === "advance") {
|
|
782
|
+
panel.proceed();
|
|
783
|
+
composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (panel.isComplete()) settle({ action: "accept", content: panel.getContent() });
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
panel.focus();
|
|
790
|
+
});
|
|
791
|
+
};
|
|
792
|
+
_cancelPending() {
|
|
793
|
+
this._pending?.abort();
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* The host id of the chat whose composer holds the open panel.
|
|
797
|
+
*
|
|
798
|
+
* Walks up from the composer rather than from `this`, because the panel lives in
|
|
799
|
+
* the composer and that is what the turn belongs to. Matches the hosts
|
|
800
|
+
* `aparte-chat-bubble._resolveTargetId()` matches, for the reason written there:
|
|
801
|
+
* Angular's wrapper root IS the `<aparte-chat>` element, while the plain-root
|
|
802
|
+
* wrappers render a `[data-aparte-chat]` div instead, so matching only the tag
|
|
803
|
+
* resolves `undefined` on three wrappers out of four.
|
|
804
|
+
*/
|
|
805
|
+
_pendingTargetId() {
|
|
806
|
+
let el = this._pending?.composer ?? null;
|
|
807
|
+
while (el) {
|
|
808
|
+
const tag = el.tagName?.toLowerCase();
|
|
809
|
+
const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
|
|
810
|
+
if (isHost && el.id) return el.id;
|
|
811
|
+
el = el.parentElement;
|
|
812
|
+
}
|
|
813
|
+
return void 0;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* The composer to present in: the nearest one in an ancestor subtree, and
|
|
817
|
+
* nothing else.
|
|
818
|
+
*
|
|
819
|
+
* There used to be a `document.querySelector('aparte-composer')` fallback, which
|
|
820
|
+
* is the "first chat on the page" bug this repo has now fixed in four other
|
|
821
|
+
* places: on a page with two chats, an elicitation that could not find its own
|
|
822
|
+
* composer mounted its panel in the OTHER chat's — so one conversation's question
|
|
823
|
+
* appeared under the other conversation, and answering it resolved a tool call
|
|
824
|
+
* belonging to a chat the user was not looking at.
|
|
825
|
+
*
|
|
826
|
+
* Returning `null` instead resolves `cancel`, which is honest: nothing was
|
|
827
|
+
* shown, so nothing was answered. The warning names the fix, because this is a
|
|
828
|
+
* setup mistake and only the developer can correct it — the guide's own example
|
|
829
|
+
* puts `<aparte-elicitation>` inside `<aparte-chat>`.
|
|
830
|
+
*/
|
|
831
|
+
_getComposer() {
|
|
832
|
+
let node = this.parentElement;
|
|
833
|
+
while (node) {
|
|
834
|
+
const composer = node.querySelector("aparte-composer");
|
|
835
|
+
if (composer && typeof composer.showPanel === "function") return composer;
|
|
836
|
+
const tag = node.tagName?.toLowerCase();
|
|
837
|
+
const isChatBoundary = tag === "aparte-chat" || tag === "aparte-chat-component" || node.hasAttribute?.("data-aparte-chat");
|
|
838
|
+
if (isChatBoundary) break;
|
|
839
|
+
node = node.parentElement;
|
|
840
|
+
}
|
|
841
|
+
console.warn(
|
|
842
|
+
"[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."
|
|
843
|
+
);
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (typeof customElements !== "undefined" && !customElements.get("aparte-elicitation")) {
|
|
848
|
+
customElements.define("aparte-elicitation", AparteElicitation);
|
|
849
|
+
}
|
|
634
850
|
class AparteChat extends HTMLElement {
|
|
635
851
|
static get observedAttributes() {
|
|
636
852
|
return ["placeholder", "disabled", "center-empty", "attachments"];
|
|
@@ -683,6 +899,7 @@ class AparteChat extends HTMLElement {
|
|
|
683
899
|
const attachments = this.hasAttribute("attachments");
|
|
684
900
|
this.innerHTML = `
|
|
685
901
|
<aparte-chat-viewport></aparte-chat-viewport>
|
|
902
|
+
<aparte-elicitation></aparte-elicitation>
|
|
686
903
|
<aparte-composer${composerAttrs}>
|
|
687
904
|
<div class="aparte-composer-shell">
|
|
688
905
|
${attachments ? "<aparte-composer-attachments></aparte-composer-attachments>" : ""}
|
|
@@ -804,7 +1021,38 @@ class AparteChatBubble extends HTMLElement {
|
|
|
804
1021
|
this._updateName();
|
|
805
1022
|
this._updateLocalizedLabels();
|
|
806
1023
|
this._updateWaiting();
|
|
1024
|
+
this._renderAvatar();
|
|
1025
|
+
this._relabelSegments();
|
|
1026
|
+
this._updateTimestamp(this.getAttribute("timestamp"));
|
|
807
1027
|
};
|
|
1028
|
+
/**
|
|
1029
|
+
* Ask every rendered segment to re-read its config-derived text.
|
|
1030
|
+
*
|
|
1031
|
+
* Not `_renderSegments()`, which wipes the container and rebuilds: that destroys a
|
|
1032
|
+
* mounted artifact preview, reverts a reasoning block the reader expanded by
|
|
1033
|
+
* clicking `<summary>` (the DOM's real state is never written back to `collapsed`),
|
|
1034
|
+
* resets scroll inside long terminal panes, drops focus from an Approve/Reject
|
|
1035
|
+
* gate, and throws away the incremental Markdown parser's buffered lookahead
|
|
1036
|
+
* mid-stream — for a change that added no content. It also fires container-wide
|
|
1037
|
+
* childList mutations, which is what the viewport's observer reads as "scroll to
|
|
1038
|
+
* the bottom".
|
|
1039
|
+
*
|
|
1040
|
+
* `relabel` is the narrow alternative, bound by the same no-child-node rule as
|
|
1041
|
+
* `update()`. A renderer that has no config-derived text does not implement it,
|
|
1042
|
+
* and this loop simply skips it.
|
|
1043
|
+
*/
|
|
1044
|
+
_relabelSegments() {
|
|
1045
|
+
if (!this._segmentsEl) return;
|
|
1046
|
+
for (const segment of this._segments) {
|
|
1047
|
+
const renderer = resolveSegmentRenderer(segment.type, this._cfg);
|
|
1048
|
+
if (!renderer?.relabel) continue;
|
|
1049
|
+
const el = this._segmentsEl.querySelector(
|
|
1050
|
+
`:scope > [data-segment-id="${cssEscape(segment.id)}"]`
|
|
1051
|
+
);
|
|
1052
|
+
if (!el) continue;
|
|
1053
|
+
runWithConfig(this._cfg, () => renderer.relabel(el, segment));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
808
1056
|
/**
|
|
809
1057
|
* Re-apply the locale strings written straight into the markup by `_render()` —
|
|
810
1058
|
* the accessible names a screen reader reads, which nothing else refreshes.
|
|
@@ -892,7 +1140,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
892
1140
|
}
|
|
893
1141
|
/** Set segments for rich content */
|
|
894
1142
|
setSegments(segments) {
|
|
895
|
-
this._segments = segments;
|
|
1143
|
+
this._segments = [...segments];
|
|
896
1144
|
this._renderSegments();
|
|
897
1145
|
this._updateWaiting();
|
|
898
1146
|
}
|
|
@@ -904,16 +1152,16 @@ class AparteChatBubble extends HTMLElement {
|
|
|
904
1152
|
}
|
|
905
1153
|
/** Update a specific segment */
|
|
906
1154
|
updateSegment(segmentId, updates) {
|
|
907
|
-
const index = this._segments.findIndex((
|
|
1155
|
+
const index = this._segments.findIndex((s) => s.id === segmentId);
|
|
908
1156
|
if (index !== -1) {
|
|
909
|
-
const updated =
|
|
1157
|
+
const updated = mergeSegmentUpdate(this._segments[index], updates);
|
|
910
1158
|
this._segments[index] = updated;
|
|
911
1159
|
this._applySegmentUpdate(segmentId, updated, updates);
|
|
912
1160
|
}
|
|
913
1161
|
}
|
|
914
1162
|
/** Append content to a segment */
|
|
915
1163
|
appendToSegment(segmentId, content) {
|
|
916
|
-
const segment = this._segments.find((
|
|
1164
|
+
const segment = this._segments.find((s) => s.id === segmentId);
|
|
917
1165
|
if (segment && "content" in segment) {
|
|
918
1166
|
segment.content += content;
|
|
919
1167
|
this._applySegmentUpdate(segmentId, segment, { content: segment.content });
|
|
@@ -939,7 +1187,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
939
1187
|
* the human-in-the-loop control.
|
|
940
1188
|
*/
|
|
941
1189
|
removeSegment(segmentId) {
|
|
942
|
-
const index = this._segments.findIndex((
|
|
1190
|
+
const index = this._segments.findIndex((s) => s.id === segmentId);
|
|
943
1191
|
if (index !== -1) {
|
|
944
1192
|
this._segments.splice(index, 1);
|
|
945
1193
|
}
|
|
@@ -1022,7 +1270,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1022
1270
|
} else {
|
|
1023
1271
|
warnMissingRenderer(segment.type);
|
|
1024
1272
|
const fallback = document.createElement("div");
|
|
1025
|
-
fallback.className = "segment segment-unknown";
|
|
1273
|
+
fallback.className = "segment aparte-segment-unknown";
|
|
1026
1274
|
fallback.textContent = `[Unknown segment type: ${segment.type}]`;
|
|
1027
1275
|
this._segmentsEl.appendChild(fallback);
|
|
1028
1276
|
}
|
|
@@ -1135,9 +1383,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1135
1383
|
* (same null-guarded degradation as the other region hooks).
|
|
1136
1384
|
*/
|
|
1137
1385
|
_updateWaiting() {
|
|
1386
|
+
const empty = this._segments.length === 0 && !this._content.trim();
|
|
1387
|
+
const waiting = this._streaming && this._role !== "user" && empty;
|
|
1388
|
+
const box = this.querySelector(".aparte-message-content");
|
|
1389
|
+
if (box) box.hidden = empty && !waiting;
|
|
1138
1390
|
const el = this.querySelector(".aparte-waiting");
|
|
1139
1391
|
if (!el) return;
|
|
1140
|
-
const waiting = this._streaming && this._role !== "user" && this._segments.length === 0 && !this._content.trim();
|
|
1141
1392
|
el.hidden = !waiting;
|
|
1142
1393
|
if (!waiting) return;
|
|
1143
1394
|
const label = this._cfg.getLocale().typing;
|
|
@@ -1145,9 +1396,13 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1145
1396
|
if (sr && sr.textContent !== label) sr.textContent = label;
|
|
1146
1397
|
}
|
|
1147
1398
|
/**
|
|
1148
|
-
* Hand the avatar host element off to the registered AvatarProvider, if
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1399
|
+
* Hand the avatar host element off to the registered AvatarProvider, if any.
|
|
1400
|
+
*
|
|
1401
|
+
* With no provider the slot is left exactly as the shell rendered it — which for
|
|
1402
|
+
* the default shell means EMPTY, and hidden by `.aparte-avatar:empty`. This used
|
|
1403
|
+
* to claim it "falls back to the default initial rendered by `_render()`"; there
|
|
1404
|
+
* is no such initial, and believing there was is what made `_updateRole` write
|
|
1405
|
+
* one.
|
|
1151
1406
|
*/
|
|
1152
1407
|
_renderAvatar() {
|
|
1153
1408
|
const avatar = this.querySelector(".aparte-avatar");
|
|
@@ -1175,7 +1430,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1175
1430
|
}
|
|
1176
1431
|
if (avatar) {
|
|
1177
1432
|
avatar.setAttribute("data-role", this._role);
|
|
1178
|
-
avatar.textContent = this._getAvatarInitial();
|
|
1433
|
+
if (avatar.textContent) avatar.textContent = this._getAvatarInitial();
|
|
1179
1434
|
}
|
|
1180
1435
|
if (nameEl) {
|
|
1181
1436
|
nameEl.textContent = this._getDisplayName();
|
|
@@ -1186,7 +1441,9 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1186
1441
|
_updateName() {
|
|
1187
1442
|
const avatar = this.querySelector(".aparte-avatar");
|
|
1188
1443
|
const nameEl = this.querySelector(".aparte-name");
|
|
1189
|
-
if (avatar && !this._cfg.getAvatarProvider())
|
|
1444
|
+
if (avatar && avatar.textContent && !this._cfg.getAvatarProvider()) {
|
|
1445
|
+
avatar.textContent = this._getAvatarInitial();
|
|
1446
|
+
}
|
|
1190
1447
|
if (nameEl) nameEl.textContent = this._getDisplayName();
|
|
1191
1448
|
}
|
|
1192
1449
|
_updateContent() {
|
|
@@ -1242,7 +1499,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1242
1499
|
} else {
|
|
1243
1500
|
warnMissingRenderer(segment.type);
|
|
1244
1501
|
const fallback = document.createElement("div");
|
|
1245
|
-
fallback.className = "segment segment-unknown";
|
|
1502
|
+
fallback.className = "segment aparte-segment-unknown";
|
|
1246
1503
|
fallback.textContent = `[Unknown segment type: ${segment.type}]`;
|
|
1247
1504
|
this._segmentsEl.appendChild(fallback);
|
|
1248
1505
|
}
|
|
@@ -1262,7 +1519,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1262
1519
|
_reflectError() {
|
|
1263
1520
|
const message = this.querySelector(".aparte-message");
|
|
1264
1521
|
if (!message) return;
|
|
1265
|
-
const hasError = this._segments.some((
|
|
1522
|
+
const hasError = this._segments.some((s) => s.type === "error");
|
|
1266
1523
|
if (hasError) message.setAttribute("data-error", "");
|
|
1267
1524
|
else message.removeAttribute("data-error");
|
|
1268
1525
|
}
|
|
@@ -1271,7 +1528,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1271
1528
|
if (!timestampEl || !value) return;
|
|
1272
1529
|
try {
|
|
1273
1530
|
const date = new Date(isNaN(Number(value)) ? value : Number(value));
|
|
1274
|
-
timestampEl.textContent = date.toLocaleTimeString(void 0, {
|
|
1531
|
+
timestampEl.textContent = date.toLocaleTimeString(this._cfg.getLocale().tag || void 0, {
|
|
1275
1532
|
hour: "2-digit",
|
|
1276
1533
|
minute: "2-digit"
|
|
1277
1534
|
});
|
|
@@ -1297,18 +1554,18 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1297
1554
|
const customAttachment = this._cfg.getAttachmentRenderer?.();
|
|
1298
1555
|
if (customAttachment) {
|
|
1299
1556
|
this._attachmentsEl.replaceChildren();
|
|
1300
|
-
for (const
|
|
1301
|
-
const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(
|
|
1557
|
+
for (const a4 of this._attachments) {
|
|
1558
|
+
const el = segmentRenderResultToElement(runWithConfig(this._cfg, () => customAttachment(a4)));
|
|
1302
1559
|
if (el) this._attachmentsEl.appendChild(el);
|
|
1303
1560
|
}
|
|
1304
1561
|
return;
|
|
1305
1562
|
}
|
|
1306
|
-
this._attachmentsEl.innerHTML = this._attachments.map((
|
|
1307
|
-
const name = escapeHtml(
|
|
1308
|
-
if (
|
|
1309
|
-
return `<div class="aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${escapeHtml(
|
|
1563
|
+
this._attachmentsEl.innerHTML = this._attachments.map((a4) => {
|
|
1564
|
+
const name = escapeHtml(a4.name);
|
|
1565
|
+
if (a4.type.startsWith("image/")) {
|
|
1566
|
+
return `<div class="aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${escapeHtml(a4.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
|
|
1310
1567
|
}
|
|
1311
|
-
return `<div class="aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${escapeHtml(this._fileExt(
|
|
1568
|
+
return `<div class="aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${escapeHtml(this._fileExt(a4.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
|
|
1312
1569
|
}).join("");
|
|
1313
1570
|
if (!this._cfg.getHostHandlers().attachmentPreview) return;
|
|
1314
1571
|
this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
|
|
@@ -1413,14 +1670,14 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1413
1670
|
const buttons = [];
|
|
1414
1671
|
if (this._role === "user") {
|
|
1415
1672
|
if (config.user) {
|
|
1416
|
-
for (const
|
|
1673
|
+
for (const a4 of config.user) buttons.push(this._actionButtonHtml(a4, icons, locale));
|
|
1417
1674
|
} else {
|
|
1418
1675
|
if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
|
|
1419
1676
|
if (config.edit) buttons.push(this._actionButtonHtml("edit", icons, locale));
|
|
1420
1677
|
}
|
|
1421
1678
|
} else if (this._role === "assistant") {
|
|
1422
1679
|
if (config.assistant) {
|
|
1423
|
-
for (const
|
|
1680
|
+
for (const a4 of config.assistant) buttons.push(this._actionButtonHtml(a4, icons, locale));
|
|
1424
1681
|
} else {
|
|
1425
1682
|
if (config.copy) buttons.push(this._actionButtonHtml("copy", icons, locale));
|
|
1426
1683
|
if (config.retry) buttons.push(this._actionButtonHtml("retry", icons, locale));
|
|
@@ -1455,16 +1712,16 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1455
1712
|
/** Append the registered custom action buttons for this bubble's role. */
|
|
1456
1713
|
_appendCustomActions(icons) {
|
|
1457
1714
|
if (!this._actionBarEl) return;
|
|
1458
|
-
for (const
|
|
1459
|
-
const roles =
|
|
1715
|
+
for (const a4 of this._cfg.getActions("bubble")) {
|
|
1716
|
+
const roles = a4.bubble?.roles ?? ["user", "assistant"];
|
|
1460
1717
|
if (!roles.includes(this._role)) continue;
|
|
1461
1718
|
const btn = document.createElement("button");
|
|
1462
1719
|
btn.className = "aparte-action-btn aparte-action-custom";
|
|
1463
|
-
btn.dataset["action"] = `custom:${
|
|
1464
|
-
btn.setAttribute("aria-label",
|
|
1465
|
-
btn.setAttribute("title",
|
|
1466
|
-
const fromProvider = icons[
|
|
1467
|
-
btn.innerHTML =
|
|
1720
|
+
btn.dataset["action"] = `custom:${a4.id}`;
|
|
1721
|
+
btn.setAttribute("aria-label", a4.label);
|
|
1722
|
+
btn.setAttribute("title", a4.label);
|
|
1723
|
+
const fromProvider = icons[a4.icon];
|
|
1724
|
+
btn.innerHTML = a4.icon.startsWith("<") ? a4.icon : typeof fromProvider === "function" ? fromProvider() : a4.iconFallback ?? "";
|
|
1468
1725
|
this._actionBarEl.appendChild(btn);
|
|
1469
1726
|
}
|
|
1470
1727
|
}
|
|
@@ -1530,12 +1787,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1530
1787
|
composed: true,
|
|
1531
1788
|
detail
|
|
1532
1789
|
}));
|
|
1533
|
-
this._cfg.getActions("bubble").find((
|
|
1790
|
+
this._cfg.getActions("bubble").find((x) => x.id === actionId)?.onClick?.(e2);
|
|
1534
1791
|
return;
|
|
1535
1792
|
}
|
|
1536
1793
|
switch (action) {
|
|
1537
1794
|
case "copy": {
|
|
1538
|
-
const text = this._content || this._segments.map((
|
|
1795
|
+
const text = this._content || this._segments.map((s) => s.content ?? "").join("\n");
|
|
1539
1796
|
const icons = this._cfg.getIconProvider();
|
|
1540
1797
|
const locale = this._cfg.getLocale();
|
|
1541
1798
|
navigator.clipboard.writeText(text).then(() => {
|
|
@@ -1973,7 +2230,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
1973
2230
|
if (!message.segments) {
|
|
1974
2231
|
message.segments = [];
|
|
1975
2232
|
}
|
|
1976
|
-
const index = message.segments.findIndex((
|
|
2233
|
+
const index = message.segments.findIndex((s) => s.id === segmentId);
|
|
1977
2234
|
const segment = index === -1 ? void 0 : message.segments[index];
|
|
1978
2235
|
if (segment && "content" in segment) {
|
|
1979
2236
|
message.segments[index] = {
|
|
@@ -2023,38 +2280,42 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2023
2280
|
if (!message.segments) {
|
|
2024
2281
|
message.segments = [];
|
|
2025
2282
|
}
|
|
2026
|
-
const stamped = stampSegmentOnInsert(
|
|
2283
|
+
const stamped = stampSegmentOnInsert(
|
|
2284
|
+
message.segments,
|
|
2285
|
+
segment,
|
|
2286
|
+
messageId,
|
|
2287
|
+
// THIS chat's defaults, not the page's: two chats on one page can be
|
|
2288
|
+
// configured differently, and the config seam is per instance.
|
|
2289
|
+
resolveConfig(this).getSegmentDefaults(segment.type)
|
|
2290
|
+
);
|
|
2027
2291
|
message.segments.push(stamped);
|
|
2028
2292
|
this._notifyBubble(messageId, "addSegment", stamped);
|
|
2029
2293
|
this._autoScroll();
|
|
2030
2294
|
}
|
|
2031
|
-
updateSegment(
|
|
2295
|
+
updateSegment(a4, b2, c2) {
|
|
2032
2296
|
const clientForm = c2 === void 0 && typeof b2 === "object";
|
|
2033
|
-
const messageId = clientForm ? this._activeMessageId() :
|
|
2034
|
-
const segmentId = clientForm ?
|
|
2297
|
+
const messageId = clientForm ? this._activeMessageId() : a4;
|
|
2298
|
+
const segmentId = clientForm ? a4 : b2;
|
|
2035
2299
|
const updates = clientForm ? b2 : c2;
|
|
2036
2300
|
if (!messageId) return;
|
|
2037
2301
|
const message = this._repo.getMessageById(messageId);
|
|
2038
2302
|
if (!message?.segments) return;
|
|
2039
|
-
const segmentIndex = message.segments.findIndex((
|
|
2303
|
+
const segmentIndex = message.segments.findIndex((s) => s.id === segmentId);
|
|
2040
2304
|
if (segmentIndex !== -1) {
|
|
2041
2305
|
const current = message.segments[segmentIndex];
|
|
2042
2306
|
const stamped = stampSegmentOnUpdate(current, updates);
|
|
2043
|
-
message.segments[segmentIndex] =
|
|
2044
|
-
...current,
|
|
2045
|
-
...stamped
|
|
2046
|
-
};
|
|
2307
|
+
message.segments[segmentIndex] = mergeSegmentUpdate(current, stamped);
|
|
2047
2308
|
this._notifyBubble(messageId, "updateSegment", { segmentId, updates: stamped });
|
|
2048
2309
|
}
|
|
2049
2310
|
}
|
|
2050
|
-
removeSegment(
|
|
2311
|
+
removeSegment(a4, b2) {
|
|
2051
2312
|
const clientForm = b2 === void 0;
|
|
2052
|
-
const messageId = clientForm ? this._activeMessageId() :
|
|
2053
|
-
const segmentId = clientForm ?
|
|
2313
|
+
const messageId = clientForm ? this._activeMessageId() : a4;
|
|
2314
|
+
const segmentId = clientForm ? a4 : b2;
|
|
2054
2315
|
if (!messageId || !segmentId) return;
|
|
2055
2316
|
const message = this._repo.getMessageById(messageId);
|
|
2056
2317
|
if (message?.segments) {
|
|
2057
|
-
const idx = message.segments.findIndex((
|
|
2318
|
+
const idx = message.segments.findIndex((s) => s.id === segmentId);
|
|
2058
2319
|
if (idx !== -1) {
|
|
2059
2320
|
message.segments.splice(idx, 1);
|
|
2060
2321
|
renumberSegments(message.segments);
|
|
@@ -2143,7 +2404,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2143
2404
|
* which both records the message and creates its bubble element.
|
|
2144
2405
|
*/
|
|
2145
2406
|
addMessage(message) {
|
|
2146
|
-
this._repo.addOrUpdateMessage(this._repo.headId, { ...message });
|
|
2407
|
+
this._repo.addOrUpdateMessage(this._repo.headId, adoptMessageSegments({ ...message }));
|
|
2147
2408
|
this._pruneRenderedBubbles();
|
|
2148
2409
|
this._autoScroll();
|
|
2149
2410
|
}
|
|
@@ -2154,8 +2415,20 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2154
2415
|
* When `_frameworkManagedDOM` is true, only the internal repo is updated —
|
|
2155
2416
|
* the framework owns the DOM and will create the bubble element itself.
|
|
2156
2417
|
*/
|
|
2157
|
-
appendMessage(message) {
|
|
2158
|
-
|
|
2418
|
+
appendMessage(message, options) {
|
|
2419
|
+
const stored = options?.historical ? adoptMessageSegments(message) : message.segments?.length ? { ...message, segments: message.segments.reduce(
|
|
2420
|
+
(acc, segment) => {
|
|
2421
|
+
acc.push(stampSegmentOnInsert(
|
|
2422
|
+
acc,
|
|
2423
|
+
segment,
|
|
2424
|
+
message.id,
|
|
2425
|
+
resolveConfig(this).getSegmentDefaults(segment.type)
|
|
2426
|
+
));
|
|
2427
|
+
return acc;
|
|
2428
|
+
},
|
|
2429
|
+
[]
|
|
2430
|
+
) } : { ...message };
|
|
2431
|
+
this._repo.addOrUpdateMessage(this._repo.headId, stored);
|
|
2159
2432
|
if (!this._frameworkManagedDOM) {
|
|
2160
2433
|
const wrapper = this.querySelector(".aparte-messages-wrapper");
|
|
2161
2434
|
if (wrapper) {
|
|
@@ -2172,7 +2445,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2172
2445
|
} else {
|
|
2173
2446
|
wrapper.appendChild(bubble);
|
|
2174
2447
|
}
|
|
2175
|
-
populateBubbleFromMessage(bubble,
|
|
2448
|
+
populateBubbleFromMessage(bubble, stored);
|
|
2176
2449
|
}
|
|
2177
2450
|
}
|
|
2178
2451
|
this._pruneRenderedBubbles();
|
|
@@ -2322,7 +2595,13 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2322
2595
|
*/
|
|
2323
2596
|
importTree(tree) {
|
|
2324
2597
|
this.clearAll({ revokeAttachments: false });
|
|
2325
|
-
this._repo.import(
|
|
2598
|
+
this._repo.import({
|
|
2599
|
+
...tree,
|
|
2600
|
+
messages: tree.messages.map((entry) => ({
|
|
2601
|
+
...entry,
|
|
2602
|
+
message: adoptMessageSegments(entry.message)
|
|
2603
|
+
}))
|
|
2604
|
+
});
|
|
2326
2605
|
this._reRenderActivePath();
|
|
2327
2606
|
}
|
|
2328
2607
|
/**
|
|
@@ -2370,7 +2649,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2370
2649
|
setMessages(messages) {
|
|
2371
2650
|
this.clearAll({ revokeAttachments: false });
|
|
2372
2651
|
for (const m2 of messages) {
|
|
2373
|
-
this.appendMessage(m2);
|
|
2652
|
+
this.appendMessage(m2, { historical: true });
|
|
2374
2653
|
}
|
|
2375
2654
|
}
|
|
2376
2655
|
/**
|
|
@@ -2868,6 +3147,20 @@ class AparteComposer extends HTMLElement {
|
|
|
2868
3147
|
_panelMode = "submit";
|
|
2869
3148
|
_panelSubmitEnabled = false;
|
|
2870
3149
|
_panelOnSubmit = null;
|
|
3150
|
+
/**
|
|
3151
|
+
* Who owns the one panel slot, and how to tell them they lost it.
|
|
3152
|
+
*
|
|
3153
|
+
* There is exactly one slot, `showPanel` empties it unconditionally, and nothing
|
|
3154
|
+
* used to say whose it was. Three paths therefore closed a panel whose owner was
|
|
3155
|
+
* still awaiting an answer, and none of them told the owner: a second `showPanel`,
|
|
3156
|
+
* the owner-of-record's own `hidePanel`, and — the one that actually bit —
|
|
3157
|
+
* `_handleMessageDone`, which fires on EVERY turn end. A question still open when a
|
|
3158
|
+
* turn finished lost its panel while `<aparte-elicitation>` kept `_pending` set,
|
|
3159
|
+
* so the request never settled AND every later request was short-circuited for the
|
|
3160
|
+
* life of the page.
|
|
3161
|
+
*/
|
|
3162
|
+
_panelToken = null;
|
|
3163
|
+
_panelOnEvict = null;
|
|
2871
3164
|
// Internal bus events that represent an observable STATE change — these are
|
|
2872
3165
|
// mirrored to the public `aparte-composer-change` DOM event. `submit`/`cancel`
|
|
2873
3166
|
// are actions, not state, and are covered by `aparte-send`/`aparte-cancel`.
|
|
@@ -3035,9 +3328,14 @@ class AparteComposer extends HTMLElement {
|
|
|
3035
3328
|
* used to set on a child: an attribute is themeable, is visible to a consumer's
|
|
3036
3329
|
* own rules, and does not clobber a `display` the consumer had set (the restore
|
|
3037
3330
|
* wrote `''`, not the previous value).
|
|
3331
|
+
*
|
|
3332
|
+
* Returns the TOKEN for this panel. Pass it back to `hidePanel` so a presenter
|
|
3333
|
+
* that has already lost the slot cannot close the panel that replaced it, and
|
|
3334
|
+
* supply `onEvict` to be told when that happens — an owner that is not told is an
|
|
3335
|
+
* owner whose promise nobody can settle.
|
|
3038
3336
|
*/
|
|
3039
3337
|
showPanel(panel, options) {
|
|
3040
|
-
this.
|
|
3338
|
+
this._evictPanel();
|
|
3041
3339
|
const inputEl = this.querySelector("aparte-composer-input");
|
|
3042
3340
|
this.setAttribute("data-panel-active", "");
|
|
3043
3341
|
panel.dataset["apartePanel"] = "true";
|
|
@@ -3050,10 +3348,31 @@ class AparteComposer extends HTMLElement {
|
|
|
3050
3348
|
this._panelSubmitEnabled = options?.submitEnabled ?? false;
|
|
3051
3349
|
this._panelMode = options?.mode ?? "submit";
|
|
3052
3350
|
this._panelOnSubmit = options?.onSubmit ?? null;
|
|
3351
|
+
this._panelOnEvict = options?.onEvict ?? null;
|
|
3352
|
+
const token = Symbol("aparte-composer-panel");
|
|
3353
|
+
this._panelToken = token;
|
|
3053
3354
|
this._emit("panel-change", { active: true, submitEnabled: this._panelSubmitEnabled, mode: this._panelMode });
|
|
3355
|
+
return token;
|
|
3054
3356
|
}
|
|
3055
|
-
/**
|
|
3056
|
-
|
|
3357
|
+
/**
|
|
3358
|
+
* Remove the panel and restore the composer's own controls.
|
|
3359
|
+
*
|
|
3360
|
+
* With a `token`, this closes the panel only if that token still owns the slot —
|
|
3361
|
+
* so a presenter settling late cannot tear down the panel that replaced it. With
|
|
3362
|
+
* no token it closes whatever is there, which is what a consumer driving the
|
|
3363
|
+
* composer directly means, and what `reset()` needs.
|
|
3364
|
+
*/
|
|
3365
|
+
hidePanel(token) {
|
|
3366
|
+
if (token !== void 0 && token !== this._panelToken) return;
|
|
3367
|
+
this._teardownPanel();
|
|
3368
|
+
}
|
|
3369
|
+
/** Close the panel AND tell its owner, so a pending request never orphans. */
|
|
3370
|
+
_evictPanel() {
|
|
3371
|
+
const onEvict = this._panelOnEvict;
|
|
3372
|
+
this._teardownPanel();
|
|
3373
|
+
onEvict?.();
|
|
3374
|
+
}
|
|
3375
|
+
_teardownPanel() {
|
|
3057
3376
|
const existing = this.querySelector("[data-aparte-panel]");
|
|
3058
3377
|
if (existing) existing.remove();
|
|
3059
3378
|
this.removeAttribute("data-panel-active");
|
|
@@ -3061,6 +3380,8 @@ class AparteComposer extends HTMLElement {
|
|
|
3061
3380
|
this._panelSubmitEnabled = false;
|
|
3062
3381
|
this._panelMode = "submit";
|
|
3063
3382
|
this._panelOnSubmit = null;
|
|
3383
|
+
this._panelOnEvict = null;
|
|
3384
|
+
this._panelToken = null;
|
|
3064
3385
|
this._emit("panel-change", { active: false, submitEnabled: false, mode: "submit" });
|
|
3065
3386
|
this.focus();
|
|
3066
3387
|
}
|
|
@@ -3138,7 +3459,7 @@ class AparteComposer extends HTMLElement {
|
|
|
3138
3459
|
reset() {
|
|
3139
3460
|
this.setValue("");
|
|
3140
3461
|
this.clearAttachments();
|
|
3141
|
-
if (this._panelActive) this.
|
|
3462
|
+
if (this._panelActive) this._evictPanel();
|
|
3142
3463
|
}
|
|
3143
3464
|
/** Focus the input primitive inside this composer. */
|
|
3144
3465
|
focus() {
|
|
@@ -3207,7 +3528,7 @@ class AparteComposer extends HTMLElement {
|
|
|
3207
3528
|
if (!this._isForThisComposer(e2)) return;
|
|
3208
3529
|
this._streaming = false;
|
|
3209
3530
|
this._emit("streaming-change", { streaming: false });
|
|
3210
|
-
if (this._panelActive) this.
|
|
3531
|
+
if (this._panelActive) this._evictPanel();
|
|
3211
3532
|
}
|
|
3212
3533
|
}
|
|
3213
3534
|
if (!customElements.get("aparte-composer")) {
|
|
@@ -3231,6 +3552,7 @@ class AparteComposerInput extends HTMLElement {
|
|
|
3231
3552
|
this._render();
|
|
3232
3553
|
this._connectToRoot();
|
|
3233
3554
|
this._scheduleInitialReflow();
|
|
3555
|
+
this._unsubscribes.push(subscribeConfigChange(this, () => this._updatePlaceholder()));
|
|
3234
3556
|
}
|
|
3235
3557
|
disconnectedCallback() {
|
|
3236
3558
|
this._editor?.removeEventListener("input", this._onInput);
|
|
@@ -3368,10 +3690,10 @@ class AparteComposerInput extends HTMLElement {
|
|
|
3368
3690
|
}
|
|
3369
3691
|
}
|
|
3370
3692
|
_handleFocus() {
|
|
3371
|
-
this.classList.add("is-focused");
|
|
3693
|
+
this.classList.add("aparte-is-focused");
|
|
3372
3694
|
}
|
|
3373
3695
|
_handleBlur() {
|
|
3374
|
-
this.classList.remove("is-focused");
|
|
3696
|
+
this.classList.remove("aparte-is-focused");
|
|
3375
3697
|
}
|
|
3376
3698
|
_handlePaste(e2) {
|
|
3377
3699
|
e2.preventDefault();
|
|
@@ -3414,9 +3736,9 @@ class AparteComposerInput extends HTMLElement {
|
|
|
3414
3736
|
}
|
|
3415
3737
|
_updatePlaceholder() {
|
|
3416
3738
|
if (this._editor) {
|
|
3417
|
-
const
|
|
3418
|
-
this._editor.setAttribute("data-placeholder",
|
|
3419
|
-
this._editor.setAttribute("aria-label",
|
|
3739
|
+
const p = this._getPlaceholder();
|
|
3740
|
+
this._editor.setAttribute("data-placeholder", p);
|
|
3741
|
+
this._editor.setAttribute("aria-label", p);
|
|
3420
3742
|
}
|
|
3421
3743
|
}
|
|
3422
3744
|
_updatePlaceholderVisibility() {
|
|
@@ -3434,6 +3756,16 @@ if (!customElements.get("aparte-composer-input")) {
|
|
|
3434
3756
|
class AparteComposerSend extends HTMLElement {
|
|
3435
3757
|
_button = null;
|
|
3436
3758
|
_unsubscribes = [];
|
|
3759
|
+
/**
|
|
3760
|
+
* The last `panel-change` payload.
|
|
3761
|
+
*
|
|
3762
|
+
* This button has four meanings — send, stop, submit an answer, advance to the
|
|
3763
|
+
* next question — and three of them are decided by state it does not own: the
|
|
3764
|
+
* root's `streaming`, and this payload. It was read straight out of the event's
|
|
3765
|
+
* arguments and thrown away, so nothing could recompute the button's chrome
|
|
3766
|
+
* afterwards; a config change had no way to know which of the four to write.
|
|
3767
|
+
*/
|
|
3768
|
+
_panel = null;
|
|
3437
3769
|
// Bound handler
|
|
3438
3770
|
_onClick = this._handleClick.bind(this);
|
|
3439
3771
|
connectedCallback() {
|
|
@@ -3483,27 +3815,56 @@ class AparteComposerSend extends HTMLElement {
|
|
|
3483
3815
|
root._on("attachments-change", () => this._syncState())
|
|
3484
3816
|
);
|
|
3485
3817
|
this._unsubscribes.push(
|
|
3486
|
-
root._on("panel-change", (
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
this._button.disabled = !submitEnabled;
|
|
3490
|
-
const cfg = resolveConfig(this);
|
|
3491
|
-
const advancing = mode === "advance";
|
|
3492
|
-
this._button.innerHTML = advancing ? cfg.getIcon("nextBranch") : this._getSubmitIcon();
|
|
3493
|
-
const label = advancing ? cfg.t("elicitationNext") || "Next" : cfg.t("submitButton") || "Submit";
|
|
3494
|
-
this._button.setAttribute("aria-label", label);
|
|
3495
|
-
this._button.setAttribute("title", label);
|
|
3496
|
-
this._button.classList.remove("is-streaming");
|
|
3497
|
-
} else {
|
|
3498
|
-
const root2 = this._getRoot();
|
|
3499
|
-
if (root2?.streaming) {
|
|
3500
|
-
this._syncStreamingState(true);
|
|
3501
|
-
} else {
|
|
3502
|
-
this._syncState();
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3818
|
+
root._on("panel-change", (payload) => {
|
|
3819
|
+
this._panel = payload;
|
|
3820
|
+
this._refreshChrome();
|
|
3505
3821
|
})
|
|
3506
3822
|
);
|
|
3823
|
+
this._unsubscribes.push(subscribeConfigChange(this, () => this._refreshChrome()));
|
|
3824
|
+
}
|
|
3825
|
+
/**
|
|
3826
|
+
* Write the chrome for the mode the button is IN, deciding before writing.
|
|
3827
|
+
*
|
|
3828
|
+
* Never `_render()`: it returns early once the button exists, and its own
|
|
3829
|
+
* disabled/icon computation consults neither `root.streaming` nor the panel — so
|
|
3830
|
+
* rebuilding mid-turn would put a paper plane back while a reply was still
|
|
3831
|
+
* streaming, and rebuilding with the question panel open would silently drop out
|
|
3832
|
+
* of answer mode. It would also take the focus off the one control in this
|
|
3833
|
+
* composer most likely to be holding it.
|
|
3834
|
+
*/
|
|
3835
|
+
_refreshChrome() {
|
|
3836
|
+
if (!this._button) return;
|
|
3837
|
+
if (this._panel?.active) {
|
|
3838
|
+
this._syncPanelState();
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3841
|
+
if (this._getRoot()?.streaming) {
|
|
3842
|
+
this._syncStreamingState(true);
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
this._syncState();
|
|
3846
|
+
}
|
|
3847
|
+
/**
|
|
3848
|
+
* Panel open: this one button now means "answer", and WHICH answer depends on
|
|
3849
|
+
* where you are in the form.
|
|
3850
|
+
*
|
|
3851
|
+
* The icon has to move with the meaning: it drew a paper plane while the label
|
|
3852
|
+
* already said "Submit", so it read as "send a message" while it meant "answer
|
|
3853
|
+
* this question". And a check on a form with three questions left was just as
|
|
3854
|
+
* wrong — hence a chevron while there is more ahead. The visual is what a user
|
|
3855
|
+
* reads.
|
|
3856
|
+
*/
|
|
3857
|
+
_syncPanelState() {
|
|
3858
|
+
const panel = this._panel;
|
|
3859
|
+
if (!this._button || !panel?.active) return;
|
|
3860
|
+
const cfg = resolveConfig(this);
|
|
3861
|
+
const advancing = panel.mode === "advance";
|
|
3862
|
+
this._button.disabled = !panel.submitEnabled;
|
|
3863
|
+
this._button.innerHTML = advancing ? cfg.getIcon("nextBranch") : this._getSubmitIcon();
|
|
3864
|
+
const label = advancing ? cfg.t("elicitationNext") || "Next" : cfg.t("submitButton") || "Submit";
|
|
3865
|
+
this._button.setAttribute("aria-label", label);
|
|
3866
|
+
this._button.setAttribute("title", label);
|
|
3867
|
+
this._button.classList.remove("aparte-is-streaming");
|
|
3507
3868
|
}
|
|
3508
3869
|
_handleClick(e2) {
|
|
3509
3870
|
e2.preventDefault();
|
|
@@ -3519,16 +3880,17 @@ class AparteComposerSend extends HTMLElement {
|
|
|
3519
3880
|
const label = resolveConfig(this).t("sendButton") || "Send";
|
|
3520
3881
|
this._button.setAttribute("aria-label", label);
|
|
3521
3882
|
this._button.setAttribute("title", label);
|
|
3522
|
-
this._button.classList.remove("is-streaming");
|
|
3883
|
+
this._button.classList.remove("aparte-is-streaming");
|
|
3523
3884
|
}
|
|
3524
3885
|
_syncStreamingState(streaming) {
|
|
3525
3886
|
if (!this._button) return;
|
|
3526
3887
|
if (streaming) {
|
|
3527
3888
|
this._button.disabled = false;
|
|
3528
3889
|
this._button.innerHTML = this._getStopIcon();
|
|
3529
|
-
this.
|
|
3530
|
-
this._button.setAttribute("
|
|
3531
|
-
this._button.
|
|
3890
|
+
const label = resolveConfig(this).t("stopButton") || "Stop";
|
|
3891
|
+
this._button.setAttribute("aria-label", label);
|
|
3892
|
+
this._button.setAttribute("title", label);
|
|
3893
|
+
this._button.classList.add("aparte-is-streaming");
|
|
3532
3894
|
} else {
|
|
3533
3895
|
this._syncState();
|
|
3534
3896
|
}
|
|
@@ -3558,6 +3920,7 @@ class AparteComposerCancel extends HTMLElement {
|
|
|
3558
3920
|
connectedCallback() {
|
|
3559
3921
|
this._render();
|
|
3560
3922
|
this._connectToRoot();
|
|
3923
|
+
this._unsubscribes.push(subscribeConfigChange(this, () => this._refreshChrome()));
|
|
3561
3924
|
}
|
|
3562
3925
|
disconnectedCallback() {
|
|
3563
3926
|
this._button?.removeEventListener("click", this._onClick);
|
|
@@ -3595,6 +3958,20 @@ class AparteComposerCancel extends HTMLElement {
|
|
|
3595
3958
|
e2.preventDefault();
|
|
3596
3959
|
this._getRoot()?.cancel();
|
|
3597
3960
|
}
|
|
3961
|
+
/**
|
|
3962
|
+
* Re-read the accessible name and the icon in place.
|
|
3963
|
+
*
|
|
3964
|
+
* `hidden` is NOT touched: `_render()` always renders this button hidden and only
|
|
3965
|
+
* the root's `streaming-change` listener ever un-hides it, so a rebuild would make
|
|
3966
|
+
* the stop button vanish in the middle of a turn.
|
|
3967
|
+
*/
|
|
3968
|
+
_refreshChrome() {
|
|
3969
|
+
if (!this._button) return;
|
|
3970
|
+
const label = resolveConfig(this).t("stopButton") || "Stop";
|
|
3971
|
+
this._button.setAttribute("aria-label", label);
|
|
3972
|
+
this._button.setAttribute("title", label);
|
|
3973
|
+
this._button.innerHTML = this._getStopIcon();
|
|
3974
|
+
}
|
|
3598
3975
|
_getStopIcon() {
|
|
3599
3976
|
return resolveConfig(this).getIcon("stop");
|
|
3600
3977
|
}
|
|
@@ -3700,6 +4077,7 @@ class AparteComposerAddAttachment extends HTMLElement {
|
|
|
3700
4077
|
this._render();
|
|
3701
4078
|
this._connectToRoot();
|
|
3702
4079
|
this._setupDragDrop();
|
|
4080
|
+
this._unsubscribes.push(subscribeConfigChange(this, () => this._refreshChrome()));
|
|
3703
4081
|
}
|
|
3704
4082
|
disconnectedCallback() {
|
|
3705
4083
|
this._button?.removeEventListener("click", this._onClick);
|
|
@@ -3716,6 +4094,24 @@ class AparteComposerAddAttachment extends HTMLElement {
|
|
|
3716
4094
|
_getRoot() {
|
|
3717
4095
|
return this.closest("aparte-composer");
|
|
3718
4096
|
}
|
|
4097
|
+
/**
|
|
4098
|
+
* Re-read the label and the icon on the button that already exists.
|
|
4099
|
+
*
|
|
4100
|
+
* Deliberately not a re-render: `_render()` returns early once the button is
|
|
4101
|
+
* there, and its `disabled` computation ignores `root.streaming` while the
|
|
4102
|
+
* streaming listener sets `disabled` directly — so rebuilding would silently
|
|
4103
|
+
* re-enable the attach button mid-turn, and drop focus if the user were on it.
|
|
4104
|
+
* The native file input and the drag listeners live outside this element
|
|
4105
|
+
* (on `document.body` and on the composer root), so they are untouched either way.
|
|
4106
|
+
*/
|
|
4107
|
+
_refreshChrome() {
|
|
4108
|
+
if (!this._button) return;
|
|
4109
|
+
const cfg = resolveConfig(this);
|
|
4110
|
+
const label = cfg.t("actionUpload") || "Attach file";
|
|
4111
|
+
this._button.setAttribute("aria-label", label);
|
|
4112
|
+
this._button.setAttribute("title", label);
|
|
4113
|
+
this._button.innerHTML = cfg.getIcon("paperclip") || this._defaultIcon();
|
|
4114
|
+
}
|
|
3719
4115
|
_render() {
|
|
3720
4116
|
if (this.querySelector(".aparte-caa-button")) return;
|
|
3721
4117
|
const label = resolveConfig(this).t("actionUpload") || "Attach file";
|
|
@@ -3769,15 +4165,15 @@ class AparteComposerAddAttachment extends HTMLElement {
|
|
|
3769
4165
|
const onDragOver = (e2) => {
|
|
3770
4166
|
if (root.disabled) return;
|
|
3771
4167
|
prevent(e2);
|
|
3772
|
-
root.classList.add("is-dragover");
|
|
4168
|
+
root.classList.add("aparte-is-dragover");
|
|
3773
4169
|
};
|
|
3774
4170
|
const onDragLeave = (e2) => {
|
|
3775
4171
|
prevent(e2);
|
|
3776
|
-
root.classList.remove("is-dragover");
|
|
4172
|
+
root.classList.remove("aparte-is-dragover");
|
|
3777
4173
|
};
|
|
3778
4174
|
const onDrop = (e2) => {
|
|
3779
4175
|
prevent(e2);
|
|
3780
|
-
root.classList.remove("is-dragover");
|
|
4176
|
+
root.classList.remove("aparte-is-dragover");
|
|
3781
4177
|
if (root.disabled) return;
|
|
3782
4178
|
const files = e2.dataTransfer?.files;
|
|
3783
4179
|
if (files?.length) this._getRoot()?.addAttachments(files);
|
|
@@ -3809,6 +4205,9 @@ class AparteComposerAction extends HTMLElement {
|
|
|
3809
4205
|
connectedCallback() {
|
|
3810
4206
|
this._render();
|
|
3811
4207
|
this._connectToRoot();
|
|
4208
|
+
this._unsubscribes.push(subscribeConfigChange(this, () => {
|
|
4209
|
+
if (this._button) this._button.innerHTML = this._resolveIcon(this.getAttribute("icon") ?? "");
|
|
4210
|
+
}));
|
|
3812
4211
|
}
|
|
3813
4212
|
disconnectedCallback() {
|
|
3814
4213
|
this._button?.removeEventListener("click", this._onClick);
|
|
@@ -4067,162 +4466,6 @@ class AparteConversationList extends HTMLElement {
|
|
|
4067
4466
|
}
|
|
4068
4467
|
}
|
|
4069
4468
|
if (!customElements.get("aparte-conversation-list")) customElements.define("aparte-conversation-list", AparteConversationList);
|
|
4070
|
-
class AparteElicitation extends HTMLElement {
|
|
4071
|
-
_pending = null;
|
|
4072
|
-
/**
|
|
4073
|
-
* A turn ended — cancel the open question only if it was OUR turn.
|
|
4074
|
-
*
|
|
4075
|
-
* These two listeners sit on `window` and had no instance filter at all, so a
|
|
4076
|
-
* Stop (or an error) in one chat cancelled the question a DIFFERENT chat was
|
|
4077
|
-
* waiting on — and that chat's model was told the user had refused a question
|
|
4078
|
-
* the user was still looking at. Same defect as the `compact()` handler that
|
|
4079
|
-
* emptied both chats, which its four sibling handlers in `AparteClient` already
|
|
4080
|
-
* guarded against.
|
|
4081
|
-
*
|
|
4082
|
-
* The leniency rule is the composer's, deliberately: an event with no
|
|
4083
|
-
* `targetId` is for everyone (a single-chat app never sets one), and a chat we
|
|
4084
|
-
* cannot identify accepts everything rather than becoming deaf. Only a
|
|
4085
|
-
* MISMATCH is ignored.
|
|
4086
|
-
*/
|
|
4087
|
-
_onTurnEnd = (e2) => {
|
|
4088
|
-
const evtTargetId = e2.detail?.targetId;
|
|
4089
|
-
const own = this._pendingTargetId();
|
|
4090
|
-
if (evtTargetId && own && evtTargetId !== own) return;
|
|
4091
|
-
this._cancelPending();
|
|
4092
|
-
};
|
|
4093
|
-
connectedCallback() {
|
|
4094
|
-
this.style.display = "none";
|
|
4095
|
-
resolveConfig(this).setElicitationPresenter(this._present);
|
|
4096
|
-
window.addEventListener("aparte-message-aborted", this._onTurnEnd);
|
|
4097
|
-
window.addEventListener("aparte-message-error", this._onTurnEnd);
|
|
4098
|
-
}
|
|
4099
|
-
/**
|
|
4100
|
-
* The boundary above us appeared, changed, or went away — move the
|
|
4101
|
-
* registration with it.
|
|
4102
|
-
*
|
|
4103
|
-
* `connectedCallback` alone is not enough and cannot be: registering is a
|
|
4104
|
-
* WRITE, and under all four wrappers it happens before `attachConfig` runs, so
|
|
4105
|
-
* it lands on the global singleton. `requestUserInput()` then resolves the
|
|
4106
|
-
* instance config, finds nothing, and answers the model `cancel` — the model
|
|
4107
|
-
* hears the user refuse a question the user never saw.
|
|
4108
|
-
*
|
|
4109
|
-
* See {@link AparteConfigAware}.
|
|
4110
|
-
*/
|
|
4111
|
-
aparteConfigChanged(next, previous) {
|
|
4112
|
-
if (previous.getElicitationPresenter() === this._present) previous.setElicitationPresenter(null);
|
|
4113
|
-
next.setElicitationPresenter(this._present);
|
|
4114
|
-
}
|
|
4115
|
-
disconnectedCallback() {
|
|
4116
|
-
const cfg = resolveConfig(this);
|
|
4117
|
-
if (cfg.getElicitationPresenter() === this._present) cfg.setElicitationPresenter(null);
|
|
4118
|
-
window.removeEventListener("aparte-message-aborted", this._onTurnEnd);
|
|
4119
|
-
window.removeEventListener("aparte-message-error", this._onTurnEnd);
|
|
4120
|
-
this._cancelPending();
|
|
4121
|
-
}
|
|
4122
|
-
_present = (request) => {
|
|
4123
|
-
if (this._pending) return Promise.resolve({ action: "cancel" });
|
|
4124
|
-
const composer = this._getComposer();
|
|
4125
|
-
if (!composer) return Promise.resolve({ action: "cancel" });
|
|
4126
|
-
return new Promise((resolve) => {
|
|
4127
|
-
let done = false;
|
|
4128
|
-
const settle = (result) => {
|
|
4129
|
-
if (done) return;
|
|
4130
|
-
done = true;
|
|
4131
|
-
this._pending = null;
|
|
4132
|
-
composer.hidePanel();
|
|
4133
|
-
resolve(result);
|
|
4134
|
-
};
|
|
4135
|
-
if (request.signal) {
|
|
4136
|
-
if (request.signal.aborted) {
|
|
4137
|
-
settle({ action: "cancel" });
|
|
4138
|
-
return;
|
|
4139
|
-
}
|
|
4140
|
-
request.signal.addEventListener("abort", () => settle({ action: "cancel" }), { once: true });
|
|
4141
|
-
}
|
|
4142
|
-
const cfg = resolveConfig(this);
|
|
4143
|
-
const panel = runWithConfig(cfg, () => buildElicitationPanel(request.message, request.schema, () => {
|
|
4144
|
-
composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
|
|
4145
|
-
}));
|
|
4146
|
-
const skip = document.createElement("button");
|
|
4147
|
-
skip.type = "button";
|
|
4148
|
-
skip.className = "aparte-elic-skip";
|
|
4149
|
-
skip.textContent = cfg.t("elicitationSkip");
|
|
4150
|
-
skip.addEventListener("click", () => settle({ action: "decline" }));
|
|
4151
|
-
panel.dismiss.appendChild(skip);
|
|
4152
|
-
this._pending = { settle, composer };
|
|
4153
|
-
composer.showPanel(panel.el, {
|
|
4154
|
-
submitEnabled: panel.canProceed(),
|
|
4155
|
-
mode: panel.mode(),
|
|
4156
|
-
onSubmit: () => {
|
|
4157
|
-
if (panel.mode() === "advance") {
|
|
4158
|
-
panel.proceed();
|
|
4159
|
-
composer.setPanelSubmitEnabled(panel.canProceed(), panel.mode());
|
|
4160
|
-
return;
|
|
4161
|
-
}
|
|
4162
|
-
if (panel.isComplete()) settle({ action: "accept", content: panel.getContent() });
|
|
4163
|
-
}
|
|
4164
|
-
});
|
|
4165
|
-
panel.focus();
|
|
4166
|
-
});
|
|
4167
|
-
};
|
|
4168
|
-
_cancelPending() {
|
|
4169
|
-
this._pending?.settle({ action: "cancel" });
|
|
4170
|
-
}
|
|
4171
|
-
/**
|
|
4172
|
-
* The host id of the chat whose composer holds the open panel.
|
|
4173
|
-
*
|
|
4174
|
-
* Walks up from the composer rather than from `this`, because the panel lives in
|
|
4175
|
-
* the composer and that is what the turn belongs to. Matches the hosts
|
|
4176
|
-
* `aparte-chat-bubble._resolveTargetId()` matches, for the reason written there:
|
|
4177
|
-
* Angular's wrapper root IS the `<aparte-chat>` element, while the plain-root
|
|
4178
|
-
* wrappers render a `[data-aparte-chat]` div instead, so matching only the tag
|
|
4179
|
-
* resolves `undefined` on three wrappers out of four.
|
|
4180
|
-
*/
|
|
4181
|
-
_pendingTargetId() {
|
|
4182
|
-
let el = this._pending?.composer ?? null;
|
|
4183
|
-
while (el) {
|
|
4184
|
-
const tag = el.tagName?.toLowerCase();
|
|
4185
|
-
const isHost = tag === "aparte-chat" || tag === "aparte-chat-component" || el.hasAttribute?.("data-aparte-chat");
|
|
4186
|
-
if (isHost && el.id) return el.id;
|
|
4187
|
-
el = el.parentElement;
|
|
4188
|
-
}
|
|
4189
|
-
return void 0;
|
|
4190
|
-
}
|
|
4191
|
-
/**
|
|
4192
|
-
* The composer to present in: the nearest one in an ancestor subtree, and
|
|
4193
|
-
* nothing else.
|
|
4194
|
-
*
|
|
4195
|
-
* There used to be a `document.querySelector('aparte-composer')` fallback, which
|
|
4196
|
-
* is the "first chat on the page" bug this repo has now fixed in four other
|
|
4197
|
-
* places: on a page with two chats, an elicitation that could not find its own
|
|
4198
|
-
* composer mounted its panel in the OTHER chat's — so one conversation's question
|
|
4199
|
-
* appeared under the other conversation, and answering it resolved a tool call
|
|
4200
|
-
* belonging to a chat the user was not looking at.
|
|
4201
|
-
*
|
|
4202
|
-
* Returning `null` instead resolves `cancel`, which is honest: nothing was
|
|
4203
|
-
* shown, so nothing was answered. The warning names the fix, because this is a
|
|
4204
|
-
* setup mistake and only the developer can correct it — the guide's own example
|
|
4205
|
-
* puts `<aparte-elicitation>` inside `<aparte-chat>`.
|
|
4206
|
-
*/
|
|
4207
|
-
_getComposer() {
|
|
4208
|
-
let node = this.parentElement;
|
|
4209
|
-
while (node) {
|
|
4210
|
-
const composer = node.querySelector("aparte-composer");
|
|
4211
|
-
if (composer && typeof composer.showPanel === "function") return composer;
|
|
4212
|
-
const tag = node.tagName?.toLowerCase();
|
|
4213
|
-
const isChatBoundary = tag === "aparte-chat" || tag === "aparte-chat-component" || node.hasAttribute?.("data-aparte-chat");
|
|
4214
|
-
if (isChatBoundary) break;
|
|
4215
|
-
node = node.parentElement;
|
|
4216
|
-
}
|
|
4217
|
-
console.warn(
|
|
4218
|
-
"[aparte-elicitation] No <aparte-composer> in this element's subtree, so the request was cancelled — the model will read that as a refusal. Move <aparte-elicitation> inside the <aparte-chat> it belongs to. It is deliberately NOT borrowing another chat's composer: on a page with two chats that put the question under the wrong one."
|
|
4219
|
-
);
|
|
4220
|
-
return null;
|
|
4221
|
-
}
|
|
4222
|
-
}
|
|
4223
|
-
if (typeof customElements !== "undefined" && !customElements.get("aparte-elicitation")) {
|
|
4224
|
-
customElements.define("aparte-elicitation", AparteElicitation);
|
|
4225
|
-
}
|
|
4226
4469
|
function registerAllComponents() {
|
|
4227
4470
|
const _chat = customElements.get("aparte-chat");
|
|
4228
4471
|
const _viewport = customElements.get("aparte-chat-viewport");
|
|
@@ -4233,21 +4476,22 @@ function registerAllComponents() {
|
|
|
4233
4476
|
}
|
|
4234
4477
|
}
|
|
4235
4478
|
export {
|
|
4236
|
-
A as
|
|
4237
|
-
a as
|
|
4238
|
-
b as
|
|
4239
|
-
c as
|
|
4240
|
-
d as
|
|
4241
|
-
e as
|
|
4242
|
-
f as
|
|
4243
|
-
g as
|
|
4244
|
-
h as
|
|
4479
|
+
A as APARTE_CONFIG_CHANGE,
|
|
4480
|
+
a as APARTE_CONVERSATION_SCHEMA_VERSION,
|
|
4481
|
+
b as APARTE_DEFAULT_BUBBLE_ACTIONS,
|
|
4482
|
+
c as APARTE_DEFAULT_HOST_HANDLERS,
|
|
4483
|
+
d as APARTE_DEFAULT_ICON_FALLBACKS,
|
|
4484
|
+
e as APARTE_DEFAULT_LOCALE,
|
|
4485
|
+
f as APARTE_DEFAULT_SKELETON_FALLBACKS,
|
|
4486
|
+
g as APARTE_DEFAULT_UI_EVENTS,
|
|
4487
|
+
h as APARTE_HOST_ATTR,
|
|
4488
|
+
i as AparteBackendTransport,
|
|
4245
4489
|
AparteChat,
|
|
4246
4490
|
AparteChatBubble,
|
|
4247
|
-
|
|
4491
|
+
j as AparteChatHost,
|
|
4248
4492
|
AparteChatStatus,
|
|
4249
4493
|
AparteChatViewport,
|
|
4250
|
-
|
|
4494
|
+
k as AparteClient,
|
|
4251
4495
|
AparteComposer,
|
|
4252
4496
|
AparteComposerAction,
|
|
4253
4497
|
AparteComposerAddAttachment,
|
|
@@ -4256,57 +4500,61 @@ export {
|
|
|
4256
4500
|
AparteComposerInput,
|
|
4257
4501
|
AparteComposerSend,
|
|
4258
4502
|
AparteComposerToolbar,
|
|
4259
|
-
|
|
4260
|
-
|
|
4503
|
+
l as AparteConfig,
|
|
4504
|
+
m as AparteConversationController,
|
|
4261
4505
|
AparteConversationList,
|
|
4262
|
-
|
|
4263
|
-
|
|
4506
|
+
n as AparteConversationManager,
|
|
4507
|
+
o as AparteDirectTransport,
|
|
4264
4508
|
AparteElicitation,
|
|
4265
|
-
|
|
4266
|
-
|
|
4509
|
+
AparteElicitationAbortError,
|
|
4510
|
+
q as AparteError,
|
|
4511
|
+
r as AparteErrorCode,
|
|
4267
4512
|
AparteMessageRepository,
|
|
4268
4513
|
AparteOptgroup,
|
|
4269
4514
|
AparteOption,
|
|
4270
4515
|
AparteProgressSpinner,
|
|
4271
4516
|
AparteSelect,
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4517
|
+
t as AparteStreamParser,
|
|
4518
|
+
u as aparteGlobalConfig,
|
|
4519
|
+
v as applyElementProps,
|
|
4520
|
+
w as attachConfig,
|
|
4521
|
+
buildApprovalPanel,
|
|
4276
4522
|
buildElicitationPanel,
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4523
|
+
z as collectRendererStyles,
|
|
4524
|
+
B as contentToText,
|
|
4525
|
+
C as contextConfig,
|
|
4526
|
+
D as createAparteChatHandler,
|
|
4527
|
+
E as createStreamAdapter,
|
|
4282
4528
|
cssEscape,
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4529
|
+
G as declineDefaultRenderers,
|
|
4530
|
+
H as defaultSanitizer,
|
|
4531
|
+
I as deriveArtifactKind,
|
|
4532
|
+
J as detachConfig,
|
|
4287
4533
|
escapeAttr,
|
|
4288
4534
|
escapeHtml,
|
|
4289
|
-
|
|
4290
|
-
|
|
4535
|
+
M as filesToAttachments,
|
|
4536
|
+
N as getAllRenderers,
|
|
4291
4537
|
getSegmentRenderer,
|
|
4292
4538
|
installDefaultRenderersOnce,
|
|
4293
4539
|
isAwaitingReply,
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4540
|
+
R as isFormatAdapter,
|
|
4541
|
+
S as isSafeUrl,
|
|
4542
|
+
T as isSegmentSettled,
|
|
4543
|
+
U as parseAparteEventStream,
|
|
4544
|
+
V as parseMarkdownToSegments,
|
|
4299
4545
|
populateBubbleFromMessage,
|
|
4300
|
-
|
|
4546
|
+
X as readableToAsyncIterable,
|
|
4301
4547
|
registerAllComponents,
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4548
|
+
Y as registerDefaultRenderers,
|
|
4549
|
+
Z as registerSegmentRenderer,
|
|
4550
|
+
_ as requestUserInput,
|
|
4305
4551
|
resolveConfig,
|
|
4306
4552
|
revokeAttachmentUrls,
|
|
4307
4553
|
runWithConfig,
|
|
4308
|
-
|
|
4309
|
-
|
|
4554
|
+
a2 as segmentDuration,
|
|
4555
|
+
a3 as segmentTiming,
|
|
4556
|
+
subscribeConfigChange,
|
|
4557
|
+
a5 as unregisterSegmentRenderer,
|
|
4310
4558
|
uuid
|
|
4311
4559
|
};
|
|
4312
4560
|
//# sourceMappingURL=index.js.map
|