@syntrologie/adapt-chatbot 2.8.0-canary.43 → 2.8.0-canary.430
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.d.ts +175 -0
- package/dist/AdaptiveChatBar.d.ts.map +1 -0
- package/dist/AdaptiveChatBar.js +11 -0
- package/dist/AdaptiveChatBar.js.map +7 -0
- package/dist/AdaptiveChatBarMountable.d.ts +80 -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 +415 -0
- package/dist/AdaptiveChatTrail.d.ts.map +1 -0
- package/dist/AdaptiveChatTrail.js +11 -0
- package/dist/AdaptiveChatTrail.js.map +7 -0
- package/dist/AdaptiveChatTrail.test.d.ts +2 -0
- package/dist/AdaptiveChatTrail.test.d.ts.map +1 -0
- package/dist/AdaptiveChipsStrip.d.ts +1150 -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 +60 -0
- package/dist/ChatAssistantLit.d.ts.map +1 -0
- package/dist/ChatAssistantLit.js +13 -0
- package/dist/ChatAssistantLit.js.map +7 -0
- package/dist/ChatSession.d.ts +230 -0
- package/dist/ChatSession.d.ts.map +1 -0
- package/dist/ChatTransport.d.ts +360 -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/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 +83 -0
- package/dist/Turnstile.d.ts.map +1 -0
- package/dist/cdn.d.ts +2 -6
- package/dist/cdn.d.ts.map +1 -1
- package/dist/chunk-5W75LR6M.js +460 -0
- package/dist/chunk-5W75LR6M.js.map +7 -0
- package/dist/chunk-7WSMNAKW.js +3824 -0
- package/dist/chunk-7WSMNAKW.js.map +7 -0
- package/dist/chunk-EWPPVPJ4.js +14 -0
- package/dist/chunk-EWPPVPJ4.js.map +7 -0
- package/dist/chunk-GP4OSTZT.js +641 -0
- package/dist/chunk-GP4OSTZT.js.map +7 -0
- package/dist/chunk-HELUC5JI.js +8971 -0
- package/dist/chunk-HELUC5JI.js.map +7 -0
- package/dist/chunk-HVGQJZMP.js +1520 -0
- package/dist/chunk-HVGQJZMP.js.map +7 -0
- package/dist/chunk-KPWW7N27.js +235 -0
- package/dist/chunk-KPWW7N27.js.map +7 -0
- package/dist/chunk-UVKRO5ER.js +13 -0
- package/dist/chunk-UVKRO5ER.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/elements/ActionHandler.d.ts +34 -0
- package/dist/elements/ActionHandler.d.ts.map +1 -0
- package/dist/elements/ElementInstanceStore.d.ts +207 -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/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/envelope.d.ts +24 -0
- package/dist/elements/envelope.d.ts.map +1 -0
- package/dist/elements/fetcher.d.ts +40 -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/types.d.ts +106 -0
- package/dist/elements/types.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__/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 +30 -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/runtime-lit.d.ts +8 -0
- package/dist/runtime-lit.d.ts.map +1 -0
- package/dist/runtime.d.ts +10 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2232 -29
- package/dist/runtime.js.map +7 -0
- package/dist/schema.d.ts +3340 -11
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +220 -31
- package/dist/schema.js.map +7 -0
- package/dist/test/setup.d.ts +1 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/types.d.ts +61 -40
- package/dist/types.d.ts.map +1 -1
- package/package.json +44 -23
- 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,1520 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderMarkdown
|
|
3
|
+
} from "./chunk-7WSMNAKW.js";
|
|
4
|
+
|
|
5
|
+
// src/AdaptiveChatTrail.ts
|
|
6
|
+
import { css, html, LitElement, nothing } from "lit";
|
|
7
|
+
import { styleMap } from "lit/directives/style-map.js";
|
|
8
|
+
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
9
|
+
var DEFAULT_VISIBLE = 3;
|
|
10
|
+
var TRAIL_MAX_WIDTH = "88%";
|
|
11
|
+
var OPACITY_STEP = 0.22;
|
|
12
|
+
var OPACITY_FLOOR = 0.18;
|
|
13
|
+
var Y_DRIFT_PX = -2;
|
|
14
|
+
var TRAIL_BLUR_VEIL_PX = 72;
|
|
15
|
+
var TRAIL_BLUR_MAX_PX = 5;
|
|
16
|
+
var _AdaptiveChatTrail = class _AdaptiveChatTrail extends LitElement {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
this.messages = [];
|
|
20
|
+
this.tileReceipts = [];
|
|
21
|
+
this.inlineWidgets = [];
|
|
22
|
+
this.mountInlineWidget = void 0;
|
|
23
|
+
this.visibleCount = DEFAULT_VISIBLE;
|
|
24
|
+
this.expanded = false;
|
|
25
|
+
this.forceExpanded = false;
|
|
26
|
+
this.inFlight = false;
|
|
27
|
+
this.thinkingText = "";
|
|
28
|
+
this.introSuggestion = void 0;
|
|
29
|
+
/**
|
|
30
|
+
* Pre-conversation phantom assistant message. Rendered only when
|
|
31
|
+
* `messages` is empty. NOT injected into chatSession state — the
|
|
32
|
+
* greeting is configuration, not conversation, so it disappears
|
|
33
|
+
* automatically when the first real message arrives.
|
|
34
|
+
*/
|
|
35
|
+
this.greeting = void 0;
|
|
36
|
+
// ── Anchor-scroll model (FEAT-1783535427, shared contract with
|
|
37
|
+
// <syntro-chat>) — applies to the SCROLLABLE modes (expanded /
|
|
38
|
+
// forceExpanded). The ambient peek strip keeps its bottom pin: it is
|
|
39
|
+
// overflow-hidden (not user-scrollable) and the pin is what keeps the
|
|
40
|
+
// latest line visible under the blur veil.
|
|
41
|
+
/** Follow the bottom on content growth. On by default (history
|
|
42
|
+
* browsing); off from a new user message until the visitor opts back in
|
|
43
|
+
* (pill tap or a user-gesture scroll that reaches the bottom). */
|
|
44
|
+
this._stickToBottom = true;
|
|
45
|
+
/** The user message the current turn is anchored to. */
|
|
46
|
+
this._anchorMsgId = null;
|
|
47
|
+
/** Whether a turn anchor is live (drives expand-mid-turn landing). */
|
|
48
|
+
this._anchorActive = false;
|
|
49
|
+
/** Keep enforcing chip-at-top on updates (late geometry, mode flips)
|
|
50
|
+
* until the user scrolls or opts into following. */
|
|
51
|
+
this._anchorHold = false;
|
|
52
|
+
/** Number of messages whose BOTTOM edge is still below the viewport —
|
|
53
|
+
* messages the visitor hasn't scrolled past yet. Drives the
|
|
54
|
+
* "⌄ N messages" pill and stays accurate as they scroll (one long
|
|
55
|
+
* streaming reply counts as 1 until its end has been read). Reactive. */
|
|
56
|
+
this._messagesBelow = 0;
|
|
57
|
+
/** Timestamp of the last user scroll gesture (wheel/touch): scroll
|
|
58
|
+
* events outside this window are programmatic (our own pin/anchor) and
|
|
59
|
+
* must not re-engage following — anchoring the newest message clamps AT
|
|
60
|
+
* the bottom in any longer conversation. */
|
|
61
|
+
this._lastUserScrollIntentTs = 0;
|
|
62
|
+
this._markUserScrollIntent = () => {
|
|
63
|
+
this._lastUserScrollIntentTs = Date.now();
|
|
64
|
+
};
|
|
65
|
+
this._onTrailScroll = () => {
|
|
66
|
+
const container = this._trailContainer();
|
|
67
|
+
if (!container) return;
|
|
68
|
+
const atBottom = this._isAtBottom(container);
|
|
69
|
+
this._messagesBelow = this._scrollable && !atBottom ? this._countMessagesBelow(container) : 0;
|
|
70
|
+
const isUserDriven = Date.now() - this._lastUserScrollIntentTs < _AdaptiveChatTrail._USER_SCROLL_INTENT_WINDOW_MS;
|
|
71
|
+
if (isUserDriven) {
|
|
72
|
+
this._anchorHold = false;
|
|
73
|
+
this._stickToBottom = atBottom;
|
|
74
|
+
if (atBottom) this._anchorActive = false;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/** Pill tap: jump to the newest content and follow for the rest of the turn. */
|
|
78
|
+
this._onScrollToRecent = () => {
|
|
79
|
+
this._stickToBottom = true;
|
|
80
|
+
this._anchorHold = false;
|
|
81
|
+
this._anchorActive = false;
|
|
82
|
+
requestAnimationFrame(() => {
|
|
83
|
+
const container = this._trailContainer();
|
|
84
|
+
if (!container) return;
|
|
85
|
+
container.scrollTop = container.scrollHeight;
|
|
86
|
+
this._syncContentBelow();
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
// No `createRenderRoot()` override → default open shadow root. CSS
|
|
90
|
+
// custom properties (--sc-tile-text-color, --sc-content-bubble-*,
|
|
91
|
+
// etc.) inherit through the shadow boundary, so canvas-level theming
|
|
92
|
+
// continues to cascade into the chips without the light-DOM hack.
|
|
93
|
+
// See the `static styles` block above for the in-shadow stylesheet.
|
|
94
|
+
this._onExpand = () => {
|
|
95
|
+
this.expanded = true;
|
|
96
|
+
this.dispatchEvent(new CustomEvent("trail-expand", { bubbles: true, composed: true }));
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Approve a pending client tool call. The trail emits a generic
|
|
100
|
+
* event so the host can decide whether to forward to chatSession
|
|
101
|
+
* (the common path) or override. Keeps the trail itself free of
|
|
102
|
+
* direct chatSession coupling — it's a pure view component.
|
|
103
|
+
*/
|
|
104
|
+
this._onToolCallClick = (tc) => {
|
|
105
|
+
if (tc.status !== "pending") return;
|
|
106
|
+
this.dispatchEvent(
|
|
107
|
+
new CustomEvent("trail-toolcall-approved", {
|
|
108
|
+
detail: { toolCallId: tc.id, approved: true },
|
|
109
|
+
bubbles: true,
|
|
110
|
+
composed: true
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
this._onCollapse = () => {
|
|
115
|
+
this.expanded = false;
|
|
116
|
+
this.dispatchEvent(new CustomEvent("trail-collapse", { bubbles: true, composed: true }));
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* instanceId → id of the message that was newest when the receipt first
|
|
120
|
+
* appeared. This is what makes a receipt read like a chat bubble: it renders
|
|
121
|
+
* inline right after its anchor message, so newer messages push it up into
|
|
122
|
+
* history instead of it staying pinned below everything. Assigned once per
|
|
123
|
+
* instanceId (in willUpdate, the only place that sees both `messages` and
|
|
124
|
+
* `tileReceipts` at the moment a tile mounts) and never re-homed — a
|
|
125
|
+
* dismiss/bring-back keeps the chip where it was created. `null` = the tile
|
|
126
|
+
* mounted before the first chat message (canvas replay); those fall to the
|
|
127
|
+
* tail strip (see render).
|
|
128
|
+
*/
|
|
129
|
+
this._receiptAnchors = /* @__PURE__ */ new Map();
|
|
130
|
+
/**
|
|
131
|
+
* instanceId → anchor message id for inline widgets — same mechanism as
|
|
132
|
+
* `_receiptAnchors`. The inline carousel renders right after the message
|
|
133
|
+
* that was newest when it mounted, so it reads like a rich reply and
|
|
134
|
+
* drifts up into history as later messages append.
|
|
135
|
+
*/
|
|
136
|
+
this._inlineAnchors = /* @__PURE__ */ new Map();
|
|
137
|
+
/**
|
|
138
|
+
* instanceId → the cached inline widget entry (render node + props target +
|
|
139
|
+
* optional registry cleanup). Cached so a prop patch (same instanceId, new
|
|
140
|
+
* props) updates the EXISTING element instead of tearing it down and
|
|
141
|
+
* re-creating it — which would reset the carousel's scroll position +
|
|
142
|
+
* internal state. Created lazily the first time we see the widget:
|
|
143
|
+
* `document.createElement(tag)` for a defined tag, or a registry mount for a
|
|
144
|
+
* widget-id. An unresolvable widget caches `null` so the fail-safe renders
|
|
145
|
+
* nothing without re-checking the registry.
|
|
146
|
+
*/
|
|
147
|
+
this._inlineElements = /* @__PURE__ */ new Map();
|
|
148
|
+
this._onTileReceiptClick = (instanceId) => {
|
|
149
|
+
this.dispatchEvent(
|
|
150
|
+
new CustomEvent("tile-receipt-activate", {
|
|
151
|
+
detail: { instanceId },
|
|
152
|
+
bubbles: true,
|
|
153
|
+
composed: true
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
this._onAlertReceiptClick = (instanceId) => {
|
|
158
|
+
this.dispatchEvent(
|
|
159
|
+
new CustomEvent("alert-receipt-activate", {
|
|
160
|
+
detail: { instanceId },
|
|
161
|
+
bubbles: true,
|
|
162
|
+
composed: true
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
this._onIntroSuggestionClick = () => {
|
|
167
|
+
const s = this.introSuggestion;
|
|
168
|
+
if (!s) return;
|
|
169
|
+
this.dispatchEvent(
|
|
170
|
+
new CustomEvent("trail-intro-suggestion", {
|
|
171
|
+
detail: { prompt: s.prompt },
|
|
172
|
+
bubbles: true,
|
|
173
|
+
composed: true
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Visitor tapped a `suggest_replies` inline chip. Emit
|
|
179
|
+
* `trail-suggested-reply` carrying the chip text — the parent
|
|
180
|
+
* (AdaptiveChatBar) re-dispatches it as `chat-message-sent`, the
|
|
181
|
+
* SAME channel the input row uses, so the reply rides the existing
|
|
182
|
+
* send path (mountable → chatSession.send → transport). No fabricated
|
|
183
|
+
* AG-UI events; this is exactly what typing the text and hitting
|
|
184
|
+
* Enter would do. The chips clear on their own once the reply lands
|
|
185
|
+
* and a newer assistant message becomes the latest.
|
|
186
|
+
*/
|
|
187
|
+
this._onSuggestedReplyClick = (text) => {
|
|
188
|
+
const trimmed = text.trim();
|
|
189
|
+
if (!trimmed) return;
|
|
190
|
+
this.dispatchEvent(
|
|
191
|
+
new CustomEvent("trail-suggested-reply", {
|
|
192
|
+
detail: { text: trimmed },
|
|
193
|
+
bubbles: true,
|
|
194
|
+
composed: true
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
get _scrollable() {
|
|
200
|
+
return this.expanded || this.forceExpanded;
|
|
201
|
+
}
|
|
202
|
+
_trailContainer() {
|
|
203
|
+
return this.renderRoot.querySelector("[data-syntro-chat-trail]");
|
|
204
|
+
}
|
|
205
|
+
_isAtBottom(container) {
|
|
206
|
+
return container.scrollHeight - container.scrollTop - container.clientHeight <= _AdaptiveChatTrail._AT_BOTTOM_EPSILON_PX;
|
|
207
|
+
}
|
|
208
|
+
/** Count chips whose bottom edge hasn't entered the viewport yet. */
|
|
209
|
+
_countMessagesBelow(container) {
|
|
210
|
+
const viewportBottom = container.getBoundingClientRect().top + container.clientHeight;
|
|
211
|
+
let count = 0;
|
|
212
|
+
for (const chip of this.renderRoot.querySelectorAll("[data-trail-chip]")) {
|
|
213
|
+
if (chip.getBoundingClientRect().bottom > viewportBottom + 4) count += 1;
|
|
214
|
+
}
|
|
215
|
+
return count > 0 ? count : 1;
|
|
216
|
+
}
|
|
217
|
+
_syncContentBelow() {
|
|
218
|
+
const container = this._trailContainer();
|
|
219
|
+
if (!container) return;
|
|
220
|
+
this._messagesBelow = this._scrollable && !this._isAtBottom(container) ? this._countMessagesBelow(container) : 0;
|
|
221
|
+
}
|
|
222
|
+
/** Scroll so the anchored user chip sits at the top of the trail
|
|
223
|
+
* viewport. Idempotent — safe to re-apply on every update while the
|
|
224
|
+
* hold is live (content grows BELOW the anchor, so the target is
|
|
225
|
+
* stable; re-applying also covers late geometry, e.g. expanding
|
|
226
|
+
* mid-turn from the ambient strip). */
|
|
227
|
+
_applyAnchor() {
|
|
228
|
+
requestAnimationFrame(() => {
|
|
229
|
+
const container = this._trailContainer();
|
|
230
|
+
if (!container || this._anchorMsgId === null) return;
|
|
231
|
+
const chip = this.renderRoot.querySelector(
|
|
232
|
+
`[data-trail-chip-id="${String(this._anchorMsgId)}"]`
|
|
233
|
+
);
|
|
234
|
+
if (!chip) return;
|
|
235
|
+
const top = chip.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
|
|
236
|
+
container.scrollTop = top;
|
|
237
|
+
this._syncContentBelow();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
_pinToBottom() {
|
|
241
|
+
requestAnimationFrame(() => {
|
|
242
|
+
const container = this._trailContainer();
|
|
243
|
+
if (!container) return;
|
|
244
|
+
container.scrollTop = container.scrollHeight;
|
|
245
|
+
this._syncContentBelow();
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Whether to render the thinking-dots chip after the last message.
|
|
250
|
+
* True iff a request is in flight AND the assistant hasn't started
|
|
251
|
+
* streaming a reply yet — once the first delta lands, the streaming
|
|
252
|
+
* chip's caret takes over and the thinking indicator hides so the
|
|
253
|
+
* visitor isn't shown both at once. Also hidden when the last
|
|
254
|
+
* message is an error/system message, because the request that
|
|
255
|
+
* "errored" is already over from the user's perspective.
|
|
256
|
+
*/
|
|
257
|
+
_shouldShowThinking() {
|
|
258
|
+
if (!this.inFlight) return false;
|
|
259
|
+
const last = this.messages[this.messages.length - 1];
|
|
260
|
+
if (!last) return true;
|
|
261
|
+
if (last.role === "assistant" && last.status === "streaming") {
|
|
262
|
+
return (last.text ?? "").trim().length === 0;
|
|
263
|
+
}
|
|
264
|
+
if (last.role === "system" || last.status === "error") return false;
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
disconnectedCallback() {
|
|
268
|
+
super.disconnectedCallback();
|
|
269
|
+
for (const entry of this._inlineElements.values()) {
|
|
270
|
+
entry?.cleanup?.();
|
|
271
|
+
}
|
|
272
|
+
this._inlineElements.clear();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The id of the newest message the render will actually PAINT — the only
|
|
276
|
+
* valid anchor target. Anchoring to the raw last message is wrong-by-
|
|
277
|
+
* construction for tool-call turns: mid-turn the newest message is often a
|
|
278
|
+
* whitespace-only assistant shell (some providers emit whitespace text
|
|
279
|
+
* deltas around tool calls) that the empty-bubble filter drops, so an
|
|
280
|
+
* anchor recorded on it can never be in the painted set and the receipt
|
|
281
|
+
* falls to the tail strip forever — resurrecting BUG-1783015943 (seen live
|
|
282
|
+
* on vela desktop, BUG-1783630187). `isSuggestionHost=false` here: a
|
|
283
|
+
* message whose ONLY content is reply chips is too ephemeral to anchor on
|
|
284
|
+
* (the chips clear when the visitor answers), so walk past it too.
|
|
285
|
+
*/
|
|
286
|
+
_newestRenderableMessageId() {
|
|
287
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
288
|
+
const m = this.messages[i];
|
|
289
|
+
if (m.role !== "assistant" || assistantChipRendersContent(m, false)) return m.id;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
willUpdate(changed) {
|
|
294
|
+
if (changed.has("messages")) {
|
|
295
|
+
const lastId = this._newestRenderableMessageId();
|
|
296
|
+
if (lastId !== null) {
|
|
297
|
+
for (const [key, anchor] of this._receiptAnchors) {
|
|
298
|
+
if (anchor === null) this._receiptAnchors.set(key, lastId);
|
|
299
|
+
}
|
|
300
|
+
for (const [key, anchor] of this._inlineAnchors) {
|
|
301
|
+
if (anchor === null) this._inlineAnchors.set(key, lastId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (changed.has("tileReceipts")) {
|
|
306
|
+
const lastId = this._newestRenderableMessageId();
|
|
307
|
+
const live = /* @__PURE__ */ new Set();
|
|
308
|
+
for (const r of this.tileReceipts) {
|
|
309
|
+
live.add(r.instanceId);
|
|
310
|
+
if (!this._receiptAnchors.has(r.instanceId)) {
|
|
311
|
+
this._receiptAnchors.set(r.instanceId, lastId);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const key of this._receiptAnchors.keys()) {
|
|
315
|
+
if (!live.has(key)) this._receiptAnchors.delete(key);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (changed.has("inlineWidgets")) {
|
|
319
|
+
const lastId = this._newestRenderableMessageId();
|
|
320
|
+
const live = /* @__PURE__ */ new Set();
|
|
321
|
+
for (const w of this.inlineWidgets) {
|
|
322
|
+
live.add(w.instanceId);
|
|
323
|
+
if (!this._inlineAnchors.has(w.instanceId)) {
|
|
324
|
+
this._inlineAnchors.set(w.instanceId, lastId);
|
|
325
|
+
}
|
|
326
|
+
this._ensureInlineElement(w);
|
|
327
|
+
}
|
|
328
|
+
for (const key of this._inlineAnchors.keys()) {
|
|
329
|
+
if (!live.has(key)) this._inlineAnchors.delete(key);
|
|
330
|
+
}
|
|
331
|
+
for (const key of this._inlineElements.keys()) {
|
|
332
|
+
if (!live.has(key)) {
|
|
333
|
+
this._inlineElements.get(key)?.cleanup?.();
|
|
334
|
+
this._inlineElements.delete(key);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Create (once) or refresh the cached element for an inline widget.
|
|
341
|
+
*
|
|
342
|
+
* Two mount paths, both keeping the chat bundle free of any widget-package
|
|
343
|
+
* import:
|
|
344
|
+
* - Direct tag (self-defining, server-mounted widgets like the reco
|
|
345
|
+
* carousel whose declared `widget` IS its custom-element tag):
|
|
346
|
+
* `document.createElement(tag)`.
|
|
347
|
+
* - Widget-id (admin-template widgets like `adaptive-product:card` or
|
|
348
|
+
* `adaptive-faq:accordion`, whose `widget` is a runtime widget-id, NOT a
|
|
349
|
+
* valid custom-element tag): mount through the runtime widget registry
|
|
350
|
+
* via {@link mountInlineWidget}, which resolves the id to its real
|
|
351
|
+
* element exactly like the deck `SyntroTileCard` path. Without this,
|
|
352
|
+
* `createElement('adaptive-product:card')` yields an inert unknown
|
|
353
|
+
* element and the tile renders permanently blank (the "inline mode
|
|
354
|
+
* shows nothing but the reco" bug).
|
|
355
|
+
*
|
|
356
|
+
* Fail-safe: an id that resolves to neither a defined tag nor a registered
|
|
357
|
+
* widget caches `null` and renders nothing (no broken box).
|
|
358
|
+
*/
|
|
359
|
+
_ensureInlineElement(w) {
|
|
360
|
+
let entry = this._inlineElements.get(w.instanceId);
|
|
361
|
+
if (entry === void 0) {
|
|
362
|
+
if (customElements.get(w.tag)) {
|
|
363
|
+
const el = document.createElement(w.tag);
|
|
364
|
+
el.setAttribute("data-inline-widget-el", "");
|
|
365
|
+
entry = { node: el, propsTarget: el };
|
|
366
|
+
} else if (this.mountInlineWidget) {
|
|
367
|
+
const container = document.createElement("div");
|
|
368
|
+
container.setAttribute("data-inline-widget-el", "");
|
|
369
|
+
const mounted = this.mountInlineWidget(w.tag, container, w.props);
|
|
370
|
+
entry = mounted ? { node: container, propsTarget: mounted.element, cleanup: mounted.cleanup } : null;
|
|
371
|
+
} else {
|
|
372
|
+
entry = null;
|
|
373
|
+
}
|
|
374
|
+
this._inlineElements.set(w.instanceId, entry);
|
|
375
|
+
}
|
|
376
|
+
if (!entry) return;
|
|
377
|
+
entry.propsTarget.props = w.props;
|
|
378
|
+
}
|
|
379
|
+
updated(changed) {
|
|
380
|
+
if (!changed.has("expanded") && !changed.has("forceExpanded") && !changed.has("messages")) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (changed.has("messages")) {
|
|
384
|
+
let lastUserIdx = -1;
|
|
385
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
386
|
+
if (this.messages[i].role === "user") {
|
|
387
|
+
lastUserIdx = i;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const lastUser = lastUserIdx >= 0 ? this.messages[lastUserIdx] : void 0;
|
|
392
|
+
const isLiveTurn = lastUser !== void 0 && this.messages.slice(lastUserIdx + 1).every((m) => m.role !== "user" && m.status === "streaming");
|
|
393
|
+
if (lastUser && isLiveTurn && lastUser.id !== this._anchorMsgId) {
|
|
394
|
+
this._anchorMsgId = lastUser.id;
|
|
395
|
+
this._anchorActive = true;
|
|
396
|
+
this._anchorHold = true;
|
|
397
|
+
this._stickToBottom = false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!this._scrollable) {
|
|
401
|
+
this._pinToBottom();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (this._anchorActive && this._anchorHold) {
|
|
405
|
+
this._applyAnchor();
|
|
406
|
+
} else if (this._stickToBottom) {
|
|
407
|
+
this._pinToBottom();
|
|
408
|
+
} else {
|
|
409
|
+
requestAnimationFrame(() => this._syncContentBelow());
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** Render a set of inline widgets (reco carousel etc.) as
|
|
413
|
+
* `[data-trail-widget]` blocks, anchored to a message position like
|
|
414
|
+
* receipts. Each block hosts the cached custom element for that
|
|
415
|
+
* instanceId — a defined tag mounts inline; an undefined tag renders
|
|
416
|
+
* nothing (fail-safe). Rendered at every trail site (inline per anchor,
|
|
417
|
+
* tail, empty-state) so placement can't diverge. */
|
|
418
|
+
_renderInlineWidgetStrip(list) {
|
|
419
|
+
if (list.length === 0) return nothing;
|
|
420
|
+
const blocks = list.map((w) => {
|
|
421
|
+
const entry = this._inlineElements.get(w.instanceId);
|
|
422
|
+
if (!entry) return nothing;
|
|
423
|
+
return html`<div
|
|
424
|
+
data-trail-widget
|
|
425
|
+
data-instance-id=${w.instanceId}
|
|
426
|
+
style=${styleMap(inlineWidgetBlockStyles())}
|
|
427
|
+
>${entry.node}</div>`;
|
|
428
|
+
}).filter((b) => b !== nothing);
|
|
429
|
+
if (blocks.length === 0) return nothing;
|
|
430
|
+
return html`${blocks}`;
|
|
431
|
+
}
|
|
432
|
+
/** A row of receipt chips, used at every trail render site (inline per
|
|
433
|
+
* anchor message, the tail fallback, and the empty-state branches) so the
|
|
434
|
+
* markup + styling can never diverge. Returns `nothing` for an empty list. */
|
|
435
|
+
_renderReceiptStrip(list) {
|
|
436
|
+
if (list.length === 0) return nothing;
|
|
437
|
+
return html`<div data-trail-tile-receipts style=${styleMap(tileReceiptStripStyles())}>
|
|
438
|
+
${list.map((tr) => this._renderReceiptChip(tr))}
|
|
439
|
+
</div>`;
|
|
440
|
+
}
|
|
441
|
+
/** One receipt chip — a single, unified, single-line design for both kinds
|
|
442
|
+
* (nav "alert" + mounted "tile"). Same container/title/action style so they
|
|
443
|
+
* read as one chip family; the icon + action label carry the kind. No
|
|
444
|
+
* subtitle: the old "Brought up earlier" line was hardcoded (wrong for
|
|
445
|
+
* same-turn mounts) and made the chip taller — dropping it is the "shorter"
|
|
446
|
+
* chip. Rendered at every trail site so the kinds can't diverge. */
|
|
447
|
+
_renderReceiptChip(tr) {
|
|
448
|
+
if (tr.kind === "alert") {
|
|
449
|
+
return html`<button
|
|
450
|
+
type="button"
|
|
451
|
+
data-trail-alert-receipt
|
|
452
|
+
data-instance-id=${tr.instanceId}
|
|
453
|
+
@click=${() => this._onAlertReceiptClick(tr.instanceId)}
|
|
454
|
+
style=${styleMap(tileReceiptChipStyles())}
|
|
455
|
+
>
|
|
456
|
+
<span style=${styleMap(alertReceiptIconStyles())} aria-hidden="true">⚡</span>
|
|
457
|
+
<span style=${styleMap(receiptChipTitleStyles("alert"))}>${tr.title}</span>
|
|
458
|
+
<span style=${styleMap(tileReceiptActionStyles(false))}>Go →</span>
|
|
459
|
+
</button>`;
|
|
460
|
+
}
|
|
461
|
+
return html`<button
|
|
462
|
+
type="button"
|
|
463
|
+
data-trail-tile-receipt
|
|
464
|
+
data-instance-id=${tr.instanceId}
|
|
465
|
+
@click=${() => this._onTileReceiptClick(tr.instanceId)}
|
|
466
|
+
style=${styleMap(tileReceiptChipStyles())}
|
|
467
|
+
>
|
|
468
|
+
<span style=${styleMap(tileReceiptGlyphStyles())} aria-hidden="true"
|
|
469
|
+
><i style=${styleMap(tileReceiptGlyphLayerStyles(0))}></i
|
|
470
|
+
><i style=${styleMap(tileReceiptGlyphLayerStyles(1))}></i
|
|
471
|
+
></span>
|
|
472
|
+
<span style=${styleMap(receiptChipTitleStyles("tile"))}>${tr.title}</span>
|
|
473
|
+
<span style=${styleMap(tileReceiptActionStyles(tr.dismissed))}
|
|
474
|
+
>${tr.dismissed ? "Bring back \u21BA" : "View \u25B8"}</span
|
|
475
|
+
>
|
|
476
|
+
</button>`;
|
|
477
|
+
}
|
|
478
|
+
render() {
|
|
479
|
+
if (this.messages.length === 0 && !this.greeting && !this.introSuggestion && this.tileReceipts.length === 0 && this.inlineWidgets.length === 0)
|
|
480
|
+
return nothing;
|
|
481
|
+
if (this.messages.length === 0 && (this.greeting || this.introSuggestion)) {
|
|
482
|
+
return html`<div data-syntro-chat-trail data-state="empty" style=${styleMap({
|
|
483
|
+
display: "flex",
|
|
484
|
+
flexDirection: "column",
|
|
485
|
+
alignItems: "center",
|
|
486
|
+
justifyContent: "center",
|
|
487
|
+
textAlign: "center",
|
|
488
|
+
gap: "12px",
|
|
489
|
+
flex: "1 1 auto",
|
|
490
|
+
width: "100%",
|
|
491
|
+
padding: "24px 16px",
|
|
492
|
+
pointerEvents: "auto"
|
|
493
|
+
})}>
|
|
494
|
+
<div data-trail-empty-icon style=${styleMap(emptyIconStyles())} aria-hidden="true">✦</div>
|
|
495
|
+
${this.greeting ? html`<h2
|
|
496
|
+
data-trail-empty-title
|
|
497
|
+
style=${styleMap(emptyTitleStyles())}
|
|
498
|
+
>${unsafeHTML(renderMarkdown(stripTrailingWhitespace(this.greeting)))}</h2>` : nothing}
|
|
499
|
+
${this.introSuggestion ? html`<button
|
|
500
|
+
type="button"
|
|
501
|
+
data-trail-intro-suggestion
|
|
502
|
+
@click=${this._onIntroSuggestionClick}
|
|
503
|
+
style=${styleMap(introSuggestionStyles())}
|
|
504
|
+
>${this.introSuggestion.label}</button>` : nothing}
|
|
505
|
+
${this._renderReceiptStrip(this.tileReceipts)}
|
|
506
|
+
${this._renderInlineWidgetStrip(this.inlineWidgets)}
|
|
507
|
+
</div>`;
|
|
508
|
+
}
|
|
509
|
+
if (this.messages.length === 0 && (this.tileReceipts.length > 0 || this.inlineWidgets.length > 0)) {
|
|
510
|
+
return html`<div data-syntro-chat-trail data-state="empty" style=${styleMap({
|
|
511
|
+
display: "flex",
|
|
512
|
+
flexDirection: "column",
|
|
513
|
+
alignItems: "flex-start",
|
|
514
|
+
gap: "6px",
|
|
515
|
+
width: "100%",
|
|
516
|
+
padding: "8px 0",
|
|
517
|
+
pointerEvents: "auto"
|
|
518
|
+
})}>
|
|
519
|
+
${this._renderReceiptStrip(this.tileReceipts)}
|
|
520
|
+
${this._renderInlineWidgetStrip(this.inlineWidgets)}
|
|
521
|
+
</div>`;
|
|
522
|
+
}
|
|
523
|
+
let suggestionHostId = null;
|
|
524
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
525
|
+
const m = this.messages[i];
|
|
526
|
+
if (m.role === "user") break;
|
|
527
|
+
if (m.role === "assistant" && extractSuggestedReplies(m.toolCalls).length > 0) {
|
|
528
|
+
suggestionHostId = m.id;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const renderable = this.messages.filter(
|
|
533
|
+
(m) => m.role !== "assistant" || assistantChipRendersContent(m, m.id === suggestionHostId)
|
|
534
|
+
);
|
|
535
|
+
const visible = this.expanded || this.forceExpanded ? renderable : renderable.slice(-this.visibleCount);
|
|
536
|
+
const hidden = renderable.length - visible.length;
|
|
537
|
+
const visibleIds = new Set(visible.map((m) => m.id));
|
|
538
|
+
const receiptsByAnchor = /* @__PURE__ */ new Map();
|
|
539
|
+
const tailReceipts = [];
|
|
540
|
+
for (const r of this.tileReceipts) {
|
|
541
|
+
const anchor = this._receiptAnchors.get(r.instanceId) ?? null;
|
|
542
|
+
if (anchor !== null && visibleIds.has(anchor)) {
|
|
543
|
+
const list = receiptsByAnchor.get(anchor);
|
|
544
|
+
if (list) list.push(r);
|
|
545
|
+
else receiptsByAnchor.set(anchor, [r]);
|
|
546
|
+
} else {
|
|
547
|
+
tailReceipts.push(r);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const inlineByAnchor = /* @__PURE__ */ new Map();
|
|
551
|
+
const tailInlineWidgets = [];
|
|
552
|
+
for (const w of this.inlineWidgets) {
|
|
553
|
+
const anchor = this._inlineAnchors.get(w.instanceId) ?? null;
|
|
554
|
+
if (anchor !== null && visibleIds.has(anchor)) {
|
|
555
|
+
const list = inlineByAnchor.get(anchor);
|
|
556
|
+
if (list) list.push(w);
|
|
557
|
+
else inlineByAnchor.set(anchor, [w]);
|
|
558
|
+
} else {
|
|
559
|
+
tailInlineWidgets.push(w);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const baseStyles = {
|
|
563
|
+
display: "flex",
|
|
564
|
+
flexDirection: "column",
|
|
565
|
+
gap: "6px",
|
|
566
|
+
// NO `justifyContent: flex-end` here — combined with `overflow: auto`
|
|
567
|
+
// it's a known cross-browser bug: oversized flex children get
|
|
568
|
+
// pushed ABOVE the container without contributing to scrollHeight,
|
|
569
|
+
// so the scrollbar never appears. We rely on `:first-child {
|
|
570
|
+
// margin-top: auto }` (injected as global CSS) to push short
|
|
571
|
+
// content to the bottom of the box while leaving overflow handling
|
|
572
|
+
// to the native scroll container.
|
|
573
|
+
width: "100%",
|
|
574
|
+
// The trail itself has no chrome — chips sit on the host page bg.
|
|
575
|
+
padding: "0",
|
|
576
|
+
pointerEvents: "auto"
|
|
577
|
+
};
|
|
578
|
+
const modeStyles = this.forceExpanded ? {
|
|
579
|
+
// Full-screen host: fill all available vertical space and
|
|
580
|
+
// scroll the overflow. ``flex: 1 1 auto`` (basis AUTO, not 0) —
|
|
581
|
+
// in a DEFINITE-height parent (full-screen panel) grow:1 still
|
|
582
|
+
// fills all free space and margin-top:auto bottom-anchors, so
|
|
583
|
+
// fill behavior is unchanged; but basis auto also lets the trail
|
|
584
|
+
// report its CONTENT height as its natural size, so in an
|
|
585
|
+
// INDEFINITE-height parent (velvet's shrink-wrap bottom sheet)
|
|
586
|
+
// the trail contributes real content height instead of collapsing
|
|
587
|
+
// to 0 — which is what lets the sheet hug its content with no void.
|
|
588
|
+
// shrink:1 + min-height:0 still lets the trail shrink first when
|
|
589
|
+
// the parent runs out of room; the input row is absolute-pinned
|
|
590
|
+
// (never pushed off) and the header shrinks after the trail.
|
|
591
|
+
flex: "1 1 auto",
|
|
592
|
+
minHeight: "0",
|
|
593
|
+
overflowY: "auto",
|
|
594
|
+
// ``contain`` prevents scroll from bubbling out to the host
|
|
595
|
+
// page when the trail hits top or bottom. Required for the
|
|
596
|
+
// panel-scroll-leak regression test in
|
|
597
|
+
// SyntroBottomSheet.test.ts.
|
|
598
|
+
overscrollBehavior: "contain",
|
|
599
|
+
paddingTop: "8px",
|
|
600
|
+
borderTop: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))",
|
|
601
|
+
scrollBehavior: "smooth",
|
|
602
|
+
scrollbarWidth: "thin"
|
|
603
|
+
} : this.expanded ? {
|
|
604
|
+
maxHeight: "min(320px, 40vh)",
|
|
605
|
+
overflowY: "auto",
|
|
606
|
+
paddingTop: "8px",
|
|
607
|
+
borderTop: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))",
|
|
608
|
+
scrollBehavior: "smooth",
|
|
609
|
+
scrollbarWidth: "thin"
|
|
610
|
+
} : {
|
|
611
|
+
// Collapsed mode = ambient peek surface. Never a scrollbar:
|
|
612
|
+
// long messages slide up behind the blur veil instead of
|
|
613
|
+
// exposing a scrollbar. The "↑ N more · expand" affordance
|
|
614
|
+
// is the only entry into history; clicking it switches to
|
|
615
|
+
// expanded mode where scroll IS allowed.
|
|
616
|
+
// Height kept short so the trail reads as a peek strip above
|
|
617
|
+
// the chat bar, not a panel — long single replies fade into
|
|
618
|
+
// the blur veil at the top, user expands to read the rest.
|
|
619
|
+
maxHeight: "min(140px, 22vh)",
|
|
620
|
+
overflow: "hidden"
|
|
621
|
+
};
|
|
622
|
+
const containerStyles = { ...baseStyles, ...modeStyles };
|
|
623
|
+
const frameStyles = this.forceExpanded ? {
|
|
624
|
+
position: "relative",
|
|
625
|
+
width: "100%",
|
|
626
|
+
display: "flex",
|
|
627
|
+
flexDirection: "column",
|
|
628
|
+
// basis AUTO (see the scroller's modeStyles note): fills a definite
|
|
629
|
+
// parent, contributes content height in an indefinite one (velvet
|
|
630
|
+
// shrink-wrap). Every fill layer in the chain must be basis-auto or
|
|
631
|
+
// the min-content of a basis-0 layer collapses the whole chain to 0.
|
|
632
|
+
flex: "1 1 auto",
|
|
633
|
+
minHeight: "0"
|
|
634
|
+
} : {
|
|
635
|
+
position: "relative",
|
|
636
|
+
width: "100%"
|
|
637
|
+
};
|
|
638
|
+
return html`
|
|
639
|
+
${// Single toggle button above the frame. Same DOM node morphs:
|
|
640
|
+
// collapsed → "↑ N more · expand" / "↑ expand" (calls _onExpand)
|
|
641
|
+
// expanded → "⌄ minimize" (calls _onCollapse)
|
|
642
|
+
// Lives OUTSIDE the frame so the absolute blur veil (top:0,
|
|
643
|
+
// z-index:2 inside the frame) doesn't cover it. Shown whenever
|
|
644
|
+
// there's anything in the trail — the user expects this affordance
|
|
645
|
+
// available regardless of how many chips are on screen.
|
|
646
|
+
// When ``forceExpanded`` is set by the host (full-screen panel,
|
|
647
|
+
// etc.), there's nowhere to collapse INTO and the affordance
|
|
648
|
+
// would just confuse the user — suppress it entirely.
|
|
649
|
+
this.messages.length === 0 || this.forceExpanded ? nothing : this.expanded ? html`<button
|
|
650
|
+
type="button"
|
|
651
|
+
data-trail-toggle
|
|
652
|
+
data-trail-collapse
|
|
653
|
+
@click=${this._onCollapse}
|
|
654
|
+
style=${styleMap(moreStyles())}
|
|
655
|
+
>⌄ minimize</button>` : html`<button
|
|
656
|
+
type="button"
|
|
657
|
+
data-trail-toggle
|
|
658
|
+
data-trail-more
|
|
659
|
+
@click=${this._onExpand}
|
|
660
|
+
style=${styleMap(moreStyles())}
|
|
661
|
+
>${hidden > 0 ? html`↑ ${hidden} more · expand` : html`↑ expand`}</button>`}
|
|
662
|
+
<div data-syntro-chat-trail-frame style=${styleMap(frameStyles)}>
|
|
663
|
+
<div
|
|
664
|
+
data-syntro-chat-trail
|
|
665
|
+
style=${styleMap(containerStyles)}
|
|
666
|
+
@scroll=${this._onTrailScroll}
|
|
667
|
+
@wheel=${this._markUserScrollIntent}
|
|
668
|
+
@touchmove=${this._markUserScrollIntent}
|
|
669
|
+
>
|
|
670
|
+
${visible.map((m, i) => {
|
|
671
|
+
const pos = this.expanded || this.forceExpanded ? 0 : visible.length - 1 - i;
|
|
672
|
+
const state = chipVisualState(m);
|
|
673
|
+
const isStreaming = state.kind === "streaming";
|
|
674
|
+
const {
|
|
675
|
+
pendingToolCalls: toolCalls,
|
|
676
|
+
receipts,
|
|
677
|
+
replyChips
|
|
678
|
+
} = computeAssistantChipParts(m, m.id === suggestionHostId);
|
|
679
|
+
const renderedText = m.role === "assistant" ? (
|
|
680
|
+
// Never paint a degenerate serialized-response dump, even if the
|
|
681
|
+
// turn also carries a legit part (receipt/chip) that keeps it in
|
|
682
|
+
// the renderable set. Mirrors the filter's drop signal.
|
|
683
|
+
looksLikeSerializedModelResponse(m.text ?? "") ? nothing : html`${unsafeHTML(renderMarkdown(stripTrailingWhitespace(m.text)))}`
|
|
684
|
+
) : html`${m.text}`;
|
|
685
|
+
const chipStyle = isStreaming ? {
|
|
686
|
+
...chipStyles(state, pos),
|
|
687
|
+
animation: "syntro-trail-chip-enter 220ms cubic-bezier(0.22, 1, 0.36, 1) both"
|
|
688
|
+
} : chipStyles(state, pos);
|
|
689
|
+
return html`<div
|
|
690
|
+
data-trail-chip
|
|
691
|
+
data-trail-chip-id=${String(m.id)}
|
|
692
|
+
data-role=${m.role}
|
|
693
|
+
data-status=${m.status ?? "complete"}
|
|
694
|
+
style=${styleMap(chipStyle)}
|
|
695
|
+
>${renderedText}${isStreaming ? html`<span
|
|
696
|
+
data-trail-caret
|
|
697
|
+
aria-hidden="true"
|
|
698
|
+
style=${styleMap(caretStyles())}
|
|
699
|
+
></span>` : nothing}${toolCalls.length > 0 ? html`<div data-trail-toolcalls style=${styleMap(toolCallStripStyles())}>
|
|
700
|
+
${toolCalls.map(
|
|
701
|
+
(tc) => html`<button
|
|
702
|
+
type="button"
|
|
703
|
+
data-trail-toolcall
|
|
704
|
+
data-tool-id=${tc.id}
|
|
705
|
+
data-tool-status=${tc.status}
|
|
706
|
+
@click=${() => this._onToolCallClick(tc)}
|
|
707
|
+
style=${styleMap(pendingToolCallChipStyles())}
|
|
708
|
+
title="${tc.name} · ${tc.status}"
|
|
709
|
+
>? ${tc.name}</button>`
|
|
710
|
+
)}
|
|
711
|
+
</div>` : nothing}${receipts.length > 0 ? html`<div data-trail-receipts style=${styleMap(receiptStripStyles())}>
|
|
712
|
+
${receipts.map(
|
|
713
|
+
(r) => html`<div
|
|
714
|
+
data-trail-receipt
|
|
715
|
+
data-receipt-type=${r.type}
|
|
716
|
+
style=${styleMap(receiptStyles())}
|
|
717
|
+
><span aria-hidden="true" style=${styleMap(receiptIconStyles())}>✓</span
|
|
718
|
+
><span>${receiptText(r)}</span></div>`
|
|
719
|
+
)}
|
|
720
|
+
</div>` : nothing}${replyChips.length > 0 ? html`<div data-trail-reply-chips style=${styleMap(replyChipStripStyles())}>
|
|
721
|
+
${replyChips.map(
|
|
722
|
+
(reply) => html`<button
|
|
723
|
+
type="button"
|
|
724
|
+
data-trail-reply-chip
|
|
725
|
+
@click=${() => this._onSuggestedReplyClick(reply)}
|
|
726
|
+
style=${styleMap(replyChipStyles())}
|
|
727
|
+
>${reply}</button>`
|
|
728
|
+
)}
|
|
729
|
+
</div>` : nothing}</div>${// Receipt chips that mounted while THIS message was newest render
|
|
730
|
+
// inline right here, so they read as part of the flow and drift up
|
|
731
|
+
// as later messages append below them.
|
|
732
|
+
this._renderReceiptStrip(receiptsByAnchor.get(m.id) ?? [])}${// Inline widgets (reco carousel) anchored to this message render
|
|
733
|
+
// right here in the conversation flow — the whole point of the
|
|
734
|
+
// chat-inline slot: visible in the trail, not behind the panel.
|
|
735
|
+
this._renderInlineWidgetStrip(inlineByAnchor.get(m.id) ?? [])}`;
|
|
736
|
+
})}
|
|
737
|
+
${// Thinking indicator — fills the gap between the user's
|
|
738
|
+
// most recent message and the assistant's first token.
|
|
739
|
+
// Shows when chatSession.inFlight is true AND the last
|
|
740
|
+
// message is NOT a streaming assistant message (the
|
|
741
|
+
// assistant's own caret takes over once tokens arrive).
|
|
742
|
+
// Styled as an assistant chip with three bouncing dots
|
|
743
|
+
// instead of text, so the thread reads naturally — the
|
|
744
|
+
// visitor sees an "assistant" bubble that's clearly busy.
|
|
745
|
+
this._shouldShowThinking() ? html`<div
|
|
746
|
+
data-trail-thinking
|
|
747
|
+
data-role="assistant"
|
|
748
|
+
data-status="thinking"
|
|
749
|
+
aria-label=${this.thinkingText ? `Assistant is thinking: ${this.thinkingText}` : "Assistant is thinking"}
|
|
750
|
+
style=${styleMap(thinkingSlotStyles())}
|
|
751
|
+
>
|
|
752
|
+
${// When the backend has streamed reasoning narration into
|
|
753
|
+
// `thinkingText`, render the tail of it as a small italic
|
|
754
|
+
// line — the model's own intent ("Let me check what we have
|
|
755
|
+
// for student athletes…") narrates the gap better than
|
|
756
|
+
// silent dots. Falls back to the bouncing-dot pulse when
|
|
757
|
+
// no narration has arrived yet.
|
|
758
|
+
this.thinkingText ? html`<span
|
|
759
|
+
data-trail-thinking-text
|
|
760
|
+
style=${styleMap(thinkingTextStyles())}
|
|
761
|
+
>${truncateThinkingTail(this.thinkingText)}</span
|
|
762
|
+
>` : html`<span data-trail-thinking-dots style=${styleMap(thinkingDotsStyles())}>
|
|
763
|
+
<span style=${styleMap(thinkingDotStyles(0))}></span>
|
|
764
|
+
<span style=${styleMap(thinkingDotStyles(1))}></span>
|
|
765
|
+
<span style=${styleMap(thinkingDotStyles(2))}></span>
|
|
766
|
+
</span>`}
|
|
767
|
+
</div>` : nothing}
|
|
768
|
+
${// Tail strip: receipts with no in-slice anchor (mounted pre-chat, or
|
|
769
|
+
// their anchor message scrolled out of the ambient view). Keeps them
|
|
770
|
+
// reachable at the bottom — the old always-pinned position — rather
|
|
771
|
+
// than dropping them. In expanded/maximized mode every message is
|
|
772
|
+
// painted, so this is usually empty and receipts sit inline instead.
|
|
773
|
+
this._renderReceiptStrip(tailReceipts)}
|
|
774
|
+
${// Inline widgets with no in-slice anchor (mounted pre-chat, or their
|
|
775
|
+
// anchor scrolled out of the ambient view) render at the tail.
|
|
776
|
+
this._renderInlineWidgetStrip(tailInlineWidgets)}
|
|
777
|
+
</div>
|
|
778
|
+
${// The blur veil only fires in collapsed (ambient) mode.
|
|
779
|
+
// Expanded = the user explicitly asked for history; blurring
|
|
780
|
+
// the top of the panel would hide content (including the
|
|
781
|
+
// minimize button just above the trail's top edge) the user
|
|
782
|
+
// came here to see.
|
|
783
|
+
this.expanded || this.forceExpanded ? nothing : html`<div
|
|
784
|
+
data-trail-blur-veil
|
|
785
|
+
aria-hidden="true"
|
|
786
|
+
style=${styleMap(blurVeilStyles())}
|
|
787
|
+
></div>`}
|
|
788
|
+
${// "Scroll to most recent" pill (FEAT-1783535427): shown only in
|
|
789
|
+
// the scrollable modes when content extends below the viewport —
|
|
790
|
+
// the visitor is reading the reply top-down and this is the
|
|
791
|
+
// one-tap opt-in back to the newest content. Absolute inside the
|
|
792
|
+
// positioning frame so it floats over the scroller's bottom edge.
|
|
793
|
+
this._scrollable && this._messagesBelow > 0 ? html`<button
|
|
794
|
+
type="button"
|
|
795
|
+
data-trail-scroll-recent
|
|
796
|
+
aria-label="Scroll to most recent — ${this._messagesBelow} ${this._messagesBelow === 1 ? "message" : "messages"} below"
|
|
797
|
+
@click=${this._onScrollToRecent}
|
|
798
|
+
style=${styleMap(scrollRecentPillStyles())}
|
|
799
|
+
><span aria-hidden="true" style="font-size:11px;line-height:1;">⌄</span><span>${this._messagesBelow} ${this._messagesBelow === 1 ? "message" : "messages"}</span></button>` : nothing}
|
|
800
|
+
</div>
|
|
801
|
+
`;
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
// Trail-local styles. The chat-trail used to render into light DOM
|
|
805
|
+
// (`createRenderRoot() { return this }`) and inject these rules into
|
|
806
|
+
// `document.head` so chip + markdown styles could find their selectors.
|
|
807
|
+
// That broke the moment the trail was hosted inside any shadow root
|
|
808
|
+
// (sc-mount → custom-canvas → velvet-mobile → sc-mount → chat-bar →
|
|
809
|
+
// chat-trail is the production chain): CSS animations require the
|
|
810
|
+
// @keyframes rule to be in the SAME TreeScope as the element using
|
|
811
|
+
// it, and document-level keyframes don't cross a shadow boundary.
|
|
812
|
+
// Result was silent — `animation-name` parsed fine, the property got
|
|
813
|
+
// the right resolved value, but the browser never instantiated an
|
|
814
|
+
// Animation object because no matching keyframe was visible from
|
|
815
|
+
// inside the shadow tree. `getAnimations()` returned `[]`, dots sat
|
|
816
|
+
// frozen at base scale, the typing indicator looked dead.
|
|
817
|
+
//
|
|
818
|
+
// Shadow DOM with `static styles` is the right architecture here.
|
|
819
|
+
// CSS custom properties (--sc-tile-text-color, --sc-content-bubble-*,
|
|
820
|
+
// etc.) inherit through shadow boundaries by default, so the canvas
|
|
821
|
+
// theme cascade still reaches the chips without the light-DOM
|
|
822
|
+
// workaround. As a bonus the markdown reset rules can't leak out
|
|
823
|
+
// onto the host page, and the keyframes are owned by the trail's
|
|
824
|
+
// own tree scope where the chip + dot elements live.
|
|
825
|
+
_AdaptiveChatTrail.styles = css`
|
|
826
|
+
[data-syntro-chat-trail] > :first-child {
|
|
827
|
+
margin-top: auto;
|
|
828
|
+
}
|
|
829
|
+
[data-trail-chip] > p:first-child {
|
|
830
|
+
margin-top: 0;
|
|
831
|
+
}
|
|
832
|
+
[data-trail-chip] > p:last-child {
|
|
833
|
+
margin-bottom: 0;
|
|
834
|
+
}
|
|
835
|
+
[data-trail-chip] p {
|
|
836
|
+
margin: 4px 0;
|
|
837
|
+
}
|
|
838
|
+
[data-trail-chip] br + br {
|
|
839
|
+
display: none;
|
|
840
|
+
}
|
|
841
|
+
[data-trail-chip] ul,
|
|
842
|
+
[data-trail-chip] ol {
|
|
843
|
+
margin: 4px 0;
|
|
844
|
+
padding-left: 1.1em;
|
|
845
|
+
}
|
|
846
|
+
[data-trail-chip] li {
|
|
847
|
+
margin: 0;
|
|
848
|
+
}
|
|
849
|
+
[data-trail-chip] li + li {
|
|
850
|
+
margin-top: 1px;
|
|
851
|
+
}
|
|
852
|
+
[data-trail-chip] pre {
|
|
853
|
+
margin: 6px 0;
|
|
854
|
+
padding: 6px 8px;
|
|
855
|
+
background: var(--sc-content-code-background-block, rgba(0, 0, 0, 0.35));
|
|
856
|
+
border-radius: 6px;
|
|
857
|
+
overflow-x: auto;
|
|
858
|
+
font-size: 11px;
|
|
859
|
+
}
|
|
860
|
+
[data-trail-chip] code {
|
|
861
|
+
font-family: ui-monospace, SF Mono, Menlo, monospace;
|
|
862
|
+
font-size: 11px;
|
|
863
|
+
background: var(--sc-content-code-background, rgba(0, 0, 0, 0.25));
|
|
864
|
+
padding: 1px 4px;
|
|
865
|
+
border-radius: 3px;
|
|
866
|
+
}
|
|
867
|
+
[data-trail-chip] pre code {
|
|
868
|
+
background: transparent;
|
|
869
|
+
padding: 0;
|
|
870
|
+
}
|
|
871
|
+
[data-trail-chip] a {
|
|
872
|
+
color: var(--sc-content-link-color, var(--sc-color-primary, #b72e2a));
|
|
873
|
+
text-decoration: underline;
|
|
874
|
+
}
|
|
875
|
+
[data-trail-chip] strong {
|
|
876
|
+
font-weight: 600;
|
|
877
|
+
}
|
|
878
|
+
/* Streaming-caret blink for in-progress assistant chips. */
|
|
879
|
+
@keyframes syntro-trail-caret {
|
|
880
|
+
0% {
|
|
881
|
+
opacity: 1;
|
|
882
|
+
}
|
|
883
|
+
50% {
|
|
884
|
+
opacity: 0;
|
|
885
|
+
}
|
|
886
|
+
100% {
|
|
887
|
+
opacity: 1;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/* Thinking-dot scale + opacity pulse. The naked-dots redesign
|
|
891
|
+
relies entirely on this motion to signal "alive" — there's no
|
|
892
|
+
chip frame around the dots, so a subtle animation reads as
|
|
893
|
+
broken. 0.4 → 1.0 scale with 0.25 → 1.0 opacity gives an
|
|
894
|
+
unmistakable breathing cue. */
|
|
895
|
+
@keyframes syntro-trail-thinking-pulse {
|
|
896
|
+
0%,
|
|
897
|
+
100% {
|
|
898
|
+
transform: scale(0.4);
|
|
899
|
+
opacity: 0.25;
|
|
900
|
+
}
|
|
901
|
+
45% {
|
|
902
|
+
transform: scale(1);
|
|
903
|
+
opacity: 1;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
/* Chip entrance — fades a freshly-mounted chip up from below
|
|
907
|
+
so the thinking → first-streaming-chip handoff feels like one
|
|
908
|
+
continuous motion instead of an element swap. */
|
|
909
|
+
@keyframes syntro-trail-chip-enter {
|
|
910
|
+
from {
|
|
911
|
+
opacity: 0;
|
|
912
|
+
transform: translateY(6px) scale(0.97);
|
|
913
|
+
}
|
|
914
|
+
to {
|
|
915
|
+
opacity: 1;
|
|
916
|
+
transform: translateY(0) scale(1);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
`;
|
|
920
|
+
_AdaptiveChatTrail.properties = {
|
|
921
|
+
messages: { attribute: false },
|
|
922
|
+
visibleCount: { type: Number },
|
|
923
|
+
expanded: { type: Boolean },
|
|
924
|
+
greeting: { type: String },
|
|
925
|
+
/** When true: trail renders fully expanded AND the
|
|
926
|
+
* expand/collapse affordance is hidden. Used by hosts that mount
|
|
927
|
+
* the chat in a full-screen surface (the mobile panel) where
|
|
928
|
+
* collapse makes no sense — there's nothing to collapse INTO. */
|
|
929
|
+
forceExpanded: { type: Boolean },
|
|
930
|
+
/**
|
|
931
|
+
* Mirrors `chatSession.inFlight` — true between `send()` and the
|
|
932
|
+
* assistant's first `receiveStart`/`receiveDelta`, OR while a
|
|
933
|
+
* stream is in progress. Drives the "thinking" indicator that
|
|
934
|
+
* fills the gap between the user's just-sent message and the
|
|
935
|
+
* first assistant token. Wired through AdaptiveChatBar (it owns
|
|
936
|
+
* the chatSession subscription and forwards inFlight as a prop).
|
|
937
|
+
*/
|
|
938
|
+
inFlight: { type: Boolean },
|
|
939
|
+
/**
|
|
940
|
+
* Live reasoning narration streamed from the model via AG-UI
|
|
941
|
+
* `THINKING_TEXT_MESSAGE_CONTENT`. When non-empty, the thinking
|
|
942
|
+
* chip renders this in place of the bouncing dots so the visitor
|
|
943
|
+
* sees what the model is actually doing instead of staring at
|
|
944
|
+
* silent loading state during a tool-call gap.
|
|
945
|
+
*/
|
|
946
|
+
thinkingText: { attribute: false },
|
|
947
|
+
/**
|
|
948
|
+
* Pre-conversation suggestion chip. When the visitor opens the
|
|
949
|
+
* chat and `messages` is empty, the trail renders this as a
|
|
950
|
+
* button beneath the greeting. Clicking it dispatches a
|
|
951
|
+
* `trail-intro-suggestion` event with the configured prompt —
|
|
952
|
+
* the parent (chat-bar) forwards that to chatSession.send().
|
|
953
|
+
* Disappears the moment the first message lands.
|
|
954
|
+
*/
|
|
955
|
+
introSuggestion: { attribute: false },
|
|
956
|
+
/**
|
|
957
|
+
* Tappable tile-receipt chips rendered once at the end of the
|
|
958
|
+
* trail. Produced from the store by Task 3 (AdaptiveChatBar).
|
|
959
|
+
* Each chip emits `tile-receipt-activate` with its instanceId.
|
|
960
|
+
*/
|
|
961
|
+
tileReceipts: { attribute: false },
|
|
962
|
+
/**
|
|
963
|
+
* Widgets rendered INLINE in the trail (reco carousel etc.),
|
|
964
|
+
* anchored to a message position like receipts. Sourced from the
|
|
965
|
+
* store's `chat-inline` slot mounts via AdaptiveChatBar.
|
|
966
|
+
*/
|
|
967
|
+
inlineWidgets: { attribute: false },
|
|
968
|
+
/**
|
|
969
|
+
* Registry-backed mounter for inline widgets whose id is not their
|
|
970
|
+
* custom-element tag. Injected from the chat-bar mountable.
|
|
971
|
+
*/
|
|
972
|
+
mountInlineWidget: { attribute: false },
|
|
973
|
+
_messagesBelow: { state: true }
|
|
974
|
+
};
|
|
975
|
+
_AdaptiveChatTrail._AT_BOTTOM_EPSILON_PX = 50;
|
|
976
|
+
_AdaptiveChatTrail._USER_SCROLL_INTENT_WINDOW_MS = 1200;
|
|
977
|
+
var AdaptiveChatTrail = _AdaptiveChatTrail;
|
|
978
|
+
function scrollRecentPillStyles() {
|
|
979
|
+
return {
|
|
980
|
+
position: "absolute",
|
|
981
|
+
bottom: "8px",
|
|
982
|
+
left: "50%",
|
|
983
|
+
transform: "translateX(-50%)",
|
|
984
|
+
display: "flex",
|
|
985
|
+
alignItems: "center",
|
|
986
|
+
gap: "5px",
|
|
987
|
+
height: "26px",
|
|
988
|
+
padding: "0 10px",
|
|
989
|
+
borderRadius: "9999px",
|
|
990
|
+
border: "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))",
|
|
991
|
+
background: "var(--sc-content-bubble-bg, rgba(20, 22, 28, 0.85))",
|
|
992
|
+
color: "var(--sc-tile-text-color, rgba(255, 255, 255, 0.92))",
|
|
993
|
+
fontSize: "12px",
|
|
994
|
+
fontWeight: "500",
|
|
995
|
+
lineHeight: "1",
|
|
996
|
+
whiteSpace: "nowrap",
|
|
997
|
+
cursor: "pointer",
|
|
998
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.25)",
|
|
999
|
+
zIndex: "3"
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function blurVeilStyles() {
|
|
1003
|
+
const mask = "linear-gradient(to bottom, black 0%, black 40%, transparent 100%)";
|
|
1004
|
+
return {
|
|
1005
|
+
position: "absolute",
|
|
1006
|
+
top: "0",
|
|
1007
|
+
left: "0",
|
|
1008
|
+
right: "0",
|
|
1009
|
+
height: `${TRAIL_BLUR_VEIL_PX}px`,
|
|
1010
|
+
pointerEvents: "none",
|
|
1011
|
+
backdropFilter: `blur(${TRAIL_BLUR_MAX_PX}px)`,
|
|
1012
|
+
WebkitBackdropFilter: `blur(${TRAIL_BLUR_MAX_PX}px)`,
|
|
1013
|
+
maskImage: mask,
|
|
1014
|
+
WebkitMaskImage: mask,
|
|
1015
|
+
zIndex: "2"
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function chipVisualState(m) {
|
|
1019
|
+
if (m.role === "system") {
|
|
1020
|
+
return { kind: "notice", role: m.role };
|
|
1021
|
+
}
|
|
1022
|
+
if (m.role === "assistant" && m.status === "error") {
|
|
1023
|
+
return { kind: "settled", role: m.role };
|
|
1024
|
+
}
|
|
1025
|
+
if (m.status === "error") {
|
|
1026
|
+
return { kind: "notice", role: m.role };
|
|
1027
|
+
}
|
|
1028
|
+
if (m.role === "assistant" && m.status === "streaming") {
|
|
1029
|
+
return { kind: "streaming" };
|
|
1030
|
+
}
|
|
1031
|
+
return { kind: "settled", role: m.role };
|
|
1032
|
+
}
|
|
1033
|
+
function chipStyles(state, pos) {
|
|
1034
|
+
const opacity = Math.max(OPACITY_FLOOR, 1 - pos * OPACITY_STEP);
|
|
1035
|
+
const yPx = pos * Y_DRIFT_PX;
|
|
1036
|
+
let border;
|
|
1037
|
+
let background;
|
|
1038
|
+
let color;
|
|
1039
|
+
let alignSelf;
|
|
1040
|
+
switch (state.kind) {
|
|
1041
|
+
case "settled":
|
|
1042
|
+
if (state.role === "user") {
|
|
1043
|
+
border = "var(--sc-content-bubble-border-user, 1px solid rgba(255, 255, 255, 0.28))";
|
|
1044
|
+
background = "var(--sc-content-bubble-background-user, rgba(255, 255, 255, 0.10))";
|
|
1045
|
+
alignSelf = "flex-end";
|
|
1046
|
+
} else {
|
|
1047
|
+
border = "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))";
|
|
1048
|
+
background = "var(--sc-content-bubble-background, rgba(20, 22, 24, 0.35))";
|
|
1049
|
+
alignSelf = "flex-start";
|
|
1050
|
+
}
|
|
1051
|
+
color = "var(--sc-tile-title-color, inherit)";
|
|
1052
|
+
break;
|
|
1053
|
+
case "streaming":
|
|
1054
|
+
case "pending":
|
|
1055
|
+
border = "var(--sc-content-bubble-border, 1px solid rgba(255, 255, 255, 0.18))";
|
|
1056
|
+
background = "var(--sc-content-bubble-background, rgba(20, 22, 24, 0.35))";
|
|
1057
|
+
color = "var(--sc-tile-title-color, inherit)";
|
|
1058
|
+
alignSelf = "flex-start";
|
|
1059
|
+
break;
|
|
1060
|
+
case "notice":
|
|
1061
|
+
border = "var(--sc-content-bubble-border-error, 1px solid rgba(255, 255, 255, 0.22))";
|
|
1062
|
+
background = "var(--sc-content-bubble-background-error, rgba(20, 22, 24, 0.28))";
|
|
1063
|
+
color = "var(--sc-content-bubble-text-error, rgba(255, 255, 255, 0.75))";
|
|
1064
|
+
alignSelf = state.role === "user" ? "flex-end" : "flex-start";
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
alignSelf,
|
|
1069
|
+
// Single shared width policy for EVERY bubble (user / assistant /
|
|
1070
|
+
// streaming / pending / error). Short and long replies now read as a
|
|
1071
|
+
// consistent column: `fit-content` sizes each bubble to its text, and
|
|
1072
|
+
// `TRAIL_MAX_WIDTH` caps the wide ones at the same ceiling — no more
|
|
1073
|
+
// per-role or per-path width drift (user feedback: chat tiles had
|
|
1074
|
+
// differing widths).
|
|
1075
|
+
maxWidth: TRAIL_MAX_WIDTH,
|
|
1076
|
+
width: "fit-content",
|
|
1077
|
+
// Bumped from 11px → 13px after the chat lid in production read as
|
|
1078
|
+
// cramped at the smaller size — body chat copy needs to be visitor-
|
|
1079
|
+
// legible on a phone without zoom, and 11px reads as a footnote
|
|
1080
|
+
// rather than the primary conversation surface. Code blocks inside
|
|
1081
|
+
// chip body scale proportionally (10px → 11px) below.
|
|
1082
|
+
fontSize: "13px",
|
|
1083
|
+
lineHeight: "1.45",
|
|
1084
|
+
padding: "5px 11px",
|
|
1085
|
+
borderRadius: "10px",
|
|
1086
|
+
border,
|
|
1087
|
+
background,
|
|
1088
|
+
// Frosted-glass bubbles are OPT-IN via the `--sc-content-bubble-blur`
|
|
1089
|
+
// knob (a host that wants glass sets e.g. `blur(8px)` deliberately); the
|
|
1090
|
+
// DEFAULT is `none`. A backdrop-filter on the bubble composites its OWN
|
|
1091
|
+
// text onto a GPU layer (grayscale AA + the blurred backdrop bleeding
|
|
1092
|
+
// under thin glyphs) — the resting-state compositing contract's
|
|
1093
|
+
// "backdrop-filter behind text" case: soft rasterization everywhere and
|
|
1094
|
+
// fully BLANKED text on some Android GPUs. Glass-by-default shipped that
|
|
1095
|
+
// hazard to every non-velvet surface, so the default flipped to `none`
|
|
1096
|
+
// (GPU/compositing hazard audit, Fix 3). Gate:
|
|
1097
|
+
// AdaptiveChatTrail.compositing.test.ts.
|
|
1098
|
+
backdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1099
|
+
WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1100
|
+
color,
|
|
1101
|
+
transition: "opacity 240ms cubic-bezier(0.22, 1, 0.36, 1), transform 240ms cubic-bezier(0.22, 1, 0.36, 1)",
|
|
1102
|
+
// Per-position opacity + y-drift falloff is still per-chip — those
|
|
1103
|
+
// operate naturally per element. Blur is NOT per-chip anymore: it's
|
|
1104
|
+
// applied via an absolute-positioned veil at the top of the scroll
|
|
1105
|
+
// viewport (TRAIL_BLUR_VEIL_PX above), so a single long message
|
|
1106
|
+
// doesn't get uniformly blurred across its full body.
|
|
1107
|
+
opacity: String(toFixedTrim(opacity)),
|
|
1108
|
+
transform: `translateY(${yPx}px)`,
|
|
1109
|
+
// User messages render as raw text (`html` template); pre-wrap preserves
|
|
1110
|
+
// newlines the visitor typed. Assistant messages render as markdown HTML
|
|
1111
|
+
// (`<p>` tags from marked); pre-wrap there would make marked's inter-tag
|
|
1112
|
+
// text nodes (the `\n` between/after `<p>` tags) visible, producing the
|
|
1113
|
+
// trailing/inter-paragraph blank lines visitors reported. Markdown's own
|
|
1114
|
+
// structure carries the paragraph breaks, so assistant chips use the
|
|
1115
|
+
// default `normal` whitespace handling.
|
|
1116
|
+
whiteSpace: (state.kind === "settled" || state.kind === "notice") && state.role === "user" ? "pre-wrap" : "normal",
|
|
1117
|
+
wordBreak: "break-word"
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
function toolCallStripStyles() {
|
|
1121
|
+
return {
|
|
1122
|
+
display: "flex",
|
|
1123
|
+
flexWrap: "wrap",
|
|
1124
|
+
gap: "4px",
|
|
1125
|
+
marginTop: "4px",
|
|
1126
|
+
paddingTop: "4px",
|
|
1127
|
+
borderTop: "1px dashed var(--sc-content-divider-color, rgba(255, 255, 255, 0.12))"
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function pendingToolCallChipStyles() {
|
|
1131
|
+
return {
|
|
1132
|
+
display: "inline-flex",
|
|
1133
|
+
alignItems: "center",
|
|
1134
|
+
gap: "4px",
|
|
1135
|
+
padding: "2px 7px",
|
|
1136
|
+
border: "var(--sc-content-bubble-border-user, 1px solid rgba(255, 255, 255, 0.28))",
|
|
1137
|
+
background: "var(--sc-content-bubble-background-user, rgba(255, 255, 255, 0.10))",
|
|
1138
|
+
color: "var(--sc-tile-text-color, inherit)",
|
|
1139
|
+
borderRadius: "999px",
|
|
1140
|
+
fontSize: "10px",
|
|
1141
|
+
fontWeight: "500",
|
|
1142
|
+
fontFamily: "ui-monospace, SF Mono, Menlo, monospace",
|
|
1143
|
+
cursor: "pointer",
|
|
1144
|
+
opacity: "1"
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function deriveActivityReceipt(tc) {
|
|
1148
|
+
if (tc.name !== "primary_response_action") return null;
|
|
1149
|
+
const args = tc.args;
|
|
1150
|
+
if (!args || typeof args !== "object") return null;
|
|
1151
|
+
const templateId = typeof args.template_id === "string" ? args.template_id : "";
|
|
1152
|
+
const content = args.content ?? null;
|
|
1153
|
+
const title = content && typeof content.title === "string" && content.title.trim() ? content.title.trim() : "";
|
|
1154
|
+
const id = templateId.toLowerCase();
|
|
1155
|
+
let type;
|
|
1156
|
+
if (id.includes("faq")) type = "FAQ";
|
|
1157
|
+
else if (id.includes("chart") || id.includes("graph")) type = "chart";
|
|
1158
|
+
else if (id.includes("card") || id.includes("product")) type = "card";
|
|
1159
|
+
else if (id.includes("alert") || id.includes("navigation") || id.includes("nav")) type = "alert";
|
|
1160
|
+
else type = "added to your view";
|
|
1161
|
+
const label = title || templateId || "something";
|
|
1162
|
+
return { type, label };
|
|
1163
|
+
}
|
|
1164
|
+
function receiptText(r) {
|
|
1165
|
+
switch (r.type) {
|
|
1166
|
+
case "FAQ":
|
|
1167
|
+
return "Opened the FAQ for you";
|
|
1168
|
+
case "chart":
|
|
1169
|
+
return `Put together ${r.label} for you`;
|
|
1170
|
+
case "card":
|
|
1171
|
+
return "Pulled up the product for you";
|
|
1172
|
+
case "alert":
|
|
1173
|
+
return `Here's a shortcut: ${r.label}`;
|
|
1174
|
+
default:
|
|
1175
|
+
return `Pulled up ${r.label} for you`;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
function extractSuggestedReplies(toolCalls) {
|
|
1179
|
+
if (!toolCalls) return [];
|
|
1180
|
+
for (const tc of toolCalls) {
|
|
1181
|
+
if (tc.name !== "suggest_replies") continue;
|
|
1182
|
+
const replies = tc.args?.replies;
|
|
1183
|
+
if (!Array.isArray(replies)) return [];
|
|
1184
|
+
return replies.filter((r) => typeof r === "string" && r.trim().length > 0).map((r) => r.trim()).slice(0, 4);
|
|
1185
|
+
}
|
|
1186
|
+
return [];
|
|
1187
|
+
}
|
|
1188
|
+
function computeAssistantChipParts(m, isSuggestionHost) {
|
|
1189
|
+
const toolCalls = m.toolCalls ?? [];
|
|
1190
|
+
return {
|
|
1191
|
+
pendingToolCalls: toolCalls.filter((tc) => tc.status === "pending"),
|
|
1192
|
+
receipts: toolCalls.map(deriveActivityReceipt).filter((r) => r !== null),
|
|
1193
|
+
replyChips: isSuggestionHost ? extractSuggestedReplies(toolCalls) : []
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
function looksLikeSerializedModelResponse(text) {
|
|
1197
|
+
const t = text.trimStart();
|
|
1198
|
+
if (t.startsWith("(Empty response")) return true;
|
|
1199
|
+
let hits = 0;
|
|
1200
|
+
if (/['"]stop_reason['"]\s*:/.test(t)) hits++;
|
|
1201
|
+
if (/['"]signature['"]\s*:/.test(t)) hits++;
|
|
1202
|
+
if (/['"]type['"]\s*:\s*['"]thinking['"]/.test(t)) hits++;
|
|
1203
|
+
if (/['"]content['"]\s*:\s*\[/.test(t)) hits++;
|
|
1204
|
+
return hits >= 2;
|
|
1205
|
+
}
|
|
1206
|
+
function assistantChipRendersContent(m, isSuggestionHost) {
|
|
1207
|
+
const text = (m.text ?? "").trim();
|
|
1208
|
+
if (text.length > 0 && !looksLikeSerializedModelResponse(text)) return true;
|
|
1209
|
+
const { pendingToolCalls, receipts, replyChips } = computeAssistantChipParts(m, isSuggestionHost);
|
|
1210
|
+
return pendingToolCalls.length > 0 || receipts.length > 0 || replyChips.length > 0;
|
|
1211
|
+
}
|
|
1212
|
+
function inlineWidgetBlockStyles() {
|
|
1213
|
+
return {
|
|
1214
|
+
alignSelf: "stretch",
|
|
1215
|
+
width: "100%",
|
|
1216
|
+
minWidth: "0",
|
|
1217
|
+
marginTop: "6px",
|
|
1218
|
+
contain: "layout"
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function receiptStripStyles() {
|
|
1222
|
+
return {
|
|
1223
|
+
display: "flex",
|
|
1224
|
+
flexDirection: "column",
|
|
1225
|
+
gap: "2px",
|
|
1226
|
+
marginTop: "4px"
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
function receiptStyles() {
|
|
1230
|
+
return {
|
|
1231
|
+
display: "inline-flex",
|
|
1232
|
+
alignItems: "center",
|
|
1233
|
+
gap: "5px",
|
|
1234
|
+
alignSelf: "flex-start",
|
|
1235
|
+
fontSize: "11px",
|
|
1236
|
+
lineHeight: "1.3",
|
|
1237
|
+
fontWeight: "500",
|
|
1238
|
+
letterSpacing: "0.01em",
|
|
1239
|
+
// Inherit the chip's OWN resolved text color (muted via opacity), so the
|
|
1240
|
+
// receipt is legible on every theme by construction. The old
|
|
1241
|
+
// `--sc-content-muted-color` fallback was rgba(255,255,255,…) — white —
|
|
1242
|
+
// invisible on light canvases; a `--sc-tile-text-color` token can likewise
|
|
1243
|
+
// be unset/mismatched on dark canvases. `currentColor` always matches the
|
|
1244
|
+
// chip text the visitor is already reading.
|
|
1245
|
+
color: "currentColor",
|
|
1246
|
+
opacity: "0.7"
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
function receiptIconStyles() {
|
|
1250
|
+
return {
|
|
1251
|
+
display: "inline-flex",
|
|
1252
|
+
alignItems: "center",
|
|
1253
|
+
justifyContent: "center",
|
|
1254
|
+
width: "14px",
|
|
1255
|
+
height: "14px",
|
|
1256
|
+
borderRadius: "50%",
|
|
1257
|
+
fontSize: "9px",
|
|
1258
|
+
fontWeight: "700",
|
|
1259
|
+
color: "#fff",
|
|
1260
|
+
background: "hsl(var(--sc-accent-color, 145 40% 45%) / 0.7)"
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
function replyChipStripStyles() {
|
|
1264
|
+
return {
|
|
1265
|
+
display: "flex",
|
|
1266
|
+
flexWrap: "wrap",
|
|
1267
|
+
gap: "6px",
|
|
1268
|
+
marginTop: "6px",
|
|
1269
|
+
alignSelf: "flex-start"
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
function tileReceiptStripStyles() {
|
|
1273
|
+
return {
|
|
1274
|
+
display: "flex",
|
|
1275
|
+
flexDirection: "column",
|
|
1276
|
+
gap: "6px",
|
|
1277
|
+
marginTop: "6px",
|
|
1278
|
+
width: "100%",
|
|
1279
|
+
alignSelf: "stretch"
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function replyChipStyles() {
|
|
1283
|
+
return {
|
|
1284
|
+
display: "inline-flex",
|
|
1285
|
+
alignItems: "center",
|
|
1286
|
+
padding: "5px 12px",
|
|
1287
|
+
border: "1px solid hsl(var(--sc-accent-color, 145 40% 45%) / 0.40)",
|
|
1288
|
+
borderRadius: "9999px",
|
|
1289
|
+
background: "hsl(var(--sc-accent-color, 145 40% 45%) / 0.12)",
|
|
1290
|
+
color: "var(--sc-tile-text-color, currentColor)",
|
|
1291
|
+
font: "inherit",
|
|
1292
|
+
fontSize: "12px",
|
|
1293
|
+
fontWeight: "500",
|
|
1294
|
+
lineHeight: "1.2",
|
|
1295
|
+
cursor: "pointer",
|
|
1296
|
+
backdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1297
|
+
WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1298
|
+
transition: "background 150ms ease, transform 120ms ease"
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
function alertReceiptIconStyles() {
|
|
1302
|
+
return {
|
|
1303
|
+
flex: "0 0 auto",
|
|
1304
|
+
width: "26px",
|
|
1305
|
+
height: "26px",
|
|
1306
|
+
borderRadius: "8px",
|
|
1307
|
+
display: "grid",
|
|
1308
|
+
placeItems: "center",
|
|
1309
|
+
background: "hsl(var(--sc-accent-color))",
|
|
1310
|
+
color: "var(--sc-accent-foreground, #fff)",
|
|
1311
|
+
fontSize: "14px"
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
function tileReceiptChipStyles() {
|
|
1315
|
+
return {
|
|
1316
|
+
display: "flex",
|
|
1317
|
+
alignItems: "center",
|
|
1318
|
+
gap: "10px",
|
|
1319
|
+
width: "100%",
|
|
1320
|
+
textAlign: "left",
|
|
1321
|
+
cursor: "pointer",
|
|
1322
|
+
padding: "9px 10px 9px 11px",
|
|
1323
|
+
borderRadius: "12px",
|
|
1324
|
+
border: "1px solid hsl(var(--sc-accent-color) / 0.28)",
|
|
1325
|
+
background: "hsl(var(--sc-accent-color) / 0.06)"
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function tileReceiptGlyphStyles() {
|
|
1329
|
+
return { position: "relative", flex: "0 0 auto", width: "28px", height: "28px" };
|
|
1330
|
+
}
|
|
1331
|
+
function tileReceiptGlyphLayerStyles(i) {
|
|
1332
|
+
return {
|
|
1333
|
+
position: "absolute",
|
|
1334
|
+
inset: "0",
|
|
1335
|
+
borderRadius: "7px",
|
|
1336
|
+
background: "hsl(var(--sc-accent-color) / 0.22)",
|
|
1337
|
+
boxShadow: "inset 0 0 0 1px hsl(var(--sc-accent-color) / 0.4)",
|
|
1338
|
+
...i === 0 ? { transform: "translate(3px,-3px) rotate(6deg)", opacity: "0.5" } : {}
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function receiptChipTitleStyles(_kind) {
|
|
1342
|
+
return {
|
|
1343
|
+
// Grows to fill the space between the icon and the action pill, truncating
|
|
1344
|
+
// long product names on one line (the chip is now single-line).
|
|
1345
|
+
flex: "1",
|
|
1346
|
+
minWidth: "0",
|
|
1347
|
+
fontSize: "13px",
|
|
1348
|
+
fontWeight: "650",
|
|
1349
|
+
lineHeight: "1.2",
|
|
1350
|
+
color: "var(--sc-content-text-color, #0f172a)",
|
|
1351
|
+
whiteSpace: "nowrap",
|
|
1352
|
+
overflow: "hidden",
|
|
1353
|
+
textOverflow: "ellipsis"
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
function tileReceiptActionStyles(dismissed) {
|
|
1357
|
+
return {
|
|
1358
|
+
flex: "0 0 auto",
|
|
1359
|
+
fontSize: "11px",
|
|
1360
|
+
fontWeight: "700",
|
|
1361
|
+
borderRadius: "999px",
|
|
1362
|
+
padding: "4px 10px",
|
|
1363
|
+
...dismissed ? { background: "hsl(var(--sc-accent-color) / 0.14)", color: "hsl(var(--sc-accent-color))" } : {
|
|
1364
|
+
background: "hsl(var(--sc-accent-color) / 0.9)",
|
|
1365
|
+
color: "var(--sc-accent-foreground, #fff)"
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
function caretStyles() {
|
|
1370
|
+
return {
|
|
1371
|
+
display: "inline-block",
|
|
1372
|
+
width: "6px",
|
|
1373
|
+
height: "12px",
|
|
1374
|
+
marginLeft: "3px",
|
|
1375
|
+
verticalAlign: "-1px",
|
|
1376
|
+
background: "hsl(var(--sc-accent-color) / 0.85)",
|
|
1377
|
+
borderRadius: "1px",
|
|
1378
|
+
animation: "syntro-trail-caret 1s steps(2, end) infinite"
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
function thinkingSlotStyles() {
|
|
1382
|
+
return {
|
|
1383
|
+
alignSelf: "flex-start",
|
|
1384
|
+
color: "var(--sc-tile-title-color, inherit)",
|
|
1385
|
+
padding: "4px 4px",
|
|
1386
|
+
animation: "syntro-trail-chip-enter 220ms cubic-bezier(0.22, 1, 0.36, 1) both"
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function thinkingDotsStyles() {
|
|
1390
|
+
return {
|
|
1391
|
+
display: "inline-flex",
|
|
1392
|
+
alignItems: "center",
|
|
1393
|
+
gap: "6px",
|
|
1394
|
+
// Line height matches what a single-line text chip would compute
|
|
1395
|
+
// to, so swapping out the slot for the streaming chip doesn't
|
|
1396
|
+
// shift the next chip up or down by a couple of pixels.
|
|
1397
|
+
lineHeight: "14px"
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
function thinkingDotStyles(index) {
|
|
1401
|
+
return {
|
|
1402
|
+
display: "inline-block",
|
|
1403
|
+
width: "7px",
|
|
1404
|
+
height: "7px",
|
|
1405
|
+
borderRadius: "50%",
|
|
1406
|
+
background: "currentColor",
|
|
1407
|
+
// The dots are now the entire visual — no bubble framing them —
|
|
1408
|
+
// so the pulse has to do all the work the chip used to do. Scale
|
|
1409
|
+
// 0.4 → 1.0 with opacity 0.25 → 1.0 gives an unmistakable
|
|
1410
|
+
// breathing motion: at the trough each dot reads like a faint
|
|
1411
|
+
// ghost, at the peak like a solid dot. 940ms cycle is a touch
|
|
1412
|
+
// faster than the original 1.05s; without the chip frame the
|
|
1413
|
+
// shorter cycle feels purposeful instead of agitated. Stagger
|
|
1414
|
+
// 130ms = clear rolling cadence across all three.
|
|
1415
|
+
transformOrigin: "center",
|
|
1416
|
+
animation: `syntro-trail-thinking-pulse 940ms cubic-bezier(0.4, 0, 0.6, 1) ${index * 0.13}s infinite both`
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
function moreStyles() {
|
|
1420
|
+
return {
|
|
1421
|
+
// Center horizontally regardless of the parent's display model.
|
|
1422
|
+
// ``alignSelf: center`` only works when the parent is a flex/grid
|
|
1423
|
+
// container with cross-axis alignment; the chat-bar's flex column
|
|
1424
|
+
// stretches us full-width instead. ``display: block`` + auto inline
|
|
1425
|
+
// margins centers the auto-sized button without needing the parent
|
|
1426
|
+
// to opt in.
|
|
1427
|
+
display: "block",
|
|
1428
|
+
marginLeft: "auto",
|
|
1429
|
+
marginRight: "auto",
|
|
1430
|
+
marginBottom: "6px",
|
|
1431
|
+
fontSize: "10px",
|
|
1432
|
+
fontWeight: "500",
|
|
1433
|
+
letterSpacing: "0.06em",
|
|
1434
|
+
padding: "4px 10px",
|
|
1435
|
+
border: "1px solid hsl(var(--sc-accent-color) / 0.32)",
|
|
1436
|
+
borderRadius: "999px",
|
|
1437
|
+
background: "hsl(var(--sc-accent-color) / 0.10)",
|
|
1438
|
+
color: "var(--sc-tile-text-color, currentColor)",
|
|
1439
|
+
cursor: "pointer",
|
|
1440
|
+
opacity: "0.85",
|
|
1441
|
+
backdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1442
|
+
WebkitBackdropFilter: "var(--sc-content-bubble-blur, none)",
|
|
1443
|
+
transition: "opacity 150ms ease, background 150ms ease"
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function stripTrailingWhitespace(text) {
|
|
1447
|
+
return text.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
1448
|
+
}
|
|
1449
|
+
function toFixedTrim(n) {
|
|
1450
|
+
const s = n.toFixed(2);
|
|
1451
|
+
return s.replace(/\.?0+$/, "") || "0";
|
|
1452
|
+
}
|
|
1453
|
+
var THINKING_TAIL_MAX = 120;
|
|
1454
|
+
function truncateThinkingTail(text) {
|
|
1455
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
1456
|
+
if (flat.length <= THINKING_TAIL_MAX) return flat;
|
|
1457
|
+
return `\u2026${flat.slice(-THINKING_TAIL_MAX)}`;
|
|
1458
|
+
}
|
|
1459
|
+
function thinkingTextStyles() {
|
|
1460
|
+
return {
|
|
1461
|
+
fontStyle: "italic",
|
|
1462
|
+
opacity: "0.75",
|
|
1463
|
+
fontSize: "13px",
|
|
1464
|
+
lineHeight: "18px",
|
|
1465
|
+
maxWidth: "32ch",
|
|
1466
|
+
overflow: "hidden",
|
|
1467
|
+
textOverflow: "ellipsis",
|
|
1468
|
+
whiteSpace: "nowrap"
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
function introSuggestionStyles() {
|
|
1472
|
+
return {
|
|
1473
|
+
display: "inline-flex",
|
|
1474
|
+
alignItems: "center",
|
|
1475
|
+
gap: "6px",
|
|
1476
|
+
marginTop: "4px",
|
|
1477
|
+
padding: "10px 18px",
|
|
1478
|
+
borderRadius: "9999px",
|
|
1479
|
+
border: "0",
|
|
1480
|
+
background: "linear-gradient(135deg, var(--sc-color-primary, #5faf7d) 0%, var(--sc-color-primary-hover, #3d8a5e) 100%)",
|
|
1481
|
+
color: "#fff",
|
|
1482
|
+
cursor: "pointer",
|
|
1483
|
+
font: "inherit",
|
|
1484
|
+
fontSize: "14px",
|
|
1485
|
+
fontWeight: "600",
|
|
1486
|
+
transition: "transform 120ms ease, filter 160ms ease"
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
function emptyIconStyles() {
|
|
1490
|
+
return {
|
|
1491
|
+
display: "inline-flex",
|
|
1492
|
+
alignItems: "center",
|
|
1493
|
+
justifyContent: "center",
|
|
1494
|
+
width: "48px",
|
|
1495
|
+
height: "48px",
|
|
1496
|
+
borderRadius: "9999px",
|
|
1497
|
+
background: "var(--sc-tile-icon-background, linear-gradient(135deg, var(--sc-color-primary, #5faf7d) 0%, var(--sc-color-primary-hover, #3d8a5e) 100%))",
|
|
1498
|
+
color: "#fff",
|
|
1499
|
+
fontSize: "22px"
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
function emptyTitleStyles() {
|
|
1503
|
+
return {
|
|
1504
|
+
margin: "0",
|
|
1505
|
+
fontSize: "15px",
|
|
1506
|
+
fontWeight: "600",
|
|
1507
|
+
lineHeight: "1.35",
|
|
1508
|
+
color: "var(--sc-content-text-color, inherit)",
|
|
1509
|
+
maxWidth: "32ch"
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
if (!customElements.get("adaptive-chat-trail")) {
|
|
1513
|
+
customElements.define("adaptive-chat-trail", AdaptiveChatTrail);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
export {
|
|
1517
|
+
AdaptiveChatTrail,
|
|
1518
|
+
looksLikeSerializedModelResponse
|
|
1519
|
+
};
|
|
1520
|
+
//# sourceMappingURL=chunk-HVGQJZMP.js.map
|