@syntrologie/adapt-chatbot 2.27.0 → 2.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AdaptiveChatBar.d.ts +76 -0
- package/dist/AdaptiveChatBar.d.ts.map +1 -0
- package/dist/AdaptiveChatBar.js +10 -0
- package/dist/AdaptiveChatBar.js.map +7 -0
- package/dist/AdaptiveChatBarMountable.d.ts +35 -0
- package/dist/AdaptiveChatBarMountable.d.ts.map +1 -0
- package/dist/AdaptiveChatTrail.d.ts +77 -0
- package/dist/AdaptiveChatTrail.d.ts.map +1 -0
- package/dist/AdaptiveChatTrail.js +9 -0
- package/dist/AdaptiveChatTrail.js.map +7 -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 +22 -50
- package/dist/ChatAssistantLit.d.ts.map +1 -1
- package/dist/ChatAssistantLit.js +5 -3
- package/dist/ChatSession.d.ts +178 -0
- package/dist/ChatSession.d.ts.map +1 -0
- package/dist/ChatTransport.d.ts +283 -0
- package/dist/ChatTransport.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/Turnstile.d.ts +83 -0
- package/dist/Turnstile.d.ts.map +1 -0
- package/dist/chunk-435KJD27.js +192 -0
- package/dist/chunk-435KJD27.js.map +7 -0
- package/dist/chunk-AUER7ZCK.js +634 -0
- package/dist/chunk-AUER7ZCK.js.map +7 -0
- package/dist/chunk-DOMEUJR7.js +382 -0
- package/dist/chunk-DOMEUJR7.js.map +7 -0
- package/dist/{chunk-W457NMGD.js → chunk-KUO67E2W.js} +1537 -4130
- package/dist/chunk-KUO67E2W.js.map +7 -0
- package/dist/chunk-QELVKBQV.js +214 -0
- package/dist/chunk-QELVKBQV.js.map +7 -0
- package/dist/chunk-UC4XU6GH.js +3306 -0
- package/dist/chunk-UC4XU6GH.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 +155 -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/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 +32 -0
- package/dist/elements/index.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__/observer-isolation.test.d.ts +13 -0
- package/dist/observer/__tests__/observer-isolation.test.d.ts.map +1 -0
- package/dist/observer/__tests__/queue.test.d.ts +2 -0
- package/dist/observer/__tests__/queue.test.d.ts.map +1 -0
- package/dist/observer/__tests__/transport.test.d.ts +2 -0
- package/dist/observer/__tests__/transport.test.d.ts.map +1 -0
- package/dist/observer/allowlist.d.ts +32 -0
- package/dist/observer/allowlist.d.ts.map +1 -0
- package/dist/observer/index.d.ts +35 -0
- package/dist/observer/index.d.ts.map +1 -0
- package/dist/observer/queue.d.ts +57 -0
- package/dist/observer/queue.d.ts.map +1 -0
- package/dist/observer/transport.d.ts +26 -0
- package/dist/observer/transport.d.ts.map +1 -0
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1617 -2
- package/dist/runtime.js.map +4 -4
- package/dist/schema.d.ts +3120 -7
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +40 -0
- package/dist/schema.js.map +2 -2
- package/dist/types.d.ts +30 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -1
- package/dist/chunk-W457NMGD.js.map +0 -7
package/dist/runtime.js
CHANGED
|
@@ -1,8 +1,1568 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
ActionHandler,
|
|
3
|
+
AgUiTransport,
|
|
4
|
+
ChatAssistantLitMountable,
|
|
5
|
+
ElementInstanceStore,
|
|
6
|
+
ItemHandler,
|
|
7
|
+
TileHandler,
|
|
8
|
+
acquireTokenWithChallenge,
|
|
9
|
+
decodeMutationEnvelope,
|
|
10
|
+
fetchMountedElements,
|
|
11
|
+
renderFallbackHtml
|
|
12
|
+
} from "./chunk-KUO67E2W.js";
|
|
13
|
+
import "./chunk-QELVKBQV.js";
|
|
14
|
+
import "./chunk-DOMEUJR7.js";
|
|
15
|
+
import "./chunk-UC4XU6GH.js";
|
|
16
|
+
import "./chunk-AUER7ZCK.js";
|
|
17
|
+
import "./chunk-435KJD27.js";
|
|
4
18
|
import "./chunk-UVKRO5ER.js";
|
|
5
19
|
|
|
20
|
+
// src/ChatSession.ts
|
|
21
|
+
var CHAT_SESSION_STORAGE_KEY = "syntro:chat:v1";
|
|
22
|
+
function isValidMessage(value) {
|
|
23
|
+
if (typeof value !== "object" || value === null) return false;
|
|
24
|
+
const m = value;
|
|
25
|
+
return (typeof m.id === "number" || typeof m.id === "string") && (m.role === "user" || m.role === "assistant" || m.role === "system") && typeof m.text === "string";
|
|
26
|
+
}
|
|
27
|
+
function loadFromStorage() {
|
|
28
|
+
try {
|
|
29
|
+
const raw = globalThis.localStorage?.getItem(CHAT_SESSION_STORAGE_KEY);
|
|
30
|
+
if (!raw) return null;
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
if (!parsed || !Array.isArray(parsed.messages)) return null;
|
|
33
|
+
const messages = parsed.messages.filter(isValidMessage);
|
|
34
|
+
const nextId = typeof parsed.nextId === "number" ? parsed.nextId : messages.length + 1;
|
|
35
|
+
return { messages, nextId };
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
var ChatSession = class {
|
|
41
|
+
constructor() {
|
|
42
|
+
this._messages = [];
|
|
43
|
+
this._inFlight = false;
|
|
44
|
+
this._nextId = 1;
|
|
45
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
46
|
+
this.sendListeners = /* @__PURE__ */ new Set();
|
|
47
|
+
this.interruptListeners = /* @__PURE__ */ new Set();
|
|
48
|
+
this.toolResultListeners = /* @__PURE__ */ new Set();
|
|
49
|
+
const restored = loadFromStorage();
|
|
50
|
+
if (restored) {
|
|
51
|
+
this._messages = restored.messages;
|
|
52
|
+
this._nextId = restored.nextId;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Snapshot the current state. Always returns a fresh immutable view. */
|
|
56
|
+
getState() {
|
|
57
|
+
return { messages: [...this._messages], inFlight: this._inFlight };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Subscribe to state changes. Called immediately with the current
|
|
61
|
+
* state, then again on every change. Returns an unsubscribe function.
|
|
62
|
+
*/
|
|
63
|
+
subscribe(cb) {
|
|
64
|
+
this.subscribers.add(cb);
|
|
65
|
+
cb(this.getState());
|
|
66
|
+
return () => {
|
|
67
|
+
this.subscribers.delete(cb);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* User submitted a message. Appends a user-role message, sets
|
|
72
|
+
* inFlight=true, notifies state subscribers, and fires a "send"
|
|
73
|
+
* event so transports can pick it up. Empty/whitespace text is a
|
|
74
|
+
* no-op (matches the chat bar's local guard).
|
|
75
|
+
*/
|
|
76
|
+
send(text, opts) {
|
|
77
|
+
const trimmed = text.trim();
|
|
78
|
+
if (!trimmed) return;
|
|
79
|
+
this._messages.push({ id: this._nextId++, role: "user", text: trimmed });
|
|
80
|
+
this._inFlight = true;
|
|
81
|
+
this.notify();
|
|
82
|
+
const event = { text: trimmed };
|
|
83
|
+
if (opts?.activeLidSlot) event.activeLidSlot = opts.activeLidSlot;
|
|
84
|
+
for (const listener of this.sendListeners) listener(event);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Single-shot assistant reply (no streaming). Equivalent to
|
|
88
|
+
* receiveStart + receiveDelta + receiveEnd in one call. Useful for
|
|
89
|
+
* stub transports and tests that don't model streaming.
|
|
90
|
+
*/
|
|
91
|
+
receive(text) {
|
|
92
|
+
const id = `m-${this._nextId++}`;
|
|
93
|
+
this._messages.push({ id, role: "assistant", text, status: "complete" });
|
|
94
|
+
this._inFlight = false;
|
|
95
|
+
this.notify();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Begin a streaming assistant message. Appends an empty assistant
|
|
99
|
+
* message with status='streaming'. Caller (transport adapter) feeds
|
|
100
|
+
* deltas via receiveDelta(id, text) and signals completion via
|
|
101
|
+
* receiveEnd(id). `inFlight` stays true through the stream.
|
|
102
|
+
*/
|
|
103
|
+
receiveStart(id) {
|
|
104
|
+
this._messages.push({ id, role: "assistant", text: "", status: "streaming" });
|
|
105
|
+
this._inFlight = true;
|
|
106
|
+
this.notify();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Append a delta to a streaming message. No-op when the id is
|
|
110
|
+
* unknown (race between transport events and reset, etc.) — never
|
|
111
|
+
* throws so transports can fire-and-forget.
|
|
112
|
+
*/
|
|
113
|
+
receiveDelta(id, delta) {
|
|
114
|
+
const msg = this._messages.find((m) => m.id === id);
|
|
115
|
+
if (!msg || msg.status !== "streaming") return;
|
|
116
|
+
msg.text += delta;
|
|
117
|
+
this.notify();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Mark a streaming message complete and clear inFlight. No-op when
|
|
121
|
+
* the id is unknown (defensive against transport double-fires).
|
|
122
|
+
*/
|
|
123
|
+
receiveEnd(id) {
|
|
124
|
+
const msg = this._messages.find((m) => m.id === id);
|
|
125
|
+
if (msg && msg.status === "streaming") {
|
|
126
|
+
msg.status = "complete";
|
|
127
|
+
}
|
|
128
|
+
this._inFlight = false;
|
|
129
|
+
this.notify();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Transport reports a fatal error. Marks any in-flight streaming
|
|
133
|
+
* message as 'error' (so the trail can render a styled error chip
|
|
134
|
+
* instead of pretending the partial text was a complete answer),
|
|
135
|
+
* appends a system-role message with the error text for visibility,
|
|
136
|
+
* and clears inFlight.
|
|
137
|
+
*/
|
|
138
|
+
error(message) {
|
|
139
|
+
for (const m of this._messages) {
|
|
140
|
+
if (m.status === "streaming") m.status = "error";
|
|
141
|
+
}
|
|
142
|
+
this._messages.push({
|
|
143
|
+
id: `err-${this._nextId++}`,
|
|
144
|
+
role: "system",
|
|
145
|
+
text: message,
|
|
146
|
+
status: "error"
|
|
147
|
+
});
|
|
148
|
+
this._inFlight = false;
|
|
149
|
+
this.notify();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* User clicked the in-flight stop button. Clears inFlight and
|
|
153
|
+
* fires an "interrupt" event so transports can cancel their
|
|
154
|
+
* in-flight request. No-op if not in-flight (idempotent).
|
|
155
|
+
*/
|
|
156
|
+
interrupt() {
|
|
157
|
+
if (!this._inFlight) return;
|
|
158
|
+
this._inFlight = false;
|
|
159
|
+
this.notify();
|
|
160
|
+
for (const listener of this.interruptListeners) listener();
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Wipe state. Used by the canvas-close path (start a fresh
|
|
164
|
+
* conversation next time) and by tests.
|
|
165
|
+
*/
|
|
166
|
+
reset() {
|
|
167
|
+
this._messages = [];
|
|
168
|
+
this._inFlight = false;
|
|
169
|
+
this._nextId = 1;
|
|
170
|
+
this.notify();
|
|
171
|
+
}
|
|
172
|
+
/** Register a transport's send listener. Returns unsubscribe. */
|
|
173
|
+
onSend(listener) {
|
|
174
|
+
this.sendListeners.add(listener);
|
|
175
|
+
return () => {
|
|
176
|
+
this.sendListeners.delete(listener);
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* True when at least one transport has wired itself to the session's
|
|
181
|
+
* send pipeline. Views can use this to surface "no chat backend
|
|
182
|
+
* connected" affordances instead of hanging in `inFlight` after a
|
|
183
|
+
* send fires into the void.
|
|
184
|
+
*/
|
|
185
|
+
hasTransport() {
|
|
186
|
+
return this.sendListeners.size > 0;
|
|
187
|
+
}
|
|
188
|
+
/** Register a transport's interrupt listener. Returns unsubscribe. */
|
|
189
|
+
onInterrupt(listener) {
|
|
190
|
+
this.interruptListeners.add(listener);
|
|
191
|
+
return () => {
|
|
192
|
+
this.interruptListeners.delete(listener);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Register a transport's tool-result listener. The transport
|
|
197
|
+
* forwards `tool-result` actions back to the agent after the user
|
|
198
|
+
* approves or rejects a client-tool call. Returns unsubscribe.
|
|
199
|
+
*/
|
|
200
|
+
onToolResult(listener) {
|
|
201
|
+
this.toolResultListeners.add(listener);
|
|
202
|
+
return () => {
|
|
203
|
+
this.toolResultListeners.delete(listener);
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// -------------------------------------------------------------------------
|
|
207
|
+
// Tool calls
|
|
208
|
+
// -------------------------------------------------------------------------
|
|
209
|
+
/**
|
|
210
|
+
* Attach a tool call to a streaming assistant message. No-op when
|
|
211
|
+
* the message id is unknown (race between transport events and
|
|
212
|
+
* reset / late mount).
|
|
213
|
+
*/
|
|
214
|
+
addToolCall(messageId, toolCall) {
|
|
215
|
+
const msg = this._messages.find((m) => m.id === messageId);
|
|
216
|
+
if (!msg) return;
|
|
217
|
+
msg.toolCalls = [...msg.toolCalls ?? [], { ...toolCall }];
|
|
218
|
+
this.notify();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Partially update a tool call by id. Used by the transport to
|
|
222
|
+
* advance status (args-streaming → running → done) as AG-UI events
|
|
223
|
+
* arrive. No-op when the id is unknown.
|
|
224
|
+
*/
|
|
225
|
+
updateToolCall(toolCallId, patch) {
|
|
226
|
+
for (const msg of this._messages) {
|
|
227
|
+
const tcs = msg.toolCalls;
|
|
228
|
+
if (!tcs) continue;
|
|
229
|
+
const idx = tcs.findIndex((tc) => tc.id === toolCallId);
|
|
230
|
+
if (idx === -1) continue;
|
|
231
|
+
const next = [...tcs];
|
|
232
|
+
next[idx] = { ...next[idx], ...patch };
|
|
233
|
+
msg.toolCalls = next;
|
|
234
|
+
this.notify();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Resolve a (client-) tool call. Marks the call done, persists, and
|
|
240
|
+
* fires onToolResult so the transport can forward the result + the
|
|
241
|
+
* user's approve/reject decision back to the agent. No-op when the
|
|
242
|
+
* id is unknown.
|
|
243
|
+
*/
|
|
244
|
+
resolveToolCall(toolCallId, result, approved) {
|
|
245
|
+
let found = false;
|
|
246
|
+
for (const msg of this._messages) {
|
|
247
|
+
const tcs = msg.toolCalls;
|
|
248
|
+
if (!tcs) continue;
|
|
249
|
+
const idx = tcs.findIndex((tc) => tc.id === toolCallId);
|
|
250
|
+
if (idx === -1) continue;
|
|
251
|
+
const next = [...tcs];
|
|
252
|
+
next[idx] = { ...next[idx], status: "done" };
|
|
253
|
+
msg.toolCalls = next;
|
|
254
|
+
found = true;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (!found) return;
|
|
258
|
+
this.notify();
|
|
259
|
+
for (const listener of this.toolResultListeners) {
|
|
260
|
+
listener({ toolCallId, result, approved });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
notify() {
|
|
264
|
+
const state = this.getState();
|
|
265
|
+
this.persist();
|
|
266
|
+
for (const sub of this.subscribers) sub(state);
|
|
267
|
+
}
|
|
268
|
+
persist() {
|
|
269
|
+
try {
|
|
270
|
+
if (this._messages.length === 0) {
|
|
271
|
+
globalThis.localStorage?.removeItem(CHAT_SESSION_STORAGE_KEY);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const payload = { messages: this._messages, nextId: this._nextId };
|
|
275
|
+
globalThis.localStorage?.setItem(CHAT_SESSION_STORAGE_KEY, JSON.stringify(payload));
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var chatSession = new ChatSession();
|
|
281
|
+
|
|
282
|
+
// src/ChatTransport.ts
|
|
283
|
+
var FALLBACK_DEBOUNCE_MS = 1500;
|
|
284
|
+
function readSyntroToken() {
|
|
285
|
+
if (typeof window === "undefined") return void 0;
|
|
286
|
+
const cfg = window.__SYNTRO_CONFIG__;
|
|
287
|
+
const token = cfg?.token;
|
|
288
|
+
return typeof token === "string" && token.length > 0 ? token : void 0;
|
|
289
|
+
}
|
|
290
|
+
function debug(...args) {
|
|
291
|
+
if (typeof window === "undefined") return;
|
|
292
|
+
if (window.__SYNTRO_CHAT_DEBUG__) {
|
|
293
|
+
console.debug("[chat-transport]", ...args);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
var ChatTransport = class {
|
|
297
|
+
constructor() {
|
|
298
|
+
this._config = null;
|
|
299
|
+
this._status = "idle";
|
|
300
|
+
this._agui = null;
|
|
301
|
+
this._transportUnsub = null;
|
|
302
|
+
this._sessionUnsubSend = null;
|
|
303
|
+
this._sessionUnsubInterrupt = null;
|
|
304
|
+
this._sessionUnsubToolResult = null;
|
|
305
|
+
this._connectInFlight = null;
|
|
306
|
+
/** Bounded streaming-message id for the currently-being-typed assistant turn. */
|
|
307
|
+
this._currentAssistantMessageId = null;
|
|
308
|
+
/** Outcome bookkeeping for telemetry. */
|
|
309
|
+
this._hadTurnstileToken = null;
|
|
310
|
+
/** Count of successful round-trips for telemetry payloads. */
|
|
311
|
+
this._messagesSucceeded = 0;
|
|
312
|
+
/** True once a successful assistant message has landed — gates fallback. */
|
|
313
|
+
this._hasSucceeded = false;
|
|
314
|
+
/** True once fallback has fired (one-shot). */
|
|
315
|
+
this._fallbackRendered = false;
|
|
316
|
+
/**
|
|
317
|
+
* Per-send override for ``X-Active-Lid-Slot``. Set in the
|
|
318
|
+
* ``chatSession.onSend`` listener immediately before each
|
|
319
|
+
* ``_forwardUserMessage`` and consumed by ``buildHeaders`` on the
|
|
320
|
+
* outbound request. The singleton transport is shared by every
|
|
321
|
+
* mounted chat-bar (drawer + inline) so cached ``_config.activeLidSlot``
|
|
322
|
+
* follows whichever bar last reconfigured — using that for routing
|
|
323
|
+
* silently misroutes tiles when both bars coexist. Per-send threading
|
|
324
|
+
* pins the header to the bar that actually sent the message.
|
|
325
|
+
*/
|
|
326
|
+
this._pendingLidSlot = null;
|
|
327
|
+
/** Active debounce timer; null when no debounce pending. */
|
|
328
|
+
this._errorDebounceTimer = null;
|
|
329
|
+
/** Most recent error payload, captured so debounced fallback can attach it. */
|
|
330
|
+
this._lastErrorPayload = null;
|
|
331
|
+
this._fallbackListeners = /* @__PURE__ */ new Set();
|
|
332
|
+
/**
|
|
333
|
+
* Per-configure-cycle id for correlating telemetry events from a
|
|
334
|
+
* single transport lifetime. Mirrors ChatAssistantLit's mountId
|
|
335
|
+
* but scoped to configure cycles since the transport is a
|
|
336
|
+
* singleton across mounts.
|
|
337
|
+
*/
|
|
338
|
+
this._transportId = `tx_${Math.random().toString(36).slice(2, 8)}`;
|
|
339
|
+
this._configuredAt = 0;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Configure the transport. Idempotent — calling again with the same
|
|
343
|
+
* backendUrl is a no-op; calling with a different backendUrl tears
|
|
344
|
+
* the connection down and re-arms.
|
|
345
|
+
*/
|
|
346
|
+
configure(config) {
|
|
347
|
+
const same = this._config && this._config.backendUrl === config.backendUrl && this._config.threadId === config.threadId;
|
|
348
|
+
if (same) {
|
|
349
|
+
this._config = { ...this._config, ...config };
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this._disconnect();
|
|
353
|
+
this._config = config;
|
|
354
|
+
this._status = "configured";
|
|
355
|
+
this._transportId = `tx_${Math.random().toString(36).slice(2, 8)}`;
|
|
356
|
+
this._configuredAt = Date.now();
|
|
357
|
+
this._wireSession();
|
|
358
|
+
debug("configured", {
|
|
359
|
+
transportId: this._transportId,
|
|
360
|
+
backendUrl: config.backendUrl,
|
|
361
|
+
threadId: config.threadId
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/** ms since the most recent configure() call. 0 before any configure. */
|
|
365
|
+
_ageMs() {
|
|
366
|
+
return this._configuredAt === 0 ? 0 : Date.now() - this._configuredAt;
|
|
367
|
+
}
|
|
368
|
+
/** True when configure() has been called and we're ready to lazy-connect on send. */
|
|
369
|
+
get isConfigured() {
|
|
370
|
+
return this._status !== "idle";
|
|
371
|
+
}
|
|
372
|
+
/** True when AgUiTransport is up. */
|
|
373
|
+
get isConnected() {
|
|
374
|
+
return this._status === "connected";
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Subscribe to fallback events — fires once per configure cycle
|
|
378
|
+
* when the transport gives up after sustained failure. Hosts
|
|
379
|
+
* (typically AdaptiveChatBarMountable) use this to swap the chat
|
|
380
|
+
* bar for a static "contact support" card.
|
|
381
|
+
*/
|
|
382
|
+
onFallback(listener) {
|
|
383
|
+
this._fallbackListeners.add(listener);
|
|
384
|
+
return () => {
|
|
385
|
+
this._fallbackListeners.delete(listener);
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Test seam — drive a synthetic error event through the transport's
|
|
390
|
+
* error handling without standing up a real AgUi transport. Production
|
|
391
|
+
* code path uses _onTransportEvent. Exported as a public method to
|
|
392
|
+
* keep the test isolation simple; not part of the documented API.
|
|
393
|
+
*/
|
|
394
|
+
simulateError(payload) {
|
|
395
|
+
this._handleErrorEvent(payload);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Test seam — register a synthetic successful message-complete so
|
|
399
|
+
* the hasSucceeded gate flips without a real AgUi round-trip.
|
|
400
|
+
*/
|
|
401
|
+
simulateSuccessfulMessage() {
|
|
402
|
+
this._hasSucceeded = true;
|
|
403
|
+
this._messagesSucceeded += 1;
|
|
404
|
+
this._clearDebounceTimer();
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Test seam — return the lid slot that the next outbound request
|
|
408
|
+
* WOULD tag ``X-Active-Lid-Slot`` with, given current state. Mirrors
|
|
409
|
+
* the exact resolution buildHeaders uses (per-send slot wins over
|
|
410
|
+
* cached config). Not part of the documented API.
|
|
411
|
+
*/
|
|
412
|
+
getActiveLidSlotForTest() {
|
|
413
|
+
return this._pendingLidSlot ?? this._config?.activeLidSlot ?? null;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Tear connection-level state down. Used internally by configure()
|
|
417
|
+
* to swap backends; preserves host-registered fallback listeners
|
|
418
|
+
* because the host UI handler (e.g. AdaptiveChatBarMountable's
|
|
419
|
+
* "swap to fallback card" callback) is configuration-independent.
|
|
420
|
+
*/
|
|
421
|
+
_disconnect() {
|
|
422
|
+
if (this._transportUnsub) {
|
|
423
|
+
this._transportUnsub();
|
|
424
|
+
this._transportUnsub = null;
|
|
425
|
+
}
|
|
426
|
+
if (this._sessionUnsubSend) {
|
|
427
|
+
this._sessionUnsubSend();
|
|
428
|
+
this._sessionUnsubSend = null;
|
|
429
|
+
}
|
|
430
|
+
if (this._sessionUnsubInterrupt) {
|
|
431
|
+
this._sessionUnsubInterrupt();
|
|
432
|
+
this._sessionUnsubInterrupt = null;
|
|
433
|
+
}
|
|
434
|
+
if (this._sessionUnsubToolResult) {
|
|
435
|
+
this._sessionUnsubToolResult();
|
|
436
|
+
this._sessionUnsubToolResult = null;
|
|
437
|
+
}
|
|
438
|
+
if (this._agui) {
|
|
439
|
+
this._agui.disconnect();
|
|
440
|
+
this._agui = null;
|
|
441
|
+
}
|
|
442
|
+
this._clearDebounceTimer();
|
|
443
|
+
this._config = null;
|
|
444
|
+
this._status = "idle";
|
|
445
|
+
this._connectInFlight = null;
|
|
446
|
+
this._currentAssistantMessageId = null;
|
|
447
|
+
this._hadTurnstileToken = null;
|
|
448
|
+
this._messagesSucceeded = 0;
|
|
449
|
+
this._hasSucceeded = false;
|
|
450
|
+
this._fallbackRendered = false;
|
|
451
|
+
this._lastErrorPayload = null;
|
|
452
|
+
this._pendingLidSlot = null;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Tear everything down — connection state PLUS host listeners.
|
|
456
|
+
* Use this in test teardown or when fully shutting the transport
|
|
457
|
+
* (page unload, integration test reset). The mountable calls
|
|
458
|
+
* _disconnect indirectly via reconfigure.
|
|
459
|
+
*/
|
|
460
|
+
reset() {
|
|
461
|
+
this._disconnect();
|
|
462
|
+
this._fallbackListeners.clear();
|
|
463
|
+
}
|
|
464
|
+
_clearDebounceTimer() {
|
|
465
|
+
if (this._errorDebounceTimer) {
|
|
466
|
+
clearTimeout(this._errorDebounceTimer);
|
|
467
|
+
this._errorDebounceTimer = null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Shared error-handling kernel — called by the AG-UI subscriber and
|
|
472
|
+
* by the simulateError test seam. Publishes transport_error
|
|
473
|
+
* telemetry, captures lastErrorPayload, and starts the debounce
|
|
474
|
+
* timer that will fire fallback if no successful message arrives
|
|
475
|
+
* before it expires. Gated by hasSucceeded (post-success errors
|
|
476
|
+
* never fallback) and _fallbackRendered (one-shot).
|
|
477
|
+
*/
|
|
478
|
+
_handleErrorEvent(payload) {
|
|
479
|
+
const status = payload.status ?? null;
|
|
480
|
+
const body = payload.body ? String(payload.body).slice(0, 200) : null;
|
|
481
|
+
this._lastErrorPayload = {
|
|
482
|
+
message: payload.message,
|
|
483
|
+
status,
|
|
484
|
+
body,
|
|
485
|
+
errorName: payload.errorName ?? null
|
|
486
|
+
};
|
|
487
|
+
this._config?.runtime.events.publish("chatbot.transport_error", {
|
|
488
|
+
source: "chat-transport",
|
|
489
|
+
transportId: this._transportId,
|
|
490
|
+
transportAgeMs: this._ageMs(),
|
|
491
|
+
messagesSucceeded: this._messagesSucceeded,
|
|
492
|
+
hadTurnstileToken: this._hadTurnstileToken,
|
|
493
|
+
hasSucceeded: this._hasSucceeded,
|
|
494
|
+
errorMessage: payload.message ?? null,
|
|
495
|
+
errorStatus: status,
|
|
496
|
+
errorBody: body,
|
|
497
|
+
errorName: payload.errorName ?? null
|
|
498
|
+
});
|
|
499
|
+
if (this._hasSucceeded || this._fallbackRendered || this._errorDebounceTimer) return;
|
|
500
|
+
this._errorDebounceTimer = setTimeout(() => {
|
|
501
|
+
this._errorDebounceTimer = null;
|
|
502
|
+
if (this._hasSucceeded || this._fallbackRendered) return;
|
|
503
|
+
this._renderFallback("connect_failed");
|
|
504
|
+
}, FALLBACK_DEBOUNCE_MS);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Fire the fallback. One-shot per configure cycle. Notifies all
|
|
508
|
+
* fallback listeners with the per-customer card config and the
|
|
509
|
+
* diagnostic snapshot for telemetry/debug.
|
|
510
|
+
*/
|
|
511
|
+
_renderFallback(reason) {
|
|
512
|
+
if (this._fallbackRendered) return;
|
|
513
|
+
this._fallbackRendered = true;
|
|
514
|
+
debug("fallback", { reason, transportId: this._transportId, ageMs: this._ageMs() });
|
|
515
|
+
const fallback = this._config?.fallback ?? {};
|
|
516
|
+
const payload = {
|
|
517
|
+
reason,
|
|
518
|
+
fallback,
|
|
519
|
+
transportId: this._transportId,
|
|
520
|
+
transportAgeMs: this._ageMs(),
|
|
521
|
+
messagesSucceeded: this._messagesSucceeded,
|
|
522
|
+
hadTurnstileToken: this._hadTurnstileToken,
|
|
523
|
+
errorStatus: this._lastErrorPayload?.status ?? null,
|
|
524
|
+
errorBody: this._lastErrorPayload?.body ?? null,
|
|
525
|
+
errorMessage: this._lastErrorPayload?.message ?? null,
|
|
526
|
+
errorName: this._lastErrorPayload?.errorName ?? null
|
|
527
|
+
};
|
|
528
|
+
this._config?.runtime.events.publish(
|
|
529
|
+
"chatbot.fallback_rendered",
|
|
530
|
+
payload
|
|
531
|
+
);
|
|
532
|
+
for (const listener of this._fallbackListeners) listener(payload);
|
|
533
|
+
}
|
|
534
|
+
// -------------------------------------------------------------------------
|
|
535
|
+
// Internal: chatSession wiring
|
|
536
|
+
// -------------------------------------------------------------------------
|
|
537
|
+
_wireSession() {
|
|
538
|
+
this._sessionUnsubSend = chatSession.onSend(({ text, activeLidSlot }) => {
|
|
539
|
+
this._pendingLidSlot = typeof activeLidSlot === "string" && activeLidSlot.length > 0 ? activeLidSlot : null;
|
|
540
|
+
void this._forwardUserMessage(text);
|
|
541
|
+
});
|
|
542
|
+
this._sessionUnsubInterrupt = chatSession.onInterrupt(() => {
|
|
543
|
+
this._agui?.send({ type: "stop-generation" });
|
|
544
|
+
});
|
|
545
|
+
this._sessionUnsubToolResult = chatSession.onToolResult(({ toolCallId, result, approved }) => {
|
|
546
|
+
this._agui?.send({ type: "tool-result", toolCallId, result, approved });
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
async _forwardUserMessage(text) {
|
|
550
|
+
const ok = await this._ensureConnected();
|
|
551
|
+
if (!ok || !this._agui) {
|
|
552
|
+
chatSession.error("Couldn't connect to chat. Please try again.");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
this._agui.send({ type: "user-message", text });
|
|
556
|
+
}
|
|
557
|
+
// -------------------------------------------------------------------------
|
|
558
|
+
// Internal: connect (lazy, Turnstile-gated)
|
|
559
|
+
// -------------------------------------------------------------------------
|
|
560
|
+
/**
|
|
561
|
+
* Ensure AgUiTransport is up. Idempotent — multiple concurrent calls
|
|
562
|
+
* dedupe to a single Turnstile acquisition + transport setup.
|
|
563
|
+
* Returns true on success, false on terminal failure.
|
|
564
|
+
*/
|
|
565
|
+
async _ensureConnected() {
|
|
566
|
+
if (this._status === "connected" && this._agui) return true;
|
|
567
|
+
if (!this._config) return false;
|
|
568
|
+
if (this._connectInFlight) return this._connectInFlight;
|
|
569
|
+
this._connectInFlight = (async () => {
|
|
570
|
+
this._status = "acquiring";
|
|
571
|
+
const cft = await this._acquireTurnstileWithChallenge();
|
|
572
|
+
this._hadTurnstileToken = cft !== null;
|
|
573
|
+
if (this._config === null) return false;
|
|
574
|
+
const baseUrl = this._config.backendUrl.replace(/\/$/, "");
|
|
575
|
+
const streamUrl = `${baseUrl}/api/adaptive/stream`;
|
|
576
|
+
const token = readSyntroToken();
|
|
577
|
+
const buildHeaders = () => {
|
|
578
|
+
const h = {};
|
|
579
|
+
if (token) h.Authorization = `Bearer ${token}`;
|
|
580
|
+
if (cft) h["CF-Turnstile-Token"] = cft;
|
|
581
|
+
const lidSlot = this._pendingLidSlot ?? this._config?.activeLidSlot;
|
|
582
|
+
if (typeof lidSlot === "string" && lidSlot.length > 0) {
|
|
583
|
+
h["X-Active-Lid-Slot"] = lidSlot;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
const rt = this._config?.runtime;
|
|
587
|
+
const fromRuntime = rt?.telemetry?.getDistinctId?.();
|
|
588
|
+
const fromWindow = window.posthog?.get_distinct_id?.();
|
|
589
|
+
const did = fromRuntime || fromWindow;
|
|
590
|
+
if (typeof did === "string" && did.length > 0) {
|
|
591
|
+
h["X-Distinct-Id"] = did;
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
return h;
|
|
596
|
+
};
|
|
597
|
+
const runtime2 = this._config.runtime;
|
|
598
|
+
const resolveForwardedProps = () => {
|
|
599
|
+
const raw = this._config?.forwardedProps;
|
|
600
|
+
return typeof raw === "function" ? raw() : raw;
|
|
601
|
+
};
|
|
602
|
+
this._agui = new AgUiTransport({
|
|
603
|
+
url: streamUrl,
|
|
604
|
+
headers: buildHeaders,
|
|
605
|
+
threadId: this._config.threadId,
|
|
606
|
+
clientTools: this._config.clientTools,
|
|
607
|
+
// Adaptive runtime SDK needs the `syntro_chat_session` cookie to
|
|
608
|
+
// round-trip cross-origin so subsequent boot fetches can rehydrate
|
|
609
|
+
// LLM-authored UI elements. Editor / action-plan chat surfaces
|
|
610
|
+
// auth via `?token=` and intentionally leave this unset (see
|
|
611
|
+
// AgUiTransportOptions.credentials docstring).
|
|
612
|
+
credentials: "include",
|
|
613
|
+
forwardedProps: resolveForwardedProps,
|
|
614
|
+
onA2UIEvent: (payload) => {
|
|
615
|
+
const mutations = decodeMutationEnvelope(payload);
|
|
616
|
+
if (mutations !== null) {
|
|
617
|
+
const handler = this._config?.onElementMutation;
|
|
618
|
+
if (handler) {
|
|
619
|
+
try {
|
|
620
|
+
handler(mutations);
|
|
621
|
+
runtime2.events.publish("chatbot.element_mutation_applied", {
|
|
622
|
+
source: "chat-transport",
|
|
623
|
+
count: mutations.length
|
|
624
|
+
});
|
|
625
|
+
} catch (err) {
|
|
626
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
627
|
+
console.error("[chat-transport] element mutation apply failed:", msg);
|
|
628
|
+
}
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
runtime2.actions.applyBatch([payload]).then(() => {
|
|
634
|
+
runtime2.events.publish("chatbot.a2ui_applied", { source: "chat-transport" });
|
|
635
|
+
}).catch((err) => {
|
|
636
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
637
|
+
console.error("[chat-transport] A2UI apply failed:", msg);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
this._agui.connect();
|
|
642
|
+
this._transportUnsub = this._agui.subscribe((event) => this._onTransportEvent(event));
|
|
643
|
+
this._status = "connected";
|
|
644
|
+
return true;
|
|
645
|
+
})();
|
|
646
|
+
try {
|
|
647
|
+
return await this._connectInFlight;
|
|
648
|
+
} finally {
|
|
649
|
+
this._connectInFlight = null;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Acquire a Turnstile token via the managed-challenge flow.
|
|
654
|
+
* Delegates to the shared Turnstile helper that owns the verify
|
|
655
|
+
* panel lifecycle. Returns null when Turnstile is disabled at
|
|
656
|
+
* build time or acquisition fails.
|
|
657
|
+
*/
|
|
658
|
+
async _acquireTurnstileWithChallenge() {
|
|
659
|
+
const { token } = await acquireTokenWithChallenge();
|
|
660
|
+
return token;
|
|
661
|
+
}
|
|
662
|
+
// -------------------------------------------------------------------------
|
|
663
|
+
// Internal: AgUi event → chatSession
|
|
664
|
+
// -------------------------------------------------------------------------
|
|
665
|
+
/**
|
|
666
|
+
* Look up a tool call's current chatSession-side status by id.
|
|
667
|
+
* Used to decide between addToolCall (first sighting) and
|
|
668
|
+
* updateToolCall (subsequent updates) when AG-UI re-emits
|
|
669
|
+
* `tool-call` events for client tools transitioning to 'pending'.
|
|
670
|
+
*/
|
|
671
|
+
_findToolCallStatus(toolCallId) {
|
|
672
|
+
const state = chatSession.getState();
|
|
673
|
+
for (const msg of state.messages) {
|
|
674
|
+
const tc = msg.toolCalls?.find((t) => t.id === toolCallId);
|
|
675
|
+
if (tc) return tc.status;
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
/** Map AG-UI ToolCallStatus to TrailToolCall status. */
|
|
680
|
+
_mapToolCallStatus(status) {
|
|
681
|
+
switch (status) {
|
|
682
|
+
case "args-streaming":
|
|
683
|
+
return "args-streaming";
|
|
684
|
+
case "pending":
|
|
685
|
+
return "pending";
|
|
686
|
+
case "running":
|
|
687
|
+
return "running";
|
|
688
|
+
case "done":
|
|
689
|
+
return "done";
|
|
690
|
+
case "error":
|
|
691
|
+
return "error";
|
|
692
|
+
default:
|
|
693
|
+
return "running";
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
_onTransportEvent(event) {
|
|
697
|
+
const runtime2 = this._config?.runtime;
|
|
698
|
+
switch (event.type) {
|
|
699
|
+
case "session-ready":
|
|
700
|
+
case "messages-snapshot":
|
|
701
|
+
case "typing":
|
|
702
|
+
return;
|
|
703
|
+
case "message-append": {
|
|
704
|
+
this._currentAssistantMessageId = event.message.id;
|
|
705
|
+
chatSession.receiveStart(event.message.id);
|
|
706
|
+
if (event.message.content && event.message.content.length > 0) {
|
|
707
|
+
chatSession.receiveDelta(event.message.id, event.message.content);
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
case "message-delta": {
|
|
712
|
+
const id = event.messageId ?? this._currentAssistantMessageId;
|
|
713
|
+
if (!id || !event.delta) return;
|
|
714
|
+
chatSession.receiveDelta(id, event.delta);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
case "message-complete": {
|
|
718
|
+
const id = event.messageId ?? this._currentAssistantMessageId;
|
|
719
|
+
if (!id) return;
|
|
720
|
+
chatSession.receiveEnd(id);
|
|
721
|
+
this._currentAssistantMessageId = null;
|
|
722
|
+
this._messagesSucceeded += 1;
|
|
723
|
+
this._hasSucceeded = true;
|
|
724
|
+
this._clearDebounceTimer();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
case "tool-call": {
|
|
728
|
+
const targetMessageId = event.messageId ?? this._currentAssistantMessageId;
|
|
729
|
+
if (!targetMessageId) return;
|
|
730
|
+
const existing = this._findToolCallStatus(event.toolCall.id);
|
|
731
|
+
if (!existing) {
|
|
732
|
+
chatSession.addToolCall(targetMessageId, {
|
|
733
|
+
id: event.toolCall.id,
|
|
734
|
+
name: event.toolCall.name,
|
|
735
|
+
status: this._mapToolCallStatus(event.toolCall.status)
|
|
736
|
+
});
|
|
737
|
+
} else {
|
|
738
|
+
chatSession.updateToolCall(event.toolCall.id, {
|
|
739
|
+
name: event.toolCall.name,
|
|
740
|
+
status: this._mapToolCallStatus(event.toolCall.status)
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
case "tool-call-args-delta":
|
|
746
|
+
debug("tool-call-args-delta", event);
|
|
747
|
+
return;
|
|
748
|
+
case "tool-call-done":
|
|
749
|
+
chatSession.updateToolCall(event.toolCallId, { status: "done" });
|
|
750
|
+
return;
|
|
751
|
+
case "a2ui":
|
|
752
|
+
return;
|
|
753
|
+
case "error": {
|
|
754
|
+
const status = event.status ?? null;
|
|
755
|
+
const body = event.body ? String(event.body).slice(0, 200) : null;
|
|
756
|
+
console.warn(
|
|
757
|
+
`[chat-transport] error status=${status ?? "no-status"} succeeded=${this._messagesSucceeded}`,
|
|
758
|
+
event
|
|
759
|
+
);
|
|
760
|
+
this._handleErrorEvent({
|
|
761
|
+
message: event.message,
|
|
762
|
+
status,
|
|
763
|
+
body,
|
|
764
|
+
errorName: event.errorName ?? null
|
|
765
|
+
});
|
|
766
|
+
chatSession.error(event.message ?? "Chat connection failed");
|
|
767
|
+
this._currentAssistantMessageId = null;
|
|
768
|
+
if (this._agui) {
|
|
769
|
+
this._agui.disconnect();
|
|
770
|
+
this._agui = null;
|
|
771
|
+
}
|
|
772
|
+
if (this._transportUnsub) {
|
|
773
|
+
this._transportUnsub();
|
|
774
|
+
this._transportUnsub = null;
|
|
775
|
+
}
|
|
776
|
+
this._status = "error";
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
var chatTransport = new ChatTransport();
|
|
783
|
+
|
|
784
|
+
// src/observer/allowlist.ts
|
|
785
|
+
var MAX_TEXT_LEN = 200;
|
|
786
|
+
var SIGNIFICANT_CLICK_TAGS = /* @__PURE__ */ new Set(["button", "a"]);
|
|
787
|
+
function truncate(s) {
|
|
788
|
+
return s.length <= MAX_TEXT_LEN ? s : `${s.slice(0, MAX_TEXT_LEN - 1)}\u2026`;
|
|
789
|
+
}
|
|
790
|
+
function firstElement(props) {
|
|
791
|
+
const els = props.$elements;
|
|
792
|
+
if (Array.isArray(els) && els.length > 0 && typeof els[0] === "object" && els[0] !== null) {
|
|
793
|
+
return els[0];
|
|
794
|
+
}
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
function elementText(props) {
|
|
798
|
+
const el = firstElement(props);
|
|
799
|
+
if (!el) return "";
|
|
800
|
+
const t = el.text;
|
|
801
|
+
return typeof t === "string" ? t.trim() : "";
|
|
802
|
+
}
|
|
803
|
+
function elementTag(props) {
|
|
804
|
+
const explicit = props.$element_tag_name;
|
|
805
|
+
if (typeof explicit === "string") return explicit.toLowerCase();
|
|
806
|
+
const el = firstElement(props);
|
|
807
|
+
if (el && typeof el.tag_name === "string") return el.tag_name.toLowerCase();
|
|
808
|
+
return "";
|
|
809
|
+
}
|
|
810
|
+
function elementRole(props) {
|
|
811
|
+
const el = firstElement(props);
|
|
812
|
+
if (!el) return "";
|
|
813
|
+
const role = el.attr__role;
|
|
814
|
+
return typeof role === "string" ? role.toLowerCase() : "";
|
|
815
|
+
}
|
|
816
|
+
function formName(props) {
|
|
817
|
+
const el = firstElement(props);
|
|
818
|
+
if (!el) return "unnamed";
|
|
819
|
+
const name = el.attr__name;
|
|
820
|
+
if (typeof name === "string" && name) return name;
|
|
821
|
+
const id = el.attr__id;
|
|
822
|
+
if (typeof id === "string" && id) return id;
|
|
823
|
+
return "unnamed";
|
|
824
|
+
}
|
|
825
|
+
function isSignificantClickTarget(props) {
|
|
826
|
+
const tag = elementTag(props);
|
|
827
|
+
if (SIGNIFICANT_CLICK_TAGS.has(tag)) return true;
|
|
828
|
+
if (elementRole(props) === "button") return true;
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
function pathname(props) {
|
|
832
|
+
const pn = props.$pathname;
|
|
833
|
+
return typeof pn === "string" ? pn : "/";
|
|
834
|
+
}
|
|
835
|
+
function renderCustomEvent(raw) {
|
|
836
|
+
const fields = [];
|
|
837
|
+
for (const [k, v] of Object.entries(raw.properties)) {
|
|
838
|
+
if (k.startsWith("$")) continue;
|
|
839
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
840
|
+
fields.push(`${k}=${v}`);
|
|
841
|
+
if (fields.length >= 2) break;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return fields.length > 0 ? `${raw.event} (${fields.join(", ")})` : raw.event;
|
|
845
|
+
}
|
|
846
|
+
function matchEvent(raw, opts = {}) {
|
|
847
|
+
const props = raw.properties;
|
|
848
|
+
switch (raw.event) {
|
|
849
|
+
case "$pageview": {
|
|
850
|
+
const path = pathname(props);
|
|
851
|
+
return {
|
|
852
|
+
ts: raw.timestamp,
|
|
853
|
+
kind: "nav",
|
|
854
|
+
text: truncate(`navigated to ${path}`),
|
|
855
|
+
ref: path
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
case "$autocapture": {
|
|
859
|
+
const eventType = props.$event_type;
|
|
860
|
+
if (eventType === "click") {
|
|
861
|
+
if (!isSignificantClickTarget(props)) return null;
|
|
862
|
+
const text = elementText(props) || elementTag(props);
|
|
863
|
+
return {
|
|
864
|
+
ts: raw.timestamp,
|
|
865
|
+
kind: "click",
|
|
866
|
+
text: truncate(`clicked '${text}'`)
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
if (eventType === "submit" && elementTag(props) === "form") {
|
|
870
|
+
return {
|
|
871
|
+
ts: raw.timestamp,
|
|
872
|
+
kind: "form",
|
|
873
|
+
text: truncate(`submitted form '${formName(props)}'`)
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
case "$rageclick": {
|
|
879
|
+
const text = elementText(props) || elementTag(props);
|
|
880
|
+
return {
|
|
881
|
+
ts: raw.timestamp,
|
|
882
|
+
kind: "rage",
|
|
883
|
+
text: truncate(`rage-clicked '${text}'`)
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
case "$dead_click": {
|
|
887
|
+
const text = elementText(props) || elementTag(props);
|
|
888
|
+
return {
|
|
889
|
+
ts: raw.timestamp,
|
|
890
|
+
kind: "dead",
|
|
891
|
+
text: truncate(`clicked '${text}' (no response)`)
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
// ── Canonical runtime-bus events (sources: `'canvas'` instrumentation
|
|
895
|
+
// or the event-processor's rrweb detectors). These don't go through
|
|
896
|
+
// PostHog; the chat-bar's bus translator forwards them as-is with
|
|
897
|
+
// the canonical event name, and matchEvent picks the right slot
|
|
898
|
+
// here. Keeps a single allowlist as the source of truth for what
|
|
899
|
+
// the agent's observation tail can carry.
|
|
900
|
+
case "nav.section_viewed": {
|
|
901
|
+
const section = typeof props.section === "string" ? props.section : "";
|
|
902
|
+
if (!section) return null;
|
|
903
|
+
return {
|
|
904
|
+
ts: raw.timestamp,
|
|
905
|
+
kind: "view",
|
|
906
|
+
text: truncate(`viewed the '${section}' section`),
|
|
907
|
+
ref: section
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
case "nav.scroll_depth": {
|
|
911
|
+
const pct = typeof props.percent === "number" ? props.percent : null;
|
|
912
|
+
if (pct === null) return null;
|
|
913
|
+
return {
|
|
914
|
+
ts: raw.timestamp,
|
|
915
|
+
kind: "scroll",
|
|
916
|
+
text: truncate(`scrolled to ${pct}% of the page`)
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
case "ui.scroll_thrash": {
|
|
920
|
+
return {
|
|
921
|
+
ts: raw.timestamp,
|
|
922
|
+
kind: "scroll",
|
|
923
|
+
text: "scrolled up and down repeatedly (looking for something)"
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
case "ui.idle": {
|
|
927
|
+
const ms = typeof props.durationMs === "number" ? props.durationMs : null;
|
|
928
|
+
const secs = ms !== null ? Math.round(ms / 1e3) : null;
|
|
929
|
+
return {
|
|
930
|
+
ts: raw.timestamp,
|
|
931
|
+
kind: "idle",
|
|
932
|
+
text: secs !== null ? `idle for ${secs}s` : "idle"
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
case "ui.hover": {
|
|
936
|
+
const text = elementText(props) || elementTag(props);
|
|
937
|
+
return {
|
|
938
|
+
ts: raw.timestamp,
|
|
939
|
+
kind: "hover",
|
|
940
|
+
text: truncate(`hovered on '${text}'`)
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
case "ui.hesitation": {
|
|
944
|
+
const text = elementText(props) || elementTag(props);
|
|
945
|
+
return {
|
|
946
|
+
ts: raw.timestamp,
|
|
947
|
+
kind: "hesitation",
|
|
948
|
+
text: truncate(`hesitated near '${text}'`)
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
case "ui.focus_bounce": {
|
|
952
|
+
const text = elementText(props) || elementTag(props);
|
|
953
|
+
return {
|
|
954
|
+
ts: raw.timestamp,
|
|
955
|
+
kind: "form",
|
|
956
|
+
text: truncate(`focused '${text}' but didn't fill it`)
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
default: {
|
|
960
|
+
const allow = opts.observableEvents ?? [];
|
|
961
|
+
if (allow.includes(raw.event)) {
|
|
962
|
+
return {
|
|
963
|
+
ts: raw.timestamp,
|
|
964
|
+
kind: "custom",
|
|
965
|
+
text: truncate(renderCustomEvent(raw))
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/observer/queue.ts
|
|
974
|
+
var ObserverQueue = class {
|
|
975
|
+
constructor(opts) {
|
|
976
|
+
this._buffer = [];
|
|
977
|
+
this._sent = 0;
|
|
978
|
+
this._dropped = 0;
|
|
979
|
+
this._lastError = null;
|
|
980
|
+
this._disabled = false;
|
|
981
|
+
this._intervalTimer = null;
|
|
982
|
+
this._backoffTimer = null;
|
|
983
|
+
this._currentBackoffMs = 0;
|
|
984
|
+
this._inFlight = false;
|
|
985
|
+
this._opts = {
|
|
986
|
+
backoffStartMs: 1e3,
|
|
987
|
+
backoffCapMs: 3e4,
|
|
988
|
+
...opts
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
push(event) {
|
|
992
|
+
if (this._disabled) return;
|
|
993
|
+
if (this._buffer.length >= this._opts.queueCap) {
|
|
994
|
+
this._buffer.shift();
|
|
995
|
+
this._dropped += 1;
|
|
996
|
+
}
|
|
997
|
+
this._buffer.push(event);
|
|
998
|
+
if (this._buffer.length >= this._opts.batchSize) {
|
|
999
|
+
this._scheduleImmediate();
|
|
1000
|
+
} else {
|
|
1001
|
+
this._scheduleInterval();
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
async flushNow() {
|
|
1005
|
+
this._clearInterval();
|
|
1006
|
+
await this._flush();
|
|
1007
|
+
}
|
|
1008
|
+
stats() {
|
|
1009
|
+
return {
|
|
1010
|
+
queued: this._buffer.length,
|
|
1011
|
+
sent: this._sent,
|
|
1012
|
+
dropped: this._dropped,
|
|
1013
|
+
lastError: this._lastError,
|
|
1014
|
+
disabled: this._disabled
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
_scheduleImmediate() {
|
|
1018
|
+
queueMicrotask(() => {
|
|
1019
|
+
void this._flush();
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
_scheduleInterval() {
|
|
1023
|
+
if (this._intervalTimer != null) return;
|
|
1024
|
+
this._intervalTimer = setTimeout(() => {
|
|
1025
|
+
this._intervalTimer = null;
|
|
1026
|
+
void this._flush();
|
|
1027
|
+
}, this._opts.flushIntervalMs);
|
|
1028
|
+
}
|
|
1029
|
+
_clearInterval() {
|
|
1030
|
+
if (this._intervalTimer != null) {
|
|
1031
|
+
clearTimeout(this._intervalTimer);
|
|
1032
|
+
this._intervalTimer = null;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
async _flush() {
|
|
1036
|
+
if (this._disabled || this._inFlight || this._buffer.length === 0) return;
|
|
1037
|
+
this._clearInterval();
|
|
1038
|
+
const batch = this._buffer.splice(0, this._opts.batchSize);
|
|
1039
|
+
this._inFlight = true;
|
|
1040
|
+
try {
|
|
1041
|
+
await this._opts.flush(batch);
|
|
1042
|
+
this._sent += batch.length;
|
|
1043
|
+
this._currentBackoffMs = 0;
|
|
1044
|
+
this._lastError = null;
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
this._buffer.unshift(...batch);
|
|
1047
|
+
const perm = err?.permanent === true;
|
|
1048
|
+
this._lastError = err instanceof Error ? err.message : String(err?.message ?? err);
|
|
1049
|
+
if (perm) {
|
|
1050
|
+
this._disabled = true;
|
|
1051
|
+
} else {
|
|
1052
|
+
this._scheduleBackoff();
|
|
1053
|
+
}
|
|
1054
|
+
} finally {
|
|
1055
|
+
this._inFlight = false;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
_scheduleBackoff() {
|
|
1059
|
+
const next = this._currentBackoffMs === 0 ? this._opts.backoffStartMs : Math.min(this._currentBackoffMs * 2, this._opts.backoffCapMs);
|
|
1060
|
+
this._currentBackoffMs = next;
|
|
1061
|
+
if (this._backoffTimer != null) clearTimeout(this._backoffTimer);
|
|
1062
|
+
this._backoffTimer = setTimeout(() => {
|
|
1063
|
+
this._backoffTimer = null;
|
|
1064
|
+
void this._flush();
|
|
1065
|
+
}, next);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
// src/observer/transport.ts
|
|
1070
|
+
function buildBody(distinctId, batch) {
|
|
1071
|
+
const body = { distinct_id: distinctId, batch };
|
|
1072
|
+
return JSON.stringify(body);
|
|
1073
|
+
}
|
|
1074
|
+
function createTransport(opts) {
|
|
1075
|
+
return {
|
|
1076
|
+
async send(batch) {
|
|
1077
|
+
const body = buildBody(opts.getDistinctId(), batch);
|
|
1078
|
+
const resp = await fetch(opts.url, {
|
|
1079
|
+
method: "POST",
|
|
1080
|
+
headers: {
|
|
1081
|
+
Authorization: `Bearer ${opts.token()}`,
|
|
1082
|
+
"Content-Type": "application/json"
|
|
1083
|
+
},
|
|
1084
|
+
body,
|
|
1085
|
+
keepalive: true,
|
|
1086
|
+
credentials: "include"
|
|
1087
|
+
});
|
|
1088
|
+
if (!resp.ok) {
|
|
1089
|
+
const err = new Error(`observation POST ${resp.status}`);
|
|
1090
|
+
if (resp.status >= 400 && resp.status < 500 && resp.status !== 429) {
|
|
1091
|
+
err.permanent = true;
|
|
1092
|
+
}
|
|
1093
|
+
throw err;
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
sendBeacon(batch) {
|
|
1097
|
+
const body = buildBody(opts.getDistinctId(), batch);
|
|
1098
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
1099
|
+
const beacon = navigator?.sendBeacon;
|
|
1100
|
+
if (typeof beacon !== "function") return false;
|
|
1101
|
+
return beacon.call(navigator, opts.url, blob);
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/observer/index.ts
|
|
1107
|
+
var _active = null;
|
|
1108
|
+
function _attachPostHog(ingest, _observableEvents) {
|
|
1109
|
+
const ph = globalThis.posthog;
|
|
1110
|
+
if (!ph) return () => {
|
|
1111
|
+
};
|
|
1112
|
+
const addHook = ph._addCaptureHook;
|
|
1113
|
+
if (typeof addHook !== "function") return () => {
|
|
1114
|
+
};
|
|
1115
|
+
const handler = (eventName, properties) => {
|
|
1116
|
+
if (typeof eventName !== "string") return;
|
|
1117
|
+
ingest({
|
|
1118
|
+
event: eventName,
|
|
1119
|
+
timestamp: Date.now(),
|
|
1120
|
+
properties: properties ?? {}
|
|
1121
|
+
});
|
|
1122
|
+
};
|
|
1123
|
+
addHook.call(ph, handler);
|
|
1124
|
+
return () => {
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function _attachPageHide(_transport, queue) {
|
|
1128
|
+
const handler = () => {
|
|
1129
|
+
const stats = queue.stats();
|
|
1130
|
+
if (stats.queued > 0) {
|
|
1131
|
+
void queue.flushNow();
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
window.addEventListener("pagehide", handler, { capture: true });
|
|
1135
|
+
return () => window.removeEventListener("pagehide", handler, { capture: true });
|
|
1136
|
+
}
|
|
1137
|
+
function startObserver(opts) {
|
|
1138
|
+
if (_active) {
|
|
1139
|
+
_active.refCount += 1;
|
|
1140
|
+
return _makeHandle();
|
|
1141
|
+
}
|
|
1142
|
+
const transport = opts.transport ?? createTransport(opts);
|
|
1143
|
+
const queue = new ObserverQueue({
|
|
1144
|
+
batchSize: 10,
|
|
1145
|
+
flushIntervalMs: 2e3,
|
|
1146
|
+
queueCap: 200,
|
|
1147
|
+
flush: (batch) => transport.send(batch)
|
|
1148
|
+
});
|
|
1149
|
+
const ingest = (raw) => {
|
|
1150
|
+
const ev = matchEvent(raw, { observableEvents: opts.observableEvents });
|
|
1151
|
+
if (ev !== null) {
|
|
1152
|
+
queue.push(ev);
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
const unsubscribePh = _attachPostHog(ingest, opts.observableEvents ?? []);
|
|
1156
|
+
const unsubscribePageHide = _attachPageHide(transport, queue);
|
|
1157
|
+
_active = {
|
|
1158
|
+
refCount: 1,
|
|
1159
|
+
queue,
|
|
1160
|
+
unsubscribe: () => {
|
|
1161
|
+
unsubscribePh();
|
|
1162
|
+
unsubscribePageHide();
|
|
1163
|
+
},
|
|
1164
|
+
options: opts
|
|
1165
|
+
};
|
|
1166
|
+
return _makeHandle();
|
|
1167
|
+
}
|
|
1168
|
+
function _makeHandle() {
|
|
1169
|
+
return {
|
|
1170
|
+
stop() {
|
|
1171
|
+
if (!_active) return;
|
|
1172
|
+
_active.refCount -= 1;
|
|
1173
|
+
if (_active.refCount <= 0) {
|
|
1174
|
+
_active.unsubscribe();
|
|
1175
|
+
_active = null;
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
stats() {
|
|
1179
|
+
return _active?.queue.stats() ?? {
|
|
1180
|
+
queued: 0,
|
|
1181
|
+
sent: 0,
|
|
1182
|
+
dropped: 0,
|
|
1183
|
+
lastError: null,
|
|
1184
|
+
disabled: true
|
|
1185
|
+
};
|
|
1186
|
+
},
|
|
1187
|
+
ingest(raw) {
|
|
1188
|
+
if (!_active) return;
|
|
1189
|
+
const ev = matchEvent(raw, {
|
|
1190
|
+
observableEvents: _active.options.observableEvents
|
|
1191
|
+
});
|
|
1192
|
+
if (ev !== null) _active.queue.push(ev);
|
|
1193
|
+
},
|
|
1194
|
+
flushNow() {
|
|
1195
|
+
return _active?.queue.flushNow() ?? Promise.resolve();
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/AdaptiveChatBarMountable.ts
|
|
1201
|
+
var STATE_KEY = "__syntroChatBarMount";
|
|
1202
|
+
function getState(container) {
|
|
1203
|
+
return container[STATE_KEY] ?? null;
|
|
1204
|
+
}
|
|
1205
|
+
function setState(container, state) {
|
|
1206
|
+
container[STATE_KEY] = state;
|
|
1207
|
+
}
|
|
1208
|
+
function applyPlaceholder(bar, cfg) {
|
|
1209
|
+
if (cfg.placeholder !== void 0) bar.placeholder = cfg.placeholder;
|
|
1210
|
+
if (cfg.greeting !== void 0) bar.greeting = cfg.greeting;
|
|
1211
|
+
}
|
|
1212
|
+
var _elementStore = null;
|
|
1213
|
+
var _templateWidgetMap = /* @__PURE__ */ new Map();
|
|
1214
|
+
function resolveTileWidget(templateId) {
|
|
1215
|
+
return _templateWidgetMap.get(templateId);
|
|
1216
|
+
}
|
|
1217
|
+
function refreshTemplateWidgetMap(uiTemplates) {
|
|
1218
|
+
_templateWidgetMap.clear();
|
|
1219
|
+
if (!uiTemplates) return;
|
|
1220
|
+
const tiles = uiTemplates.tiles;
|
|
1221
|
+
if (!Array.isArray(tiles)) return;
|
|
1222
|
+
for (const t of tiles) {
|
|
1223
|
+
const id = typeof t?.id === "string" ? t.id : null;
|
|
1224
|
+
const widget = typeof t?.widget === "string" ? t.widget : null;
|
|
1225
|
+
if (id && widget) _templateWidgetMap.set(id, widget);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
function getOrCreateElementStore(runtime2) {
|
|
1229
|
+
if (_elementStore) return _elementStore;
|
|
1230
|
+
_elementStore = new ElementInstanceStore({
|
|
1231
|
+
actions: runtime2.actions,
|
|
1232
|
+
// Pass the runtime's event bus so ItemHandler can broadcast
|
|
1233
|
+
// `element.compositional_append` / `_patch` / `_remove` events
|
|
1234
|
+
// to container widgets (chips strip, FAQ accordion, nav tips).
|
|
1235
|
+
events: {
|
|
1236
|
+
publish: runtime2.events.publish.bind(runtime2.events),
|
|
1237
|
+
subscribe: runtime2.events.subscribe?.bind(runtime2.events)
|
|
1238
|
+
},
|
|
1239
|
+
handlers: [new TileHandler(resolveTileWidget), new ActionHandler(), new ItemHandler()],
|
|
1240
|
+
resolveTileWidget
|
|
1241
|
+
});
|
|
1242
|
+
return _elementStore;
|
|
1243
|
+
}
|
|
1244
|
+
var _hydrationStarted = false;
|
|
1245
|
+
function hydrateOnce(runtime2, backendUrl) {
|
|
1246
|
+
if (_hydrationStarted) return;
|
|
1247
|
+
_hydrationStarted = true;
|
|
1248
|
+
const store = getOrCreateElementStore(runtime2);
|
|
1249
|
+
const trimmed = backendUrl.replace(/\/$/, "");
|
|
1250
|
+
const endpoint = trimmed ? `${trimmed}/api/adaptive/mounted_elements` : "/api/adaptive/mounted_elements";
|
|
1251
|
+
fetchMountedElements({ endpoint }).then((response) => {
|
|
1252
|
+
if (!response) return;
|
|
1253
|
+
void store.hydrate(response.mounted_elements);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
function configureTransportIfPossible(cfg) {
|
|
1257
|
+
if (cfg.backendUrl === void 0 || !cfg.runtime) return;
|
|
1258
|
+
const elementsActive = cfg.elementsEnabled === true && cfg.uiTemplates != null;
|
|
1259
|
+
refreshTemplateWidgetMap(elementsActive ? cfg.uiTemplates : void 0);
|
|
1260
|
+
const forwardedProps = elementsActive ? { elementsEnabled: true, uiTemplates: cfg.uiTemplates } : void 0;
|
|
1261
|
+
const runtime2 = cfg.runtime;
|
|
1262
|
+
const onElementMutation = elementsActive ? (mutations) => {
|
|
1263
|
+
void getOrCreateElementStore(runtime2).apply(mutations);
|
|
1264
|
+
} : void 0;
|
|
1265
|
+
if (elementsActive) {
|
|
1266
|
+
hydrateOnce(runtime2, cfg.backendUrl);
|
|
1267
|
+
}
|
|
1268
|
+
chatTransport.configure({
|
|
1269
|
+
...cfg,
|
|
1270
|
+
forwardedProps,
|
|
1271
|
+
onElementMutation,
|
|
1272
|
+
activeLidSlot: cfg._syntroSlotName
|
|
1273
|
+
});
|
|
1274
|
+
startObserverIfPossible(cfg);
|
|
1275
|
+
}
|
|
1276
|
+
var _observerStarted = false;
|
|
1277
|
+
function startObserverIfPossible(cfg) {
|
|
1278
|
+
if (_observerStarted) return;
|
|
1279
|
+
if (typeof window === "undefined") return;
|
|
1280
|
+
const runtime2 = cfg.runtime;
|
|
1281
|
+
if (!runtime2?.events?.subscribe) return;
|
|
1282
|
+
const trimmed = (cfg.backendUrl ?? "").replace(/\/$/, "");
|
|
1283
|
+
const url = trimmed ? `${trimmed}/api/adaptive/observation` : "/api/adaptive/observation";
|
|
1284
|
+
const getDistinctId = () => {
|
|
1285
|
+
const ph = window.posthog;
|
|
1286
|
+
try {
|
|
1287
|
+
const v = ph?.get_distinct_id?.();
|
|
1288
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
1289
|
+
} catch {
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
const token = () => {
|
|
1294
|
+
const c = window.__SYNTRO_CONFIG__;
|
|
1295
|
+
return typeof c?.token === "string" ? c.token : "";
|
|
1296
|
+
};
|
|
1297
|
+
const handle = startObserver({ url, token, getDistinctId });
|
|
1298
|
+
window.__syntroObserverStats = () => handle.stats();
|
|
1299
|
+
runtime2.events.subscribe({}, (evt) => {
|
|
1300
|
+
const e = evt;
|
|
1301
|
+
if (!e?.name) return;
|
|
1302
|
+
const ts = typeof e.ts === "number" ? e.ts : Date.now();
|
|
1303
|
+
if (e.source === "posthog") {
|
|
1304
|
+
const original = e.props?.originalEvent;
|
|
1305
|
+
if (typeof original !== "string" || original.length === 0) return;
|
|
1306
|
+
const properties = { ...e.props ?? {} };
|
|
1307
|
+
if (typeof e.props?.pathname === "string") properties.$pathname = e.props.pathname;
|
|
1308
|
+
if (typeof e.props?.url === "string") properties.$current_url = e.props.url;
|
|
1309
|
+
handle.ingest({ event: original, timestamp: ts, properties });
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
handle.ingest({
|
|
1313
|
+
event: e.name,
|
|
1314
|
+
timestamp: ts,
|
|
1315
|
+
properties: { ...e.props ?? {} }
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
_observerStarted = true;
|
|
1319
|
+
}
|
|
1320
|
+
function wireListeners(bar, state) {
|
|
1321
|
+
const onMessageSent = (e) => {
|
|
1322
|
+
const text = e.detail.text;
|
|
1323
|
+
chatSession.send(text, { activeLidSlot: state.cfg._syntroSlotName });
|
|
1324
|
+
if (!chatSession.hasTransport()) {
|
|
1325
|
+
chatSession.error("Chat backend not configured \u2014 set backendUrl in the canvas config.");
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
const onInterrupt = () => {
|
|
1329
|
+
chatSession.interrupt();
|
|
1330
|
+
};
|
|
1331
|
+
const onToolCallApproved = (e) => {
|
|
1332
|
+
const detail = e.detail;
|
|
1333
|
+
chatSession.resolveToolCall(detail.toolCallId, {}, detail.approved);
|
|
1334
|
+
};
|
|
1335
|
+
const onClose = () => {
|
|
1336
|
+
state.cfg.onClose?.();
|
|
1337
|
+
};
|
|
1338
|
+
bar.addEventListener("chat-message-sent", onMessageSent);
|
|
1339
|
+
bar.addEventListener("chat-interrupt", onInterrupt);
|
|
1340
|
+
bar.addEventListener("canvas-close", onClose);
|
|
1341
|
+
bar.addEventListener("trail-toolcall-approved", onToolCallApproved);
|
|
1342
|
+
return () => {
|
|
1343
|
+
bar.removeEventListener("chat-message-sent", onMessageSent);
|
|
1344
|
+
bar.removeEventListener("chat-interrupt", onInterrupt);
|
|
1345
|
+
bar.removeEventListener("canvas-close", onClose);
|
|
1346
|
+
bar.removeEventListener("trail-toolcall-approved", onToolCallApproved);
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
var AdaptiveChatBarMountable = {
|
|
1350
|
+
mount(container, mountConfig) {
|
|
1351
|
+
const cfg = mountConfig ?? {};
|
|
1352
|
+
configureTransportIfPossible(cfg);
|
|
1353
|
+
const bar = document.createElement("adaptive-chat-bar");
|
|
1354
|
+
applyPlaceholder(bar, cfg);
|
|
1355
|
+
const unsubSession = chatSession.subscribe((s) => {
|
|
1356
|
+
bar.messages = [...s.messages];
|
|
1357
|
+
bar.inFlight = s.inFlight;
|
|
1358
|
+
});
|
|
1359
|
+
const state = { bar, cleanup: () => {
|
|
1360
|
+
}, cfg };
|
|
1361
|
+
const unwireListeners = wireListeners(bar, state);
|
|
1362
|
+
container.appendChild(bar);
|
|
1363
|
+
const unsubFallback = chatTransport.onFallback(() => {
|
|
1364
|
+
bar.remove();
|
|
1365
|
+
container.innerHTML = renderFallbackHtml(state.cfg.fallback);
|
|
1366
|
+
});
|
|
1367
|
+
state.cleanup = () => {
|
|
1368
|
+
unsubSession();
|
|
1369
|
+
unsubFallback();
|
|
1370
|
+
unwireListeners();
|
|
1371
|
+
bar.remove();
|
|
1372
|
+
setState(container, null);
|
|
1373
|
+
};
|
|
1374
|
+
setState(container, state);
|
|
1375
|
+
return state.cleanup;
|
|
1376
|
+
},
|
|
1377
|
+
update(container, mountConfig) {
|
|
1378
|
+
const state = getState(container);
|
|
1379
|
+
if (!state) return;
|
|
1380
|
+
const cfg = mountConfig ?? {};
|
|
1381
|
+
configureTransportIfPossible(cfg);
|
|
1382
|
+
applyPlaceholder(state.bar, cfg);
|
|
1383
|
+
state.cfg = cfg;
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
// src/AdaptiveChipsStripMountable.ts
|
|
1388
|
+
var STATE_KEY2 = "__syntroChipsStripMount";
|
|
1389
|
+
function getState2(container) {
|
|
1390
|
+
return container[STATE_KEY2] ?? null;
|
|
1391
|
+
}
|
|
1392
|
+
function setState2(container, state) {
|
|
1393
|
+
container[STATE_KEY2] = state;
|
|
1394
|
+
}
|
|
1395
|
+
function applyProps(strip, cfg) {
|
|
1396
|
+
if (cfg.chips !== void 0) strip.chips = cfg.chips;
|
|
1397
|
+
if (cfg.runtime !== void 0) {
|
|
1398
|
+
strip.runtimeRef = cfg.runtime;
|
|
1399
|
+
}
|
|
1400
|
+
if (cfg.chromeless !== void 0) strip.chromeless = cfg.chromeless;
|
|
1401
|
+
}
|
|
1402
|
+
var AdaptiveChipsStripMountable = {
|
|
1403
|
+
mount(container, mountConfig) {
|
|
1404
|
+
const cfg = mountConfig ?? {};
|
|
1405
|
+
const strip = document.createElement("adaptive-chips-strip");
|
|
1406
|
+
applyProps(strip, cfg);
|
|
1407
|
+
const state = {
|
|
1408
|
+
strip,
|
|
1409
|
+
listeners: {
|
|
1410
|
+
revealed: (e) => {
|
|
1411
|
+
const cb = cfg.onChipRevealed;
|
|
1412
|
+
if (cb) {
|
|
1413
|
+
cb(
|
|
1414
|
+
e.detail
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
dismissed: (e) => {
|
|
1419
|
+
const cb = cfg.onChipDismissed;
|
|
1420
|
+
if (cb) cb(e.detail);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
strip.addEventListener("chip-revealed", state.listeners.revealed);
|
|
1425
|
+
strip.addEventListener("chip-dismissed", state.listeners.dismissed);
|
|
1426
|
+
container.appendChild(strip);
|
|
1427
|
+
setState2(container, state);
|
|
1428
|
+
return () => {
|
|
1429
|
+
strip.removeEventListener("chip-revealed", state.listeners.revealed);
|
|
1430
|
+
strip.removeEventListener("chip-dismissed", state.listeners.dismissed);
|
|
1431
|
+
strip.remove();
|
|
1432
|
+
setState2(container, null);
|
|
1433
|
+
};
|
|
1434
|
+
},
|
|
1435
|
+
update(container, mountConfig) {
|
|
1436
|
+
const state = getState2(container);
|
|
1437
|
+
if (!state) return;
|
|
1438
|
+
const cfg = mountConfig ?? {};
|
|
1439
|
+
applyProps(state.strip, cfg);
|
|
1440
|
+
state.strip.removeEventListener("chip-revealed", state.listeners.revealed);
|
|
1441
|
+
state.strip.removeEventListener("chip-dismissed", state.listeners.dismissed);
|
|
1442
|
+
state.listeners.revealed = (e) => {
|
|
1443
|
+
const cb = cfg.onChipRevealed;
|
|
1444
|
+
if (cb) {
|
|
1445
|
+
cb(e.detail);
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
state.listeners.dismissed = (e) => {
|
|
1449
|
+
const cb = cfg.onChipDismissed;
|
|
1450
|
+
if (cb) cb(e.detail);
|
|
1451
|
+
};
|
|
1452
|
+
state.strip.addEventListener("chip-revealed", state.listeners.revealed);
|
|
1453
|
+
state.strip.addEventListener("chip-dismissed", state.listeners.dismissed);
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
// src/NavLinkMountable.ts
|
|
1458
|
+
var STATE_KEY3 = "__syntroNavLinkMount";
|
|
1459
|
+
function getState3(container) {
|
|
1460
|
+
return container[STATE_KEY3] ?? null;
|
|
1461
|
+
}
|
|
1462
|
+
function setState3(container, state) {
|
|
1463
|
+
container[STATE_KEY3] = state;
|
|
1464
|
+
}
|
|
1465
|
+
function navigateTo(rawUrl) {
|
|
1466
|
+
if (!rawUrl) return;
|
|
1467
|
+
let resolved;
|
|
1468
|
+
try {
|
|
1469
|
+
resolved = new URL(rawUrl, window.location.origin);
|
|
1470
|
+
} catch {
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
if (resolved.origin !== window.location.origin) return;
|
|
1474
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return;
|
|
1475
|
+
window.history.pushState(null, "", resolved.toString());
|
|
1476
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
1477
|
+
}
|
|
1478
|
+
function buildButton(cfg) {
|
|
1479
|
+
const button = document.createElement("button");
|
|
1480
|
+
button.type = "button";
|
|
1481
|
+
button.dataset.syntroNavLink = "";
|
|
1482
|
+
button.style.display = "inline-flex";
|
|
1483
|
+
button.style.alignItems = "center";
|
|
1484
|
+
button.style.gap = "6px";
|
|
1485
|
+
button.style.padding = "6px 12px";
|
|
1486
|
+
button.style.borderRadius = "8px";
|
|
1487
|
+
button.style.fontFamily = "var(--sc-font-family, 'system-ui')";
|
|
1488
|
+
button.style.fontSize = "12px";
|
|
1489
|
+
button.style.fontWeight = "600";
|
|
1490
|
+
button.style.cursor = "pointer";
|
|
1491
|
+
button.style.color = "var(--sc-accent-foreground, currentColor)";
|
|
1492
|
+
button.style.background = "hsl(var(--sc-accent-color) / 0.85)";
|
|
1493
|
+
button.style.border = "1px solid hsl(var(--sc-accent-color) / 0.30)";
|
|
1494
|
+
applyLabel(button, cfg);
|
|
1495
|
+
return button;
|
|
1496
|
+
}
|
|
1497
|
+
function applyLabel(button, cfg) {
|
|
1498
|
+
const url = String(cfg.url ?? "");
|
|
1499
|
+
const label = cfg.label ?? (url ? `Go to ${url}` : "Go");
|
|
1500
|
+
button.textContent = label;
|
|
1501
|
+
}
|
|
1502
|
+
var NavLinkMountable = {
|
|
1503
|
+
mount(container, mountConfig) {
|
|
1504
|
+
const cfg = mountConfig ?? {};
|
|
1505
|
+
const button = buildButton(cfg);
|
|
1506
|
+
const onClick = (_e) => {
|
|
1507
|
+
const current = getState3(container);
|
|
1508
|
+
const url = current?.cfg.url ?? cfg.url;
|
|
1509
|
+
navigateTo(String(url ?? ""));
|
|
1510
|
+
};
|
|
1511
|
+
button.addEventListener("click", onClick);
|
|
1512
|
+
container.appendChild(button);
|
|
1513
|
+
setState3(container, { button, cfg, onClick });
|
|
1514
|
+
return () => {
|
|
1515
|
+
button.removeEventListener("click", onClick);
|
|
1516
|
+
button.remove();
|
|
1517
|
+
setState3(container, null);
|
|
1518
|
+
};
|
|
1519
|
+
},
|
|
1520
|
+
update(container, mountConfig) {
|
|
1521
|
+
const state = getState3(container);
|
|
1522
|
+
if (!state) return;
|
|
1523
|
+
const cfg = mountConfig ?? {};
|
|
1524
|
+
applyLabel(state.button, cfg);
|
|
1525
|
+
state.cfg = cfg;
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
// src/TextAnswerMountable.ts
|
|
1530
|
+
var STATE_KEY4 = "__syntroTextAnswerMount";
|
|
1531
|
+
function getState4(container) {
|
|
1532
|
+
return container[STATE_KEY4] ?? null;
|
|
1533
|
+
}
|
|
1534
|
+
function setState4(container, state) {
|
|
1535
|
+
container[STATE_KEY4] = state;
|
|
1536
|
+
}
|
|
1537
|
+
function applyText(p, text) {
|
|
1538
|
+
p.textContent = text;
|
|
1539
|
+
}
|
|
1540
|
+
var TextAnswerMountable = {
|
|
1541
|
+
mount(container, mountConfig) {
|
|
1542
|
+
const cfg = mountConfig ?? {};
|
|
1543
|
+
const paragraph = document.createElement("p");
|
|
1544
|
+
paragraph.dataset.syntroTextAnswer = "";
|
|
1545
|
+
paragraph.style.margin = "0";
|
|
1546
|
+
paragraph.style.fontSize = "12px";
|
|
1547
|
+
paragraph.style.lineHeight = "1.55";
|
|
1548
|
+
paragraph.style.color = "var(--sc-tile-text-color, currentColor)";
|
|
1549
|
+
applyText(paragraph, String(cfg.text ?? ""));
|
|
1550
|
+
container.appendChild(paragraph);
|
|
1551
|
+
setState4(container, { paragraph, cfg });
|
|
1552
|
+
return () => {
|
|
1553
|
+
paragraph.remove();
|
|
1554
|
+
setState4(container, null);
|
|
1555
|
+
};
|
|
1556
|
+
},
|
|
1557
|
+
update(container, mountConfig) {
|
|
1558
|
+
const state = getState4(container);
|
|
1559
|
+
if (!state) return;
|
|
1560
|
+
const cfg = mountConfig ?? {};
|
|
1561
|
+
applyText(state.paragraph, String(cfg.text ?? ""));
|
|
1562
|
+
state.cfg = cfg;
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
|
|
6
1566
|
// src/runtime.ts
|
|
7
1567
|
var runtime = {
|
|
8
1568
|
id: "adaptive-chatbot",
|
|
@@ -19,6 +1579,61 @@ var runtime = {
|
|
|
19
1579
|
description: "AI-powered chat assistant with SSE streaming and A2UI support",
|
|
20
1580
|
icon: "\u{1F4AC}"
|
|
21
1581
|
}
|
|
1582
|
+
},
|
|
1583
|
+
// PRD §3 chat lid: the standalone chat bar UI shell. Used as a slot
|
|
1584
|
+
// `lid` widget (where it replaces the launcher) or as a regular
|
|
1585
|
+
// tile widget (renders inside the drawer). Headless of any chat
|
|
1586
|
+
// transport — emits chat-message-sent / chat-interrupt / canvas-close
|
|
1587
|
+
// for the parent to wire to whatever pipeline it owns.
|
|
1588
|
+
{
|
|
1589
|
+
id: "adaptive-chatbot:chat-bar",
|
|
1590
|
+
component: AdaptiveChatBarMountable,
|
|
1591
|
+
metadata: {
|
|
1592
|
+
name: "Chat Bar",
|
|
1593
|
+
description: "Always-visible chat input row with bubble-up trail. Headless \u2014 wire to your own transport.",
|
|
1594
|
+
icon: "\u2726"
|
|
1595
|
+
}
|
|
1596
|
+
},
|
|
1597
|
+
// PRD §4.5 suggested-chips tile: horizontally-wrapping chip strip
|
|
1598
|
+
// with click-to-reveal payload drawer. Each chip is a
|
|
1599
|
+
// `suggestions:chip` action.
|
|
1600
|
+
{
|
|
1601
|
+
id: "adaptive-chatbot:chips-strip",
|
|
1602
|
+
component: AdaptiveChipsStripMountable,
|
|
1603
|
+
metadata: {
|
|
1604
|
+
name: "Suggested Chips",
|
|
1605
|
+
description: "Wrapping chip strip with click-to-reveal payloads.",
|
|
1606
|
+
icon: "\u2728"
|
|
1607
|
+
}
|
|
1608
|
+
},
|
|
1609
|
+
// Built-in chip payload — a single paragraph of body text. Chip
|
|
1610
|
+
// payloads are widget references (same {widget,props} shape as
|
|
1611
|
+
// tile/lid configs); this is the simplest one shipped out of the
|
|
1612
|
+
// box so the chip pattern works without requiring a downstream
|
|
1613
|
+
// adaptive for a "just answer with text" case.
|
|
1614
|
+
{
|
|
1615
|
+
id: "adaptive-chatbot:text-answer",
|
|
1616
|
+
component: TextAnswerMountable,
|
|
1617
|
+
metadata: {
|
|
1618
|
+
name: "Text Answer",
|
|
1619
|
+
description: "Single paragraph payload \u2014 the default chip-drawer content.",
|
|
1620
|
+
icon: "\xB6"
|
|
1621
|
+
}
|
|
1622
|
+
},
|
|
1623
|
+
// Chip payload for LLM-authored navigation suggestions. Backed by
|
|
1624
|
+
// the `suggest-navigation` ItemTemplate: the backend wraps a curated
|
|
1625
|
+
// URL into props.url, the chip's payload-drawer mounts this widget,
|
|
1626
|
+
// user clicks "Go to <url>" → same-origin SPA navigation. Kept here
|
|
1627
|
+
// (not in adaptive-nav) so chip payloads ship with the chatbot
|
|
1628
|
+
// bundle that already provides chips-strip + text-answer.
|
|
1629
|
+
{
|
|
1630
|
+
id: "adaptive-chatbot:nav-link",
|
|
1631
|
+
component: NavLinkMountable,
|
|
1632
|
+
metadata: {
|
|
1633
|
+
name: "Nav Link",
|
|
1634
|
+
description: "Same-origin navigation button \u2014 used as a chip payload for LLM-authored nav suggestions.",
|
|
1635
|
+
icon: "\u2192"
|
|
1636
|
+
}
|
|
22
1637
|
}
|
|
23
1638
|
]
|
|
24
1639
|
};
|