dewee-webchat 0.4.1 → 0.5.0-beta.1
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 +103 -7
- package/astro/DeweeChat.astro +2 -2
- package/dist/{chunk-VU2M42GA.js → chunk-M6J3IXEI.js} +669 -73
- package/dist/{chunk-EVZ5K7BN.js → chunk-WHHQ6FKK.js} +1 -1
- package/dist/dewee-webchat.js +37 -30
- package/dist/{handler-DunrbnSD.d.ts → handler-B-c6tDr7.d.ts} +50 -5
- package/dist/index.d.ts +56 -7
- package/dist/index.js +4 -2
- package/dist/page-context-H3ZROFAV.js +137 -0
- package/dist/{protocol-CLbGE9Ns.d.ts → protocol-BbjGSfZa.d.ts} +21 -2
- package/dist/react.d.ts +4 -2
- package/dist/react.js +9 -7
- package/dist/server-identity.d.ts +2 -2
- package/dist/server.d.ts +7 -4
- package/dist/server.js +631 -61
- package/dist/vue.d.ts +68 -3
- package/dist/vue.js +12 -5
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
isLinkErrorCode,
|
|
10
10
|
parseJobAccessLaunch,
|
|
11
11
|
stripPageBlock
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-WHHQ6FKK.js";
|
|
13
13
|
|
|
14
14
|
// src/actions/directives.ts
|
|
15
15
|
var DIRECTIVE_LANG = "dewee-action";
|
|
@@ -79,14 +79,44 @@ function stripDirectives(content) {
|
|
|
79
79
|
return removeBlocks(lines, findBlocks(lines, DIRECTIVE_OPEN_RE));
|
|
80
80
|
}
|
|
81
81
|
function isActionResultMessage(content) {
|
|
82
|
-
return
|
|
82
|
+
return /^\s*```dewee-action-result(?:\s|$)/.test(content);
|
|
83
|
+
}
|
|
84
|
+
function compactResultBodies(content) {
|
|
85
|
+
const open = /```dewee-action-result\s+/g;
|
|
86
|
+
const bodies = [];
|
|
87
|
+
let match;
|
|
88
|
+
while (match = open.exec(content)) {
|
|
89
|
+
const start = open.lastIndex;
|
|
90
|
+
if (content[start] !== "{") continue;
|
|
91
|
+
let depth = 0, quoted = false, escaped = false;
|
|
92
|
+
for (let i = start; i < content.length; i++) {
|
|
93
|
+
const c = content[i];
|
|
94
|
+
if (quoted) {
|
|
95
|
+
if (escaped) escaped = false;
|
|
96
|
+
else if (c === "\\") escaped = true;
|
|
97
|
+
else if (c === '"') quoted = false;
|
|
98
|
+
} else if (c === '"') quoted = true;
|
|
99
|
+
else if (c === "{") depth++;
|
|
100
|
+
else if (c === "}" && --depth === 0) {
|
|
101
|
+
const close = /^\s*```/.exec(content.slice(i + 1));
|
|
102
|
+
if (close) {
|
|
103
|
+
bodies.push(content.slice(start, i + 1));
|
|
104
|
+
open.lastIndex = i + 1 + close[0].length;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return bodies;
|
|
83
111
|
}
|
|
84
112
|
function parseActionResults(content) {
|
|
85
113
|
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
86
114
|
const out = [];
|
|
87
|
-
|
|
115
|
+
const blocks = findBlocks(lines, RESULT_OPEN_RE);
|
|
116
|
+
const bodies = blocks.length ? blocks.map((b) => b.body.join("\n")) : compactResultBodies(content);
|
|
117
|
+
for (const body of bodies) {
|
|
88
118
|
try {
|
|
89
|
-
const r = JSON.parse(
|
|
119
|
+
const r = JSON.parse(body);
|
|
90
120
|
if (r && typeof r.id === "string" && ["ok", "denied", "error"].includes(r.status)) out.push(r);
|
|
91
121
|
} catch {
|
|
92
122
|
}
|
|
@@ -279,6 +309,257 @@ function raceAbort(p, signal) {
|
|
|
279
309
|
});
|
|
280
310
|
}
|
|
281
311
|
|
|
312
|
+
// src/actions/dom-actions.ts
|
|
313
|
+
var DOM_ACTION_NAMES = ["page.snapshot", "page.scroll", "page.click", "page.fill"];
|
|
314
|
+
var INTERACTIVE = [
|
|
315
|
+
"a[href]",
|
|
316
|
+
"button",
|
|
317
|
+
"input",
|
|
318
|
+
"select",
|
|
319
|
+
"textarea",
|
|
320
|
+
"summary",
|
|
321
|
+
"[contenteditable]:not([contenteditable=false])",
|
|
322
|
+
...["button", "link", "checkbox", "radio", "switch", "tab", "menuitem", "option", "combobox", "textbox", "searchbox"].map((r) => `[role=${r}]`)
|
|
323
|
+
].join(",");
|
|
324
|
+
var UNFILLABLE_INPUTS = /* @__PURE__ */ new Set(["checkbox", "radio", "file", "submit", "button", "image", "reset", "hidden", "password"]);
|
|
325
|
+
var SENSITIVE_AUTOCOMPLETE = /\b(cc-[a-z-]+|one-time-code|current-password|new-password)\b/i;
|
|
326
|
+
var SENSITIVE_NAME = /(passw|passcode|\bpin\b|otp|one.?time|cvv|cvc|card.?(num|no)|cc.?num|security.?code|\bssn\b)/i;
|
|
327
|
+
var MAX_NAME = 80;
|
|
328
|
+
var MAX_VALUE = 60;
|
|
329
|
+
var DEFAULT_LIMIT = 40;
|
|
330
|
+
var clip = (s, n) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
|
|
331
|
+
var squash = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
332
|
+
var refSchema = { type: "string", pattern: "^e[0-9]{1,4}$" };
|
|
333
|
+
var genSchema = { type: "integer", minimum: 1 };
|
|
334
|
+
function createDomActions(o) {
|
|
335
|
+
let generation = 0;
|
|
336
|
+
let refs = /* @__PURE__ */ new Map();
|
|
337
|
+
const rootEl = () => {
|
|
338
|
+
const r = o.root();
|
|
339
|
+
const el2 = typeof r === "string" ? document.querySelector(r) : r;
|
|
340
|
+
if (r !== void 0 && !el2) throw new Error(`domActionsRoot ${JSON.stringify(r)} matches no element`);
|
|
341
|
+
return el2 ?? document.body;
|
|
342
|
+
};
|
|
343
|
+
const allowed = (el2, root) => el2.isConnected && root.contains(el2) && !o.exclude?.(el2) && !el2.closest("[data-dewee-ignore]") && !isSensitive(el2) && isVisible(el2);
|
|
344
|
+
const resolve = (args) => {
|
|
345
|
+
if (args.generation !== generation) {
|
|
346
|
+
throw new Error(`Stale ref: generation ${args.generation} is not the latest snapshot (${generation || "none"}). Call page.snapshot again.`);
|
|
347
|
+
}
|
|
348
|
+
const el2 = refs.get(args.ref);
|
|
349
|
+
if (!el2) throw new Error(`Unknown ref "${args.ref}". Call page.snapshot again.`);
|
|
350
|
+
if (!allowed(el2, rootEl())) throw new Error(`Ref "${args.ref}" is no longer available. Call page.snapshot again.`);
|
|
351
|
+
return el2;
|
|
352
|
+
};
|
|
353
|
+
const snapshot = {
|
|
354
|
+
name: "page.snapshot",
|
|
355
|
+
description: 'List the interactive elements visible on the page (links, buttons, form fields) as lines `ref role "name" [state]`. Use the ref together with the returned generation in page.scroll, page.click and page.fill. Every snapshot invalidates older refs. Use query to filter by visible name, offset/limit to page through long lists.',
|
|
356
|
+
sensitivity: "read",
|
|
357
|
+
parameters: {
|
|
358
|
+
type: "object",
|
|
359
|
+
properties: { query: { type: "string", maxLength: 100 }, offset: { type: "integer", minimum: 0 }, limit: { type: "integer", minimum: 1, maximum: 100 } },
|
|
360
|
+
additionalProperties: false
|
|
361
|
+
},
|
|
362
|
+
handler: ({ query, offset = 0, limit = DEFAULT_LIMIT }) => {
|
|
363
|
+
const root = rootEl();
|
|
364
|
+
generation++;
|
|
365
|
+
refs = /* @__PURE__ */ new Map();
|
|
366
|
+
const q = squash(query).toLowerCase();
|
|
367
|
+
const lines = [];
|
|
368
|
+
let total = 0;
|
|
369
|
+
for (const el2 of root.querySelectorAll(INTERACTIVE)) {
|
|
370
|
+
if (!allowed(el2, root)) continue;
|
|
371
|
+
const line = describe(el2);
|
|
372
|
+
if (q && !line.toLowerCase().includes(q)) continue;
|
|
373
|
+
total++;
|
|
374
|
+
if (total <= offset || lines.length >= limit) continue;
|
|
375
|
+
const ref = `e${total}`;
|
|
376
|
+
refs.set(ref, el2);
|
|
377
|
+
lines.push(`${ref} ${line}`);
|
|
378
|
+
}
|
|
379
|
+
return { generation, url: location.pathname + location.search, title: clip(document.title, MAX_NAME), total, offset, elements: lines, ...scrollState(root) };
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
const scroll = {
|
|
383
|
+
name: "page.scroll",
|
|
384
|
+
description: "Scroll the page by about one screen (direction up/down) or to the top/bottom, or scroll an element from page.snapshot into view (ref + generation).",
|
|
385
|
+
sensitivity: "read",
|
|
386
|
+
parameters: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: { direction: { type: "string", enum: ["up", "down", "top", "bottom"] }, ref: refSchema, generation: genSchema },
|
|
389
|
+
additionalProperties: false
|
|
390
|
+
},
|
|
391
|
+
handler: ({ direction, ref, generation: gen }) => {
|
|
392
|
+
const root = rootEl();
|
|
393
|
+
if (ref !== void 0) {
|
|
394
|
+
if (gen === void 0) throw new Error("generation is required with ref");
|
|
395
|
+
const el2 = resolve({ ref, generation: gen });
|
|
396
|
+
el2.scrollIntoView?.({ block: "center", inline: "nearest" });
|
|
397
|
+
return { ref, ...scrollState(root) };
|
|
398
|
+
}
|
|
399
|
+
if (!direction) throw new Error("Give a direction or a ref");
|
|
400
|
+
const box = scroller(root);
|
|
401
|
+
const page = box === null ? window.innerHeight : box.clientHeight;
|
|
402
|
+
const current = box === null ? window.scrollY : box.scrollTop;
|
|
403
|
+
const max = box === null ? document.documentElement.scrollHeight : box.scrollHeight;
|
|
404
|
+
const top = direction === "top" ? 0 : direction === "bottom" ? max : current + (direction === "down" ? 0.8 : -0.8) * page;
|
|
405
|
+
if (box === null) window.scrollTo({ top });
|
|
406
|
+
else box.scrollTop = top;
|
|
407
|
+
return { direction, ...scrollState(root) };
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const click = {
|
|
411
|
+
name: "page.click",
|
|
412
|
+
description: "Click an element from page.snapshot (ref + generation). The visitor approves every click.",
|
|
413
|
+
sensitivity: "mutate",
|
|
414
|
+
parameters: { type: "object", properties: { ref: refSchema, generation: genSchema }, required: ["ref", "generation"], additionalProperties: false },
|
|
415
|
+
handler: (args) => {
|
|
416
|
+
const el2 = resolve(args);
|
|
417
|
+
if (isDisabled(el2)) throw new Error(`Ref "${args.ref}" is disabled`);
|
|
418
|
+
el2.scrollIntoView?.({ block: "center", inline: "nearest" });
|
|
419
|
+
if (el2 instanceof HTMLElement) {
|
|
420
|
+
el2.focus({ preventScroll: true });
|
|
421
|
+
el2.click();
|
|
422
|
+
} else {
|
|
423
|
+
el2.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, composed: true }));
|
|
424
|
+
}
|
|
425
|
+
return { ref: args.ref, clicked: describe(el2) };
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
const fill = {
|
|
429
|
+
name: "page.fill",
|
|
430
|
+
description: "Replace the value of a text field, textarea, select (option value or label) or editable area from page.snapshot (ref + generation). The visitor approves every fill.",
|
|
431
|
+
sensitivity: "mutate",
|
|
432
|
+
parameters: {
|
|
433
|
+
type: "object",
|
|
434
|
+
properties: { ref: refSchema, generation: genSchema, text: { type: "string", maxLength: 1e4 } },
|
|
435
|
+
required: ["ref", "generation", "text"],
|
|
436
|
+
additionalProperties: false
|
|
437
|
+
},
|
|
438
|
+
handler: (args) => {
|
|
439
|
+
const el2 = resolve(args);
|
|
440
|
+
if (isDisabled(el2) || el2.readOnly) throw new Error(`Ref "${args.ref}" is not editable`);
|
|
441
|
+
setValue(el2, args.text);
|
|
442
|
+
return { ref: args.ref, value: clip(currentValue(el2), MAX_VALUE) };
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
return {
|
|
446
|
+
actions: [snapshot, scroll, click, fill],
|
|
447
|
+
target: (args) => {
|
|
448
|
+
try {
|
|
449
|
+
return describe(resolve(args));
|
|
450
|
+
} catch {
|
|
451
|
+
return void 0;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function setValue(el2, text) {
|
|
457
|
+
if (el2 instanceof HTMLInputElement) {
|
|
458
|
+
if (UNFILLABLE_INPUTS.has(el2.type)) throw new Error(`Inputs of type "${el2.type}" cannot be filled; use page.click`);
|
|
459
|
+
nativeSet(HTMLInputElement.prototype, el2, text);
|
|
460
|
+
} else if (el2 instanceof HTMLTextAreaElement) {
|
|
461
|
+
nativeSet(HTMLTextAreaElement.prototype, el2, text);
|
|
462
|
+
} else if (el2 instanceof HTMLSelectElement) {
|
|
463
|
+
const opt = [...el2.options].find((op) => op.value === text) ?? [...el2.options].find((op) => squash(op.textContent).toLowerCase() === squash(text).toLowerCase());
|
|
464
|
+
if (!opt) throw new Error(`No option "${clip(text, MAX_VALUE)}"`);
|
|
465
|
+
nativeSet(HTMLSelectElement.prototype, el2, opt.value);
|
|
466
|
+
} else if (el2 instanceof HTMLElement && el2.isContentEditable) {
|
|
467
|
+
el2.focus({ preventScroll: true });
|
|
468
|
+
el2.textContent = text;
|
|
469
|
+
el2.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true, inputType: "insertReplacementText", data: text }));
|
|
470
|
+
return;
|
|
471
|
+
} else {
|
|
472
|
+
throw new Error("This element cannot be filled; use page.click");
|
|
473
|
+
}
|
|
474
|
+
el2.focus({ preventScroll: true });
|
|
475
|
+
el2.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
476
|
+
el2.dispatchEvent(new Event("change", { bubbles: true }));
|
|
477
|
+
}
|
|
478
|
+
function nativeSet(proto, el2, value) {
|
|
479
|
+
const set = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
480
|
+
if (set) set.call(el2, value);
|
|
481
|
+
else el2.value = value;
|
|
482
|
+
}
|
|
483
|
+
function currentValue(el2) {
|
|
484
|
+
if (el2 instanceof HTMLInputElement || el2 instanceof HTMLTextAreaElement || el2 instanceof HTMLSelectElement) return el2.value;
|
|
485
|
+
return squash(el2.textContent);
|
|
486
|
+
}
|
|
487
|
+
function isDisabled(el2) {
|
|
488
|
+
return el2.disabled === true || el2.closest("fieldset[disabled]") !== null || el2.getAttribute("aria-disabled") === "true";
|
|
489
|
+
}
|
|
490
|
+
function isSensitive(el2) {
|
|
491
|
+
if (el2 instanceof HTMLInputElement && el2.type === "password") return true;
|
|
492
|
+
if (!(el2 instanceof HTMLInputElement || el2 instanceof HTMLTextAreaElement || el2 instanceof HTMLSelectElement)) return false;
|
|
493
|
+
if (SENSITIVE_AUTOCOMPLETE.test(el2.getAttribute("autocomplete") ?? "")) return true;
|
|
494
|
+
const hint = [el2.name, el2.id, el2.getAttribute("aria-label"), el2.placeholder].filter(Boolean).join(" ");
|
|
495
|
+
return SENSITIVE_NAME.test(hint);
|
|
496
|
+
}
|
|
497
|
+
function isVisible(el2) {
|
|
498
|
+
if (el2 instanceof HTMLInputElement && el2.type === "hidden") return false;
|
|
499
|
+
if (el2.closest("[hidden],[inert],[aria-hidden=true]")) return false;
|
|
500
|
+
for (let n = el2; n; n = n.parentElement) {
|
|
501
|
+
const cs = getComputedStyle(n);
|
|
502
|
+
if (cs.display === "none" || n === el2 && (cs.visibility === "hidden" || cs.visibility === "collapse")) return false;
|
|
503
|
+
}
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
function role(el2) {
|
|
507
|
+
const explicit = el2.getAttribute("role");
|
|
508
|
+
if (explicit) return explicit;
|
|
509
|
+
const tag = el2.tagName.toLowerCase();
|
|
510
|
+
if (tag === "a") return "link";
|
|
511
|
+
if (tag === "select") return "combobox";
|
|
512
|
+
if (tag === "textarea") return "textbox";
|
|
513
|
+
if (tag === "summary") return "button";
|
|
514
|
+
if (el2 instanceof HTMLInputElement) {
|
|
515
|
+
const t = el2.type;
|
|
516
|
+
if (t === "checkbox" || t === "radio") return t;
|
|
517
|
+
if (t === "submit" || t === "button" || t === "reset" || t === "image") return "button";
|
|
518
|
+
if (t === "search") return "searchbox";
|
|
519
|
+
if (t === "range") return "slider";
|
|
520
|
+
return t === "text" ? "textbox" : `textbox:${t}`;
|
|
521
|
+
}
|
|
522
|
+
if (el2.isContentEditable) return "textbox";
|
|
523
|
+
return tag;
|
|
524
|
+
}
|
|
525
|
+
function accessibleName(el2) {
|
|
526
|
+
const labelledBy = el2.getAttribute("aria-labelledby");
|
|
527
|
+
if (labelledBy) {
|
|
528
|
+
const text2 = labelledBy.split(/\s+/).map((id) => squash(document.getElementById(id)?.textContent)).filter(Boolean).join(" ");
|
|
529
|
+
if (text2) return text2;
|
|
530
|
+
}
|
|
531
|
+
const aria = squash(el2.getAttribute("aria-label"));
|
|
532
|
+
if (aria) return aria;
|
|
533
|
+
const labels = el2.labels;
|
|
534
|
+
if (labels?.length) {
|
|
535
|
+
const text2 = squash([...labels].map((l) => l.textContent).join(" "));
|
|
536
|
+
if (text2) return text2;
|
|
537
|
+
}
|
|
538
|
+
if (el2 instanceof HTMLInputElement && ["submit", "button", "reset"].includes(el2.type)) return squash(el2.value);
|
|
539
|
+
const text = el2 instanceof HTMLSelectElement || el2 instanceof HTMLTextAreaElement || el2 instanceof HTMLInputElement ? "" : squash(el2.textContent);
|
|
540
|
+
return text || squash(el2.getAttribute("placeholder")) || squash(el2.getAttribute("title")) || squash(el2.querySelector("img[alt]")?.getAttribute("alt")) || squash(el2.getAttribute("name"));
|
|
541
|
+
}
|
|
542
|
+
function describe(el2) {
|
|
543
|
+
const parts = [role(el2), JSON.stringify(clip(accessibleName(el2), MAX_NAME))];
|
|
544
|
+
if (el2 instanceof HTMLInputElement && (el2.type === "checkbox" || el2.type === "radio")) parts.push(el2.checked ? "checked" : "unchecked");
|
|
545
|
+
else if (el2 instanceof HTMLInputElement || el2 instanceof HTMLTextAreaElement || el2 instanceof HTMLSelectElement) {
|
|
546
|
+
const v = currentValue(el2);
|
|
547
|
+
if (v && !(el2 instanceof HTMLInputElement && ["submit", "button", "reset"].includes(el2.type))) parts.push(`value=${JSON.stringify(clip(v, MAX_VALUE))}`);
|
|
548
|
+
}
|
|
549
|
+
if (el2 instanceof HTMLAnchorElement) parts.push(`href=${JSON.stringify(clip(el2.getAttribute("href") ?? "", MAX_NAME))}`);
|
|
550
|
+
if (isDisabled(el2)) parts.push("disabled");
|
|
551
|
+
return parts.join(" ");
|
|
552
|
+
}
|
|
553
|
+
function scroller(root) {
|
|
554
|
+
if (root === document.body || root === document.documentElement) return null;
|
|
555
|
+
return root.scrollHeight > root.clientHeight ? root : null;
|
|
556
|
+
}
|
|
557
|
+
function scrollState(root) {
|
|
558
|
+
const box = scroller(root);
|
|
559
|
+
if (box === null) return { scrollY: Math.round(window.scrollY), maxScrollY: Math.max(0, Math.round(document.documentElement.scrollHeight - window.innerHeight)) };
|
|
560
|
+
return { scrollY: Math.round(box.scrollTop), maxScrollY: Math.max(0, Math.round(box.scrollHeight - box.clientHeight)) };
|
|
561
|
+
}
|
|
562
|
+
|
|
282
563
|
// src/widget/api-client.ts
|
|
283
564
|
var RENEW_CODES = /* @__PURE__ */ new Set(["LINK_EXPIRED", "LINK_REVOKED"]);
|
|
284
565
|
var VERIFIED_REFRESH_MS = 6e4;
|
|
@@ -333,6 +614,8 @@ var ApiClient = class {
|
|
|
333
614
|
session = null;
|
|
334
615
|
config = null;
|
|
335
616
|
sessionPromise = null;
|
|
617
|
+
supersededSessions = /* @__PURE__ */ new Map();
|
|
618
|
+
sessionGeneration = 0;
|
|
336
619
|
url(action, qs) {
|
|
337
620
|
const base = this.o.endpoint.replace(/\/+$/, "");
|
|
338
621
|
const q = qs ? "?" + new URLSearchParams(qs).toString() : "";
|
|
@@ -367,7 +650,7 @@ var ApiClient = class {
|
|
|
367
650
|
h[HEADER_GATEWAY_TOKEN] = this.o.gatewayToken;
|
|
368
651
|
}
|
|
369
652
|
if (auth) {
|
|
370
|
-
const s =
|
|
653
|
+
const s = await this.ensureSession();
|
|
371
654
|
h.authorization = `Bearer ${s.token}`;
|
|
372
655
|
}
|
|
373
656
|
return h;
|
|
@@ -381,16 +664,24 @@ var ApiClient = class {
|
|
|
381
664
|
}
|
|
382
665
|
return body;
|
|
383
666
|
}
|
|
384
|
-
|
|
667
|
+
/** Obtain a short-lived, URL-bound image capability without exposing the visitor bearer in src. */
|
|
668
|
+
async imageUrl(source, signal) {
|
|
669
|
+
const res = await this.request(this.url("image-ticket"), { method: "POST", headers: await this.headers(true), body: JSON.stringify({ url: source }), redirect: "error", signal });
|
|
670
|
+
const data = await this.parse(res);
|
|
671
|
+
if (typeof data.ticket !== "string" || !/^[A-Za-z0-9_-]{1,8192}$/.test(data.ticket)) throw new Error("Invalid image ticket");
|
|
672
|
+
return this.url("image", { ticket: data.ticket });
|
|
673
|
+
}
|
|
674
|
+
async getConfig(force = false, probePageContext = false) {
|
|
385
675
|
if (this.config && !force) return this.config;
|
|
386
|
-
this.config = await this.parse(await this.request(this.url("config"), { headers: await this.headers() }));
|
|
676
|
+
this.config = await this.parse(await this.request(this.url("config", probePageContext ? { pageContext: "1" } : void 0), { headers: await this.headers() }));
|
|
387
677
|
return this.config;
|
|
388
678
|
}
|
|
389
679
|
/** When each signed-in session should be refreshed, on this browser's clock. */
|
|
390
680
|
refreshAt = /* @__PURE__ */ new WeakMap();
|
|
391
681
|
/**
|
|
392
682
|
* Session reuse. A signed-in session follows its schedule (see `scheduleRefresh`). An anonymous
|
|
393
|
-
* token is refreshed once less than 10% of its lifetime (at most 24 h, at least 30 s
|
|
683
|
+
* token is refreshed once less than 10% of its lifetime (at most 24 h, at least 30 s, and no more
|
|
684
|
+
* than half the lifetime) remains; a
|
|
394
685
|
* stored token from before `obtainedAt` existed uses the 24 h margin. When the backend reports the
|
|
395
686
|
* lifetime (`expiresIn`), it is counted from receipt, so a skewed browser clock does not matter.
|
|
396
687
|
*/
|
|
@@ -399,7 +690,7 @@ var ApiClient = class {
|
|
|
399
690
|
const reported = typeof s.expiresIn === "number" && s.expiresIn >= 0 && s.obtainedAt;
|
|
400
691
|
const end = reported ? s.obtainedAt + s.expiresIn : s.expiresAt;
|
|
401
692
|
const lifetime = reported ? s.expiresIn : s.obtainedAt ? s.expiresAt - s.obtainedAt : Infinity;
|
|
402
|
-
const margin = Math.min(864e5, Math.max(3e4, lifetime * 0.1));
|
|
693
|
+
const margin = Math.min(864e5, lifetime / 2, Math.max(3e4, lifetime * 0.1));
|
|
403
694
|
return end - Date.now() > margin;
|
|
404
695
|
}
|
|
405
696
|
/**
|
|
@@ -417,28 +708,50 @@ var ApiClient = class {
|
|
|
417
708
|
if (!saved) return null;
|
|
418
709
|
try {
|
|
419
710
|
const s = JSON.parse(saved);
|
|
420
|
-
|
|
711
|
+
if (s.verified) {
|
|
712
|
+
store.del(this.storageKey);
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
return s;
|
|
421
716
|
} catch {
|
|
422
717
|
return null;
|
|
423
718
|
}
|
|
424
719
|
}
|
|
720
|
+
/** The visitor of the session in use, or null before one is held. Backend conversations are keyed by it. */
|
|
721
|
+
visitorId() {
|
|
722
|
+
return this.session?.visitorId ?? null;
|
|
723
|
+
}
|
|
425
724
|
/** Returns a valid visitor session, creating or refreshing it when needed. */
|
|
426
725
|
ensureSession() {
|
|
726
|
+
if (this.sessionPromise) return this.sessionPromise;
|
|
427
727
|
if (this.session && this.fresh(this.session)) return Promise.resolve(this.session);
|
|
428
|
-
|
|
728
|
+
const generation = this.sessionGeneration;
|
|
729
|
+
const promise = this.loadSession(generation).catch((error) => {
|
|
730
|
+
if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
|
|
731
|
+
throw error;
|
|
732
|
+
}).finally(() => {
|
|
733
|
+
if (this.sessionPromise === promise) this.sessionPromise = null;
|
|
734
|
+
this.supersededSessions.delete(generation);
|
|
735
|
+
});
|
|
736
|
+
return this.sessionPromise = promise;
|
|
429
737
|
}
|
|
430
|
-
async loadSession() {
|
|
738
|
+
async loadSession(generation) {
|
|
431
739
|
if (!this.session) {
|
|
432
740
|
const s = this.stored();
|
|
433
741
|
if (s) {
|
|
434
|
-
if (this.fresh(s)
|
|
742
|
+
if (this.fresh(s)) {
|
|
743
|
+
const cfg = await this.getConfig();
|
|
744
|
+
if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
|
|
745
|
+
if (!cfg.identity) return this.session = s;
|
|
746
|
+
}
|
|
435
747
|
this.session = s;
|
|
436
748
|
}
|
|
437
749
|
}
|
|
438
|
-
return this.createSession();
|
|
750
|
+
return this.createSession(generation);
|
|
439
751
|
}
|
|
440
|
-
async createSession() {
|
|
752
|
+
async createSession(generation) {
|
|
441
753
|
const cfg = await this.getConfig();
|
|
754
|
+
if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
|
|
442
755
|
const body = {};
|
|
443
756
|
const previous = this.session && !this.session.verified ? this.session : this.stored();
|
|
444
757
|
if (previous?.token) body.token = previous.token;
|
|
@@ -462,9 +775,12 @@ var ApiClient = class {
|
|
|
462
775
|
s = await post();
|
|
463
776
|
}
|
|
464
777
|
} catch (e) {
|
|
778
|
+
if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
|
|
779
|
+
this.session = null;
|
|
465
780
|
if (e instanceof ApiError && e.status === 401) tabStore.del(this.reuseKey);
|
|
466
781
|
throw e;
|
|
467
782
|
}
|
|
783
|
+
if (generation !== this.sessionGeneration) return this.supersededSessions.get(generation);
|
|
468
784
|
const now = Date.now();
|
|
469
785
|
const held = { ...s, obtainedAt: now };
|
|
470
786
|
this.session = held;
|
|
@@ -494,8 +810,14 @@ var ApiClient = class {
|
|
|
494
810
|
* stored anonymous visitor id is kept, so an anonymous visitor's history survives.
|
|
495
811
|
*/
|
|
496
812
|
async renewSession() {
|
|
813
|
+
const previousGeneration = this.sessionGeneration;
|
|
814
|
+
const previous = this.sessionPromise;
|
|
815
|
+
this.sessionGeneration++;
|
|
497
816
|
this.session = null;
|
|
498
|
-
|
|
817
|
+
this.sessionPromise = null;
|
|
818
|
+
const next = this.ensureSession();
|
|
819
|
+
if (previous) this.supersededSessions.set(previousGeneration, next);
|
|
820
|
+
return next;
|
|
499
821
|
}
|
|
500
822
|
/** Requests a single-use launch ticket. Does not refresh or swap the signed-in session after refusal. */
|
|
501
823
|
async jobAccess(signal) {
|
|
@@ -547,8 +869,10 @@ var ApiClient = class {
|
|
|
547
869
|
await this.ensureSession();
|
|
548
870
|
let res = await this.request(this.url("chat"), { method: "POST", headers: await this.headers(true), body: JSON.stringify(input), signal });
|
|
549
871
|
if (res.status === 401) {
|
|
872
|
+
const before = this.visitorId();
|
|
550
873
|
this.forgetSession();
|
|
551
874
|
await this.ensureSession();
|
|
875
|
+
if (input.actionsHash && this.visitorId() !== before) input = { ...input, actionsHash: void 0 };
|
|
552
876
|
res = await this.request(this.url("chat"), { method: "POST", headers: await this.headers(true), body: JSON.stringify(input), signal });
|
|
553
877
|
}
|
|
554
878
|
if (!res.ok || !res.body) await this.parse(res);
|
|
@@ -821,7 +1145,10 @@ var en = {
|
|
|
821
1145
|
jobAccessOpened: "Job access opened in a new window. Review and confirm any permissions there.",
|
|
822
1146
|
jobAccessUnavailable: "Job access could not be opened. Sign in again or retry.",
|
|
823
1147
|
jobAccessPopupBlocked: "Allow popups for this site, then choose Job access again.",
|
|
824
|
-
jobAccessPopupClosed: "The window was closed. Choose Job access again to retry."
|
|
1148
|
+
jobAccessPopupClosed: "The window was closed. Choose Job access again to retry.",
|
|
1149
|
+
pageShare: "Share this page with the assistant for this message",
|
|
1150
|
+
pageChanged: "The page changed. Review it and choose to share again before sending.",
|
|
1151
|
+
pageUnavailable: "Page details are unavailable. You can send without them."
|
|
825
1152
|
};
|
|
826
1153
|
var vi = {
|
|
827
1154
|
title: "Tr\u1EE3 l\xFD",
|
|
@@ -881,7 +1208,10 @@ var vi = {
|
|
|
881
1208
|
jobAccessOpened: "\u0110\xE3 m\u1EDF c\u1EEDa s\u1ED5 quy\u1EC1n c\xF4ng vi\u1EC7c. H\xE3y xem l\u1EA1i v\xE0 x\xE1c nh\u1EADn quy\u1EC1n t\u1EA1i \u0111\xF3.",
|
|
882
1209
|
jobAccessUnavailable: "Ch\u01B0a m\u1EDF \u0111\u01B0\u1EE3c quy\u1EC1n c\xF4ng vi\u1EC7c. H\xE3y \u0111\u0103ng nh\u1EADp l\u1EA1i ho\u1EB7c th\u1EED l\u1EA1i.",
|
|
883
1210
|
jobAccessPopupBlocked: "Cho ph\xE9p c\u1EEDa s\u1ED5 b\u1EADt l\xEAn cho trang n\xE0y r\u1ED3i ch\u1ECDn Quy\u1EC1n c\xF4ng vi\u1EC7c l\u1EA7n n\u1EEFa.",
|
|
884
|
-
jobAccessPopupClosed: "C\u1EEDa s\u1ED5 \u0111\xE3 \u0111\xF3ng. Ch\u1ECDn Quy\u1EC1n c\xF4ng vi\u1EC7c l\u1EA7n n\u1EEFa \u0111\u1EC3 th\u1EED l\u1EA1i."
|
|
1211
|
+
jobAccessPopupClosed: "C\u1EEDa s\u1ED5 \u0111\xE3 \u0111\xF3ng. Ch\u1ECDn Quy\u1EC1n c\xF4ng vi\u1EC7c l\u1EA7n n\u1EEFa \u0111\u1EC3 th\u1EED l\u1EA1i.",
|
|
1212
|
+
pageShare: "Chia s\u1EBB trang n\xE0y v\u1EDBi tr\u1EE3 l\xFD trong tin nh\u1EAFn n\xE0y",
|
|
1213
|
+
pageChanged: "Trang \u0111\xE3 thay \u0111\u1ED5i. H\xE3y xem l\u1EA1i v\xE0 ch\u1ECDn chia s\u1EBB tr\u01B0\u1EDBc khi g\u1EEDi.",
|
|
1214
|
+
pageUnavailable: "Kh\xF4ng th\u1EC3 \u0111\u1ECDc n\u1ED9i dung trang. B\u1EA1n v\u1EABn c\xF3 th\u1EC3 g\u1EEDi tin nh\u1EAFn."
|
|
885
1215
|
};
|
|
886
1216
|
function pickStrings(locale) {
|
|
887
1217
|
const l = (locale || (typeof document !== "undefined" ? document.documentElement.lang : "") || (typeof navigator !== "undefined" ? navigator.language : "") || "en").toLowerCase();
|
|
@@ -901,6 +1231,40 @@ var icons = {
|
|
|
901
1231
|
};
|
|
902
1232
|
|
|
903
1233
|
// src/widget/markdown.ts
|
|
1234
|
+
var imageObservers = /* @__PURE__ */ new WeakMap();
|
|
1235
|
+
var imageLoads = /* @__PURE__ */ new WeakMap();
|
|
1236
|
+
function disposeMarkdownImages(root) {
|
|
1237
|
+
for (const image of root.querySelectorAll("img.md-image")) {
|
|
1238
|
+
imageObservers.get(image)?.disconnect();
|
|
1239
|
+
imageObservers.delete(image);
|
|
1240
|
+
imageLoads.get(image)?.abort();
|
|
1241
|
+
imageLoads.delete(image);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function safeImageSrc(doc, raw, allowed) {
|
|
1245
|
+
try {
|
|
1246
|
+
if (!raw || /[\u0000-\u0020\u007f\\]/.test(raw) || raw.startsWith("//")) return null;
|
|
1247
|
+
const decoded = decodeURIComponent(raw);
|
|
1248
|
+
if (/[\u0000-\u0020\u007f\\]/.test(decoded) || decoded.startsWith("//")) return null;
|
|
1249
|
+
const page = new URL(doc.defaultView?.location.href ?? doc.baseURI);
|
|
1250
|
+
const url = new URL(raw, doc.baseURI);
|
|
1251
|
+
if (url.username || url.password || !["http:", "https:"].includes(url.protocol)) return null;
|
|
1252
|
+
if (/\.svg$/i.test(decodeURIComponent(url.pathname))) return null;
|
|
1253
|
+
if (url.origin === page.origin) return url.href;
|
|
1254
|
+
if (url.protocol !== "https:") return null;
|
|
1255
|
+
const trusted = allowed.some((item) => {
|
|
1256
|
+
try {
|
|
1257
|
+
const origin = new URL(item);
|
|
1258
|
+
return origin.protocol === "https:" && !origin.username && !origin.password && origin.href === origin.origin + "/" && origin.origin === url.origin;
|
|
1259
|
+
} catch {
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
return trusted ? url.href : null;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
904
1268
|
function el(doc, tag, cls) {
|
|
905
1269
|
const e = doc.createElement(tag);
|
|
906
1270
|
if (cls) e.className = cls;
|
|
@@ -912,45 +1276,87 @@ function safeHref(url) {
|
|
|
912
1276
|
if (/^[/#?]/.test(u) && !u.startsWith("//")) return u;
|
|
913
1277
|
return null;
|
|
914
1278
|
}
|
|
915
|
-
var INLINE_SRC =
|
|
916
|
-
function renderInline(doc, text, parent) {
|
|
1279
|
+
var INLINE_SRC = /\\!\[[^\]]*\]\([^)\s]+\)|(`+)([\s\S]*?[^`])\1(?!`)|\*\*([^*]+?)\*\*|__([^_]+?)__|~~([^~]+?)~~|\*([^*\s][^*]*?)\*|(?<![\w])_([^_\s][^_]*?)_(?![\w])|(?<!\\)!\[([^\]]*)\]\(([^)\s]+)\)|\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/;
|
|
1280
|
+
function renderInline(doc, text, parent, options = {}) {
|
|
917
1281
|
const re = new RegExp(INLINE_SRC.source, "g");
|
|
918
1282
|
let last = 0;
|
|
919
1283
|
for (let m; m = re.exec(text); ) {
|
|
920
1284
|
if (m.index > last) parent.appendChild(doc.createTextNode(text.slice(last, m.index)));
|
|
921
1285
|
last = m.index + m[0].length;
|
|
922
|
-
if (m[
|
|
1286
|
+
if (m[0].startsWith("\\!")) {
|
|
1287
|
+
parent.appendChild(doc.createTextNode(m[0].slice(1)));
|
|
1288
|
+
} else if (m[2] !== void 0) {
|
|
923
1289
|
const c = el(doc, "code");
|
|
924
1290
|
c.textContent = m[2];
|
|
925
1291
|
parent.appendChild(c);
|
|
926
1292
|
} else if (m[3] !== void 0 || m[4] !== void 0) {
|
|
927
1293
|
const s = el(doc, "strong");
|
|
928
|
-
renderInline(doc, m[3] ?? m[4], s);
|
|
1294
|
+
renderInline(doc, m[3] ?? m[4], s, options);
|
|
929
1295
|
parent.appendChild(s);
|
|
930
1296
|
} else if (m[5] !== void 0) {
|
|
931
1297
|
const s = el(doc, "del");
|
|
932
|
-
renderInline(doc, m[5], s);
|
|
1298
|
+
renderInline(doc, m[5], s, options);
|
|
933
1299
|
parent.appendChild(s);
|
|
934
1300
|
} else if (m[6] !== void 0 || m[7] !== void 0) {
|
|
935
1301
|
const s = el(doc, "em");
|
|
936
|
-
renderInline(doc, m[6] ?? m[7], s);
|
|
1302
|
+
renderInline(doc, m[6] ?? m[7], s, options);
|
|
937
1303
|
parent.appendChild(s);
|
|
938
1304
|
} else if (m[8] !== void 0) {
|
|
939
|
-
const
|
|
1305
|
+
const original = options.deferImages ? null : safeImageSrc(doc, m[9], options.imageOrigins ?? []);
|
|
1306
|
+
if (!original || !options.imageProxy) parent.appendChild(doc.createTextNode(m[8]));
|
|
1307
|
+
else {
|
|
1308
|
+
const img = el(doc, "img", "md-image");
|
|
1309
|
+
img.alt = m[8];
|
|
1310
|
+
img.setAttribute("loading", "lazy");
|
|
1311
|
+
img.setAttribute("decoding", "async");
|
|
1312
|
+
img.referrerPolicy = "no-referrer";
|
|
1313
|
+
let retried = false;
|
|
1314
|
+
const load = () => {
|
|
1315
|
+
const controller = new AbortController();
|
|
1316
|
+
imageLoads.set(img, controller);
|
|
1317
|
+
return options.imageProxy(original, controller.signal).then((src) => {
|
|
1318
|
+
if (!controller.signal.aborted && img.isConnected) img.src = src;
|
|
1319
|
+
}).catch(() => {
|
|
1320
|
+
if (!controller.signal.aborted && img.isConnected) img.replaceWith(doc.createTextNode(img.alt));
|
|
1321
|
+
}).finally(() => {
|
|
1322
|
+
if (imageLoads.get(img) === controller) imageLoads.delete(img);
|
|
1323
|
+
});
|
|
1324
|
+
};
|
|
1325
|
+
img.addEventListener("error", () => {
|
|
1326
|
+
if (!retried) {
|
|
1327
|
+
retried = true;
|
|
1328
|
+
void load();
|
|
1329
|
+
} else img.replaceWith(doc.createTextNode(img.alt));
|
|
1330
|
+
});
|
|
1331
|
+
parent.appendChild(img);
|
|
1332
|
+
const Observer = doc.defaultView?.IntersectionObserver;
|
|
1333
|
+
if (Observer) {
|
|
1334
|
+
const observer = new Observer((entries) => {
|
|
1335
|
+
if (!entries.some((entry) => entry.isIntersecting)) return;
|
|
1336
|
+
observer.disconnect();
|
|
1337
|
+
imageObservers.delete(img);
|
|
1338
|
+
if (img.parentNode) void load();
|
|
1339
|
+
}, { rootMargin: "200px" });
|
|
1340
|
+
imageObservers.set(img, observer);
|
|
1341
|
+
observer.observe(img);
|
|
1342
|
+
} else void load();
|
|
1343
|
+
}
|
|
1344
|
+
} else if (m[10] !== void 0) {
|
|
1345
|
+
const href = safeHref(m[11]);
|
|
940
1346
|
if (href) {
|
|
941
1347
|
const a = el(doc, "a");
|
|
942
1348
|
a.href = href;
|
|
943
1349
|
a.target = "_blank";
|
|
944
1350
|
a.rel = "noopener noreferrer nofollow";
|
|
945
|
-
renderInline(doc, m[
|
|
1351
|
+
renderInline(doc, m[10], a, options);
|
|
946
1352
|
parent.appendChild(a);
|
|
947
|
-
} else parent.appendChild(doc.createTextNode(m[
|
|
948
|
-
} else if (m[
|
|
1353
|
+
} else parent.appendChild(doc.createTextNode(m[10]));
|
|
1354
|
+
} else if (m[12] !== void 0) {
|
|
949
1355
|
const a = el(doc, "a");
|
|
950
|
-
a.href = m[
|
|
1356
|
+
a.href = m[12];
|
|
951
1357
|
a.target = "_blank";
|
|
952
1358
|
a.rel = "noopener noreferrer nofollow";
|
|
953
|
-
a.textContent = m[
|
|
1359
|
+
a.textContent = m[12];
|
|
954
1360
|
parent.appendChild(a);
|
|
955
1361
|
}
|
|
956
1362
|
}
|
|
@@ -958,7 +1364,7 @@ function renderInline(doc, text, parent) {
|
|
|
958
1364
|
}
|
|
959
1365
|
var isTableSep = (l) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(l);
|
|
960
1366
|
var splitRow = (l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
|
|
961
|
-
function renderMarkdown(doc, src) {
|
|
1367
|
+
function renderMarkdown(doc, src, options = {}) {
|
|
962
1368
|
const frag = doc.createDocumentFragment();
|
|
963
1369
|
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
964
1370
|
let i = 0;
|
|
@@ -986,7 +1392,7 @@ function renderMarkdown(doc, src) {
|
|
|
986
1392
|
if (h) {
|
|
987
1393
|
const tag = `h${Math.min(6, h[1].length + 2)}`;
|
|
988
1394
|
const e = el(doc, tag);
|
|
989
|
-
renderInline(doc, h[2], e);
|
|
1395
|
+
renderInline(doc, h[2], e, options);
|
|
990
1396
|
frag.appendChild(e);
|
|
991
1397
|
i++;
|
|
992
1398
|
continue;
|
|
@@ -1003,7 +1409,7 @@ function renderMarkdown(doc, src) {
|
|
|
1003
1409
|
const tr = el(doc, "tr");
|
|
1004
1410
|
for (const c of splitRow(line)) {
|
|
1005
1411
|
const th = el(doc, "th");
|
|
1006
|
-
renderInline(doc, c, th);
|
|
1412
|
+
renderInline(doc, c, th, options);
|
|
1007
1413
|
tr.appendChild(th);
|
|
1008
1414
|
}
|
|
1009
1415
|
thead.appendChild(tr);
|
|
@@ -1014,7 +1420,7 @@ function renderMarkdown(doc, src) {
|
|
|
1014
1420
|
const r = el(doc, "tr");
|
|
1015
1421
|
for (const c of splitRow(lines[i])) {
|
|
1016
1422
|
const td = el(doc, "td");
|
|
1017
|
-
renderInline(doc, c, td);
|
|
1423
|
+
renderInline(doc, c, td, options);
|
|
1018
1424
|
r.appendChild(td);
|
|
1019
1425
|
}
|
|
1020
1426
|
tbody.appendChild(r);
|
|
@@ -1029,7 +1435,7 @@ function renderMarkdown(doc, src) {
|
|
|
1029
1435
|
const body = [];
|
|
1030
1436
|
while (i < lines.length && /^\s*>/.test(lines[i])) body.push(lines[i++].replace(/^\s*>\s?/, ""));
|
|
1031
1437
|
const q = el(doc, "blockquote");
|
|
1032
|
-
q.appendChild(renderMarkdown(doc, body.join("\n")));
|
|
1438
|
+
q.appendChild(renderMarkdown(doc, body.join("\n"), options));
|
|
1033
1439
|
frag.appendChild(q);
|
|
1034
1440
|
continue;
|
|
1035
1441
|
}
|
|
@@ -1045,11 +1451,11 @@ function renderMarkdown(doc, src) {
|
|
|
1045
1451
|
if (m[1].length > baseIndent) {
|
|
1046
1452
|
const sub = [];
|
|
1047
1453
|
while (i < lines.length && (/^\s+/.test(lines[i]) && lines[i].match(/^(\s*)/)[1].length > baseIndent)) sub.push(lines[i++].slice(baseIndent + 2));
|
|
1048
|
-
list.lastElementChild?.appendChild(renderMarkdown(doc, sub.join("\n")));
|
|
1454
|
+
list.lastElementChild?.appendChild(renderMarkdown(doc, sub.join("\n"), options));
|
|
1049
1455
|
continue;
|
|
1050
1456
|
}
|
|
1051
1457
|
const item = el(doc, "li");
|
|
1052
|
-
renderInline(doc, m[3], item);
|
|
1458
|
+
renderInline(doc, m[3], item, options);
|
|
1053
1459
|
list.appendChild(item);
|
|
1054
1460
|
i++;
|
|
1055
1461
|
}
|
|
@@ -1062,7 +1468,7 @@ function renderMarkdown(doc, src) {
|
|
|
1062
1468
|
const p = el(doc, "p");
|
|
1063
1469
|
para.forEach((l, idx) => {
|
|
1064
1470
|
if (idx) p.appendChild(el(doc, "br"));
|
|
1065
|
-
renderInline(doc, l, p);
|
|
1471
|
+
renderInline(doc, l, p, options);
|
|
1066
1472
|
});
|
|
1067
1473
|
frag.appendChild(p);
|
|
1068
1474
|
}
|
|
@@ -1260,6 +1666,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
1260
1666
|
.md li { margin: 3px 0; }
|
|
1261
1667
|
.md h3, .md h4, .md h5, .md h6 { margin: 14px 0 6px; font-size: 15px; line-height: 1.35; }
|
|
1262
1668
|
.md a { color: var(--dw-accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
1669
|
+
.md .md-image { display: block; max-width: 100%; max-height: 320px; min-height: 48px; width: auto; height: auto; object-fit: contain; border-radius: 8px; }
|
|
1263
1670
|
.md code { font-family: var(--dw-mono); font-size: .86em; background: var(--dw-surface); padding: 1.5px 5px; border-radius: 5px; }
|
|
1264
1671
|
.md pre { background: #12132a; color: #e6e6f5; padding: 12px 14px; border-radius: 12px; overflow-x: auto; font-size: 12.5px; line-height: 1.55; }
|
|
1265
1672
|
.md pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
|
|
@@ -1354,6 +1761,9 @@ details > summary svg.ic { width: 14px; height: 14px; flex: none; }
|
|
|
1354
1761
|
|
|
1355
1762
|
/* ---------- composer ---------- */
|
|
1356
1763
|
.composer { padding: 10px 12px 12px; border-top: 1px solid var(--dw-border); background: var(--dw-bg); }
|
|
1764
|
+
.page-context { font-size: 12px; margin-bottom: 8px; }
|
|
1765
|
+
.page-context label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
|
|
1766
|
+
.page-preview { max-height: 96px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; padding: 6px; margin: 6px 0; border: 1px solid var(--dw-border); border-radius: 6px; font: inherit; }
|
|
1357
1767
|
.box {
|
|
1358
1768
|
display: flex; align-items: flex-end; gap: 8px; border: 1px solid var(--dw-border); border-radius: 14px;
|
|
1359
1769
|
padding: 6px 6px 6px 12px; background: var(--dw-bg); transition: border-color .15s, box-shadow .15s;
|
|
@@ -1386,7 +1796,7 @@ textarea::placeholder { color: var(--dw-muted); }
|
|
|
1386
1796
|
|
|
1387
1797
|
// src/widget/element.ts
|
|
1388
1798
|
var DEFAULT_COOLDOWN_MS = 5e3;
|
|
1389
|
-
var BOOL_ATTRS = ["show-reasoning", "show-tool-calls", "allow-agent-switch", "open", "persist", "hide-branding", "launcher-avatar"];
|
|
1799
|
+
var BOOL_ATTRS = ["show-reasoning", "show-tool-calls", "allow-agent-switch", "open", "persist", "hide-branding", "launcher-avatar", "dom-actions"];
|
|
1390
1800
|
var ATTRS = [
|
|
1391
1801
|
"endpoint",
|
|
1392
1802
|
"agent",
|
|
@@ -1407,6 +1817,11 @@ var ATTRS = [
|
|
|
1407
1817
|
"width",
|
|
1408
1818
|
"height",
|
|
1409
1819
|
"z-index",
|
|
1820
|
+
"dom-actions-root",
|
|
1821
|
+
"image-origins",
|
|
1822
|
+
"page-context",
|
|
1823
|
+
"context-selector",
|
|
1824
|
+
"exclude-selectors",
|
|
1410
1825
|
...BOOL_ATTRS
|
|
1411
1826
|
];
|
|
1412
1827
|
var camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -1454,17 +1869,24 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1454
1869
|
api = null;
|
|
1455
1870
|
apiKey = "";
|
|
1456
1871
|
server = null;
|
|
1872
|
+
sharedPage;
|
|
1873
|
+
preparingPage = false;
|
|
1874
|
+
pagePending = false;
|
|
1875
|
+
pageGeneration = 0;
|
|
1457
1876
|
agentId = "";
|
|
1458
1877
|
conversationId = "";
|
|
1459
1878
|
messages = [];
|
|
1460
1879
|
running = null;
|
|
1461
1880
|
jobAccessLaunch = null;
|
|
1462
1881
|
activated = false;
|
|
1882
|
+
probedPage = false;
|
|
1463
1883
|
strings = pickStrings();
|
|
1464
1884
|
mql = null;
|
|
1465
1885
|
registry = new ActionRegistry();
|
|
1466
1886
|
propActionNames = /* @__PURE__ */ new Set();
|
|
1467
1887
|
hooks = {};
|
|
1888
|
+
/** Built-in DOM actions, created on first use when `domActions` is on (keeps the snapshot refs). */
|
|
1889
|
+
dom = null;
|
|
1468
1890
|
acks = /* @__PURE__ */ new Map();
|
|
1469
1891
|
/** Pause after a RATE_LIMITED refusal; send stays disabled until it ends. */
|
|
1470
1892
|
cooldown = new SendCooldown((s) => this.renderCooldown(s), () => this.endCooldown());
|
|
@@ -1482,6 +1904,8 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1482
1904
|
if (v === null) continue;
|
|
1483
1905
|
if (BOOL_ATTRS.includes(a)) c[camel(a)] = parseBool(v);
|
|
1484
1906
|
else if (a === "suggestions") c.suggestions = v.trim().startsWith("[") ? safeJson(v, []) : v.split("|").map((s) => s.trim()).filter(Boolean);
|
|
1907
|
+
else if (a === "image-origins") c.imageOrigins = v.trim().startsWith("[") ? safeJson(v, []) : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1908
|
+
else if (a === "exclude-selectors") c.excludeSelectors = v.trim().startsWith("[") ? safeJson(v, []) : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1485
1909
|
else if (a === "z-index") c.zIndex = Number(v);
|
|
1486
1910
|
else c[camel(a)] = v;
|
|
1487
1911
|
}
|
|
@@ -1489,13 +1913,18 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1489
1913
|
}
|
|
1490
1914
|
/** Updates configuration at runtime (wins over attributes). */
|
|
1491
1915
|
configure(partial) {
|
|
1492
|
-
const { actions, confirm, getPageContext, headers, fetch, ...rest } = partial;
|
|
1916
|
+
const { actions, confirm, getPageContext, headers, fetch, domActionsRoot, contextProvider, ...rest } = partial;
|
|
1493
1917
|
const has = (k) => Object.prototype.hasOwnProperty.call(partial, k);
|
|
1494
1918
|
if (has("actions")) this.setPropActions(actions ?? []);
|
|
1495
1919
|
if (has("confirm")) this.hooks.confirm = confirm;
|
|
1496
1920
|
if (has("getPageContext")) this.hooks.getPageContext = getPageContext;
|
|
1497
1921
|
if (has("headers")) this.hooks.headers = headers;
|
|
1498
1922
|
if (has("fetch")) this.hooks.fetch = fetch;
|
|
1923
|
+
if (has("contextProvider")) this.hooks.contextProvider = contextProvider;
|
|
1924
|
+
if (has("domActionsRoot")) {
|
|
1925
|
+
if (domActionsRoot === void 0) delete this.hooks.domActionsRoot;
|
|
1926
|
+
else this.hooks.domActionsRoot = domActionsRoot;
|
|
1927
|
+
}
|
|
1499
1928
|
if (!Object.keys(rest).length) return;
|
|
1500
1929
|
this.overrides = { ...this.overrides, ...rest };
|
|
1501
1930
|
if (this.els) this.applyConfig();
|
|
@@ -1523,9 +1952,23 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1523
1952
|
set fetch(v) {
|
|
1524
1953
|
this.hooks.fetch = v;
|
|
1525
1954
|
}
|
|
1526
|
-
/** Names of the
|
|
1955
|
+
/** Names of the page actions offered to the agent (host actions, then enabled built-ins). */
|
|
1527
1956
|
get actionNames() {
|
|
1528
|
-
return this.
|
|
1957
|
+
return this.actionRegistry().names();
|
|
1958
|
+
}
|
|
1959
|
+
/** Host actions plus the enabled built-in DOM actions whose names the host did not take. */
|
|
1960
|
+
actionRegistry() {
|
|
1961
|
+
if (!this.config.domActions) return this.registry;
|
|
1962
|
+
this.dom ??= createDomActions({
|
|
1963
|
+
root: () => this.hooks.domActionsRoot ?? this.getAttribute("dom-actions-root") ?? void 0,
|
|
1964
|
+
exclude: (el2) => el2 === this || this.contains(el2) || el2.closest("dewee-chat") !== null
|
|
1965
|
+
});
|
|
1966
|
+
const host = this.registry.names().map((n) => this.registry.get(n));
|
|
1967
|
+
const room = ACTION_LIMITS.maxActions - host.length;
|
|
1968
|
+
const builtins = this.dom.actions.filter((a) => !this.registry.get(a.name)).slice(0, Math.max(0, room));
|
|
1969
|
+
const all = new ActionRegistry();
|
|
1970
|
+
all.register([...host, ...builtins]);
|
|
1971
|
+
return all;
|
|
1529
1972
|
}
|
|
1530
1973
|
setPropActions(actions) {
|
|
1531
1974
|
const next = new Set(actions.map((a) => a.name));
|
|
@@ -1554,16 +1997,49 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1554
1997
|
/** Sends a message programmatically (opens the widget first). */
|
|
1555
1998
|
async send(text) {
|
|
1556
1999
|
const message = text.trim();
|
|
1557
|
-
if (!message || this.running || this.cooldown.active) return;
|
|
2000
|
+
if (!message || this.running || this.preparingPage || this.cooldown.active) return;
|
|
1558
2001
|
if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.open();
|
|
1559
2002
|
await this.activate();
|
|
2003
|
+
let attachment;
|
|
2004
|
+
if (this.els.pageChoice.checked) {
|
|
2005
|
+
if (this.pagePending) {
|
|
2006
|
+
this.showNotice(this.strings.pageUnavailable);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
this.preparingPage = true;
|
|
2010
|
+
const generation = this.pageGeneration;
|
|
2011
|
+
try {
|
|
2012
|
+
const previous = this.sharedPage;
|
|
2013
|
+
const current = await this.captureCurrentPage();
|
|
2014
|
+
if (!this.els.pageChoice.checked || generation !== this.pageGeneration) return;
|
|
2015
|
+
if (!previous || !current || this.pageFingerprint(previous) !== this.pageFingerprint(current)) {
|
|
2016
|
+
this.sharedPage = current;
|
|
2017
|
+
this.renderPagePreview();
|
|
2018
|
+
this.els.pageChoice.checked = false;
|
|
2019
|
+
this.showNotice(this.strings.pageChanged);
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
attachment = current;
|
|
2023
|
+
} catch {
|
|
2024
|
+
this.sharedPage = void 0;
|
|
2025
|
+
this.els.pageChoice.checked = false;
|
|
2026
|
+
this.renderPagePreview();
|
|
2027
|
+
this.showNotice(this.strings.pageUnavailable);
|
|
2028
|
+
return;
|
|
2029
|
+
} finally {
|
|
2030
|
+
this.preparingPage = false;
|
|
2031
|
+
}
|
|
2032
|
+
this.els.pageChoice.checked = false;
|
|
2033
|
+
this.sharedPage = void 0;
|
|
2034
|
+
this.renderPagePreview();
|
|
2035
|
+
}
|
|
1560
2036
|
this.els.input.value = "";
|
|
1561
2037
|
this.autosize();
|
|
1562
2038
|
let next = { message, hidden: false };
|
|
1563
2039
|
for (let round = 0; next; round++) {
|
|
1564
|
-
const reply = await this.runTurn(next.message, next.hidden);
|
|
2040
|
+
const reply = await this.runTurn(next.message, next.hidden, round === 0 ? attachment : void 0);
|
|
1565
2041
|
next = null;
|
|
1566
|
-
if (reply.status !== "done" || reply.stopped || !this.
|
|
2042
|
+
if (reply.status !== "done" || reply.stopped || !this.actionRegistry().size) break;
|
|
1567
2043
|
const { calls } = parseDirectives(reply.content);
|
|
1568
2044
|
if (!calls.length) break;
|
|
1569
2045
|
reply.actions = calls.map((c) => ({ id: c.id, name: c.name, args: c.ok ? c.args : void 0, status: "pending" }));
|
|
@@ -1580,14 +2056,15 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1580
2056
|
}
|
|
1581
2057
|
}
|
|
1582
2058
|
/** One request/response with the agent. */
|
|
1583
|
-
async runTurn(message, hidden) {
|
|
2059
|
+
async runTurn(message, hidden, attachment) {
|
|
1584
2060
|
this.pushMessage({ role: "user", content: message, hidden });
|
|
1585
2061
|
const reply = { role: "assistant", content: "", status: "streaming", startedAt: Date.now() };
|
|
1586
2062
|
this.pushMessage(reply);
|
|
1587
2063
|
this.setRunning(new AbortController());
|
|
1588
2064
|
this.emit("dewee:send", { message, agent: this.agentId, conversationId: this.conversationId, auto: hidden || void 0 });
|
|
1589
2065
|
try {
|
|
1590
|
-
await this.client().
|
|
2066
|
+
await this.client().ensureSession();
|
|
2067
|
+
await this.client().chat({ ...this.chatBody(message), ...attachment ? { page_context: attachment } : {} }, (e) => this.onStreamEvent(reply, e), this.running.signal);
|
|
1591
2068
|
if (reply.status === "streaming") reply.status = "done";
|
|
1592
2069
|
} catch (e) {
|
|
1593
2070
|
if (e.name === "AbortError") {
|
|
@@ -1603,7 +2080,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1603
2080
|
this.updateMessage(reply);
|
|
1604
2081
|
this.setRunning(null);
|
|
1605
2082
|
if (reply.status === "done") {
|
|
1606
|
-
if (reply.actionsHash && !reply.stopped) this.setAck(reply.actionsHash);
|
|
2083
|
+
if (reply.actionsHash && reply.ackVisitor && !reply.stopped) this.setAck(reply.actionsHash, reply.ackVisitor);
|
|
1607
2084
|
this.emit("dewee:reply", { content: reply.content, agent: this.agentId, conversationId: this.conversationId });
|
|
1608
2085
|
if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.setAttribute("data-unread", "");
|
|
1609
2086
|
}
|
|
@@ -1612,9 +2089,11 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1612
2089
|
}
|
|
1613
2090
|
chatBody(message) {
|
|
1614
2091
|
const body = { message, agent: this.agentId, conversationId: this.conversationId };
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
2092
|
+
const registry = this.actionRegistry();
|
|
2093
|
+
if (registry.size) {
|
|
2094
|
+
body.actions = registry.manifest();
|
|
2095
|
+
const visitor = this.client().visitorId();
|
|
2096
|
+
const ack = visitor ? this.getAck(visitor) : void 0;
|
|
1618
2097
|
if (ack) body.actionsHash = ack;
|
|
1619
2098
|
}
|
|
1620
2099
|
const ctx = this.pageContext();
|
|
@@ -1637,15 +2116,21 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1637
2116
|
return void 0;
|
|
1638
2117
|
}
|
|
1639
2118
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
2119
|
+
/**
|
|
2120
|
+
* The backend keys a conversation by visitor as well as by id, so a sign-in, a sign-out or another
|
|
2121
|
+
* user starts a conversation that has not seen the manifest, even with the same conversation id.
|
|
2122
|
+
*/
|
|
2123
|
+
ackKey(visitor) {
|
|
2124
|
+
return `dewee-webchat:actions:${this.config.endpoint}:${this.agentId}:${visitor}:${this.conversationId}`;
|
|
1642
2125
|
}
|
|
1643
|
-
getAck() {
|
|
1644
|
-
|
|
2126
|
+
getAck(visitor) {
|
|
2127
|
+
const key = this.ackKey(visitor);
|
|
2128
|
+
return this.acks.get(key) ?? (this.config.persist !== false ? store2.get(key) ?? void 0 : void 0);
|
|
1645
2129
|
}
|
|
1646
|
-
setAck(hash) {
|
|
1647
|
-
this.
|
|
1648
|
-
|
|
2130
|
+
setAck(hash, visitor) {
|
|
2131
|
+
const key = this.ackKey(visitor);
|
|
2132
|
+
this.acks.set(key, hash);
|
|
2133
|
+
if (this.config.persist !== false) store2.set(key, hash);
|
|
1649
2134
|
}
|
|
1650
2135
|
/** Executes the calls of one answer in order. Resolves null when the visitor stopped the chain. */
|
|
1651
2136
|
async runActions(reply, calls) {
|
|
@@ -1653,15 +2138,16 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1653
2138
|
this.setRunning(ctrl);
|
|
1654
2139
|
const results = [];
|
|
1655
2140
|
const base = { agent: this.agentId, conversationId: this.conversationId };
|
|
2141
|
+
const registry = this.actionRegistry();
|
|
1656
2142
|
try {
|
|
1657
2143
|
for (const [i, call] of calls.entries()) {
|
|
1658
2144
|
const view = reply.actions[i];
|
|
1659
|
-
const sensitivity = call.ok ?
|
|
2145
|
+
const sensitivity = call.ok ? registry.get(call.name)?.sensitivity : void 0;
|
|
1660
2146
|
const args = call.ok ? call.args : void 0;
|
|
1661
2147
|
this.emit("dewee:action", { phase: "start", id: call.id, name: call.name, sensitivity, args, ...base });
|
|
1662
2148
|
const r = await runActionCall(call, {
|
|
1663
|
-
registry
|
|
1664
|
-
confirm: (action, a) => this.confirmAction(action, a, ctrl.signal),
|
|
2149
|
+
registry,
|
|
2150
|
+
confirm: (action, a) => this.confirmAction(registry, action, a, ctrl.signal),
|
|
1665
2151
|
signal: ctrl.signal,
|
|
1666
2152
|
timeoutMs: this.config.actionTimeoutMs,
|
|
1667
2153
|
...base,
|
|
@@ -1680,10 +2166,13 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1680
2166
|
}
|
|
1681
2167
|
return ctrl.signal.aborted ? null : results;
|
|
1682
2168
|
}
|
|
1683
|
-
confirmAction(action, args, signal) {
|
|
1684
|
-
|
|
2169
|
+
confirmAction(registry, action, args, signal) {
|
|
2170
|
+
const builtin = this.dom?.actions.includes(registry.get(action.name));
|
|
2171
|
+
const target = builtin ? this.dom.target(args) : void 0;
|
|
2172
|
+
const shown = target ? { ...args, target } : args;
|
|
2173
|
+
if (this.hooks.confirm) return this.hooks.confirm(action, shown);
|
|
1685
2174
|
if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.open();
|
|
1686
|
-
return showConfirmDialog(this.els.panel, this.strings, action,
|
|
2175
|
+
return showConfirmDialog(this.els.panel, this.strings, action, shown, signal);
|
|
1687
2176
|
}
|
|
1688
2177
|
showNotice(text) {
|
|
1689
2178
|
const div = document.createElement("div");
|
|
@@ -1811,6 +2300,12 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1811
2300
|
}
|
|
1812
2301
|
}
|
|
1813
2302
|
disconnectedCallback() {
|
|
2303
|
+
this.pageGeneration++;
|
|
2304
|
+
this.sharedPage = void 0;
|
|
2305
|
+
this.els.pageChoice.checked = false;
|
|
2306
|
+
this.renderPagePreview();
|
|
2307
|
+
disposeMarkdownImages(this.root);
|
|
2308
|
+
for (const message of this.messages) if (message.view) message.view.mdKey = void 0;
|
|
1814
2309
|
this.jobAccessLaunch?.abort();
|
|
1815
2310
|
this.stop();
|
|
1816
2311
|
this.mql?.removeEventListener("change", this.onScheme);
|
|
@@ -1892,6 +2387,8 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1892
2387
|
e.jobAccess.setAttribute("aria-label", s.jobAccess);
|
|
1893
2388
|
e.jobAccess.title = s.jobAccess;
|
|
1894
2389
|
e.input.placeholder = c.placeholder || s.placeholder;
|
|
2390
|
+
e.pageLabel.textContent = s.pageShare;
|
|
2391
|
+
this.updatePageChoice();
|
|
1895
2392
|
e.foot.hidden = !!c.hideBranding;
|
|
1896
2393
|
e.foot.querySelector(".by").textContent = s.poweredBy + " ";
|
|
1897
2394
|
this.renderSubtitle();
|
|
@@ -1899,11 +2396,16 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1899
2396
|
if (this.activated && this.api && this.apiKey !== [c.endpoint, c.gatewayUrl, c.gatewayToken].join("|")) {
|
|
1900
2397
|
this.activated = false;
|
|
1901
2398
|
this.server = null;
|
|
2399
|
+
this.probedPage = false;
|
|
2400
|
+
this.activate();
|
|
2401
|
+
} else if (this.activated && !this.probedPage && (c.pageContext === "metadata" || c.pageContext === "content")) {
|
|
2402
|
+
this.activated = false;
|
|
1902
2403
|
this.activate();
|
|
1903
2404
|
} else if (this.activated && c.agent && c.agent !== this.agentId && this.server) {
|
|
1904
2405
|
this.switchAgent(c.agent);
|
|
1905
2406
|
} else if (this.activated) {
|
|
1906
|
-
this.renderLog();
|
|
2407
|
+
if (!this.messages.length) this.renderLog();
|
|
2408
|
+
else for (const message of this.messages) this.updateMessage(message);
|
|
1907
2409
|
}
|
|
1908
2410
|
}
|
|
1909
2411
|
renderShell() {
|
|
@@ -1924,6 +2426,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1924
2426
|
<div class="log" part="log" role="log" aria-live="polite"></div>
|
|
1925
2427
|
<div class="challenge"><slot name="challenge"></slot></div>
|
|
1926
2428
|
<form class="composer" part="composer">
|
|
2429
|
+
<div class="page-context" hidden><label><input type="checkbox" class="page-choice"> <span class="page-label"></span></label><pre class="page-preview" hidden></pre></div>
|
|
1927
2430
|
<div class="cooldown" part="cooldown" aria-hidden="true" hidden></div>
|
|
1928
2431
|
<div class="sr-only announcer" role="status" aria-live="polite"></div>
|
|
1929
2432
|
<div class="box">
|
|
@@ -1950,7 +2453,11 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1950
2453
|
close: $(".close"),
|
|
1951
2454
|
foot: $(".foot"),
|
|
1952
2455
|
cooldown: $(".cooldown"),
|
|
1953
|
-
announcer: $(".announcer")
|
|
2456
|
+
announcer: $(".announcer"),
|
|
2457
|
+
pageChoice: $(".page-choice"),
|
|
2458
|
+
pageWrap: $(".page-context"),
|
|
2459
|
+
pageLabel: $(".page-label"),
|
|
2460
|
+
pagePreview: $(".page-preview")
|
|
1954
2461
|
};
|
|
1955
2462
|
const e = this.els;
|
|
1956
2463
|
e.launcher.addEventListener("click", () => this.toggle());
|
|
@@ -1962,6 +2469,9 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1962
2469
|
e.jobAccess.addEventListener("click", () => {
|
|
1963
2470
|
void this.openJobAccess();
|
|
1964
2471
|
});
|
|
2472
|
+
e.pageChoice.addEventListener("change", () => {
|
|
2473
|
+
void this.choosePage();
|
|
2474
|
+
});
|
|
1965
2475
|
e.select.addEventListener("change", () => this.switchAgent(e.select.value));
|
|
1966
2476
|
$("form").addEventListener("submit", (ev) => {
|
|
1967
2477
|
ev.preventDefault();
|
|
@@ -1986,6 +2496,80 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1986
2496
|
t.style.height = "auto";
|
|
1987
2497
|
t.style.height = Math.min(t.scrollHeight, 140) + "px";
|
|
1988
2498
|
}
|
|
2499
|
+
updatePageChoice() {
|
|
2500
|
+
const mode = this.config.pageContext;
|
|
2501
|
+
const available = (mode === "metadata" || mode === "content") && this.server?.pageContextVersions?.includes(1) === true;
|
|
2502
|
+
this.els.pageWrap.hidden = !available;
|
|
2503
|
+
if (!available) {
|
|
2504
|
+
this.pageGeneration++;
|
|
2505
|
+
this.pagePending = false;
|
|
2506
|
+
this.els.pageChoice.checked = false;
|
|
2507
|
+
this.sharedPage = void 0;
|
|
2508
|
+
this.renderPagePreview();
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
async captureCurrentPage() {
|
|
2512
|
+
const c = this.config;
|
|
2513
|
+
if (c.pageContext !== "metadata" && c.pageContext !== "content" || !this.server?.pageContextVersions?.includes(1)) return void 0;
|
|
2514
|
+
const before = document.location.href;
|
|
2515
|
+
const { capturePageContext, providedPageContext } = await import("./page-context-H3ZROFAV.js");
|
|
2516
|
+
const provider = this.hooks.contextProvider;
|
|
2517
|
+
const configKey = JSON.stringify([c.pageContext, c.contextSelector, c.excludeSelectors]);
|
|
2518
|
+
const base = capturePageContext(document, { mode: provider ? "metadata" : c.pageContext, selector: c.contextSelector, excludeSelectors: c.excludeSelectors });
|
|
2519
|
+
let result = base;
|
|
2520
|
+
if (base && provider) {
|
|
2521
|
+
let timer;
|
|
2522
|
+
try {
|
|
2523
|
+
const supplied = await Promise.race([provider(), new Promise((_, reject) => {
|
|
2524
|
+
timer = setTimeout(() => reject(new Error("Page context timed out")), 5e3);
|
|
2525
|
+
})]);
|
|
2526
|
+
result = providedPageContext(supplied, base, c.pageContext);
|
|
2527
|
+
} finally {
|
|
2528
|
+
if (timer) clearTimeout(timer);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
const now = this.config;
|
|
2532
|
+
if (!this.isConnected || before !== document.location.href || provider !== this.hooks.contextProvider || configKey !== JSON.stringify([now.pageContext, now.contextSelector, now.excludeSelectors])) return void 0;
|
|
2533
|
+
return result;
|
|
2534
|
+
}
|
|
2535
|
+
pageFingerprint(page) {
|
|
2536
|
+
const { captured_at: _capturedAt, ...stable } = page;
|
|
2537
|
+
return JSON.stringify(stable);
|
|
2538
|
+
}
|
|
2539
|
+
renderPagePreview() {
|
|
2540
|
+
const preview = this.els.pagePreview;
|
|
2541
|
+
preview.hidden = !this.sharedPage;
|
|
2542
|
+
preview.textContent = this.sharedPage ? [this.sharedPage.title, this.sharedPage.url, this.sharedPage.description, this.sharedPage.selection, this.sharedPage.content?.text].filter(Boolean).join("\n\n") : "";
|
|
2543
|
+
}
|
|
2544
|
+
async choosePage() {
|
|
2545
|
+
const generation = ++this.pageGeneration;
|
|
2546
|
+
if (!this.els.pageChoice.checked) {
|
|
2547
|
+
this.pagePending = false;
|
|
2548
|
+
this.sharedPage = void 0;
|
|
2549
|
+
this.renderPagePreview();
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
this.sharedPage = void 0;
|
|
2553
|
+
this.renderPagePreview();
|
|
2554
|
+
this.pagePending = true;
|
|
2555
|
+
try {
|
|
2556
|
+
const page = await this.captureCurrentPage();
|
|
2557
|
+
if (generation !== this.pageGeneration || !this.els.pageChoice.checked) return;
|
|
2558
|
+
if (!page) throw new Error("Page unavailable");
|
|
2559
|
+
this.sharedPage = page;
|
|
2560
|
+
this.renderPagePreview();
|
|
2561
|
+
this.emit("dewee:context-ready", { available: true });
|
|
2562
|
+
} catch {
|
|
2563
|
+
if (generation !== this.pageGeneration) return;
|
|
2564
|
+
this.sharedPage = void 0;
|
|
2565
|
+
this.els.pageChoice.checked = false;
|
|
2566
|
+
this.renderPagePreview();
|
|
2567
|
+
this.showNotice(this.strings.pageUnavailable);
|
|
2568
|
+
this.emit("dewee:context-error", { code: "page_unavailable" });
|
|
2569
|
+
} finally {
|
|
2570
|
+
if (generation === this.pageGeneration) this.pagePending = false;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
1989
2573
|
updateSendButton() {
|
|
1990
2574
|
const b = this.els.send;
|
|
1991
2575
|
b.innerHTML = this.running ? icons.stop : icons.send;
|
|
@@ -2036,7 +2620,8 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2036
2620
|
this.activated = true;
|
|
2037
2621
|
this.renderLog();
|
|
2038
2622
|
try {
|
|
2039
|
-
this.
|
|
2623
|
+
this.probedPage = this.config.pageContext === "metadata" || this.config.pageContext === "content";
|
|
2624
|
+
this.server = await this.client().getConfig(true, this.probedPage);
|
|
2040
2625
|
this.els.input.maxLength = this.server.maxMessageLength;
|
|
2041
2626
|
const wanted = this.config.agent;
|
|
2042
2627
|
const known = (id) => !!id && (this.server.agents.length === 0 || this.server.agents.some((a) => a.id === id));
|
|
@@ -2048,6 +2633,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2048
2633
|
this.showError(null, e);
|
|
2049
2634
|
}
|
|
2050
2635
|
this.renderSubtitle();
|
|
2636
|
+
this.updatePageChoice();
|
|
2051
2637
|
this.emit("dewee:ready", { agent: this.agentId, config: this.server });
|
|
2052
2638
|
}
|
|
2053
2639
|
async switchAgent(id) {
|
|
@@ -2074,6 +2660,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2074
2660
|
}
|
|
2075
2661
|
renderLog() {
|
|
2076
2662
|
const log = this.els.log;
|
|
2663
|
+
disposeMarkdownImages(log);
|
|
2077
2664
|
log.replaceChildren();
|
|
2078
2665
|
if (!this.messages.length) log.appendChild(this.renderWelcome());
|
|
2079
2666
|
for (const m of this.messages) log.appendChild(this.renderMessage(m));
|
|
@@ -2173,13 +2760,18 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2173
2760
|
v.tools.clear();
|
|
2174
2761
|
}
|
|
2175
2762
|
v.trace.hidden = !v.trace.childElementCount;
|
|
2176
|
-
const shown = this.
|
|
2763
|
+
const shown = this.actionRegistry().size || m.actions ? stripDirectives(m.content) : m.content;
|
|
2177
2764
|
const typing = m.status === "streaming" && !shown;
|
|
2178
2765
|
v.md.hidden = !typing && !shown && !!m.actions?.length;
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
v.md
|
|
2766
|
+
const mdKey = JSON.stringify([shown, typing, m.status === "streaming", c.endpoint, c.imageOrigins, this.strings.thinking]);
|
|
2767
|
+
if (v.mdKey !== mdKey) {
|
|
2768
|
+
v.mdKey = mdKey;
|
|
2769
|
+
disposeMarkdownImages(v.md);
|
|
2770
|
+
if (typing) {
|
|
2771
|
+
v.md.innerHTML = `<span class="typing" aria-label="${this.strings.thinking}"><i></i><i></i><i></i></span>`;
|
|
2772
|
+
} else {
|
|
2773
|
+
v.md.replaceChildren(renderMarkdown(document, shown, { imageOrigins: Array.isArray(c.imageOrigins) ? c.imageOrigins : [], imageProxy: (source, signal) => this.client().imageUrl(source, signal), deferImages: m.status === "streaming" }));
|
|
2774
|
+
}
|
|
2183
2775
|
v.md.classList.toggle("cursor", m.status === "streaming");
|
|
2184
2776
|
}
|
|
2185
2777
|
this.renderActions(v, m.actions ?? []);
|
|
@@ -2233,7 +2825,10 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
2233
2825
|
onStreamEvent(m, e) {
|
|
2234
2826
|
switch (e.type) {
|
|
2235
2827
|
case "start":
|
|
2236
|
-
if (e.actionsHash)
|
|
2828
|
+
if (e.actionsHash) {
|
|
2829
|
+
m.actionsHash = e.actionsHash;
|
|
2830
|
+
m.ackVisitor = this.client().visitorId() ?? void 0;
|
|
2831
|
+
}
|
|
2237
2832
|
if (e.conversationId && e.conversationId !== this.conversationId) {
|
|
2238
2833
|
this.conversationId = e.conversationId;
|
|
2239
2834
|
if (this.config.persist !== false) store2.set(this.convKey(), e.conversationId);
|
|
@@ -2416,6 +3011,7 @@ function mount(config = {}, target) {
|
|
|
2416
3011
|
defineDeweeChat();
|
|
2417
3012
|
|
|
2418
3013
|
export {
|
|
3014
|
+
DOM_ACTION_NAMES,
|
|
2419
3015
|
RESERVED_HEADERS,
|
|
2420
3016
|
ApiError,
|
|
2421
3017
|
ApiClient,
|