@smooai/chat-widget 0.9.0 → 0.10.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/README.md +1 -0
- package/dist/chat-widget.global.js +857 -850
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1275 -1268
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +15 -1
- package/src/element.ts +12 -5
- package/src/logo.ts +9 -4
- package/src/styles.ts +3 -1
|
@@ -11442,396 +11442,703 @@ var SmoothAgentChat = (function(exports) {
|
|
|
11442
11442
|
AsYouType.prototype = Object.create(AsYouType$1.prototype, {});
|
|
11443
11443
|
AsYouType.prototype.constructor = AsYouType;
|
|
11444
11444
|
//#endregion
|
|
11445
|
-
//#region src/
|
|
11446
|
-
/**
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
|
|
11455
|
-
|
|
11456
|
-
|
|
11457
|
-
|
|
11458
|
-
|
|
11459
|
-
|
|
11460
|
-
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
|
|
11464
|
-
|
|
11465
|
-
|
|
11466
|
-
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
|
|
11477
|
-
|
|
11478
|
-
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
|
|
11445
|
+
//#region src/markdown.ts
|
|
11446
|
+
/**
|
|
11447
|
+
* A tiny, safe-by-default Markdown → HTML renderer for the chat widget.
|
|
11448
|
+
*
|
|
11449
|
+
* ## Why a hand-rolled renderer (and not markdown-it / snarkdown)?
|
|
11450
|
+
*
|
|
11451
|
+
* The widget renders **untrusted** text in two places: the assistant's reply
|
|
11452
|
+
* (LLM output, which can echo attacker-supplied content) and citation snippets
|
|
11453
|
+
* (raw scraped page chunks). Today both are written via `textContent`, so
|
|
11454
|
+
* `**bold**`, numbered lists, and `[links](url)` show up literally. We want
|
|
11455
|
+
* them rendered — without re-opening the XSS hole that `textContent` was
|
|
11456
|
+
* guarding against.
|
|
11457
|
+
*
|
|
11458
|
+
* markdown-it with `html:false` is safe-by-default but ships ~30 kB min into
|
|
11459
|
+
* what is an embeddable **global** bundle, where every kilobyte is on the host
|
|
11460
|
+
* page's critical path. snarkdown is ~1 kB but emits raw HTML, so it would
|
|
11461
|
+
* require bolting on a separate sanitizer. Instead, this renderer is
|
|
11462
|
+
* **safe-by-construction**:
|
|
11463
|
+
*
|
|
11464
|
+
* 1. It is a *tokenizer*, not an HTML passthrough. It only ever emits a
|
|
11465
|
+
* fixed allowlist of tags (`p`, `br`, `strong`, `em`, `ul`/`ol`/`li`,
|
|
11466
|
+
* `code`/`pre`, `a`, `blockquote`). There is no code path that copies a
|
|
11467
|
+
* tag out of the input — a literal `<script>` in the input is treated as
|
|
11468
|
+
* plain text.
|
|
11469
|
+
* 2. **Every** text run is HTML-escaped via {@link escapeHtml} before it
|
|
11470
|
+
* reaches the output. Raw `<`, `>`, `&`, `"`, `'` can never become markup.
|
|
11471
|
+
* 3. **Images are dropped entirely** — `` renders as its alt text,
|
|
11472
|
+
* no `<img>` is ever produced (a scraped tracking pixel must not load).
|
|
11473
|
+
* 4. **Links** are gated through {@link safeHttpUrl}: only absolute `http(s)`
|
|
11474
|
+
* URLs become anchors (with `target="_blank"` + a hardened `rel`);
|
|
11475
|
+
* `javascript:`/`data:`/relative/etc. fall back to plain (escaped) text.
|
|
11476
|
+
* 5. **Headings** (`#`..`######`) are *downgraded* to bold lines — a full
|
|
11477
|
+
* `<h1>` is far too large inside a chat bubble or citation card.
|
|
11478
|
+
*
|
|
11479
|
+
* The output is a string of HTML that is only ever assigned to `innerHTML` of
|
|
11480
|
+
* an element the caller controls; because of (1)–(4) it can only contain the
|
|
11481
|
+
* allowlisted, attribute-sanitized tags.
|
|
11482
|
+
*
|
|
11483
|
+
* Supported subset (deliberately small):
|
|
11484
|
+
* - Paragraphs (blank-line separated) and hard/soft line breaks
|
|
11485
|
+
* - `**bold**` / `__bold__`, `*italic*` / `_italic_`
|
|
11486
|
+
* - `` `inline code` `` and fenced ``` ```code blocks``` ```
|
|
11487
|
+
* - `- ` / `* ` / `+ ` unordered lists, `1.` ordered lists
|
|
11488
|
+
* - `> ` blockquotes
|
|
11489
|
+
* - `[text](http(s)://url)` links (images dropped to alt text)
|
|
11490
|
+
* - `#`..`######` headings → bold line
|
|
11491
|
+
*/
|
|
11492
|
+
/** Escape the five HTML-significant characters so a text run can never be markup. */
|
|
11493
|
+
function escapeHtml(value) {
|
|
11494
|
+
return value.replace(/[&<>"']/g, (c) => {
|
|
11495
|
+
switch (c) {
|
|
11496
|
+
case "&": return "&";
|
|
11497
|
+
case "<": return "<";
|
|
11498
|
+
case ">": return ">";
|
|
11499
|
+
case "\"": return """;
|
|
11500
|
+
default: return "'";
|
|
11487
11501
|
}
|
|
11488
|
-
};
|
|
11502
|
+
});
|
|
11489
11503
|
}
|
|
11490
11504
|
/**
|
|
11491
|
-
*
|
|
11492
|
-
*
|
|
11505
|
+
* Return `url` only if it is a valid absolute `http(s)` URL, else `null`.
|
|
11506
|
+
*
|
|
11507
|
+
* SECURITY: link targets here originate from untrusted content (LLM output /
|
|
11508
|
+
* scraped citation chunks). Allowing an arbitrary string as an `href` permits
|
|
11509
|
+
* `javascript:`/`data:`/`vbscript:` URLs that execute on click — a stored-XSS
|
|
11510
|
+
* vector. Only absolute http(s) links are rendered as anchors; anything else
|
|
11511
|
+
* falls back to plain text upstream.
|
|
11493
11512
|
*/
|
|
11494
|
-
function
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
11499
|
-
|
|
11500
|
-
|
|
11501
|
-
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
11502
|
-
return typeof o;
|
|
11503
|
-
} : function(o) {
|
|
11504
|
-
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
11505
|
-
}, _typeof(o);
|
|
11506
|
-
}
|
|
11507
|
-
//#endregion
|
|
11508
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPrimitive.js
|
|
11509
|
-
function toPrimitive(t, r) {
|
|
11510
|
-
if ("object" != _typeof(t) || !t) return t;
|
|
11511
|
-
var e = t[Symbol.toPrimitive];
|
|
11512
|
-
if (void 0 !== e) {
|
|
11513
|
-
var i = e.call(t, r || "default");
|
|
11514
|
-
if ("object" != _typeof(i)) return i;
|
|
11515
|
-
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
11513
|
+
function safeHttpUrl(url) {
|
|
11514
|
+
if (!url) return null;
|
|
11515
|
+
try {
|
|
11516
|
+
const parsed = new URL(url);
|
|
11517
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : null;
|
|
11518
|
+
} catch {
|
|
11519
|
+
return null;
|
|
11516
11520
|
}
|
|
11517
|
-
return ("string" === r ? String : Number)(t);
|
|
11518
|
-
}
|
|
11519
|
-
//#endregion
|
|
11520
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPropertyKey.js
|
|
11521
|
-
function toPropertyKey(t) {
|
|
11522
|
-
var i = toPrimitive(t, "string");
|
|
11523
|
-
return "symbol" == _typeof(i) ? i : i + "";
|
|
11524
|
-
}
|
|
11525
|
-
//#endregion
|
|
11526
|
-
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/defineProperty.js
|
|
11527
|
-
function _defineProperty(e, r, t) {
|
|
11528
|
-
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
11529
|
-
value: t,
|
|
11530
|
-
enumerable: !0,
|
|
11531
|
-
configurable: !0,
|
|
11532
|
-
writable: !0
|
|
11533
|
-
}) : e[r] = t, e;
|
|
11534
11521
|
}
|
|
11535
|
-
//#endregion
|
|
11536
|
-
//#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/transport.js
|
|
11537
11522
|
/**
|
|
11538
|
-
*
|
|
11523
|
+
* Render the inline span grammar of a single line/segment to safe HTML.
|
|
11539
11524
|
*
|
|
11540
|
-
*
|
|
11541
|
-
*
|
|
11542
|
-
*
|
|
11543
|
-
* incoming string frames + lifecycle events.
|
|
11544
|
-
*/
|
|
11545
|
-
const WS_CONNECTING = 0;
|
|
11546
|
-
const WS_OPEN = 1;
|
|
11547
|
-
const WS_CLOSING = 2;
|
|
11548
|
-
/** Default connect timeout (ms) for the WebSocket transport. */
|
|
11549
|
-
const DEFAULT_CONNECT_TIMEOUT = 3e4;
|
|
11550
|
-
/**
|
|
11551
|
-
* Default transport backed by a `WebSocket`-like object. By default it uses the
|
|
11552
|
-
* global `WebSocket`; pass a `factory` to inject one (e.g. the `ws` package on
|
|
11553
|
-
* Node, or a mock in tests).
|
|
11525
|
+
* Order matters: code spans are extracted first (their contents are *not*
|
|
11526
|
+
* further parsed), then images are stripped to alt text, then links, then
|
|
11527
|
+
* emphasis. Every literal text run is escaped on the way out.
|
|
11554
11528
|
*/
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
this.url = url;
|
|
11565
|
-
this.connectTimeout = connectTimeout;
|
|
11566
|
-
if (factory) this.factory = factory;
|
|
11567
|
-
else {
|
|
11568
|
-
const G = globalThis;
|
|
11569
|
-
if (!G.WebSocket) throw new Error("No global WebSocket available; pass a WebSocketFactory to WebSocketTransport.");
|
|
11570
|
-
const Ctor = G.WebSocket;
|
|
11571
|
-
this.factory = (u) => new Ctor(u);
|
|
11529
|
+
function renderInline(input) {
|
|
11530
|
+
let out = "";
|
|
11531
|
+
let i = 0;
|
|
11532
|
+
const n = input.length;
|
|
11533
|
+
let buf = "";
|
|
11534
|
+
const flush = () => {
|
|
11535
|
+
if (buf) {
|
|
11536
|
+
out += escapeHtml(buf);
|
|
11537
|
+
buf = "";
|
|
11572
11538
|
}
|
|
11573
|
-
}
|
|
11574
|
-
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11579
|
-
|
|
11580
|
-
|
|
11539
|
+
};
|
|
11540
|
+
while (i < n) {
|
|
11541
|
+
const ch = input[i];
|
|
11542
|
+
if (ch === "`") {
|
|
11543
|
+
const end = input.indexOf("`", i + 1);
|
|
11544
|
+
if (end > i) {
|
|
11545
|
+
flush();
|
|
11546
|
+
out += `<code>${escapeHtml(input.slice(i + 1, end))}</code>`;
|
|
11547
|
+
i = end + 1;
|
|
11548
|
+
continue;
|
|
11549
|
+
}
|
|
11581
11550
|
}
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
11589
|
-
|
|
11590
|
-
} catch {}
|
|
11551
|
+
if (ch === "!" && input[i + 1] === "[") {
|
|
11552
|
+
const m = imageAt(input, i);
|
|
11553
|
+
if (m) {
|
|
11554
|
+
flush();
|
|
11555
|
+
out += renderInline(m.alt);
|
|
11556
|
+
i = m.end;
|
|
11557
|
+
continue;
|
|
11558
|
+
}
|
|
11591
11559
|
}
|
|
11592
|
-
|
|
11593
|
-
const
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
socket.addEventListener("close", (ev) => {
|
|
11626
|
-
if (this.socket !== socket) return;
|
|
11627
|
-
if (timer) clearTimeout(timer);
|
|
11628
|
-
for (const h of this.closeHandlers) h({
|
|
11629
|
-
code: ev.code,
|
|
11630
|
-
reason: ev.reason
|
|
11631
|
-
});
|
|
11632
|
-
});
|
|
11633
|
-
socket.addEventListener("message", (ev) => {
|
|
11634
|
-
if (this.socket !== socket) return;
|
|
11635
|
-
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
11636
|
-
for (const h of this.messageHandlers) h(data);
|
|
11637
|
-
});
|
|
11638
|
-
});
|
|
11639
|
-
}
|
|
11640
|
-
send(data) {
|
|
11641
|
-
if (!this.socket || this.socket.readyState !== WS_OPEN) throw new Error(`Cannot send: transport is "${this.state}"`);
|
|
11642
|
-
this.socket.send(data);
|
|
11643
|
-
}
|
|
11644
|
-
close(code, reason) {
|
|
11645
|
-
this.socket?.close(code, reason);
|
|
11646
|
-
}
|
|
11647
|
-
onMessage(handler) {
|
|
11648
|
-
this.messageHandlers.add(handler);
|
|
11649
|
-
return () => this.messageHandlers.delete(handler);
|
|
11650
|
-
}
|
|
11651
|
-
onClose(handler) {
|
|
11652
|
-
this.closeHandlers.add(handler);
|
|
11653
|
-
return () => this.closeHandlers.delete(handler);
|
|
11560
|
+
if (ch === "[") {
|
|
11561
|
+
const m = linkAt(input, i);
|
|
11562
|
+
if (m) {
|
|
11563
|
+
flush();
|
|
11564
|
+
const safe = safeHttpUrl(m.href);
|
|
11565
|
+
const inner = renderInline(m.text);
|
|
11566
|
+
if (safe) out += `<a href="${escapeHtml(safe)}" target="_blank" rel="noopener noreferrer nofollow">${inner}</a>`;
|
|
11567
|
+
else out += inner;
|
|
11568
|
+
i = m.end;
|
|
11569
|
+
continue;
|
|
11570
|
+
}
|
|
11571
|
+
}
|
|
11572
|
+
if (ch === "*" && input[i + 1] === "*" || ch === "_" && input[i + 1] === "_") {
|
|
11573
|
+
const marker = ch + ch;
|
|
11574
|
+
const end = input.indexOf(marker, i + 2);
|
|
11575
|
+
if (end > i + 1) {
|
|
11576
|
+
flush();
|
|
11577
|
+
out += `<strong>${renderInline(input.slice(i + 2, end))}</strong>`;
|
|
11578
|
+
i = end + 2;
|
|
11579
|
+
continue;
|
|
11580
|
+
}
|
|
11581
|
+
}
|
|
11582
|
+
if (ch === "*" || ch === "_") {
|
|
11583
|
+
const end = input.indexOf(ch, i + 1);
|
|
11584
|
+
if (end > i + 1 && input[i + 1] !== ch) {
|
|
11585
|
+
flush();
|
|
11586
|
+
out += `<em>${renderInline(input.slice(i + 1, end))}</em>`;
|
|
11587
|
+
i = end + 1;
|
|
11588
|
+
continue;
|
|
11589
|
+
}
|
|
11590
|
+
}
|
|
11591
|
+
buf += ch;
|
|
11592
|
+
i++;
|
|
11654
11593
|
}
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11594
|
+
flush();
|
|
11595
|
+
return out;
|
|
11596
|
+
}
|
|
11597
|
+
/** Parse a `[text](href)` link starting at `start` (`input[start] === '['`). */
|
|
11598
|
+
function linkAt(input, start) {
|
|
11599
|
+
const close = matchBracket(input, start);
|
|
11600
|
+
if (close < 0 || input[close + 1] !== "(") return null;
|
|
11601
|
+
const paren = input.indexOf(")", close + 2);
|
|
11602
|
+
if (paren < 0) return null;
|
|
11603
|
+
return {
|
|
11604
|
+
text: input.slice(start + 1, close),
|
|
11605
|
+
href: input.slice(close + 2, paren).trim().split(/\s+/)[0] ?? "",
|
|
11606
|
+
end: paren + 1
|
|
11607
|
+
};
|
|
11608
|
+
}
|
|
11609
|
+
/** Parse a `` image starting at `start` (`input[start] === '!'`). */
|
|
11610
|
+
function imageAt(input, start) {
|
|
11611
|
+
const link = linkAt(input, start + 1);
|
|
11612
|
+
if (!link) return null;
|
|
11613
|
+
return {
|
|
11614
|
+
alt: link.text,
|
|
11615
|
+
end: link.end
|
|
11616
|
+
};
|
|
11617
|
+
}
|
|
11618
|
+
/** Find the matching `]` for a `[` at `open`, honoring one level of nesting. */
|
|
11619
|
+
function matchBracket(input, open) {
|
|
11620
|
+
let depth = 0;
|
|
11621
|
+
for (let i = open; i < input.length; i++) {
|
|
11622
|
+
const c = input[i];
|
|
11623
|
+
if (c === "[") depth++;
|
|
11624
|
+
else if (c === "]") {
|
|
11625
|
+
depth--;
|
|
11626
|
+
if (depth === 0) return i;
|
|
11627
|
+
}
|
|
11658
11628
|
}
|
|
11659
|
-
|
|
11660
|
-
//#endregion
|
|
11661
|
-
//#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/types.js
|
|
11662
|
-
/** Every server→client `type` discriminator value. */
|
|
11663
|
-
const EVENT_TYPES = [
|
|
11664
|
-
"immediate_response",
|
|
11665
|
-
"eventual_response",
|
|
11666
|
-
"stream_chunk",
|
|
11667
|
-
"stream_token",
|
|
11668
|
-
"keepalive",
|
|
11669
|
-
"write_confirmation_required",
|
|
11670
|
-
"otp_verification_required",
|
|
11671
|
-
"otp_sent",
|
|
11672
|
-
"otp_verified",
|
|
11673
|
-
"otp_invalid",
|
|
11674
|
-
"error",
|
|
11675
|
-
"pong"
|
|
11676
|
-
];
|
|
11677
|
-
/** True if `frame` looks like any server event (has a known `type` discriminator). */
|
|
11678
|
-
function isServerEvent(frame) {
|
|
11679
|
-
return typeof frame === "object" && frame !== null && "type" in frame && typeof frame.type === "string" && EVENT_TYPES.includes(frame.type);
|
|
11629
|
+
return -1;
|
|
11680
11630
|
}
|
|
11681
|
-
|
|
11682
|
-
|
|
11631
|
+
const UL_RE = /^\s*[-*+]\s+(.*)$/;
|
|
11632
|
+
const OL_RE = /^\s*\d+[.)]\s+(.*)$/;
|
|
11633
|
+
const HEADING_RE = /^\s{0,3}(#{1,6})\s+(.*)$/;
|
|
11634
|
+
const QUOTE_RE = /^\s*>\s?(.*)$/;
|
|
11635
|
+
const FENCE_RE = /^\s*(`{3,}|~{3,})\s*(.*)$/;
|
|
11683
11636
|
/**
|
|
11684
|
-
*
|
|
11685
|
-
* smooth-operator WebSocket protocol.
|
|
11637
|
+
* Render a full Markdown string to safe HTML.
|
|
11686
11638
|
*
|
|
11687
|
-
*
|
|
11688
|
-
*
|
|
11689
|
-
* - **Transport-agnostic.** The client never touches a real socket directly; it
|
|
11690
|
-
* talks to an injectable {@link Transport}. The default ({@link WebSocketTransport})
|
|
11691
|
-
* uses the global `WebSocket`, but tests inject a mock and Node can inject `ws`.
|
|
11692
|
-
* - **Request/response correlation by `requestId`.** Every action gets a generated
|
|
11693
|
-
* `requestId`; the client routes incoming events back to the originating call.
|
|
11694
|
-
* - **Streaming as an async iterator.** `sendMessage` returns a {@link MessageTurn}
|
|
11695
|
-
* that is both awaitable (resolves with the terminal `eventual_response`) and
|
|
11696
|
-
* async-iterable (yields each `stream_token` / `stream_chunk` / HITL event in
|
|
11697
|
-
* order). This models the `stream_token`/`stream_chunk` → `eventual_response`
|
|
11698
|
-
* flow without forcing a callback style on the caller.
|
|
11699
|
-
* - **No live server required.** Correctness is fully unit-testable with a mock
|
|
11700
|
-
* transport (see `test/client.test.ts`).
|
|
11639
|
+
* @returns a string containing only the allowlisted tags described in the
|
|
11640
|
+
* module doc. Safe to assign to `innerHTML` of a caller-owned element.
|
|
11701
11641
|
*/
|
|
11702
|
-
|
|
11703
|
-
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11642
|
+
function renderMarkdown(src) {
|
|
11643
|
+
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
11644
|
+
const out = [];
|
|
11645
|
+
let i = 0;
|
|
11646
|
+
while (i < lines.length) {
|
|
11647
|
+
const line = lines[i];
|
|
11648
|
+
const fence = FENCE_RE.exec(line);
|
|
11649
|
+
if (fence) {
|
|
11650
|
+
const marker = fence[1];
|
|
11651
|
+
const body = [];
|
|
11652
|
+
i++;
|
|
11653
|
+
while (i < lines.length && !lines[i].trimStart().startsWith(marker)) {
|
|
11654
|
+
body.push(lines[i]);
|
|
11655
|
+
i++;
|
|
11656
|
+
}
|
|
11657
|
+
if (i < lines.length) i++;
|
|
11658
|
+
out.push(`<pre><code>${escapeHtml(body.join("\n"))}</code></pre>`);
|
|
11659
|
+
continue;
|
|
11660
|
+
}
|
|
11661
|
+
if (line.trim() === "") {
|
|
11662
|
+
i++;
|
|
11663
|
+
continue;
|
|
11664
|
+
}
|
|
11665
|
+
const heading = HEADING_RE.exec(line);
|
|
11666
|
+
if (heading) {
|
|
11667
|
+
out.push(`<p><strong>${renderInline(heading[2])}</strong></p>`);
|
|
11668
|
+
i++;
|
|
11669
|
+
continue;
|
|
11670
|
+
}
|
|
11671
|
+
if (UL_RE.test(line) || OL_RE.test(line)) {
|
|
11672
|
+
const ordered = OL_RE.test(line) && !UL_RE.test(line);
|
|
11673
|
+
const re = ordered ? OL_RE : UL_RE;
|
|
11674
|
+
const items = [];
|
|
11675
|
+
while (i < lines.length) {
|
|
11676
|
+
const m = re.exec(lines[i]);
|
|
11677
|
+
if (!m) break;
|
|
11678
|
+
items.push(`<li>${renderInline(m[1])}</li>`);
|
|
11679
|
+
i++;
|
|
11680
|
+
}
|
|
11681
|
+
out.push(`<${ordered ? "ol" : "ul"}>${items.join("")}</${ordered ? "ol" : "ul"}>`);
|
|
11682
|
+
continue;
|
|
11683
|
+
}
|
|
11684
|
+
if (QUOTE_RE.test(line)) {
|
|
11685
|
+
const quoted = [];
|
|
11686
|
+
while (i < lines.length) {
|
|
11687
|
+
const m = QUOTE_RE.exec(lines[i]);
|
|
11688
|
+
if (!m) break;
|
|
11689
|
+
quoted.push(m[1]);
|
|
11690
|
+
i++;
|
|
11691
|
+
}
|
|
11692
|
+
out.push(`<blockquote>${renderInline(quoted.join("\n")).replace(/\n/g, "<br>")}</blockquote>`);
|
|
11693
|
+
continue;
|
|
11694
|
+
}
|
|
11695
|
+
const para = [];
|
|
11696
|
+
while (i < lines.length) {
|
|
11697
|
+
const l = lines[i];
|
|
11698
|
+
if (l.trim() === "" || FENCE_RE.test(l) || HEADING_RE.test(l) || UL_RE.test(l) || OL_RE.test(l) || QUOTE_RE.test(l)) break;
|
|
11699
|
+
para.push(l);
|
|
11700
|
+
i++;
|
|
11701
|
+
}
|
|
11702
|
+
out.push(`<p>${renderInline(para.join("\n")).replace(/\n/g, "<br>")}</p>`);
|
|
11708
11703
|
}
|
|
11709
|
-
|
|
11704
|
+
return out.join("");
|
|
11705
|
+
}
|
|
11706
|
+
const SNIPPET_MAX = 260;
|
|
11710
11707
|
/**
|
|
11711
|
-
*
|
|
11712
|
-
*
|
|
11713
|
-
*
|
|
11708
|
+
* Clean a raw scraped citation snippet into a short, readable excerpt.
|
|
11709
|
+
*
|
|
11710
|
+
* Scraped chunks frequently begin with page boilerplate — a logo image wrapped
|
|
11711
|
+
* in a link, standalone nav, repeated whitespace — e.g.
|
|
11712
|
+
* `[](…) # Our Work We build…`. The source itself is already linked
|
|
11713
|
+
* from the citation card, so the snippet only needs to be a clean teaser.
|
|
11714
|
+
*
|
|
11715
|
+
* Steps:
|
|
11716
|
+
* 1. Strip a leading image / logo-link (`[](…)` or ``).
|
|
11717
|
+
* 2. Drop a leading standalone heading marker (`#`/`##`).
|
|
11718
|
+
* 3. Collapse all runs of whitespace to single spaces.
|
|
11719
|
+
* 4. Truncate to ~{@link SNIPPET_MAX} chars at a word boundary, adding `…`.
|
|
11720
|
+
*
|
|
11721
|
+
* The result is still rendered through {@link renderMarkdown} downstream, so any
|
|
11722
|
+
* remaining inline markup (bold/links) stays safe.
|
|
11714
11723
|
*/
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11724
|
+
function cleanCitationSnippet(raw) {
|
|
11725
|
+
let s = raw ?? "";
|
|
11726
|
+
let changed = true;
|
|
11727
|
+
while (changed) {
|
|
11728
|
+
changed = false;
|
|
11729
|
+
const before = s;
|
|
11730
|
+
s = s.replace(/^\s*\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)\s*/, "");
|
|
11731
|
+
s = s.replace(/^\s*!\[[^\]]*\]\([^)]*\)\s*/, "");
|
|
11732
|
+
s = s.replace(/^\s*#{1,6}\s+/, "");
|
|
11733
|
+
if (s !== before) changed = true;
|
|
11721
11734
|
}
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
_defineProperty(this, "code", void 0);
|
|
11728
|
-
_defineProperty(this, "requestId", void 0);
|
|
11729
|
-
this.name = "ProtocolError";
|
|
11730
|
-
this.code = code;
|
|
11731
|
-
this.requestId = requestId;
|
|
11735
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
11736
|
+
if (s.length > SNIPPET_MAX) {
|
|
11737
|
+
const cut = s.slice(0, SNIPPET_MAX);
|
|
11738
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
11739
|
+
s = (lastSpace > SNIPPET_MAX * .6 ? cut.slice(0, lastSpace) : cut).trimEnd() + "…";
|
|
11732
11740
|
}
|
|
11733
|
-
|
|
11734
|
-
|
|
11741
|
+
return s;
|
|
11742
|
+
}
|
|
11743
|
+
//#endregion
|
|
11744
|
+
//#region src/config.ts
|
|
11735
11745
|
/**
|
|
11736
|
-
*
|
|
11737
|
-
* or async-iterate it to receive every intermediate event in arrival order.
|
|
11746
|
+
* Public configuration surface for the chat widget.
|
|
11738
11747
|
*
|
|
11739
|
-
*
|
|
11740
|
-
*
|
|
11741
|
-
*
|
|
11742
|
-
* if (ev.type === 'stream_token') process.stdout.write(ev.token ?? '');
|
|
11743
|
-
* }
|
|
11744
|
-
* const final = await turn; // EventualResponse
|
|
11745
|
-
* ```
|
|
11748
|
+
* A host page configures the widget either declaratively (HTML attributes on the
|
|
11749
|
+
* `<smooth-agent-chat>` element) or programmatically (passing this object to
|
|
11750
|
+
* {@link mountChatWidget} / `element.configure(...)`).
|
|
11746
11751
|
*/
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11752
|
+
/** Resolve a partial config against the built-in defaults. */
|
|
11753
|
+
function resolveConfig(config) {
|
|
11754
|
+
const theme = config.theme ?? {};
|
|
11755
|
+
const primary = theme.primary ?? "#00a6a6";
|
|
11756
|
+
const primaryText = theme.primaryText ?? "#f8fafc";
|
|
11757
|
+
const assistantBubble = theme.chatBubbleInbound ?? theme.assistantBubble ?? "#06134b";
|
|
11758
|
+
const assistantBubbleText = theme.chatBubbleInboundText ?? theme.assistantBubbleText ?? "#f8fafc";
|
|
11759
|
+
const userBubble = theme.chatBubbleOutbound ?? theme.userBubble ?? primary;
|
|
11760
|
+
const userBubbleText = theme.chatBubbleOutboundText ?? theme.userBubbleText ?? primaryText;
|
|
11761
|
+
return {
|
|
11762
|
+
endpoint: config.endpoint,
|
|
11763
|
+
mode: config.mode ?? "popover",
|
|
11764
|
+
agentId: config.agentId,
|
|
11765
|
+
agentName: config.agentName ?? "Assistant",
|
|
11766
|
+
logoUrl: safeHttpUrl(config.logoUrl) ?? void 0,
|
|
11767
|
+
userName: config.userName,
|
|
11768
|
+
userEmail: config.userEmail,
|
|
11769
|
+
userPhone: config.userPhone,
|
|
11770
|
+
authContext: config.authContext,
|
|
11771
|
+
placeholder: config.placeholder ?? "Type a message…",
|
|
11772
|
+
greeting: config.greeting ?? "Hi! How can I help you today?",
|
|
11773
|
+
connectionErrorMessage: config.connectionErrorMessage ?? "We couldn't reach the chat. Please try again in a moment.",
|
|
11774
|
+
startOpen: config.startOpen ?? false,
|
|
11775
|
+
examplePrompts: (config.examplePrompts ?? []).filter((p) => p.trim().length > 0).slice(0, 5),
|
|
11776
|
+
requireName: config.requireName ?? false,
|
|
11777
|
+
requireEmail: config.requireEmail ?? false,
|
|
11778
|
+
requirePhone: config.requirePhone ?? false,
|
|
11779
|
+
collectPhone: config.collectPhone ?? true,
|
|
11780
|
+
collectConsent: config.collectConsent ?? true,
|
|
11781
|
+
allowChatRestore: config.allowChatRestore ?? true,
|
|
11782
|
+
allowAnonymous: config.allowAnonymous ?? false,
|
|
11783
|
+
theme: {
|
|
11784
|
+
text: theme.text ?? "#f8fafc",
|
|
11785
|
+
background: theme.background ?? "#040d30",
|
|
11786
|
+
primary,
|
|
11787
|
+
primaryText,
|
|
11788
|
+
secondary: theme.secondary ?? "#ff6b6c",
|
|
11789
|
+
assistantBubble,
|
|
11790
|
+
assistantBubbleText,
|
|
11791
|
+
userBubble,
|
|
11792
|
+
userBubbleText,
|
|
11793
|
+
border: theme.border ?? "rgba(255, 255, 255, 0.1)"
|
|
11785
11794
|
}
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
|
|
11796
|
-
|
|
11797
|
-
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
|
|
11801
|
-
|
|
11802
|
-
|
|
11795
|
+
};
|
|
11796
|
+
}
|
|
11797
|
+
/**
|
|
11798
|
+
* Whether the pre-chat identity form should gate the conversation: at least one
|
|
11799
|
+
* field is required and anonymous chat is not allowed.
|
|
11800
|
+
*/
|
|
11801
|
+
function needsUserInfo(resolved) {
|
|
11802
|
+
return !resolved.allowAnonymous && (resolved.requireName || resolved.requireEmail || resolved.requirePhone);
|
|
11803
|
+
}
|
|
11804
|
+
//#endregion
|
|
11805
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/typeof.js
|
|
11806
|
+
function _typeof(o) {
|
|
11807
|
+
"@babel/helpers - typeof";
|
|
11808
|
+
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
11809
|
+
return typeof o;
|
|
11810
|
+
} : function(o) {
|
|
11811
|
+
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
11812
|
+
}, _typeof(o);
|
|
11813
|
+
}
|
|
11814
|
+
//#endregion
|
|
11815
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPrimitive.js
|
|
11816
|
+
function toPrimitive(t, r) {
|
|
11817
|
+
if ("object" != _typeof(t) || !t) return t;
|
|
11818
|
+
var e = t[Symbol.toPrimitive];
|
|
11819
|
+
if (void 0 !== e) {
|
|
11820
|
+
var i = e.call(t, r || "default");
|
|
11821
|
+
if ("object" != _typeof(i)) return i;
|
|
11822
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
11803
11823
|
}
|
|
11804
|
-
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
11808
|
-
|
|
11809
|
-
|
|
11810
|
-
|
|
11811
|
-
|
|
11824
|
+
return ("string" === r ? String : Number)(t);
|
|
11825
|
+
}
|
|
11826
|
+
//#endregion
|
|
11827
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/toPropertyKey.js
|
|
11828
|
+
function toPropertyKey(t) {
|
|
11829
|
+
var i = toPrimitive(t, "string");
|
|
11830
|
+
return "symbol" == _typeof(i) ? i : i + "";
|
|
11831
|
+
}
|
|
11832
|
+
//#endregion
|
|
11833
|
+
//#region \0@oxc-project+runtime@0.134.0/helpers/esm/defineProperty.js
|
|
11834
|
+
function _defineProperty(e, r, t) {
|
|
11835
|
+
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
11836
|
+
value: t,
|
|
11837
|
+
enumerable: !0,
|
|
11838
|
+
configurable: !0,
|
|
11839
|
+
writable: !0
|
|
11840
|
+
}) : e[r] = t, e;
|
|
11841
|
+
}
|
|
11842
|
+
//#endregion
|
|
11843
|
+
//#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/transport.js
|
|
11844
|
+
/**
|
|
11845
|
+
* Transport abstraction for the client.
|
|
11846
|
+
*
|
|
11847
|
+
* The client is deliberately decoupled from any concrete WebSocket implementation
|
|
11848
|
+
* so it can be unit-tested with a mock and run on Node, the browser, or a custom
|
|
11849
|
+
* socket. A transport is anything that can send a string frame and surface
|
|
11850
|
+
* incoming string frames + lifecycle events.
|
|
11851
|
+
*/
|
|
11852
|
+
const WS_CONNECTING = 0;
|
|
11853
|
+
const WS_OPEN = 1;
|
|
11854
|
+
const WS_CLOSING = 2;
|
|
11855
|
+
/** Default connect timeout (ms) for the WebSocket transport. */
|
|
11856
|
+
const DEFAULT_CONNECT_TIMEOUT = 3e4;
|
|
11857
|
+
/**
|
|
11858
|
+
* Default transport backed by a `WebSocket`-like object. By default it uses the
|
|
11859
|
+
* global `WebSocket`; pass a `factory` to inject one (e.g. the `ws` package on
|
|
11860
|
+
* Node, or a mock in tests).
|
|
11861
|
+
*/
|
|
11862
|
+
var WebSocketTransport = class {
|
|
11863
|
+
constructor(url, factory, connectTimeout = DEFAULT_CONNECT_TIMEOUT) {
|
|
11864
|
+
_defineProperty(this, "socket", null);
|
|
11865
|
+
_defineProperty(this, "url", void 0);
|
|
11866
|
+
_defineProperty(this, "factory", void 0);
|
|
11867
|
+
_defineProperty(this, "connectTimeout", void 0);
|
|
11868
|
+
_defineProperty(this, "messageHandlers", /* @__PURE__ */ new Set());
|
|
11869
|
+
_defineProperty(this, "closeHandlers", /* @__PURE__ */ new Set());
|
|
11870
|
+
_defineProperty(this, "errorHandlers", /* @__PURE__ */ new Set());
|
|
11871
|
+
this.url = url;
|
|
11872
|
+
this.connectTimeout = connectTimeout;
|
|
11873
|
+
if (factory) this.factory = factory;
|
|
11874
|
+
else {
|
|
11875
|
+
const G = globalThis;
|
|
11876
|
+
if (!G.WebSocket) throw new Error("No global WebSocket available; pass a WebSocketFactory to WebSocketTransport.");
|
|
11877
|
+
const Ctor = G.WebSocket;
|
|
11878
|
+
this.factory = (u) => new Ctor(u);
|
|
11812
11879
|
}
|
|
11813
|
-
|
|
11814
|
-
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
value: void 0,
|
|
11822
|
-
done: true
|
|
11823
|
-
});
|
|
11880
|
+
}
|
|
11881
|
+
get state() {
|
|
11882
|
+
if (!this.socket) return "closed";
|
|
11883
|
+
switch (this.socket.readyState) {
|
|
11884
|
+
case WS_CONNECTING: return "connecting";
|
|
11885
|
+
case WS_OPEN: return "open";
|
|
11886
|
+
case WS_CLOSING: return "closing";
|
|
11887
|
+
default: return "closed";
|
|
11824
11888
|
}
|
|
11825
11889
|
}
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
11890
|
+
connect() {
|
|
11891
|
+
if (this.socket && this.socket.readyState === WS_OPEN) return Promise.resolve();
|
|
11892
|
+
if (this.socket && this.socket.readyState !== WS_OPEN) {
|
|
11893
|
+
const stale = this.socket;
|
|
11894
|
+
this.socket = null;
|
|
11895
|
+
try {
|
|
11896
|
+
stale.close();
|
|
11897
|
+
} catch {}
|
|
11898
|
+
}
|
|
11899
|
+
return new Promise((resolve, reject) => {
|
|
11900
|
+
const socket = this.factory(this.url);
|
|
11901
|
+
this.socket = socket;
|
|
11902
|
+
let settled = false;
|
|
11903
|
+
const timer = this.connectTimeout > 0 ? setTimeout(() => {
|
|
11904
|
+
if (settled) return;
|
|
11905
|
+
settled = true;
|
|
11906
|
+
if (this.socket === socket) this.socket = null;
|
|
11907
|
+
try {
|
|
11908
|
+
socket.close();
|
|
11909
|
+
} catch {}
|
|
11910
|
+
reject(/* @__PURE__ */ new Error(`WebSocket connect to ${this.url} timed out after ${this.connectTimeout}ms`));
|
|
11911
|
+
}, this.connectTimeout) : void 0;
|
|
11912
|
+
socket.addEventListener("open", () => {
|
|
11913
|
+
if (this.socket !== socket) return;
|
|
11914
|
+
if (settled) return;
|
|
11915
|
+
settled = true;
|
|
11916
|
+
if (timer) clearTimeout(timer);
|
|
11917
|
+
resolve();
|
|
11918
|
+
});
|
|
11919
|
+
socket.addEventListener("error", (ev) => {
|
|
11920
|
+
if (this.socket !== socket) return;
|
|
11921
|
+
for (const h of this.errorHandlers) h(ev);
|
|
11922
|
+
if (!settled && this.state !== "open") {
|
|
11923
|
+
settled = true;
|
|
11924
|
+
if (timer) clearTimeout(timer);
|
|
11925
|
+
if (this.socket === socket) this.socket = null;
|
|
11926
|
+
try {
|
|
11927
|
+
socket.close();
|
|
11928
|
+
} catch {}
|
|
11929
|
+
reject(ev instanceof Error ? ev : /* @__PURE__ */ new Error("WebSocket connection error"));
|
|
11930
|
+
}
|
|
11931
|
+
});
|
|
11932
|
+
socket.addEventListener("close", (ev) => {
|
|
11933
|
+
if (this.socket !== socket) return;
|
|
11934
|
+
if (timer) clearTimeout(timer);
|
|
11935
|
+
for (const h of this.closeHandlers) h({
|
|
11936
|
+
code: ev.code,
|
|
11937
|
+
reason: ev.reason
|
|
11938
|
+
});
|
|
11939
|
+
});
|
|
11940
|
+
socket.addEventListener("message", (ev) => {
|
|
11941
|
+
if (this.socket !== socket) return;
|
|
11942
|
+
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
11943
|
+
for (const h of this.messageHandlers) h(data);
|
|
11944
|
+
});
|
|
11945
|
+
});
|
|
11946
|
+
}
|
|
11947
|
+
send(data) {
|
|
11948
|
+
if (!this.socket || this.socket.readyState !== WS_OPEN) throw new Error(`Cannot send: transport is "${this.state}"`);
|
|
11949
|
+
this.socket.send(data);
|
|
11950
|
+
}
|
|
11951
|
+
close(code, reason) {
|
|
11952
|
+
this.socket?.close(code, reason);
|
|
11953
|
+
}
|
|
11954
|
+
onMessage(handler) {
|
|
11955
|
+
this.messageHandlers.add(handler);
|
|
11956
|
+
return () => this.messageHandlers.delete(handler);
|
|
11957
|
+
}
|
|
11958
|
+
onClose(handler) {
|
|
11959
|
+
this.closeHandlers.add(handler);
|
|
11960
|
+
return () => this.closeHandlers.delete(handler);
|
|
11961
|
+
}
|
|
11962
|
+
onError(handler) {
|
|
11963
|
+
this.errorHandlers.add(handler);
|
|
11964
|
+
return () => this.errorHandlers.delete(handler);
|
|
11965
|
+
}
|
|
11966
|
+
};
|
|
11967
|
+
//#endregion
|
|
11968
|
+
//#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/types.js
|
|
11969
|
+
/** Every server→client `type` discriminator value. */
|
|
11970
|
+
const EVENT_TYPES = [
|
|
11971
|
+
"immediate_response",
|
|
11972
|
+
"eventual_response",
|
|
11973
|
+
"stream_chunk",
|
|
11974
|
+
"stream_token",
|
|
11975
|
+
"keepalive",
|
|
11976
|
+
"write_confirmation_required",
|
|
11977
|
+
"otp_verification_required",
|
|
11978
|
+
"otp_sent",
|
|
11979
|
+
"otp_verified",
|
|
11980
|
+
"otp_invalid",
|
|
11981
|
+
"error",
|
|
11982
|
+
"pong"
|
|
11983
|
+
];
|
|
11984
|
+
/** True if `frame` looks like any server event (has a known `type` discriminator). */
|
|
11985
|
+
function isServerEvent(frame) {
|
|
11986
|
+
return typeof frame === "object" && frame !== null && "type" in frame && typeof frame.type === "string" && EVENT_TYPES.includes(frame.type);
|
|
11987
|
+
}
|
|
11988
|
+
//#endregion
|
|
11989
|
+
//#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/client.js
|
|
11990
|
+
/**
|
|
11991
|
+
* SmoothAgentClient — a minimal, idiomatic, transport-agnostic client for the
|
|
11992
|
+
* smooth-operator WebSocket protocol.
|
|
11993
|
+
*
|
|
11994
|
+
* Design goals
|
|
11995
|
+
* ------------
|
|
11996
|
+
* - **Transport-agnostic.** The client never touches a real socket directly; it
|
|
11997
|
+
* talks to an injectable {@link Transport}. The default ({@link WebSocketTransport})
|
|
11998
|
+
* uses the global `WebSocket`, but tests inject a mock and Node can inject `ws`.
|
|
11999
|
+
* - **Request/response correlation by `requestId`.** Every action gets a generated
|
|
12000
|
+
* `requestId`; the client routes incoming events back to the originating call.
|
|
12001
|
+
* - **Streaming as an async iterator.** `sendMessage` returns a {@link MessageTurn}
|
|
12002
|
+
* that is both awaitable (resolves with the terminal `eventual_response`) and
|
|
12003
|
+
* async-iterable (yields each `stream_token` / `stream_chunk` / HITL event in
|
|
12004
|
+
* order). This models the `stream_token`/`stream_chunk` → `eventual_response`
|
|
12005
|
+
* flow without forcing a callback style on the caller.
|
|
12006
|
+
* - **No live server required.** Correctness is fully unit-testable with a mock
|
|
12007
|
+
* transport (see `test/client.test.ts`).
|
|
12008
|
+
*/
|
|
12009
|
+
let _Symbol$asyncIterator;
|
|
12010
|
+
/** A timeout that yields no terminal event. */
|
|
12011
|
+
var RequestTimeoutError = class extends Error {
|
|
12012
|
+
constructor(requestId, ms) {
|
|
12013
|
+
super(`Request ${requestId} timed out after ${ms}ms`);
|
|
12014
|
+
this.name = "RequestTimeoutError";
|
|
12015
|
+
}
|
|
12016
|
+
};
|
|
12017
|
+
/**
|
|
12018
|
+
* A streaming turn that received no terminal `eventual_response` / `error` within the
|
|
12019
|
+
* configured {@link SmoothAgentClientOptions.turnTimeout}. The turn rejects with this
|
|
12020
|
+
* and its async iteration throws it, so a stuck server can never hang the caller.
|
|
12021
|
+
*/
|
|
12022
|
+
var TurnTimeoutError = class extends Error {
|
|
12023
|
+
constructor(requestId, ms) {
|
|
12024
|
+
super(`Turn ${requestId} timed out after ${ms}ms without a terminal response`);
|
|
12025
|
+
_defineProperty(this, "requestId", void 0);
|
|
12026
|
+
this.name = "TurnTimeoutError";
|
|
12027
|
+
this.requestId = requestId;
|
|
12028
|
+
}
|
|
12029
|
+
};
|
|
12030
|
+
/** A protocol-level error event surfaced as a throwable. */
|
|
12031
|
+
var ProtocolError = class extends Error {
|
|
12032
|
+
constructor(code, message, requestId) {
|
|
12033
|
+
super(message);
|
|
12034
|
+
_defineProperty(this, "code", void 0);
|
|
12035
|
+
_defineProperty(this, "requestId", void 0);
|
|
12036
|
+
this.name = "ProtocolError";
|
|
12037
|
+
this.code = code;
|
|
12038
|
+
this.requestId = requestId;
|
|
12039
|
+
}
|
|
12040
|
+
};
|
|
12041
|
+
_Symbol$asyncIterator = Symbol.asyncIterator;
|
|
12042
|
+
/**
|
|
12043
|
+
* A streaming message turn. Await it for the terminal {@link EventualResponse},
|
|
12044
|
+
* or async-iterate it to receive every intermediate event in arrival order.
|
|
12045
|
+
*
|
|
12046
|
+
* ```ts
|
|
12047
|
+
* const turn = client.sendMessage({ sessionId, message: 'hi' });
|
|
12048
|
+
* for await (const ev of turn) {
|
|
12049
|
+
* if (ev.type === 'stream_token') process.stdout.write(ev.token ?? '');
|
|
12050
|
+
* }
|
|
12051
|
+
* const final = await turn; // EventualResponse
|
|
12052
|
+
* ```
|
|
12053
|
+
*/
|
|
12054
|
+
var MessageTurn = class {
|
|
12055
|
+
constructor(requestId, onClose, turnTimeout = 0) {
|
|
12056
|
+
_defineProperty(
|
|
12057
|
+
this,
|
|
12058
|
+
/** The requestId this turn is correlated on. */
|
|
12059
|
+
"requestId",
|
|
12060
|
+
void 0
|
|
12061
|
+
);
|
|
12062
|
+
_defineProperty(this, "queue", []);
|
|
12063
|
+
_defineProperty(this, "waiter", null);
|
|
12064
|
+
_defineProperty(this, "done", false);
|
|
12065
|
+
_defineProperty(this, "finalEvent", null);
|
|
12066
|
+
_defineProperty(this, "error", null);
|
|
12067
|
+
_defineProperty(this, "settled", void 0);
|
|
12068
|
+
_defineProperty(this, "settle", void 0);
|
|
12069
|
+
_defineProperty(this, "fail", void 0);
|
|
12070
|
+
_defineProperty(this, "onClose", void 0);
|
|
12071
|
+
_defineProperty(this, "timeoutTimer", void 0);
|
|
12072
|
+
this.requestId = requestId;
|
|
12073
|
+
this.onClose = onClose;
|
|
12074
|
+
this.settled = new Promise((resolve, reject) => {
|
|
12075
|
+
this.settle = resolve;
|
|
12076
|
+
this.fail = reject;
|
|
12077
|
+
});
|
|
12078
|
+
this.settled.catch(() => {});
|
|
12079
|
+
if (turnTimeout > 0) this.timeoutTimer = setTimeout(() => {
|
|
12080
|
+
this.finish(null, new TurnTimeoutError(this.requestId, turnTimeout));
|
|
12081
|
+
}, turnTimeout);
|
|
12082
|
+
}
|
|
12083
|
+
/** Feed an event into the turn (called by the client's dispatcher). */
|
|
12084
|
+
push(event) {
|
|
12085
|
+
if (this.done) return;
|
|
12086
|
+
if (event.type === "error") {
|
|
12087
|
+
const code = event.data?.error?.code ?? "INTERNAL_ERROR";
|
|
12088
|
+
const message = event.data?.error?.message ?? "Unknown protocol error";
|
|
12089
|
+
this.deliver(event);
|
|
12090
|
+
this.finish(null, new ProtocolError(code, message, this.requestId));
|
|
12091
|
+
return;
|
|
12092
|
+
}
|
|
12093
|
+
this.deliver(event);
|
|
12094
|
+
if (event.type === "eventual_response") this.finish(event, null);
|
|
12095
|
+
}
|
|
12096
|
+
/** Force-close the turn (e.g. on disconnect) with an error. */
|
|
12097
|
+
abort(err) {
|
|
12098
|
+
if (this.done) return;
|
|
12099
|
+
this.finish(null, err);
|
|
12100
|
+
}
|
|
12101
|
+
deliver(event) {
|
|
12102
|
+
if (this.waiter) {
|
|
12103
|
+
const w = this.waiter;
|
|
12104
|
+
this.waiter = null;
|
|
12105
|
+
w.resolve({
|
|
12106
|
+
value: event,
|
|
12107
|
+
done: false
|
|
12108
|
+
});
|
|
12109
|
+
} else this.queue.push(event);
|
|
12110
|
+
}
|
|
12111
|
+
finish(final, err) {
|
|
12112
|
+
if (this.done) return;
|
|
12113
|
+
this.done = true;
|
|
12114
|
+
this.finalEvent = final;
|
|
12115
|
+
this.error = err;
|
|
12116
|
+
if (this.timeoutTimer) {
|
|
12117
|
+
clearTimeout(this.timeoutTimer);
|
|
12118
|
+
this.timeoutTimer = void 0;
|
|
12119
|
+
}
|
|
12120
|
+
this.onClose();
|
|
12121
|
+
if (err) this.fail(err);
|
|
12122
|
+
else if (final) this.settle(final);
|
|
12123
|
+
if (this.waiter) {
|
|
12124
|
+
const w = this.waiter;
|
|
12125
|
+
this.waiter = null;
|
|
12126
|
+
if (err) w.reject(err);
|
|
12127
|
+
else w.resolve({
|
|
12128
|
+
value: void 0,
|
|
12129
|
+
done: true
|
|
12130
|
+
});
|
|
12131
|
+
}
|
|
12132
|
+
}
|
|
12133
|
+
[_Symbol$asyncIterator]() {
|
|
12134
|
+
return { next: () => {
|
|
12135
|
+
if (this.queue.length > 0) return Promise.resolve({
|
|
12136
|
+
value: this.queue.shift(),
|
|
12137
|
+
done: false
|
|
12138
|
+
});
|
|
12139
|
+
if (this.done) {
|
|
12140
|
+
if (this.error) return Promise.reject(this.error);
|
|
12141
|
+
return Promise.resolve({
|
|
11835
12142
|
value: void 0,
|
|
11836
12143
|
done: true
|
|
11837
12144
|
});
|
|
@@ -12974,504 +13281,199 @@ var SmoothAgentChat = (function(exports) {
|
|
|
12974
13281
|
},
|
|
12975
13282
|
error: void 0
|
|
12976
13283
|
});
|
|
12977
|
-
break;
|
|
12978
|
-
case "otp_verified":
|
|
12979
|
-
if (this.interrupt?.kind === "otp") this.setInterrupt(null);
|
|
12980
|
-
break;
|
|
12981
|
-
case "otp_invalid":
|
|
12982
|
-
if (this.interrupt?.kind === "otp") this.setInterrupt({
|
|
12983
|
-
...this.interrupt,
|
|
12984
|
-
error: str(inner.message) ?? "That code was incorrect.",
|
|
12985
|
-
attemptsRemaining: num(inner.attemptsRemaining)
|
|
12986
|
-
});
|
|
12987
|
-
break;
|
|
12988
|
-
case "write_confirmation_required":
|
|
12989
|
-
this.setInterrupt({
|
|
12990
|
-
kind: "confirm",
|
|
12991
|
-
toolId: str(inner.toolId),
|
|
12992
|
-
actionDescription: str(inner.actionDescription)
|
|
12993
|
-
});
|
|
12994
|
-
break;
|
|
12995
|
-
default: break;
|
|
12996
|
-
}
|
|
12997
|
-
}
|
|
12998
|
-
/**
|
|
12999
|
-
* Begin the cross-device restore: POST `/internal/identity/request-otp` for
|
|
13000
|
-
* `email` over `channel`. The view collects the email via an explicit affordance.
|
|
13001
|
-
*/
|
|
13002
|
-
async requestIdentityOtp(email, channel = "email") {
|
|
13003
|
-
const trimmed = email.trim();
|
|
13004
|
-
if (!trimmed) return;
|
|
13005
|
-
this.setIdentityRestore({
|
|
13006
|
-
phase: "requesting",
|
|
13007
|
-
email: trimmed,
|
|
13008
|
-
channel
|
|
13009
|
-
});
|
|
13010
|
-
if (!this.sessionId) try {
|
|
13011
|
-
await this.connect();
|
|
13012
|
-
} catch {}
|
|
13013
|
-
if (!this.sessionId) {
|
|
13014
|
-
this.setIdentityRestore({
|
|
13015
|
-
phase: "error",
|
|
13016
|
-
message: "No active session to verify against."
|
|
13017
|
-
});
|
|
13018
|
-
return;
|
|
13019
|
-
}
|
|
13020
|
-
try {
|
|
13021
|
-
const json = await this.postInternal("/internal/identity/request-otp", {
|
|
13022
|
-
sessionId: this.sessionId,
|
|
13023
|
-
email: trimmed,
|
|
13024
|
-
channel
|
|
13025
|
-
});
|
|
13026
|
-
const masked = typeof json.maskedDestination === "string" ? json.maskedDestination : void 0;
|
|
13027
|
-
this.setIdentityRestore({
|
|
13028
|
-
phase: "awaiting_code",
|
|
13029
|
-
email: trimmed,
|
|
13030
|
-
channel,
|
|
13031
|
-
maskedDestination: masked
|
|
13032
|
-
});
|
|
13033
|
-
} catch (err) {
|
|
13034
|
-
this.setIdentityRestore({
|
|
13035
|
-
phase: "error",
|
|
13036
|
-
message: err instanceof Error ? err.message : "Could not send a verification code."
|
|
13037
|
-
});
|
|
13038
|
-
}
|
|
13039
|
-
}
|
|
13040
|
-
/** Submit the code: POST `/internal/identity/verify-otp`, then resolve on success. */
|
|
13041
|
-
async verifyIdentityOtp(code) {
|
|
13042
|
-
const state = this.identityRestore;
|
|
13043
|
-
const trimmed = code.trim();
|
|
13044
|
-
if (!trimmed || state.phase !== "awaiting_code") return;
|
|
13045
|
-
const { email, channel } = state;
|
|
13046
|
-
if (!this.sessionId) {
|
|
13047
|
-
this.setIdentityRestore({
|
|
13048
|
-
phase: "error",
|
|
13049
|
-
message: "No active session to verify against."
|
|
13050
|
-
});
|
|
13051
|
-
return;
|
|
13052
|
-
}
|
|
13053
|
-
this.setIdentityRestore({
|
|
13054
|
-
phase: "verifying",
|
|
13055
|
-
email,
|
|
13056
|
-
channel
|
|
13057
|
-
});
|
|
13058
|
-
try {
|
|
13059
|
-
const json = await this.postInternal("/internal/identity/verify-otp", {
|
|
13060
|
-
sessionId: this.sessionId,
|
|
13061
|
-
email,
|
|
13062
|
-
code: trimmed
|
|
13063
|
-
});
|
|
13064
|
-
if (json.event === "otp_verified") {
|
|
13065
|
-
this.store.getState().setVerifiedEmail(email, this.sessionId);
|
|
13066
|
-
await this.resolveIdentity(email);
|
|
13067
|
-
} else if (json.event === "otp_invalid") {
|
|
13068
|
-
const remaining = typeof json.attemptsRemaining === "number" ? json.attemptsRemaining : void 0;
|
|
13069
|
-
this.setIdentityRestore({
|
|
13070
|
-
phase: "awaiting_code",
|
|
13071
|
-
email,
|
|
13072
|
-
channel,
|
|
13073
|
-
error: "That code was incorrect.",
|
|
13074
|
-
attemptsRemaining: remaining
|
|
13075
|
-
});
|
|
13076
|
-
} else this.setIdentityRestore({
|
|
13077
|
-
phase: "error",
|
|
13078
|
-
message: "Verification failed."
|
|
13079
|
-
});
|
|
13080
|
-
} catch (err) {
|
|
13081
|
-
this.setIdentityRestore({
|
|
13082
|
-
phase: "error",
|
|
13083
|
-
message: err instanceof Error ? err.message : "Verification failed."
|
|
13084
|
-
});
|
|
13085
|
-
}
|
|
13086
|
-
}
|
|
13087
|
-
/** Resolve the verified identity → restorable conversations via POST `/internal/identity/resolve`. */
|
|
13088
|
-
async resolveIdentity(email) {
|
|
13089
|
-
if (!this.sessionId) return;
|
|
13090
|
-
this.setIdentityRestore({
|
|
13091
|
-
phase: "resolving",
|
|
13092
|
-
email
|
|
13093
|
-
});
|
|
13094
|
-
try {
|
|
13095
|
-
const json = await this.postInternal("/internal/identity/resolve", {
|
|
13096
|
-
sessionId: this.sessionId,
|
|
13097
|
-
email
|
|
13098
|
-
});
|
|
13099
|
-
if (json.resolved !== true) {
|
|
13100
|
-
this.setIdentityRestore({
|
|
13101
|
-
phase: "resolved",
|
|
13102
|
-
email,
|
|
13103
|
-
conversations: []
|
|
13104
|
-
});
|
|
13105
|
-
return;
|
|
13106
|
-
}
|
|
13107
|
-
const raw = json.conversations;
|
|
13108
|
-
const conversations = Array.isArray(raw) ? raw.map((c) => {
|
|
13109
|
-
if (!c || typeof c !== "object") return null;
|
|
13110
|
-
const o = c;
|
|
13111
|
-
const conversationId = typeof o.conversationId === "string" ? o.conversationId : "";
|
|
13112
|
-
const sessionId = typeof o.sessionId === "string" ? o.sessionId : "";
|
|
13113
|
-
if (!sessionId) return null;
|
|
13114
|
-
return {
|
|
13115
|
-
conversationId,
|
|
13116
|
-
sessionId,
|
|
13117
|
-
lastActivityAt: typeof o.lastActivityAt === "string" ? o.lastActivityAt : void 0,
|
|
13118
|
-
preview: typeof o.preview === "string" ? o.preview : void 0
|
|
13119
|
-
};
|
|
13120
|
-
}).filter((c) => c !== null) : [];
|
|
13121
|
-
this.setIdentityRestore({
|
|
13122
|
-
phase: "resolved",
|
|
13123
|
-
email,
|
|
13124
|
-
conversations
|
|
13125
|
-
});
|
|
13126
|
-
} catch (err) {
|
|
13127
|
-
this.setIdentityRestore({
|
|
13128
|
-
phase: "error",
|
|
13129
|
-
message: err instanceof Error ? err.message : "Could not load your chats."
|
|
13130
|
-
});
|
|
13131
|
-
}
|
|
13132
|
-
}
|
|
13133
|
-
/**
|
|
13134
|
-
* Replay a chosen restorable conversation: point the live session at its
|
|
13135
|
-
* sessionId, hydrate its transcript (get_session + get_messages), and persist
|
|
13136
|
-
* the new pointer so the next `sendMessage` continues it.
|
|
13137
|
-
*/
|
|
13138
|
-
async restoreConversation(sessionId) {
|
|
13139
|
-
if (!this.client) await this.ensureClient();
|
|
13140
|
-
const proven = this.sessionId ? this.verifiedEmailForSession(this.sessionId) : null;
|
|
13141
|
-
if (await this.tryResume(sessionId)) {
|
|
13142
|
-
this.store.getState().setSessionId(sessionId);
|
|
13143
|
-
if (proven) this.store.getState().setVerifiedEmail(proven, sessionId);
|
|
13144
|
-
this.setIdentityRestore({ phase: "idle" });
|
|
13145
|
-
this.setStatus("ready");
|
|
13146
|
-
} else this.setIdentityRestore({
|
|
13147
|
-
phase: "error",
|
|
13148
|
-
message: "That conversation is no longer available."
|
|
13149
|
-
});
|
|
13150
|
-
}
|
|
13151
|
-
/** Dismiss the cross-device restore panel. */
|
|
13152
|
-
cancelIdentityRestore() {
|
|
13153
|
-
this.setIdentityRestore({ phase: "idle" });
|
|
13154
|
-
}
|
|
13155
|
-
/** Tear down the underlying client. */
|
|
13156
|
-
disconnect() {
|
|
13157
|
-
this.client?.disconnect("widget closed");
|
|
13158
|
-
this.client = null;
|
|
13159
|
-
this.sessionId = null;
|
|
13160
|
-
this.activeRequestId = null;
|
|
13161
|
-
this.resumeAttempted = false;
|
|
13162
|
-
this.setInterrupt(null);
|
|
13163
|
-
this.setStatus("closed");
|
|
13164
|
-
}
|
|
13165
|
-
};
|
|
13166
|
-
//#endregion
|
|
13167
|
-
//#region src/logo.ts
|
|
13168
|
-
/**
|
|
13169
|
-
* The Smooth logo, inlined as an SVG string so the full-page header can render
|
|
13170
|
-
* it without a separate network fetch (the IIFE bundle is self-contained).
|
|
13171
|
-
*
|
|
13172
|
-
* GENERATED from `assets/smooth-logo.svg` — do not edit by hand. Regenerate with:
|
|
13173
|
-
* node -e ... (see the commit that added this file)
|
|
13174
|
-
*/
|
|
13175
|
-
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>";
|
|
13176
|
-
//#endregion
|
|
13177
|
-
//#region src/markdown.ts
|
|
13178
|
-
/**
|
|
13179
|
-
* A tiny, safe-by-default Markdown → HTML renderer for the chat widget.
|
|
13180
|
-
*
|
|
13181
|
-
* ## Why a hand-rolled renderer (and not markdown-it / snarkdown)?
|
|
13182
|
-
*
|
|
13183
|
-
* The widget renders **untrusted** text in two places: the assistant's reply
|
|
13184
|
-
* (LLM output, which can echo attacker-supplied content) and citation snippets
|
|
13185
|
-
* (raw scraped page chunks). Today both are written via `textContent`, so
|
|
13186
|
-
* `**bold**`, numbered lists, and `[links](url)` show up literally. We want
|
|
13187
|
-
* them rendered — without re-opening the XSS hole that `textContent` was
|
|
13188
|
-
* guarding against.
|
|
13189
|
-
*
|
|
13190
|
-
* markdown-it with `html:false` is safe-by-default but ships ~30 kB min into
|
|
13191
|
-
* what is an embeddable **global** bundle, where every kilobyte is on the host
|
|
13192
|
-
* page's critical path. snarkdown is ~1 kB but emits raw HTML, so it would
|
|
13193
|
-
* require bolting on a separate sanitizer. Instead, this renderer is
|
|
13194
|
-
* **safe-by-construction**:
|
|
13195
|
-
*
|
|
13196
|
-
* 1. It is a *tokenizer*, not an HTML passthrough. It only ever emits a
|
|
13197
|
-
* fixed allowlist of tags (`p`, `br`, `strong`, `em`, `ul`/`ol`/`li`,
|
|
13198
|
-
* `code`/`pre`, `a`, `blockquote`). There is no code path that copies a
|
|
13199
|
-
* tag out of the input — a literal `<script>` in the input is treated as
|
|
13200
|
-
* plain text.
|
|
13201
|
-
* 2. **Every** text run is HTML-escaped via {@link escapeHtml} before it
|
|
13202
|
-
* reaches the output. Raw `<`, `>`, `&`, `"`, `'` can never become markup.
|
|
13203
|
-
* 3. **Images are dropped entirely** — `` renders as its alt text,
|
|
13204
|
-
* no `<img>` is ever produced (a scraped tracking pixel must not load).
|
|
13205
|
-
* 4. **Links** are gated through {@link safeHttpUrl}: only absolute `http(s)`
|
|
13206
|
-
* URLs become anchors (with `target="_blank"` + a hardened `rel`);
|
|
13207
|
-
* `javascript:`/`data:`/relative/etc. fall back to plain (escaped) text.
|
|
13208
|
-
* 5. **Headings** (`#`..`######`) are *downgraded* to bold lines — a full
|
|
13209
|
-
* `<h1>` is far too large inside a chat bubble or citation card.
|
|
13210
|
-
*
|
|
13211
|
-
* The output is a string of HTML that is only ever assigned to `innerHTML` of
|
|
13212
|
-
* an element the caller controls; because of (1)–(4) it can only contain the
|
|
13213
|
-
* allowlisted, attribute-sanitized tags.
|
|
13214
|
-
*
|
|
13215
|
-
* Supported subset (deliberately small):
|
|
13216
|
-
* - Paragraphs (blank-line separated) and hard/soft line breaks
|
|
13217
|
-
* - `**bold**` / `__bold__`, `*italic*` / `_italic_`
|
|
13218
|
-
* - `` `inline code` `` and fenced ``` ```code blocks``` ```
|
|
13219
|
-
* - `- ` / `* ` / `+ ` unordered lists, `1.` ordered lists
|
|
13220
|
-
* - `> ` blockquotes
|
|
13221
|
-
* - `[text](http(s)://url)` links (images dropped to alt text)
|
|
13222
|
-
* - `#`..`######` headings → bold line
|
|
13223
|
-
*/
|
|
13224
|
-
/** Escape the five HTML-significant characters so a text run can never be markup. */
|
|
13225
|
-
function escapeHtml(value) {
|
|
13226
|
-
return value.replace(/[&<>"']/g, (c) => {
|
|
13227
|
-
switch (c) {
|
|
13228
|
-
case "&": return "&";
|
|
13229
|
-
case "<": return "<";
|
|
13230
|
-
case ">": return ">";
|
|
13231
|
-
case "\"": return """;
|
|
13232
|
-
default: return "'";
|
|
13233
|
-
}
|
|
13234
|
-
});
|
|
13235
|
-
}
|
|
13236
|
-
/**
|
|
13237
|
-
* Return `url` only if it is a valid absolute `http(s)` URL, else `null`.
|
|
13238
|
-
*
|
|
13239
|
-
* SECURITY: link targets here originate from untrusted content (LLM output /
|
|
13240
|
-
* scraped citation chunks). Allowing an arbitrary string as an `href` permits
|
|
13241
|
-
* `javascript:`/`data:`/`vbscript:` URLs that execute on click — a stored-XSS
|
|
13242
|
-
* vector. Only absolute http(s) links are rendered as anchors; anything else
|
|
13243
|
-
* falls back to plain text upstream.
|
|
13244
|
-
*/
|
|
13245
|
-
function safeHttpUrl(url) {
|
|
13246
|
-
if (!url) return null;
|
|
13247
|
-
try {
|
|
13248
|
-
const parsed = new URL(url);
|
|
13249
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : null;
|
|
13250
|
-
} catch {
|
|
13251
|
-
return null;
|
|
13252
|
-
}
|
|
13253
|
-
}
|
|
13254
|
-
/**
|
|
13255
|
-
* Render the inline span grammar of a single line/segment to safe HTML.
|
|
13256
|
-
*
|
|
13257
|
-
* Order matters: code spans are extracted first (their contents are *not*
|
|
13258
|
-
* further parsed), then images are stripped to alt text, then links, then
|
|
13259
|
-
* emphasis. Every literal text run is escaped on the way out.
|
|
13260
|
-
*/
|
|
13261
|
-
function renderInline(input) {
|
|
13262
|
-
let out = "";
|
|
13263
|
-
let i = 0;
|
|
13264
|
-
const n = input.length;
|
|
13265
|
-
let buf = "";
|
|
13266
|
-
const flush = () => {
|
|
13267
|
-
if (buf) {
|
|
13268
|
-
out += escapeHtml(buf);
|
|
13269
|
-
buf = "";
|
|
13270
|
-
}
|
|
13271
|
-
};
|
|
13272
|
-
while (i < n) {
|
|
13273
|
-
const ch = input[i];
|
|
13274
|
-
if (ch === "`") {
|
|
13275
|
-
const end = input.indexOf("`", i + 1);
|
|
13276
|
-
if (end > i) {
|
|
13277
|
-
flush();
|
|
13278
|
-
out += `<code>${escapeHtml(input.slice(i + 1, end))}</code>`;
|
|
13279
|
-
i = end + 1;
|
|
13280
|
-
continue;
|
|
13281
|
-
}
|
|
13282
|
-
}
|
|
13283
|
-
if (ch === "!" && input[i + 1] === "[") {
|
|
13284
|
-
const m = imageAt(input, i);
|
|
13285
|
-
if (m) {
|
|
13286
|
-
flush();
|
|
13287
|
-
out += renderInline(m.alt);
|
|
13288
|
-
i = m.end;
|
|
13289
|
-
continue;
|
|
13290
|
-
}
|
|
13291
|
-
}
|
|
13292
|
-
if (ch === "[") {
|
|
13293
|
-
const m = linkAt(input, i);
|
|
13294
|
-
if (m) {
|
|
13295
|
-
flush();
|
|
13296
|
-
const safe = safeHttpUrl(m.href);
|
|
13297
|
-
const inner = renderInline(m.text);
|
|
13298
|
-
if (safe) out += `<a href="${escapeHtml(safe)}" target="_blank" rel="noopener noreferrer nofollow">${inner}</a>`;
|
|
13299
|
-
else out += inner;
|
|
13300
|
-
i = m.end;
|
|
13301
|
-
continue;
|
|
13302
|
-
}
|
|
13303
|
-
}
|
|
13304
|
-
if (ch === "*" && input[i + 1] === "*" || ch === "_" && input[i + 1] === "_") {
|
|
13305
|
-
const marker = ch + ch;
|
|
13306
|
-
const end = input.indexOf(marker, i + 2);
|
|
13307
|
-
if (end > i + 1) {
|
|
13308
|
-
flush();
|
|
13309
|
-
out += `<strong>${renderInline(input.slice(i + 2, end))}</strong>`;
|
|
13310
|
-
i = end + 2;
|
|
13311
|
-
continue;
|
|
13312
|
-
}
|
|
13313
|
-
}
|
|
13314
|
-
if (ch === "*" || ch === "_") {
|
|
13315
|
-
const end = input.indexOf(ch, i + 1);
|
|
13316
|
-
if (end > i + 1 && input[i + 1] !== ch) {
|
|
13317
|
-
flush();
|
|
13318
|
-
out += `<em>${renderInline(input.slice(i + 1, end))}</em>`;
|
|
13319
|
-
i = end + 1;
|
|
13320
|
-
continue;
|
|
13321
|
-
}
|
|
13322
|
-
}
|
|
13323
|
-
buf += ch;
|
|
13324
|
-
i++;
|
|
13325
|
-
}
|
|
13326
|
-
flush();
|
|
13327
|
-
return out;
|
|
13328
|
-
}
|
|
13329
|
-
/** Parse a `[text](href)` link starting at `start` (`input[start] === '['`). */
|
|
13330
|
-
function linkAt(input, start) {
|
|
13331
|
-
const close = matchBracket(input, start);
|
|
13332
|
-
if (close < 0 || input[close + 1] !== "(") return null;
|
|
13333
|
-
const paren = input.indexOf(")", close + 2);
|
|
13334
|
-
if (paren < 0) return null;
|
|
13335
|
-
return {
|
|
13336
|
-
text: input.slice(start + 1, close),
|
|
13337
|
-
href: input.slice(close + 2, paren).trim().split(/\s+/)[0] ?? "",
|
|
13338
|
-
end: paren + 1
|
|
13339
|
-
};
|
|
13340
|
-
}
|
|
13341
|
-
/** Parse a `` image starting at `start` (`input[start] === '!'`). */
|
|
13342
|
-
function imageAt(input, start) {
|
|
13343
|
-
const link = linkAt(input, start + 1);
|
|
13344
|
-
if (!link) return null;
|
|
13345
|
-
return {
|
|
13346
|
-
alt: link.text,
|
|
13347
|
-
end: link.end
|
|
13348
|
-
};
|
|
13349
|
-
}
|
|
13350
|
-
/** Find the matching `]` for a `[` at `open`, honoring one level of nesting. */
|
|
13351
|
-
function matchBracket(input, open) {
|
|
13352
|
-
let depth = 0;
|
|
13353
|
-
for (let i = open; i < input.length; i++) {
|
|
13354
|
-
const c = input[i];
|
|
13355
|
-
if (c === "[") depth++;
|
|
13356
|
-
else if (c === "]") {
|
|
13357
|
-
depth--;
|
|
13358
|
-
if (depth === 0) return i;
|
|
13359
|
-
}
|
|
13360
|
-
}
|
|
13361
|
-
return -1;
|
|
13362
|
-
}
|
|
13363
|
-
const UL_RE = /^\s*[-*+]\s+(.*)$/;
|
|
13364
|
-
const OL_RE = /^\s*\d+[.)]\s+(.*)$/;
|
|
13365
|
-
const HEADING_RE = /^\s{0,3}(#{1,6})\s+(.*)$/;
|
|
13366
|
-
const QUOTE_RE = /^\s*>\s?(.*)$/;
|
|
13367
|
-
const FENCE_RE = /^\s*(`{3,}|~{3,})\s*(.*)$/;
|
|
13368
|
-
/**
|
|
13369
|
-
* Render a full Markdown string to safe HTML.
|
|
13370
|
-
*
|
|
13371
|
-
* @returns a string containing only the allowlisted tags described in the
|
|
13372
|
-
* module doc. Safe to assign to `innerHTML` of a caller-owned element.
|
|
13373
|
-
*/
|
|
13374
|
-
function renderMarkdown(src) {
|
|
13375
|
-
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
13376
|
-
const out = [];
|
|
13377
|
-
let i = 0;
|
|
13378
|
-
while (i < lines.length) {
|
|
13379
|
-
const line = lines[i];
|
|
13380
|
-
const fence = FENCE_RE.exec(line);
|
|
13381
|
-
if (fence) {
|
|
13382
|
-
const marker = fence[1];
|
|
13383
|
-
const body = [];
|
|
13384
|
-
i++;
|
|
13385
|
-
while (i < lines.length && !lines[i].trimStart().startsWith(marker)) {
|
|
13386
|
-
body.push(lines[i]);
|
|
13387
|
-
i++;
|
|
13388
|
-
}
|
|
13389
|
-
if (i < lines.length) i++;
|
|
13390
|
-
out.push(`<pre><code>${escapeHtml(body.join("\n"))}</code></pre>`);
|
|
13391
|
-
continue;
|
|
13284
|
+
break;
|
|
13285
|
+
case "otp_verified":
|
|
13286
|
+
if (this.interrupt?.kind === "otp") this.setInterrupt(null);
|
|
13287
|
+
break;
|
|
13288
|
+
case "otp_invalid":
|
|
13289
|
+
if (this.interrupt?.kind === "otp") this.setInterrupt({
|
|
13290
|
+
...this.interrupt,
|
|
13291
|
+
error: str(inner.message) ?? "That code was incorrect.",
|
|
13292
|
+
attemptsRemaining: num(inner.attemptsRemaining)
|
|
13293
|
+
});
|
|
13294
|
+
break;
|
|
13295
|
+
case "write_confirmation_required":
|
|
13296
|
+
this.setInterrupt({
|
|
13297
|
+
kind: "confirm",
|
|
13298
|
+
toolId: str(inner.toolId),
|
|
13299
|
+
actionDescription: str(inner.actionDescription)
|
|
13300
|
+
});
|
|
13301
|
+
break;
|
|
13302
|
+
default: break;
|
|
13392
13303
|
}
|
|
13393
|
-
|
|
13394
|
-
|
|
13395
|
-
|
|
13304
|
+
}
|
|
13305
|
+
/**
|
|
13306
|
+
* Begin the cross-device restore: POST `/internal/identity/request-otp` for
|
|
13307
|
+
* `email` over `channel`. The view collects the email via an explicit affordance.
|
|
13308
|
+
*/
|
|
13309
|
+
async requestIdentityOtp(email, channel = "email") {
|
|
13310
|
+
const trimmed = email.trim();
|
|
13311
|
+
if (!trimmed) return;
|
|
13312
|
+
this.setIdentityRestore({
|
|
13313
|
+
phase: "requesting",
|
|
13314
|
+
email: trimmed,
|
|
13315
|
+
channel
|
|
13316
|
+
});
|
|
13317
|
+
if (!this.sessionId) try {
|
|
13318
|
+
await this.connect();
|
|
13319
|
+
} catch {}
|
|
13320
|
+
if (!this.sessionId) {
|
|
13321
|
+
this.setIdentityRestore({
|
|
13322
|
+
phase: "error",
|
|
13323
|
+
message: "No active session to verify against."
|
|
13324
|
+
});
|
|
13325
|
+
return;
|
|
13396
13326
|
}
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
|
|
13400
|
-
|
|
13401
|
-
|
|
13327
|
+
try {
|
|
13328
|
+
const json = await this.postInternal("/internal/identity/request-otp", {
|
|
13329
|
+
sessionId: this.sessionId,
|
|
13330
|
+
email: trimmed,
|
|
13331
|
+
channel
|
|
13332
|
+
});
|
|
13333
|
+
const masked = typeof json.maskedDestination === "string" ? json.maskedDestination : void 0;
|
|
13334
|
+
this.setIdentityRestore({
|
|
13335
|
+
phase: "awaiting_code",
|
|
13336
|
+
email: trimmed,
|
|
13337
|
+
channel,
|
|
13338
|
+
maskedDestination: masked
|
|
13339
|
+
});
|
|
13340
|
+
} catch (err) {
|
|
13341
|
+
this.setIdentityRestore({
|
|
13342
|
+
phase: "error",
|
|
13343
|
+
message: err instanceof Error ? err.message : "Could not send a verification code."
|
|
13344
|
+
});
|
|
13402
13345
|
}
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13411
|
-
|
|
13412
|
-
|
|
13413
|
-
|
|
13414
|
-
|
|
13346
|
+
}
|
|
13347
|
+
/** Submit the code: POST `/internal/identity/verify-otp`, then resolve on success. */
|
|
13348
|
+
async verifyIdentityOtp(code) {
|
|
13349
|
+
const state = this.identityRestore;
|
|
13350
|
+
const trimmed = code.trim();
|
|
13351
|
+
if (!trimmed || state.phase !== "awaiting_code") return;
|
|
13352
|
+
const { email, channel } = state;
|
|
13353
|
+
if (!this.sessionId) {
|
|
13354
|
+
this.setIdentityRestore({
|
|
13355
|
+
phase: "error",
|
|
13356
|
+
message: "No active session to verify against."
|
|
13357
|
+
});
|
|
13358
|
+
return;
|
|
13415
13359
|
}
|
|
13416
|
-
|
|
13417
|
-
|
|
13418
|
-
|
|
13419
|
-
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
|
|
13360
|
+
this.setIdentityRestore({
|
|
13361
|
+
phase: "verifying",
|
|
13362
|
+
email,
|
|
13363
|
+
channel
|
|
13364
|
+
});
|
|
13365
|
+
try {
|
|
13366
|
+
const json = await this.postInternal("/internal/identity/verify-otp", {
|
|
13367
|
+
sessionId: this.sessionId,
|
|
13368
|
+
email,
|
|
13369
|
+
code: trimmed
|
|
13370
|
+
});
|
|
13371
|
+
if (json.event === "otp_verified") {
|
|
13372
|
+
this.store.getState().setVerifiedEmail(email, this.sessionId);
|
|
13373
|
+
await this.resolveIdentity(email);
|
|
13374
|
+
} else if (json.event === "otp_invalid") {
|
|
13375
|
+
const remaining = typeof json.attemptsRemaining === "number" ? json.attemptsRemaining : void 0;
|
|
13376
|
+
this.setIdentityRestore({
|
|
13377
|
+
phase: "awaiting_code",
|
|
13378
|
+
email,
|
|
13379
|
+
channel,
|
|
13380
|
+
error: "That code was incorrect.",
|
|
13381
|
+
attemptsRemaining: remaining
|
|
13382
|
+
});
|
|
13383
|
+
} else this.setIdentityRestore({
|
|
13384
|
+
phase: "error",
|
|
13385
|
+
message: "Verification failed."
|
|
13386
|
+
});
|
|
13387
|
+
} catch (err) {
|
|
13388
|
+
this.setIdentityRestore({
|
|
13389
|
+
phase: "error",
|
|
13390
|
+
message: err instanceof Error ? err.message : "Verification failed."
|
|
13391
|
+
});
|
|
13426
13392
|
}
|
|
13427
|
-
|
|
13428
|
-
|
|
13429
|
-
|
|
13430
|
-
|
|
13431
|
-
|
|
13432
|
-
|
|
13393
|
+
}
|
|
13394
|
+
/** Resolve the verified identity → restorable conversations via POST `/internal/identity/resolve`. */
|
|
13395
|
+
async resolveIdentity(email) {
|
|
13396
|
+
if (!this.sessionId) return;
|
|
13397
|
+
this.setIdentityRestore({
|
|
13398
|
+
phase: "resolving",
|
|
13399
|
+
email
|
|
13400
|
+
});
|
|
13401
|
+
try {
|
|
13402
|
+
const json = await this.postInternal("/internal/identity/resolve", {
|
|
13403
|
+
sessionId: this.sessionId,
|
|
13404
|
+
email
|
|
13405
|
+
});
|
|
13406
|
+
if (json.resolved !== true) {
|
|
13407
|
+
this.setIdentityRestore({
|
|
13408
|
+
phase: "resolved",
|
|
13409
|
+
email,
|
|
13410
|
+
conversations: []
|
|
13411
|
+
});
|
|
13412
|
+
return;
|
|
13413
|
+
}
|
|
13414
|
+
const raw = json.conversations;
|
|
13415
|
+
const conversations = Array.isArray(raw) ? raw.map((c) => {
|
|
13416
|
+
if (!c || typeof c !== "object") return null;
|
|
13417
|
+
const o = c;
|
|
13418
|
+
const conversationId = typeof o.conversationId === "string" ? o.conversationId : "";
|
|
13419
|
+
const sessionId = typeof o.sessionId === "string" ? o.sessionId : "";
|
|
13420
|
+
if (!sessionId) return null;
|
|
13421
|
+
return {
|
|
13422
|
+
conversationId,
|
|
13423
|
+
sessionId,
|
|
13424
|
+
lastActivityAt: typeof o.lastActivityAt === "string" ? o.lastActivityAt : void 0,
|
|
13425
|
+
preview: typeof o.preview === "string" ? o.preview : void 0
|
|
13426
|
+
};
|
|
13427
|
+
}).filter((c) => c !== null) : [];
|
|
13428
|
+
this.setIdentityRestore({
|
|
13429
|
+
phase: "resolved",
|
|
13430
|
+
email,
|
|
13431
|
+
conversations
|
|
13432
|
+
});
|
|
13433
|
+
} catch (err) {
|
|
13434
|
+
this.setIdentityRestore({
|
|
13435
|
+
phase: "error",
|
|
13436
|
+
message: err instanceof Error ? err.message : "Could not load your chats."
|
|
13437
|
+
});
|
|
13433
13438
|
}
|
|
13434
|
-
out.push(`<p>${renderInline(para.join("\n")).replace(/\n/g, "<br>")}</p>`);
|
|
13435
13439
|
}
|
|
13436
|
-
|
|
13437
|
-
|
|
13438
|
-
|
|
13439
|
-
|
|
13440
|
-
|
|
13441
|
-
|
|
13442
|
-
|
|
13443
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
13449
|
-
|
|
13450
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
13453
|
-
* The result is still rendered through {@link renderMarkdown} downstream, so any
|
|
13454
|
-
* remaining inline markup (bold/links) stays safe.
|
|
13455
|
-
*/
|
|
13456
|
-
function cleanCitationSnippet(raw) {
|
|
13457
|
-
let s = raw ?? "";
|
|
13458
|
-
let changed = true;
|
|
13459
|
-
while (changed) {
|
|
13460
|
-
changed = false;
|
|
13461
|
-
const before = s;
|
|
13462
|
-
s = s.replace(/^\s*\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)\s*/, "");
|
|
13463
|
-
s = s.replace(/^\s*!\[[^\]]*\]\([^)]*\)\s*/, "");
|
|
13464
|
-
s = s.replace(/^\s*#{1,6}\s+/, "");
|
|
13465
|
-
if (s !== before) changed = true;
|
|
13440
|
+
/**
|
|
13441
|
+
* Replay a chosen restorable conversation: point the live session at its
|
|
13442
|
+
* sessionId, hydrate its transcript (get_session + get_messages), and persist
|
|
13443
|
+
* the new pointer so the next `sendMessage` continues it.
|
|
13444
|
+
*/
|
|
13445
|
+
async restoreConversation(sessionId) {
|
|
13446
|
+
if (!this.client) await this.ensureClient();
|
|
13447
|
+
const proven = this.sessionId ? this.verifiedEmailForSession(this.sessionId) : null;
|
|
13448
|
+
if (await this.tryResume(sessionId)) {
|
|
13449
|
+
this.store.getState().setSessionId(sessionId);
|
|
13450
|
+
if (proven) this.store.getState().setVerifiedEmail(proven, sessionId);
|
|
13451
|
+
this.setIdentityRestore({ phase: "idle" });
|
|
13452
|
+
this.setStatus("ready");
|
|
13453
|
+
} else this.setIdentityRestore({
|
|
13454
|
+
phase: "error",
|
|
13455
|
+
message: "That conversation is no longer available."
|
|
13456
|
+
});
|
|
13466
13457
|
}
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
const lastSpace = cut.lastIndexOf(" ");
|
|
13471
|
-
s = (lastSpace > SNIPPET_MAX * .6 ? cut.slice(0, lastSpace) : cut).trimEnd() + "…";
|
|
13458
|
+
/** Dismiss the cross-device restore panel. */
|
|
13459
|
+
cancelIdentityRestore() {
|
|
13460
|
+
this.setIdentityRestore({ phase: "idle" });
|
|
13472
13461
|
}
|
|
13473
|
-
|
|
13474
|
-
|
|
13462
|
+
/** Tear down the underlying client. */
|
|
13463
|
+
disconnect() {
|
|
13464
|
+
this.client?.disconnect("widget closed");
|
|
13465
|
+
this.client = null;
|
|
13466
|
+
this.sessionId = null;
|
|
13467
|
+
this.activeRequestId = null;
|
|
13468
|
+
this.resumeAttempted = false;
|
|
13469
|
+
this.setInterrupt(null);
|
|
13470
|
+
this.setStatus("closed");
|
|
13471
|
+
}
|
|
13472
|
+
};
|
|
13473
|
+
//#endregion
|
|
13474
|
+
//#region src/logo.ts
|
|
13475
|
+
/** The square Smooth icon (the stylized `th` glyph) — used as the default full-page header avatar. */
|
|
13476
|
+
const SMOOTH_ICON_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 150 150\">\n <defs>\n <style>\n .cls-1 {\n fill: url(#linear-gradient);\n }\n </style>\n <linearGradient id=\"linear-gradient\" x1=\"31.06\" y1=\"37.6\" x2=\"118.5\" y2=\"125.04\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\".43\" stop-color=\"#00a6a6\"/>\n <stop offset=\"1\" stop-color=\"#1238dd\"/>\n </linearGradient>\n </defs>\n <path class=\"cls-1\" d=\"M55.03,55.47v13.28H19.24v-13.28h35.79ZM26.83,41.83h17.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.01ZM93.74,80.08v32.71h-17.89V36.39h17.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>";
|
|
13475
13477
|
//#endregion
|
|
13476
13478
|
//#region src/styles.ts
|
|
13477
13479
|
/**
|
|
@@ -13646,8 +13648,9 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13646
13648
|
0 1px 0 rgba(255, 255, 255, .25) inset;
|
|
13647
13649
|
}
|
|
13648
13650
|
.avatar svg { width: 22px; height: 22px; }
|
|
13649
|
-
.avatar .logo-wrap { display: flex; }
|
|
13651
|
+
.avatar .logo-wrap { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
|
|
13650
13652
|
.avatar .logo { height: 22px; width: auto; display: block; }
|
|
13653
|
+
.avatar .logo-img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; display: block; border-radius: 9px; }
|
|
13651
13654
|
.meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
|
13652
13655
|
.title { font-weight: 650; font-size: 15.5px; letter-spacing: -.01em; line-height: 1.1; }
|
|
13653
13656
|
.status {
|
|
@@ -13696,6 +13699,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
13696
13699
|
.panel.fullpage .header { padding: 18px 22px; }
|
|
13697
13700
|
.panel.fullpage .avatar { width: 44px; height: 44px; }
|
|
13698
13701
|
.panel.fullpage .avatar .logo { height: 26px; }
|
|
13702
|
+
.panel.fullpage .avatar svg { width: 28px; height: 28px; }
|
|
13699
13703
|
|
|
13700
13704
|
/* ────────────────────────────── Messages ──────────────────────────── */
|
|
13701
13705
|
.messages {
|
|
@@ -14217,6 +14221,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
14217
14221
|
"endpoint",
|
|
14218
14222
|
"agent-id",
|
|
14219
14223
|
"agent-name",
|
|
14224
|
+
"logo-url",
|
|
14220
14225
|
"placeholder",
|
|
14221
14226
|
"greeting",
|
|
14222
14227
|
"start-open",
|
|
@@ -14328,6 +14333,7 @@ var SmoothAgentChat = (function(exports) {
|
|
|
14328
14333
|
mode: this.overrides.mode ?? (modeAttr === "fullpage" ? "fullpage" : modeAttr === "popover" ? "popover" : void 0) ?? "popover",
|
|
14329
14334
|
agentId,
|
|
14330
14335
|
agentName: this.overrides.agentName ?? this.getAttribute("agent-name") ?? void 0,
|
|
14336
|
+
logoUrl: this.overrides.logoUrl ?? this.getAttribute("logo-url") ?? void 0,
|
|
14331
14337
|
userName: this.overrides.userName,
|
|
14332
14338
|
userEmail: this.overrides.userEmail,
|
|
14333
14339
|
userPhone: this.overrides.userPhone,
|
|
@@ -14382,8 +14388,9 @@ var SmoothAgentChat = (function(exports) {
|
|
|
14382
14388
|
const style = document.createElement("style");
|
|
14383
14389
|
style.textContent = buildStyles(resolved.theme, resolved.mode);
|
|
14384
14390
|
const monogram = escapeHtml((resolved.agentName.trim().charAt(0) || "A").toUpperCase());
|
|
14391
|
+
const headerLogo = resolved.logoUrl ? `<img src="${escapeHtml(resolved.logoUrl)}" alt="" class="logo-img" />` : SMOOTH_ICON_SVG;
|
|
14385
14392
|
const header = fullpage ? `<div class="header">
|
|
14386
|
-
<div class="avatar"><span class="logo-wrap">${
|
|
14393
|
+
<div class="avatar"><span class="logo-wrap">${headerLogo}</span></div>
|
|
14387
14394
|
<div class="meta">
|
|
14388
14395
|
<span class="title">${escapeHtml(resolved.agentName)}</span>
|
|
14389
14396
|
<span class="status"><span class="dot off"></span><span class="status-text"></span></span>
|