@lightworkai.official/debug-capture 0.19.3 → 0.19.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/live.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export interface RingingCall {
2
2
  id: string;
3
- ticketId: string;
3
+ /** A เรื่อง's call — or a chat's (โทรจากแชท, 0.19.4): then `chatId`, and the key "แชท". */
4
+ ticketId: string | null;
5
+ chatId?: string | null;
4
6
  ticketKey: string;
5
7
  ticketTitle: string;
6
8
  status: string;
@@ -10,7 +12,7 @@ export interface RingingCall {
10
12
  /** `wss://support.example/api/presence/ws?t=…` — the support host's scheme, the socket's own path. Pure, for the tests. */
11
13
  export declare function liveSocketUrl(host: string, ticket: string): string;
12
14
  /** Where the card sends them: their request, with the call — the support host rings it there. Pure, for the tests. */
13
- export declare function answerPath(call: Pick<RingingCall, "id" | "ticketId">): string;
15
+ export declare function answerPath(call: Pick<RingingCall, "id" | "ticketId" | "chatId">): string;
14
16
  /** Is this a call ringing for the person at this widget? */
15
17
  export declare function ringsHere(call: Pick<RingingCall, "status" | "side">): boolean;
16
18
  type LiveFrame = {
@@ -65,6 +65,14 @@ export declare function bubbleLabel(s: {
65
65
  shown: boolean;
66
66
  isNew: boolean;
67
67
  } | null;
68
+ export interface DeskLook {
69
+ label?: string;
70
+ color?: string;
71
+ /** A path on the support host; null: the chat glyph on the colour. */
72
+ faceUrl?: string | null;
73
+ }
74
+ /** The face's address for a desk's look: its own path on the support host, the shipped one, or none (the glyph). Pure. */
75
+ export declare function lookFaceUrl(host: string, look: DeskLook | null): string | null;
68
76
  /** Open the panel — from the bubble, or from the host app's own button (`openSupportChat`). */
69
77
  export declare function openSupportChat(): void;
70
78
  /** Fold the panel — it stays loaded, so the chat is there as it was the next time. */
@@ -72,4 +80,6 @@ export declare function closeSupportChat(refocus?: boolean): void;
72
80
  /** The bubble, once — hidden until the host's token says somebody is signed in. */
73
81
  export declare function mountSupportChat(): void;
74
82
  /** Take it away — for a host that unmounts, and for the tests. */
83
+ /** Ask again whether there is a bubble, and what waits (0.19.5) — after a sign-in or a sign-out, or `show` changed. */
84
+ export declare function refreshSupportChat(): Promise<void>;
75
85
  export declare function unmountSupportChat(): void;
package/dist/ui/ring.d.ts CHANGED
@@ -2,6 +2,8 @@ export interface RingCall {
2
2
  id: string;
3
3
  ticketKey: string;
4
4
  ticketTitle: string;
5
+ /** A chat's call (โทรจากแชท, 0.19.4) — said as แชทกับทีมงาน, never "เรื่อง แชท". */
6
+ chatId?: string | null;
5
7
  }
6
8
  export interface RingHandlers {
7
9
  /** The portal's way: runs INSIDE the press — a tab can be opened only there, before anything is awaited. */
@@ -14,8 +16,8 @@ export interface RingHandlers {
14
16
  }
15
17
  /** `frame`: the call's own card while its frame comes; `portal`: answer in a new tab (no frame, or it failed). */
16
18
  export type RingMode = "frame" | "portal";
17
- /** "เรื่อง CALL-12 · title" — the line under the card's title. Pure, for the tests. */
18
- export declare function ringAbout(template: string, call: RingCall): string;
19
+ /** "เรื่อง CALL-12 · title" — the line under the card's title; a chat's call, its own words (`chat`). Pure, for the tests. */
20
+ export declare function ringAbout(template: string, call: RingCall, chat?: string): string;
19
21
  /** Show the ring for this call — once; the same call again changes nothing. */
20
22
  export declare function showRing(call: RingCall, handlers: RingHandlers, mode?: RingMode): void;
21
23
  /** รับสาย pressed while the call's frame comes: said, and nothing more to press until it is there. */
@@ -106,6 +106,8 @@ export interface Strings {
106
106
  /** A call from the support team (ui/ring). `ringAbout` carries `{key}` and `{title}`. */
107
107
  ringTitle: string;
108
108
  ringAbout: string;
109
+ /** A call from the chat, not a เรื่อง (โทรจากแชท, 0.19.4) — the line under the card's title. */
110
+ ringChat: string;
109
111
  ringHow: string;
110
112
  ringOpen: string;
111
113
  ringDecline: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightworkai.official/debug-capture",
3
- "version": "0.19.3",
3
+ "version": "0.19.5",
4
4
  "description": "Report-a-problem widget: rolling network/console/action capture, optional rrweb session replay, annotated screenshot, and submission to a Lightwork Support host",
5
5
  "type": "module",
6
6
  "module": "./dist/index.mjs",
package/src/config.ts CHANGED
@@ -232,6 +232,12 @@ export interface ChatOptions {
232
232
  * of "แชทกับทีมงาน", or `false` for the button alone.
233
233
  */
234
234
  label?: string | false;
235
+ /**
236
+ * Whether the bubble is for whoever is signed in now (0.19.5) — asked before every read; false: no bubble. For a
237
+ * host whose own staff should not chat with its support desk, or not on every page. Ask again after a sign-in with
238
+ * `refreshSupportChat()`.
239
+ */
240
+ show?: () => boolean;
235
241
  }
236
242
 
237
243
  /** The bubble's place, from `chat`: null when it is off. */
package/src/index.ts CHANGED
@@ -77,7 +77,7 @@ export type { OpenPortalOptions } from "./portal";
77
77
  * แชทกับทีม (0.19): `chat` in the config mounts the bubble on install; a host with its own button opens and closes
78
78
  * the same panel. The panel is the support host's page, framed — as the tickets are, maintained where they live.
79
79
  */
80
- export { openSupportChat, closeSupportChat, mountSupportChat, unmountSupportChat, supportFrameUrl, badgeText, chatLift, panelHeight } from "./ui/chatBubble";
80
+ export { openSupportChat, closeSupportChat, mountSupportChat, unmountSupportChat, refreshSupportChat, supportFrameUrl, badgeText, chatLift, panelHeight } from "./ui/chatBubble";
81
81
  export type { ChatOptions } from "./config";
82
82
 
83
83
  export type * from "./types";
package/src/live.ts CHANGED
@@ -31,7 +31,9 @@ import { inkOnPage, removeInkLayer } from "./ui/inkOverlay";
31
31
 
32
32
  export interface RingingCall {
33
33
  id: string;
34
- ticketId: string;
34
+ /** A เรื่อง's call — or a chat's (โทรจากแชท, 0.19.4): then `chatId`, and the key "แชท". */
35
+ ticketId: string | null;
36
+ chatId?: string | null;
35
37
  ticketKey: string;
36
38
  ticketTitle: string;
37
39
  status: string;
@@ -65,7 +67,9 @@ export function liveSocketUrl(host: string, ticket: string): string {
65
67
  }
66
68
 
67
69
  /** Where the card sends them: their request, with the call — the support host rings it there. Pure, for the tests. */
68
- export function answerPath(call: Pick<RingingCall, "id" | "ticketId">): string {
70
+ export function answerPath(call: Pick<RingingCall, "id" | "ticketId" | "chatId">): string {
71
+ // A chat's call (0.19.4): their chat with the team, where it rings.
72
+ if (call.chatId || !call.ticketId) return `/me/chat?call=${call.id}`;
69
73
  return `/me/tickets/${call.ticketId}?call=${call.id}`;
70
74
  }
71
75
 
@@ -33,11 +33,11 @@ const CSS = `
33
33
  .bubble {
34
34
  position: fixed; z-index: 2147483000; width: ${SIZE}px; height: ${SIZE}px; border-radius: 50%;
35
35
  display: grid; place-items: center; border: 0; cursor: pointer; padding: 0;
36
- background: #00468f; color: #fff; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 4px 10px -4px rgba(0, 0, 0, 0.2);
36
+ background: var(--lw-chat-color, #00468f); color: #fff; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 4px 10px -4px rgba(0, 0, 0, 0.2);
37
37
  transition: transform 150ms ease, background 150ms ease;
38
38
  font-family: var(--lw-host-font, ${OWN_FONTS});
39
39
  }
40
- .bubble:hover { background: #003a78; transform: translateY(-1px); }
40
+ .bubble:hover { filter: brightness(0.92); transform: translateY(-1px); }
41
41
  .bubble { touch-action: none; }
42
42
  .bubble.dragging { cursor: grabbing; transform: scale(1.06); transition: none; box-shadow: 0 18px 35px -8px rgba(0, 0, 0, 0.4); }
43
43
  .bubble:focus-visible { outline: 3px solid rgba(0, 70, 143, 0.35); outline-offset: 3px; }
@@ -65,7 +65,7 @@ const CSS = `
65
65
  .label .text { display: block; max-width: min(240px, calc(100vw - ${SIZE + 64}px)); overflow: hidden; text-overflow: ellipsis; }
66
66
  .at-left .label { right: auto; left: calc(100% + 12px); transform: translate(-4px, -50%); }
67
67
  .at-left .label::after { right: auto; left: -4px; }
68
- .new .label { color: #00468f; }
68
+ .new .label { color: var(--lw-chat-color, #00468f); }
69
69
  .labelled .label { opacity: 1; visibility: visible; pointer-events: auto; transform: translate(0, -50%); }
70
70
  @media (hover: hover) { .bubble:hover .label { opacity: 1; visibility: visible; pointer-events: auto; transform: translate(0, -50%); } }
71
71
  .unlabelled .label, .bubble.dragging .label, .open .label { display: none; }
@@ -207,7 +207,9 @@ let opened = false;
207
207
  function setLabel(): void {
208
208
  if (!label || !bubble) return;
209
209
  const config = getConfig();
210
- const own = config.chat && config.chat !== true ? config.chat.label : undefined;
210
+ // The host's own words in code win; else the desk's (ปุ่มแชท, its look); else the shipped ones.
211
+ const hostOwn = config.chat && config.chat !== true ? config.chat.label : undefined;
212
+ const own = hostOwn !== undefined ? hostOwn : deskLook?.label;
211
213
  const l = bubbleLabel({ own, unread, opened, open: isOpen }, strings(config.locale));
212
214
  bubble.classList.toggle("unlabelled", !l);
213
215
  bubble.classList.toggle("labelled", !!l?.shown);
@@ -216,28 +218,83 @@ function setLabel(): void {
216
218
  label.querySelector(".text")!.textContent = l?.text ?? "";
217
219
  }
218
220
 
221
+ /*
222
+ * The desk's look (0.19.5, the support desk's ปุ่มแชท — docs/CHAT-BUBBLE-PLAN.md): its face (or the chat glyph on its
223
+ * colour), its colour, the words beside the button — set on the desk, read with the first answer here.
224
+ */
225
+ export interface DeskLook {
226
+ label?: string;
227
+ color?: string;
228
+ /** A path on the support host; null: the chat glyph on the colour. */
229
+ faceUrl?: string | null;
230
+ }
231
+ let deskLook: DeskLook | null = null;
232
+ /** The face's address for a desk's look: its own path on the support host, the shipped one, or none (the glyph). Pure. */
233
+ export function lookFaceUrl(host: string, look: DeskLook | null): string | null {
234
+ if (!look || look.faceUrl === undefined) return supportFaceUrl(host);
235
+ if (look.faceUrl === null) return null;
236
+ try {
237
+ return new URL(look.faceUrl.replace(/^\//, ""), base(host)).toString();
238
+ } catch {
239
+ return supportFaceUrl(host);
240
+ }
241
+ }
242
+ function wearLook(look: DeskLook | null): void {
243
+ deskLook = look;
244
+ if (!root || !bubble) return;
245
+ const color = look?.color && /^#[0-9a-f]{6}$/i.test(look.color) ? look.color : null;
246
+ if (color) root.style.setProperty("--lw-chat-color", color);
247
+ else root.style.removeProperty("--lw-chat-color");
248
+ const face = shadow?.querySelector<HTMLImageElement>(".face");
249
+ const url = lookFaceUrl(getConfig().host, look);
250
+ if (face) {
251
+ if (!url) {
252
+ bubble.classList.remove("has-face");
253
+ face.removeAttribute("src");
254
+ } else if (face.getAttribute("src") !== url) {
255
+ bubble.classList.remove("has-face");
256
+ face.src = url;
257
+ }
258
+ }
259
+ setLabel();
260
+ }
261
+
219
262
  let asking: Promise<void> | null = null;
220
263
  /** What waits for the person, read with the host's token — no token: nobody is signed in, and there is no bubble. */
221
264
  async function refreshUnread(): Promise<void> {
222
- asking ??= (async () => {
223
- try {
224
- const token = await hostToken();
225
- if (!token) return void show(false);
226
- const config = getConfig();
227
- const res = await fetch(new URL("ingest/support-unread", base(config.host)).toString(), { headers: { "x-realm-key": config.realmKey, authorization: `Bearer ${token}` } });
228
- // A support host from before the chat, or a token it does not accept: no bubble rather than one that cannot open.
229
- if (res.status === 404 || res.status === 401 || res.status === 403) return void show(false);
230
- if (!res.ok) return;
231
- show(true);
232
- const out = (await res.json()) as { total?: number };
233
- if (!isOpen) setBadge(typeof out.total === "number" ? out.total : 0);
234
- } catch {
235
- /* the next nudge asks again */
236
- } finally {
237
- asking = null;
238
- }
239
- })();
240
- return asking;
265
+ if (asking) return asking;
266
+ /*
267
+ * Kept until it settles, then let go — by THIS read only. (`asking ??= (async () => { … finally { asking = null } })()`
268
+ * lost the let-go when the read ended before its first await — `show` saying no: the finally ran before the
269
+ * assignment, and every read after was that settled one, never asked again.)
270
+ */
271
+ const run = readUnread();
272
+ asking = run;
273
+ try {
274
+ await run;
275
+ } finally {
276
+ if (asking === run) asking = null;
277
+ }
278
+ }
279
+ async function readUnread(): Promise<void> {
280
+ try {
281
+ const config = getConfig();
282
+ // The host's own say (0.19.5): not for whoever is signed in now — no bubble, nothing asked.
283
+ const opts = config.chat && config.chat !== true ? config.chat : null;
284
+ if (opts?.show && !opts.show()) return void show(false);
285
+ const token = await hostToken();
286
+ if (!token) return void show(false);
287
+ const res = await fetch(new URL("ingest/support-unread", base(config.host)).toString(), { headers: { "x-realm-key": config.realmKey, authorization: `Bearer ${token}` } });
288
+ // A support host from before the chat, a desk that turned its bubble off, or a token it does not accept: no bubble.
289
+ if (res.status === 404 || res.status === 401 || res.status === 403) return void show(false);
290
+ if (!res.ok) return;
291
+ const out = (await res.json()) as { total?: number; look?: DeskLook };
292
+ if (out.look && typeof out.look === "object") wearLook(out.look);
293
+ show(true);
294
+ if (!isOpen) setBadge(typeof out.total === "number" ? out.total : 0);
295
+ } catch {
296
+ /* the next nudge asks again */
297
+ }
241
298
  }
242
299
 
243
300
  function show(on: boolean): void {
@@ -491,8 +548,12 @@ export function mountSupportChat(): void {
491
548
  // The desk's face (the owner, 10-31: "all the support icon that user see … use this icon"): the support host's own,
492
549
  // the one its replies wear — the chat glyph until it is there, and if it never comes.
493
550
  const face = shadow.querySelector<HTMLImageElement>(".face")!;
494
- face.addEventListener("load", () => bubble?.classList.add("has-face"), { once: true });
495
- face.src = supportFaceUrl(config.host);
551
+ // Every picture it is given (the desk's look may change it, 0.19.5): worn once loaded, the glyph if it fails.
552
+ face.addEventListener("load", () => {
553
+ if (face.getAttribute("src")) bubble?.classList.add("has-face");
554
+ });
555
+ face.addEventListener("error", () => bubble?.classList.remove("has-face"));
556
+ face.src = lookFaceUrl(config.host, deskLook) ?? supportFaceUrl(config.host);
496
557
  // Where the person left it on this device, else the config's place (0.19.2).
497
558
  loadPlace();
498
559
  layout();
@@ -521,7 +582,13 @@ export function mountSupportChat(): void {
521
582
  }
522
583
 
523
584
  /** Take it away — for a host that unmounts, and for the tests. */
585
+ /** Ask again whether there is a bubble, and what waits (0.19.5) — after a sign-in or a sign-out, or `show` changed. */
586
+ export function refreshSupportChat(): Promise<void> {
587
+ return root ? refreshUnread() : Promise.resolve();
588
+ }
589
+
524
590
  export function unmountSupportChat(): void {
591
+ deskLook = null;
525
592
  stopFrames?.();
526
593
  stopFrames = null;
527
594
  window.removeEventListener("message", onMessage);
package/src/ui/ring.ts CHANGED
@@ -87,6 +87,8 @@ export interface RingCall {
87
87
  id: string;
88
88
  ticketKey: string;
89
89
  ticketTitle: string;
90
+ /** A chat's call (โทรจากแชท, 0.19.4) — said as แชทกับทีมงาน, never "เรื่อง แชท". */
91
+ chatId?: string | null;
90
92
  }
91
93
 
92
94
  export interface RingHandlers {
@@ -106,8 +108,9 @@ let host: HTMLElement | null = null;
106
108
  let shownId: string | null = null;
107
109
  let titleBefore: string | null = null;
108
110
 
109
- /** "เรื่อง CALL-12 · title" — the line under the card's title. Pure, for the tests. */
110
- export function ringAbout(template: string, call: RingCall): string {
111
+ /** "เรื่อง CALL-12 · title" — the line under the card's title; a chat's call, its own words (`chat`). Pure, for the tests. */
112
+ export function ringAbout(template: string, call: RingCall, chat?: string): string {
113
+ if (call.chatId) return chat ?? call.ticketTitle;
111
114
  return template.replace("{key}", call.ticketKey).replace("{title}", call.ticketTitle);
112
115
  }
113
116
 
@@ -164,7 +167,7 @@ function render(root: ShadowRoot, call: RingCall, handlers: RingHandlers, mode:
164
167
  h.textContent = s.ringTitle;
165
168
  const about = document.createElement("p");
166
169
  about.className = "about";
167
- about.textContent = ringAbout(s.ringAbout, call);
170
+ about.textContent = ringAbout(s.ringAbout, call, s.ringChat);
168
171
  about.title = about.textContent;
169
172
  words.append(h, about);
170
173
  head.append(icon, words);
package/src/ui/strings.ts CHANGED
@@ -111,6 +111,8 @@ export interface Strings {
111
111
  /** A call from the support team (ui/ring). `ringAbout` carries `{key}` and `{title}`. */
112
112
  ringTitle: string;
113
113
  ringAbout: string;
114
+ /** A call from the chat, not a เรื่อง (โทรจากแชท, 0.19.4) — the line under the card's title. */
115
+ ringChat: string;
114
116
  ringHow: string;
115
117
  ringOpen: string;
116
118
  ringDecline: string;
@@ -211,6 +213,7 @@ const th: Strings = {
211
213
  hours: "{n} ชั่วโมง",
212
214
  ringTitle: "ทีมงานกำลังโทรหาคุณ",
213
215
  ringAbout: "เรื่อง {key} · {title}",
216
+ ringChat: "จากแชทกับทีมงาน",
214
217
  ringHow: "รับสายในหน้าคำร้องของคุณ — เปิดในแท็บใหม่ ไม่ต้องเข้าสู่ระบบอีก",
215
218
  ringOpen: "เปิดเพื่อรับสาย",
216
219
  ringDecline: "ไม่รับ",
@@ -306,6 +309,7 @@ const en: Strings = {
306
309
  hours: "{n} hours",
307
310
  ringTitle: "The support team is calling you",
308
311
  ringAbout: "About {key} · {title}",
312
+ ringChat: "From your chat with support",
309
313
  ringHow: "Answer on your request's page — it opens in a new tab, already signed in",
310
314
  ringOpen: "Open to answer",
311
315
  ringDecline: "Decline",