@syntrologie/adapt-chatbot 2.45.0 → 2.46.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.
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  renderMarkdown
3
- } from "./chunk-VZIUXXAM.js";
3
+ } from "./chunk-TZA7572J.js";
4
4
 
5
5
  // src/AdaptiveChatTrail.ts
6
6
  import { css, html, LitElement, nothing } from "lit";
7
+ import { repeat } from "lit/directives/repeat.js";
7
8
  import { styleMap } from "lit/directives/style-map.js";
8
9
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
9
10
  var DEFAULT_VISIBLE = 3;
@@ -301,6 +302,7 @@ var _AdaptiveChatTrail = class _AdaptiveChatTrail extends LitElement {
301
302
  data-trail-thinking
302
303
  data-role="assistant"
303
304
  data-status="thinking"
305
+ aria-live="off"
304
306
  aria-label=${// The accessible name is the sentence on screen. When the dots are the
305
307
  // only visual there is no sentence to mirror, so name the state with
306
308
  // the SAME verb the visible copy uses ("typing", not "thinking").
@@ -624,9 +626,31 @@ var _AdaptiveChatTrail = class _AdaptiveChatTrail extends LitElement {
624
626
  >
625
627
  </button>`;
626
628
  }
629
+ /**
630
+ * The conversation live region.
631
+ *
632
+ * Always rendered, in EVERY branch of `render()` — including the ones that
633
+ * paint no transcript at all. A live region must already be in the DOM when
634
+ * its first child arrives; one created in the same mutation as its content
635
+ * is not reliably announced by NVDA or JAWS (adversarial review I3), and the
636
+ * content that would be lost is the assistant's first reply of the session.
637
+ *
638
+ * `display: contents` means it contributes no box and no layout anywhere it
639
+ * is mounted, so it is safe to place in an empty-state hero or in the flex
640
+ * scroll container alike. In the empty states it is rendered LAST so it can
641
+ * never become the `:first-child` any layout rule targets.
642
+ */
643
+ _renderConversationLog(children) {
644
+ return html`<div
645
+ data-syntro-chat-log
646
+ role="log"
647
+ aria-label="Conversation"
648
+ style="display:contents"
649
+ >${children}</div>`;
650
+ }
627
651
  render() {
628
652
  if (this.messages.length === 0 && !this.greeting && !this.introSuggestion && this.tileReceipts.length === 0 && this.inlineWidgets.length === 0)
629
- return nothing;
653
+ return this._renderConversationLog(nothing);
630
654
  if (this.messages.length === 0 && (this.greeting || this.introSuggestion)) {
631
655
  return html`<div data-syntro-chat-trail data-state="empty" style=${styleMap({
632
656
  display: "flex",
@@ -660,6 +684,7 @@ var _AdaptiveChatTrail = class _AdaptiveChatTrail extends LitElement {
660
684
  >${this.introSuggestion.label}</button>` : nothing}
661
685
  ${this._renderReceiptStrip(this.tileReceipts)}
662
686
  ${this._renderInlineWidgetStrip(this.inlineWidgets)}
687
+ ${this._renderConversationLog(nothing)}
663
688
  </div>`;
664
689
  }
665
690
  if (this.messages.length === 0 && (this.tileReceipts.length > 0 || this.inlineWidgets.length > 0)) {
@@ -674,6 +699,7 @@ var _AdaptiveChatTrail = class _AdaptiveChatTrail extends LitElement {
674
699
  })}>
675
700
  ${this._renderReceiptStrip(this.tileReceipts)}
676
701
  ${this._renderInlineWidgetStrip(this.inlineWidgets)}
702
+ ${this._renderConversationLog(nothing)}
677
703
  </div>`;
678
704
  }
679
705
  let turnReplies = [];
@@ -809,39 +835,11 @@ var _AdaptiveChatTrail = class _AdaptiveChatTrail extends LitElement {
809
835
  position: "relative",
810
836
  width: "100%"
811
837
  };
812
- return html`
813
- ${// Single toggle button above the frame. Same DOM node morphs:
814
- // collapsed "↑ N more · expand" / "↑ expand" (calls _onExpand)
815
- // expanded → "⌄ minimize" (calls _onCollapse)
816
- // Lives OUTSIDE the frame so the absolute blur veil (top:0,
817
- // z-index:2 inside the frame) doesn't cover it. Shown whenever
818
- // there's anything in the trail — the user expects this affordance
819
- // available regardless of how many chips are on screen.
820
- // When ``forceExpanded`` is set by the host (full-screen panel,
821
- // etc.), there's nowhere to collapse INTO and the affordance
822
- // would just confuse the user — suppress it entirely.
823
- this.messages.length === 0 || this.forceExpanded ? nothing : this.expanded ? html`<button
824
- type="button"
825
- data-trail-toggle
826
- data-trail-collapse
827
- @click=${this._onCollapse}
828
- style=${styleMap(moreStyles())}
829
- >⌄ minimize</button>` : html`<button
830
- type="button"
831
- data-trail-toggle
832
- data-trail-more
833
- @click=${this._onExpand}
834
- style=${styleMap(moreStyles())}
835
- >${hidden > 0 ? html`↑ ${hidden} more · expand` : html`↑ expand`}</button>`}
836
- <div data-syntro-chat-trail-frame style=${styleMap(frameStyles)}>
837
- <div
838
- data-syntro-chat-trail
839
- style=${styleMap(containerStyles)}
840
- @scroll=${this._onTrailScroll}
841
- @wheel=${this._markUserScrollIntent}
842
- @touchmove=${this._markUserScrollIntent}
843
- >
844
- ${visible.map((m, i) => {
838
+ const isStreamingMsg = (m) => m.role === "assistant" && m.status === "streaming";
839
+ const lastVisible = visible[visible.length - 1];
840
+ const streamingTail = lastVisible !== void 0 && isStreamingMsg(lastVisible);
841
+ const strandedStreaming = (m, i) => isStreamingMsg(m) && !(streamingTail && i === visible.length - 1);
842
+ const renderChip = (m, i) => {
845
843
  const pos = this.expanded || this.forceExpanded ? 0 : visible.length - 1 - i;
846
844
  const state = chipVisualState(m);
847
845
  const isStreaming = state.kind === "streaming";
@@ -861,61 +859,112 @@ var _AdaptiveChatTrail = class _AdaptiveChatTrail extends LitElement {
861
859
  animation: "syntro-trail-chip-enter 220ms cubic-bezier(0.22, 1, 0.36, 1) both"
862
860
  } : chipStyles(state, pos);
863
861
  const personaByline = this.personaName && this._isAssistantTurnStart(m) ? html`<div data-trail-persona-byline style=${styleMap(personaBylineStyles())}>
864
- ${this.personaAvatarSm ? html`<img
865
- data-trail-persona-avatar
866
- src=${this.personaAvatarSm}
867
- alt=""
868
- style=${styleMap(personaAvatarStyles())}
869
- />` : nothing}<span>${this.personaName}</span>
870
- </div>` : nothing;
862
+ ${this.personaAvatarSm ? html`<img
863
+ data-trail-persona-avatar
864
+ src=${this.personaAvatarSm}
865
+ alt=""
866
+ style=${styleMap(personaAvatarStyles())}
867
+ />` : nothing}<span>${this.personaName}</span>
868
+ </div>` : nothing;
871
869
  return html`<div
872
- data-trail-chip
873
- data-trail-chip-id=${String(m.id)}
874
- data-role=${m.role}
875
- data-status=${m.status ?? "complete"}
876
- style=${styleMap(chipStyle)}
877
- >${personaByline}${renderedText}${isStreaming ? html`<span
878
- data-trail-caret
879
- aria-hidden="true"
880
- style=${styleMap(caretStyles())}
881
- ></span>` : nothing}${toolCalls.length > 0 ? html`<div data-trail-toolcalls style=${styleMap(toolCallStripStyles())}>
882
- ${toolCalls.map(
870
+ data-trail-chip
871
+ data-trail-chip-id=${String(m.id)}
872
+ data-role=${m.role}
873
+ data-status=${m.status ?? "complete"}
874
+ aria-hidden=${strandedStreaming(m, i) ? "true" : nothing}
875
+ ?inert=${strandedStreaming(m, i)}
876
+ style=${styleMap(chipStyle)}
877
+ >${personaByline}${renderedText}${isStreaming ? html`<span
878
+ data-trail-caret
879
+ aria-hidden="true"
880
+ style=${styleMap(caretStyles())}
881
+ ></span>` : nothing}${toolCalls.length > 0 ? html`<div data-trail-toolcalls style=${styleMap(toolCallStripStyles())}>
882
+ ${toolCalls.map(
883
883
  (tc) => html`<button
884
- type="button"
885
- data-trail-toolcall
886
- data-tool-id=${tc.id}
887
- data-tool-status=${tc.status}
888
- @click=${() => this._onToolCallClick(tc)}
889
- style=${styleMap(pendingToolCallChipStyles())}
890
- title="${tc.name} · ${tc.status}"
891
- >? ${tc.name}</button>`
884
+ type="button"
885
+ data-trail-toolcall
886
+ data-tool-id=${tc.id}
887
+ data-tool-status=${tc.status}
888
+ @click=${() => this._onToolCallClick(tc)}
889
+ style=${styleMap(pendingToolCallChipStyles())}
890
+ title="${tc.name} · ${tc.status}"
891
+ >? ${tc.name}</button>`
892
892
  )}
893
- </div>` : nothing}${receipts.length > 0 ? html`<div data-trail-receipts style=${styleMap(receiptStripStyles())}>
894
- ${receipts.map(
893
+ </div>` : nothing}${receipts.length > 0 ? html`<div data-trail-receipts style=${styleMap(receiptStripStyles())}>
894
+ ${receipts.map(
895
895
  (r) => html`<div
896
- data-trail-receipt
897
- data-receipt-type=${r.type}
898
- style=${styleMap(receiptStyles())}
899
- ><span aria-hidden="true" style=${styleMap(receiptIconStyles())}>✓</span
900
- ><span>${receiptText(r)}</span></div>`
896
+ data-trail-receipt
897
+ data-receipt-type=${r.type}
898
+ style=${styleMap(receiptStyles())}
899
+ ><span aria-hidden="true" style=${styleMap(receiptIconStyles())}>✓</span
900
+ ><span>${receiptText(r)}</span></div>`
901
901
  )}
902
- </div>` : nothing}${replyChips.length > 0 ? html`<div data-trail-reply-chips style=${styleMap(replyChipStripStyles())}>
903
- ${replyChips.map(
902
+ </div>` : nothing}${replyChips.length > 0 ? html`<div data-trail-reply-chips style=${styleMap(replyChipStripStyles())}>
903
+ ${replyChips.map(
904
904
  (reply) => html`<button
905
- type="button"
906
- data-trail-reply-chip
907
- @click=${() => this._onSuggestedReplyClick(reply)}
908
- style=${styleMap(replyChipStyles())}
909
- >${reply}</button>`
905
+ type="button"
906
+ data-trail-reply-chip
907
+ @click=${() => this._onSuggestedReplyClick(reply)}
908
+ style=${styleMap(replyChipStyles())}
909
+ >${reply}</button>`
910
910
  )}
911
- </div>` : nothing}</div>${// Receipt chips that mounted while THIS message was newest render
911
+ </div>` : nothing}</div>${// Receipt chips that mounted while THIS message was newest render
912
912
  // inline right here, so they read as part of the flow and drift up
913
913
  // as later messages append below them.
914
914
  this._renderReceiptStrip(receiptsByAnchor.get(m.id) ?? [])}${// Inline widgets (reco carousel) anchored to this message render
915
915
  // right here in the conversation flow — the whole point of the
916
916
  // chat-inline slot: visible in the trail, not behind the panel.
917
917
  this._renderInlineWidgetStrip(inlineByAnchor.get(m.id) ?? [])}`;
918
- })}
918
+ };
919
+ const committed = streamingTail ? visible.slice(0, -1) : visible;
920
+ const committedChips = repeat(
921
+ committed,
922
+ (m) => m.id,
923
+ (m, i) => renderChip(m, i)
924
+ );
925
+ const streamingChip = streamingTail ? renderChip(lastVisible, visible.length - 1) : nothing;
926
+ return html`
927
+ ${// Single toggle button above the frame. Same DOM node morphs:
928
+ // collapsed → "↑ N more · expand" / "↑ expand" (calls _onExpand)
929
+ // expanded → "⌄ minimize" (calls _onCollapse)
930
+ // Lives OUTSIDE the frame so the absolute blur veil (top:0,
931
+ // z-index:2 inside the frame) doesn't cover it. Shown whenever
932
+ // there's anything in the trail — the user expects this affordance
933
+ // available regardless of how many chips are on screen.
934
+ // When ``forceExpanded`` is set by the host (full-screen panel,
935
+ // etc.), there's nowhere to collapse INTO and the affordance
936
+ // would just confuse the user — suppress it entirely.
937
+ this.messages.length === 0 || this.forceExpanded ? nothing : this.expanded ? html`<button
938
+ type="button"
939
+ data-trail-toggle
940
+ data-trail-collapse
941
+ @click=${this._onCollapse}
942
+ style=${styleMap(moreStyles())}
943
+ >⌄ minimize</button>` : html`<button
944
+ type="button"
945
+ data-trail-toggle
946
+ data-trail-more
947
+ @click=${this._onExpand}
948
+ style=${styleMap(moreStyles())}
949
+ >${hidden > 0 ? html`↑ ${hidden} more · expand` : html`↑ expand`}</button>`}
950
+ <div data-syntro-chat-trail-frame style=${styleMap(frameStyles)}>
951
+ <div
952
+ data-syntro-chat-trail
953
+ style=${styleMap(containerStyles)}
954
+ @scroll=${this._onTrailScroll}
955
+ @wheel=${this._markUserScrollIntent}
956
+ @touchmove=${this._markUserScrollIntent}
957
+ >
958
+ <!-- Committed transcript. Rendered UNCONDITIONALLY, even empty: a live
959
+ region has to be in the DOM before its first child arrives or the
960
+ announcement is dropped (review I3), and the first assistant reply
961
+ of a session is exactly the one that matters. display:contents
962
+ keeps the scroll container's flex layout byte-identical — the
963
+ wrapper is a semantic node only, which is why the bottom-anchor
964
+ rule reaches through it, and past it while it is still empty. -->
965
+ ${this._renderConversationLog(committedChips)}
966
+ ${// The in-flight reply: same slot on screen, outside the log.
967
+ streamingChip}
919
968
  ${// Thinking indicator — fills the gap between the user's
920
969
  // most recent message and the assistant's first token.
921
970
  // Shows when chatSession.inFlight is true AND the last
@@ -992,6 +1041,22 @@ _AdaptiveChatTrail.styles = css`
992
1041
  [data-syntro-chat-trail]:not([data-state="empty"]) > :first-child {
993
1042
  margin-top: auto;
994
1043
  }
1044
+ /* …and THROUGH the conversation-log wrapper. That wrapper is
1045
+ display: contents (a semantic node carrying role=log, see render), so
1046
+ margin on it does nothing — the auto margin has to land on the first
1047
+ real chip inside it or a short thread floats to the top of a tall
1048
+ panel instead of hugging the composer. */
1049
+ [data-syntro-chat-trail]:not([data-state="empty"]) > [data-syntro-chat-log] > :first-child {
1050
+ margin-top: auto;
1051
+ }
1052
+ /* …and PAST it while it is still empty. The wrapper is always rendered (it
1053
+ is a live region, which must pre-exist its content), so with only an
1054
+ in-flight reply on screen the wrapper IS the :first-child and has no box
1055
+ to carry the margin. :empty ignores comment nodes, so Lit's own markers
1056
+ inside the wrapper do not defeat it. */
1057
+ [data-syntro-chat-trail]:not([data-state="empty"]) > [data-syntro-chat-log]:empty + * {
1058
+ margin-top: auto;
1059
+ }
995
1060
  [data-trail-chip] > p:first-child {
996
1061
  margin-top: 0;
997
1062
  }
@@ -1766,4 +1831,4 @@ export {
1766
1831
  AdaptiveChatTrail,
1767
1832
  looksLikeSerializedModelResponse
1768
1833
  };
1769
- //# sourceMappingURL=chunk-EKGGGUI2.js.map
1834
+ //# sourceMappingURL=chunk-7TT7YYML.js.map