@smooai/chat-widget 0.5.2 → 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 +473 -40
- 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 +473 -40
- 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
package/dist/index.js
CHANGED
|
@@ -351,6 +351,305 @@ var ConversationController = class {
|
|
|
351
351
|
*/
|
|
352
352
|
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>";
|
|
353
353
|
//#endregion
|
|
354
|
+
//#region src/markdown.ts
|
|
355
|
+
/**
|
|
356
|
+
* A tiny, safe-by-default Markdown → HTML renderer for the chat widget.
|
|
357
|
+
*
|
|
358
|
+
* ## Why a hand-rolled renderer (and not markdown-it / snarkdown)?
|
|
359
|
+
*
|
|
360
|
+
* The widget renders **untrusted** text in two places: the assistant's reply
|
|
361
|
+
* (LLM output, which can echo attacker-supplied content) and citation snippets
|
|
362
|
+
* (raw scraped page chunks). Today both are written via `textContent`, so
|
|
363
|
+
* `**bold**`, numbered lists, and `[links](url)` show up literally. We want
|
|
364
|
+
* them rendered — without re-opening the XSS hole that `textContent` was
|
|
365
|
+
* guarding against.
|
|
366
|
+
*
|
|
367
|
+
* markdown-it with `html:false` is safe-by-default but ships ~30 kB min into
|
|
368
|
+
* what is an embeddable **global** bundle, where every kilobyte is on the host
|
|
369
|
+
* page's critical path. snarkdown is ~1 kB but emits raw HTML, so it would
|
|
370
|
+
* require bolting on a separate sanitizer. Instead, this renderer is
|
|
371
|
+
* **safe-by-construction**:
|
|
372
|
+
*
|
|
373
|
+
* 1. It is a *tokenizer*, not an HTML passthrough. It only ever emits a
|
|
374
|
+
* fixed allowlist of tags (`p`, `br`, `strong`, `em`, `ul`/`ol`/`li`,
|
|
375
|
+
* `code`/`pre`, `a`, `blockquote`). There is no code path that copies a
|
|
376
|
+
* tag out of the input — a literal `<script>` in the input is treated as
|
|
377
|
+
* plain text.
|
|
378
|
+
* 2. **Every** text run is HTML-escaped via {@link escapeHtml} before it
|
|
379
|
+
* reaches the output. Raw `<`, `>`, `&`, `"`, `'` can never become markup.
|
|
380
|
+
* 3. **Images are dropped entirely** — `` renders as its alt text,
|
|
381
|
+
* no `<img>` is ever produced (a scraped tracking pixel must not load).
|
|
382
|
+
* 4. **Links** are gated through {@link safeHttpUrl}: only absolute `http(s)`
|
|
383
|
+
* URLs become anchors (with `target="_blank"` + a hardened `rel`);
|
|
384
|
+
* `javascript:`/`data:`/relative/etc. fall back to plain (escaped) text.
|
|
385
|
+
* 5. **Headings** (`#`..`######`) are *downgraded* to bold lines — a full
|
|
386
|
+
* `<h1>` is far too large inside a chat bubble or citation card.
|
|
387
|
+
*
|
|
388
|
+
* The output is a string of HTML that is only ever assigned to `innerHTML` of
|
|
389
|
+
* an element the caller controls; because of (1)–(4) it can only contain the
|
|
390
|
+
* allowlisted, attribute-sanitized tags.
|
|
391
|
+
*
|
|
392
|
+
* Supported subset (deliberately small):
|
|
393
|
+
* - Paragraphs (blank-line separated) and hard/soft line breaks
|
|
394
|
+
* - `**bold**` / `__bold__`, `*italic*` / `_italic_`
|
|
395
|
+
* - `` `inline code` `` and fenced ``` ```code blocks``` ```
|
|
396
|
+
* - `- ` / `* ` / `+ ` unordered lists, `1.` ordered lists
|
|
397
|
+
* - `> ` blockquotes
|
|
398
|
+
* - `[text](http(s)://url)` links (images dropped to alt text)
|
|
399
|
+
* - `#`..`######` headings → bold line
|
|
400
|
+
*/
|
|
401
|
+
/** Escape the five HTML-significant characters so a text run can never be markup. */
|
|
402
|
+
function escapeHtml(value) {
|
|
403
|
+
return value.replace(/[&<>"']/g, (c) => {
|
|
404
|
+
switch (c) {
|
|
405
|
+
case "&": return "&";
|
|
406
|
+
case "<": return "<";
|
|
407
|
+
case ">": return ">";
|
|
408
|
+
case "\"": return """;
|
|
409
|
+
default: return "'";
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Return `url` only if it is a valid absolute `http(s)` URL, else `null`.
|
|
415
|
+
*
|
|
416
|
+
* SECURITY: link targets here originate from untrusted content (LLM output /
|
|
417
|
+
* scraped citation chunks). Allowing an arbitrary string as an `href` permits
|
|
418
|
+
* `javascript:`/`data:`/`vbscript:` URLs that execute on click — a stored-XSS
|
|
419
|
+
* vector. Only absolute http(s) links are rendered as anchors; anything else
|
|
420
|
+
* falls back to plain text upstream.
|
|
421
|
+
*/
|
|
422
|
+
function safeHttpUrl(url) {
|
|
423
|
+
if (!url) return null;
|
|
424
|
+
try {
|
|
425
|
+
const parsed = new URL(url);
|
|
426
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : null;
|
|
427
|
+
} catch {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Render the inline span grammar of a single line/segment to safe HTML.
|
|
433
|
+
*
|
|
434
|
+
* Order matters: code spans are extracted first (their contents are *not*
|
|
435
|
+
* further parsed), then images are stripped to alt text, then links, then
|
|
436
|
+
* emphasis. Every literal text run is escaped on the way out.
|
|
437
|
+
*/
|
|
438
|
+
function renderInline(input) {
|
|
439
|
+
let out = "";
|
|
440
|
+
let i = 0;
|
|
441
|
+
const n = input.length;
|
|
442
|
+
let buf = "";
|
|
443
|
+
const flush = () => {
|
|
444
|
+
if (buf) {
|
|
445
|
+
out += escapeHtml(buf);
|
|
446
|
+
buf = "";
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
while (i < n) {
|
|
450
|
+
const ch = input[i];
|
|
451
|
+
if (ch === "`") {
|
|
452
|
+
const end = input.indexOf("`", i + 1);
|
|
453
|
+
if (end > i) {
|
|
454
|
+
flush();
|
|
455
|
+
out += `<code>${escapeHtml(input.slice(i + 1, end))}</code>`;
|
|
456
|
+
i = end + 1;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (ch === "!" && input[i + 1] === "[") {
|
|
461
|
+
const m = imageAt(input, i);
|
|
462
|
+
if (m) {
|
|
463
|
+
flush();
|
|
464
|
+
out += renderInline(m.alt);
|
|
465
|
+
i = m.end;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (ch === "[") {
|
|
470
|
+
const m = linkAt(input, i);
|
|
471
|
+
if (m) {
|
|
472
|
+
flush();
|
|
473
|
+
const safe = safeHttpUrl(m.href);
|
|
474
|
+
const inner = renderInline(m.text);
|
|
475
|
+
if (safe) out += `<a href="${escapeHtml(safe)}" target="_blank" rel="noopener noreferrer nofollow">${inner}</a>`;
|
|
476
|
+
else out += inner;
|
|
477
|
+
i = m.end;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (ch === "*" && input[i + 1] === "*" || ch === "_" && input[i + 1] === "_") {
|
|
482
|
+
const marker = ch + ch;
|
|
483
|
+
const end = input.indexOf(marker, i + 2);
|
|
484
|
+
if (end > i + 1) {
|
|
485
|
+
flush();
|
|
486
|
+
out += `<strong>${renderInline(input.slice(i + 2, end))}</strong>`;
|
|
487
|
+
i = end + 2;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (ch === "*" || ch === "_") {
|
|
492
|
+
const end = input.indexOf(ch, i + 1);
|
|
493
|
+
if (end > i + 1 && input[i + 1] !== ch) {
|
|
494
|
+
flush();
|
|
495
|
+
out += `<em>${renderInline(input.slice(i + 1, end))}</em>`;
|
|
496
|
+
i = end + 1;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
buf += ch;
|
|
501
|
+
i++;
|
|
502
|
+
}
|
|
503
|
+
flush();
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
/** Parse a `[text](href)` link starting at `start` (`input[start] === '['`). */
|
|
507
|
+
function linkAt(input, start) {
|
|
508
|
+
const close = matchBracket(input, start);
|
|
509
|
+
if (close < 0 || input[close + 1] !== "(") return null;
|
|
510
|
+
const paren = input.indexOf(")", close + 2);
|
|
511
|
+
if (paren < 0) return null;
|
|
512
|
+
return {
|
|
513
|
+
text: input.slice(start + 1, close),
|
|
514
|
+
href: input.slice(close + 2, paren).trim().split(/\s+/)[0] ?? "",
|
|
515
|
+
end: paren + 1
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/** Parse a `` image starting at `start` (`input[start] === '!'`). */
|
|
519
|
+
function imageAt(input, start) {
|
|
520
|
+
const link = linkAt(input, start + 1);
|
|
521
|
+
if (!link) return null;
|
|
522
|
+
return {
|
|
523
|
+
alt: link.text,
|
|
524
|
+
end: link.end
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/** Find the matching `]` for a `[` at `open`, honoring one level of nesting. */
|
|
528
|
+
function matchBracket(input, open) {
|
|
529
|
+
let depth = 0;
|
|
530
|
+
for (let i = open; i < input.length; i++) {
|
|
531
|
+
const c = input[i];
|
|
532
|
+
if (c === "[") depth++;
|
|
533
|
+
else if (c === "]") {
|
|
534
|
+
depth--;
|
|
535
|
+
if (depth === 0) return i;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return -1;
|
|
539
|
+
}
|
|
540
|
+
const UL_RE = /^\s*[-*+]\s+(.*)$/;
|
|
541
|
+
const OL_RE = /^\s*\d+[.)]\s+(.*)$/;
|
|
542
|
+
const HEADING_RE = /^\s{0,3}(#{1,6})\s+(.*)$/;
|
|
543
|
+
const QUOTE_RE = /^\s*>\s?(.*)$/;
|
|
544
|
+
const FENCE_RE = /^\s*(`{3,}|~{3,})\s*(.*)$/;
|
|
545
|
+
/**
|
|
546
|
+
* Render a full Markdown string to safe HTML.
|
|
547
|
+
*
|
|
548
|
+
* @returns a string containing only the allowlisted tags described in the
|
|
549
|
+
* module doc. Safe to assign to `innerHTML` of a caller-owned element.
|
|
550
|
+
*/
|
|
551
|
+
function renderMarkdown(src) {
|
|
552
|
+
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
553
|
+
const out = [];
|
|
554
|
+
let i = 0;
|
|
555
|
+
while (i < lines.length) {
|
|
556
|
+
const line = lines[i];
|
|
557
|
+
const fence = FENCE_RE.exec(line);
|
|
558
|
+
if (fence) {
|
|
559
|
+
const marker = fence[1];
|
|
560
|
+
const body = [];
|
|
561
|
+
i++;
|
|
562
|
+
while (i < lines.length && !lines[i].trimStart().startsWith(marker)) {
|
|
563
|
+
body.push(lines[i]);
|
|
564
|
+
i++;
|
|
565
|
+
}
|
|
566
|
+
if (i < lines.length) i++;
|
|
567
|
+
out.push(`<pre><code>${escapeHtml(body.join("\n"))}</code></pre>`);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (line.trim() === "") {
|
|
571
|
+
i++;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const heading = HEADING_RE.exec(line);
|
|
575
|
+
if (heading) {
|
|
576
|
+
out.push(`<p><strong>${renderInline(heading[2])}</strong></p>`);
|
|
577
|
+
i++;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (UL_RE.test(line) || OL_RE.test(line)) {
|
|
581
|
+
const ordered = OL_RE.test(line) && !UL_RE.test(line);
|
|
582
|
+
const re = ordered ? OL_RE : UL_RE;
|
|
583
|
+
const items = [];
|
|
584
|
+
while (i < lines.length) {
|
|
585
|
+
const m = re.exec(lines[i]);
|
|
586
|
+
if (!m) break;
|
|
587
|
+
items.push(`<li>${renderInline(m[1])}</li>`);
|
|
588
|
+
i++;
|
|
589
|
+
}
|
|
590
|
+
out.push(`<${ordered ? "ol" : "ul"}>${items.join("")}</${ordered ? "ol" : "ul"}>`);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (QUOTE_RE.test(line)) {
|
|
594
|
+
const quoted = [];
|
|
595
|
+
while (i < lines.length) {
|
|
596
|
+
const m = QUOTE_RE.exec(lines[i]);
|
|
597
|
+
if (!m) break;
|
|
598
|
+
quoted.push(m[1]);
|
|
599
|
+
i++;
|
|
600
|
+
}
|
|
601
|
+
out.push(`<blockquote>${renderInline(quoted.join("\n")).replace(/\n/g, "<br>")}</blockquote>`);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const para = [];
|
|
605
|
+
while (i < lines.length) {
|
|
606
|
+
const l = lines[i];
|
|
607
|
+
if (l.trim() === "" || FENCE_RE.test(l) || HEADING_RE.test(l) || UL_RE.test(l) || OL_RE.test(l) || QUOTE_RE.test(l)) break;
|
|
608
|
+
para.push(l);
|
|
609
|
+
i++;
|
|
610
|
+
}
|
|
611
|
+
out.push(`<p>${renderInline(para.join("\n")).replace(/\n/g, "<br>")}</p>`);
|
|
612
|
+
}
|
|
613
|
+
return out.join("");
|
|
614
|
+
}
|
|
615
|
+
const SNIPPET_MAX = 260;
|
|
616
|
+
/**
|
|
617
|
+
* Clean a raw scraped citation snippet into a short, readable excerpt.
|
|
618
|
+
*
|
|
619
|
+
* Scraped chunks frequently begin with page boilerplate — a logo image wrapped
|
|
620
|
+
* in a link, standalone nav, repeated whitespace — e.g.
|
|
621
|
+
* `[](…) # Our Work We build…`. The source itself is already linked
|
|
622
|
+
* from the citation card, so the snippet only needs to be a clean teaser.
|
|
623
|
+
*
|
|
624
|
+
* Steps:
|
|
625
|
+
* 1. Strip a leading image / logo-link (`[](…)` or ``).
|
|
626
|
+
* 2. Drop a leading standalone heading marker (`#`/`##`).
|
|
627
|
+
* 3. Collapse all runs of whitespace to single spaces.
|
|
628
|
+
* 4. Truncate to ~{@link SNIPPET_MAX} chars at a word boundary, adding `…`.
|
|
629
|
+
*
|
|
630
|
+
* The result is still rendered through {@link renderMarkdown} downstream, so any
|
|
631
|
+
* remaining inline markup (bold/links) stays safe.
|
|
632
|
+
*/
|
|
633
|
+
function cleanCitationSnippet(raw) {
|
|
634
|
+
let s = raw ?? "";
|
|
635
|
+
let changed = true;
|
|
636
|
+
while (changed) {
|
|
637
|
+
changed = false;
|
|
638
|
+
const before = s;
|
|
639
|
+
s = s.replace(/^\s*\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)\s*/, "");
|
|
640
|
+
s = s.replace(/^\s*!\[[^\]]*\]\([^)]*\)\s*/, "");
|
|
641
|
+
s = s.replace(/^\s*#{1,6}\s+/, "");
|
|
642
|
+
if (s !== before) changed = true;
|
|
643
|
+
}
|
|
644
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
645
|
+
if (s.length > SNIPPET_MAX) {
|
|
646
|
+
const cut = s.slice(0, SNIPPET_MAX);
|
|
647
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
648
|
+
s = (lastSpace > SNIPPET_MAX * .6 ? cut.slice(0, lastSpace) : cut).trimEnd() + "…";
|
|
649
|
+
}
|
|
650
|
+
return s;
|
|
651
|
+
}
|
|
652
|
+
//#endregion
|
|
354
653
|
//#region src/styles.ts
|
|
355
654
|
/**
|
|
356
655
|
* Render the widget's scoped stylesheet — the "Aurora Glass" design system.
|
|
@@ -675,6 +974,50 @@ function buildStyles(theme, mode = "popover") {
|
|
|
675
974
|
}
|
|
676
975
|
@keyframes sac-blink { to { opacity: 0 } }
|
|
677
976
|
|
|
977
|
+
/* ─────────────── Rendered markdown (assistant bubbles / snippets) ─────────── */
|
|
978
|
+
/* The renderer (markdown.ts) emits a small allowlisted set of tags; these rules
|
|
979
|
+
keep them legible inside the tight Aurora-Glass bubble + citation card. */
|
|
980
|
+
/* Block-level markdown drives its own spacing/wrapping, so opt out of the
|
|
981
|
+
bubble's pre-wrap (which would otherwise add stray blank lines). */
|
|
982
|
+
.bubble.md { white-space: normal; }
|
|
983
|
+
.md > :first-child { margin-top: 0; }
|
|
984
|
+
.md > :last-child { margin-bottom: 0; }
|
|
985
|
+
.md p { margin: 0 0 8px; }
|
|
986
|
+
.md ul, .md ol { margin: 6px 0 8px; padding-left: 20px; }
|
|
987
|
+
.md li { margin: 2px 0; }
|
|
988
|
+
.md li::marker { color: color-mix(in srgb, var(--sac-primary) 75%, transparent); }
|
|
989
|
+
.md a {
|
|
990
|
+
color: color-mix(in srgb, var(--sac-primary) 92%, #fff);
|
|
991
|
+
text-decoration: underline;
|
|
992
|
+
text-underline-offset: 2px;
|
|
993
|
+
word-break: break-word;
|
|
994
|
+
}
|
|
995
|
+
.md a:hover { text-decoration: none; }
|
|
996
|
+
.md strong { font-weight: 700; }
|
|
997
|
+
.md em { font-style: italic; }
|
|
998
|
+
.md code {
|
|
999
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1000
|
+
font-size: .9em;
|
|
1001
|
+
padding: 1px 5px;
|
|
1002
|
+
border-radius: 5px;
|
|
1003
|
+
background: color-mix(in srgb, var(--sac-text) 10%, transparent);
|
|
1004
|
+
}
|
|
1005
|
+
.md pre {
|
|
1006
|
+
margin: 6px 0 8px;
|
|
1007
|
+
padding: 9px 11px;
|
|
1008
|
+
border-radius: 9px;
|
|
1009
|
+
overflow-x: auto;
|
|
1010
|
+
background: color-mix(in srgb, var(--sac-text) 9%, transparent);
|
|
1011
|
+
border: 1px solid color-mix(in srgb, var(--sac-text) 8%, transparent);
|
|
1012
|
+
}
|
|
1013
|
+
.md pre code { padding: 0; background: none; font-size: 12px; line-height: 1.45; }
|
|
1014
|
+
.md blockquote {
|
|
1015
|
+
margin: 6px 0;
|
|
1016
|
+
padding: 2px 0 2px 11px;
|
|
1017
|
+
border-left: 2px solid color-mix(in srgb, var(--sac-primary) 55%, transparent);
|
|
1018
|
+
color: color-mix(in srgb, var(--sac-text) 78%, transparent);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
678
1021
|
/* Full-page: center the conversation in a readable column. */
|
|
679
1022
|
.panel.fullpage .messages { padding: 26px 20px; }
|
|
680
1023
|
.panel.fullpage .row { max-width: 760px; width: 100%; margin-left: auto; margin-right: auto; }
|
|
@@ -953,24 +1296,6 @@ const ICON = {
|
|
|
953
1296
|
/** Tool-confirmation interrupt — a shield. */
|
|
954
1297
|
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>`
|
|
955
1298
|
};
|
|
956
|
-
/**
|
|
957
|
-
* Return `url` only if it is a valid absolute `http(s)` URL, else `null`.
|
|
958
|
-
*
|
|
959
|
-
* SECURITY: citation URLs originate from indexed content (web / GitHub
|
|
960
|
-
* connectors), which can be attacker-influenceable. Assigning an arbitrary
|
|
961
|
-
* string to `<a>.href` allows `javascript:`/`data:`/`vbscript:` URLs that
|
|
962
|
-
* execute on click — a stored-XSS vector. Only http(s) links are rendered as
|
|
963
|
-
* anchors; anything else falls back to plain text.
|
|
964
|
-
*/
|
|
965
|
-
function safeHttpUrl(url) {
|
|
966
|
-
if (!url) return null;
|
|
967
|
-
try {
|
|
968
|
-
const parsed = new URL(url);
|
|
969
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : null;
|
|
970
|
-
} catch {
|
|
971
|
-
return null;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
1299
|
var SmoothAgentChatElement = class extends HTMLElement {
|
|
975
1300
|
static get observedAttributes() {
|
|
976
1301
|
return OBSERVED;
|
|
@@ -987,6 +1312,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
987
1312
|
_defineProperty(this, "userInfoSatisfied", false);
|
|
988
1313
|
_defineProperty(this, "hasSent", false);
|
|
989
1314
|
_defineProperty(this, "examplePrompts", []);
|
|
1315
|
+
_defineProperty(this, "greeting", "");
|
|
990
1316
|
_defineProperty(this, "interrupt", null);
|
|
991
1317
|
_defineProperty(this, "interruptEl", null);
|
|
992
1318
|
_defineProperty(this, "panelEl", null);
|
|
@@ -996,6 +1322,11 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
996
1322
|
_defineProperty(this, "dotEl", null);
|
|
997
1323
|
_defineProperty(this, "inputEl", null);
|
|
998
1324
|
_defineProperty(this, "sendBtn", null);
|
|
1325
|
+
_defineProperty(this, "streamBubbleEl", null);
|
|
1326
|
+
_defineProperty(this, "streamMsgId", null);
|
|
1327
|
+
_defineProperty(this, "streamTarget", "");
|
|
1328
|
+
_defineProperty(this, "displayedLength", 0);
|
|
1329
|
+
_defineProperty(this, "rafId", 0);
|
|
999
1330
|
this.root = this.attachShadow({ mode: "open" });
|
|
1000
1331
|
}
|
|
1001
1332
|
connectedCallback() {
|
|
@@ -1004,6 +1335,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
1004
1335
|
}
|
|
1005
1336
|
disconnectedCallback() {
|
|
1006
1337
|
this.mounted = false;
|
|
1338
|
+
this.resetReveal();
|
|
1007
1339
|
this.controller?.disconnect();
|
|
1008
1340
|
this.controller = null;
|
|
1009
1341
|
}
|
|
@@ -1072,8 +1404,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
1072
1404
|
if (!this.controller) {
|
|
1073
1405
|
this.controller = new ConversationController(config, {
|
|
1074
1406
|
onMessages: (messages) => {
|
|
1075
|
-
this.messages
|
|
1076
|
-
this.renderMessages(resolved.greeting);
|
|
1407
|
+
this.handleMessages(messages, resolved.greeting);
|
|
1077
1408
|
},
|
|
1078
1409
|
onStatus: (status) => {
|
|
1079
1410
|
this.status = status;
|
|
@@ -1108,6 +1439,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
1108
1439
|
<button class="close" aria-label="Close chat">${ICON.close}</button>
|
|
1109
1440
|
</div>`;
|
|
1110
1441
|
this.examplePrompts = resolved.examplePrompts;
|
|
1442
|
+
this.greeting = resolved.greeting;
|
|
1111
1443
|
const gating = needsUserInfo(resolved) && !this.userInfoSatisfied;
|
|
1112
1444
|
const field = (name, type, label, autocomplete) => `<label class="pc-field"><span>${escapeHtml(label)}</span><input name="${name}" type="${type}" autocomplete="${autocomplete}" required /></label>`;
|
|
1113
1445
|
const prechatHtml = `
|
|
@@ -1144,6 +1476,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
1144
1476
|
`;
|
|
1145
1477
|
const logoSvg = container.querySelector(".logo-wrap svg");
|
|
1146
1478
|
if (logoSvg) logoSvg.setAttribute("class", "logo");
|
|
1479
|
+
this.resetReveal();
|
|
1147
1480
|
this.root.replaceChildren(style, container);
|
|
1148
1481
|
this.launcherEl = container.querySelector(".launcher");
|
|
1149
1482
|
this.panelEl = container.querySelector(".panel");
|
|
@@ -1170,7 +1503,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
1170
1503
|
});
|
|
1171
1504
|
if (fullpage && !gating) this.controller?.connect().catch(() => {});
|
|
1172
1505
|
this.syncOpenState();
|
|
1173
|
-
if (!gating) this.renderMessages(
|
|
1506
|
+
if (!gating) this.renderMessages();
|
|
1174
1507
|
this.renderStatus();
|
|
1175
1508
|
this.renderComposerState();
|
|
1176
1509
|
this.renderInterrupt();
|
|
@@ -1301,9 +1634,35 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
1301
1634
|
ta.style.height = "auto";
|
|
1302
1635
|
ta.style.height = `${ta.scrollHeight}px`;
|
|
1303
1636
|
}
|
|
1304
|
-
|
|
1637
|
+
/**
|
|
1638
|
+
* Receive a new message snapshot from the controller and update the view.
|
|
1639
|
+
*
|
|
1640
|
+
* `onMessages` fires on *every* `stream_token` delta. Rebuilding the whole
|
|
1641
|
+
* list each frame is wasteful and fights the smooth reveal, so we take the
|
|
1642
|
+
* cheap path when the only change is the trailing streaming assistant message
|
|
1643
|
+
* growing: just bump the reveal *target* (the rAF loop drains it). Any
|
|
1644
|
+
* structural change (new message, finalize, citations) triggers a full
|
|
1645
|
+
* rebuild via {@link renderMessages}.
|
|
1646
|
+
*/
|
|
1647
|
+
handleMessages(messages, greeting) {
|
|
1648
|
+
this.greeting = greeting;
|
|
1649
|
+
const prev = this.messages;
|
|
1650
|
+
const last = messages[messages.length - 1];
|
|
1651
|
+
const prevLast = prev[prev.length - 1];
|
|
1652
|
+
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;
|
|
1653
|
+
this.messages = messages;
|
|
1654
|
+
if (!structural && last && last.streaming && last.id === this.streamMsgId) {
|
|
1655
|
+
this.streamTarget = last.text;
|
|
1656
|
+
this.ensureRevealLoop();
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
this.renderMessages();
|
|
1660
|
+
}
|
|
1661
|
+
renderMessages() {
|
|
1305
1662
|
if (!this.messagesEl) return;
|
|
1663
|
+
this.resetReveal();
|
|
1306
1664
|
this.messagesEl.replaceChildren();
|
|
1665
|
+
const greeting = this.greeting;
|
|
1307
1666
|
if (this.messages.length === 0 && greeting) this.messagesEl.appendChild(this.buildRow("assistant", this.greetingBubble(greeting)));
|
|
1308
1667
|
if (!this.hasSent && this.messages.length === 0 && this.examplePrompts.length > 0) {
|
|
1309
1668
|
const chips = document.createElement("div");
|
|
@@ -1326,12 +1685,94 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
1326
1685
|
bubble.append(this.typingDot(), this.typingDot(), this.typingDot());
|
|
1327
1686
|
} else if (msg.streaming) {
|
|
1328
1687
|
bubble.classList.add("cursor");
|
|
1329
|
-
|
|
1688
|
+
this.bindReveal(msg, bubble);
|
|
1689
|
+
} else if (msg.role === "assistant") {
|
|
1690
|
+
bubble.classList.add("md");
|
|
1691
|
+
bubble.innerHTML = renderMarkdown(msg.text);
|
|
1330
1692
|
} else bubble.textContent = msg.text;
|
|
1331
1693
|
this.messagesEl.appendChild(this.buildRow(msg.role, bubble));
|
|
1332
1694
|
if (msg.role === "assistant" && !msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
|
|
1333
1695
|
}
|
|
1334
|
-
this.
|
|
1696
|
+
this.scrollToBottom(true);
|
|
1697
|
+
}
|
|
1698
|
+
/** True when the host/user prefers reduced motion (snap, no typewriter). */
|
|
1699
|
+
prefersReducedMotion() {
|
|
1700
|
+
return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Bind the rAF typewriter to a freshly built streaming bubble. Preserves the
|
|
1704
|
+
* already-revealed prefix across rebuilds (so a structural rebuild mid-stream
|
|
1705
|
+
* doesn't restart the reveal from zero), then resumes the loop.
|
|
1706
|
+
*/
|
|
1707
|
+
bindReveal(msg, bubble) {
|
|
1708
|
+
const carryOver = msg.id === this.streamMsgId ? Math.min(this.displayedLength, msg.text.length) : 0;
|
|
1709
|
+
this.streamBubbleEl = bubble;
|
|
1710
|
+
this.streamMsgId = msg.id;
|
|
1711
|
+
this.streamTarget = msg.text;
|
|
1712
|
+
this.displayedLength = carryOver;
|
|
1713
|
+
if (this.prefersReducedMotion()) {
|
|
1714
|
+
this.displayedLength = msg.text.length;
|
|
1715
|
+
bubble.textContent = msg.text;
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
bubble.textContent = msg.text.slice(0, this.displayedLength);
|
|
1719
|
+
this.ensureRevealLoop();
|
|
1720
|
+
}
|
|
1721
|
+
/** Start the rAF loop if it isn't already running. */
|
|
1722
|
+
ensureRevealLoop() {
|
|
1723
|
+
if (this.prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
|
|
1724
|
+
this.snapReveal();
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
if (this.rafId) return;
|
|
1728
|
+
this.rafId = requestAnimationFrame(() => this.tickReveal());
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* One animation frame of the reveal. Advances `displayedLength` toward the
|
|
1732
|
+
* buffered target at an ADAPTIVE rate — the deeper the backlog, the more
|
|
1733
|
+
* chars per frame — so the reveal stays smooth yet never falls behind the
|
|
1734
|
+
* network. Updates ONLY the bound bubble's textContent (no list rebuild).
|
|
1735
|
+
*/
|
|
1736
|
+
tickReveal() {
|
|
1737
|
+
this.rafId = 0;
|
|
1738
|
+
const bubble = this.streamBubbleEl;
|
|
1739
|
+
if (!bubble) return;
|
|
1740
|
+
const target = this.streamTarget;
|
|
1741
|
+
const remaining = target.length - this.displayedLength;
|
|
1742
|
+
if (remaining <= 0) return;
|
|
1743
|
+
const step = Math.max(2, Math.min(remaining, Math.ceil(remaining / 8)));
|
|
1744
|
+
this.displayedLength = Math.min(target.length, this.displayedLength + step);
|
|
1745
|
+
bubble.textContent = target.slice(0, this.displayedLength);
|
|
1746
|
+
this.scrollToBottom(false);
|
|
1747
|
+
this.rafId = requestAnimationFrame(() => this.tickReveal());
|
|
1748
|
+
}
|
|
1749
|
+
/** Snap the bound bubble to the full buffered text immediately (reduced-motion / no-rAF). */
|
|
1750
|
+
snapReveal() {
|
|
1751
|
+
if (this.streamBubbleEl) {
|
|
1752
|
+
this.displayedLength = this.streamTarget.length;
|
|
1753
|
+
this.streamBubbleEl.textContent = this.streamTarget;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
/** Cancel any in-flight reveal loop and clear its state (called on full rebuild). */
|
|
1757
|
+
resetReveal() {
|
|
1758
|
+
if (this.rafId && typeof cancelAnimationFrame === "function") cancelAnimationFrame(this.rafId);
|
|
1759
|
+
this.rafId = 0;
|
|
1760
|
+
this.streamBubbleEl = null;
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Auto-scroll the message list to the bottom — but don't fight a visitor who
|
|
1764
|
+
* has scrolled up to read history. When `force` (a structural rebuild) we
|
|
1765
|
+
* always pin to bottom; during the streaming reveal we only follow if the
|
|
1766
|
+
* viewport is already near the bottom.
|
|
1767
|
+
*/
|
|
1768
|
+
scrollToBottom(force) {
|
|
1769
|
+
const el = this.messagesEl;
|
|
1770
|
+
if (!el) return;
|
|
1771
|
+
if (force) {
|
|
1772
|
+
el.scrollTop = el.scrollHeight;
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (el.scrollHeight - el.scrollTop - el.clientHeight < 80) el.scrollTop = el.scrollHeight;
|
|
1335
1776
|
}
|
|
1336
1777
|
/** Wrap a bubble in a `.row`, prefixing assistant rows with a mini avatar. */
|
|
1337
1778
|
buildRow(role, bubble) {
|
|
@@ -1388,7 +1829,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
1388
1829
|
a.className = "src-title";
|
|
1389
1830
|
a.href = safeUrl;
|
|
1390
1831
|
a.target = "_blank";
|
|
1391
|
-
a.rel = "noopener noreferrer";
|
|
1832
|
+
a.rel = "noopener noreferrer nofollow";
|
|
1392
1833
|
titleEl = a;
|
|
1393
1834
|
} else {
|
|
1394
1835
|
titleEl = document.createElement("span");
|
|
@@ -1397,10 +1838,13 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
1397
1838
|
titleEl.textContent = c.title || c.id || "Source";
|
|
1398
1839
|
li.appendChild(titleEl);
|
|
1399
1840
|
if (c.snippet) {
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1841
|
+
const cleaned = cleanCitationSnippet(c.snippet);
|
|
1842
|
+
if (cleaned) {
|
|
1843
|
+
const snip = document.createElement("span");
|
|
1844
|
+
snip.className = "src-snippet md";
|
|
1845
|
+
snip.innerHTML = renderMarkdown(cleaned);
|
|
1846
|
+
li.appendChild(snip);
|
|
1847
|
+
}
|
|
1404
1848
|
}
|
|
1405
1849
|
list.appendChild(li);
|
|
1406
1850
|
}
|
|
@@ -1437,17 +1881,6 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
1437
1881
|
this.controller.send(text);
|
|
1438
1882
|
}
|
|
1439
1883
|
};
|
|
1440
|
-
function escapeHtml(value) {
|
|
1441
|
-
return value.replace(/[&<>"']/g, (c) => {
|
|
1442
|
-
switch (c) {
|
|
1443
|
-
case "&": return "&";
|
|
1444
|
-
case "<": return "<";
|
|
1445
|
-
case ">": return ">";
|
|
1446
|
-
case "\"": return """;
|
|
1447
|
-
default: return "'";
|
|
1448
|
-
}
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
1884
|
/** Register the custom element once. Safe to call multiple times. */
|
|
1452
1885
|
function defineChatWidget() {
|
|
1453
1886
|
if (typeof customElements !== "undefined" && !customElements.get("smooth-agent-chat")) customElements.define(ELEMENT_TAG, SmoothAgentChatElement);
|