@syntrologie/adapt-chatbot 2.8.0-canary.62 → 2.8.0-canary.621
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/AdaptiveChatBar.composer.test.d.ts +2 -0
- package/dist/AdaptiveChatBar.composer.test.d.ts.map +1 -0
- package/dist/AdaptiveChatBar.d.ts +248 -0
- package/dist/AdaptiveChatBar.d.ts.map +1 -0
- package/dist/AdaptiveChatBar.interleave.test.d.ts +9 -0
- package/dist/AdaptiveChatBar.interleave.test.d.ts.map +1 -0
- package/dist/AdaptiveChatBar.js +12 -0
- package/dist/AdaptiveChatBar.js.map +7 -0
- package/dist/AdaptiveChatBarMountable.d.ts +93 -0
- package/dist/AdaptiveChatBarMountable.d.ts.map +1 -0
- package/dist/AdaptiveChatTrail.anchor-scroll.test.d.ts +2 -0
- package/dist/AdaptiveChatTrail.anchor-scroll.test.d.ts.map +1 -0
- package/dist/AdaptiveChatTrail.d.ts +539 -0
- package/dist/AdaptiveChatTrail.d.ts.map +1 -0
- package/dist/AdaptiveChatTrail.follow-bottom.test.d.ts +2 -0
- package/dist/AdaptiveChatTrail.follow-bottom.test.d.ts.map +1 -0
- package/dist/AdaptiveChatTrail.js +12 -0
- package/dist/AdaptiveChatTrail.js.map +7 -0
- package/dist/AdaptiveChatTrail.live-region.test.d.ts +2 -0
- package/dist/AdaptiveChatTrail.live-region.test.d.ts.map +1 -0
- package/dist/AdaptiveChatTrail.test.d.ts +2 -0
- package/dist/AdaptiveChatTrail.test.d.ts.map +1 -0
- package/dist/AdaptiveChipsStrip.d.ts +1156 -0
- package/dist/AdaptiveChipsStrip.d.ts.map +1 -0
- package/dist/AdaptiveChipsStrip.js +11 -0
- package/dist/AdaptiveChipsStrip.js.map +7 -0
- package/dist/AdaptiveChipsStripMountable.d.ts +24 -0
- package/dist/AdaptiveChipsStripMountable.d.ts.map +1 -0
- package/dist/ChatAssistantLit.d.ts +64 -0
- package/dist/ChatAssistantLit.d.ts.map +1 -0
- package/dist/ChatAssistantLit.js +14 -0
- package/dist/ChatAssistantLit.js.map +7 -0
- package/dist/ChatSession.d.ts +275 -0
- package/dist/ChatSession.d.ts.map +1 -0
- package/dist/ChatTransport.d.ts +448 -0
- package/dist/ChatTransport.d.ts.map +1 -0
- package/dist/InlineRegistryWidgetMount.test.d.ts +2 -0
- package/dist/InlineRegistryWidgetMount.test.d.ts.map +1 -0
- package/dist/InlineWidgetLiveMount.test.d.ts +2 -0
- package/dist/InlineWidgetLiveMount.test.d.ts.map +1 -0
- package/dist/LeaveAMessage.leadconnector.integration.test.d.ts +2 -0
- package/dist/LeaveAMessage.leadconnector.integration.test.d.ts.map +1 -0
- package/dist/LeaveAMessageLit.d.ts +329 -0
- package/dist/LeaveAMessageLit.d.ts.map +1 -0
- package/dist/LeaveAMessageLit.test.d.ts +2 -0
- package/dist/LeaveAMessageLit.test.d.ts.map +1 -0
- package/dist/LeaveAMessageMountable.test.d.ts +2 -0
- package/dist/LeaveAMessageMountable.test.d.ts.map +1 -0
- package/dist/NavLinkMountable.d.ts +25 -0
- package/dist/NavLinkMountable.d.ts.map +1 -0
- package/dist/NavLinkMountable.test.d.ts +2 -0
- package/dist/NavLinkMountable.test.d.ts.map +1 -0
- package/dist/TextAnswerMountable.d.ts +17 -0
- package/dist/TextAnswerMountable.d.ts.map +1 -0
- package/dist/TextAnswerMountable.test.d.ts +2 -0
- package/dist/TextAnswerMountable.test.d.ts.map +1 -0
- package/dist/Turnstile.d.ts +101 -0
- package/dist/Turnstile.d.ts.map +1 -0
- package/dist/cdn.d.ts +16 -8
- package/dist/cdn.d.ts.map +1 -1
- package/dist/chunk-4V5L7IRV.js +4711 -0
- package/dist/chunk-4V5L7IRV.js.map +7 -0
- package/dist/chunk-EWPPVPJ4.js +14 -0
- package/dist/chunk-EWPPVPJ4.js.map +7 -0
- package/dist/chunk-HD2NXGYO.js +8445 -0
- package/dist/chunk-HD2NXGYO.js.map +7 -0
- package/dist/chunk-HV52LYST.js +641 -0
- package/dist/chunk-HV52LYST.js.map +7 -0
- package/dist/chunk-LY5AGXVD.js +21 -0
- package/dist/chunk-LY5AGXVD.js.map +7 -0
- package/dist/chunk-T6VEDUXD.js +694 -0
- package/dist/chunk-T6VEDUXD.js.map +7 -0
- package/dist/chunk-UFTSEIQP.js +1877 -0
- package/dist/chunk-UFTSEIQP.js.map +7 -0
- package/dist/chunk-VIRIVP7H.js +898 -0
- package/dist/chunk-VIRIVP7H.js.map +7 -0
- package/dist/chunk-YFZCMLV6.js +4078 -0
- package/dist/chunk-YFZCMLV6.js.map +7 -0
- package/dist/editor-lit.d.ts +37 -0
- package/dist/editor-lit.d.ts.map +1 -0
- package/dist/editor-lit.js +134 -0
- package/dist/editor-lit.js.map +7 -0
- package/dist/editorToken.d.ts +23 -0
- package/dist/editorToken.d.ts.map +1 -0
- package/dist/element-session.d.ts +3 -0
- package/dist/element-session.d.ts.map +1 -0
- package/dist/element-session.js +9 -0
- package/dist/element-session.js.map +7 -0
- package/dist/elements/ActionHandler.d.ts +34 -0
- package/dist/elements/ActionHandler.d.ts.map +1 -0
- package/dist/elements/ElementInstanceStore.d.ts +232 -0
- package/dist/elements/ElementInstanceStore.d.ts.map +1 -0
- package/dist/elements/ElementInstanceStore.test.d.ts +2 -0
- package/dist/elements/ElementInstanceStore.test.d.ts.map +1 -0
- package/dist/elements/ElementSessionController.d.ts +37 -0
- package/dist/elements/ElementSessionController.d.ts.map +1 -0
- package/dist/elements/ElementSessionController.test.d.ts +2 -0
- package/dist/elements/ElementSessionController.test.d.ts.map +1 -0
- package/dist/elements/ElementTypeHandler.d.ts +77 -0
- package/dist/elements/ElementTypeHandler.d.ts.map +1 -0
- package/dist/elements/ItemHandler.d.ts +60 -0
- package/dist/elements/ItemHandler.d.ts.map +1 -0
- package/dist/elements/ItemHandler.test.d.ts +2 -0
- package/dist/elements/ItemHandler.test.d.ts.map +1 -0
- package/dist/elements/TileHandler.d.ts +52 -0
- package/dist/elements/TileHandler.d.ts.map +1 -0
- package/dist/elements/TileHandler.test.d.ts +2 -0
- package/dist/elements/TileHandler.test.d.ts.map +1 -0
- package/dist/elements/__tests__/ElementInstanceStore.recall.test.d.ts +2 -0
- package/dist/elements/__tests__/ElementInstanceStore.recall.test.d.ts.map +1 -0
- package/dist/elements/blockRenderer.d.ts +46 -0
- package/dist/elements/blockRenderer.d.ts.map +1 -0
- package/dist/elements/blockRenderer.test.d.ts +13 -0
- package/dist/elements/blockRenderer.test.d.ts.map +1 -0
- package/dist/elements/blocks.d.ts +58 -0
- package/dist/elements/blocks.d.ts.map +1 -0
- package/dist/elements/createElementStore.d.ts +11 -0
- package/dist/elements/createElementStore.d.ts.map +1 -0
- package/dist/elements/envelope.d.ts +24 -0
- package/dist/elements/envelope.d.ts.map +1 -0
- package/dist/elements/fetcher.d.ts +43 -0
- package/dist/elements/fetcher.d.ts.map +1 -0
- package/dist/elements/index.d.ts +33 -0
- package/dist/elements/index.d.ts.map +1 -0
- package/dist/elements/store-singleton.d.ts +4 -0
- package/dist/elements/store-singleton.d.ts.map +1 -0
- package/dist/elements/tileLifecycleProps.d.ts +13 -0
- package/dist/elements/tileLifecycleProps.d.ts.map +1 -0
- package/dist/elements/types.d.ts +117 -0
- package/dist/elements/types.d.ts.map +1 -0
- package/dist/findSmartCanvas.d.ts +19 -0
- package/dist/findSmartCanvas.d.ts.map +1 -0
- package/dist/hostAgentController.d.ts +65 -0
- package/dist/hostAgentController.d.ts.map +1 -0
- package/dist/hostAgentController.test.d.ts +2 -0
- package/dist/hostAgentController.test.d.ts.map +1 -0
- package/dist/observer/__tests__/allowlist.test.d.ts +9 -0
- package/dist/observer/__tests__/allowlist.test.d.ts.map +1 -0
- package/dist/observer/__tests__/bus-translate.test.d.ts +16 -0
- package/dist/observer/__tests__/bus-translate.test.d.ts.map +1 -0
- package/dist/observer/__tests__/detector-observation-contract.test.d.ts +25 -0
- package/dist/observer/__tests__/detector-observation-contract.test.d.ts.map +1 -0
- package/dist/observer/__tests__/lifecycle.test.d.ts +2 -0
- package/dist/observer/__tests__/lifecycle.test.d.ts.map +1 -0
- package/dist/observer/__tests__/observer-enrich.test.d.ts +10 -0
- package/dist/observer/__tests__/observer-enrich.test.d.ts.map +1 -0
- package/dist/observer/__tests__/observer-isolation.test.d.ts +13 -0
- package/dist/observer/__tests__/observer-isolation.test.d.ts.map +1 -0
- package/dist/observer/__tests__/queue.test.d.ts +2 -0
- package/dist/observer/__tests__/queue.test.d.ts.map +1 -0
- package/dist/observer/__tests__/transport.test.d.ts +2 -0
- package/dist/observer/__tests__/transport.test.d.ts.map +1 -0
- package/dist/observer/allowlist.d.ts +32 -0
- package/dist/observer/allowlist.d.ts.map +1 -0
- package/dist/observer/bus-translate.d.ts +33 -0
- package/dist/observer/bus-translate.d.ts.map +1 -0
- package/dist/observer/index.d.ts +44 -0
- package/dist/observer/index.d.ts.map +1 -0
- package/dist/observer/queue.d.ts +63 -0
- package/dist/observer/queue.d.ts.map +1 -0
- package/dist/observer/transport.d.ts +27 -0
- package/dist/observer/transport.d.ts.map +1 -0
- package/dist/observer/wire.d.ts +54 -0
- package/dist/observer/wire.d.ts.map +1 -0
- package/dist/observer/wire.test.d.ts +2 -0
- package/dist/observer/wire.test.d.ts.map +1 -0
- package/dist/personas.d.ts +30 -0
- package/dist/personas.d.ts.map +1 -0
- package/dist/runtime-lit.d.ts +8 -0
- package/dist/runtime-lit.d.ts.map +1 -0
- package/dist/runtime.d.ts +63 -5
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +4439 -28
- package/dist/runtime.js.map +7 -0
- package/dist/schema.d.ts +3688 -11
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +578 -30
- package/dist/schema.js.map +7 -0
- package/dist/support-chat/__tests__/lc-fetch-stub.d.ts +52 -0
- package/dist/support-chat/__tests__/lc-fetch-stub.d.ts.map +1 -0
- package/dist/support-chat/boot.d.ts +47 -0
- package/dist/support-chat/boot.d.ts.map +1 -0
- package/dist/support-chat/boot.test.d.ts +2 -0
- package/dist/support-chat/boot.test.d.ts.map +1 -0
- package/dist/support-chat/crisp.d.ts +4 -0
- package/dist/support-chat/crisp.d.ts.map +1 -0
- package/dist/support-chat/crisp.test.d.ts +2 -0
- package/dist/support-chat/crisp.test.d.ts.map +1 -0
- package/dist/support-chat/inbox.d.ts +57 -0
- package/dist/support-chat/inbox.d.ts.map +1 -0
- package/dist/support-chat/inbox.test.d.ts +2 -0
- package/dist/support-chat/inbox.test.d.ts.map +1 -0
- package/dist/support-chat/leadconnector.d.ts +13 -0
- package/dist/support-chat/leadconnector.d.ts.map +1 -0
- package/dist/support-chat/leadconnector.test.d.ts +2 -0
- package/dist/support-chat/leadconnector.test.d.ts.map +1 -0
- package/dist/support-chat/registry.d.ts +5 -0
- package/dist/support-chat/registry.d.ts.map +1 -0
- package/dist/support-chat/registry.test.d.ts +2 -0
- package/dist/support-chat/registry.test.d.ts.map +1 -0
- package/dist/support-chat/schema-widgetid.test.d.ts +2 -0
- package/dist/support-chat/schema-widgetid.test.d.ts.map +1 -0
- package/dist/support-chat/types.d.ts +19 -0
- package/dist/support-chat/types.d.ts.map +1 -0
- package/dist/test/setup.d.ts +1 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/types.d.ts +89 -40
- package/dist/types.d.ts.map +1 -1
- package/package.json +49 -24
- package/dist/ChatAssistant.d.ts +0 -18
- package/dist/ChatAssistant.d.ts.map +0 -1
- package/dist/ChatAssistant.js +0 -178
- package/dist/SyntroChatbot.d.ts +0 -14
- package/dist/SyntroChatbot.d.ts.map +0 -1
- package/dist/SyntroChatbot.js +0 -56
- package/dist/actionParser.d.ts +0 -15
- package/dist/actionParser.d.ts.map +0 -1
- package/dist/actionParser.js +0 -52
- package/dist/apiClient.d.ts +0 -23
- package/dist/apiClient.d.ts.map +0 -1
- package/dist/apiClient.js +0 -51
- package/dist/cdn.js +0 -37
- package/dist/editor.d.ts +0 -17
- package/dist/editor.d.ts.map +0 -1
- package/dist/editor.js +0 -36
- package/dist/tools/bridge.d.ts +0 -26
- package/dist/tools/bridge.d.ts.map +0 -1
- package/dist/tools/bridge.js +0 -25
- package/dist/tools/expanders.d.ts +0 -17
- package/dist/tools/expanders.d.ts.map +0 -1
- package/dist/tools/expanders.js +0 -41
- package/dist/tools/index.d.ts +0 -14
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -11
- package/dist/tools/registrations/GetContextTool.d.ts +0 -7
- package/dist/tools/registrations/GetContextTool.d.ts.map +0 -1
- package/dist/tools/registrations/GetContextTool.js +0 -18
- package/dist/tools/registrations/ScrollToTool.d.ts +0 -7
- package/dist/tools/registrations/ScrollToTool.d.ts.map +0 -1
- package/dist/tools/registrations/ScrollToTool.js +0 -38
- package/dist/tools/registrations/ShowOverlayTool.d.ts +0 -7
- package/dist/tools/registrations/ShowOverlayTool.d.ts.map +0 -1
- package/dist/tools/registrations/ShowOverlayTool.js +0 -47
- package/dist/tools/registrations/index.d.ts +0 -19
- package/dist/tools/registrations/index.d.ts.map +0 -1
- package/dist/tools/registrations/index.js +0 -7
- package/dist/types.js +0 -6
- package/dist/useChat.d.ts +0 -25
- package/dist/useChat.d.ts.map +0 -1
- package/dist/useChat.js +0 -106
|
@@ -0,0 +1,1877 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderMarkdown
|
|
3
|
+
} from "./chunk-YFZCMLV6.js";
|
|
4
|
+
import {
|
|
5
|
+
IMAGE_STANDIN_BACKGROUND,
|
|
6
|
+
standinInitial
|
|
7
|
+
} from "./chunk-4V5L7IRV.js";
|
|
8
|
+
|
|
9
|
+
// src/AdaptiveChatTrail.ts
|
|
10
|
+
import { css, html, LitElement, nothing } from "lit";
|
|
11
|
+
import { repeat } from "lit/directives/repeat.js";
|
|
12
|
+
import { styleMap } from "lit/directives/style-map.js";
|
|
13
|
+
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
14
|
+
var DEFAULT_VISIBLE = 3;
|
|
15
|
+
var TRAIL_MAX_WIDTH = "88%";
|
|
16
|
+
var OPACITY_STEP = 0.22;
|
|
17
|
+
var OPACITY_FLOOR = 0.18;
|
|
18
|
+
var Y_DRIFT_PX = -2;
|
|
19
|
+
var TRAIL_BLUR_VEIL_PX = 72;
|
|
20
|
+
var TRAIL_BLUR_MAX_PX = 5;
|
|
21
|
+
var _AdaptiveChatTrail = class _AdaptiveChatTrail extends LitElement {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
this.messages = [];
|
|
25
|
+
this.tileReceipts = [];
|
|
26
|
+
this.inlineWidgets = [];
|
|
27
|
+
this.mountInlineWidget = void 0;
|
|
28
|
+
this.visibleCount = DEFAULT_VISIBLE;
|
|
29
|
+
this.expanded = false;
|
|
30
|
+
this.forceExpanded = false;
|
|
31
|
+
this.hideTopBorder = false;
|
|
32
|
+
this.followBottom = false;
|
|
33
|
+
this.inFlight = false;
|
|
34
|
+
this.thinkingText = "";
|
|
35
|
+
/** Persona identity (FEAT-1784917430) — name + small avatar for assistant
|
|
36
|
+
* turn-start chips and the thinking indicator. Empty = legacy rendering. */
|
|
37
|
+
this.personaName = "";
|
|
38
|
+
this.personaAvatarSm = "";
|
|
39
|
+
this.introSuggestion = void 0;
|
|
40
|
+
/**
|
|
41
|
+
* Pre-conversation phantom assistant message. Rendered only when
|
|
42
|
+
* `messages` is empty. NOT injected into chatSession state — the
|
|
43
|
+
* greeting is configuration, not conversation, so it disappears
|
|
44
|
+
* automatically when the first real message arrives.
|
|
45
|
+
*/
|
|
46
|
+
this.greeting = void 0;
|
|
47
|
+
// ── Anchor-scroll model (FEAT-1783535427, shared contract with
|
|
48
|
+
// <syntro-chat>) — applies to the SCROLLABLE modes (expanded /
|
|
49
|
+
// forceExpanded). The ambient peek strip keeps its bottom pin: it is
|
|
50
|
+
// overflow-hidden (not user-scrollable) and the pin is what keeps the
|
|
51
|
+
// latest line visible under the blur veil.
|
|
52
|
+
/** Follow the bottom on content growth. On by default (history
|
|
53
|
+
* browsing); off from a new user message until the visitor opts back in
|
|
54
|
+
* (pill tap or a user-gesture scroll that reaches the bottom). */
|
|
55
|
+
this._stickToBottom = true;
|
|
56
|
+
/** The user message the current turn is anchored to. */
|
|
57
|
+
this._anchorMsgId = null;
|
|
58
|
+
/** Whether a turn anchor is live (drives expand-mid-turn landing). */
|
|
59
|
+
this._anchorActive = false;
|
|
60
|
+
/** Keep enforcing chip-at-top on updates (late geometry, mode flips)
|
|
61
|
+
* until the user scrolls or opts into following. */
|
|
62
|
+
this._anchorHold = false;
|
|
63
|
+
/** Number of messages whose BOTTOM edge is still below the viewport —
|
|
64
|
+
* messages the visitor hasn't scrolled past yet. Drives the
|
|
65
|
+
* "⌄ N messages" pill and stays accurate as they scroll (one long
|
|
66
|
+
* streaming reply counts as 1 until its end has been read). Reactive. */
|
|
67
|
+
this._messagesBelow = 0;
|
|
68
|
+
/** Timestamp of the last user scroll gesture (wheel/touch): scroll
|
|
69
|
+
* events outside this window are programmatic (our own pin/anchor) and
|
|
70
|
+
* must not re-engage following — anchoring the newest message clamps AT
|
|
71
|
+
* the bottom in any longer conversation. */
|
|
72
|
+
this._lastUserScrollIntentTs = 0;
|
|
73
|
+
this._markUserScrollIntent = () => {
|
|
74
|
+
this._lastUserScrollIntentTs = Date.now();
|
|
75
|
+
};
|
|
76
|
+
this._onTrailScroll = () => {
|
|
77
|
+
const container = this._trailContainer();
|
|
78
|
+
if (!container) return;
|
|
79
|
+
const atBottom = this._isAtBottom(container);
|
|
80
|
+
this._messagesBelow = this._scrollable && !atBottom ? this._countMessagesBelow(container) : 0;
|
|
81
|
+
const isUserDriven = Date.now() - this._lastUserScrollIntentTs < _AdaptiveChatTrail._USER_SCROLL_INTENT_WINDOW_MS;
|
|
82
|
+
if (isUserDriven) {
|
|
83
|
+
this._anchorHold = false;
|
|
84
|
+
this._stickToBottom = atBottom;
|
|
85
|
+
if (atBottom) this._anchorActive = false;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/** Pill tap: jump to the newest content and follow for the rest of the turn. */
|
|
89
|
+
this._onScrollToRecent = () => {
|
|
90
|
+
this._stickToBottom = true;
|
|
91
|
+
this._anchorHold = false;
|
|
92
|
+
this._anchorActive = false;
|
|
93
|
+
requestAnimationFrame(() => {
|
|
94
|
+
const container = this._trailContainer();
|
|
95
|
+
if (!container) return;
|
|
96
|
+
container.scrollTop = container.scrollHeight;
|
|
97
|
+
this._syncContentBelow();
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
// No `createRenderRoot()` override → default open shadow root. CSS
|
|
101
|
+
// custom properties (--sc-tile-text-color, --sc-content-bubble-*,
|
|
102
|
+
// etc.) inherit through the shadow boundary, so canvas-level theming
|
|
103
|
+
// continues to cascade into the chips without the light-DOM hack.
|
|
104
|
+
// See the `static styles` block above for the in-shadow stylesheet.
|
|
105
|
+
this._onExpand = () => {
|
|
106
|
+
this.expanded = true;
|
|
107
|
+
this.dispatchEvent(new CustomEvent("trail-expand", { bubbles: true, composed: true }));
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Approve a pending client tool call. The trail emits a generic
|
|
111
|
+
* event so the host can decide whether to forward to chatSession
|
|
112
|
+
* (the common path) or override. Keeps the trail itself free of
|
|
113
|
+
* direct chatSession coupling — it's a pure view component.
|
|
114
|
+
*/
|
|
115
|
+
this._onToolCallClick = (tc) => {
|
|
116
|
+
if (tc.status !== "pending") return;
|
|
117
|
+
this.dispatchEvent(
|
|
118
|
+
new CustomEvent("trail-toolcall-approved", {
|
|
119
|
+
detail: { toolCallId: tc.id, approved: true },
|
|
120
|
+
bubbles: true,
|
|
121
|
+
composed: true
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
this._onCollapse = () => {
|
|
126
|
+
this.expanded = false;
|
|
127
|
+
this.dispatchEvent(new CustomEvent("trail-collapse", { bubbles: true, composed: true }));
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* instanceId → id of the message that was newest when the receipt first
|
|
131
|
+
* appeared. This is what makes a receipt read like a chat bubble: it renders
|
|
132
|
+
* inline right after its anchor message, so newer messages push it up into
|
|
133
|
+
* history instead of it staying pinned below everything. Assigned once per
|
|
134
|
+
* instanceId (in willUpdate, the only place that sees both `messages` and
|
|
135
|
+
* `tileReceipts` at the moment a tile mounts) and never re-homed — a
|
|
136
|
+
* dismiss/bring-back keeps the chip where it was created. `null` = the tile
|
|
137
|
+
* mounted before the first chat message (canvas replay); those fall to the
|
|
138
|
+
* tail strip (see render).
|
|
139
|
+
*/
|
|
140
|
+
this._receiptAnchors = /* @__PURE__ */ new Map();
|
|
141
|
+
/**
|
|
142
|
+
* instanceId → anchor message id for inline widgets — same mechanism as
|
|
143
|
+
* `_receiptAnchors`. The inline carousel renders right after the message
|
|
144
|
+
* that was newest when it mounted, so it reads like a rich reply and
|
|
145
|
+
* drifts up into history as later messages append.
|
|
146
|
+
*/
|
|
147
|
+
this._inlineAnchors = /* @__PURE__ */ new Map();
|
|
148
|
+
/**
|
|
149
|
+
* instanceId → the cached inline widget entry (render node + props target +
|
|
150
|
+
* optional registry cleanup). Cached so a prop patch (same instanceId, new
|
|
151
|
+
* props) updates the EXISTING element instead of tearing it down and
|
|
152
|
+
* re-creating it — which would reset the carousel's scroll position +
|
|
153
|
+
* internal state. Created lazily the first time we see the widget:
|
|
154
|
+
* `document.createElement(tag)` for a defined tag, or a registry mount for a
|
|
155
|
+
* widget-id. An unresolvable widget caches `null` so the fail-safe renders
|
|
156
|
+
* nothing without re-checking the registry.
|
|
157
|
+
*/
|
|
158
|
+
this._inlineElements = /* @__PURE__ */ new Map();
|
|
159
|
+
this._onTileReceiptClick = (instanceId) => {
|
|
160
|
+
this.dispatchEvent(
|
|
161
|
+
new CustomEvent("tile-receipt-activate", {
|
|
162
|
+
detail: { instanceId },
|
|
163
|
+
bubbles: true,
|
|
164
|
+
composed: true
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
this._onAlertReceiptClick = (instanceId) => {
|
|
169
|
+
this.dispatchEvent(
|
|
170
|
+
new CustomEvent("alert-receipt-activate", {
|
|
171
|
+
detail: { instanceId },
|
|
172
|
+
bubbles: true,
|
|
173
|
+
composed: true
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
this._onIntroSuggestionClick = () => {
|
|
178
|
+
const s = this.introSuggestion;
|
|
179
|
+
if (!s) return;
|
|
180
|
+
this.dispatchEvent(
|
|
181
|
+
new CustomEvent("trail-intro-suggestion", {
|
|
182
|
+
detail: { prompt: s.prompt },
|
|
183
|
+
bubbles: true,
|
|
184
|
+
composed: true
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Visitor tapped a `suggest_replies` inline chip. Emit
|
|
190
|
+
* `trail-suggested-reply` carrying the chip text — the parent
|
|
191
|
+
* (AdaptiveChatBar) re-dispatches it as `chat-message-sent`, the
|
|
192
|
+
* SAME channel the input row uses, so the reply rides the existing
|
|
193
|
+
* send path (mountable → chatSession.send → transport). No fabricated
|
|
194
|
+
* AG-UI events; this is exactly what typing the text and hitting
|
|
195
|
+
* Enter would do. The chips clear on their own once the reply lands
|
|
196
|
+
* and a newer assistant message becomes the latest.
|
|
197
|
+
*/
|
|
198
|
+
this._onSuggestedReplyClick = (text) => {
|
|
199
|
+
const trimmed = text.trim();
|
|
200
|
+
if (!trimmed) return;
|
|
201
|
+
this.dispatchEvent(
|
|
202
|
+
new CustomEvent("trail-suggested-reply", {
|
|
203
|
+
detail: { text: trimmed },
|
|
204
|
+
bubbles: true,
|
|
205
|
+
composed: true
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/** True when `m` opens an assistant run (previous visible message is not
|
|
211
|
+
* assistant) — the persona byline renders once per turn, not per chip. */
|
|
212
|
+
_isAssistantTurnStart(m) {
|
|
213
|
+
if (m.role !== "assistant") return false;
|
|
214
|
+
const i = this.messages.findIndex((x) => x.id === m.id);
|
|
215
|
+
if (i <= 0) return true;
|
|
216
|
+
return this.messages[i - 1].role !== "assistant";
|
|
217
|
+
}
|
|
218
|
+
get _scrollable() {
|
|
219
|
+
return this.expanded || this.forceExpanded;
|
|
220
|
+
}
|
|
221
|
+
_trailContainer() {
|
|
222
|
+
return this.renderRoot.querySelector("[data-syntro-chat-trail]");
|
|
223
|
+
}
|
|
224
|
+
_isAtBottom(container) {
|
|
225
|
+
return container.scrollHeight - container.scrollTop - container.clientHeight <= _AdaptiveChatTrail._AT_BOTTOM_EPSILON_PX;
|
|
226
|
+
}
|
|
227
|
+
/** Count chips whose bottom edge hasn't entered the viewport yet. */
|
|
228
|
+
_countMessagesBelow(container) {
|
|
229
|
+
const viewportBottom = container.getBoundingClientRect().top + container.clientHeight;
|
|
230
|
+
let count = 0;
|
|
231
|
+
for (const chip of this.renderRoot.querySelectorAll("[data-trail-chip]")) {
|
|
232
|
+
if (chip.getBoundingClientRect().bottom > viewportBottom + 4) count += 1;
|
|
233
|
+
}
|
|
234
|
+
return count > 0 ? count : 1;
|
|
235
|
+
}
|
|
236
|
+
_syncContentBelow() {
|
|
237
|
+
const container = this._trailContainer();
|
|
238
|
+
if (!container) return;
|
|
239
|
+
this._messagesBelow = this._scrollable && !this._isAtBottom(container) ? this._countMessagesBelow(container) : 0;
|
|
240
|
+
}
|
|
241
|
+
/** Scroll so the anchored user chip sits at the top of the trail
|
|
242
|
+
* viewport. Idempotent — safe to re-apply on every update while the
|
|
243
|
+
* hold is live (content grows BELOW the anchor, so the target is
|
|
244
|
+
* stable; re-applying also covers late geometry, e.g. expanding
|
|
245
|
+
* mid-turn from the ambient strip). */
|
|
246
|
+
_applyAnchor() {
|
|
247
|
+
requestAnimationFrame(() => {
|
|
248
|
+
const container = this._trailContainer();
|
|
249
|
+
if (!container || this._anchorMsgId === null) return;
|
|
250
|
+
const chip = this.renderRoot.querySelector(
|
|
251
|
+
`[data-trail-chip-id="${String(this._anchorMsgId)}"]`
|
|
252
|
+
);
|
|
253
|
+
if (!chip) return;
|
|
254
|
+
const top = chip.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
|
|
255
|
+
container.scrollTop = top;
|
|
256
|
+
this._syncContentBelow();
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
_pinToBottom() {
|
|
260
|
+
requestAnimationFrame(() => {
|
|
261
|
+
const container = this._trailContainer();
|
|
262
|
+
if (!container) return;
|
|
263
|
+
container.scrollTop = container.scrollHeight;
|
|
264
|
+
this._syncContentBelow();
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Whether to render the thinking-dots chip after the last message.
|
|
269
|
+
* True iff a request is in flight AND the assistant hasn't started
|
|
270
|
+
* streaming a reply yet — once the first delta lands, the streaming
|
|
271
|
+
* chip's caret takes over and the thinking indicator hides so the
|
|
272
|
+
* visitor isn't shown both at once. Also hidden when the last
|
|
273
|
+
* message is an error/system message, because the request that
|
|
274
|
+
* "errored" is already over from the user's perspective.
|
|
275
|
+
*/
|
|
276
|
+
_shouldShowThinking() {
|
|
277
|
+
if (!this.inFlight) return false;
|
|
278
|
+
const last = this.messages[this.messages.length - 1];
|
|
279
|
+
if (!last) return true;
|
|
280
|
+
if (last.role === "assistant" && last.status === "streaming") {
|
|
281
|
+
return (last.text ?? "").trim().length === 0;
|
|
282
|
+
}
|
|
283
|
+
if (last.role === "system" || last.status === "error") return false;
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The "assistant is busy" indicator — ONE status line, never a stack.
|
|
288
|
+
*
|
|
289
|
+
* Two facts can be live at once here: WHO is answering (`personaName`, a
|
|
290
|
+
* static sticky pick, fixed for the whole turn) and WHAT is happening
|
|
291
|
+
* (`thinkingText`, rewritten by `ChatTransport`'s narration cycle every ~4s).
|
|
292
|
+
* They used to render as two independently-gated sibling rows, so a
|
|
293
|
+
* persona-enabled workspace showed "Jordan is typing…" stacked directly
|
|
294
|
+
* above the phase copy — the same claim twice, in two different verbs, and
|
|
295
|
+
* "Jordan is typing…" above three bouncing dots before any phase arrived.
|
|
296
|
+
* `thinkingStatusLine()` merges them into one sentence instead, and the
|
|
297
|
+
* dots remain the sole fallback when neither fact is known.
|
|
298
|
+
*
|
|
299
|
+
* The row's own height must NOT change when the phase text rotates, or the
|
|
300
|
+
* whole trail below it jumps every 4 seconds. `thinkingTextStyles()` pins
|
|
301
|
+
* that with a fixed `line-height` plus `white-space: nowrap` +
|
|
302
|
+
* `text-overflow: ellipsis`, so a short and a long phase paint the same box.
|
|
303
|
+
*/
|
|
304
|
+
_renderThinking() {
|
|
305
|
+
const status = thinkingStatusLine(this.personaName, this.thinkingText);
|
|
306
|
+
return html`<div
|
|
307
|
+
data-trail-thinking
|
|
308
|
+
data-role="assistant"
|
|
309
|
+
data-status="thinking"
|
|
310
|
+
aria-live="off"
|
|
311
|
+
aria-label=${// The accessible name is the sentence on screen. When the dots are the
|
|
312
|
+
// only visual there is no sentence to mirror, so name the state with
|
|
313
|
+
// the SAME verb the visible copy uses ("typing", not "thinking").
|
|
314
|
+
// NOTE: a workspace with a configured `assistantName` but no persona
|
|
315
|
+
// roster still lands on this hardcoded 'Assistant' — `AdaptiveChatTrail`
|
|
316
|
+
// is never handed `assistantName` (see BUG-1786919868 report).
|
|
317
|
+
status || "Assistant is typing\u2026"}
|
|
318
|
+
style=${styleMap(thinkingSlotStyles())}
|
|
319
|
+
>
|
|
320
|
+
${status ? html`<span data-trail-thinking-status style=${styleMap(thinkingStatusRowStyles())}>
|
|
321
|
+
${this.personaAvatarSm ? html`<img
|
|
322
|
+
data-trail-persona-avatar
|
|
323
|
+
src=${this.personaAvatarSm}
|
|
324
|
+
alt=""
|
|
325
|
+
style=${styleMap(personaAvatarStyles())}
|
|
326
|
+
/>` : nothing}<span data-trail-thinking-text style=${styleMap(thinkingTextStyles())}
|
|
327
|
+
>${status}</span
|
|
328
|
+
>
|
|
329
|
+
</span>` : html`<span data-trail-thinking-dots style=${styleMap(thinkingDotsStyles())}>
|
|
330
|
+
<span style=${styleMap(thinkingDotStyles(0))}></span>
|
|
331
|
+
<span style=${styleMap(thinkingDotStyles(1))}></span>
|
|
332
|
+
<span style=${styleMap(thinkingDotStyles(2))}></span>
|
|
333
|
+
</span>`}
|
|
334
|
+
</div>`;
|
|
335
|
+
}
|
|
336
|
+
connectedCallback() {
|
|
337
|
+
super.connectedCallback();
|
|
338
|
+
this.classList.add("ph-no-capture");
|
|
339
|
+
}
|
|
340
|
+
disconnectedCallback() {
|
|
341
|
+
super.disconnectedCallback();
|
|
342
|
+
for (const entry of this._inlineElements.values()) {
|
|
343
|
+
entry?.cleanup?.();
|
|
344
|
+
}
|
|
345
|
+
this._inlineElements.clear();
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The id of the newest message the render will actually PAINT — the only
|
|
349
|
+
* valid anchor target. Anchoring to the raw last message is wrong-by-
|
|
350
|
+
* construction for tool-call turns: mid-turn the newest message is often a
|
|
351
|
+
* whitespace-only assistant shell (some providers emit whitespace text
|
|
352
|
+
* deltas around tool calls) that the empty-bubble filter drops, so an
|
|
353
|
+
* anchor recorded on it can never be in the painted set and the receipt
|
|
354
|
+
* falls to the tail strip forever — resurrecting BUG-1783015943 (seen live
|
|
355
|
+
* on vela desktop, BUG-1783630187). `isSuggestionHost=false` here: a
|
|
356
|
+
* message whose ONLY content is reply chips is too ephemeral to anchor on
|
|
357
|
+
* (the chips clear when the visitor answers), so walk past it too.
|
|
358
|
+
*/
|
|
359
|
+
_newestRenderableMessageId() {
|
|
360
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
361
|
+
const m = this.messages[i];
|
|
362
|
+
if (m.role !== "assistant" || assistantChipRendersContent(m, [])) return m.id;
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Server-authoritative anchor for a restored element: `turnId` is the
|
|
368
|
+
* persisted `mounted_by.turn_id` — the id of the user message that
|
|
369
|
+
* triggered the mounting turn. The element anchors to the first
|
|
370
|
+
* content-bearing assistant message of that turn (where it appeared live:
|
|
371
|
+
* intro text, then the mount, then any follow-up), falling back to the
|
|
372
|
+
* turn's user message when the assistant hasn't produced renderable text.
|
|
373
|
+
* Returns null when `turnId` is absent or not in the transcript (nudge
|
|
374
|
+
* mounts carry a runId) — the caller then uses the legacy
|
|
375
|
+
* newest-renderable anchor. Without this, a reload assigned every element
|
|
376
|
+
* to the newest message of the fully restored transcript, dumping all
|
|
377
|
+
* inline views below the last message (the arrival-timing bug).
|
|
378
|
+
*/
|
|
379
|
+
/**
|
|
380
|
+
* Exact restore anchor: `toolCallId` is the persisted id of the tool call
|
|
381
|
+
* that mounted the element, and the transcript message hosting that call
|
|
382
|
+
* carries it in `toolCalls`. Anchor to the host message itself when it
|
|
383
|
+
* renders content, else walk BACK to the nearest renderable message — a
|
|
384
|
+
* standalone tool-call message persists with empty content and must not
|
|
385
|
+
* strand the element. This survives every turn shape (text→mount,
|
|
386
|
+
* text→text→mount, mount→text); `_anchorFromTurn` is the coarser fallback
|
|
387
|
+
* for elements persisted before tool_call_id existed.
|
|
388
|
+
*/
|
|
389
|
+
_anchorFromToolCall(toolCallId, turnId) {
|
|
390
|
+
if (!toolCallId) return null;
|
|
391
|
+
let start = 0;
|
|
392
|
+
let end = this.messages.length;
|
|
393
|
+
const turnIdx = turnId ? this.messages.findIndex((m) => String(m.id) === turnId) : -1;
|
|
394
|
+
if (turnIdx >= 0) {
|
|
395
|
+
start = turnIdx + 1;
|
|
396
|
+
end = this.messages.findIndex((m, i) => i > turnIdx && m.role === "user");
|
|
397
|
+
if (end < 0) end = this.messages.length;
|
|
398
|
+
}
|
|
399
|
+
let idx = -1;
|
|
400
|
+
for (let i = start; i < end; i++) {
|
|
401
|
+
if ((this.messages[i].toolCalls ?? []).some((tc) => tc.id === toolCallId)) {
|
|
402
|
+
idx = i;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (idx < 0) return null;
|
|
407
|
+
for (let i = idx; i >= 0; i--) {
|
|
408
|
+
const m = this.messages[i];
|
|
409
|
+
if (m.role !== "assistant" || assistantChipRendersContent(m, [])) return m.id;
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
_anchorFromTurn(turnId) {
|
|
414
|
+
if (!turnId) return null;
|
|
415
|
+
const idx = this.messages.findIndex((m) => String(m.id) === turnId);
|
|
416
|
+
if (idx < 0) return null;
|
|
417
|
+
for (let i = idx + 1; i < this.messages.length; i++) {
|
|
418
|
+
const m = this.messages[i];
|
|
419
|
+
if (m.role === "user") break;
|
|
420
|
+
if (m.role === "assistant" && assistantChipRendersContent(m, [])) return m.id;
|
|
421
|
+
}
|
|
422
|
+
return this.messages[idx].id;
|
|
423
|
+
}
|
|
424
|
+
willUpdate(changed) {
|
|
425
|
+
if (changed.has("messages")) {
|
|
426
|
+
const lastId = this._newestRenderableMessageId();
|
|
427
|
+
if (lastId !== null) {
|
|
428
|
+
const receiptTurns = new Map(this.tileReceipts.map((r) => [r.instanceId, r.turnId]));
|
|
429
|
+
const widgetTurns = new Map(this.inlineWidgets.map((w) => [w.instanceId, w.turnId]));
|
|
430
|
+
const receiptCalls = new Map(this.tileReceipts.map((r) => [r.instanceId, r.toolCallId]));
|
|
431
|
+
const widgetCalls = new Map(this.inlineWidgets.map((w) => [w.instanceId, w.toolCallId]));
|
|
432
|
+
for (const [key, anchor] of this._receiptAnchors) {
|
|
433
|
+
if (anchor === null) {
|
|
434
|
+
this._receiptAnchors.set(
|
|
435
|
+
key,
|
|
436
|
+
this._anchorFromToolCall(receiptCalls.get(key), receiptTurns.get(key)) ?? this._anchorFromTurn(receiptTurns.get(key)) ?? lastId
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const [key, anchor] of this._inlineAnchors) {
|
|
441
|
+
if (anchor === null) {
|
|
442
|
+
this._inlineAnchors.set(
|
|
443
|
+
key,
|
|
444
|
+
this._anchorFromToolCall(widgetCalls.get(key), widgetTurns.get(key)) ?? this._anchorFromTurn(widgetTurns.get(key)) ?? lastId
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (changed.has("tileReceipts")) {
|
|
451
|
+
const lastId = this._newestRenderableMessageId();
|
|
452
|
+
const live = /* @__PURE__ */ new Set();
|
|
453
|
+
for (const r of this.tileReceipts) {
|
|
454
|
+
live.add(r.instanceId);
|
|
455
|
+
if (!this._receiptAnchors.has(r.instanceId)) {
|
|
456
|
+
this._receiptAnchors.set(
|
|
457
|
+
r.instanceId,
|
|
458
|
+
this._anchorFromToolCall(r.toolCallId, r.turnId) ?? this._anchorFromTurn(r.turnId) ?? lastId
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
for (const key of this._receiptAnchors.keys()) {
|
|
463
|
+
if (!live.has(key)) this._receiptAnchors.delete(key);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (changed.has("inlineWidgets")) {
|
|
467
|
+
const lastId = this._newestRenderableMessageId();
|
|
468
|
+
const live = /* @__PURE__ */ new Set();
|
|
469
|
+
for (const w of this.inlineWidgets) {
|
|
470
|
+
live.add(w.instanceId);
|
|
471
|
+
if (!this._inlineAnchors.has(w.instanceId)) {
|
|
472
|
+
this._inlineAnchors.set(
|
|
473
|
+
w.instanceId,
|
|
474
|
+
this._anchorFromToolCall(w.toolCallId, w.turnId) ?? this._anchorFromTurn(w.turnId) ?? lastId
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
this._ensureInlineElement(w);
|
|
478
|
+
}
|
|
479
|
+
for (const key of this._inlineAnchors.keys()) {
|
|
480
|
+
if (!live.has(key)) this._inlineAnchors.delete(key);
|
|
481
|
+
}
|
|
482
|
+
for (const key of this._inlineElements.keys()) {
|
|
483
|
+
if (!live.has(key)) {
|
|
484
|
+
this._inlineElements.get(key)?.cleanup?.();
|
|
485
|
+
this._inlineElements.delete(key);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Create (once) or refresh the cached element for an inline widget.
|
|
492
|
+
*
|
|
493
|
+
* Two mount paths, both keeping the chat bundle free of any widget-package
|
|
494
|
+
* import:
|
|
495
|
+
* - Direct tag (self-defining, server-mounted widgets like the reco
|
|
496
|
+
* carousel whose declared `widget` IS its custom-element tag):
|
|
497
|
+
* `document.createElement(tag)`.
|
|
498
|
+
* - Widget-id (admin-template widgets like `adaptive-product:card` or
|
|
499
|
+
* `adaptive-faq:accordion`, whose `widget` is a runtime widget-id, NOT a
|
|
500
|
+
* valid custom-element tag): mount through the runtime widget registry
|
|
501
|
+
* via {@link mountInlineWidget}, which resolves the id to its real
|
|
502
|
+
* element exactly like the deck `SyntroTileCard` path. Without this,
|
|
503
|
+
* `createElement('adaptive-product:card')` yields an inert unknown
|
|
504
|
+
* element and the tile renders permanently blank (the "inline mode
|
|
505
|
+
* shows nothing but the reco" bug).
|
|
506
|
+
*
|
|
507
|
+
* Fail-safe: an id that resolves to neither a defined tag nor a registered
|
|
508
|
+
* widget caches `null` and renders nothing (no broken box).
|
|
509
|
+
*/
|
|
510
|
+
_ensureInlineElement(w) {
|
|
511
|
+
let entry = this._inlineElements.get(w.instanceId);
|
|
512
|
+
if (entry === void 0) {
|
|
513
|
+
if (customElements.get(w.tag)) {
|
|
514
|
+
const el = document.createElement(w.tag);
|
|
515
|
+
el.setAttribute("data-inline-widget-el", "");
|
|
516
|
+
entry = { node: el, propsTarget: el };
|
|
517
|
+
} else if (this.mountInlineWidget) {
|
|
518
|
+
const container = document.createElement("div");
|
|
519
|
+
container.setAttribute("data-inline-widget-el", "");
|
|
520
|
+
const mounted = this.mountInlineWidget(w.tag, container, w.props);
|
|
521
|
+
entry = mounted ? { node: container, propsTarget: mounted.element, cleanup: mounted.cleanup } : null;
|
|
522
|
+
} else {
|
|
523
|
+
entry = null;
|
|
524
|
+
}
|
|
525
|
+
this._inlineElements.set(w.instanceId, entry);
|
|
526
|
+
}
|
|
527
|
+
if (!entry) return;
|
|
528
|
+
entry.propsTarget.props = w.props;
|
|
529
|
+
}
|
|
530
|
+
updated(changed) {
|
|
531
|
+
if (!changed.has("expanded") && !changed.has("forceExpanded") && !changed.has("messages")) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (changed.has("messages") && !this.followBottom) {
|
|
535
|
+
let lastUserIdx = -1;
|
|
536
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
537
|
+
if (this.messages[i].role === "user") {
|
|
538
|
+
lastUserIdx = i;
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const lastUser = lastUserIdx >= 0 ? this.messages[lastUserIdx] : void 0;
|
|
543
|
+
const isLiveTurn = lastUser !== void 0 && this.messages.slice(lastUserIdx + 1).every((m) => m.role !== "user" && m.status === "streaming");
|
|
544
|
+
if (lastUser && isLiveTurn && lastUser.id !== this._anchorMsgId) {
|
|
545
|
+
this._anchorMsgId = lastUser.id;
|
|
546
|
+
this._anchorActive = true;
|
|
547
|
+
this._anchorHold = true;
|
|
548
|
+
this._stickToBottom = false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (!this._scrollable) {
|
|
552
|
+
this._pinToBottom();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (this._anchorActive && this._anchorHold) {
|
|
556
|
+
this._applyAnchor();
|
|
557
|
+
} else if (this._stickToBottom) {
|
|
558
|
+
this._pinToBottom();
|
|
559
|
+
} else {
|
|
560
|
+
requestAnimationFrame(() => this._syncContentBelow());
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/** Render a set of inline widgets (reco carousel etc.) as
|
|
564
|
+
* `[data-trail-widget]` blocks, anchored to a message position like
|
|
565
|
+
* receipts. Each block hosts the cached custom element for that
|
|
566
|
+
* instanceId — a defined tag mounts inline; an undefined tag renders
|
|
567
|
+
* nothing (fail-safe). Rendered at every trail site (inline per anchor,
|
|
568
|
+
* tail, empty-state) so placement can't diverge. */
|
|
569
|
+
_renderInlineWidgetStrip(list) {
|
|
570
|
+
if (list.length === 0) return nothing;
|
|
571
|
+
const blocks = list.map((w) => {
|
|
572
|
+
const entry = this._inlineElements.get(w.instanceId);
|
|
573
|
+
if (!entry) return nothing;
|
|
574
|
+
return html`<div
|
|
575
|
+
data-trail-widget
|
|
576
|
+
data-instance-id=${w.instanceId}
|
|
577
|
+
style=${styleMap(inlineWidgetBlockStyles())}
|
|
578
|
+
>${entry.node}</div>`;
|
|
579
|
+
}).filter((b) => b !== nothing);
|
|
580
|
+
if (blocks.length === 0) return nothing;
|
|
581
|
+
return html`${blocks}`;
|
|
582
|
+
}
|
|
583
|
+
/** A row of receipt chips, used at every trail render site (inline per
|
|
584
|
+
* anchor message, the tail fallback, and the empty-state branches) so the
|
|
585
|
+
* markup + styling can never diverge. Returns `nothing` for an empty list. */
|
|
586
|
+
_renderReceiptStrip(list) {
|
|
587
|
+
if (list.length === 0) return nothing;
|
|
588
|
+
return html`<div data-trail-tile-receipts style=${styleMap(tileReceiptStripStyles())}>
|
|
589
|
+
${list.map((tr) => this._renderReceiptChip(tr))}
|
|
590
|
+
</div>`;
|
|
591
|
+
}
|
|
592
|
+
/** One receipt chip — a single, unified, single-line design for both kinds
|
|
593
|
+
* (nav "alert" + mounted "tile"). Same container/title/action style so they
|
|
594
|
+
* read as one chip family; the icon + action label carry the kind. No
|
|
595
|
+
* subtitle: the old "Brought up earlier" line was hardcoded (wrong for
|
|
596
|
+
* same-turn mounts) and made the chip taller — dropping it is the "shorter"
|
|
597
|
+
* chip. Rendered at every trail site so the kinds can't diverge. */
|
|
598
|
+
_renderReceiptChip(tr) {
|
|
599
|
+
if (tr.kind === "alert") {
|
|
600
|
+
return html`<button
|
|
601
|
+
type="button"
|
|
602
|
+
data-trail-alert-receipt
|
|
603
|
+
data-instance-id=${tr.instanceId}
|
|
604
|
+
@click=${() => this._onAlertReceiptClick(tr.instanceId)}
|
|
605
|
+
style=${styleMap(tileReceiptChipStyles())}
|
|
606
|
+
>
|
|
607
|
+
<span style=${styleMap(alertReceiptIconStyles())} aria-hidden="true">⚡</span>
|
|
608
|
+
<span style=${styleMap(receiptChipTitleStyles("alert"))}>${tr.title}</span>
|
|
609
|
+
<span style=${styleMap(tileReceiptActionStyles(false))}>Go →</span>
|
|
610
|
+
</button>`;
|
|
611
|
+
}
|
|
612
|
+
return html`<button
|
|
613
|
+
type="button"
|
|
614
|
+
data-trail-tile-receipt
|
|
615
|
+
data-instance-id=${tr.instanceId}
|
|
616
|
+
@click=${() => this._onTileReceiptClick(tr.instanceId)}
|
|
617
|
+
style=${styleMap(tileReceiptChipStyles())}
|
|
618
|
+
>
|
|
619
|
+
<span style=${styleMap(tileReceiptGlyphStyles())} aria-hidden="true"
|
|
620
|
+
>${tr.imageUrl ? (
|
|
621
|
+
// COI-037: a tiny product thumbnail — the decorative stack glyph
|
|
622
|
+
// read as a BROKEN image next to a product name (tester report).
|
|
623
|
+
html`<img
|
|
624
|
+
src=${tr.imageUrl}
|
|
625
|
+
alt=""
|
|
626
|
+
loading="lazy"
|
|
627
|
+
style=${styleMap(tileReceiptThumbStyles())}
|
|
628
|
+
/>`
|
|
629
|
+
) : (
|
|
630
|
+
// No image (a FAQ, a product the store has no photo for): the
|
|
631
|
+
// title's monogram on the brand stand-in, the same treatment the
|
|
632
|
+
// product card gives an imageless product (adaptive-product
|
|
633
|
+
// imageStandin.ts). The old two-layer stack glyph used the
|
|
634
|
+
// accent token, which on light-accent brands (iGlow) painted a
|
|
635
|
+
// pale empty square.
|
|
636
|
+
html`<span data-trail-receipt-monogram style=${styleMap(tileReceiptMonogramStyles())}
|
|
637
|
+
>${standinInitial(tr.title) ?? "\u2022"}</span
|
|
638
|
+
>`
|
|
639
|
+
)}</span
|
|
640
|
+
>
|
|
641
|
+
<span style=${styleMap(receiptChipTitleStyles("tile"))}>${tr.title}</span>
|
|
642
|
+
<span style=${styleMap(tileReceiptActionStyles(tr.dismissed))}
|
|
643
|
+
>${tr.dismissed ? "Bring back \u21BA" : "View \u25B8"}</span
|
|
644
|
+
>
|
|
645
|
+
</button>`;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* The conversation live region.
|
|
649
|
+
*
|
|
650
|
+
* Always rendered, in EVERY branch of `render()` — including the ones that
|
|
651
|
+
* paint no transcript at all. A live region must already be in the DOM when
|
|
652
|
+
* its first child arrives; one created in the same mutation as its content
|
|
653
|
+
* is not reliably announced by NVDA or JAWS (adversarial review I3), and the
|
|
654
|
+
* content that would be lost is the assistant's first reply of the session.
|
|
655
|
+
*
|
|
656
|
+
* `display: contents` means it contributes no box and no layout anywhere it
|
|
657
|
+
* is mounted, so it is safe to place in an empty-state hero or in the flex
|
|
658
|
+
* scroll container alike. In the empty states it is rendered LAST so it can
|
|
659
|
+
* never become the `:first-child` any layout rule targets.
|
|
660
|
+
*/
|
|
661
|
+
_renderConversationLog(children) {
|
|
662
|
+
return html`<div
|
|
663
|
+
data-syntro-chat-log
|
|
664
|
+
role="log"
|
|
665
|
+
aria-label="Conversation"
|
|
666
|
+
style="display:contents"
|
|
667
|
+
>${children}</div>`;
|
|
668
|
+
}
|
|
669
|
+
render() {
|
|
670
|
+
if (this.messages.length === 0 && !this.greeting && !this.introSuggestion && this.tileReceipts.length === 0 && this.inlineWidgets.length === 0)
|
|
671
|
+
return this._renderConversationLog(nothing);
|
|
672
|
+
if (this.messages.length === 0 && (this.greeting || this.introSuggestion)) {
|
|
673
|
+
return html`<div data-syntro-chat-trail data-state="empty" style=${styleMap({
|
|
674
|
+
display: "flex",
|
|
675
|
+
flexDirection: "column",
|
|
676
|
+
alignItems: "center",
|
|
677
|
+
justifyContent: "center",
|
|
678
|
+
textAlign: "center",
|
|
679
|
+
gap: "12px",
|
|
680
|
+
flex: "1 1 auto",
|
|
681
|
+
width: "100%",
|
|
682
|
+
padding: "24px 16px",
|
|
683
|
+
pointerEvents: "auto"
|
|
684
|
+
})}>
|
|
685
|
+
<div data-trail-empty-icon style=${styleMap(emptyIconStyles())} aria-hidden="true">
|
|
686
|
+
${this.personaAvatarSm ? html`<img
|
|
687
|
+
data-trail-empty-avatar
|
|
688
|
+
src=${this.personaAvatarSm}
|
|
689
|
+
alt=""
|
|
690
|
+
style="width:44px;height:44px;border-radius:9999px;object-fit:cover;display:block;"
|
|
691
|
+
/>` : "\u2726"}
|
|
692
|
+
</div>
|
|
693
|
+
${this.greeting ? html`<h2
|
|
694
|
+
data-trail-empty-title
|
|
695
|
+
style=${styleMap(emptyTitleStyles())}
|
|
696
|
+
>${unsafeHTML(renderMarkdown(stripTrailingWhitespace(this.greeting)))}</h2>` : nothing}
|
|
697
|
+
${this.introSuggestion ? html`<button
|
|
698
|
+
type="button"
|
|
699
|
+
data-trail-intro-suggestion
|
|
700
|
+
@click=${this._onIntroSuggestionClick}
|
|
701
|
+
style=${styleMap(introSuggestionStyles())}
|
|
702
|
+
>${this.introSuggestion.label}</button>` : nothing}
|
|
703
|
+
${this._renderReceiptStrip(this.tileReceipts)}
|
|
704
|
+
${this._renderInlineWidgetStrip(this.inlineWidgets)}
|
|
705
|
+
${this._renderConversationLog(nothing)}
|
|
706
|
+
</div>`;
|
|
707
|
+
}
|
|
708
|
+
if (this.messages.length === 0 && (this.tileReceipts.length > 0 || this.inlineWidgets.length > 0)) {
|
|
709
|
+
return html`<div data-syntro-chat-trail data-state="empty" style=${styleMap({
|
|
710
|
+
display: "flex",
|
|
711
|
+
flexDirection: "column",
|
|
712
|
+
alignItems: "flex-start",
|
|
713
|
+
gap: "6px",
|
|
714
|
+
width: "100%",
|
|
715
|
+
padding: "8px 0",
|
|
716
|
+
pointerEvents: "auto"
|
|
717
|
+
})}>
|
|
718
|
+
${this._renderReceiptStrip(this.tileReceipts)}
|
|
719
|
+
${this._renderInlineWidgetStrip(this.inlineWidgets)}
|
|
720
|
+
${this._renderConversationLog(nothing)}
|
|
721
|
+
</div>`;
|
|
722
|
+
}
|
|
723
|
+
let turnReplies = [];
|
|
724
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
725
|
+
const m = this.messages[i];
|
|
726
|
+
if (m.role === "user") break;
|
|
727
|
+
if (m.role === "assistant") {
|
|
728
|
+
const replies = extractSuggestedReplies(m.toolCalls);
|
|
729
|
+
if (replies.length > 0) {
|
|
730
|
+
turnReplies = replies;
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
let suggestionHostId = null;
|
|
736
|
+
if (turnReplies.length > 0) {
|
|
737
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
738
|
+
const m = this.messages[i];
|
|
739
|
+
if (m.role === "user") break;
|
|
740
|
+
if (m.role === "assistant" && (assistantChipRendersContent(m, []) || extractSuggestedReplies(m.toolCalls).length > 0)) {
|
|
741
|
+
suggestionHostId = m.id;
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const renderable = this.messages.filter(
|
|
747
|
+
(m) => m.role !== "assistant" || assistantChipRendersContent(m, m.id === suggestionHostId ? turnReplies : [])
|
|
748
|
+
);
|
|
749
|
+
const visible = this.expanded || this.forceExpanded ? renderable : renderable.slice(-this.visibleCount);
|
|
750
|
+
const hidden = renderable.length - visible.length;
|
|
751
|
+
const visibleIds = new Set(visible.map((m) => m.id));
|
|
752
|
+
const receiptsByAnchor = /* @__PURE__ */ new Map();
|
|
753
|
+
const tailReceipts = [];
|
|
754
|
+
for (const r of this.tileReceipts) {
|
|
755
|
+
const anchor = this._receiptAnchors.get(r.instanceId) ?? null;
|
|
756
|
+
if (anchor !== null && visibleIds.has(anchor)) {
|
|
757
|
+
const list = receiptsByAnchor.get(anchor);
|
|
758
|
+
if (list) list.push(r);
|
|
759
|
+
else receiptsByAnchor.set(anchor, [r]);
|
|
760
|
+
} else {
|
|
761
|
+
tailReceipts.push(r);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const inlineByAnchor = /* @__PURE__ */ new Map();
|
|
765
|
+
const tailInlineWidgets = [];
|
|
766
|
+
for (const w of this.inlineWidgets) {
|
|
767
|
+
const anchor = this._inlineAnchors.get(w.instanceId) ?? null;
|
|
768
|
+
if (anchor !== null && visibleIds.has(anchor)) {
|
|
769
|
+
const list = inlineByAnchor.get(anchor);
|
|
770
|
+
if (list) list.push(w);
|
|
771
|
+
else inlineByAnchor.set(anchor, [w]);
|
|
772
|
+
} else {
|
|
773
|
+
tailInlineWidgets.push(w);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const baseStyles = {
|
|
777
|
+
display: "flex",
|
|
778
|
+
flexDirection: "column",
|
|
779
|
+
gap: "6px",
|
|
780
|
+
// NO `justifyContent: flex-end` here — combined with `overflow: auto`
|
|
781
|
+
// it's a known cross-browser bug: oversized flex children get
|
|
782
|
+
// pushed ABOVE the container without contributing to scrollHeight,
|
|
783
|
+
// so the scrollbar never appears. We rely on `:first-child {
|
|
784
|
+
// margin-top: auto }` (injected as global CSS) to push short
|
|
785
|
+
// content to the bottom of the box while leaving overflow handling
|
|
786
|
+
// to the native scroll container.
|
|
787
|
+
width: "100%",
|
|
788
|
+
// The trail itself has no chrome — chips sit on the host page bg.
|
|
789
|
+
padding: "0",
|
|
790
|
+
pointerEvents: "auto"
|
|
791
|
+
};
|
|
792
|
+
const modeStyles = this.forceExpanded ? {
|
|
793
|
+
// Full-screen host: fill all available vertical space and
|
|
794
|
+
// scroll the overflow. ``flex: 1 1 auto`` (basis AUTO, not 0) —
|
|
795
|
+
// in a DEFINITE-height parent (full-screen panel) grow:1 still
|
|
796
|
+
// fills all free space and margin-top:auto bottom-anchors, so
|
|
797
|
+
// fill behavior is unchanged; but basis auto also lets the trail
|
|
798
|
+
// report its CONTENT height as its natural size, so in an
|
|
799
|
+
// INDEFINITE-height parent (velvet's shrink-wrap bottom sheet)
|
|
800
|
+
// the trail contributes real content height instead of collapsing
|
|
801
|
+
// to 0 — which is what lets the sheet hug its content with no void.
|
|
802
|
+
// shrink:1 + min-height:0 still lets the trail shrink first when
|
|
803
|
+
// the parent runs out of room; the input row is absolute-pinned
|
|
804
|
+
// (never pushed off) and the header shrinks after the trail.
|
|
805
|
+
flex: "1 1 auto",
|
|
806
|
+
minHeight: "0",
|
|
807
|
+
overflowY: "auto",
|
|
808
|
+
// ``contain`` prevents scroll from bubbling out to the host
|
|
809
|
+
// page when the trail hits top or bottom. Required for the
|
|
810
|
+
// panel-scroll-leak regression test in
|
|
811
|
+
// SyntroBottomSheet.test.ts.
|
|
812
|
+
overscrollBehavior: "contain",
|
|
813
|
+
paddingTop: "8px",
|
|
814
|
+
...this.hideTopBorder ? {} : {
|
|
815
|
+
borderTop: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))"
|
|
816
|
+
},
|
|
817
|
+
scrollBehavior: "smooth",
|
|
818
|
+
scrollbarWidth: "thin"
|
|
819
|
+
} : this.expanded ? {
|
|
820
|
+
maxHeight: "min(320px, 40vh)",
|
|
821
|
+
overflowY: "auto",
|
|
822
|
+
paddingTop: "8px",
|
|
823
|
+
...this.hideTopBorder ? {} : {
|
|
824
|
+
borderTop: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))"
|
|
825
|
+
},
|
|
826
|
+
scrollBehavior: "smooth",
|
|
827
|
+
scrollbarWidth: "thin"
|
|
828
|
+
} : {
|
|
829
|
+
// Collapsed mode = ambient peek surface. Never a scrollbar:
|
|
830
|
+
// long messages slide up behind the blur veil instead of
|
|
831
|
+
// exposing a scrollbar. The "↑ N more · expand" affordance
|
|
832
|
+
// is the only entry into history; clicking it switches to
|
|
833
|
+
// expanded mode where scroll IS allowed.
|
|
834
|
+
// Height kept short so the trail reads as a peek strip above
|
|
835
|
+
// the chat bar, not a panel — long single replies fade into
|
|
836
|
+
// the blur veil at the top, user expands to read the rest.
|
|
837
|
+
maxHeight: "min(140px, 22vh)",
|
|
838
|
+
overflow: "hidden"
|
|
839
|
+
};
|
|
840
|
+
const containerStyles = { ...baseStyles, ...modeStyles };
|
|
841
|
+
const frameStyles = this.forceExpanded ? {
|
|
842
|
+
position: "relative",
|
|
843
|
+
width: "100%",
|
|
844
|
+
display: "flex",
|
|
845
|
+
flexDirection: "column",
|
|
846
|
+
// basis AUTO (see the scroller's modeStyles note): fills a definite
|
|
847
|
+
// parent, contributes content height in an indefinite one (velvet
|
|
848
|
+
// shrink-wrap). Every fill layer in the chain must be basis-auto or
|
|
849
|
+
// the min-content of a basis-0 layer collapses the whole chain to 0.
|
|
850
|
+
flex: "1 1 auto",
|
|
851
|
+
minHeight: "0"
|
|
852
|
+
} : {
|
|
853
|
+
position: "relative",
|
|
854
|
+
width: "100%"
|
|
855
|
+
};
|
|
856
|
+
const isStreamingMsg = (m) => m.role === "assistant" && m.status === "streaming";
|
|
857
|
+
const lastVisible = visible[visible.length - 1];
|
|
858
|
+
const streamingTail = lastVisible !== void 0 && isStreamingMsg(lastVisible);
|
|
859
|
+
const strandedStreaming = (m, i) => isStreamingMsg(m) && !(streamingTail && i === visible.length - 1);
|
|
860
|
+
const renderChip = (m, i) => {
|
|
861
|
+
const pos = this.expanded || this.forceExpanded ? 0 : visible.length - 1 - i;
|
|
862
|
+
const state = chipVisualState(m);
|
|
863
|
+
const isStreaming = state.kind === "streaming";
|
|
864
|
+
const {
|
|
865
|
+
pendingToolCalls: toolCalls,
|
|
866
|
+
receipts,
|
|
867
|
+
replyChips
|
|
868
|
+
} = computeAssistantChipParts(m, m.id === suggestionHostId ? turnReplies : []);
|
|
869
|
+
const renderedText = m.role === "assistant" ? (
|
|
870
|
+
// Never paint a degenerate serialized-response dump, even if the
|
|
871
|
+
// turn also carries a legit part (receipt/chip) that keeps it in
|
|
872
|
+
// the renderable set. Mirrors the filter's drop signal.
|
|
873
|
+
looksLikeSerializedModelResponse(m.text ?? "") ? nothing : html`${unsafeHTML(renderMarkdown(stripTrailingWhitespace(m.text)))}`
|
|
874
|
+
) : html`${m.text}`;
|
|
875
|
+
const chipStyle = isStreaming ? {
|
|
876
|
+
...chipStyles(state, pos),
|
|
877
|
+
animation: "syntro-trail-chip-enter 220ms cubic-bezier(0.22, 1, 0.36, 1) both"
|
|
878
|
+
} : chipStyles(state, pos);
|
|
879
|
+
const personaByline = this.personaName && this._isAssistantTurnStart(m) ? html`<div data-trail-persona-byline style=${styleMap(personaBylineStyles())}>
|
|
880
|
+
${this.personaAvatarSm ? html`<img
|
|
881
|
+
data-trail-persona-avatar
|
|
882
|
+
src=${this.personaAvatarSm}
|
|
883
|
+
alt=""
|
|
884
|
+
style=${styleMap(personaAvatarStyles())}
|
|
885
|
+
/>` : nothing}<span>${this.personaName}</span>
|
|
886
|
+
</div>` : nothing;
|
|
887
|
+
return html`<div
|
|
888
|
+
data-trail-chip
|
|
889
|
+
data-trail-chip-id=${String(m.id)}
|
|
890
|
+
data-role=${m.role}
|
|
891
|
+
data-status=${m.status ?? "complete"}
|
|
892
|
+
aria-hidden=${strandedStreaming(m, i) ? "true" : nothing}
|
|
893
|
+
?inert=${strandedStreaming(m, i)}
|
|
894
|
+
style=${styleMap(chipStyle)}
|
|
895
|
+
>${personaByline}${renderedText}${isStreaming ? html`<span
|
|
896
|
+
data-trail-caret
|
|
897
|
+
aria-hidden="true"
|
|
898
|
+
style=${styleMap(caretStyles())}
|
|
899
|
+
></span>` : nothing}${toolCalls.length > 0 ? html`<div data-trail-toolcalls style=${styleMap(toolCallStripStyles())}>
|
|
900
|
+
${toolCalls.map(
|
|
901
|
+
(tc) => html`<button
|
|
902
|
+
type="button"
|
|
903
|
+
data-trail-toolcall
|
|
904
|
+
data-tool-id=${tc.id}
|
|
905
|
+
data-tool-status=${tc.status}
|
|
906
|
+
@click=${() => this._onToolCallClick(tc)}
|
|
907
|
+
style=${styleMap(pendingToolCallChipStyles())}
|
|
908
|
+
title="${tc.name} · ${tc.status}"
|
|
909
|
+
>? ${tc.name}</button>`
|
|
910
|
+
)}
|
|
911
|
+
</div>` : nothing}${receipts.length > 0 ? html`<div data-trail-receipts style=${styleMap(receiptStripStyles())}>
|
|
912
|
+
${receipts.map(
|
|
913
|
+
(r) => html`<div
|
|
914
|
+
data-trail-receipt
|
|
915
|
+
data-receipt-type=${r.type}
|
|
916
|
+
style=${styleMap(receiptStyles())}
|
|
917
|
+
><span aria-hidden="true" style=${styleMap(receiptIconStyles())}>✓</span
|
|
918
|
+
><span>${receiptText(r)}</span></div>`
|
|
919
|
+
)}
|
|
920
|
+
</div>` : nothing}${replyChips.length > 0 ? html`<div data-trail-reply-chips style=${styleMap(replyChipStripStyles())}>
|
|
921
|
+
${replyChips.map(
|
|
922
|
+
(reply) => html`<button
|
|
923
|
+
type="button"
|
|
924
|
+
data-trail-reply-chip
|
|
925
|
+
@click=${() => this._onSuggestedReplyClick(reply)}
|
|
926
|
+
style=${styleMap(replyChipStyles())}
|
|
927
|
+
>${reply}</button>`
|
|
928
|
+
)}
|
|
929
|
+
</div>` : nothing}</div>${// Receipt chips that mounted while THIS message was newest render
|
|
930
|
+
// inline right here, so they read as part of the flow and drift up
|
|
931
|
+
// as later messages append below them.
|
|
932
|
+
this._renderReceiptStrip(receiptsByAnchor.get(m.id) ?? [])}${// Inline widgets (reco carousel) anchored to this message render
|
|
933
|
+
// right here in the conversation flow — the whole point of the
|
|
934
|
+
// chat-inline slot: visible in the trail, not behind the panel.
|
|
935
|
+
this._renderInlineWidgetStrip(inlineByAnchor.get(m.id) ?? [])}`;
|
|
936
|
+
};
|
|
937
|
+
const committed = streamingTail ? visible.slice(0, -1) : visible;
|
|
938
|
+
const committedChips = repeat(
|
|
939
|
+
committed,
|
|
940
|
+
(m) => m.id,
|
|
941
|
+
(m, i) => renderChip(m, i)
|
|
942
|
+
);
|
|
943
|
+
const streamingChip = streamingTail ? renderChip(lastVisible, visible.length - 1) : nothing;
|
|
944
|
+
return html`
|
|
945
|
+
${// Single toggle button above the frame. Same DOM node morphs:
|
|
946
|
+
// collapsed → "↑ N more · expand" / "↑ expand" (calls _onExpand)
|
|
947
|
+
// expanded → "⌄ minimize" (calls _onCollapse)
|
|
948
|
+
// Lives OUTSIDE the frame so the absolute blur veil (top:0,
|
|
949
|
+
// z-index:2 inside the frame) doesn't cover it. Shown whenever
|
|
950
|
+
// there's anything in the trail — the user expects this affordance
|
|
951
|
+
// available regardless of how many chips are on screen.
|
|
952
|
+
// When ``forceExpanded`` is set by the host (full-screen panel,
|
|
953
|
+
// etc.), there's nowhere to collapse INTO and the affordance
|
|
954
|
+
// would just confuse the user — suppress it entirely.
|
|
955
|
+
this.messages.length === 0 || this.forceExpanded ? nothing : this.expanded ? html`<button
|
|
956
|
+
type="button"
|
|
957
|
+
data-trail-toggle
|
|
958
|
+
data-trail-collapse
|
|
959
|
+
@click=${this._onCollapse}
|
|
960
|
+
style=${styleMap(moreStyles())}
|
|
961
|
+
>⌄ minimize</button>` : html`<button
|
|
962
|
+
type="button"
|
|
963
|
+
data-trail-toggle
|
|
964
|
+
data-trail-more
|
|
965
|
+
@click=${this._onExpand}
|
|
966
|
+
style=${styleMap(moreStyles())}
|
|
967
|
+
>${hidden > 0 ? html`↑ ${hidden} more · expand` : html`↑ expand`}</button>`}
|
|
968
|
+
<div data-syntro-chat-trail-frame style=${styleMap(frameStyles)}>
|
|
969
|
+
<div
|
|
970
|
+
data-syntro-chat-trail
|
|
971
|
+
style=${styleMap(containerStyles)}
|
|
972
|
+
@scroll=${this._onTrailScroll}
|
|
973
|
+
@wheel=${this._markUserScrollIntent}
|
|
974
|
+
@touchmove=${this._markUserScrollIntent}
|
|
975
|
+
>
|
|
976
|
+
<!-- Committed transcript. Rendered UNCONDITIONALLY, even empty: a live
|
|
977
|
+
region has to be in the DOM before its first child arrives or the
|
|
978
|
+
announcement is dropped (review I3), and the first assistant reply
|
|
979
|
+
of a session is exactly the one that matters. display:contents
|
|
980
|
+
keeps the scroll container's flex layout byte-identical — the
|
|
981
|
+
wrapper is a semantic node only, which is why the bottom-anchor
|
|
982
|
+
rule reaches through it, and past it while it is still empty. -->
|
|
983
|
+
${this._renderConversationLog(committedChips)}
|
|
984
|
+
${// The in-flight reply: same slot on screen, outside the log.
|
|
985
|
+
streamingChip}
|
|
986
|
+
${// Thinking indicator — fills the gap between the user's
|
|
987
|
+
// most recent message and the assistant's first token.
|
|
988
|
+
// Shows when chatSession.inFlight is true AND the last
|
|
989
|
+
// message is NOT a streaming assistant message (the
|
|
990
|
+
// assistant's own caret takes over once tokens arrive).
|
|
991
|
+
// Styled as an assistant chip with three bouncing dots
|
|
992
|
+
// instead of text, so the thread reads naturally — the
|
|
993
|
+
// visitor sees an "assistant" bubble that's clearly busy.
|
|
994
|
+
this._shouldShowThinking() ? this._renderThinking() : nothing}
|
|
995
|
+
${// Tail strip: receipts with no in-slice anchor (mounted pre-chat, or
|
|
996
|
+
// their anchor message scrolled out of the ambient view). Keeps them
|
|
997
|
+
// reachable at the bottom — the old always-pinned position — rather
|
|
998
|
+
// than dropping them. In expanded/maximized mode every message is
|
|
999
|
+
// painted, so this is usually empty and receipts sit inline instead.
|
|
1000
|
+
this._renderReceiptStrip(tailReceipts)}
|
|
1001
|
+
${// Inline widgets with no in-slice anchor (mounted pre-chat, or their
|
|
1002
|
+
// anchor scrolled out of the ambient view) render at the tail.
|
|
1003
|
+
this._renderInlineWidgetStrip(tailInlineWidgets)}
|
|
1004
|
+
</div>
|
|
1005
|
+
${// The blur veil only fires in collapsed (ambient) mode.
|
|
1006
|
+
// Expanded = the user explicitly asked for history; blurring
|
|
1007
|
+
// the top of the panel would hide content (including the
|
|
1008
|
+
// minimize button just above the trail's top edge) the user
|
|
1009
|
+
// came here to see.
|
|
1010
|
+
this.expanded || this.forceExpanded ? nothing : html`<div
|
|
1011
|
+
data-trail-blur-veil
|
|
1012
|
+
aria-hidden="true"
|
|
1013
|
+
style=${styleMap(blurVeilStyles())}
|
|
1014
|
+
></div>`}
|
|
1015
|
+
${// "Scroll to most recent" pill (FEAT-1783535427): shown only in
|
|
1016
|
+
// the scrollable modes when content extends below the viewport —
|
|
1017
|
+
// the visitor is reading the reply top-down and this is the
|
|
1018
|
+
// one-tap opt-in back to the newest content. Absolute inside the
|
|
1019
|
+
// positioning frame so it floats over the scroller's bottom edge.
|
|
1020
|
+
this._scrollable && this._messagesBelow > 0 ? html`<button
|
|
1021
|
+
type="button"
|
|
1022
|
+
data-trail-scroll-recent
|
|
1023
|
+
aria-label="Scroll to most recent — ${this._messagesBelow} ${this._messagesBelow === 1 ? "message" : "messages"} below"
|
|
1024
|
+
@click=${this._onScrollToRecent}
|
|
1025
|
+
style=${styleMap(scrollRecentPillStyles())}
|
|
1026
|
+
><span aria-hidden="true" style="font-size:11px;line-height:1;">⌄</span><span>${this._messagesBelow} ${this._messagesBelow === 1 ? "message" : "messages"}</span></button>` : nothing}
|
|
1027
|
+
</div>
|
|
1028
|
+
`;
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
// Trail-local styles. The chat-trail used to render into light DOM
|
|
1032
|
+
// (`createRenderRoot() { return this }`) and inject these rules into
|
|
1033
|
+
// `document.head` so chip + markdown styles could find their selectors.
|
|
1034
|
+
// That broke the moment the trail was hosted inside any shadow root
|
|
1035
|
+
// (sc-mount → custom-canvas → velvet-mobile → sc-mount → chat-bar →
|
|
1036
|
+
// chat-trail is the production chain): CSS animations require the
|
|
1037
|
+
// @keyframes rule to be in the SAME TreeScope as the element using
|
|
1038
|
+
// it, and document-level keyframes don't cross a shadow boundary.
|
|
1039
|
+
// Result was silent — `animation-name` parsed fine, the property got
|
|
1040
|
+
// the right resolved value, but the browser never instantiated an
|
|
1041
|
+
// Animation object because no matching keyframe was visible from
|
|
1042
|
+
// inside the shadow tree. `getAnimations()` returned `[]`, dots sat
|
|
1043
|
+
// frozen at base scale, the typing indicator looked dead.
|
|
1044
|
+
//
|
|
1045
|
+
// Shadow DOM with `static styles` is the right architecture here.
|
|
1046
|
+
// CSS custom properties (--sc-tile-text-color, --sc-content-bubble-*,
|
|
1047
|
+
// etc.) inherit through shadow boundaries by default, so the canvas
|
|
1048
|
+
// theme cascade still reaches the chips without the light-DOM
|
|
1049
|
+
// workaround. As a bonus the markdown reset rules can't leak out
|
|
1050
|
+
// onto the host page, and the keyframes are owned by the trail's
|
|
1051
|
+
// own tree scope where the chip + dot elements live.
|
|
1052
|
+
_AdaptiveChatTrail.styles = css`
|
|
1053
|
+
/* Bottom-anchor for CONVERSATIONS only: a short message list hugs the
|
|
1054
|
+
composer instead of floating at the top. The :not() gate matters —
|
|
1055
|
+
unscoped, the auto margin also grabbed the empty-state hero's first
|
|
1056
|
+
child (the icon disc), ate all free space above it, and pinned the
|
|
1057
|
+
greeting to the bottom of a maximized panel, silently defeating the
|
|
1058
|
+
hero's own justify-content: center. */
|
|
1059
|
+
[data-syntro-chat-trail]:not([data-state="empty"]) > :first-child {
|
|
1060
|
+
margin-top: auto;
|
|
1061
|
+
}
|
|
1062
|
+
/* …and THROUGH the conversation-log wrapper. That wrapper is
|
|
1063
|
+
display: contents (a semantic node carrying role=log, see render), so
|
|
1064
|
+
margin on it does nothing — the auto margin has to land on the first
|
|
1065
|
+
real chip inside it or a short thread floats to the top of a tall
|
|
1066
|
+
panel instead of hugging the composer. */
|
|
1067
|
+
[data-syntro-chat-trail]:not([data-state="empty"]) > [data-syntro-chat-log] > :first-child {
|
|
1068
|
+
margin-top: auto;
|
|
1069
|
+
}
|
|
1070
|
+
/* …and PAST it while it is still empty. The wrapper is always rendered (it
|
|
1071
|
+
is a live region, which must pre-exist its content), so with only an
|
|
1072
|
+
in-flight reply on screen the wrapper IS the :first-child and has no box
|
|
1073
|
+
to carry the margin. :empty ignores comment nodes, so Lit's own markers
|
|
1074
|
+
inside the wrapper do not defeat it. */
|
|
1075
|
+
[data-syntro-chat-trail]:not([data-state="empty"]) > [data-syntro-chat-log]:empty + * {
|
|
1076
|
+
margin-top: auto;
|
|
1077
|
+
}
|
|
1078
|
+
[data-trail-chip] > p:first-child {
|
|
1079
|
+
margin-top: 0;
|
|
1080
|
+
}
|
|
1081
|
+
[data-trail-chip] > p:last-child {
|
|
1082
|
+
margin-bottom: 0;
|
|
1083
|
+
}
|
|
1084
|
+
[data-trail-chip] p {
|
|
1085
|
+
margin: 4px 0;
|
|
1086
|
+
}
|
|
1087
|
+
[data-trail-chip] br + br {
|
|
1088
|
+
display: none;
|
|
1089
|
+
}
|
|
1090
|
+
[data-trail-chip] ul,
|
|
1091
|
+
[data-trail-chip] ol {
|
|
1092
|
+
margin: 4px 0;
|
|
1093
|
+
padding-left: 1.1em;
|
|
1094
|
+
}
|
|
1095
|
+
[data-trail-chip] li {
|
|
1096
|
+
margin: 0;
|
|
1097
|
+
}
|
|
1098
|
+
[data-trail-chip] li + li {
|
|
1099
|
+
margin-top: 1px;
|
|
1100
|
+
}
|
|
1101
|
+
[data-trail-chip] pre {
|
|
1102
|
+
margin: 6px 0;
|
|
1103
|
+
padding: 6px 8px;
|
|
1104
|
+
background: var(--sc-content-code-background-block, rgba(0, 0, 0, 0.35));
|
|
1105
|
+
border-radius: 6px;
|
|
1106
|
+
overflow-x: auto;
|
|
1107
|
+
font-size: 11px;
|
|
1108
|
+
}
|
|
1109
|
+
[data-trail-chip] code {
|
|
1110
|
+
font-family: ui-monospace, SF Mono, Menlo, monospace;
|
|
1111
|
+
font-size: 11px;
|
|
1112
|
+
background: var(--sc-content-code-background, rgba(0, 0, 0, 0.25));
|
|
1113
|
+
padding: 1px 4px;
|
|
1114
|
+
border-radius: 3px;
|
|
1115
|
+
}
|
|
1116
|
+
[data-trail-chip] pre code {
|
|
1117
|
+
background: transparent;
|
|
1118
|
+
padding: 0;
|
|
1119
|
+
}
|
|
1120
|
+
[data-trail-chip] a {
|
|
1121
|
+
color: var(--sc-content-link-color, var(--sc-color-primary, #b72e2a));
|
|
1122
|
+
text-decoration: underline;
|
|
1123
|
+
}
|
|
1124
|
+
[data-trail-chip] strong {
|
|
1125
|
+
font-weight: 600;
|
|
1126
|
+
}
|
|
1127
|
+
/* Streaming-caret blink for in-progress assistant chips. */
|
|
1128
|
+
@keyframes syntro-trail-caret {
|
|
1129
|
+
0% {
|
|
1130
|
+
opacity: 1;
|
|
1131
|
+
}
|
|
1132
|
+
50% {
|
|
1133
|
+
opacity: 0;
|
|
1134
|
+
}
|
|
1135
|
+
100% {
|
|
1136
|
+
opacity: 1;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
/* Thinking-dot scale + opacity pulse. The naked-dots redesign
|
|
1140
|
+
relies entirely on this motion to signal "alive" — there's no
|
|
1141
|
+
chip frame around the dots, so a subtle animation reads as
|
|
1142
|
+
broken. 0.4 → 1.0 scale with 0.25 → 1.0 opacity gives an
|
|
1143
|
+
unmistakable breathing cue. */
|
|
1144
|
+
@keyframes syntro-trail-thinking-pulse {
|
|
1145
|
+
0%,
|
|
1146
|
+
100% {
|
|
1147
|
+
transform: scale(0.4);
|
|
1148
|
+
opacity: 0.25;
|
|
1149
|
+
}
|
|
1150
|
+
45% {
|
|
1151
|
+
transform: scale(1);
|
|
1152
|
+
opacity: 1;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
/* Chip entrance — fades a freshly-mounted chip up from below
|
|
1156
|
+
so the thinking → first-streaming-chip handoff feels like one
|
|
1157
|
+
continuous motion instead of an element swap. */
|
|
1158
|
+
@keyframes syntro-trail-chip-enter {
|
|
1159
|
+
from {
|
|
1160
|
+
opacity: 0;
|
|
1161
|
+
transform: translateY(6px) scale(0.97);
|
|
1162
|
+
}
|
|
1163
|
+
to {
|
|
1164
|
+
opacity: 1;
|
|
1165
|
+
transform: translateY(0) scale(1);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
`;
|
|
1169
|
+
_AdaptiveChatTrail.properties = {
|
|
1170
|
+
messages: { attribute: false },
|
|
1171
|
+
visibleCount: { type: Number },
|
|
1172
|
+
expanded: { type: Boolean },
|
|
1173
|
+
greeting: { type: String },
|
|
1174
|
+
/** When true: trail renders fully expanded AND the
|
|
1175
|
+
* expand/collapse affordance is hidden. Used by hosts that mount
|
|
1176
|
+
* the chat in a full-screen surface (the mobile panel) where
|
|
1177
|
+
* collapse makes no sense — there's nothing to collapse INTO. */
|
|
1178
|
+
forceExpanded: { type: Boolean },
|
|
1179
|
+
/**
|
|
1180
|
+
* BUG (found 2026-08-13, reported live as a stray horizontal rule
|
|
1181
|
+
* floating above the transcript with nothing above it): the
|
|
1182
|
+
* expanded/forceExpanded scroll container always paints a
|
|
1183
|
+
* `borderTop` to mark "the panel's top edge". That reads correctly
|
|
1184
|
+
* in `AdaptiveChatBar`, which ALWAYS renders `renderHeader(...)`
|
|
1185
|
+
* immediately above the trail when `forceExpanded` is true (see
|
|
1186
|
+
* `AdaptiveChatBar.ts`'s `${this.forceExpanded ? renderHeader(...)
|
|
1187
|
+
* : nothing}`) — the border separates header from transcript.
|
|
1188
|
+
* `LeaveAMessageLit` mounts this element directly with no header
|
|
1189
|
+
* above it, so the same border renders as an orphaned rule sitting
|
|
1190
|
+
* in blank card padding. Default `false` preserves the existing
|
|
1191
|
+
* `AdaptiveChatBar` look exactly; hosts with no header above the
|
|
1192
|
+
* trail set this `true` to suppress it. */
|
|
1193
|
+
hideTopBorder: { type: Boolean },
|
|
1194
|
+
/**
|
|
1195
|
+
* Short-thread mode (BUG-1789098964): never engage the turn anchor;
|
|
1196
|
+
* every messages change pins the bottom unless the visitor scrolled up.
|
|
1197
|
+
* The anchor contract exists so a long STREAMED answer reads top-down
|
|
1198
|
+
* from the visitor's message. The human-handoff card shows a 160px
|
|
1199
|
+
* thread whose replies land whole, and there the anchor pinned the
|
|
1200
|
+
* visitor's own chip to the top while the reply sat below the fold
|
|
1201
|
+
* (Parker, live on mainpeptides.com 2026-09-10). Hosts that render a
|
|
1202
|
+
* short thread set this `true`; the concierge leaves it `false`.
|
|
1203
|
+
*/
|
|
1204
|
+
followBottom: { type: Boolean },
|
|
1205
|
+
/**
|
|
1206
|
+
* Mirrors `chatSession.inFlight` — true between `send()` and the
|
|
1207
|
+
* assistant's first `receiveStart`/`receiveDelta`, OR while a
|
|
1208
|
+
* stream is in progress. Drives the "thinking" indicator that
|
|
1209
|
+
* fills the gap between the user's just-sent message and the
|
|
1210
|
+
* first assistant token. Wired through AdaptiveChatBar (it owns
|
|
1211
|
+
* the chatSession subscription and forwards inFlight as a prop).
|
|
1212
|
+
*/
|
|
1213
|
+
inFlight: { type: Boolean },
|
|
1214
|
+
/**
|
|
1215
|
+
* Live reasoning narration streamed from the model via AG-UI
|
|
1216
|
+
* `THINKING_TEXT_MESSAGE_CONTENT`. When non-empty, the thinking
|
|
1217
|
+
* chip renders this in place of the bouncing dots so the visitor
|
|
1218
|
+
* sees what the model is actually doing instead of staring at
|
|
1219
|
+
* silent loading state during a tool-call gap.
|
|
1220
|
+
*/
|
|
1221
|
+
thinkingText: { attribute: false },
|
|
1222
|
+
personaName: { attribute: false },
|
|
1223
|
+
personaAvatarSm: { attribute: false },
|
|
1224
|
+
/**
|
|
1225
|
+
* Pre-conversation suggestion chip. When the visitor opens the
|
|
1226
|
+
* chat and `messages` is empty, the trail renders this as a
|
|
1227
|
+
* button beneath the greeting. Clicking it dispatches a
|
|
1228
|
+
* `trail-intro-suggestion` event with the configured prompt —
|
|
1229
|
+
* the parent (chat-bar) forwards that to chatSession.send().
|
|
1230
|
+
* Disappears the moment the first message lands.
|
|
1231
|
+
*/
|
|
1232
|
+
introSuggestion: { attribute: false },
|
|
1233
|
+
/**
|
|
1234
|
+
* Tappable tile-receipt chips rendered once at the end of the
|
|
1235
|
+
* trail. Produced from the store by Task 3 (AdaptiveChatBar).
|
|
1236
|
+
* Each chip emits `tile-receipt-activate` with its instanceId.
|
|
1237
|
+
*/
|
|
1238
|
+
tileReceipts: { attribute: false },
|
|
1239
|
+
/**
|
|
1240
|
+
* Widgets rendered INLINE in the trail (reco carousel etc.),
|
|
1241
|
+
* anchored to a message position like receipts. Sourced from the
|
|
1242
|
+
* store's `chat-inline` slot mounts via AdaptiveChatBar.
|
|
1243
|
+
*/
|
|
1244
|
+
inlineWidgets: { attribute: false },
|
|
1245
|
+
/**
|
|
1246
|
+
* Registry-backed mounter for inline widgets whose id is not their
|
|
1247
|
+
* custom-element tag. Injected from the chat-bar mountable.
|
|
1248
|
+
*/
|
|
1249
|
+
mountInlineWidget: { attribute: false },
|
|
1250
|
+
_messagesBelow: { state: true }
|
|
1251
|
+
};
|
|
1252
|
+
_AdaptiveChatTrail._AT_BOTTOM_EPSILON_PX = 50;
|
|
1253
|
+
_AdaptiveChatTrail._USER_SCROLL_INTENT_WINDOW_MS = 1200;
|
|
1254
|
+
var AdaptiveChatTrail = _AdaptiveChatTrail;
|
|
1255
|
+
function scrollRecentPillStyles() {
|
|
1256
|
+
return {
|
|
1257
|
+
position: "absolute",
|
|
1258
|
+
bottom: "8px",
|
|
1259
|
+
left: "50%",
|
|
1260
|
+
transform: "translateX(-50%)",
|
|
1261
|
+
display: "flex",
|
|
1262
|
+
alignItems: "center",
|
|
1263
|
+
gap: "5px",
|
|
1264
|
+
height: "26px",
|
|
1265
|
+
padding: "0 10px",
|
|
1266
|
+
borderRadius: "9999px",
|
|
1267
|
+
border: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))",
|
|
1268
|
+
// REAL token names + correct pairing (Parker 2026-08-28, screenshot:
|
|
1269
|
+
// unreadable dark smear). `--sc-content-bubble-bg` never existed
|
|
1270
|
+
// (flattenThemeConfig emits `--sc-content-bubble-background`), so every
|
|
1271
|
+
// workspace fell through to a translucent dark fallback — and the text
|
|
1272
|
+
// rode `--sc-tile-text-color` (iGlow: #333), the TILE pairing, giving
|
|
1273
|
+
// dark-on-dark. Bubble surface -> bubble background + content text, and
|
|
1274
|
+
// the themeless fallback is OPAQUE: a translucent pill floating over
|
|
1275
|
+
// scrolling trail content is unreadable by construction.
|
|
1276
|
+
background: "var(--sc-content-bubble-background, rgb(20, 22, 28))",
|
|
1277
|
+
color: "var(--sc-content-text-color, rgba(255, 255, 255, 0.92))",
|
|
1278
|
+
fontSize: "12px",
|
|
1279
|
+
fontWeight: "500",
|
|
1280
|
+
lineHeight: "1",
|
|
1281
|
+
whiteSpace: "nowrap",
|
|
1282
|
+
cursor: "pointer",
|
|
1283
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.25)",
|
|
1284
|
+
zIndex: "3"
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
function blurVeilStyles() {
|
|
1288
|
+
const mask = "linear-gradient(to bottom, black 0%, black 40%, transparent 100%)";
|
|
1289
|
+
return {
|
|
1290
|
+
position: "absolute",
|
|
1291
|
+
top: "0",
|
|
1292
|
+
left: "0",
|
|
1293
|
+
right: "0",
|
|
1294
|
+
height: `${TRAIL_BLUR_VEIL_PX}px`,
|
|
1295
|
+
pointerEvents: "none",
|
|
1296
|
+
backdropFilter: `blur(${TRAIL_BLUR_MAX_PX}px)`,
|
|
1297
|
+
WebkitBackdropFilter: `blur(${TRAIL_BLUR_MAX_PX}px)`,
|
|
1298
|
+
maskImage: mask,
|
|
1299
|
+
WebkitMaskImage: mask,
|
|
1300
|
+
zIndex: "2"
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function chipVisualState(m) {
|
|
1304
|
+
if (m.role === "system") {
|
|
1305
|
+
return { kind: "notice", role: m.role };
|
|
1306
|
+
}
|
|
1307
|
+
if (m.role === "assistant" && m.status === "error") {
|
|
1308
|
+
return { kind: "settled", role: m.role };
|
|
1309
|
+
}
|
|
1310
|
+
if (m.status === "error") {
|
|
1311
|
+
return { kind: "notice", role: m.role };
|
|
1312
|
+
}
|
|
1313
|
+
if (m.role === "assistant" && m.status === "streaming") {
|
|
1314
|
+
return { kind: "streaming" };
|
|
1315
|
+
}
|
|
1316
|
+
return { kind: "settled", role: m.role };
|
|
1317
|
+
}
|
|
1318
|
+
function chipStyles(state, pos) {
|
|
1319
|
+
const opacity = Math.max(OPACITY_FLOOR, 1 - pos * OPACITY_STEP);
|
|
1320
|
+
const yPx = pos * Y_DRIFT_PX;
|
|
1321
|
+
let border;
|
|
1322
|
+
let background;
|
|
1323
|
+
let color;
|
|
1324
|
+
let alignSelf;
|
|
1325
|
+
switch (state.kind) {
|
|
1326
|
+
case "settled":
|
|
1327
|
+
if (state.role === "user") {
|
|
1328
|
+
border = "var(--sc-content-bubble-border-user, 1px solid rgba(255, 255, 255, 0.28))";
|
|
1329
|
+
background = "var(--sc-content-bubble-background-user, rgba(255, 255, 255, 0.10))";
|
|
1330
|
+
alignSelf = "flex-end";
|
|
1331
|
+
} else {
|
|
1332
|
+
border = "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))";
|
|
1333
|
+
background = "var(--sc-content-bubble-background, rgba(20, 22, 24, 0.35))";
|
|
1334
|
+
alignSelf = "flex-start";
|
|
1335
|
+
}
|
|
1336
|
+
color = "var(--sc-tile-title-color, inherit)";
|
|
1337
|
+
break;
|
|
1338
|
+
case "streaming":
|
|
1339
|
+
case "pending":
|
|
1340
|
+
border = "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))";
|
|
1341
|
+
background = "var(--sc-content-bubble-background, rgba(20, 22, 24, 0.35))";
|
|
1342
|
+
color = "var(--sc-tile-title-color, inherit)";
|
|
1343
|
+
alignSelf = "flex-start";
|
|
1344
|
+
break;
|
|
1345
|
+
case "notice":
|
|
1346
|
+
border = "var(--sc-content-bubble-border-error, 1px solid rgba(255, 255, 255, 0.22))";
|
|
1347
|
+
background = "var(--sc-content-bubble-background-error, rgba(20, 22, 24, 0.28))";
|
|
1348
|
+
color = "var(--sc-content-bubble-text-error, rgba(255, 255, 255, 0.75))";
|
|
1349
|
+
alignSelf = state.role === "user" ? "flex-end" : "flex-start";
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
return {
|
|
1353
|
+
alignSelf,
|
|
1354
|
+
// Single shared width policy for EVERY bubble (user / assistant /
|
|
1355
|
+
// streaming / pending / error). Short and long replies now read as a
|
|
1356
|
+
// consistent column: `fit-content` sizes each bubble to its text, and
|
|
1357
|
+
// `TRAIL_MAX_WIDTH` caps the wide ones at the same ceiling — no more
|
|
1358
|
+
// per-role or per-path width drift (user feedback: chat tiles had
|
|
1359
|
+
// differing widths).
|
|
1360
|
+
maxWidth: TRAIL_MAX_WIDTH,
|
|
1361
|
+
width: "fit-content",
|
|
1362
|
+
// Bumped from 11px → 13px after the chat lid in production read as
|
|
1363
|
+
// cramped at the smaller size — body chat copy needs to be visitor-
|
|
1364
|
+
// legible on a phone without zoom, and 11px reads as a footnote
|
|
1365
|
+
// rather than the primary conversation surface. Code blocks inside
|
|
1366
|
+
// chip body scale proportionally (10px → 11px) below.
|
|
1367
|
+
fontSize: "13px",
|
|
1368
|
+
lineHeight: "1.45",
|
|
1369
|
+
padding: "5px 11px",
|
|
1370
|
+
borderRadius: "10px",
|
|
1371
|
+
border,
|
|
1372
|
+
background,
|
|
1373
|
+
// Frosted-glass bubbles are OPT-IN via the `--sc-content-bubble-blur`
|
|
1374
|
+
// knob (a host that wants glass sets e.g. `blur(8px)` deliberately); the
|
|
1375
|
+
// DEFAULT is `none`. A backdrop-filter on the bubble composites its OWN
|
|
1376
|
+
// text onto a GPU layer (grayscale AA + the blurred backdrop bleeding
|
|
1377
|
+
// under thin glyphs) — the resting-state compositing contract's
|
|
1378
|
+
// "backdrop-filter behind text" case: soft rasterization everywhere and
|
|
1379
|
+
// fully BLANKED text on some Android GPUs. Glass-by-default shipped that
|
|
1380
|
+
// hazard to every non-velvet surface, so the default flipped to `none`
|
|
1381
|
+
// (GPU/compositing hazard audit, Fix 3). Gate:
|
|
1382
|
+
// AdaptiveChatTrail.compositing.test.ts.
|
|
1383
|
+
backdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1384
|
+
WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1385
|
+
color,
|
|
1386
|
+
transition: "opacity 240ms cubic-bezier(0.22, 1, 0.36, 1), transform 240ms cubic-bezier(0.22, 1, 0.36, 1)",
|
|
1387
|
+
// Per-position opacity + y-drift falloff is still per-chip — those
|
|
1388
|
+
// operate naturally per element. Blur is NOT per-chip anymore: it's
|
|
1389
|
+
// applied via an absolute-positioned veil at the top of the scroll
|
|
1390
|
+
// viewport (TRAIL_BLUR_VEIL_PX above), so a single long message
|
|
1391
|
+
// doesn't get uniformly blurred across its full body.
|
|
1392
|
+
opacity: String(toFixedTrim(opacity)),
|
|
1393
|
+
transform: `translateY(${yPx}px)`,
|
|
1394
|
+
// User messages render as raw text (`html` template); pre-wrap preserves
|
|
1395
|
+
// newlines the visitor typed. Assistant messages render as markdown HTML
|
|
1396
|
+
// (`<p>` tags from marked); pre-wrap there would make marked's inter-tag
|
|
1397
|
+
// text nodes (the `\n` between/after `<p>` tags) visible, producing the
|
|
1398
|
+
// trailing/inter-paragraph blank lines visitors reported. Markdown's own
|
|
1399
|
+
// structure carries the paragraph breaks, so assistant chips use the
|
|
1400
|
+
// default `normal` whitespace handling.
|
|
1401
|
+
whiteSpace: (state.kind === "settled" || state.kind === "notice") && state.role === "user" ? "pre-wrap" : "normal",
|
|
1402
|
+
wordBreak: "break-word"
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function toolCallStripStyles() {
|
|
1406
|
+
return {
|
|
1407
|
+
display: "flex",
|
|
1408
|
+
flexWrap: "wrap",
|
|
1409
|
+
gap: "4px",
|
|
1410
|
+
marginTop: "4px",
|
|
1411
|
+
paddingTop: "4px",
|
|
1412
|
+
borderTop: "1px dashed var(--sc-content-divider-color, rgba(255, 255, 255, 0.12))"
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function pendingToolCallChipStyles() {
|
|
1416
|
+
return {
|
|
1417
|
+
display: "inline-flex",
|
|
1418
|
+
alignItems: "center",
|
|
1419
|
+
gap: "4px",
|
|
1420
|
+
padding: "2px 7px",
|
|
1421
|
+
border: "var(--sc-content-bubble-border-user, 1px solid rgba(255, 255, 255, 0.28))",
|
|
1422
|
+
background: "var(--sc-content-bubble-background-user, rgba(255, 255, 255, 0.10))",
|
|
1423
|
+
color: "var(--sc-tile-text-color, inherit)",
|
|
1424
|
+
borderRadius: "999px",
|
|
1425
|
+
fontSize: "10px",
|
|
1426
|
+
fontWeight: "500",
|
|
1427
|
+
fontFamily: "ui-monospace, SF Mono, Menlo, monospace",
|
|
1428
|
+
cursor: "pointer",
|
|
1429
|
+
opacity: "1"
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function deriveActivityReceipt(tc) {
|
|
1433
|
+
if (tc.name !== "primary_response_action") return null;
|
|
1434
|
+
const args = tc.args;
|
|
1435
|
+
if (!args || typeof args !== "object") return null;
|
|
1436
|
+
const templateId = typeof args.template_id === "string" ? args.template_id : "";
|
|
1437
|
+
const content = args.content ?? null;
|
|
1438
|
+
const title = content && typeof content.title === "string" && content.title.trim() ? content.title.trim() : "";
|
|
1439
|
+
const id = templateId.toLowerCase();
|
|
1440
|
+
let type;
|
|
1441
|
+
if (id.includes("faq")) type = "FAQ";
|
|
1442
|
+
else if (id.includes("chart") || id.includes("graph")) type = "chart";
|
|
1443
|
+
else if (id.includes("card") || id.includes("product")) type = "card";
|
|
1444
|
+
else if (id.includes("alert") || id.includes("navigation") || id.includes("nav")) type = "alert";
|
|
1445
|
+
else type = "added to your view";
|
|
1446
|
+
const label = title || templateId || "something";
|
|
1447
|
+
return { type, label };
|
|
1448
|
+
}
|
|
1449
|
+
function receiptText(r) {
|
|
1450
|
+
switch (r.type) {
|
|
1451
|
+
case "FAQ":
|
|
1452
|
+
return "Opened the FAQ for you";
|
|
1453
|
+
case "chart":
|
|
1454
|
+
return `Put together ${r.label} for you`;
|
|
1455
|
+
case "card":
|
|
1456
|
+
return "Pulled up the product for you";
|
|
1457
|
+
case "alert":
|
|
1458
|
+
return `Here's a shortcut: ${r.label}`;
|
|
1459
|
+
default:
|
|
1460
|
+
return `Pulled up ${r.label} for you`;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
function extractSuggestedReplies(toolCalls) {
|
|
1464
|
+
if (!toolCalls) return [];
|
|
1465
|
+
for (const tc of toolCalls) {
|
|
1466
|
+
if (tc.name !== "suggest_replies") continue;
|
|
1467
|
+
const replies = tc.args?.replies;
|
|
1468
|
+
if (!Array.isArray(replies)) return [];
|
|
1469
|
+
return replies.filter((r) => typeof r === "string" && r.trim().length > 0).map((r) => r.trim()).slice(0, 4);
|
|
1470
|
+
}
|
|
1471
|
+
return [];
|
|
1472
|
+
}
|
|
1473
|
+
function computeAssistantChipParts(m, hostReplies) {
|
|
1474
|
+
const toolCalls = m.toolCalls ?? [];
|
|
1475
|
+
return {
|
|
1476
|
+
pendingToolCalls: toolCalls.filter((tc) => tc.status === "pending"),
|
|
1477
|
+
receipts: toolCalls.map(deriveActivityReceipt).filter((r) => r !== null),
|
|
1478
|
+
// Threaded in by the render (empty for non-hosts): under pills-last the
|
|
1479
|
+
// hosting message can differ from the message that carried the tool call.
|
|
1480
|
+
replyChips: hostReplies
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function looksLikeSerializedModelResponse(text) {
|
|
1484
|
+
const t = text.trimStart();
|
|
1485
|
+
if (t.startsWith("(Empty response")) return true;
|
|
1486
|
+
let hits = 0;
|
|
1487
|
+
if (/['"]stop_reason['"]\s*:/.test(t)) hits++;
|
|
1488
|
+
if (/['"]signature['"]\s*:/.test(t)) hits++;
|
|
1489
|
+
if (/['"]type['"]\s*:\s*['"]thinking['"]/.test(t)) hits++;
|
|
1490
|
+
if (/['"]content['"]\s*:\s*\[/.test(t)) hits++;
|
|
1491
|
+
return hits >= 2;
|
|
1492
|
+
}
|
|
1493
|
+
function assistantChipRendersContent(m, hostReplies) {
|
|
1494
|
+
const text = (m.text ?? "").trim();
|
|
1495
|
+
if (text.length > 0 && !looksLikeSerializedModelResponse(text)) return true;
|
|
1496
|
+
const { pendingToolCalls, receipts, replyChips } = computeAssistantChipParts(m, hostReplies);
|
|
1497
|
+
return pendingToolCalls.length > 0 || receipts.length > 0 || replyChips.length > 0;
|
|
1498
|
+
}
|
|
1499
|
+
function inlineWidgetBlockStyles() {
|
|
1500
|
+
return {
|
|
1501
|
+
alignSelf: "stretch",
|
|
1502
|
+
width: "100%",
|
|
1503
|
+
minWidth: "0",
|
|
1504
|
+
marginTop: "6px",
|
|
1505
|
+
contain: "layout"
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
function receiptStripStyles() {
|
|
1509
|
+
return {
|
|
1510
|
+
display: "flex",
|
|
1511
|
+
flexDirection: "column",
|
|
1512
|
+
gap: "2px",
|
|
1513
|
+
marginTop: "4px"
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function receiptStyles() {
|
|
1517
|
+
return {
|
|
1518
|
+
display: "inline-flex",
|
|
1519
|
+
alignItems: "center",
|
|
1520
|
+
gap: "5px",
|
|
1521
|
+
alignSelf: "flex-start",
|
|
1522
|
+
fontSize: "11px",
|
|
1523
|
+
lineHeight: "1.3",
|
|
1524
|
+
fontWeight: "500",
|
|
1525
|
+
letterSpacing: "0.01em",
|
|
1526
|
+
// Inherit the chip's OWN resolved text color (muted via opacity), so the
|
|
1527
|
+
// receipt is legible on every theme by construction. The old
|
|
1528
|
+
// `--sc-content-muted-color` fallback was rgba(255,255,255,…) — white —
|
|
1529
|
+
// invisible on light canvases; a `--sc-tile-text-color` token can likewise
|
|
1530
|
+
// be unset/mismatched on dark canvases. `currentColor` always matches the
|
|
1531
|
+
// chip text the visitor is already reading.
|
|
1532
|
+
color: "currentColor",
|
|
1533
|
+
opacity: "0.7"
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
function receiptIconStyles() {
|
|
1537
|
+
return {
|
|
1538
|
+
display: "inline-flex",
|
|
1539
|
+
alignItems: "center",
|
|
1540
|
+
justifyContent: "center",
|
|
1541
|
+
width: "14px",
|
|
1542
|
+
height: "14px",
|
|
1543
|
+
borderRadius: "50%",
|
|
1544
|
+
fontSize: "9px",
|
|
1545
|
+
fontWeight: "700",
|
|
1546
|
+
color: "#fff",
|
|
1547
|
+
background: "hsl(var(--sc-accent-color, 145 40% 45%) / 0.7)"
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function replyChipStripStyles() {
|
|
1551
|
+
return {
|
|
1552
|
+
display: "flex",
|
|
1553
|
+
flexWrap: "wrap",
|
|
1554
|
+
gap: "6px",
|
|
1555
|
+
marginTop: "6px",
|
|
1556
|
+
alignSelf: "flex-start"
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function tileReceiptStripStyles() {
|
|
1560
|
+
return {
|
|
1561
|
+
display: "flex",
|
|
1562
|
+
flexDirection: "column",
|
|
1563
|
+
gap: "6px",
|
|
1564
|
+
marginTop: "6px",
|
|
1565
|
+
width: "100%",
|
|
1566
|
+
alignSelf: "stretch"
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
function replyChipStyles() {
|
|
1570
|
+
return {
|
|
1571
|
+
display: "inline-flex",
|
|
1572
|
+
alignItems: "center",
|
|
1573
|
+
padding: "5px 12px",
|
|
1574
|
+
border: "1px solid hsl(var(--sc-accent-color, 145 40% 45%) / 0.40)",
|
|
1575
|
+
borderRadius: "9999px",
|
|
1576
|
+
background: "hsl(var(--sc-accent-color, 145 40% 45%) / 0.12)",
|
|
1577
|
+
color: "var(--sc-tile-text-color, currentColor)",
|
|
1578
|
+
font: "inherit",
|
|
1579
|
+
fontSize: "12px",
|
|
1580
|
+
fontWeight: "500",
|
|
1581
|
+
lineHeight: "1.2",
|
|
1582
|
+
cursor: "pointer",
|
|
1583
|
+
backdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1584
|
+
WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1585
|
+
transition: "background 150ms ease, transform 120ms ease"
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
function alertReceiptIconStyles() {
|
|
1589
|
+
return {
|
|
1590
|
+
flex: "0 0 auto",
|
|
1591
|
+
width: "26px",
|
|
1592
|
+
height: "26px",
|
|
1593
|
+
borderRadius: "8px",
|
|
1594
|
+
display: "grid",
|
|
1595
|
+
placeItems: "center",
|
|
1596
|
+
background: "hsl(var(--sc-accent-color))",
|
|
1597
|
+
color: "var(--sc-accent-foreground, #fff)",
|
|
1598
|
+
fontSize: "14px"
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
function tileReceiptChipStyles() {
|
|
1602
|
+
return {
|
|
1603
|
+
display: "flex",
|
|
1604
|
+
alignItems: "center",
|
|
1605
|
+
gap: "10px",
|
|
1606
|
+
width: "100%",
|
|
1607
|
+
textAlign: "left",
|
|
1608
|
+
cursor: "pointer",
|
|
1609
|
+
padding: "9px 10px 9px 11px",
|
|
1610
|
+
borderRadius: "12px",
|
|
1611
|
+
border: "1px solid hsl(var(--sc-accent-color) / 0.28)",
|
|
1612
|
+
background: "hsl(var(--sc-accent-color) / 0.06)"
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
function tileReceiptGlyphStyles() {
|
|
1616
|
+
return { position: "relative", flex: "0 0 auto", width: "28px", height: "28px" };
|
|
1617
|
+
}
|
|
1618
|
+
function tileReceiptThumbStyles() {
|
|
1619
|
+
return {
|
|
1620
|
+
position: "absolute",
|
|
1621
|
+
inset: "0",
|
|
1622
|
+
width: "100%",
|
|
1623
|
+
height: "100%",
|
|
1624
|
+
objectFit: "cover",
|
|
1625
|
+
borderRadius: "7px",
|
|
1626
|
+
// Same hairline the glyph layers carry, so the thumb sits in the chip
|
|
1627
|
+
// with identical weight.
|
|
1628
|
+
boxShadow: "inset 0 0 0 1px hsl(var(--sc-accent-color) / 0.4)"
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
function tileReceiptMonogramStyles() {
|
|
1632
|
+
return {
|
|
1633
|
+
position: "absolute",
|
|
1634
|
+
inset: "0",
|
|
1635
|
+
display: "flex",
|
|
1636
|
+
alignItems: "center",
|
|
1637
|
+
justifyContent: "center",
|
|
1638
|
+
borderRadius: "7px",
|
|
1639
|
+
// The shared stand-in (sdk-contracts image-standin): alpha sheen over the
|
|
1640
|
+
// brand primary, initial in the launcher's paired foreground token.
|
|
1641
|
+
background: IMAGE_STANDIN_BACKGROUND,
|
|
1642
|
+
color: "var(--sc-launcher-color, #ffffff)",
|
|
1643
|
+
font: "700 13px/1 var(--sc-font-family, inherit)",
|
|
1644
|
+
userSelect: "none"
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
function receiptChipTitleStyles(_kind) {
|
|
1648
|
+
return {
|
|
1649
|
+
// Grows to fill the space between the icon and the action pill, truncating
|
|
1650
|
+
// long product names on one line (the chip is now single-line).
|
|
1651
|
+
flex: "1",
|
|
1652
|
+
minWidth: "0",
|
|
1653
|
+
fontSize: "13px",
|
|
1654
|
+
fontWeight: "650",
|
|
1655
|
+
lineHeight: "1.2",
|
|
1656
|
+
color: "var(--sc-content-text-color, #0f172a)",
|
|
1657
|
+
whiteSpace: "nowrap",
|
|
1658
|
+
overflow: "hidden",
|
|
1659
|
+
textOverflow: "ellipsis"
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
function tileReceiptActionStyles(dismissed) {
|
|
1663
|
+
return {
|
|
1664
|
+
flex: "0 0 auto",
|
|
1665
|
+
fontSize: "11px",
|
|
1666
|
+
fontWeight: "700",
|
|
1667
|
+
borderRadius: "999px",
|
|
1668
|
+
padding: "4px 10px",
|
|
1669
|
+
...dismissed ? { background: "hsl(var(--sc-accent-color) / 0.14)", color: "hsl(var(--sc-accent-color))" } : {
|
|
1670
|
+
background: "hsl(var(--sc-accent-color) / 0.9)",
|
|
1671
|
+
color: "var(--sc-accent-foreground, #fff)"
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function caretStyles() {
|
|
1676
|
+
return {
|
|
1677
|
+
display: "inline-block",
|
|
1678
|
+
width: "6px",
|
|
1679
|
+
height: "12px",
|
|
1680
|
+
marginLeft: "3px",
|
|
1681
|
+
verticalAlign: "-1px",
|
|
1682
|
+
background: "hsl(var(--sc-accent-color) / 0.85)",
|
|
1683
|
+
borderRadius: "1px",
|
|
1684
|
+
animation: "syntro-trail-caret 1s steps(2, end) infinite"
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function thinkingSlotStyles() {
|
|
1688
|
+
return {
|
|
1689
|
+
alignSelf: "flex-start",
|
|
1690
|
+
color: "var(--sc-tile-title-color, inherit)",
|
|
1691
|
+
padding: "4px 4px",
|
|
1692
|
+
// The status sentence is `white-space: nowrap`, which makes this slot's
|
|
1693
|
+
// MIN-content width the full untruncated string. A flex item's default
|
|
1694
|
+
// `min-width: auto` refuses to shrink below that, so without these three
|
|
1695
|
+
// the composed line ("<persona> is buttoning up details to point the right
|
|
1696
|
+
// way…") pushes ~65px past a 320px trail instead of ellipsizing inside it.
|
|
1697
|
+
// `min-width: 0` unlocks shrinking, `max-width: 100%` caps it at the
|
|
1698
|
+
// trail's content box, and `box-sizing` keeps the 4px padding inside that
|
|
1699
|
+
// cap. Measured in chat-status-line-height.spec.ts.
|
|
1700
|
+
boxSizing: "border-box",
|
|
1701
|
+
maxWidth: "100%",
|
|
1702
|
+
minWidth: "0",
|
|
1703
|
+
animation: "syntro-trail-chip-enter 220ms cubic-bezier(0.22, 1, 0.36, 1) both"
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
function thinkingDotsStyles() {
|
|
1707
|
+
return {
|
|
1708
|
+
display: "inline-flex",
|
|
1709
|
+
alignItems: "center",
|
|
1710
|
+
gap: "6px",
|
|
1711
|
+
// Line height matches what a single-line text chip would compute
|
|
1712
|
+
// to, so swapping out the slot for the streaming chip doesn't
|
|
1713
|
+
// shift the next chip up or down by a couple of pixels.
|
|
1714
|
+
lineHeight: "14px"
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
function thinkingDotStyles(index) {
|
|
1718
|
+
return {
|
|
1719
|
+
display: "inline-block",
|
|
1720
|
+
width: "7px",
|
|
1721
|
+
height: "7px",
|
|
1722
|
+
borderRadius: "50%",
|
|
1723
|
+
background: "currentColor",
|
|
1724
|
+
// The dots are now the entire visual — no bubble framing them —
|
|
1725
|
+
// so the pulse has to do all the work the chip used to do. Scale
|
|
1726
|
+
// 0.4 → 1.0 with opacity 0.25 → 1.0 gives an unmistakable
|
|
1727
|
+
// breathing motion: at the trough each dot reads like a faint
|
|
1728
|
+
// ghost, at the peak like a solid dot. 940ms cycle is a touch
|
|
1729
|
+
// faster than the original 1.05s; without the chip frame the
|
|
1730
|
+
// shorter cycle feels purposeful instead of agitated. Stagger
|
|
1731
|
+
// 130ms = clear rolling cadence across all three.
|
|
1732
|
+
transformOrigin: "center",
|
|
1733
|
+
animation: `syntro-trail-thinking-pulse 940ms cubic-bezier(0.4, 0, 0.6, 1) ${index * 0.13}s infinite both`
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
function moreStyles() {
|
|
1737
|
+
return {
|
|
1738
|
+
// Center horizontally regardless of the parent's display model.
|
|
1739
|
+
// ``alignSelf: center`` only works when the parent is a flex/grid
|
|
1740
|
+
// container with cross-axis alignment; the chat-bar's flex column
|
|
1741
|
+
// stretches us full-width instead. ``display: block`` + auto inline
|
|
1742
|
+
// margins centers the auto-sized button without needing the parent
|
|
1743
|
+
// to opt in.
|
|
1744
|
+
display: "block",
|
|
1745
|
+
marginLeft: "auto",
|
|
1746
|
+
marginRight: "auto",
|
|
1747
|
+
marginBottom: "6px",
|
|
1748
|
+
fontSize: "10px",
|
|
1749
|
+
fontWeight: "500",
|
|
1750
|
+
letterSpacing: "0.06em",
|
|
1751
|
+
padding: "4px 10px",
|
|
1752
|
+
border: "1px solid hsl(var(--sc-accent-color) / 0.32)",
|
|
1753
|
+
borderRadius: "999px",
|
|
1754
|
+
background: "hsl(var(--sc-accent-color) / 0.10)",
|
|
1755
|
+
color: "var(--sc-tile-text-color, currentColor)",
|
|
1756
|
+
cursor: "pointer",
|
|
1757
|
+
opacity: "0.85",
|
|
1758
|
+
backdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1759
|
+
WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1760
|
+
transition: "opacity 150ms ease, background 150ms ease"
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
function stripTrailingWhitespace(text) {
|
|
1764
|
+
return text.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
1765
|
+
}
|
|
1766
|
+
function toFixedTrim(n) {
|
|
1767
|
+
const s = n.toFixed(2);
|
|
1768
|
+
return s.replace(/\.?0+$/, "") || "0";
|
|
1769
|
+
}
|
|
1770
|
+
var THINKING_TAIL_MAX = 120;
|
|
1771
|
+
function truncateThinkingTail(text) {
|
|
1772
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
1773
|
+
if (flat.length <= THINKING_TAIL_MAX) return flat;
|
|
1774
|
+
return `\u2026${flat.slice(-THINKING_TAIL_MAX)}`;
|
|
1775
|
+
}
|
|
1776
|
+
function thinkingStatusLine(personaName, thinkingText) {
|
|
1777
|
+
const name = (personaName ?? "").trim();
|
|
1778
|
+
const phase = thinkingText ? truncateThinkingTail(thinkingText) : "";
|
|
1779
|
+
if (!name) return phase;
|
|
1780
|
+
if (!phase) return `${name} is typing\u2026`;
|
|
1781
|
+
const predicate = `${phase.charAt(0).toLowerCase()}${phase.slice(1)}`;
|
|
1782
|
+
const tail = /[.!?…]$/.test(predicate) ? "" : "\u2026";
|
|
1783
|
+
return `${name} is ${predicate}${tail}`;
|
|
1784
|
+
}
|
|
1785
|
+
function thinkingStatusRowStyles() {
|
|
1786
|
+
return {
|
|
1787
|
+
display: "flex",
|
|
1788
|
+
alignItems: "center",
|
|
1789
|
+
gap: "5px",
|
|
1790
|
+
maxWidth: "100%",
|
|
1791
|
+
minWidth: "0"
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function thinkingTextStyles() {
|
|
1795
|
+
return {
|
|
1796
|
+
fontStyle: "italic",
|
|
1797
|
+
opacity: "0.75",
|
|
1798
|
+
fontSize: "13px",
|
|
1799
|
+
lineHeight: "18px",
|
|
1800
|
+
maxWidth: "46ch",
|
|
1801
|
+
minWidth: "0",
|
|
1802
|
+
overflow: "hidden",
|
|
1803
|
+
textOverflow: "ellipsis",
|
|
1804
|
+
whiteSpace: "nowrap"
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
function introSuggestionStyles() {
|
|
1808
|
+
return {
|
|
1809
|
+
display: "inline-flex",
|
|
1810
|
+
alignItems: "center",
|
|
1811
|
+
gap: "6px",
|
|
1812
|
+
marginTop: "4px",
|
|
1813
|
+
padding: "10px 18px",
|
|
1814
|
+
borderRadius: "9999px",
|
|
1815
|
+
border: "0",
|
|
1816
|
+
background: "linear-gradient(135deg, var(--sc-color-primary, #5faf7d) 0%, var(--sc-color-primary-hover, #3d8a5e) 100%)",
|
|
1817
|
+
color: "#fff",
|
|
1818
|
+
cursor: "pointer",
|
|
1819
|
+
font: "inherit",
|
|
1820
|
+
fontSize: "14px",
|
|
1821
|
+
fontWeight: "600",
|
|
1822
|
+
transition: "transform 120ms ease, filter 160ms ease"
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
function emptyIconStyles() {
|
|
1826
|
+
return {
|
|
1827
|
+
display: "inline-flex",
|
|
1828
|
+
alignItems: "center",
|
|
1829
|
+
justifyContent: "center",
|
|
1830
|
+
width: "48px",
|
|
1831
|
+
height: "48px",
|
|
1832
|
+
borderRadius: "9999px",
|
|
1833
|
+
background: "var(--sc-tile-icon-background, linear-gradient(135deg, var(--sc-color-primary, #5faf7d) 0%, var(--sc-color-primary-hover, #3d8a5e) 100%))",
|
|
1834
|
+
color: "#fff",
|
|
1835
|
+
fontSize: "22px"
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function emptyTitleStyles() {
|
|
1839
|
+
return {
|
|
1840
|
+
margin: "0",
|
|
1841
|
+
fontSize: "15px",
|
|
1842
|
+
fontWeight: "600",
|
|
1843
|
+
lineHeight: "1.35",
|
|
1844
|
+
color: "var(--sc-content-text-color, inherit)",
|
|
1845
|
+
maxWidth: "32ch"
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
if (!customElements.get("adaptive-chat-trail")) {
|
|
1849
|
+
customElements.define("adaptive-chat-trail", AdaptiveChatTrail);
|
|
1850
|
+
}
|
|
1851
|
+
function personaBylineStyles() {
|
|
1852
|
+
return {
|
|
1853
|
+
display: "flex",
|
|
1854
|
+
alignItems: "center",
|
|
1855
|
+
gap: "5px",
|
|
1856
|
+
fontSize: "0.68rem",
|
|
1857
|
+
fontWeight: "600",
|
|
1858
|
+
opacity: "0.65",
|
|
1859
|
+
marginBottom: "3px",
|
|
1860
|
+
lineHeight: "1"
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
function personaAvatarStyles() {
|
|
1864
|
+
return {
|
|
1865
|
+
width: "16px",
|
|
1866
|
+
height: "16px",
|
|
1867
|
+
borderRadius: "9999px",
|
|
1868
|
+
objectFit: "cover",
|
|
1869
|
+
flexShrink: "0"
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
export {
|
|
1874
|
+
AdaptiveChatTrail,
|
|
1875
|
+
looksLikeSerializedModelResponse
|
|
1876
|
+
};
|
|
1877
|
+
//# sourceMappingURL=chunk-UFTSEIQP.js.map
|