@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
package/dist/runtime.js
CHANGED
|
@@ -1,31 +1,2234 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
import {
|
|
2
|
+
ActionHandler,
|
|
3
|
+
AgUiTransport,
|
|
4
|
+
ChatAssistantLitMountable,
|
|
5
|
+
ElementInstanceStore,
|
|
6
|
+
ItemHandler,
|
|
7
|
+
TileHandler,
|
|
8
|
+
acquireTokenWithChallenge,
|
|
9
|
+
decodeMutationEnvelope,
|
|
10
|
+
fetchMountedElements,
|
|
11
|
+
renderFallbackHtml
|
|
12
|
+
} from "./chunk-HELUC5JI.js";
|
|
13
|
+
import "./chunk-5W75LR6M.js";
|
|
14
|
+
import {
|
|
15
|
+
getElementInstanceStore,
|
|
16
|
+
setElementInstanceStore
|
|
17
|
+
} from "./chunk-EWPPVPJ4.js";
|
|
18
|
+
import "./chunk-HVGQJZMP.js";
|
|
19
|
+
import "./chunk-7WSMNAKW.js";
|
|
20
|
+
import "./chunk-GP4OSTZT.js";
|
|
21
|
+
import {
|
|
22
|
+
DIVE_DEEPER_EVENT,
|
|
23
|
+
TILE_MOUNTED_EVENT,
|
|
24
|
+
TILE_UNMOUNTED_EVENT
|
|
25
|
+
} from "./chunk-KPWW7N27.js";
|
|
26
|
+
import "./chunk-UVKRO5ER.js";
|
|
27
|
+
|
|
28
|
+
// src/ChatSession.ts
|
|
29
|
+
var _userIdCounter = 0;
|
|
30
|
+
function generateUserMessageId() {
|
|
31
|
+
return `user-${Date.now()}-${++_userIdCounter}`;
|
|
32
|
+
}
|
|
33
|
+
var ChatSession = class {
|
|
34
|
+
constructor() {
|
|
35
|
+
this._messages = [];
|
|
36
|
+
this._inFlight = false;
|
|
37
|
+
this._runActive = false;
|
|
38
|
+
this._thinkingText = "";
|
|
39
|
+
this._nextId = 1;
|
|
40
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
41
|
+
this.sendListeners = /* @__PURE__ */ new Set();
|
|
42
|
+
this.interruptListeners = /* @__PURE__ */ new Set();
|
|
43
|
+
this.toolResultListeners = /* @__PURE__ */ new Set();
|
|
44
|
+
}
|
|
45
|
+
/** Snapshot the current state. Always returns a fresh immutable view. */
|
|
46
|
+
getState() {
|
|
47
|
+
return {
|
|
48
|
+
messages: [...this._messages],
|
|
49
|
+
inFlight: this._inFlight || this._runActive,
|
|
50
|
+
thinkingText: this._thinkingText
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Append a delta to the active thinking turn's narration buffer.
|
|
55
|
+
* Forwarded from the AG-UI THINKING_TEXT_MESSAGE_CONTENT event so
|
|
56
|
+
* the chat trail can render the model's own reasoning instead of
|
|
57
|
+
* silent loading dots between turns.
|
|
58
|
+
*/
|
|
59
|
+
appendThinkingDelta(delta) {
|
|
60
|
+
if (!delta) return;
|
|
61
|
+
this._thinkingText += delta;
|
|
62
|
+
this.notify();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Replace the active thinking narration outright (vs. appendThinkingDelta's
|
|
66
|
+
* streaming accumulation). Used to surface tool-call progress — e.g.
|
|
67
|
+
* "Searching…" while the agent works a tool — so a long tool phase shows
|
|
68
|
+
* what's happening instead of silent dots. No-op if unchanged.
|
|
69
|
+
*/
|
|
70
|
+
setThinkingText(text) {
|
|
71
|
+
if (this._thinkingText === text) return;
|
|
72
|
+
this._thinkingText = text;
|
|
73
|
+
this.notify();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* End the active thinking turn — drop any narration so the trail
|
|
77
|
+
* stops showing it. Fires on real text-message-start and on
|
|
78
|
+
* tool-call-start, the moment the model switches from reasoning
|
|
79
|
+
* to visible output.
|
|
80
|
+
*/
|
|
81
|
+
clearThinking() {
|
|
82
|
+
if (!this._thinkingText) return;
|
|
83
|
+
this._thinkingText = "";
|
|
84
|
+
this.notify();
|
|
85
|
+
}
|
|
86
|
+
setRunActive(active) {
|
|
87
|
+
if (this._runActive === active) return;
|
|
88
|
+
this._runActive = active;
|
|
89
|
+
if (!active) this._thinkingText = "";
|
|
90
|
+
this.notify();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Subscribe to state changes. Called immediately with the current
|
|
94
|
+
* state, then again on every change. Returns an unsubscribe function.
|
|
95
|
+
*/
|
|
96
|
+
subscribe(cb) {
|
|
97
|
+
this.subscribers.add(cb);
|
|
98
|
+
cb(this.getState());
|
|
99
|
+
return () => {
|
|
100
|
+
this.subscribers.delete(cb);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* User submitted a message. Appends a user-role message, sets
|
|
105
|
+
* inFlight=true, notifies state subscribers, and fires a "send"
|
|
106
|
+
* event so transports can pick it up. Empty/whitespace text is a
|
|
107
|
+
* no-op (matches the chat bar's local guard).
|
|
108
|
+
*/
|
|
109
|
+
send(text, opts) {
|
|
110
|
+
const trimmed = text.trim();
|
|
111
|
+
if (!trimmed) return;
|
|
112
|
+
const id = generateUserMessageId();
|
|
113
|
+
this._messages.push({ id, role: "user", text: trimmed });
|
|
114
|
+
this._inFlight = true;
|
|
115
|
+
this.notify();
|
|
116
|
+
const event = { text: trimmed, id };
|
|
117
|
+
if (opts?.activeLidSlot) event.activeLidSlot = opts.activeLidSlot;
|
|
118
|
+
for (const listener of this.sendListeners) listener(event);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Single-shot assistant reply (no streaming). Equivalent to
|
|
122
|
+
* receiveStart + receiveDelta + receiveEnd in one call. Useful for
|
|
123
|
+
* stub transports and tests that don't model streaming.
|
|
124
|
+
*/
|
|
125
|
+
receive(text) {
|
|
126
|
+
const id = `m-${this._nextId++}`;
|
|
127
|
+
this._messages.push({ id, role: "assistant", text, status: "complete" });
|
|
128
|
+
this._inFlight = false;
|
|
129
|
+
this.notify();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Begin a streaming assistant message. Appends an empty assistant
|
|
133
|
+
* message with status='streaming'. Caller (transport adapter) feeds
|
|
134
|
+
* deltas via receiveDelta(id, text) and signals completion via
|
|
135
|
+
* receiveEnd(id). `inFlight` stays true through the stream.
|
|
136
|
+
*/
|
|
137
|
+
receiveStart(id) {
|
|
138
|
+
this._messages.push({ id, role: "assistant", text: "", status: "streaming" });
|
|
139
|
+
this._inFlight = true;
|
|
140
|
+
this.notify();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Append a delta to a streaming message. No-op when the id is
|
|
144
|
+
* unknown (race between transport events and reset, etc.) — never
|
|
145
|
+
* throws so transports can fire-and-forget.
|
|
146
|
+
*/
|
|
147
|
+
receiveDelta(id, delta) {
|
|
148
|
+
const msg = this._messages.find((m) => m.id === id);
|
|
149
|
+
if (!msg || msg.status !== "streaming") return;
|
|
150
|
+
msg.text += delta;
|
|
151
|
+
this.notify();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Mark a streaming message complete and clear inFlight. No-op when
|
|
155
|
+
* the id is unknown (defensive against transport double-fires).
|
|
156
|
+
*/
|
|
157
|
+
receiveEnd(id) {
|
|
158
|
+
const msg = this._messages.find((m) => m.id === id);
|
|
159
|
+
if (msg && msg.status === "streaming") {
|
|
160
|
+
msg.status = "complete";
|
|
161
|
+
}
|
|
162
|
+
this._inFlight = false;
|
|
163
|
+
this.notify();
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Transport reports a fatal error. Marks any in-flight streaming
|
|
167
|
+
* message as 'error' (so the trail can render a styled error chip
|
|
168
|
+
* instead of pretending the partial text was a complete answer),
|
|
169
|
+
* appends a system-role message with the error text for visibility,
|
|
170
|
+
* and clears inFlight. Also drops any pending thinking narration
|
|
171
|
+
* — leaving stale "Let me check…" alongside an error chip reads
|
|
172
|
+
* as if the assistant is still working when it isn't.
|
|
173
|
+
*/
|
|
174
|
+
error(message) {
|
|
175
|
+
for (const m of this._messages) {
|
|
176
|
+
if (m.status === "streaming") m.status = "error";
|
|
177
|
+
}
|
|
178
|
+
this._messages.push({
|
|
179
|
+
id: `err-${this._nextId++}`,
|
|
180
|
+
role: "system",
|
|
181
|
+
text: message,
|
|
182
|
+
status: "error"
|
|
183
|
+
});
|
|
184
|
+
this._inFlight = false;
|
|
185
|
+
this._runActive = false;
|
|
186
|
+
this._thinkingText = "";
|
|
187
|
+
this.notify();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* User clicked the in-flight stop button. Clears inFlight and
|
|
191
|
+
* fires an "interrupt" event so transports can cancel their
|
|
192
|
+
* in-flight request. No-op if not in-flight (idempotent). Drops
|
|
193
|
+
* any pending thinking narration for the same reason as error():
|
|
194
|
+
* the visitor explicitly asked the assistant to stop, so it
|
|
195
|
+
* shouldn't keep narrating in the trail.
|
|
196
|
+
*/
|
|
197
|
+
interrupt() {
|
|
198
|
+
if (!this._inFlight && !this._runActive) return;
|
|
199
|
+
this._inFlight = false;
|
|
200
|
+
this._runActive = false;
|
|
201
|
+
this._thinkingText = "";
|
|
202
|
+
this.notify();
|
|
203
|
+
for (const listener of this.interruptListeners) listener();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Wipe state. Used by the canvas-close path (start a fresh
|
|
207
|
+
* conversation next time) and by tests.
|
|
208
|
+
*/
|
|
209
|
+
reset() {
|
|
210
|
+
this._messages = [];
|
|
211
|
+
this._inFlight = false;
|
|
212
|
+
this._runActive = false;
|
|
213
|
+
this._thinkingText = "";
|
|
214
|
+
this._nextId = 1;
|
|
215
|
+
this.notify();
|
|
216
|
+
}
|
|
217
|
+
/** Register a transport's send listener. Returns unsubscribe. */
|
|
218
|
+
onSend(listener) {
|
|
219
|
+
this.sendListeners.add(listener);
|
|
220
|
+
return () => {
|
|
221
|
+
this.sendListeners.delete(listener);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* True when at least one transport has wired itself to the session's
|
|
226
|
+
* send pipeline. Views can use this to surface "no chat backend
|
|
227
|
+
* connected" affordances instead of hanging in `inFlight` after a
|
|
228
|
+
* send fires into the void.
|
|
229
|
+
*/
|
|
230
|
+
hasTransport() {
|
|
231
|
+
return this.sendListeners.size > 0;
|
|
232
|
+
}
|
|
233
|
+
/** Register a transport's interrupt listener. Returns unsubscribe. */
|
|
234
|
+
onInterrupt(listener) {
|
|
235
|
+
this.interruptListeners.add(listener);
|
|
236
|
+
return () => {
|
|
237
|
+
this.interruptListeners.delete(listener);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Register a transport's tool-result listener. The transport
|
|
242
|
+
* forwards `tool-result` actions back to the agent after the user
|
|
243
|
+
* approves or rejects a client-tool call. Returns unsubscribe.
|
|
244
|
+
*/
|
|
245
|
+
onToolResult(listener) {
|
|
246
|
+
this.toolResultListeners.add(listener);
|
|
247
|
+
return () => {
|
|
248
|
+
this.toolResultListeners.delete(listener);
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// -------------------------------------------------------------------------
|
|
252
|
+
// Tool calls
|
|
253
|
+
// -------------------------------------------------------------------------
|
|
254
|
+
/**
|
|
255
|
+
* Attach a tool call to a streaming assistant message. No-op when
|
|
256
|
+
* the message id is unknown (race between transport events and
|
|
257
|
+
* reset / late mount).
|
|
258
|
+
*/
|
|
259
|
+
addToolCall(messageId, toolCall) {
|
|
260
|
+
const msg = this._messages.find((m) => m.id === messageId);
|
|
261
|
+
if (!msg) return;
|
|
262
|
+
msg.toolCalls = [...msg.toolCalls ?? [], { ...toolCall }];
|
|
263
|
+
this.notify();
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Partially update a tool call by id. Used by the transport to
|
|
267
|
+
* advance status (args-streaming → running → done) as AG-UI events
|
|
268
|
+
* arrive. No-op when the id is unknown.
|
|
269
|
+
*/
|
|
270
|
+
updateToolCall(toolCallId, patch) {
|
|
271
|
+
for (const msg of this._messages) {
|
|
272
|
+
const tcs = msg.toolCalls;
|
|
273
|
+
if (!tcs) continue;
|
|
274
|
+
const idx = tcs.findIndex((tc) => tc.id === toolCallId);
|
|
275
|
+
if (idx === -1) continue;
|
|
276
|
+
const next = [...tcs];
|
|
277
|
+
next[idx] = { ...next[idx], ...patch };
|
|
278
|
+
msg.toolCalls = next;
|
|
279
|
+
this.notify();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Resolve a (client-) tool call. Marks the call done, persists, and
|
|
285
|
+
* fires onToolResult so the transport can forward the result + the
|
|
286
|
+
* user's approve/reject decision back to the agent. No-op when the
|
|
287
|
+
* id is unknown.
|
|
288
|
+
*/
|
|
289
|
+
resolveToolCall(toolCallId, result, approved) {
|
|
290
|
+
let found = false;
|
|
291
|
+
for (const msg of this._messages) {
|
|
292
|
+
const tcs = msg.toolCalls;
|
|
293
|
+
if (!tcs) continue;
|
|
294
|
+
const idx = tcs.findIndex((tc) => tc.id === toolCallId);
|
|
295
|
+
if (idx === -1) continue;
|
|
296
|
+
const next = [...tcs];
|
|
297
|
+
next[idx] = { ...next[idx], status: "done" };
|
|
298
|
+
msg.toolCalls = next;
|
|
299
|
+
found = true;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
if (!found) return;
|
|
303
|
+
this.notify();
|
|
304
|
+
for (const listener of this.toolResultListeners) {
|
|
305
|
+
listener({ toolCallId, result, approved });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Hydrate from the server transcript (the /session SSE's
|
|
310
|
+
* messages-snapshot) — the server is the transcript of record
|
|
311
|
+
* (FEAT-1783701440). Snapshot order is truth: shared ids keep the
|
|
312
|
+
* LOCAL object (it may carry richer live tool-call state) but take the
|
|
313
|
+
* snapshot's position; snapshot-only messages (history on reload, a
|
|
314
|
+
* nudge missed during an SSE gap) are inserted; local-only messages
|
|
315
|
+
* (an in-flight turn, the greeting) float after in local order.
|
|
316
|
+
*
|
|
317
|
+
* Deploy-boundary guard: a user message persisted under a pre-Phase-2
|
|
318
|
+
* transport id (before ids were threaded) would arrive as
|
|
319
|
+
* "snapshot-only" next to its local twin — skip snapshot USER messages
|
|
320
|
+
* whose exact text already exists locally under a different id.
|
|
321
|
+
*/
|
|
322
|
+
hydrate(snapshotMessages) {
|
|
323
|
+
if (!snapshotMessages.length) return;
|
|
324
|
+
const localById = new Map(this._messages.map((m) => [String(m.id), m]));
|
|
325
|
+
const twinBudget = /* @__PURE__ */ new Map();
|
|
326
|
+
for (const m of this._messages) {
|
|
327
|
+
if (m.role === "user") twinBudget.set(m.text, (twinBudget.get(m.text) ?? 0) + 1);
|
|
328
|
+
}
|
|
329
|
+
const seen = /* @__PURE__ */ new Set();
|
|
330
|
+
const merged = [];
|
|
331
|
+
for (const sm of snapshotMessages) {
|
|
332
|
+
const key = String(sm.id);
|
|
333
|
+
const local = localById.get(key);
|
|
334
|
+
if (local) {
|
|
335
|
+
seen.add(key);
|
|
336
|
+
merged.push(local);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (sm.role === "user") {
|
|
340
|
+
const budget = twinBudget.get(sm.text) ?? 0;
|
|
341
|
+
if (budget > 0) {
|
|
342
|
+
twinBudget.set(sm.text, budget - 1);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
merged.push(sm);
|
|
347
|
+
}
|
|
348
|
+
for (const m of this._messages) {
|
|
349
|
+
if (!seen.has(String(m.id))) merged.push(m);
|
|
350
|
+
}
|
|
351
|
+
this._messages = merged;
|
|
352
|
+
this.notify();
|
|
353
|
+
}
|
|
354
|
+
notify() {
|
|
355
|
+
const state = this.getState();
|
|
356
|
+
for (const sub of this.subscribers) sub(state);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var chatSession = new ChatSession();
|
|
360
|
+
|
|
361
|
+
// src/ChatTransport.ts
|
|
362
|
+
var FALLBACK_DEBOUNCE_MS = 1500;
|
|
363
|
+
function readSyntroToken() {
|
|
364
|
+
if (typeof window === "undefined") return void 0;
|
|
365
|
+
const cfg = window.__SYNTRO_CONFIG__;
|
|
366
|
+
const token = cfg?.token;
|
|
367
|
+
return typeof token === "string" && token.length > 0 ? token : void 0;
|
|
368
|
+
}
|
|
369
|
+
function debug(...args) {
|
|
370
|
+
if (typeof window === "undefined") return;
|
|
371
|
+
if (window.__SYNTRO_CHAT_DEBUG__) {
|
|
372
|
+
console.debug("[chat-transport]", ...args);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function parseToolCallArgs(toolCall) {
|
|
376
|
+
if (toolCall.args && typeof toolCall.args === "object") return toolCall.args;
|
|
377
|
+
const buf = toolCall.argsBuffer;
|
|
378
|
+
if (!buf) return null;
|
|
379
|
+
try {
|
|
380
|
+
const parsed = JSON.parse(buf);
|
|
381
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
382
|
+
} catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function toolNarrationVariants(name) {
|
|
387
|
+
const n = (name || "").toLowerCase();
|
|
388
|
+
if (n.includes("search") || n.includes("retrieve"))
|
|
389
|
+
return ["Assembling relevant information", "Pulling together what matters for you"];
|
|
390
|
+
if (n.includes("route"))
|
|
391
|
+
return ["Diving deeper on specific products for you", "Exploring the best options for you"];
|
|
392
|
+
if (n.includes("read") || n.includes("section") || n.includes("overview"))
|
|
393
|
+
return ["Buttoning up details to point the right way", "Lining up the details for you"];
|
|
394
|
+
if (n.includes("selector"))
|
|
395
|
+
return ["Finalizing where to guide you", "Pinpointing the right spot for you"];
|
|
396
|
+
if (n.includes("wiki"))
|
|
397
|
+
return ["Looking that up for you", "Checking the knowledge base for you"];
|
|
398
|
+
if (n.includes("mount") || n.includes("unmount") || n.includes("element") || n.includes("response") || n.includes("primary") || n.includes("action"))
|
|
399
|
+
return ["Materializing the information for you", "Bringing it onto your screen"];
|
|
400
|
+
return ["Putting your answer together for you", "Almost there"];
|
|
401
|
+
}
|
|
402
|
+
function mergeForwardedProps(prev, next) {
|
|
403
|
+
if (next.forwardedProps !== void 0) return next;
|
|
404
|
+
if (!prev?.forwardedProps) return next;
|
|
405
|
+
return { ...next, forwardedProps: prev.forwardedProps };
|
|
406
|
+
}
|
|
407
|
+
var ChatTransport = class {
|
|
408
|
+
constructor() {
|
|
409
|
+
this._config = null;
|
|
410
|
+
this._status = "idle";
|
|
411
|
+
this._agui = null;
|
|
412
|
+
this._transportUnsub = null;
|
|
413
|
+
this._sessionUnsubSend = null;
|
|
414
|
+
this._sessionUnsubInterrupt = null;
|
|
415
|
+
this._sessionUnsubToolResult = null;
|
|
416
|
+
this._connectInFlight = null;
|
|
417
|
+
/** Bounded streaming-message id for the currently-being-typed assistant turn. */
|
|
418
|
+
this._currentAssistantMessageId = null;
|
|
419
|
+
/** Outcome bookkeeping for telemetry. */
|
|
420
|
+
this._hadTurnstileToken = null;
|
|
421
|
+
/** Count of successful round-trips for telemetry payloads. */
|
|
422
|
+
this._messagesSucceeded = 0;
|
|
423
|
+
/** True once a successful assistant message has landed — gates fallback. */
|
|
424
|
+
this._hasSucceeded = false;
|
|
425
|
+
/** True once fallback has fired (one-shot). */
|
|
426
|
+
this._fallbackRendered = false;
|
|
427
|
+
/**
|
|
428
|
+
* Per-send override for ``X-Active-Lid-Slot``. Set in the
|
|
429
|
+
* ``chatSession.onSend`` listener immediately before each
|
|
430
|
+
* ``_forwardUserMessage`` and consumed by ``buildHeaders`` on the
|
|
431
|
+
* outbound request. The singleton transport is shared by every
|
|
432
|
+
* mounted chat-bar (drawer + inline) so cached ``_config.activeLidSlot``
|
|
433
|
+
* follows whichever bar last reconfigured — using that for routing
|
|
434
|
+
* silently misroutes tiles when both bars coexist. Per-send threading
|
|
435
|
+
* pins the header to the bar that actually sent the message.
|
|
436
|
+
*/
|
|
437
|
+
this._pendingLidSlot = null;
|
|
438
|
+
/** Active debounce timer; null when no debounce pending. */
|
|
439
|
+
this._errorDebounceTimer = null;
|
|
440
|
+
/** Rotates thinking-chip phrasings during a long/looping tool phase (C9). */
|
|
441
|
+
this._thinkingCycleTimer = null;
|
|
442
|
+
/**
|
|
443
|
+
* Per-class rotation counter, keyed by the narration set's first variant.
|
|
444
|
+
* A NEW tool class shows its primary (variant A) so it's recognizable; a
|
|
445
|
+
* REPEATED same-class call advances to the next variant, so a research loop
|
|
446
|
+
* of (e.g.) several searches reads as different thinking, not one repeated
|
|
447
|
+
* label. Bounded to ~6 keys (one per tool class).
|
|
448
|
+
*/
|
|
449
|
+
this._thinkingClassSeq = /* @__PURE__ */ new Map();
|
|
450
|
+
/** Most recent error payload, captured so debounced fallback can attach it. */
|
|
451
|
+
this._lastErrorPayload = null;
|
|
452
|
+
this._fallbackListeners = /* @__PURE__ */ new Set();
|
|
453
|
+
/**
|
|
454
|
+
* Per-configure-cycle id for correlating telemetry events from a
|
|
455
|
+
* single transport lifetime. Mirrors ChatAssistantLit's mountId
|
|
456
|
+
* but scoped to configure cycles since the transport is a
|
|
457
|
+
* singleton across mounts.
|
|
458
|
+
*/
|
|
459
|
+
this._transportId = `tx_${Math.random().toString(36).slice(2, 8)}`;
|
|
460
|
+
this._configuredAt = 0;
|
|
461
|
+
// -------------------------------------------------------------------------
|
|
462
|
+
// Session stream — server-initiated events (proactive nudges)
|
|
463
|
+
// -------------------------------------------------------------------------
|
|
464
|
+
/** AbortController for the live /session SSE reader; null = not running. */
|
|
465
|
+
this._sessionStreamAbort = null;
|
|
466
|
+
/** Capped reconnect backoff for the session stream. */
|
|
467
|
+
this._sessionStreamBackoffMs = 2e3;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Configure the transport. Idempotent — calling again with the same
|
|
471
|
+
* backendUrl is a no-op; calling with a different backendUrl tears
|
|
472
|
+
* the connection down and re-arms.
|
|
473
|
+
*/
|
|
474
|
+
configure(config) {
|
|
475
|
+
const same = this._config && this._config.backendUrl === config.backendUrl && this._config.threadId === config.threadId;
|
|
476
|
+
if (same) {
|
|
477
|
+
this._config = { ...this._config, ...mergeForwardedProps(this._config, config) };
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this._disconnect();
|
|
481
|
+
this._config = config;
|
|
482
|
+
this._status = "configured";
|
|
483
|
+
this._transportId = `tx_${Math.random().toString(36).slice(2, 8)}`;
|
|
484
|
+
this._configuredAt = Date.now();
|
|
485
|
+
this._wireSession();
|
|
486
|
+
debug("configured", {
|
|
487
|
+
transportId: this._transportId,
|
|
488
|
+
backendUrl: config.backendUrl,
|
|
489
|
+
threadId: config.threadId
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
/** ms since the most recent configure() call. 0 before any configure. */
|
|
493
|
+
_ageMs() {
|
|
494
|
+
return this._configuredAt === 0 ? 0 : Date.now() - this._configuredAt;
|
|
495
|
+
}
|
|
496
|
+
/** True when configure() has been called and we're ready to lazy-connect on send. */
|
|
497
|
+
get isConfigured() {
|
|
498
|
+
return this._status !== "idle";
|
|
499
|
+
}
|
|
500
|
+
/** True when AgUiTransport is up. */
|
|
501
|
+
get isConnected() {
|
|
502
|
+
return this._status === "connected";
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Subscribe to fallback events — fires once per configure cycle
|
|
506
|
+
* when the transport gives up after sustained failure. Hosts
|
|
507
|
+
* (typically AdaptiveChatBarMountable) use this to swap the chat
|
|
508
|
+
* bar for a static "contact support" card.
|
|
509
|
+
*/
|
|
510
|
+
onFallback(listener) {
|
|
511
|
+
this._fallbackListeners.add(listener);
|
|
512
|
+
return () => {
|
|
513
|
+
this._fallbackListeners.delete(listener);
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Test seam — drive a synthetic error event through the transport's
|
|
518
|
+
* error handling without standing up a real AgUi transport. Production
|
|
519
|
+
* code path uses _onTransportEvent. Exported as a public method to
|
|
520
|
+
* keep the test isolation simple; not part of the documented API.
|
|
521
|
+
*/
|
|
522
|
+
simulateError(payload) {
|
|
523
|
+
this._handleErrorEvent(payload);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Test seam — drive the FULL server-error path (fallback kernel + the
|
|
527
|
+
* visitor-facing chip), exactly what the `case 'error'` transport-event
|
|
528
|
+
* branch does. `midAnswer` simulates an assistant message having been
|
|
529
|
+
* mid-stream when the connection died.
|
|
530
|
+
*/
|
|
531
|
+
simulateServerError(payload) {
|
|
532
|
+
if (payload.midAnswer) this._currentAssistantMessageId = "test-mid-answer";
|
|
533
|
+
this._onTransportEvent({
|
|
534
|
+
type: "error",
|
|
535
|
+
message: payload.message ?? "boom",
|
|
536
|
+
status: payload.status ?? null,
|
|
537
|
+
body: payload.body ?? null,
|
|
538
|
+
errorName: payload.errorName ?? null
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Test seam — register a synthetic successful message-complete so
|
|
543
|
+
* the hasSucceeded gate flips without a real AgUi round-trip.
|
|
544
|
+
*/
|
|
545
|
+
simulateSuccessfulMessage() {
|
|
546
|
+
this._hasSucceeded = true;
|
|
547
|
+
this._messagesSucceeded += 1;
|
|
548
|
+
this._clearDebounceTimer();
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Test seam — return the lid slot that the next outbound request
|
|
552
|
+
* WOULD tag ``X-Active-Lid-Slot`` with, given current state. Mirrors
|
|
553
|
+
* the exact resolution buildHeaders uses (per-send slot wins over
|
|
554
|
+
* cached config). Not part of the documented API.
|
|
555
|
+
*/
|
|
556
|
+
getActiveLidSlotForTest() {
|
|
557
|
+
return this._pendingLidSlot ?? this._config?.activeLidSlot ?? null;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Tear connection-level state down. Used internally by configure()
|
|
561
|
+
* to swap backends; preserves host-registered fallback listeners
|
|
562
|
+
* because the host UI handler (e.g. AdaptiveChatBarMountable's
|
|
563
|
+
* "swap to fallback card" callback) is configuration-independent.
|
|
564
|
+
*/
|
|
565
|
+
_disconnect() {
|
|
566
|
+
if (this._transportUnsub) {
|
|
567
|
+
this._transportUnsub();
|
|
568
|
+
this._transportUnsub = null;
|
|
569
|
+
}
|
|
570
|
+
if (this._sessionUnsubSend) {
|
|
571
|
+
this._sessionUnsubSend();
|
|
572
|
+
this._sessionUnsubSend = null;
|
|
573
|
+
}
|
|
574
|
+
if (this._sessionUnsubInterrupt) {
|
|
575
|
+
this._sessionUnsubInterrupt();
|
|
576
|
+
this._sessionUnsubInterrupt = null;
|
|
577
|
+
}
|
|
578
|
+
if (this._sessionUnsubToolResult) {
|
|
579
|
+
this._sessionUnsubToolResult();
|
|
580
|
+
this._sessionUnsubToolResult = null;
|
|
581
|
+
}
|
|
582
|
+
if (this._agui) {
|
|
583
|
+
this._agui.disconnect();
|
|
584
|
+
this._agui = null;
|
|
585
|
+
}
|
|
586
|
+
this._clearDebounceTimer();
|
|
587
|
+
this._stopThinkingCycle();
|
|
588
|
+
this._thinkingClassSeq.clear();
|
|
589
|
+
this._config = null;
|
|
590
|
+
this._status = "idle";
|
|
591
|
+
this._connectInFlight = null;
|
|
592
|
+
this._currentAssistantMessageId = null;
|
|
593
|
+
this._hadTurnstileToken = null;
|
|
594
|
+
this._messagesSucceeded = 0;
|
|
595
|
+
this._hasSucceeded = false;
|
|
596
|
+
this._fallbackRendered = false;
|
|
597
|
+
this._lastErrorPayload = null;
|
|
598
|
+
this._pendingLidSlot = null;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Tear everything down — connection state PLUS host listeners.
|
|
602
|
+
* Use this in test teardown or when fully shutting the transport
|
|
603
|
+
* (page unload, integration test reset). The mountable calls
|
|
604
|
+
* _disconnect indirectly via reconfigure.
|
|
605
|
+
*/
|
|
606
|
+
reset() {
|
|
607
|
+
this._disconnect();
|
|
608
|
+
this._fallbackListeners.clear();
|
|
609
|
+
this.stopSessionStream();
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Open the persistent `/api/adaptive/session` SSE and feed its frames
|
|
613
|
+
* through the SAME `_onTransportEvent` switch the AG-UI path uses
|
|
614
|
+
* (FEAT-1783640628 — proactive nudges).
|
|
615
|
+
*
|
|
616
|
+
* The AG-UI cutover left this endpoint with no client consumer, so
|
|
617
|
+
* server-INITIATED events had nowhere to land. A fetch-based reader —
|
|
618
|
+
* unlike EventSource — can send the SDK token, and the visitor ids that
|
|
619
|
+
* write the server's nudge identity bridge. Idempotent; reconnects with
|
|
620
|
+
* capped backoff; `reset()`/`stopSessionStream()` tears it down.
|
|
621
|
+
*/
|
|
622
|
+
async startSessionStream() {
|
|
623
|
+
if (this._sessionStreamAbort || !this._config) return;
|
|
624
|
+
const abort = new AbortController();
|
|
625
|
+
this._sessionStreamAbort = abort;
|
|
626
|
+
const baseUrl = this._config.backendUrl.replace(/\/$/, "");
|
|
627
|
+
const url = `${baseUrl}/api/adaptive/session`;
|
|
628
|
+
const headers = {};
|
|
629
|
+
const token = readSyntroToken();
|
|
630
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
631
|
+
const cft = await this._acquireTurnstileWithChallenge();
|
|
632
|
+
if (cft) headers["CF-Turnstile-Token"] = cft;
|
|
633
|
+
if (abort.signal.aborted) return;
|
|
634
|
+
const readIds = () => {
|
|
635
|
+
try {
|
|
636
|
+
const rt = this._config?.runtime;
|
|
637
|
+
const did = rt?.telemetry?.getDistinctId?.();
|
|
638
|
+
const sid = rt?.telemetry?.getSessionId?.();
|
|
639
|
+
return {
|
|
640
|
+
did: typeof did === "string" && did.length > 0 ? did : void 0,
|
|
641
|
+
sid: typeof sid === "string" && sid.length > 0 ? sid : void 0
|
|
642
|
+
};
|
|
643
|
+
} catch {
|
|
644
|
+
return {};
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
const hasTelemetryAccessor = () => {
|
|
648
|
+
try {
|
|
649
|
+
const rt = this._config?.runtime;
|
|
650
|
+
return typeof rt?.telemetry?.getDistinctId === "function";
|
|
651
|
+
} catch {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
let ids = readIds();
|
|
656
|
+
for (let i = 0; i < 80 && hasTelemetryAccessor() && !ids.did && !abort.signal.aborted; i++) {
|
|
657
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
658
|
+
ids = readIds();
|
|
659
|
+
}
|
|
660
|
+
if (abort.signal.aborted) return;
|
|
661
|
+
if (ids.did) headers["X-Distinct-Id"] = ids.did;
|
|
662
|
+
if (ids.sid) headers["X-Syntro-Session"] = ids.sid;
|
|
663
|
+
const runReader = async () => {
|
|
664
|
+
const response = await fetch(url, {
|
|
665
|
+
headers,
|
|
666
|
+
credentials: "include",
|
|
667
|
+
signal: abort.signal
|
|
668
|
+
});
|
|
669
|
+
if (!response.ok || !response.body) {
|
|
670
|
+
throw new Error(`session stream connect failed: ${response.status}`);
|
|
671
|
+
}
|
|
672
|
+
this._sessionStreamBackoffMs = 2e3;
|
|
673
|
+
const reader = response.body.getReader();
|
|
674
|
+
const decoder = new TextDecoder();
|
|
675
|
+
let buffer = "";
|
|
676
|
+
for (; ; ) {
|
|
677
|
+
const { done, value } = await reader.read();
|
|
678
|
+
if (done) return;
|
|
679
|
+
buffer += decoder.decode(value, { stream: true });
|
|
680
|
+
let sep = buffer.indexOf("\n\n");
|
|
681
|
+
while (sep >= 0) {
|
|
682
|
+
const frame = buffer.slice(0, sep);
|
|
683
|
+
buffer = buffer.slice(sep + 2);
|
|
684
|
+
sep = buffer.indexOf("\n\n");
|
|
685
|
+
const data = frame.split("\n").filter((l) => l.startsWith("data: ")).map((l) => l.slice(6)).join("");
|
|
686
|
+
if (!data) continue;
|
|
687
|
+
try {
|
|
688
|
+
this._onTransportEvent(JSON.parse(data));
|
|
689
|
+
} catch (err) {
|
|
690
|
+
console.warn("[adaptive-chatbot] session-stream frame parse failed", err);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
void (async () => {
|
|
696
|
+
while (!abort.signal.aborted) {
|
|
697
|
+
try {
|
|
698
|
+
await runReader();
|
|
699
|
+
} catch (err) {
|
|
700
|
+
if (abort.signal.aborted) return;
|
|
701
|
+
console.warn("[adaptive-chatbot] session stream dropped; reconnecting", err);
|
|
702
|
+
}
|
|
703
|
+
if (abort.signal.aborted) return;
|
|
704
|
+
await new Promise((r) => setTimeout(r, this._sessionStreamBackoffMs));
|
|
705
|
+
this._sessionStreamBackoffMs = Math.min(this._sessionStreamBackoffMs * 2, 3e4);
|
|
706
|
+
const fresh = readIds();
|
|
707
|
+
if (fresh.did) headers["X-Distinct-Id"] = fresh.did;
|
|
708
|
+
if (fresh.sid) headers["X-Syntro-Session"] = fresh.sid;
|
|
709
|
+
}
|
|
710
|
+
})();
|
|
711
|
+
await Promise.resolve();
|
|
712
|
+
}
|
|
713
|
+
stopSessionStream() {
|
|
714
|
+
this._sessionStreamAbort?.abort();
|
|
715
|
+
this._sessionStreamAbort = null;
|
|
716
|
+
this._sessionStreamBackoffMs = 2e3;
|
|
717
|
+
}
|
|
718
|
+
_clearDebounceTimer() {
|
|
719
|
+
if (this._errorDebounceTimer) {
|
|
720
|
+
clearTimeout(this._errorDebounceTimer);
|
|
721
|
+
this._errorDebounceTimer = null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Show a per-call rotating variant in the thinking chip immediately (so
|
|
726
|
+
* consecutive tool calls differ), then rotate through the remaining phrasings
|
|
727
|
+
* every 4s while the same tool phase runs (C9). A
|
|
728
|
+
* single tool call can sit for tens of seconds (the model reasoning about the
|
|
729
|
+
* next step, or a slow retrieval); a static chip reads as frozen and hides a
|
|
730
|
+
* loop. Cycling makes a long/looping phase read as live progress. Restarting
|
|
731
|
+
* (a new tool), any output (message/thinking), or teardown clears the cycle.
|
|
732
|
+
*/
|
|
733
|
+
_startThinkingCycle(variants) {
|
|
734
|
+
this._stopThinkingCycle();
|
|
735
|
+
const key = variants[0] ?? "";
|
|
736
|
+
const seq = this._thinkingClassSeq.get(key) ?? 0;
|
|
737
|
+
this._thinkingClassSeq.set(key, seq + 1);
|
|
738
|
+
let i = variants.length > 0 ? seq % variants.length : 0;
|
|
739
|
+
chatSession.setThinkingText(variants[i] ?? "Working on it\u2026");
|
|
740
|
+
if (variants.length < 2) return;
|
|
741
|
+
const id = setInterval(() => {
|
|
742
|
+
if (this._thinkingCycleTimer !== id) return;
|
|
743
|
+
i = (i + 1) % variants.length;
|
|
744
|
+
chatSession.setThinkingText(variants[i]);
|
|
745
|
+
}, 4e3);
|
|
746
|
+
this._thinkingCycleTimer = id;
|
|
747
|
+
}
|
|
748
|
+
_stopThinkingCycle() {
|
|
749
|
+
if (this._thinkingCycleTimer) {
|
|
750
|
+
clearInterval(this._thinkingCycleTimer);
|
|
751
|
+
this._thinkingCycleTimer = null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Shared error-handling kernel — called by the AG-UI subscriber and
|
|
756
|
+
* by the simulateError test seam. Publishes transport_error
|
|
757
|
+
* telemetry, captures lastErrorPayload, and starts the debounce
|
|
758
|
+
* timer that will fire fallback if no successful message arrives
|
|
759
|
+
* before it expires. Gated by hasSucceeded (post-success errors
|
|
760
|
+
* never fallback) and _fallbackRendered (one-shot).
|
|
761
|
+
*/
|
|
762
|
+
_handleErrorEvent(payload) {
|
|
763
|
+
const status = payload.status ?? null;
|
|
764
|
+
const body = payload.body ? String(payload.body).slice(0, 200) : null;
|
|
765
|
+
this._lastErrorPayload = {
|
|
766
|
+
message: payload.message,
|
|
767
|
+
status,
|
|
768
|
+
body,
|
|
769
|
+
errorName: payload.errorName ?? null
|
|
770
|
+
};
|
|
771
|
+
this._stopThinkingCycle();
|
|
772
|
+
this._config?.runtime.events.publish("chatbot.transport_error", {
|
|
773
|
+
source: "chat-transport",
|
|
774
|
+
transportId: this._transportId,
|
|
775
|
+
transportAgeMs: this._ageMs(),
|
|
776
|
+
messagesSucceeded: this._messagesSucceeded,
|
|
777
|
+
hadTurnstileToken: this._hadTurnstileToken,
|
|
778
|
+
hasSucceeded: this._hasSucceeded,
|
|
779
|
+
errorMessage: payload.message ?? null,
|
|
780
|
+
errorStatus: status,
|
|
781
|
+
errorBody: body,
|
|
782
|
+
errorName: payload.errorName ?? null
|
|
783
|
+
});
|
|
784
|
+
if (this._hasSucceeded || this._fallbackRendered || this._errorDebounceTimer) return;
|
|
785
|
+
this._errorDebounceTimer = setTimeout(() => {
|
|
786
|
+
this._errorDebounceTimer = null;
|
|
787
|
+
if (this._hasSucceeded || this._fallbackRendered) return;
|
|
788
|
+
this._renderFallback("connect_failed");
|
|
789
|
+
}, FALLBACK_DEBOUNCE_MS);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Fire the fallback. One-shot per configure cycle. Notifies all
|
|
793
|
+
* fallback listeners with the per-customer card config and the
|
|
794
|
+
* diagnostic snapshot for telemetry/debug.
|
|
795
|
+
*/
|
|
796
|
+
_renderFallback(reason) {
|
|
797
|
+
if (this._fallbackRendered) return;
|
|
798
|
+
this._fallbackRendered = true;
|
|
799
|
+
debug("fallback", { reason, transportId: this._transportId, ageMs: this._ageMs() });
|
|
800
|
+
const fallback = this._config?.fallback ?? {};
|
|
801
|
+
const payload = {
|
|
802
|
+
reason,
|
|
803
|
+
fallback,
|
|
804
|
+
transportId: this._transportId,
|
|
805
|
+
transportAgeMs: this._ageMs(),
|
|
806
|
+
messagesSucceeded: this._messagesSucceeded,
|
|
807
|
+
hadTurnstileToken: this._hadTurnstileToken,
|
|
808
|
+
errorStatus: this._lastErrorPayload?.status ?? null,
|
|
809
|
+
errorBody: this._lastErrorPayload?.body ?? null,
|
|
810
|
+
errorMessage: this._lastErrorPayload?.message ?? null,
|
|
811
|
+
errorName: this._lastErrorPayload?.errorName ?? null
|
|
812
|
+
};
|
|
813
|
+
this._config?.runtime.events.publish(
|
|
814
|
+
"chatbot.fallback_rendered",
|
|
815
|
+
payload
|
|
816
|
+
);
|
|
817
|
+
for (const listener of this._fallbackListeners) listener(payload);
|
|
818
|
+
}
|
|
819
|
+
// -------------------------------------------------------------------------
|
|
820
|
+
// Internal: chatSession wiring
|
|
821
|
+
// -------------------------------------------------------------------------
|
|
822
|
+
_wireSession() {
|
|
823
|
+
this._sessionUnsubSend = chatSession.onSend(({ text, id, activeLidSlot }) => {
|
|
824
|
+
this._pendingLidSlot = typeof activeLidSlot === "string" && activeLidSlot.length > 0 ? activeLidSlot : null;
|
|
825
|
+
void this._forwardUserMessage(text, id);
|
|
826
|
+
});
|
|
827
|
+
this._sessionUnsubInterrupt = chatSession.onInterrupt(() => {
|
|
828
|
+
this._agui?.send({ type: "stop-generation" });
|
|
829
|
+
});
|
|
830
|
+
this._sessionUnsubToolResult = chatSession.onToolResult(({ toolCallId, result, approved }) => {
|
|
831
|
+
this._agui?.send({ type: "tool-result", toolCallId, result, approved });
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
async _forwardUserMessage(text, id) {
|
|
835
|
+
let ok = false;
|
|
836
|
+
try {
|
|
837
|
+
ok = await this._ensureConnected();
|
|
838
|
+
} catch (err) {
|
|
839
|
+
this._status = "error";
|
|
840
|
+
this._handleErrorEvent({
|
|
841
|
+
message: err instanceof Error ? err.message : String(err),
|
|
842
|
+
errorName: err instanceof Error ? err.name : null
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
if (!ok || !this._agui) {
|
|
846
|
+
chatSession.error("Couldn't connect to chat. Please try again.");
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
this._agui.send({ type: "user-message", text, ...id ? { id } : {} });
|
|
850
|
+
}
|
|
851
|
+
// -------------------------------------------------------------------------
|
|
852
|
+
// Internal: connect (lazy, Turnstile-gated)
|
|
853
|
+
// -------------------------------------------------------------------------
|
|
854
|
+
/**
|
|
855
|
+
* Ensure AgUiTransport is up. Idempotent — multiple concurrent calls
|
|
856
|
+
* dedupe to a single Turnstile acquisition + transport setup.
|
|
857
|
+
* Returns true on success, false on terminal failure.
|
|
858
|
+
*/
|
|
859
|
+
async _ensureConnected() {
|
|
860
|
+
if (this._status === "connected" && this._agui) return true;
|
|
861
|
+
if (!this._config) return false;
|
|
862
|
+
if (this._connectInFlight) return this._connectInFlight;
|
|
863
|
+
this._connectInFlight = (async () => {
|
|
864
|
+
this._status = "acquiring";
|
|
865
|
+
const cft = await this._acquireTurnstileWithChallenge();
|
|
866
|
+
this._hadTurnstileToken = cft !== null;
|
|
867
|
+
if (this._config === null) return false;
|
|
868
|
+
const baseUrl = this._config.backendUrl.replace(/\/$/, "");
|
|
869
|
+
const streamUrl = `${baseUrl}/api/adaptive/stream`;
|
|
870
|
+
const token = readSyntroToken();
|
|
871
|
+
const buildHeaders = () => {
|
|
872
|
+
const h = {};
|
|
873
|
+
if (token) h.Authorization = `Bearer ${token}`;
|
|
874
|
+
if (cft) h["CF-Turnstile-Token"] = cft;
|
|
875
|
+
const lidSlot = this._pendingLidSlot ?? this._config?.activeLidSlot;
|
|
876
|
+
if (typeof lidSlot === "string" && lidSlot.length > 0) {
|
|
877
|
+
h["X-Active-Lid-Slot"] = lidSlot;
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
const rt = this._config?.runtime;
|
|
881
|
+
const did = rt?.telemetry?.getDistinctId?.();
|
|
882
|
+
if (typeof did === "string" && did.length > 0) {
|
|
883
|
+
h["X-Distinct-Id"] = did;
|
|
884
|
+
}
|
|
885
|
+
const sid = rt?.telemetry?.getSessionId?.();
|
|
886
|
+
if (typeof sid === "string" && sid.length > 0) {
|
|
887
|
+
h["X-Syntro-Session"] = sid;
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
return h;
|
|
892
|
+
};
|
|
893
|
+
const resolveForwardedProps = () => {
|
|
894
|
+
const raw = this._config?.forwardedProps;
|
|
895
|
+
return typeof raw === "function" ? raw() : raw;
|
|
896
|
+
};
|
|
897
|
+
this._agui = new AgUiTransport({
|
|
898
|
+
url: streamUrl,
|
|
899
|
+
headers: buildHeaders,
|
|
900
|
+
threadId: this._config.threadId,
|
|
901
|
+
clientTools: this._config.clientTools,
|
|
902
|
+
// Adaptive runtime SDK needs the `syntro_chat_session` cookie to
|
|
903
|
+
// round-trip cross-origin so subsequent boot fetches can rehydrate
|
|
904
|
+
// LLM-authored UI elements. Editor / action-plan chat surfaces
|
|
905
|
+
// auth via `?token=` and intentionally leave this unset (see
|
|
906
|
+
// AgUiTransportOptions.credentials docstring).
|
|
907
|
+
credentials: "include",
|
|
908
|
+
forwardedProps: resolveForwardedProps,
|
|
909
|
+
onA2UIEvent: (payload) => {
|
|
910
|
+
const runtime2 = this._config?.runtime;
|
|
911
|
+
if (!runtime2) return;
|
|
912
|
+
const mutations = decodeMutationEnvelope(payload);
|
|
913
|
+
if (mutations !== null) {
|
|
914
|
+
const handler = this._config?.onElementMutation;
|
|
915
|
+
if (handler) {
|
|
916
|
+
try {
|
|
917
|
+
handler(mutations);
|
|
918
|
+
runtime2.events.publish("chatbot.element_mutation_applied", {
|
|
919
|
+
source: "chat-transport",
|
|
920
|
+
count: mutations.length
|
|
921
|
+
});
|
|
922
|
+
} catch (err) {
|
|
923
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
924
|
+
console.error("[chat-transport] element mutation apply failed:", msg);
|
|
925
|
+
}
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
runtime2.actions.applyBatch([payload]).then(() => {
|
|
931
|
+
runtime2.events.publish("chatbot.a2ui_applied", { source: "chat-transport" });
|
|
932
|
+
}).catch((err) => {
|
|
933
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
934
|
+
console.error("[chat-transport] A2UI apply failed:", msg);
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
this._agui.connect();
|
|
939
|
+
this._transportUnsub = this._agui.subscribe((event) => this._onTransportEvent(event));
|
|
940
|
+
this._status = "connected";
|
|
941
|
+
return true;
|
|
942
|
+
})();
|
|
943
|
+
try {
|
|
944
|
+
return await this._connectInFlight;
|
|
945
|
+
} finally {
|
|
946
|
+
this._connectInFlight = null;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Acquire a Turnstile token via the managed-challenge flow.
|
|
951
|
+
* Delegates to the shared Turnstile helper that owns the verify
|
|
952
|
+
* panel lifecycle. Returns null when Turnstile is disabled at
|
|
953
|
+
* build time or acquisition fails.
|
|
954
|
+
*/
|
|
955
|
+
async _acquireTurnstileWithChallenge() {
|
|
956
|
+
const { token } = await acquireTokenWithChallenge();
|
|
957
|
+
return token;
|
|
958
|
+
}
|
|
959
|
+
// -------------------------------------------------------------------------
|
|
960
|
+
// Internal: AgUi event → chatSession
|
|
961
|
+
// -------------------------------------------------------------------------
|
|
962
|
+
/**
|
|
963
|
+
* Look up a tool call's current chatSession-side status by id.
|
|
964
|
+
* Used to decide between addToolCall (first sighting) and
|
|
965
|
+
* updateToolCall (subsequent updates) when AG-UI re-emits
|
|
966
|
+
* `tool-call` events for client tools transitioning to 'pending'.
|
|
967
|
+
*/
|
|
968
|
+
_findToolCallStatus(toolCallId) {
|
|
969
|
+
const state = chatSession.getState();
|
|
970
|
+
for (const msg of state.messages) {
|
|
971
|
+
const tc = msg.toolCalls?.find((t) => t.id === toolCallId);
|
|
972
|
+
if (tc) return tc.status;
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Convert one server-transcript message (the session store shape:
|
|
978
|
+
* ``{id, role, content, timestamp, status, toolCalls?}`` with toolCalls
|
|
979
|
+
* in AG-UI wire shape) into a TrailMessage. Returns null for entries the
|
|
980
|
+
* trail can't render (missing id, system/tool roles, empty content with
|
|
981
|
+
* no tool receipts) — the transcript may legitimately contain them.
|
|
982
|
+
*/
|
|
983
|
+
_snapshotToTrailMessage(raw) {
|
|
984
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
985
|
+
const m = raw;
|
|
986
|
+
const id = m.id;
|
|
987
|
+
const role = m.role;
|
|
988
|
+
if (typeof id !== "string" || !id) return null;
|
|
989
|
+
if (role !== "user" && role !== "assistant") return null;
|
|
990
|
+
const text = String(m.content ?? m.text ?? "");
|
|
991
|
+
const toolCalls = this._snapshotToolCalls(m.toolCalls);
|
|
992
|
+
if (!text && toolCalls.length === 0) return null;
|
|
993
|
+
const message = { id, role, text, status: "complete" };
|
|
994
|
+
if (toolCalls.length) message.toolCalls = toolCalls;
|
|
995
|
+
return message;
|
|
996
|
+
}
|
|
997
|
+
/** AG-UI wire-shape toolCalls → settled TrailToolCall receipts. */
|
|
998
|
+
_snapshotToolCalls(raw) {
|
|
999
|
+
if (!Array.isArray(raw)) return [];
|
|
1000
|
+
const out = [];
|
|
1001
|
+
for (const entry of raw) {
|
|
1002
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
1003
|
+
const tc = entry;
|
|
1004
|
+
const fn = tc.function ?? {};
|
|
1005
|
+
if (typeof tc.id !== "string" || typeof fn.name !== "string" || !fn.name) continue;
|
|
1006
|
+
let args;
|
|
1007
|
+
if (typeof fn.arguments === "string" && fn.arguments) {
|
|
1008
|
+
try {
|
|
1009
|
+
const parsed = JSON.parse(fn.arguments);
|
|
1010
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1011
|
+
args = parsed;
|
|
1012
|
+
}
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
out.push({ id: tc.id, name: fn.name, status: "done", ...args ? { args } : {} });
|
|
1017
|
+
}
|
|
1018
|
+
return out;
|
|
1019
|
+
}
|
|
1020
|
+
/** Map AG-UI ToolCallStatus to TrailToolCall status. */
|
|
1021
|
+
_mapToolCallStatus(status) {
|
|
1022
|
+
switch (status) {
|
|
1023
|
+
case "args-streaming":
|
|
1024
|
+
return "args-streaming";
|
|
1025
|
+
case "pending":
|
|
1026
|
+
return "pending";
|
|
1027
|
+
case "running":
|
|
1028
|
+
return "running";
|
|
1029
|
+
case "done":
|
|
1030
|
+
return "done";
|
|
1031
|
+
case "error":
|
|
1032
|
+
return "error";
|
|
1033
|
+
default:
|
|
1034
|
+
return "running";
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
_onTransportEvent(event) {
|
|
1038
|
+
switch (event.type) {
|
|
1039
|
+
case "session-ready":
|
|
1040
|
+
return;
|
|
1041
|
+
case "messages-snapshot": {
|
|
1042
|
+
const converted = (event.messages ?? []).map((m) => this._snapshotToTrailMessage(m)).filter((m) => m !== null);
|
|
1043
|
+
if (converted.length) chatSession.hydrate(converted);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
case "typing":
|
|
1047
|
+
if (!event.isTyping) this._stopThinkingCycle();
|
|
1048
|
+
chatSession.setRunActive(event.isTyping);
|
|
1049
|
+
return;
|
|
1050
|
+
case "message-append": {
|
|
1051
|
+
this._stopThinkingCycle();
|
|
1052
|
+
chatSession.clearThinking();
|
|
1053
|
+
this._currentAssistantMessageId = event.message.id;
|
|
1054
|
+
chatSession.receiveStart(event.message.id);
|
|
1055
|
+
if (event.message.content && event.message.content.length > 0) {
|
|
1056
|
+
chatSession.receiveDelta(event.message.id, event.message.content);
|
|
1057
|
+
}
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
case "message-delta": {
|
|
1061
|
+
const id = event.messageId ?? this._currentAssistantMessageId;
|
|
1062
|
+
if (!id || !event.delta) return;
|
|
1063
|
+
chatSession.receiveDelta(id, event.delta);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
case "nudge": {
|
|
1067
|
+
const id = event.messageId ?? this._currentAssistantMessageId;
|
|
1068
|
+
if (id) chatSession.receiveEnd(id);
|
|
1069
|
+
try {
|
|
1070
|
+
document.dispatchEvent(
|
|
1071
|
+
new CustomEvent("syntro:nudge", {
|
|
1072
|
+
detail: { pattern: event.pattern, confidence: event.confidence, messageId: id }
|
|
1073
|
+
})
|
|
1074
|
+
);
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
case "thinking-delta": {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
case "thinking-clear": {
|
|
1083
|
+
this._stopThinkingCycle();
|
|
1084
|
+
chatSession.clearThinking();
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
case "message-complete": {
|
|
1088
|
+
const id = event.messageId ?? this._currentAssistantMessageId;
|
|
1089
|
+
if (!id) return;
|
|
1090
|
+
this._stopThinkingCycle();
|
|
1091
|
+
chatSession.receiveEnd(id);
|
|
1092
|
+
this._currentAssistantMessageId = null;
|
|
1093
|
+
this._messagesSucceeded += 1;
|
|
1094
|
+
this._hasSucceeded = true;
|
|
1095
|
+
this._clearDebounceTimer();
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
case "tool-call": {
|
|
1099
|
+
const existing = this._findToolCallStatus(event.toolCall.id);
|
|
1100
|
+
if (!existing) {
|
|
1101
|
+
this._startThinkingCycle(toolNarrationVariants(event.toolCall.name));
|
|
1102
|
+
}
|
|
1103
|
+
const targetMessageId = event.messageId ?? this._currentAssistantMessageId;
|
|
1104
|
+
if (!targetMessageId) return;
|
|
1105
|
+
const parsedArgs = parseToolCallArgs(event.toolCall);
|
|
1106
|
+
if (!existing) {
|
|
1107
|
+
chatSession.addToolCall(targetMessageId, {
|
|
1108
|
+
id: event.toolCall.id,
|
|
1109
|
+
name: event.toolCall.name,
|
|
1110
|
+
status: this._mapToolCallStatus(event.toolCall.status),
|
|
1111
|
+
...parsedArgs ? { args: parsedArgs } : {}
|
|
1112
|
+
});
|
|
1113
|
+
} else {
|
|
1114
|
+
chatSession.updateToolCall(event.toolCall.id, {
|
|
1115
|
+
name: event.toolCall.name,
|
|
1116
|
+
status: this._mapToolCallStatus(event.toolCall.status),
|
|
1117
|
+
...parsedArgs ? { args: parsedArgs } : {}
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
case "tool-call-args-delta":
|
|
1123
|
+
debug("tool-call-args-delta", event);
|
|
1124
|
+
return;
|
|
1125
|
+
case "tool-call-done": {
|
|
1126
|
+
const doneArgs = parseToolCallArgs(event);
|
|
1127
|
+
chatSession.updateToolCall(event.toolCallId, {
|
|
1128
|
+
status: "done",
|
|
1129
|
+
...doneArgs ? { args: doneArgs } : {}
|
|
1130
|
+
});
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
case "a2ui":
|
|
1134
|
+
return;
|
|
1135
|
+
case "error": {
|
|
1136
|
+
const status = event.status ?? null;
|
|
1137
|
+
const body = event.body ? String(event.body).slice(0, 200) : null;
|
|
1138
|
+
console.warn(
|
|
1139
|
+
`[chat-transport] error status=${status ?? "no-status"} succeeded=${this._messagesSucceeded}`,
|
|
1140
|
+
event
|
|
1141
|
+
);
|
|
1142
|
+
this._handleErrorEvent({
|
|
1143
|
+
message: event.message,
|
|
1144
|
+
status,
|
|
1145
|
+
body,
|
|
1146
|
+
errorName: event.errorName ?? null
|
|
1147
|
+
});
|
|
1148
|
+
const wasMidAnswer = this._currentAssistantMessageId != null;
|
|
1149
|
+
chatSession.error(
|
|
1150
|
+
wasMidAnswer ? "The connection dropped partway through \u2014 ask again and I'll pick it back up." : "Hmm, that didn't go through. Please try again."
|
|
1151
|
+
);
|
|
1152
|
+
this._currentAssistantMessageId = null;
|
|
1153
|
+
if (this._agui) {
|
|
1154
|
+
this._agui.disconnect();
|
|
1155
|
+
this._agui = null;
|
|
1156
|
+
}
|
|
1157
|
+
if (this._transportUnsub) {
|
|
1158
|
+
this._transportUnsub();
|
|
1159
|
+
this._transportUnsub = null;
|
|
1160
|
+
}
|
|
1161
|
+
this._status = "error";
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
var chatTransport = new ChatTransport();
|
|
1168
|
+
|
|
1169
|
+
// src/observer/allowlist.ts
|
|
1170
|
+
var MAX_TEXT_LEN = 200;
|
|
1171
|
+
var SIGNIFICANT_CLICK_TAGS = /* @__PURE__ */ new Set(["button", "a"]);
|
|
1172
|
+
function truncate(s) {
|
|
1173
|
+
return s.length <= MAX_TEXT_LEN ? s : `${s.slice(0, MAX_TEXT_LEN - 1)}\u2026`;
|
|
1174
|
+
}
|
|
1175
|
+
function firstElement(props) {
|
|
1176
|
+
const els = props.$elements;
|
|
1177
|
+
if (Array.isArray(els) && els.length > 0 && typeof els[0] === "object" && els[0] !== null) {
|
|
1178
|
+
return els[0];
|
|
1179
|
+
}
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
function elementText(props) {
|
|
1183
|
+
const el = firstElement(props);
|
|
1184
|
+
if (!el) return "";
|
|
1185
|
+
const t = el.text;
|
|
1186
|
+
return typeof t === "string" ? t.trim() : "";
|
|
1187
|
+
}
|
|
1188
|
+
function elementTag(props) {
|
|
1189
|
+
const explicit = props.$element_tag_name;
|
|
1190
|
+
if (typeof explicit === "string") return explicit.toLowerCase();
|
|
1191
|
+
const el = firstElement(props);
|
|
1192
|
+
if (el && typeof el.tag_name === "string") return el.tag_name.toLowerCase();
|
|
1193
|
+
return "";
|
|
1194
|
+
}
|
|
1195
|
+
function elementRole(props) {
|
|
1196
|
+
const el = firstElement(props);
|
|
1197
|
+
if (!el) return "";
|
|
1198
|
+
const role = el.attr__role;
|
|
1199
|
+
return typeof role === "string" ? role.toLowerCase() : "";
|
|
1200
|
+
}
|
|
1201
|
+
function formName(props) {
|
|
1202
|
+
const el = firstElement(props);
|
|
1203
|
+
if (!el) return "unnamed";
|
|
1204
|
+
const name = el.attr__name;
|
|
1205
|
+
if (typeof name === "string" && name) return name;
|
|
1206
|
+
const id = el.attr__id;
|
|
1207
|
+
if (typeof id === "string" && id) return id;
|
|
1208
|
+
return "unnamed";
|
|
1209
|
+
}
|
|
1210
|
+
function isSignificantClickTarget(props) {
|
|
1211
|
+
const tag = elementTag(props);
|
|
1212
|
+
if (SIGNIFICANT_CLICK_TAGS.has(tag)) return true;
|
|
1213
|
+
if (elementRole(props) === "button") return true;
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
function pathname(props) {
|
|
1217
|
+
const pn = props.$pathname;
|
|
1218
|
+
return typeof pn === "string" ? pn : "/";
|
|
1219
|
+
}
|
|
1220
|
+
function renderCustomEvent(raw) {
|
|
1221
|
+
const fields = [];
|
|
1222
|
+
for (const [k, v] of Object.entries(raw.properties)) {
|
|
1223
|
+
if (k.startsWith("$")) continue;
|
|
1224
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
1225
|
+
fields.push(`${k}=${v}`);
|
|
1226
|
+
if (fields.length >= 2) break;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return fields.length > 0 ? `${raw.event} (${fields.join(", ")})` : raw.event;
|
|
1230
|
+
}
|
|
1231
|
+
function matchEvent(raw, opts = {}) {
|
|
1232
|
+
const props = raw.properties;
|
|
1233
|
+
switch (raw.event) {
|
|
1234
|
+
case "$pageview": {
|
|
1235
|
+
const path = pathname(props);
|
|
1236
|
+
return {
|
|
1237
|
+
ts: raw.timestamp,
|
|
1238
|
+
kind: "nav",
|
|
1239
|
+
text: truncate(`navigated to ${path}`),
|
|
1240
|
+
ref: truncate(path)
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
case "$autocapture": {
|
|
1244
|
+
const eventType = props.$event_type;
|
|
1245
|
+
if (eventType === "click") {
|
|
1246
|
+
if (!isSignificantClickTarget(props)) return null;
|
|
1247
|
+
const text = elementText(props) || elementTag(props);
|
|
1248
|
+
return {
|
|
1249
|
+
ts: raw.timestamp,
|
|
1250
|
+
kind: "click",
|
|
1251
|
+
text: truncate(`clicked '${text}'`)
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
if (eventType === "submit" && elementTag(props) === "form") {
|
|
1255
|
+
return {
|
|
1256
|
+
ts: raw.timestamp,
|
|
1257
|
+
kind: "form",
|
|
1258
|
+
text: truncate(`submitted form '${formName(props)}'`)
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
case "$rageclick": {
|
|
1264
|
+
const text = elementText(props) || elementTag(props);
|
|
1265
|
+
return {
|
|
1266
|
+
ts: raw.timestamp,
|
|
1267
|
+
kind: "rage",
|
|
1268
|
+
text: truncate(`rage-clicked '${text}'`)
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
case "$dead_click": {
|
|
1272
|
+
const text = elementText(props) || elementTag(props);
|
|
1273
|
+
return {
|
|
1274
|
+
ts: raw.timestamp,
|
|
1275
|
+
kind: "dead",
|
|
1276
|
+
text: truncate(`clicked '${text}' (no response)`)
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
// ── Canonical runtime-bus events (sources: `'canvas'` instrumentation
|
|
1280
|
+
// or the event-processor's rrweb detectors). These don't go through
|
|
1281
|
+
// PostHog; the chat-bar's bus translator forwards them as-is with
|
|
1282
|
+
// the canonical event name, and matchEvent picks the right slot
|
|
1283
|
+
// here. Keeps a single allowlist as the source of truth for what
|
|
1284
|
+
// the agent's observation tail can carry.
|
|
1285
|
+
case "nav.section_viewed": {
|
|
1286
|
+
const section = typeof props.section === "string" ? props.section : "";
|
|
1287
|
+
if (!section) return null;
|
|
1288
|
+
return {
|
|
1289
|
+
ts: raw.timestamp,
|
|
1290
|
+
kind: "view",
|
|
1291
|
+
text: truncate(`viewed the '${section}' section`),
|
|
1292
|
+
ref: truncate(section)
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
case "nav.scroll_depth": {
|
|
1296
|
+
const pct = typeof props.percent === "number" ? props.percent : null;
|
|
1297
|
+
if (pct === null) return null;
|
|
1298
|
+
return {
|
|
1299
|
+
ts: raw.timestamp,
|
|
1300
|
+
kind: "scroll",
|
|
1301
|
+
text: truncate(`scrolled to ${pct}% of the page`)
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
case "ui.scroll_thrash": {
|
|
1305
|
+
return {
|
|
1306
|
+
ts: raw.timestamp,
|
|
1307
|
+
kind: "scroll",
|
|
1308
|
+
text: "scrolled up and down repeatedly (looking for something)"
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
case "ui.idle": {
|
|
1312
|
+
const ms = typeof props.durationMs === "number" ? props.durationMs : null;
|
|
1313
|
+
const secs = ms !== null ? Math.round(ms / 1e3) : null;
|
|
1314
|
+
return {
|
|
1315
|
+
ts: raw.timestamp,
|
|
1316
|
+
kind: "idle",
|
|
1317
|
+
text: secs !== null ? `idle for ${secs}s` : "idle"
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
case "ui.hover": {
|
|
1321
|
+
const text = elementText(props) || elementTag(props);
|
|
1322
|
+
return {
|
|
1323
|
+
ts: raw.timestamp,
|
|
1324
|
+
kind: "hover",
|
|
1325
|
+
text: truncate(`hovered on '${text}'`)
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
case "ui.hesitation": {
|
|
1329
|
+
const text = elementText(props) || elementTag(props);
|
|
1330
|
+
return {
|
|
1331
|
+
ts: raw.timestamp,
|
|
1332
|
+
kind: "hesitation",
|
|
1333
|
+
text: truncate(`hesitated near '${text}'`)
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
case "ui.focus_bounce": {
|
|
1337
|
+
const text = elementText(props) || elementTag(props);
|
|
1338
|
+
return {
|
|
1339
|
+
ts: raw.timestamp,
|
|
1340
|
+
kind: "form",
|
|
1341
|
+
text: truncate(`focused '${text}' but didn't fill it`)
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
default: {
|
|
1345
|
+
const allow = opts.observableEvents ?? [];
|
|
1346
|
+
if (allow.includes(raw.event)) {
|
|
1347
|
+
return {
|
|
1348
|
+
ts: raw.timestamp,
|
|
1349
|
+
kind: "custom",
|
|
1350
|
+
text: truncate(renderCustomEvent(raw))
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/observer/queue.ts
|
|
1359
|
+
var ObserverQueue = class {
|
|
1360
|
+
constructor(opts) {
|
|
1361
|
+
this._buffer = [];
|
|
1362
|
+
this._sent = 0;
|
|
1363
|
+
this._dropped = 0;
|
|
1364
|
+
this._lastError = null;
|
|
1365
|
+
this._disabled = false;
|
|
1366
|
+
this._intervalTimer = null;
|
|
1367
|
+
this._backoffTimer = null;
|
|
1368
|
+
this._currentBackoffMs = 0;
|
|
1369
|
+
this._inFlight = false;
|
|
1370
|
+
this._opts = {
|
|
1371
|
+
backoffStartMs: 1e3,
|
|
1372
|
+
backoffCapMs: 3e4,
|
|
1373
|
+
...opts
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
push(event) {
|
|
1377
|
+
if (this._disabled) return;
|
|
1378
|
+
if (this._buffer.length >= this._opts.queueCap) {
|
|
1379
|
+
this._buffer.shift();
|
|
1380
|
+
this._dropped += 1;
|
|
1381
|
+
}
|
|
1382
|
+
this._buffer.push(event);
|
|
1383
|
+
if (this._buffer.length >= this._opts.batchSize) {
|
|
1384
|
+
this._scheduleImmediate();
|
|
1385
|
+
} else {
|
|
1386
|
+
this._scheduleInterval();
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
async flushNow() {
|
|
1390
|
+
this._clearInterval();
|
|
1391
|
+
await this._flush();
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Synchronously hand the newest backend-sized batch to an unload-safe sender.
|
|
1395
|
+
* Returns false when the sender rejected delivery and restores the batch so
|
|
1396
|
+
* the caller can fall back to fetch({ keepalive: true }).
|
|
1397
|
+
*/
|
|
1398
|
+
flushOnPageHide(send) {
|
|
1399
|
+
if (this._disabled || this._buffer.length === 0) return true;
|
|
1400
|
+
this._clearInterval();
|
|
1401
|
+
const overflow = Math.max(0, this._buffer.length - 50);
|
|
1402
|
+
if (overflow > 0) {
|
|
1403
|
+
this._buffer.splice(0, overflow);
|
|
1404
|
+
this._dropped += overflow;
|
|
1405
|
+
}
|
|
1406
|
+
const batch = this._buffer.splice(0, 50);
|
|
1407
|
+
if (!send(batch)) {
|
|
1408
|
+
this._buffer.unshift(...batch);
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
this._sent += batch.length;
|
|
1412
|
+
this._lastError = null;
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1415
|
+
stats() {
|
|
1416
|
+
return {
|
|
1417
|
+
queued: this._buffer.length,
|
|
1418
|
+
sent: this._sent,
|
|
1419
|
+
dropped: this._dropped,
|
|
1420
|
+
lastError: this._lastError,
|
|
1421
|
+
disabled: this._disabled
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
_scheduleImmediate() {
|
|
1425
|
+
queueMicrotask(() => {
|
|
1426
|
+
void this._flush();
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
_scheduleInterval() {
|
|
1430
|
+
if (this._intervalTimer != null) return;
|
|
1431
|
+
this._intervalTimer = setTimeout(() => {
|
|
1432
|
+
this._intervalTimer = null;
|
|
1433
|
+
void this._flush();
|
|
1434
|
+
}, this._opts.flushIntervalMs);
|
|
1435
|
+
}
|
|
1436
|
+
_clearInterval() {
|
|
1437
|
+
if (this._intervalTimer != null) {
|
|
1438
|
+
clearTimeout(this._intervalTimer);
|
|
1439
|
+
this._intervalTimer = null;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async _flush() {
|
|
1443
|
+
if (this._disabled || this._inFlight || this._buffer.length === 0) return;
|
|
1444
|
+
this._clearInterval();
|
|
1445
|
+
const batch = this._buffer.splice(0, this._opts.batchSize);
|
|
1446
|
+
this._inFlight = true;
|
|
1447
|
+
try {
|
|
1448
|
+
await this._opts.flush(batch);
|
|
1449
|
+
this._sent += batch.length;
|
|
1450
|
+
this._currentBackoffMs = 0;
|
|
1451
|
+
this._lastError = null;
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
this._buffer.unshift(...batch);
|
|
1454
|
+
const perm = err?.permanent === true;
|
|
1455
|
+
this._lastError = err instanceof Error ? err.message : String(err?.message ?? err);
|
|
1456
|
+
if (perm) {
|
|
1457
|
+
this._disabled = true;
|
|
1458
|
+
} else {
|
|
1459
|
+
this._scheduleBackoff();
|
|
1460
|
+
}
|
|
1461
|
+
} finally {
|
|
1462
|
+
this._inFlight = false;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
_scheduleBackoff() {
|
|
1466
|
+
const next = this._currentBackoffMs === 0 ? this._opts.backoffStartMs : Math.min(this._currentBackoffMs * 2, this._opts.backoffCapMs);
|
|
1467
|
+
this._currentBackoffMs = next;
|
|
1468
|
+
if (this._backoffTimer != null) clearTimeout(this._backoffTimer);
|
|
1469
|
+
this._backoffTimer = setTimeout(() => {
|
|
1470
|
+
this._backoffTimer = null;
|
|
1471
|
+
void this._flush();
|
|
1472
|
+
}, next);
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
// src/observer/transport.ts
|
|
1477
|
+
function buildBody(distinctId, sessionId, batch) {
|
|
1478
|
+
if (!distinctId || !sessionId) return null;
|
|
1479
|
+
const body = { distinct_id: distinctId, session_id: sessionId, batch };
|
|
1480
|
+
return JSON.stringify(body);
|
|
1481
|
+
}
|
|
1482
|
+
function createTransport(opts) {
|
|
1483
|
+
return {
|
|
1484
|
+
async send(batch) {
|
|
1485
|
+
const body = buildBody(opts.getDistinctId(), opts.getSessionId(), batch);
|
|
1486
|
+
if (body === null) return;
|
|
1487
|
+
const resp = await fetch(opts.url, {
|
|
1488
|
+
method: "POST",
|
|
1489
|
+
headers: {
|
|
1490
|
+
Authorization: `Bearer ${opts.token()}`,
|
|
1491
|
+
"Content-Type": "application/json"
|
|
1492
|
+
},
|
|
1493
|
+
body,
|
|
1494
|
+
keepalive: true,
|
|
1495
|
+
credentials: "include"
|
|
1496
|
+
});
|
|
1497
|
+
if (!resp.ok) {
|
|
1498
|
+
const err = new Error(`observation POST ${resp.status}`);
|
|
1499
|
+
if (resp.status >= 400 && resp.status < 500 && resp.status !== 429) {
|
|
1500
|
+
err.permanent = true;
|
|
1501
|
+
}
|
|
1502
|
+
throw err;
|
|
1503
|
+
}
|
|
1504
|
+
},
|
|
1505
|
+
sendBeacon(batch) {
|
|
1506
|
+
const body = buildBody(opts.getDistinctId(), opts.getSessionId(), batch);
|
|
1507
|
+
if (body === null) return false;
|
|
1508
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
1509
|
+
const beacon = navigator?.sendBeacon;
|
|
1510
|
+
if (typeof beacon !== "function") return false;
|
|
1511
|
+
const separator = opts.url.includes("?") ? "&" : "?";
|
|
1512
|
+
const url = `${opts.url}${separator}syn=${encodeURIComponent(opts.token())}`;
|
|
1513
|
+
return beacon.call(navigator, url, blob);
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/observer/index.ts
|
|
1519
|
+
var _active = null;
|
|
1520
|
+
function _attachPostHog(ingest, _observableEvents) {
|
|
1521
|
+
const ph = globalThis.posthog;
|
|
1522
|
+
if (!ph) return () => {
|
|
1523
|
+
};
|
|
1524
|
+
const addHook = ph._addCaptureHook;
|
|
1525
|
+
if (typeof addHook !== "function") return () => {
|
|
1526
|
+
};
|
|
1527
|
+
const handler = (eventName, properties) => {
|
|
1528
|
+
if (typeof eventName !== "string") return;
|
|
1529
|
+
ingest({
|
|
1530
|
+
event: eventName,
|
|
1531
|
+
timestamp: Date.now(),
|
|
1532
|
+
properties: properties ?? {}
|
|
1533
|
+
});
|
|
1534
|
+
};
|
|
1535
|
+
addHook.call(ph, handler);
|
|
1536
|
+
return () => {
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
function _attachPageHide(transport, queue) {
|
|
1540
|
+
const handler = () => {
|
|
1541
|
+
const stats = queue.stats();
|
|
1542
|
+
if (stats.queued > 0) {
|
|
1543
|
+
queue.flushOnPageHide((batch) => {
|
|
1544
|
+
if (transport.sendBeacon(batch)) return true;
|
|
1545
|
+
void transport.send(batch);
|
|
1546
|
+
return true;
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
window.addEventListener("pagehide", handler, { capture: true });
|
|
1551
|
+
return () => window.removeEventListener("pagehide", handler, { capture: true });
|
|
1552
|
+
}
|
|
1553
|
+
function startObserver(opts) {
|
|
1554
|
+
if (_active) {
|
|
1555
|
+
_active.refCount += 1;
|
|
1556
|
+
return _makeHandle(_active);
|
|
1557
|
+
}
|
|
1558
|
+
const transport = opts.transport ?? createTransport(opts);
|
|
1559
|
+
const queue = new ObserverQueue({
|
|
1560
|
+
batchSize: 10,
|
|
1561
|
+
flushIntervalMs: 2e3,
|
|
1562
|
+
queueCap: 200,
|
|
1563
|
+
flush: (batch) => transport.send(batch)
|
|
1564
|
+
});
|
|
1565
|
+
const ingest = (raw) => {
|
|
1566
|
+
const matched = matchEvent(raw, { observableEvents: opts.observableEvents });
|
|
1567
|
+
if (matched !== null) {
|
|
1568
|
+
queue.push(opts.enrich ? opts.enrich(matched, raw) : matched);
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
const unsubscribePh = _attachPostHog(ingest, opts.observableEvents ?? []);
|
|
1572
|
+
const unsubscribePageHide = _attachPageHide(transport, queue);
|
|
1573
|
+
const active = {
|
|
1574
|
+
refCount: 1,
|
|
1575
|
+
queue,
|
|
1576
|
+
unsubscribe: () => {
|
|
1577
|
+
unsubscribePh();
|
|
1578
|
+
unsubscribePageHide();
|
|
1579
|
+
},
|
|
1580
|
+
options: opts
|
|
1581
|
+
};
|
|
1582
|
+
_active = active;
|
|
1583
|
+
return _makeHandle(active);
|
|
1584
|
+
}
|
|
1585
|
+
function _makeHandle(owner) {
|
|
1586
|
+
return {
|
|
1587
|
+
stop() {
|
|
1588
|
+
if (_active !== owner) return;
|
|
1589
|
+
owner.refCount -= 1;
|
|
1590
|
+
if (owner.refCount <= 0) {
|
|
1591
|
+
owner.unsubscribe();
|
|
1592
|
+
_active = null;
|
|
1593
|
+
}
|
|
1594
|
+
},
|
|
1595
|
+
stats() {
|
|
1596
|
+
return (_active === owner ? owner.queue.stats() : null) ?? {
|
|
1597
|
+
queued: 0,
|
|
1598
|
+
sent: 0,
|
|
1599
|
+
dropped: 0,
|
|
1600
|
+
lastError: null,
|
|
1601
|
+
disabled: true
|
|
1602
|
+
};
|
|
1603
|
+
},
|
|
1604
|
+
ingest(raw) {
|
|
1605
|
+
if (_active !== owner) return;
|
|
1606
|
+
const matched = matchEvent(raw, {
|
|
1607
|
+
observableEvents: owner.options.observableEvents
|
|
1608
|
+
});
|
|
1609
|
+
if (matched !== null) {
|
|
1610
|
+
owner.queue.push(owner.options.enrich ? owner.options.enrich(matched, raw) : matched);
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
flushNow() {
|
|
1614
|
+
return _active === owner ? owner.queue.flushNow() : Promise.resolve();
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// src/observer/bus-translate.ts
|
|
1620
|
+
function eventTypeFromName(name) {
|
|
1621
|
+
switch (name) {
|
|
1622
|
+
case "ui.click":
|
|
1623
|
+
return "click";
|
|
1624
|
+
case "ui.submit":
|
|
1625
|
+
return "submit";
|
|
1626
|
+
case "ui.change":
|
|
1627
|
+
return "change";
|
|
1628
|
+
default:
|
|
1629
|
+
return void 0;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function busEventToRawEvent(e) {
|
|
1633
|
+
if (!e?.name) return null;
|
|
1634
|
+
const ts = typeof e.ts === "number" ? e.ts : Date.now();
|
|
1635
|
+
if (e.source === "posthog") {
|
|
1636
|
+
const original = e.props?.originalEvent;
|
|
1637
|
+
if (typeof original !== "string" || original.length === 0) return null;
|
|
1638
|
+
const src = e.props ?? {};
|
|
1639
|
+
const properties = { ...src };
|
|
1640
|
+
if (Array.isArray(src.elements)) properties.$elements = src.elements;
|
|
1641
|
+
if (typeof src.tagName === "string") properties.$element_tag_name = src.tagName;
|
|
1642
|
+
if (typeof src.pathname === "string") properties.$pathname = src.pathname;
|
|
1643
|
+
if (typeof src.url === "string") properties.$current_url = src.url;
|
|
1644
|
+
const eventType = eventTypeFromName(e.name);
|
|
1645
|
+
if (eventType) properties.$event_type = eventType;
|
|
1646
|
+
return { event: original, timestamp: ts, properties };
|
|
1647
|
+
}
|
|
1648
|
+
return { event: e.name, timestamp: ts, properties: { ...e.props ?? {} } };
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// src/AdaptiveChatBarMountable.ts
|
|
1652
|
+
var STATE_KEY = "__syntroChatBarMount";
|
|
1653
|
+
function getState(container) {
|
|
1654
|
+
return container[STATE_KEY] ?? null;
|
|
1655
|
+
}
|
|
1656
|
+
function setState(container, state) {
|
|
1657
|
+
container[STATE_KEY] = state;
|
|
1658
|
+
}
|
|
1659
|
+
function applyPlaceholder(bar, cfg) {
|
|
1660
|
+
if (cfg.placeholder !== void 0) bar.placeholder = cfg.placeholder;
|
|
1661
|
+
if (cfg.assistantName !== void 0) bar.assistantName = cfg.assistantName;
|
|
1662
|
+
if (cfg.greeting !== void 0) bar.greeting = cfg.greeting;
|
|
1663
|
+
bar.introSuggestion = cfg.introSuggestion;
|
|
1664
|
+
bar.forceExpanded = cfg.forceExpanded === true;
|
|
1665
|
+
if (typeof cfg.maximized === "boolean") {
|
|
1666
|
+
bar.maximized = cfg.maximized;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
var _elementStore = null;
|
|
1670
|
+
var _templateWidgetMap = /* @__PURE__ */ new Map();
|
|
1671
|
+
var SERVER_MOUNTED_TILE_WIDGETS = ["syntro-product-reco-carousel"];
|
|
1672
|
+
function resolveTileWidget(templateId) {
|
|
1673
|
+
return _templateWidgetMap.get(templateId);
|
|
1674
|
+
}
|
|
1675
|
+
function refreshTemplateWidgetMap(uiTemplates) {
|
|
1676
|
+
_templateWidgetMap.clear();
|
|
1677
|
+
for (const tag of SERVER_MOUNTED_TILE_WIDGETS) {
|
|
1678
|
+
_templateWidgetMap.set(tag, tag);
|
|
1679
|
+
}
|
|
1680
|
+
if (!uiTemplates) return;
|
|
1681
|
+
const tiles = uiTemplates.tiles;
|
|
1682
|
+
if (!Array.isArray(tiles)) return;
|
|
1683
|
+
for (const t of tiles) {
|
|
1684
|
+
const id = typeof t?.id === "string" ? t.id : null;
|
|
1685
|
+
const widget = typeof t?.widget === "string" ? t.widget : null;
|
|
1686
|
+
if (id && widget) _templateWidgetMap.set(id, widget);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function getOrCreateElementStore(runtime2) {
|
|
1690
|
+
if (_elementStore) return _elementStore;
|
|
1691
|
+
_elementStore = new ElementInstanceStore({
|
|
1692
|
+
actions: runtime2.actions,
|
|
1693
|
+
// Pass the runtime's event bus so ItemHandler can broadcast
|
|
1694
|
+
// `element.compositional_append` / `_patch` / `_remove` events
|
|
1695
|
+
// to container widgets (chips strip, FAQ accordion, nav tips).
|
|
1696
|
+
events: {
|
|
1697
|
+
publish: runtime2.events.publish.bind(runtime2.events),
|
|
1698
|
+
subscribe: runtime2.events.subscribe?.bind(runtime2.events)
|
|
1699
|
+
},
|
|
1700
|
+
handlers: [new TileHandler(resolveTileWidget), new ActionHandler(), new ItemHandler()],
|
|
1701
|
+
resolveTileWidget
|
|
1702
|
+
});
|
|
1703
|
+
setElementInstanceStore(_elementStore);
|
|
1704
|
+
return _elementStore;
|
|
1705
|
+
}
|
|
1706
|
+
var _hydrationStarted = false;
|
|
1707
|
+
function hydrateOnce(runtime2, backendUrl) {
|
|
1708
|
+
if (_hydrationStarted) return;
|
|
1709
|
+
_hydrationStarted = true;
|
|
1710
|
+
const store = getOrCreateElementStore(runtime2);
|
|
1711
|
+
const trimmed = backendUrl.replace(/\/$/, "");
|
|
1712
|
+
const endpoint = trimmed ? `${trimmed}/api/adaptive/mounted_elements` : "/api/adaptive/mounted_elements";
|
|
1713
|
+
fetchMountedElements({ endpoint }).then((response) => {
|
|
1714
|
+
if (!response) return;
|
|
1715
|
+
void store.hydrate(response.mounted_elements);
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
var PAGE_LEVEL_KINDS = /* @__PURE__ */ new Set(["view", "scroll", "idle", "hesitation", "hover", "nav"]);
|
|
1719
|
+
function buildObserverEnrich(runtimeRef) {
|
|
1720
|
+
return (ev, raw) => {
|
|
1721
|
+
const extract = runtimeRef.getProductIdentity;
|
|
1722
|
+
if (typeof extract !== "function") return ev;
|
|
1723
|
+
if (ev.kind === "click") {
|
|
1724
|
+
const chain = raw.properties.$elements;
|
|
1725
|
+
if (!Array.isArray(chain)) return ev;
|
|
1726
|
+
const id = extract(chain);
|
|
1727
|
+
if (!id) return ev;
|
|
1728
|
+
return {
|
|
1729
|
+
...ev,
|
|
1730
|
+
product: {
|
|
1731
|
+
handle: id.productHandle,
|
|
1732
|
+
url: id.productUrl,
|
|
1733
|
+
...id.productTitle ? { title: id.productTitle } : {}
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
if (!ev.product && PAGE_LEVEL_KINDS.has(ev.kind) && typeof location !== "undefined") {
|
|
1738
|
+
const id = extract([{ href: location.pathname }]);
|
|
1739
|
+
if (!id) return ev;
|
|
1740
|
+
return {
|
|
1741
|
+
...ev,
|
|
1742
|
+
product: {
|
|
1743
|
+
handle: id.productHandle,
|
|
1744
|
+
url: id.productUrl,
|
|
1745
|
+
...id.productTitle ? { title: id.productTitle } : {}
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
return ev;
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
var _nudgeSurfaceWired = false;
|
|
1753
|
+
var _nudgeListener = null;
|
|
1754
|
+
function wireNudgeSurface(runtime2, backendUrl) {
|
|
1755
|
+
if (_nudgeSurfaceWired || typeof document === "undefined") return;
|
|
1756
|
+
_nudgeSurfaceWired = true;
|
|
1757
|
+
_nudgeListener = () => {
|
|
1758
|
+
try {
|
|
1759
|
+
const store = getOrCreateElementStore(runtime2);
|
|
1760
|
+
const trimmed = backendUrl.replace(/\/$/, "");
|
|
1761
|
+
const endpoint = trimmed ? `${trimmed}/api/adaptive/mounted_elements` : "/api/adaptive/mounted_elements";
|
|
1762
|
+
void fetchMountedElements({ endpoint }).then((response) => {
|
|
1763
|
+
if (!response) return;
|
|
1764
|
+
void store.hydrate(response.mounted_elements);
|
|
1765
|
+
});
|
|
1766
|
+
const findCanvas = (root) => {
|
|
1767
|
+
const direct = root.querySelector("smart-canvas");
|
|
1768
|
+
if (direct) return direct;
|
|
1769
|
+
for (const el of root.querySelectorAll("*")) {
|
|
1770
|
+
const sr = el.shadowRoot;
|
|
1771
|
+
if (sr) {
|
|
1772
|
+
const found = findCanvas(sr);
|
|
1773
|
+
if (found) return found;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return null;
|
|
1777
|
+
};
|
|
1778
|
+
const canvas = findCanvas(document);
|
|
1779
|
+
canvas?.previewNotification?.({
|
|
1780
|
+
title: "I put something together for you",
|
|
1781
|
+
body: "Open the chat to take a look"
|
|
1782
|
+
});
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
console.warn("[adaptive-chatbot] nudge surface reaction failed", err);
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
document.addEventListener("syntro:nudge", _nudgeListener);
|
|
1788
|
+
}
|
|
1789
|
+
function configureTransportIfPossible(cfg) {
|
|
1790
|
+
if (!cfg.runtime) return;
|
|
1791
|
+
const backendUrl = cfg.runtime.backendUrl;
|
|
1792
|
+
if (!backendUrl) return;
|
|
1793
|
+
const elementsActive = cfg.elementsEnabled === true && cfg.uiTemplates != null;
|
|
1794
|
+
refreshTemplateWidgetMap(elementsActive ? cfg.uiTemplates : void 0);
|
|
1795
|
+
const forwardedProps = elementsActive ? { elementsEnabled: true, uiTemplates: cfg.uiTemplates } : void 0;
|
|
1796
|
+
const runtime2 = cfg.runtime;
|
|
1797
|
+
const onElementMutation = elementsActive ? (mutations) => {
|
|
1798
|
+
void getOrCreateElementStore(runtime2).apply(mutations);
|
|
1799
|
+
} : void 0;
|
|
1800
|
+
if (elementsActive) {
|
|
1801
|
+
hydrateOnce(runtime2, backendUrl);
|
|
1802
|
+
}
|
|
1803
|
+
chatTransport.configure({
|
|
1804
|
+
...cfg,
|
|
1805
|
+
backendUrl,
|
|
1806
|
+
forwardedProps,
|
|
1807
|
+
onElementMutation,
|
|
1808
|
+
activeLidSlot: cfg._syntroSlotName
|
|
1809
|
+
});
|
|
1810
|
+
void chatTransport.startSessionStream();
|
|
1811
|
+
wireNudgeSurface(runtime2, backendUrl);
|
|
1812
|
+
startObserverIfPossible(cfg);
|
|
1813
|
+
}
|
|
1814
|
+
var _diveDeeperWired = false;
|
|
1815
|
+
function wireDiveDeeperListener() {
|
|
1816
|
+
if (_diveDeeperWired || typeof window === "undefined") return;
|
|
1817
|
+
_diveDeeperWired = true;
|
|
1818
|
+
window.addEventListener(DIVE_DEEPER_EVENT, (e) => {
|
|
1819
|
+
const detail = e.detail ?? {};
|
|
1820
|
+
const prompt = typeof detail.prompt === "string" && detail.prompt.trim() ? detail.prompt : detail.title ? `Tell me more about: ${detail.title}` : "";
|
|
1821
|
+
if (!prompt) return;
|
|
1822
|
+
chatSession.send(prompt);
|
|
1823
|
+
if (!chatSession.hasTransport()) {
|
|
1824
|
+
chatSession.error("Chat backend not configured \u2014 set backendUrl in the canvas config.");
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
var _observerStarted = false;
|
|
1829
|
+
var _observerRuntime = null;
|
|
1830
|
+
var _observerHandle = null;
|
|
1831
|
+
var _observerBusUnsubscribe = null;
|
|
1832
|
+
function startObserverIfPossible(cfg) {
|
|
1833
|
+
if (typeof window === "undefined") return;
|
|
1834
|
+
const runtimeRef = cfg.runtime;
|
|
1835
|
+
if (!runtimeRef?.events?.subscribe) return;
|
|
1836
|
+
if (_observerStarted && _observerRuntime === runtimeRef) return;
|
|
1837
|
+
if (_observerStarted) {
|
|
1838
|
+
_observerBusUnsubscribe?.();
|
|
1839
|
+
_observerHandle?.stop();
|
|
1840
|
+
_observerBusUnsubscribe = null;
|
|
1841
|
+
_observerHandle = null;
|
|
1842
|
+
_observerStarted = false;
|
|
1843
|
+
}
|
|
1844
|
+
const trimmed = (runtimeRef.backendUrl ?? "").replace(/\/$/, "");
|
|
1845
|
+
const url = trimmed ? `${trimmed}/api/adaptive/observation` : "/api/adaptive/observation";
|
|
1846
|
+
const runtimeId = (method) => {
|
|
1847
|
+
try {
|
|
1848
|
+
const v = runtimeRef.telemetry?.[method]?.();
|
|
1849
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
1850
|
+
} catch {
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
const getDistinctId = () => runtimeId("getDistinctId");
|
|
1855
|
+
const getSessionId = () => runtimeId("getSessionId");
|
|
1856
|
+
const token = () => {
|
|
1857
|
+
const c = window.__SYNTRO_CONFIG__;
|
|
1858
|
+
return typeof c?.token === "string" ? c.token : "";
|
|
1859
|
+
};
|
|
1860
|
+
const enrich = buildObserverEnrich(runtimeRef);
|
|
1861
|
+
const handle = startObserver({ url, token, getDistinctId, getSessionId, enrich });
|
|
1862
|
+
window.__syntroObserverStats = () => handle.stats();
|
|
1863
|
+
const unsubscribe = runtimeRef.events.subscribe({}, (evt) => {
|
|
1864
|
+
const raw = busEventToRawEvent(evt);
|
|
1865
|
+
if (raw) handle.ingest(raw);
|
|
1866
|
+
});
|
|
1867
|
+
_observerRuntime = runtimeRef;
|
|
1868
|
+
_observerHandle = handle;
|
|
1869
|
+
_observerBusUnsubscribe = unsubscribe;
|
|
1870
|
+
_observerStarted = true;
|
|
1871
|
+
}
|
|
1872
|
+
function wireListeners(bar, state) {
|
|
1873
|
+
const onMessageSent = (e) => {
|
|
1874
|
+
const text = e.detail.text;
|
|
1875
|
+
chatSession.send(text, { activeLidSlot: state.cfg._syntroSlotName });
|
|
1876
|
+
if (!chatSession.hasTransport()) {
|
|
1877
|
+
chatSession.error("Chat backend not configured \u2014 set backendUrl in the canvas config.");
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
const onInterrupt = () => {
|
|
1881
|
+
chatSession.interrupt();
|
|
1882
|
+
};
|
|
1883
|
+
const onToolCallApproved = (e) => {
|
|
1884
|
+
const detail = e.detail;
|
|
1885
|
+
chatSession.resolveToolCall(detail.toolCallId, {}, detail.approved);
|
|
1886
|
+
};
|
|
1887
|
+
const onClose = () => {
|
|
1888
|
+
state.cfg.onClose?.();
|
|
1889
|
+
};
|
|
1890
|
+
bar.addEventListener("chat-message-sent", onMessageSent);
|
|
1891
|
+
bar.addEventListener("chat-interrupt", onInterrupt);
|
|
1892
|
+
bar.addEventListener("canvas-close", onClose);
|
|
1893
|
+
bar.addEventListener("trail-toolcall-approved", onToolCallApproved);
|
|
1894
|
+
return () => {
|
|
1895
|
+
bar.removeEventListener("chat-message-sent", onMessageSent);
|
|
1896
|
+
bar.removeEventListener("chat-interrupt", onInterrupt);
|
|
1897
|
+
bar.removeEventListener("canvas-close", onClose);
|
|
1898
|
+
bar.removeEventListener("trail-toolcall-approved", onToolCallApproved);
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
var AdaptiveChatBarMountable = {
|
|
1902
|
+
mount(container, mountConfig) {
|
|
1903
|
+
const cfg = mountConfig ?? {};
|
|
1904
|
+
configureTransportIfPossible(cfg);
|
|
1905
|
+
wireDiveDeeperListener();
|
|
1906
|
+
const bar = document.createElement("adaptive-chat-bar");
|
|
1907
|
+
applyPlaceholder(bar, cfg);
|
|
1908
|
+
const widgets = cfg.runtime?.widgets;
|
|
1909
|
+
if (widgets) {
|
|
1910
|
+
bar.mountInlineWidget = (widgetId, container2, props) => {
|
|
1911
|
+
if (!widgets.has(widgetId)) return null;
|
|
1912
|
+
const handle = widgets.mount(widgetId, container2, props);
|
|
1913
|
+
const element = container2.firstElementChild;
|
|
1914
|
+
if (!element) {
|
|
1915
|
+
handle.unmount?.();
|
|
1916
|
+
return null;
|
|
1917
|
+
}
|
|
1918
|
+
return { element, cleanup: () => handle.unmount?.() };
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
const unsubSession = chatSession.subscribe((s) => {
|
|
1922
|
+
bar.messages = [...s.messages];
|
|
1923
|
+
bar.inFlight = s.inFlight;
|
|
1924
|
+
bar.thinkingText = s.thinkingText;
|
|
1925
|
+
});
|
|
1926
|
+
const runtimeEvents = cfg.runtime ? cfg.runtime.events : void 0;
|
|
1927
|
+
const refreshReceipts = () => {
|
|
1928
|
+
const store = _elementStore;
|
|
1929
|
+
if (store) {
|
|
1930
|
+
bar.tileReceipts = store.listTileReceipts();
|
|
1931
|
+
bar.inlineWidgets = store.listInlineWidgets();
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
let unsubReceipts;
|
|
1935
|
+
if (runtimeEvents?.subscribe) {
|
|
1936
|
+
unsubReceipts = runtimeEvents.subscribe(
|
|
1937
|
+
{ names: [TILE_MOUNTED_EVENT, TILE_UNMOUNTED_EVENT] },
|
|
1938
|
+
refreshReceipts
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
refreshReceipts();
|
|
1942
|
+
const state = { bar, cleanup: () => {
|
|
1943
|
+
}, cfg };
|
|
1944
|
+
const unwireListeners = wireListeners(bar, state);
|
|
1945
|
+
container.appendChild(bar);
|
|
1946
|
+
const unsubFallback = chatTransport.onFallback(() => {
|
|
1947
|
+
bar.remove();
|
|
1948
|
+
container.innerHTML = renderFallbackHtml(state.cfg.fallback);
|
|
1949
|
+
});
|
|
1950
|
+
state.cleanup = () => {
|
|
1951
|
+
unsubSession();
|
|
1952
|
+
unsubFallback();
|
|
1953
|
+
unsubReceipts?.();
|
|
1954
|
+
unwireListeners();
|
|
1955
|
+
bar.remove();
|
|
1956
|
+
setState(container, null);
|
|
1957
|
+
};
|
|
1958
|
+
setState(container, state);
|
|
1959
|
+
return state.cleanup;
|
|
1960
|
+
},
|
|
1961
|
+
update(container, mountConfig) {
|
|
1962
|
+
const state = getState(container);
|
|
1963
|
+
if (!state) return;
|
|
1964
|
+
const cfg = mountConfig ?? {};
|
|
1965
|
+
configureTransportIfPossible(cfg);
|
|
1966
|
+
applyPlaceholder(state.bar, cfg);
|
|
1967
|
+
state.cfg = cfg;
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
// src/AdaptiveChipsStripMountable.ts
|
|
1972
|
+
var STATE_KEY2 = "__syntroChipsStripMount";
|
|
1973
|
+
function getState2(container) {
|
|
1974
|
+
return container[STATE_KEY2] ?? null;
|
|
1975
|
+
}
|
|
1976
|
+
function setState2(container, state) {
|
|
1977
|
+
container[STATE_KEY2] = state;
|
|
1978
|
+
}
|
|
1979
|
+
function applyProps(strip, cfg) {
|
|
1980
|
+
if (cfg.chips !== void 0) strip.chips = cfg.chips;
|
|
1981
|
+
if (cfg.runtime !== void 0) {
|
|
1982
|
+
strip.runtimeRef = cfg.runtime;
|
|
1983
|
+
}
|
|
1984
|
+
if (cfg.chromeless !== void 0) strip.chromeless = cfg.chromeless;
|
|
1985
|
+
}
|
|
1986
|
+
var AdaptiveChipsStripMountable = {
|
|
1987
|
+
mount(container, mountConfig) {
|
|
1988
|
+
const cfg = mountConfig ?? {};
|
|
1989
|
+
const strip = document.createElement("adaptive-chips-strip");
|
|
1990
|
+
applyProps(strip, cfg);
|
|
1991
|
+
const state = {
|
|
1992
|
+
strip,
|
|
1993
|
+
listeners: {
|
|
1994
|
+
revealed: (e) => {
|
|
1995
|
+
const cb = cfg.onChipRevealed;
|
|
1996
|
+
if (cb) {
|
|
1997
|
+
cb(
|
|
1998
|
+
e.detail
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
28
2001
|
},
|
|
29
|
-
|
|
2002
|
+
dismissed: (e) => {
|
|
2003
|
+
const cb = cfg.onChipDismissed;
|
|
2004
|
+
if (cb) cb(e.detail);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
strip.addEventListener("chip-revealed", state.listeners.revealed);
|
|
2009
|
+
strip.addEventListener("chip-dismissed", state.listeners.dismissed);
|
|
2010
|
+
container.appendChild(strip);
|
|
2011
|
+
setState2(container, state);
|
|
2012
|
+
return () => {
|
|
2013
|
+
strip.removeEventListener("chip-revealed", state.listeners.revealed);
|
|
2014
|
+
strip.removeEventListener("chip-dismissed", state.listeners.dismissed);
|
|
2015
|
+
strip.remove();
|
|
2016
|
+
setState2(container, null);
|
|
2017
|
+
};
|
|
2018
|
+
},
|
|
2019
|
+
update(container, mountConfig) {
|
|
2020
|
+
const state = getState2(container);
|
|
2021
|
+
if (!state) return;
|
|
2022
|
+
const cfg = mountConfig ?? {};
|
|
2023
|
+
applyProps(state.strip, cfg);
|
|
2024
|
+
state.strip.removeEventListener("chip-revealed", state.listeners.revealed);
|
|
2025
|
+
state.strip.removeEventListener("chip-dismissed", state.listeners.dismissed);
|
|
2026
|
+
state.listeners.revealed = (e) => {
|
|
2027
|
+
const cb = cfg.onChipRevealed;
|
|
2028
|
+
if (cb) {
|
|
2029
|
+
cb(e.detail);
|
|
2030
|
+
}
|
|
2031
|
+
};
|
|
2032
|
+
state.listeners.dismissed = (e) => {
|
|
2033
|
+
const cb = cfg.onChipDismissed;
|
|
2034
|
+
if (cb) cb(e.detail);
|
|
2035
|
+
};
|
|
2036
|
+
state.strip.addEventListener("chip-revealed", state.listeners.revealed);
|
|
2037
|
+
state.strip.addEventListener("chip-dismissed", state.listeners.dismissed);
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
|
|
2041
|
+
// src/NavLinkMountable.ts
|
|
2042
|
+
var STATE_KEY3 = "__syntroNavLinkMount";
|
|
2043
|
+
var SAFE_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
2044
|
+
function getState3(container) {
|
|
2045
|
+
return container[STATE_KEY3] ?? null;
|
|
2046
|
+
}
|
|
2047
|
+
function setState3(container, state) {
|
|
2048
|
+
container[STATE_KEY3] = state;
|
|
2049
|
+
}
|
|
2050
|
+
function navigateTo(rawUrl) {
|
|
2051
|
+
if (!rawUrl) return;
|
|
2052
|
+
let resolved;
|
|
2053
|
+
try {
|
|
2054
|
+
resolved = new URL(rawUrl, window.location.origin);
|
|
2055
|
+
} catch {
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (resolved.origin !== window.location.origin) return;
|
|
2059
|
+
if (!SAFE_NAVIGATION_PROTOCOLS.has(resolved.protocol)) return;
|
|
2060
|
+
window.history.pushState(null, "", resolved.toString());
|
|
2061
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
2062
|
+
}
|
|
2063
|
+
function buildButton(cfg) {
|
|
2064
|
+
const button = document.createElement("button");
|
|
2065
|
+
button.type = "button";
|
|
2066
|
+
button.dataset.syntroNavLink = "";
|
|
2067
|
+
button.style.display = "inline-flex";
|
|
2068
|
+
button.style.alignItems = "center";
|
|
2069
|
+
button.style.gap = "6px";
|
|
2070
|
+
button.style.padding = "6px 12px";
|
|
2071
|
+
button.style.borderRadius = "8px";
|
|
2072
|
+
button.style.fontFamily = "var(--sc-font-family, 'system-ui')";
|
|
2073
|
+
button.style.fontSize = "12px";
|
|
2074
|
+
button.style.fontWeight = "600";
|
|
2075
|
+
button.style.cursor = "pointer";
|
|
2076
|
+
button.style.color = "var(--sc-accent-foreground, currentColor)";
|
|
2077
|
+
button.style.background = "hsl(var(--sc-accent-color) / 0.85)";
|
|
2078
|
+
button.style.border = "1px solid hsl(var(--sc-accent-color) / 0.30)";
|
|
2079
|
+
applyLabel(button, cfg);
|
|
2080
|
+
return button;
|
|
2081
|
+
}
|
|
2082
|
+
function applyLabel(button, cfg) {
|
|
2083
|
+
const url = String(cfg.url ?? "");
|
|
2084
|
+
const label = cfg.label ?? (url ? `Go to ${url}` : "Go");
|
|
2085
|
+
button.textContent = label;
|
|
2086
|
+
}
|
|
2087
|
+
var NavLinkMountable = {
|
|
2088
|
+
mount(container, mountConfig) {
|
|
2089
|
+
const cfg = mountConfig ?? {};
|
|
2090
|
+
const button = buildButton(cfg);
|
|
2091
|
+
const onClick = (_e) => {
|
|
2092
|
+
const current = getState3(container);
|
|
2093
|
+
const url = current?.cfg.url ?? cfg.url;
|
|
2094
|
+
navigateTo(String(url ?? ""));
|
|
2095
|
+
};
|
|
2096
|
+
button.addEventListener("click", onClick);
|
|
2097
|
+
container.appendChild(button);
|
|
2098
|
+
setState3(container, { button, cfg, onClick });
|
|
2099
|
+
return () => {
|
|
2100
|
+
button.removeEventListener("click", onClick);
|
|
2101
|
+
button.remove();
|
|
2102
|
+
setState3(container, null);
|
|
2103
|
+
};
|
|
2104
|
+
},
|
|
2105
|
+
update(container, mountConfig) {
|
|
2106
|
+
const state = getState3(container);
|
|
2107
|
+
if (!state) return;
|
|
2108
|
+
const cfg = mountConfig ?? {};
|
|
2109
|
+
applyLabel(state.button, cfg);
|
|
2110
|
+
state.cfg = cfg;
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
|
|
2114
|
+
// src/TextAnswerMountable.ts
|
|
2115
|
+
var STATE_KEY4 = "__syntroTextAnswerMount";
|
|
2116
|
+
function getState4(container) {
|
|
2117
|
+
return container[STATE_KEY4] ?? null;
|
|
2118
|
+
}
|
|
2119
|
+
function setState4(container, state) {
|
|
2120
|
+
container[STATE_KEY4] = state;
|
|
2121
|
+
}
|
|
2122
|
+
function applyText(p, text) {
|
|
2123
|
+
p.textContent = text;
|
|
2124
|
+
}
|
|
2125
|
+
var TextAnswerMountable = {
|
|
2126
|
+
mount(container, mountConfig) {
|
|
2127
|
+
const cfg = mountConfig ?? {};
|
|
2128
|
+
const paragraph = document.createElement("p");
|
|
2129
|
+
paragraph.dataset.syntroTextAnswer = "";
|
|
2130
|
+
paragraph.style.margin = "0";
|
|
2131
|
+
paragraph.style.fontSize = "12px";
|
|
2132
|
+
paragraph.style.lineHeight = "1.55";
|
|
2133
|
+
paragraph.style.color = "var(--sc-tile-text-color, currentColor)";
|
|
2134
|
+
applyText(paragraph, String(cfg.text ?? ""));
|
|
2135
|
+
container.appendChild(paragraph);
|
|
2136
|
+
setState4(container, { paragraph, cfg });
|
|
2137
|
+
return () => {
|
|
2138
|
+
paragraph.remove();
|
|
2139
|
+
setState4(container, null);
|
|
2140
|
+
};
|
|
2141
|
+
},
|
|
2142
|
+
update(container, mountConfig) {
|
|
2143
|
+
const state = getState4(container);
|
|
2144
|
+
if (!state) return;
|
|
2145
|
+
const cfg = mountConfig ?? {};
|
|
2146
|
+
applyText(state.paragraph, String(cfg.text ?? ""));
|
|
2147
|
+
state.cfg = cfg;
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
// src/runtime.ts
|
|
2152
|
+
var runtime = {
|
|
2153
|
+
id: "adaptive-chatbot",
|
|
2154
|
+
version: "2.0.0",
|
|
2155
|
+
name: "Chat Assistant",
|
|
2156
|
+
description: "AI chat assistant powered by @syntrologie/chat with SSE transport",
|
|
2157
|
+
executors: [],
|
|
2158
|
+
widgets: [
|
|
2159
|
+
{
|
|
2160
|
+
id: "adaptive-chatbot:assistant",
|
|
2161
|
+
component: ChatAssistantLitMountable,
|
|
2162
|
+
metadata: {
|
|
2163
|
+
name: "Chat Assistant",
|
|
2164
|
+
description: "AI-powered chat assistant with SSE streaming and A2UI support",
|
|
2165
|
+
icon: "\u{1F4AC}"
|
|
2166
|
+
}
|
|
2167
|
+
},
|
|
2168
|
+
// PRD §3 chat lid: the standalone chat bar UI shell. Used as a slot
|
|
2169
|
+
// `lid` widget (where it replaces the launcher) or as a regular
|
|
2170
|
+
// tile widget (renders inside the drawer). Headless of any chat
|
|
2171
|
+
// transport — emits chat-message-sent / chat-interrupt / canvas-close
|
|
2172
|
+
// for the parent to wire to whatever pipeline it owns.
|
|
2173
|
+
{
|
|
2174
|
+
id: "adaptive-chatbot:chat-bar",
|
|
2175
|
+
component: AdaptiveChatBarMountable,
|
|
2176
|
+
metadata: {
|
|
2177
|
+
name: "Chat Bar",
|
|
2178
|
+
description: "Always-visible chat input row with bubble-up trail. Headless \u2014 wire to your own transport.",
|
|
2179
|
+
icon: "\u2726"
|
|
2180
|
+
}
|
|
2181
|
+
},
|
|
2182
|
+
// PRD §4.5 suggested-chips tile: horizontally-wrapping chip strip
|
|
2183
|
+
// with click-to-reveal payload drawer. Each chip is a
|
|
2184
|
+
// `suggestions:chip` action.
|
|
2185
|
+
{
|
|
2186
|
+
id: "adaptive-chatbot:chips-strip",
|
|
2187
|
+
component: AdaptiveChipsStripMountable,
|
|
2188
|
+
metadata: {
|
|
2189
|
+
name: "Suggested Chips",
|
|
2190
|
+
description: "Wrapping chip strip with click-to-reveal payloads.",
|
|
2191
|
+
icon: "\u2728"
|
|
2192
|
+
}
|
|
2193
|
+
},
|
|
2194
|
+
// Built-in chip payload — a single paragraph of body text. Chip
|
|
2195
|
+
// payloads are widget references (same {widget,props} shape as
|
|
2196
|
+
// tile/lid configs); this is the simplest one shipped out of the
|
|
2197
|
+
// box so the chip pattern works without requiring a downstream
|
|
2198
|
+
// adaptive for a "just answer with text" case.
|
|
2199
|
+
{
|
|
2200
|
+
id: "adaptive-chatbot:text-answer",
|
|
2201
|
+
component: TextAnswerMountable,
|
|
2202
|
+
metadata: {
|
|
2203
|
+
name: "Text Answer",
|
|
2204
|
+
description: "Single paragraph payload \u2014 the default chip-drawer content.",
|
|
2205
|
+
icon: "\xB6"
|
|
2206
|
+
}
|
|
2207
|
+
},
|
|
2208
|
+
// Chip payload for LLM-authored navigation suggestions. Backed by
|
|
2209
|
+
// the `suggest-navigation` ItemTemplate: the backend wraps a curated
|
|
2210
|
+
// URL into props.url, the chip's payload-drawer mounts this widget,
|
|
2211
|
+
// user clicks "Go to <url>" → same-origin SPA navigation. Kept here
|
|
2212
|
+
// (not in adaptive-nav) so chip payloads ship with the chatbot
|
|
2213
|
+
// bundle that already provides chips-strip + text-answer.
|
|
2214
|
+
{
|
|
2215
|
+
id: "adaptive-chatbot:nav-link",
|
|
2216
|
+
component: NavLinkMountable,
|
|
2217
|
+
metadata: {
|
|
2218
|
+
name: "Nav Link",
|
|
2219
|
+
description: "Same-origin navigation button \u2014 used as a chip payload for LLM-authored nav suggestions.",
|
|
2220
|
+
icon: "\u2192"
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
]
|
|
2224
|
+
};
|
|
2225
|
+
var runtime_default = runtime;
|
|
2226
|
+
export {
|
|
2227
|
+
ElementInstanceStore,
|
|
2228
|
+
TileHandler,
|
|
2229
|
+
runtime_default as default,
|
|
2230
|
+
getElementInstanceStore,
|
|
2231
|
+
runtime,
|
|
2232
|
+
setElementInstanceStore
|
|
30
2233
|
};
|
|
31
|
-
|
|
2234
|
+
//# sourceMappingURL=runtime.js.map
|