@smooai/chat-widget 0.5.1 → 0.6.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/chat-widget.global.js +592 -109
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +540 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/element.ts +208 -47
- package/src/markdown.ts +368 -0
- package/src/styles.ts +44 -0
|
@@ -49,6 +49,44 @@ var SmoothAgentChat = (function(exports) {
|
|
|
49
49
|
return !resolved.allowAnonymous && (resolved.requireName || resolved.requireEmail || resolved.requirePhone);
|
|
50
50
|
}
|
|
51
51
|
//#endregion
|
|
52
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/typeof.js
|
|
53
|
+
function _typeof(o) {
|
|
54
|
+
"@babel/helpers - typeof";
|
|
55
|
+
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
56
|
+
return typeof o;
|
|
57
|
+
} : function(o) {
|
|
58
|
+
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
59
|
+
}, _typeof(o);
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPrimitive.js
|
|
63
|
+
function toPrimitive(t, r) {
|
|
64
|
+
if ("object" != _typeof(t) || !t) return t;
|
|
65
|
+
var e = t[Symbol.toPrimitive];
|
|
66
|
+
if (void 0 !== e) {
|
|
67
|
+
var i = e.call(t, r || "default");
|
|
68
|
+
if ("object" != _typeof(i)) return i;
|
|
69
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
70
|
+
}
|
|
71
|
+
return ("string" === r ? String : Number)(t);
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPropertyKey.js
|
|
75
|
+
function toPropertyKey(t) {
|
|
76
|
+
var i = toPrimitive(t, "string");
|
|
77
|
+
return "symbol" == _typeof(i) ? i : i + "";
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/defineProperty.js
|
|
81
|
+
function _defineProperty(e, r, t) {
|
|
82
|
+
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
83
|
+
value: t,
|
|
84
|
+
enumerable: !0,
|
|
85
|
+
configurable: !0,
|
|
86
|
+
writable: !0
|
|
87
|
+
}) : e[r] = t, e;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
52
90
|
//#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/transport.js
|
|
53
91
|
/**
|
|
54
92
|
* Transport abstraction for the client.
|
|
@@ -69,14 +107,14 @@ var SmoothAgentChat = (function(exports) {
|
|
|
69
107
|
* Node, or a mock in tests).
|
|
70
108
|
*/
|
|
71
109
|
var WebSocketTransport = class {
|
|
72
|
-
socket = null;
|
|
73
|
-
url;
|
|
74
|
-
factory;
|
|
75
|
-
connectTimeout;
|
|
76
|
-
messageHandlers = /* @__PURE__ */ new Set();
|
|
77
|
-
closeHandlers = /* @__PURE__ */ new Set();
|
|
78
|
-
errorHandlers = /* @__PURE__ */ new Set();
|
|
79
110
|
constructor(url, factory, connectTimeout = DEFAULT_CONNECT_TIMEOUT) {
|
|
111
|
+
_defineProperty(this, "socket", null);
|
|
112
|
+
_defineProperty(this, "url", void 0);
|
|
113
|
+
_defineProperty(this, "factory", void 0);
|
|
114
|
+
_defineProperty(this, "connectTimeout", void 0);
|
|
115
|
+
_defineProperty(this, "messageHandlers", /* @__PURE__ */ new Set());
|
|
116
|
+
_defineProperty(this, "closeHandlers", /* @__PURE__ */ new Set());
|
|
117
|
+
_defineProperty(this, "errorHandlers", /* @__PURE__ */ new Set());
|
|
80
118
|
this.url = url;
|
|
81
119
|
this.connectTimeout = connectTimeout;
|
|
82
120
|
if (factory) this.factory = factory;
|
|
@@ -215,6 +253,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
215
253
|
* - **No live server required.** Correctness is fully unit-testable with a mock
|
|
216
254
|
* transport (see `test/client.test.ts`).
|
|
217
255
|
*/
|
|
256
|
+
let _Symbol$asyncIterator;
|
|
218
257
|
/** A timeout that yields no terminal event. */
|
|
219
258
|
var RequestTimeoutError = class extends Error {
|
|
220
259
|
constructor(requestId, ms) {
|
|
@@ -228,24 +267,25 @@ var SmoothAgentChat = (function(exports) {
|
|
|
228
267
|
* and its async iteration throws it, so a stuck server can never hang the caller.
|
|
229
268
|
*/
|
|
230
269
|
var TurnTimeoutError = class extends Error {
|
|
231
|
-
requestId;
|
|
232
270
|
constructor(requestId, ms) {
|
|
233
271
|
super(`Turn ${requestId} timed out after ${ms}ms without a terminal response`);
|
|
272
|
+
_defineProperty(this, "requestId", void 0);
|
|
234
273
|
this.name = "TurnTimeoutError";
|
|
235
274
|
this.requestId = requestId;
|
|
236
275
|
}
|
|
237
276
|
};
|
|
238
277
|
/** A protocol-level error event surfaced as a throwable. */
|
|
239
278
|
var ProtocolError = class extends Error {
|
|
240
|
-
code;
|
|
241
|
-
requestId;
|
|
242
279
|
constructor(code, message, requestId) {
|
|
243
280
|
super(message);
|
|
281
|
+
_defineProperty(this, "code", void 0);
|
|
282
|
+
_defineProperty(this, "requestId", void 0);
|
|
244
283
|
this.name = "ProtocolError";
|
|
245
284
|
this.code = code;
|
|
246
285
|
this.requestId = requestId;
|
|
247
286
|
}
|
|
248
287
|
};
|
|
288
|
+
_Symbol$asyncIterator = Symbol.asyncIterator;
|
|
249
289
|
/**
|
|
250
290
|
* A streaming message turn. Await it for the terminal {@link EventualResponse},
|
|
251
291
|
* or async-iterate it to receive every intermediate event in arrival order.
|
|
@@ -259,19 +299,23 @@ var SmoothAgentChat = (function(exports) {
|
|
|
259
299
|
* ```
|
|
260
300
|
*/
|
|
261
301
|
var MessageTurn = class {
|
|
262
|
-
/** The requestId this turn is correlated on. */
|
|
263
|
-
requestId;
|
|
264
|
-
queue = [];
|
|
265
|
-
waiter = null;
|
|
266
|
-
done = false;
|
|
267
|
-
finalEvent = null;
|
|
268
|
-
error = null;
|
|
269
|
-
settled;
|
|
270
|
-
settle;
|
|
271
|
-
fail;
|
|
272
|
-
onClose;
|
|
273
|
-
timeoutTimer;
|
|
274
302
|
constructor(requestId, onClose, turnTimeout = 0) {
|
|
303
|
+
_defineProperty(
|
|
304
|
+
this,
|
|
305
|
+
/** The requestId this turn is correlated on. */
|
|
306
|
+
"requestId",
|
|
307
|
+
void 0
|
|
308
|
+
);
|
|
309
|
+
_defineProperty(this, "queue", []);
|
|
310
|
+
_defineProperty(this, "waiter", null);
|
|
311
|
+
_defineProperty(this, "done", false);
|
|
312
|
+
_defineProperty(this, "finalEvent", null);
|
|
313
|
+
_defineProperty(this, "error", null);
|
|
314
|
+
_defineProperty(this, "settled", void 0);
|
|
315
|
+
_defineProperty(this, "settle", void 0);
|
|
316
|
+
_defineProperty(this, "fail", void 0);
|
|
317
|
+
_defineProperty(this, "onClose", void 0);
|
|
318
|
+
_defineProperty(this, "timeoutTimer", void 0);
|
|
275
319
|
this.requestId = requestId;
|
|
276
320
|
this.onClose = onClose;
|
|
277
321
|
this.settled = new Promise((resolve, reject) => {
|
|
@@ -333,7 +377,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
333
377
|
});
|
|
334
378
|
}
|
|
335
379
|
}
|
|
336
|
-
[
|
|
380
|
+
[_Symbol$asyncIterator]() {
|
|
337
381
|
return { next: () => {
|
|
338
382
|
if (this.queue.length > 0) return Promise.resolve({
|
|
339
383
|
value: this.queue.shift(),
|
|
@@ -359,18 +403,30 @@ var SmoothAgentChat = (function(exports) {
|
|
|
359
403
|
}
|
|
360
404
|
};
|
|
361
405
|
var SmoothAgentClient = class {
|
|
362
|
-
transport;
|
|
363
|
-
generateRequestId;
|
|
364
|
-
requestTimeout;
|
|
365
|
-
turnTimeout;
|
|
366
|
-
/** requestId → single-response waiter (create_session, get_session, ping, …). */
|
|
367
|
-
pending = /* @__PURE__ */ new Map();
|
|
368
|
-
/** requestId → active streaming turn (send_message, and HITL resumes). */
|
|
369
|
-
turns = /* @__PURE__ */ new Map();
|
|
370
|
-
/** Unsolicited-event listeners (keepalive, server-push). */
|
|
371
|
-
listeners = /* @__PURE__ */ new Set();
|
|
372
|
-
unsubscribe = [];
|
|
373
406
|
constructor(options) {
|
|
407
|
+
_defineProperty(this, "transport", void 0);
|
|
408
|
+
_defineProperty(this, "generateRequestId", void 0);
|
|
409
|
+
_defineProperty(this, "requestTimeout", void 0);
|
|
410
|
+
_defineProperty(this, "turnTimeout", void 0);
|
|
411
|
+
_defineProperty(
|
|
412
|
+
this,
|
|
413
|
+
/** requestId → single-response waiter (create_session, get_session, ping, …). */
|
|
414
|
+
"pending",
|
|
415
|
+
/* @__PURE__ */ new Map()
|
|
416
|
+
);
|
|
417
|
+
_defineProperty(
|
|
418
|
+
this,
|
|
419
|
+
/** requestId → active streaming turn (send_message, and HITL resumes). */
|
|
420
|
+
"turns",
|
|
421
|
+
/* @__PURE__ */ new Map()
|
|
422
|
+
);
|
|
423
|
+
_defineProperty(
|
|
424
|
+
this,
|
|
425
|
+
/** Unsolicited-event listeners (keepalive, server-push). */
|
|
426
|
+
"listeners",
|
|
427
|
+
/* @__PURE__ */ new Set()
|
|
428
|
+
);
|
|
429
|
+
_defineProperty(this, "unsubscribe", []);
|
|
374
430
|
this.transport = options.transport ?? new WebSocketTransport(withConnectionToken(options.url, options.token), options.webSocketFactory);
|
|
375
431
|
this.requestTimeout = options.requestTimeout ?? 3e4;
|
|
376
432
|
this.turnTimeout = options.turnTimeout ?? 12e4;
|
|
@@ -612,19 +668,17 @@ var SmoothAgentChat = (function(exports) {
|
|
|
612
668
|
return out;
|
|
613
669
|
}
|
|
614
670
|
var ConversationController = class {
|
|
615
|
-
config;
|
|
616
|
-
events;
|
|
617
|
-
client = null;
|
|
618
|
-
sessionId = null;
|
|
619
|
-
messages = [];
|
|
620
|
-
status = "idle";
|
|
621
|
-
seq = 0;
|
|
622
|
-
/** Visitor identity, seeded from config and updated by the pre-chat form. */
|
|
623
|
-
identity;
|
|
624
|
-
/** requestId of the in-flight turn — used to resume OTP / tool confirmations. */
|
|
625
|
-
activeRequestId = null;
|
|
626
|
-
interrupt = null;
|
|
627
671
|
constructor(config, events) {
|
|
672
|
+
_defineProperty(this, "config", void 0);
|
|
673
|
+
_defineProperty(this, "events", void 0);
|
|
674
|
+
_defineProperty(this, "client", null);
|
|
675
|
+
_defineProperty(this, "sessionId", null);
|
|
676
|
+
_defineProperty(this, "messages", []);
|
|
677
|
+
_defineProperty(this, "status", "idle");
|
|
678
|
+
_defineProperty(this, "seq", 0);
|
|
679
|
+
_defineProperty(this, "identity", void 0);
|
|
680
|
+
_defineProperty(this, "activeRequestId", null);
|
|
681
|
+
_defineProperty(this, "interrupt", null);
|
|
628
682
|
this.config = config;
|
|
629
683
|
this.events = events;
|
|
630
684
|
this.identity = {
|
|
@@ -820,6 +874,305 @@ var SmoothAgentChat = (function(exports) {
|
|
|
820
874
|
*/
|
|
821
875
|
const SMOOTH_LOGO_SVG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg id=\"Layer_1\" data-name=\"Layer 1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 550 135\">\n <defs>\n <style>\n .cls-1 {\n fill: url(#linear-gradient-3);\n }\n\n .cls-2 {\n fill: url(#linear-gradient-2);\n }\n\n .cls-3 {\n fill: url(#linear-gradient);\n fill-rule: evenodd;\n }\n </style>\n <linearGradient id=\"linear-gradient\" x1=\"115.59\" y1=\"112.81\" x2=\"25.08\" y2=\"22.3\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\".3\" stop-color=\"#f49f0a\"/>\n <stop offset=\".79\" stop-color=\"#fb7a4d\"/>\n <stop offset=\"1\" stop-color=\"#ff6b6c\"/>\n </linearGradient>\n <linearGradient id=\"linear-gradient-2\" x1=\"360.91\" y1=\"152.01\" x2=\"202.32\" y2=\"-6.59\" xlink:href=\"#linear-gradient\"/>\n <linearGradient id=\"linear-gradient-3\" x1=\"443.91\" y1=\"30.15\" x2=\"531.36\" y2=\"117.59\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\".43\" stop-color=\"#00a6a6\"/>\n <stop offset=\"1\" stop-color=\"#1238dd\"/>\n </linearGradient>\n </defs>\n <path class=\"cls-3\" d=\"M48.28,14.96c-12.39,5.21-22.54,14.64-28.65,26.61-6.12,11.97-7.8,25.72-4.77,38.81,3.04,13.09,10.6,24.69,21.36,32.75,10.76,8.06,24.02,12.05,37.44,11.28,13.42-.77,26.13-6.26,35.9-15.5,9.76-9.24,15.95-21.63,17.46-34.99,1.51-13.36-1.74-26.82-9.19-38.01-1.07-1.61-.64-3.78.97-4.85,1.61-1.07,3.78-.64,4.85.97,8.36,12.56,12.02,27.68,10.32,42.67-1.7,15-8.64,28.91-19.61,39.28-10.96,10.37-25.24,16.54-40.31,17.4-15.07.87-29.96-3.62-42.04-12.66-12.08-9.05-20.58-22.07-23.99-36.77-3.41-14.7-1.51-30.14,5.35-43.58,6.87-13.44,18.26-24.02,32.17-29.87,13.91-5.85,29.44-6.6,43.85-2.11,1.85.57,2.88,2.54,2.3,4.38-.57,1.85-2.54,2.88-4.38,2.3-12.83-4-26.67-3.33-39.06,1.88ZM111.39,19.75c0,2.07-1.68,3.75-3.75,3.75s-3.75-1.68-3.75-3.75,1.68-3.75,3.75-3.75,3.75,1.68,3.75,3.75ZM64.64,59.93c0,1.91,2.39,2.56,7.69,3.88,3.89.97,6.6,2.18,8.15,3.63,1.53,1.45,2.29,3.53,2.29,6.25,0,3.57-1.03,6.26-3.11,8.08-2.07,1.82-5.09,2.73-9.09,2.73h-9.6c-1.97,0-3.57-1.6-3.59-3.57-.01-1.99,1.6-3.61,3.59-3.61h9.41c3.15-.12,4.79-.95,4.91-2.47,0-1.3-1.03-2.21-3.07-2.73-6.91-1.72-11.11-3.44-12.6-5.15-1.48-1.71-2.23-3.77-2.23-6.19,0-6.59,3.2-9.85,9.59-9.8h10.77c1.99,0,3.6,1.61,3.6,3.59s-1.61,3.59-3.6,3.59h-9.69c-1.83,0-3.43.06-3.43,1.77Z\"/>\n <path class=\"cls-2\" d=\"M205.52,48.44h-8.86c-.44-3.75-2.23-6.65-5.38-8.72-3.16-2.07-7.03-3.1-11.6-3.1h0c-3.35,0-6.27.54-8.78,1.62-2.49,1.09-4.44,2.59-5.84,4.48-1.39,1.89-2.08,4.05-2.08,6.46h0c0,2.01.49,3.75,1.46,5.2.97,1.44,2.22,2.63,3.74,3.58,1.53.95,3.13,1.72,4.8,2.32,1.68.6,3.22,1.09,4.62,1.46h0l7.68,2.06c1.97.52,4.17,1.23,6.6,2.14,2.43.92,4.75,2.16,6.98,3.72,2.23,1.56,4.07,3.56,5.52,6,1.45,2.44,2.18,5.43,2.18,8.98h0c0,4.08-1.07,7.77-3.2,11.08-2.12,3.29-5.22,5.91-9.3,7.86-4.08,1.95-9.02,2.92-14.82,2.92h0c-5.43,0-10.11-.87-14.06-2.62-3.95-1.75-7.05-4.19-9.3-7.32-2.25-3.12-3.53-6.75-3.84-10.88h9.46c.25,2.85,1.22,5.21,2.9,7.06,1.69,1.87,3.83,3.25,6.42,4.14,2.6.89,5.41,1.34,8.42,1.34h0c3.49,0,6.63-.57,9.4-1.72,2.79-1.13,4.99-2.73,6.62-4.8,1.63-2.05,2.44-4.46,2.44-7.22h0c0-2.51-.7-4.55-2.1-6.12-1.41-1.57-3.26-2.85-5.54-3.84-2.29-.99-4.77-1.85-7.44-2.58h0l-9.3-2.66c-5.91-1.71-10.59-4.13-14.04-7.28-3.44-3.16-5.16-7.29-5.16-12.38h0c0-4.23,1.15-7.93,3.46-11.1,2.29-3.16,5.39-5.62,9.3-7.38,3.91-1.76,8.27-2.64,13.08-2.64h0c4.88,0,9.21.87,13,2.6,3.8,1.73,6.81,4.11,9.04,7.12,2.23,3,3.4,6.41,3.52,10.22h0ZM229.16,105.18h-8.72v-56.74h8.42v8.86h.74c1.19-3.03,3.1-5.38,5.74-7.06,2.63-1.69,5.79-2.54,9.48-2.54h0c3.75,0,6.87.85,9.36,2.54,2.51,1.68,4.46,4.03,5.86,7.06h.58c1.45-2.92,3.63-5.25,6.54-7,2.91-1.73,6.39-2.6,10.46-2.6h0c5.07,0,9.21,1.58,12.44,4.74,3.23,3.17,4.84,8.09,4.84,14.76h0v37.98h-8.72v-37.98c0-4.19-1.14-7.18-3.42-8.98-2.29-1.79-4.99-2.68-8.1-2.68h0c-3.99,0-7.07,1.2-9.26,3.6-2.2,2.4-3.3,5.43-3.3,9.1h0v36.94h-8.86v-38.86c0-3.23-1.05-5.83-3.14-7.82-2.09-1.97-4.79-2.96-8.08-2.96h0c-2.27,0-4.38.6-6.34,1.8-1.96,1.21-3.53,2.88-4.72,5-1.2,2.13-1.8,4.59-1.8,7.38h0v35.46ZM333.9,106.36h0c-5.12,0-9.61-1.22-13.46-3.66-3.85-2.44-6.86-5.85-9.02-10.24-2.15-4.37-3.22-9.49-3.22-15.36h0c0-5.91,1.07-11.07,3.22-15.48,2.16-4.4,5.17-7.82,9.02-10.26,3.85-2.44,8.34-3.66,13.46-3.66h0c5.12,0,9.61,1.22,13.46,3.66,3.85,2.44,6.86,5.86,9.02,10.26,2.15,4.41,3.22,9.57,3.22,15.48h0c0,5.87-1.07,10.99-3.22,15.36-2.16,4.39-5.17,7.8-9.02,10.24-3.85,2.44-8.34,3.66-13.46,3.66ZM333.9,98.52h0c3.89,0,7.09-.99,9.6-2.98,2.52-2,4.38-4.63,5.58-7.88,1.21-3.25,1.82-6.77,1.82-10.56h0c0-3.79-.61-7.32-1.82-10.6-1.2-3.27-3.06-5.91-5.58-7.94-2.51-2.01-5.71-3.02-9.6-3.02h0c-3.89,0-7.09,1.01-9.6,3.02-2.51,2.03-4.37,4.67-5.58,7.94-1.2,3.28-1.8,6.81-1.8,10.6h0c0,3.79.6,7.31,1.8,10.56,1.21,3.25,3.07,5.88,5.58,7.88,2.51,1.99,5.71,2.98,9.6,2.98ZM395.94,106.36h0c-5.12,0-9.61-1.22-13.46-3.66-3.85-2.44-6.85-5.85-9-10.24-2.16-4.37-3.24-9.49-3.24-15.36h0c0-5.91,1.08-11.07,3.24-15.48,2.15-4.4,5.15-7.82,9-10.26,3.85-2.44,8.34-3.66,13.46-3.66h0c5.12,0,9.61,1.22,13.46,3.66,3.85,2.44,6.86,5.86,9.02,10.26,2.16,4.41,3.24,9.57,3.24,15.48h0c0,5.87-1.08,10.99-3.24,15.36-2.16,4.39-5.17,7.8-9.02,10.24-3.85,2.44-8.34,3.66-13.46,3.66ZM395.94,98.52h0c3.89,0,7.09-.99,9.6-2.98,2.52-2,4.38-4.63,5.58-7.88,1.21-3.25,1.82-6.77,1.82-10.56h0c0-3.79-.61-7.32-1.82-10.6-1.2-3.27-3.06-5.91-5.58-7.94-2.51-2.01-5.71-3.02-9.6-3.02h0c-3.88,0-7.08,1.01-9.6,3.02-2.51,2.03-4.37,4.67-5.58,7.94-1.2,3.28-1.8,6.81-1.8,10.6h0c0,3.79.6,7.31,1.8,10.56,1.21,3.25,3.07,5.88,5.58,7.88,2.52,1.99,5.72,2.98,9.6,2.98Z\"/>\n <path class=\"cls-1\" d=\"M467.88,48.02v13.28h-35.79v-13.28h35.79ZM439.68,34.38h17.89v53.42c0,1.5.36,2.62,1.08,3.36.72.74,1.88,1.1,3.49,1.1.62,0,1.48-.07,2.59-.21,1.11-.14,1.91-.27,2.38-.41l2.31,13.02c-2.02.58-3.97.97-5.84,1.18-1.88.21-3.66.31-5.33.31-6.08,0-10.7-1.43-13.84-4.28-3.15-2.85-4.72-7.01-4.72-12.48v-55.01ZM506.59,72.63v32.71h-17.89V28.95h17.53v33.53h-1.13c1.4-4.55,3.6-8.21,6.59-11,2.99-2.79,7.01-4.18,12.07-4.18,4,0,7.48.89,10.46,2.67,2.97,1.78,5.28,4.29,6.92,7.54,1.64,3.25,2.46,7.02,2.46,11.33v36.5h-17.89v-33.02c0-3.21-.82-5.73-2.46-7.56-1.64-1.83-3.93-2.74-6.87-2.74-1.92,0-3.62.42-5.1,1.26-1.49.84-2.64,2.04-3.46,3.61-.82,1.57-1.23,3.49-1.23,5.74Z\"/>\n</svg>";
|
|
822
876
|
//#endregion
|
|
877
|
+
//#region src/markdown.ts
|
|
878
|
+
/**
|
|
879
|
+
* A tiny, safe-by-default Markdown → HTML renderer for the chat widget.
|
|
880
|
+
*
|
|
881
|
+
* ## Why a hand-rolled renderer (and not markdown-it / snarkdown)?
|
|
882
|
+
*
|
|
883
|
+
* The widget renders **untrusted** text in two places: the assistant's reply
|
|
884
|
+
* (LLM output, which can echo attacker-supplied content) and citation snippets
|
|
885
|
+
* (raw scraped page chunks). Today both are written via `textContent`, so
|
|
886
|
+
* `**bold**`, numbered lists, and `[links](url)` show up literally. We want
|
|
887
|
+
* them rendered — without re-opening the XSS hole that `textContent` was
|
|
888
|
+
* guarding against.
|
|
889
|
+
*
|
|
890
|
+
* markdown-it with `html:false` is safe-by-default but ships ~30 kB min into
|
|
891
|
+
* what is an embeddable **global** bundle, where every kilobyte is on the host
|
|
892
|
+
* page's critical path. snarkdown is ~1 kB but emits raw HTML, so it would
|
|
893
|
+
* require bolting on a separate sanitizer. Instead, this renderer is
|
|
894
|
+
* **safe-by-construction**:
|
|
895
|
+
*
|
|
896
|
+
* 1. It is a *tokenizer*, not an HTML passthrough. It only ever emits a
|
|
897
|
+
* fixed allowlist of tags (`p`, `br`, `strong`, `em`, `ul`/`ol`/`li`,
|
|
898
|
+
* `code`/`pre`, `a`, `blockquote`). There is no code path that copies a
|
|
899
|
+
* tag out of the input — a literal `<script>` in the input is treated as
|
|
900
|
+
* plain text.
|
|
901
|
+
* 2. **Every** text run is HTML-escaped via {@link escapeHtml} before it
|
|
902
|
+
* reaches the output. Raw `<`, `>`, `&`, `"`, `'` can never become markup.
|
|
903
|
+
* 3. **Images are dropped entirely** — `` renders as its alt text,
|
|
904
|
+
* no `<img>` is ever produced (a scraped tracking pixel must not load).
|
|
905
|
+
* 4. **Links** are gated through {@link safeHttpUrl}: only absolute `http(s)`
|
|
906
|
+
* URLs become anchors (with `target="_blank"` + a hardened `rel`);
|
|
907
|
+
* `javascript:`/`data:`/relative/etc. fall back to plain (escaped) text.
|
|
908
|
+
* 5. **Headings** (`#`..`######`) are *downgraded* to bold lines — a full
|
|
909
|
+
* `<h1>` is far too large inside a chat bubble or citation card.
|
|
910
|
+
*
|
|
911
|
+
* The output is a string of HTML that is only ever assigned to `innerHTML` of
|
|
912
|
+
* an element the caller controls; because of (1)–(4) it can only contain the
|
|
913
|
+
* allowlisted, attribute-sanitized tags.
|
|
914
|
+
*
|
|
915
|
+
* Supported subset (deliberately small):
|
|
916
|
+
* - Paragraphs (blank-line separated) and hard/soft line breaks
|
|
917
|
+
* - `**bold**` / `__bold__`, `*italic*` / `_italic_`
|
|
918
|
+
* - `` `inline code` `` and fenced ``` ```code blocks``` ```
|
|
919
|
+
* - `- ` / `* ` / `+ ` unordered lists, `1.` ordered lists
|
|
920
|
+
* - `> ` blockquotes
|
|
921
|
+
* - `[text](http(s)://url)` links (images dropped to alt text)
|
|
922
|
+
* - `#`..`######` headings → bold line
|
|
923
|
+
*/
|
|
924
|
+
/** Escape the five HTML-significant characters so a text run can never be markup. */
|
|
925
|
+
function escapeHtml(value) {
|
|
926
|
+
return value.replace(/[&<>"']/g, (c) => {
|
|
927
|
+
switch (c) {
|
|
928
|
+
case "&": return "&";
|
|
929
|
+
case "<": return "<";
|
|
930
|
+
case ">": return ">";
|
|
931
|
+
case "\"": return """;
|
|
932
|
+
default: return "'";
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Return `url` only if it is a valid absolute `http(s)` URL, else `null`.
|
|
938
|
+
*
|
|
939
|
+
* SECURITY: link targets here originate from untrusted content (LLM output /
|
|
940
|
+
* scraped citation chunks). Allowing an arbitrary string as an `href` permits
|
|
941
|
+
* `javascript:`/`data:`/`vbscript:` URLs that execute on click — a stored-XSS
|
|
942
|
+
* vector. Only absolute http(s) links are rendered as anchors; anything else
|
|
943
|
+
* falls back to plain text upstream.
|
|
944
|
+
*/
|
|
945
|
+
function safeHttpUrl(url) {
|
|
946
|
+
if (!url) return null;
|
|
947
|
+
try {
|
|
948
|
+
const parsed = new URL(url);
|
|
949
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : null;
|
|
950
|
+
} catch {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Render the inline span grammar of a single line/segment to safe HTML.
|
|
956
|
+
*
|
|
957
|
+
* Order matters: code spans are extracted first (their contents are *not*
|
|
958
|
+
* further parsed), then images are stripped to alt text, then links, then
|
|
959
|
+
* emphasis. Every literal text run is escaped on the way out.
|
|
960
|
+
*/
|
|
961
|
+
function renderInline(input) {
|
|
962
|
+
let out = "";
|
|
963
|
+
let i = 0;
|
|
964
|
+
const n = input.length;
|
|
965
|
+
let buf = "";
|
|
966
|
+
const flush = () => {
|
|
967
|
+
if (buf) {
|
|
968
|
+
out += escapeHtml(buf);
|
|
969
|
+
buf = "";
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
while (i < n) {
|
|
973
|
+
const ch = input[i];
|
|
974
|
+
if (ch === "`") {
|
|
975
|
+
const end = input.indexOf("`", i + 1);
|
|
976
|
+
if (end > i) {
|
|
977
|
+
flush();
|
|
978
|
+
out += `<code>${escapeHtml(input.slice(i + 1, end))}</code>`;
|
|
979
|
+
i = end + 1;
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (ch === "!" && input[i + 1] === "[") {
|
|
984
|
+
const m = imageAt(input, i);
|
|
985
|
+
if (m) {
|
|
986
|
+
flush();
|
|
987
|
+
out += renderInline(m.alt);
|
|
988
|
+
i = m.end;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (ch === "[") {
|
|
993
|
+
const m = linkAt(input, i);
|
|
994
|
+
if (m) {
|
|
995
|
+
flush();
|
|
996
|
+
const safe = safeHttpUrl(m.href);
|
|
997
|
+
const inner = renderInline(m.text);
|
|
998
|
+
if (safe) out += `<a href="${escapeHtml(safe)}" target="_blank" rel="noopener noreferrer nofollow">${inner}</a>`;
|
|
999
|
+
else out += inner;
|
|
1000
|
+
i = m.end;
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
if (ch === "*" && input[i + 1] === "*" || ch === "_" && input[i + 1] === "_") {
|
|
1005
|
+
const marker = ch + ch;
|
|
1006
|
+
const end = input.indexOf(marker, i + 2);
|
|
1007
|
+
if (end > i + 1) {
|
|
1008
|
+
flush();
|
|
1009
|
+
out += `<strong>${renderInline(input.slice(i + 2, end))}</strong>`;
|
|
1010
|
+
i = end + 2;
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (ch === "*" || ch === "_") {
|
|
1015
|
+
const end = input.indexOf(ch, i + 1);
|
|
1016
|
+
if (end > i + 1 && input[i + 1] !== ch) {
|
|
1017
|
+
flush();
|
|
1018
|
+
out += `<em>${renderInline(input.slice(i + 1, end))}</em>`;
|
|
1019
|
+
i = end + 1;
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
buf += ch;
|
|
1024
|
+
i++;
|
|
1025
|
+
}
|
|
1026
|
+
flush();
|
|
1027
|
+
return out;
|
|
1028
|
+
}
|
|
1029
|
+
/** Parse a `[text](href)` link starting at `start` (`input[start] === '['`). */
|
|
1030
|
+
function linkAt(input, start) {
|
|
1031
|
+
const close = matchBracket(input, start);
|
|
1032
|
+
if (close < 0 || input[close + 1] !== "(") return null;
|
|
1033
|
+
const paren = input.indexOf(")", close + 2);
|
|
1034
|
+
if (paren < 0) return null;
|
|
1035
|
+
return {
|
|
1036
|
+
text: input.slice(start + 1, close),
|
|
1037
|
+
href: input.slice(close + 2, paren).trim().split(/\s+/)[0] ?? "",
|
|
1038
|
+
end: paren + 1
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
/** Parse a `` image starting at `start` (`input[start] === '!'`). */
|
|
1042
|
+
function imageAt(input, start) {
|
|
1043
|
+
const link = linkAt(input, start + 1);
|
|
1044
|
+
if (!link) return null;
|
|
1045
|
+
return {
|
|
1046
|
+
alt: link.text,
|
|
1047
|
+
end: link.end
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
/** Find the matching `]` for a `[` at `open`, honoring one level of nesting. */
|
|
1051
|
+
function matchBracket(input, open) {
|
|
1052
|
+
let depth = 0;
|
|
1053
|
+
for (let i = open; i < input.length; i++) {
|
|
1054
|
+
const c = input[i];
|
|
1055
|
+
if (c === "[") depth++;
|
|
1056
|
+
else if (c === "]") {
|
|
1057
|
+
depth--;
|
|
1058
|
+
if (depth === 0) return i;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return -1;
|
|
1062
|
+
}
|
|
1063
|
+
const UL_RE = /^\s*[-*+]\s+(.*)$/;
|
|
1064
|
+
const OL_RE = /^\s*\d+[.)]\s+(.*)$/;
|
|
1065
|
+
const HEADING_RE = /^\s{0,3}(#{1,6})\s+(.*)$/;
|
|
1066
|
+
const QUOTE_RE = /^\s*>\s?(.*)$/;
|
|
1067
|
+
const FENCE_RE = /^\s*(`{3,}|~{3,})\s*(.*)$/;
|
|
1068
|
+
/**
|
|
1069
|
+
* Render a full Markdown string to safe HTML.
|
|
1070
|
+
*
|
|
1071
|
+
* @returns a string containing only the allowlisted tags described in the
|
|
1072
|
+
* module doc. Safe to assign to `innerHTML` of a caller-owned element.
|
|
1073
|
+
*/
|
|
1074
|
+
function renderMarkdown(src) {
|
|
1075
|
+
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
1076
|
+
const out = [];
|
|
1077
|
+
let i = 0;
|
|
1078
|
+
while (i < lines.length) {
|
|
1079
|
+
const line = lines[i];
|
|
1080
|
+
const fence = FENCE_RE.exec(line);
|
|
1081
|
+
if (fence) {
|
|
1082
|
+
const marker = fence[1];
|
|
1083
|
+
const body = [];
|
|
1084
|
+
i++;
|
|
1085
|
+
while (i < lines.length && !lines[i].trimStart().startsWith(marker)) {
|
|
1086
|
+
body.push(lines[i]);
|
|
1087
|
+
i++;
|
|
1088
|
+
}
|
|
1089
|
+
if (i < lines.length) i++;
|
|
1090
|
+
out.push(`<pre><code>${escapeHtml(body.join("\n"))}</code></pre>`);
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (line.trim() === "") {
|
|
1094
|
+
i++;
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
const heading = HEADING_RE.exec(line);
|
|
1098
|
+
if (heading) {
|
|
1099
|
+
out.push(`<p><strong>${renderInline(heading[2])}</strong></p>`);
|
|
1100
|
+
i++;
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
if (UL_RE.test(line) || OL_RE.test(line)) {
|
|
1104
|
+
const ordered = OL_RE.test(line) && !UL_RE.test(line);
|
|
1105
|
+
const re = ordered ? OL_RE : UL_RE;
|
|
1106
|
+
const items = [];
|
|
1107
|
+
while (i < lines.length) {
|
|
1108
|
+
const m = re.exec(lines[i]);
|
|
1109
|
+
if (!m) break;
|
|
1110
|
+
items.push(`<li>${renderInline(m[1])}</li>`);
|
|
1111
|
+
i++;
|
|
1112
|
+
}
|
|
1113
|
+
out.push(`<${ordered ? "ol" : "ul"}>${items.join("")}</${ordered ? "ol" : "ul"}>`);
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
if (QUOTE_RE.test(line)) {
|
|
1117
|
+
const quoted = [];
|
|
1118
|
+
while (i < lines.length) {
|
|
1119
|
+
const m = QUOTE_RE.exec(lines[i]);
|
|
1120
|
+
if (!m) break;
|
|
1121
|
+
quoted.push(m[1]);
|
|
1122
|
+
i++;
|
|
1123
|
+
}
|
|
1124
|
+
out.push(`<blockquote>${renderInline(quoted.join("\n")).replace(/\n/g, "<br>")}</blockquote>`);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
const para = [];
|
|
1128
|
+
while (i < lines.length) {
|
|
1129
|
+
const l = lines[i];
|
|
1130
|
+
if (l.trim() === "" || FENCE_RE.test(l) || HEADING_RE.test(l) || UL_RE.test(l) || OL_RE.test(l) || QUOTE_RE.test(l)) break;
|
|
1131
|
+
para.push(l);
|
|
1132
|
+
i++;
|
|
1133
|
+
}
|
|
1134
|
+
out.push(`<p>${renderInline(para.join("\n")).replace(/\n/g, "<br>")}</p>`);
|
|
1135
|
+
}
|
|
1136
|
+
return out.join("");
|
|
1137
|
+
}
|
|
1138
|
+
const SNIPPET_MAX = 260;
|
|
1139
|
+
/**
|
|
1140
|
+
* Clean a raw scraped citation snippet into a short, readable excerpt.
|
|
1141
|
+
*
|
|
1142
|
+
* Scraped chunks frequently begin with page boilerplate — a logo image wrapped
|
|
1143
|
+
* in a link, standalone nav, repeated whitespace — e.g.
|
|
1144
|
+
* `[](…) # Our Work We build…`. The source itself is already linked
|
|
1145
|
+
* from the citation card, so the snippet only needs to be a clean teaser.
|
|
1146
|
+
*
|
|
1147
|
+
* Steps:
|
|
1148
|
+
* 1. Strip a leading image / logo-link (`[](…)` or ``).
|
|
1149
|
+
* 2. Drop a leading standalone heading marker (`#`/`##`).
|
|
1150
|
+
* 3. Collapse all runs of whitespace to single spaces.
|
|
1151
|
+
* 4. Truncate to ~{@link SNIPPET_MAX} chars at a word boundary, adding `…`.
|
|
1152
|
+
*
|
|
1153
|
+
* The result is still rendered through {@link renderMarkdown} downstream, so any
|
|
1154
|
+
* remaining inline markup (bold/links) stays safe.
|
|
1155
|
+
*/
|
|
1156
|
+
function cleanCitationSnippet(raw) {
|
|
1157
|
+
let s = raw ?? "";
|
|
1158
|
+
let changed = true;
|
|
1159
|
+
while (changed) {
|
|
1160
|
+
changed = false;
|
|
1161
|
+
const before = s;
|
|
1162
|
+
s = s.replace(/^\s*\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)\s*/, "");
|
|
1163
|
+
s = s.replace(/^\s*!\[[^\]]*\]\([^)]*\)\s*/, "");
|
|
1164
|
+
s = s.replace(/^\s*#{1,6}\s+/, "");
|
|
1165
|
+
if (s !== before) changed = true;
|
|
1166
|
+
}
|
|
1167
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
1168
|
+
if (s.length > SNIPPET_MAX) {
|
|
1169
|
+
const cut = s.slice(0, SNIPPET_MAX);
|
|
1170
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
1171
|
+
s = (lastSpace > SNIPPET_MAX * .6 ? cut.slice(0, lastSpace) : cut).trimEnd() + "…";
|
|
1172
|
+
}
|
|
1173
|
+
return s;
|
|
1174
|
+
}
|
|
1175
|
+
//#endregion
|
|
823
1176
|
//#region src/styles.ts
|
|
824
1177
|
/**
|
|
825
1178
|
* Render the widget's scoped stylesheet — the "Aurora Glass" design system.
|
|
@@ -1144,6 +1497,50 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1144
1497
|
}
|
|
1145
1498
|
@keyframes sac-blink { to { opacity: 0 } }
|
|
1146
1499
|
|
|
1500
|
+
/* ─────────────── Rendered markdown (assistant bubbles / snippets) ─────────── */
|
|
1501
|
+
/* The renderer (markdown.ts) emits a small allowlisted set of tags; these rules
|
|
1502
|
+
keep them legible inside the tight Aurora-Glass bubble + citation card. */
|
|
1503
|
+
/* Block-level markdown drives its own spacing/wrapping, so opt out of the
|
|
1504
|
+
bubble's pre-wrap (which would otherwise add stray blank lines). */
|
|
1505
|
+
.bubble.md { white-space: normal; }
|
|
1506
|
+
.md > :first-child { margin-top: 0; }
|
|
1507
|
+
.md > :last-child { margin-bottom: 0; }
|
|
1508
|
+
.md p { margin: 0 0 8px; }
|
|
1509
|
+
.md ul, .md ol { margin: 6px 0 8px; padding-left: 20px; }
|
|
1510
|
+
.md li { margin: 2px 0; }
|
|
1511
|
+
.md li::marker { color: color-mix(in srgb, var(--sac-primary) 75%, transparent); }
|
|
1512
|
+
.md a {
|
|
1513
|
+
color: color-mix(in srgb, var(--sac-primary) 92%, #fff);
|
|
1514
|
+
text-decoration: underline;
|
|
1515
|
+
text-underline-offset: 2px;
|
|
1516
|
+
word-break: break-word;
|
|
1517
|
+
}
|
|
1518
|
+
.md a:hover { text-decoration: none; }
|
|
1519
|
+
.md strong { font-weight: 700; }
|
|
1520
|
+
.md em { font-style: italic; }
|
|
1521
|
+
.md code {
|
|
1522
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1523
|
+
font-size: .9em;
|
|
1524
|
+
padding: 1px 5px;
|
|
1525
|
+
border-radius: 5px;
|
|
1526
|
+
background: color-mix(in srgb, var(--sac-text) 10%, transparent);
|
|
1527
|
+
}
|
|
1528
|
+
.md pre {
|
|
1529
|
+
margin: 6px 0 8px;
|
|
1530
|
+
padding: 9px 11px;
|
|
1531
|
+
border-radius: 9px;
|
|
1532
|
+
overflow-x: auto;
|
|
1533
|
+
background: color-mix(in srgb, var(--sac-text) 9%, transparent);
|
|
1534
|
+
border: 1px solid color-mix(in srgb, var(--sac-text) 8%, transparent);
|
|
1535
|
+
}
|
|
1536
|
+
.md pre code { padding: 0; background: none; font-size: 12px; line-height: 1.45; }
|
|
1537
|
+
.md blockquote {
|
|
1538
|
+
margin: 6px 0;
|
|
1539
|
+
padding: 2px 0 2px 11px;
|
|
1540
|
+
border-left: 2px solid color-mix(in srgb, var(--sac-primary) 55%, transparent);
|
|
1541
|
+
color: color-mix(in srgb, var(--sac-text) 78%, transparent);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1147
1544
|
/* Full-page: center the conversation in a readable column. */
|
|
1148
1545
|
.panel.fullpage .messages { padding: 26px 20px; }
|
|
1149
1546
|
.panel.fullpage .row { max-width: 760px; width: 100%; margin-left: auto; margin-right: auto; }
|
|
@@ -1422,53 +1819,37 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1422
1819
|
/** Tool-confirmation interrupt — a shield. */
|
|
1423
1820
|
shield: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3 5 6v5c0 4.4 3 7.2 7 8.5 4-1.3 7-4.1 7-8.5V6l-7-3Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="m9 11.5 2 2 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>`
|
|
1424
1821
|
};
|
|
1425
|
-
/**
|
|
1426
|
-
* Return `url` only if it is a valid absolute `http(s)` URL, else `null`.
|
|
1427
|
-
*
|
|
1428
|
-
* SECURITY: citation URLs originate from indexed content (web / GitHub
|
|
1429
|
-
* connectors), which can be attacker-influenceable. Assigning an arbitrary
|
|
1430
|
-
* string to `<a>.href` allows `javascript:`/`data:`/`vbscript:` URLs that
|
|
1431
|
-
* execute on click — a stored-XSS vector. Only http(s) links are rendered as
|
|
1432
|
-
* anchors; anything else falls back to plain text.
|
|
1433
|
-
*/
|
|
1434
|
-
function safeHttpUrl(url) {
|
|
1435
|
-
if (!url) return null;
|
|
1436
|
-
try {
|
|
1437
|
-
const parsed = new URL(url);
|
|
1438
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : null;
|
|
1439
|
-
} catch {
|
|
1440
|
-
return null;
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
1822
|
var SmoothAgentChatElement = class extends HTMLElement {
|
|
1444
1823
|
static get observedAttributes() {
|
|
1445
1824
|
return OBSERVED;
|
|
1446
1825
|
}
|
|
1447
|
-
root;
|
|
1448
|
-
controller = null;
|
|
1449
|
-
overrides = {};
|
|
1450
|
-
open = false;
|
|
1451
|
-
messages = [];
|
|
1452
|
-
status = "idle";
|
|
1453
|
-
mounted = false;
|
|
1454
|
-
/** True once the visitor has cleared the pre-chat identity gate (or it's not needed). */
|
|
1455
|
-
userInfoSatisfied = false;
|
|
1456
|
-
/** True after the visitor has sent their first message (hides starter chips). */
|
|
1457
|
-
hasSent = false;
|
|
1458
|
-
/** Starter prompts shown as chips in the empty state. */
|
|
1459
|
-
examplePrompts = [];
|
|
1460
|
-
/** Current mid-turn interrupt (OTP / tool-confirmation), or null. */
|
|
1461
|
-
interrupt = null;
|
|
1462
|
-
interruptEl = null;
|
|
1463
|
-
panelEl = null;
|
|
1464
|
-
launcherEl = null;
|
|
1465
|
-
messagesEl = null;
|
|
1466
|
-
statusEl = null;
|
|
1467
|
-
dotEl = null;
|
|
1468
|
-
inputEl = null;
|
|
1469
|
-
sendBtn = null;
|
|
1470
1826
|
constructor() {
|
|
1471
1827
|
super();
|
|
1828
|
+
_defineProperty(this, "root", void 0);
|
|
1829
|
+
_defineProperty(this, "controller", null);
|
|
1830
|
+
_defineProperty(this, "overrides", {});
|
|
1831
|
+
_defineProperty(this, "open", false);
|
|
1832
|
+
_defineProperty(this, "messages", []);
|
|
1833
|
+
_defineProperty(this, "status", "idle");
|
|
1834
|
+
_defineProperty(this, "mounted", false);
|
|
1835
|
+
_defineProperty(this, "userInfoSatisfied", false);
|
|
1836
|
+
_defineProperty(this, "hasSent", false);
|
|
1837
|
+
_defineProperty(this, "examplePrompts", []);
|
|
1838
|
+
_defineProperty(this, "greeting", "");
|
|
1839
|
+
_defineProperty(this, "interrupt", null);
|
|
1840
|
+
_defineProperty(this, "interruptEl", null);
|
|
1841
|
+
_defineProperty(this, "panelEl", null);
|
|
1842
|
+
_defineProperty(this, "launcherEl", null);
|
|
1843
|
+
_defineProperty(this, "messagesEl", null);
|
|
1844
|
+
_defineProperty(this, "statusEl", null);
|
|
1845
|
+
_defineProperty(this, "dotEl", null);
|
|
1846
|
+
_defineProperty(this, "inputEl", null);
|
|
1847
|
+
_defineProperty(this, "sendBtn", null);
|
|
1848
|
+
_defineProperty(this, "streamBubbleEl", null);
|
|
1849
|
+
_defineProperty(this, "streamMsgId", null);
|
|
1850
|
+
_defineProperty(this, "streamTarget", "");
|
|
1851
|
+
_defineProperty(this, "displayedLength", 0);
|
|
1852
|
+
_defineProperty(this, "rafId", 0);
|
|
1472
1853
|
this.root = this.attachShadow({ mode: "open" });
|
|
1473
1854
|
}
|
|
1474
1855
|
connectedCallback() {
|
|
@@ -1477,6 +1858,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1477
1858
|
}
|
|
1478
1859
|
disconnectedCallback() {
|
|
1479
1860
|
this.mounted = false;
|
|
1861
|
+
this.resetReveal();
|
|
1480
1862
|
this.controller?.disconnect();
|
|
1481
1863
|
this.controller = null;
|
|
1482
1864
|
}
|
|
@@ -1545,8 +1927,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1545
1927
|
if (!this.controller) {
|
|
1546
1928
|
this.controller = new ConversationController(config, {
|
|
1547
1929
|
onMessages: (messages) => {
|
|
1548
|
-
this.messages
|
|
1549
|
-
this.renderMessages(resolved.greeting);
|
|
1930
|
+
this.handleMessages(messages, resolved.greeting);
|
|
1550
1931
|
},
|
|
1551
1932
|
onStatus: (status) => {
|
|
1552
1933
|
this.status = status;
|
|
@@ -1581,6 +1962,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1581
1962
|
<button class="close" aria-label="Close chat">${ICON.close}</button>
|
|
1582
1963
|
</div>`;
|
|
1583
1964
|
this.examplePrompts = resolved.examplePrompts;
|
|
1965
|
+
this.greeting = resolved.greeting;
|
|
1584
1966
|
const gating = needsUserInfo(resolved) && !this.userInfoSatisfied;
|
|
1585
1967
|
const field = (name, type, label, autocomplete) => `<label class="pc-field"><span>${escapeHtml(label)}</span><input name="${name}" type="${type}" autocomplete="${autocomplete}" required /></label>`;
|
|
1586
1968
|
const prechatHtml = `
|
|
@@ -1617,6 +1999,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1617
1999
|
`;
|
|
1618
2000
|
const logoSvg = container.querySelector(".logo-wrap svg");
|
|
1619
2001
|
if (logoSvg) logoSvg.setAttribute("class", "logo");
|
|
2002
|
+
this.resetReveal();
|
|
1620
2003
|
this.root.replaceChildren(style, container);
|
|
1621
2004
|
this.launcherEl = container.querySelector(".launcher");
|
|
1622
2005
|
this.panelEl = container.querySelector(".panel");
|
|
@@ -1643,7 +2026,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1643
2026
|
});
|
|
1644
2027
|
if (fullpage && !gating) this.controller?.connect().catch(() => {});
|
|
1645
2028
|
this.syncOpenState();
|
|
1646
|
-
if (!gating) this.renderMessages(
|
|
2029
|
+
if (!gating) this.renderMessages();
|
|
1647
2030
|
this.renderStatus();
|
|
1648
2031
|
this.renderComposerState();
|
|
1649
2032
|
this.renderInterrupt();
|
|
@@ -1774,9 +2157,35 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1774
2157
|
ta.style.height = "auto";
|
|
1775
2158
|
ta.style.height = `${ta.scrollHeight}px`;
|
|
1776
2159
|
}
|
|
1777
|
-
|
|
2160
|
+
/**
|
|
2161
|
+
* Receive a new message snapshot from the controller and update the view.
|
|
2162
|
+
*
|
|
2163
|
+
* `onMessages` fires on *every* `stream_token` delta. Rebuilding the whole
|
|
2164
|
+
* list each frame is wasteful and fights the smooth reveal, so we take the
|
|
2165
|
+
* cheap path when the only change is the trailing streaming assistant message
|
|
2166
|
+
* growing: just bump the reveal *target* (the rAF loop drains it). Any
|
|
2167
|
+
* structural change (new message, finalize, citations) triggers a full
|
|
2168
|
+
* rebuild via {@link renderMessages}.
|
|
2169
|
+
*/
|
|
2170
|
+
handleMessages(messages, greeting) {
|
|
2171
|
+
this.greeting = greeting;
|
|
2172
|
+
const prev = this.messages;
|
|
2173
|
+
const last = messages[messages.length - 1];
|
|
2174
|
+
const prevLast = prev[prev.length - 1];
|
|
2175
|
+
const structural = messages.length !== prev.length || !last || !prevLast || last.id !== prevLast.id || last.role !== prevLast.role || last.streaming !== prevLast.streaming || !!last.streaming && !prevLast.text && !!last.text;
|
|
2176
|
+
this.messages = messages;
|
|
2177
|
+
if (!structural && last && last.streaming && last.id === this.streamMsgId) {
|
|
2178
|
+
this.streamTarget = last.text;
|
|
2179
|
+
this.ensureRevealLoop();
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
this.renderMessages();
|
|
2183
|
+
}
|
|
2184
|
+
renderMessages() {
|
|
1778
2185
|
if (!this.messagesEl) return;
|
|
2186
|
+
this.resetReveal();
|
|
1779
2187
|
this.messagesEl.replaceChildren();
|
|
2188
|
+
const greeting = this.greeting;
|
|
1780
2189
|
if (this.messages.length === 0 && greeting) this.messagesEl.appendChild(this.buildRow("assistant", this.greetingBubble(greeting)));
|
|
1781
2190
|
if (!this.hasSent && this.messages.length === 0 && this.examplePrompts.length > 0) {
|
|
1782
2191
|
const chips = document.createElement("div");
|
|
@@ -1799,12 +2208,94 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1799
2208
|
bubble.append(this.typingDot(), this.typingDot(), this.typingDot());
|
|
1800
2209
|
} else if (msg.streaming) {
|
|
1801
2210
|
bubble.classList.add("cursor");
|
|
1802
|
-
|
|
2211
|
+
this.bindReveal(msg, bubble);
|
|
2212
|
+
} else if (msg.role === "assistant") {
|
|
2213
|
+
bubble.classList.add("md");
|
|
2214
|
+
bubble.innerHTML = renderMarkdown(msg.text);
|
|
1803
2215
|
} else bubble.textContent = msg.text;
|
|
1804
2216
|
this.messagesEl.appendChild(this.buildRow(msg.role, bubble));
|
|
1805
2217
|
if (msg.role === "assistant" && !msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
|
|
1806
2218
|
}
|
|
1807
|
-
this.
|
|
2219
|
+
this.scrollToBottom(true);
|
|
2220
|
+
}
|
|
2221
|
+
/** True when the host/user prefers reduced motion (snap, no typewriter). */
|
|
2222
|
+
prefersReducedMotion() {
|
|
2223
|
+
return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Bind the rAF typewriter to a freshly built streaming bubble. Preserves the
|
|
2227
|
+
* already-revealed prefix across rebuilds (so a structural rebuild mid-stream
|
|
2228
|
+
* doesn't restart the reveal from zero), then resumes the loop.
|
|
2229
|
+
*/
|
|
2230
|
+
bindReveal(msg, bubble) {
|
|
2231
|
+
const carryOver = msg.id === this.streamMsgId ? Math.min(this.displayedLength, msg.text.length) : 0;
|
|
2232
|
+
this.streamBubbleEl = bubble;
|
|
2233
|
+
this.streamMsgId = msg.id;
|
|
2234
|
+
this.streamTarget = msg.text;
|
|
2235
|
+
this.displayedLength = carryOver;
|
|
2236
|
+
if (this.prefersReducedMotion()) {
|
|
2237
|
+
this.displayedLength = msg.text.length;
|
|
2238
|
+
bubble.textContent = msg.text;
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
bubble.textContent = msg.text.slice(0, this.displayedLength);
|
|
2242
|
+
this.ensureRevealLoop();
|
|
2243
|
+
}
|
|
2244
|
+
/** Start the rAF loop if it isn't already running. */
|
|
2245
|
+
ensureRevealLoop() {
|
|
2246
|
+
if (this.prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
|
|
2247
|
+
this.snapReveal();
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
if (this.rafId) return;
|
|
2251
|
+
this.rafId = requestAnimationFrame(() => this.tickReveal());
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* One animation frame of the reveal. Advances `displayedLength` toward the
|
|
2255
|
+
* buffered target at an ADAPTIVE rate — the deeper the backlog, the more
|
|
2256
|
+
* chars per frame — so the reveal stays smooth yet never falls behind the
|
|
2257
|
+
* network. Updates ONLY the bound bubble's textContent (no list rebuild).
|
|
2258
|
+
*/
|
|
2259
|
+
tickReveal() {
|
|
2260
|
+
this.rafId = 0;
|
|
2261
|
+
const bubble = this.streamBubbleEl;
|
|
2262
|
+
if (!bubble) return;
|
|
2263
|
+
const target = this.streamTarget;
|
|
2264
|
+
const remaining = target.length - this.displayedLength;
|
|
2265
|
+
if (remaining <= 0) return;
|
|
2266
|
+
const step = Math.max(2, Math.min(remaining, Math.ceil(remaining / 8)));
|
|
2267
|
+
this.displayedLength = Math.min(target.length, this.displayedLength + step);
|
|
2268
|
+
bubble.textContent = target.slice(0, this.displayedLength);
|
|
2269
|
+
this.scrollToBottom(false);
|
|
2270
|
+
this.rafId = requestAnimationFrame(() => this.tickReveal());
|
|
2271
|
+
}
|
|
2272
|
+
/** Snap the bound bubble to the full buffered text immediately (reduced-motion / no-rAF). */
|
|
2273
|
+
snapReveal() {
|
|
2274
|
+
if (this.streamBubbleEl) {
|
|
2275
|
+
this.displayedLength = this.streamTarget.length;
|
|
2276
|
+
this.streamBubbleEl.textContent = this.streamTarget;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
/** Cancel any in-flight reveal loop and clear its state (called on full rebuild). */
|
|
2280
|
+
resetReveal() {
|
|
2281
|
+
if (this.rafId && typeof cancelAnimationFrame === "function") cancelAnimationFrame(this.rafId);
|
|
2282
|
+
this.rafId = 0;
|
|
2283
|
+
this.streamBubbleEl = null;
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Auto-scroll the message list to the bottom — but don't fight a visitor who
|
|
2287
|
+
* has scrolled up to read history. When `force` (a structural rebuild) we
|
|
2288
|
+
* always pin to bottom; during the streaming reveal we only follow if the
|
|
2289
|
+
* viewport is already near the bottom.
|
|
2290
|
+
*/
|
|
2291
|
+
scrollToBottom(force) {
|
|
2292
|
+
const el = this.messagesEl;
|
|
2293
|
+
if (!el) return;
|
|
2294
|
+
if (force) {
|
|
2295
|
+
el.scrollTop = el.scrollHeight;
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
if (el.scrollHeight - el.scrollTop - el.clientHeight < 80) el.scrollTop = el.scrollHeight;
|
|
1808
2299
|
}
|
|
1809
2300
|
/** Wrap a bubble in a `.row`, prefixing assistant rows with a mini avatar. */
|
|
1810
2301
|
buildRow(role, bubble) {
|
|
@@ -1861,7 +2352,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1861
2352
|
a.className = "src-title";
|
|
1862
2353
|
a.href = safeUrl;
|
|
1863
2354
|
a.target = "_blank";
|
|
1864
|
-
a.rel = "noopener noreferrer";
|
|
2355
|
+
a.rel = "noopener noreferrer nofollow";
|
|
1865
2356
|
titleEl = a;
|
|
1866
2357
|
} else {
|
|
1867
2358
|
titleEl = document.createElement("span");
|
|
@@ -1870,10 +2361,13 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1870
2361
|
titleEl.textContent = c.title || c.id || "Source";
|
|
1871
2362
|
li.appendChild(titleEl);
|
|
1872
2363
|
if (c.snippet) {
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2364
|
+
const cleaned = cleanCitationSnippet(c.snippet);
|
|
2365
|
+
if (cleaned) {
|
|
2366
|
+
const snip = document.createElement("span");
|
|
2367
|
+
snip.className = "src-snippet md";
|
|
2368
|
+
snip.innerHTML = renderMarkdown(cleaned);
|
|
2369
|
+
li.appendChild(snip);
|
|
2370
|
+
}
|
|
1877
2371
|
}
|
|
1878
2372
|
list.appendChild(li);
|
|
1879
2373
|
}
|
|
@@ -1910,17 +2404,6 @@ var SmoothAgentChat = (function(exports) {
|
|
|
1910
2404
|
this.controller.send(text);
|
|
1911
2405
|
}
|
|
1912
2406
|
};
|
|
1913
|
-
function escapeHtml(value) {
|
|
1914
|
-
return value.replace(/[&<>"']/g, (c) => {
|
|
1915
|
-
switch (c) {
|
|
1916
|
-
case "&": return "&";
|
|
1917
|
-
case "<": return "<";
|
|
1918
|
-
case ">": return ">";
|
|
1919
|
-
case "\"": return """;
|
|
1920
|
-
default: return "'";
|
|
1921
|
-
}
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
2407
|
/** Register the custom element once. Safe to call multiple times. */
|
|
1925
2408
|
function defineChatWidget() {
|
|
1926
2409
|
if (typeof customElements !== "undefined" && !customElements.get("smooth-agent-chat")) customElements.define(ELEMENT_TAG, SmoothAgentChatElement);
|