@guidelytech/guide-runtime 0.1.0 → 0.2.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.
@@ -0,0 +1,726 @@
1
+ import {
2
+ createHelpView,
3
+ el
4
+ } from "./chunk-SLBUGXNH.js";
5
+
6
+ // src/messenger-styles.ts
7
+ var MESSENGER_STYLES = `
8
+ :host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
9
+ :host([data-g-messenger="bottom-left"]) .g-launcher.g-pos-bottom-left{left:72px}
10
+ .g-m-launcher{position:fixed;z-index:calc(var(--g-z) + 1);pointer-events:auto;width:56px;height:56px;margin:16px;border-radius:999px;border:0;background:var(--g-primary);color:var(--g-primary-text);cursor:pointer;box-shadow:var(--g-shadow);display:inline-flex;align-items:center;justify-content:center;padding:0;transition:transform .15s}
11
+ .g-m-launcher:hover{transform:scale(1.05)}
12
+ .g-m-launcher:focus-visible{outline:2px solid var(--g-primary);outline-offset:3px}
13
+ .g-m-launcher svg{width:26px;height:26px;fill:currentColor}
14
+ .g-m-badge{position:absolute;top:-2px;right:-2px;min-width:20px;height:20px;padding:0 5px;border-radius:999px;background:#e11d48;color:#fff;font-size:11px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;border:2px solid var(--g-bg)}
15
+ .g-messenger{bottom:76px;height:min(640px,calc(100vh - 110px))}
16
+ .g-m-head{padding:16px 48px 14px 20px;background:var(--g-primary);color:var(--g-primary-text);position:relative;display:flex;align-items:center;gap:8px;min-height:56px}
17
+ .g-m-head .g-close{color:inherit;top:50%;transform:translateY(-50%)}
18
+ .g-m-head .g-close:hover,.g-m-head .g-close:focus-visible{background:rgba(255,255,255,.18);color:inherit}
19
+ .g-m-head h2{margin:0;font-size:16px;font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
20
+ .g-m-back{border:0;background:transparent;color:inherit;font:inherit;font-size:18px;cursor:pointer;width:32px;height:32px;border-radius:999px;margin-left:-8px}
21
+ .g-m-back:hover,.g-m-back:focus-visible{background:rgba(255,255,255,.18);outline:none}
22
+ .g-m-main{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}
23
+ .g-m-scroll{flex:1;overflow:auto;padding:16px}
24
+ .g-m-hero{font-size:22px;font-weight:700;margin:4px 0 4px}
25
+ .g-m-greet{color:var(--g-muted);margin:0 0 16px}
26
+ .g-m-card{display:block;width:100%;text-align:left;border:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);border-radius:calc(var(--g-radius) * .8);padding:14px 16px;margin:0 0 12px;font:inherit;cursor:pointer;box-shadow:0 1px 2px rgba(0,0,0,.04)}
27
+ .g-m-card:hover{background:var(--g-subtle)}
28
+ .g-m-card:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
29
+ .g-m-card-title{font-weight:600}
30
+ .g-m-card-sub{color:var(--g-muted);font-size:13px;margin-top:2px}
31
+ .g-m-row{display:flex;align-items:flex-start;gap:10px}
32
+ .g-m-row .g-m-grow{flex:1;min-width:0}
33
+ .g-m-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
34
+ .g-m-time{color:var(--g-muted);font-size:12px;flex:none}
35
+ .g-m-dot{width:8px;height:8px;border-radius:999px;background:var(--g-primary);flex:none;margin-top:6px}
36
+ .g-m-tabs{display:flex;border-top:1px solid var(--g-border);background:var(--g-bg)}
37
+ .g-m-tab{flex:1;border:0;background:transparent;color:var(--g-muted);font:inherit;font-size:12px;font-weight:600;padding:10px 4px 12px;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:2px}
38
+ .g-m-tab[aria-selected="true"]{color:var(--g-primary)}
39
+ .g-m-tab:focus-visible{outline:2px solid var(--g-primary);outline-offset:-2px}
40
+ .g-m-tab-icon{font-size:18px;line-height:1}
41
+ .g-m-log{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:10px}
42
+ .g-m-msg{max-width:85%;display:flex;flex-direction:column;gap:4px}
43
+ .g-m-msg.g-from-user{align-self:flex-end;align-items:flex-end}
44
+ .g-m-msg.g-from-system{align-self:center;max-width:95%;text-align:center;color:var(--g-muted);font-size:12px}
45
+ .g-m-author{font-size:12px;color:var(--g-muted);display:flex;align-items:center;gap:6px}
46
+ .g-m-ai{font-size:10px;font-weight:700;letter-spacing:.04em;padding:1px 5px;border-radius:4px;background:var(--g-subtle);color:var(--g-muted)}
47
+ .g-m-bubble{padding:10px 12px;border-radius:14px;background:var(--g-subtle);color:var(--g-text);overflow-wrap:anywhere}
48
+ .g-from-user .g-m-bubble{background:var(--g-primary);color:var(--g-primary-text)}
49
+ .g-from-user .g-m-bubble a{color:inherit}
50
+ .g-m-bubble p{margin:0 0 8px}.g-m-bubble p:last-child{margin:0}
51
+ .g-m-bubble ul,.g-m-bubble ol{margin:4px 0 8px;padding-left:20px}
52
+ .g-m-bubble code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;background:color-mix(in srgb,var(--g-text) 8%,transparent);padding:1px 4px;border-radius:4px}
53
+ .g-m-bubble a{color:var(--g-primary)}
54
+ .g-m-failed .g-m-bubble{opacity:.7;font-style:italic}
55
+ .g-m-typing{display:inline-flex;gap:4px;padding:4px 0}
56
+ .g-m-typing span{width:6px;height:6px;border-radius:999px;background:var(--g-muted);animation:g-m-bounce 1s infinite}
57
+ .g-m-typing span:nth-child(2){animation-delay:.15s}.g-m-typing span:nth-child(3){animation-delay:.3s}
58
+ @keyframes g-m-bounce{0%,60%,100%{opacity:.3;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}
59
+ @media (prefers-reduced-motion: reduce){.g-m-typing span{animation:none}.g-m-launcher{transition:none}}
60
+ .g-m-chips{display:flex;flex-direction:column;gap:6px;margin-top:2px}
61
+ .g-m-chip{display:flex;align-items:center;gap:8px;text-align:left;border:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);border-radius:10px;padding:8px 10px;font:inherit;font-size:13px;cursor:pointer}
62
+ .g-m-chip:hover{background:var(--g-subtle)}
63
+ .g-m-chip:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
64
+ .g-m-chip.g-m-guide{border-color:var(--g-primary);color:var(--g-primary);font-weight:600}
65
+ .g-m-foot{border-top:1px solid var(--g-border);padding:10px 12px 12px;background:var(--g-bg)}
66
+ .g-m-compose{display:flex;align-items:flex-end;gap:8px}
67
+ .g-m-compose textarea{flex:1;resize:none;min-height:40px;max-height:120px;border:1px solid var(--g-border);border-radius:10px;padding:9px 10px;font:inherit;font-size:14px;color:var(--g-text);background:var(--g-bg)}
68
+ .g-m-compose textarea:focus-visible{outline:2px solid var(--g-primary);outline-offset:1px}
69
+ .g-m-send{width:40px;height:40px;border-radius:999px;border:0;background:var(--g-primary);color:var(--g-primary-text);cursor:pointer;font-size:18px;flex:none}
70
+ .g-m-send:disabled{opacity:.45;cursor:not-allowed}
71
+ .g-m-send:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
72
+ .g-m-human{border:0;background:transparent;color:var(--g-muted);font:inherit;font-size:12px;cursor:pointer;padding:6px 2px 0;text-decoration:underline}
73
+ .g-m-human:hover{color:var(--g-text)}
74
+ .g-m-rate{text-align:center;padding:4px 0 2px;font-size:13px;color:var(--g-muted)}
75
+ .g-m-rate button{border:0;background:transparent;font-size:22px;cursor:pointer;padding:4px 8px;border-radius:8px}
76
+ .g-m-rate button:hover,.g-m-rate button:focus-visible{background:var(--g-subtle);outline:none}
77
+ .g-m-note{color:var(--g-muted);font-size:12px;margin-top:6px}
78
+ .g-m-empty{color:var(--g-muted);text-align:center;padding:24px 8px}
79
+ `;
80
+
81
+ // src/render-safe.ts
82
+ var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
83
+ var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
84
+ function stripCitationMarkers(text) {
85
+ return text.replace(CITATION, "");
86
+ }
87
+ function safeHttpUrl(raw) {
88
+ try {
89
+ const u = new URL(raw);
90
+ return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+ function link(doc, label, href) {
96
+ const url = safeHttpUrl(href);
97
+ if (!url) return doc.createTextNode(label);
98
+ const a = doc.createElement("a");
99
+ a.href = url;
100
+ a.target = "_blank";
101
+ a.rel = "noopener noreferrer nofollow";
102
+ a.textContent = label;
103
+ return a;
104
+ }
105
+ function appendInline(doc, parent, text) {
106
+ let last = 0;
107
+ for (const m of text.matchAll(INLINE)) {
108
+ const start = m.index ?? 0;
109
+ if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
110
+ if (m[1] !== void 0) {
111
+ const b = doc.createElement("strong");
112
+ b.textContent = m[1];
113
+ parent.appendChild(b);
114
+ } else if (m[2] !== void 0) {
115
+ const c = doc.createElement("code");
116
+ c.textContent = m[2];
117
+ parent.appendChild(c);
118
+ } else if (m[4] !== void 0 && m[5] !== void 0) {
119
+ parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
120
+ } else if (m[6] !== void 0) {
121
+ parent.appendChild(link(doc, m[6], m[6]));
122
+ }
123
+ last = start + m[0].length;
124
+ }
125
+ if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
126
+ }
127
+ var BULLET = /^\s*[-*•]\s+/;
128
+ var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
129
+ function renderSafeText(doc, text) {
130
+ const frag = doc.createDocumentFragment();
131
+ const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
132
+ for (const block of blocks) {
133
+ if (!block.trim()) continue;
134
+ const lines = block.split("\n");
135
+ if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
136
+ const ordered = NUMBERED.test(lines[0]);
137
+ const list = doc.createElement(ordered ? "ol" : "ul");
138
+ for (const line of lines) {
139
+ const li = doc.createElement("li");
140
+ appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
141
+ list.appendChild(li);
142
+ }
143
+ frag.appendChild(list);
144
+ continue;
145
+ }
146
+ const p = doc.createElement("p");
147
+ lines.forEach((line, i) => {
148
+ if (i) p.appendChild(doc.createElement("br"));
149
+ appendInline(doc, p, line);
150
+ });
151
+ frag.appendChild(p);
152
+ }
153
+ return frag;
154
+ }
155
+
156
+ // src/messenger.ts
157
+ var SVG_NS = "http://www.w3.org/2000/svg";
158
+ function chatIcon(doc) {
159
+ const svg = doc.createElementNS(SVG_NS, "svg");
160
+ svg.setAttribute("viewBox", "0 0 24 24");
161
+ svg.setAttribute("aria-hidden", "true");
162
+ const path = doc.createElementNS(SVG_NS, "path");
163
+ path.setAttribute("d", "M12 3C6.5 3 2 6.8 2 11.5c0 2.4 1.2 4.6 3.1 6.1L4.5 21l3.9-2c1.1.3 2.3.5 3.6.5 5.5 0 10-3.8 10-8.5S17.5 3 12 3z");
164
+ svg.appendChild(path);
165
+ return svg;
166
+ }
167
+ function relativeTime(iso, now = Date.now()) {
168
+ const s = Math.max(0, Math.round((now - Date.parse(iso)) / 1e3));
169
+ if (!Number.isFinite(s) || s < 60) return "now";
170
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
171
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
172
+ return `${Math.floor(s / 86400)}d`;
173
+ }
174
+ function ensureStyles(scope) {
175
+ const root = scope.getRootNode();
176
+ const container = "host" in root ? root : scope.ownerDocument.head;
177
+ if (container.querySelector?.("style[data-g-messenger]")) return;
178
+ const style = scope.ownerDocument.createElement("style");
179
+ style.setAttribute("data-g-messenger", "");
180
+ style.textContent = MESSENGER_STYLES;
181
+ container.appendChild(style);
182
+ }
183
+ function renderMessenger(options) {
184
+ const { scope, config, api } = options;
185
+ const doc = scope.ownerDocument;
186
+ const poll = options.pollMs ?? { thread: 5e3, background: 6e4 };
187
+ ensureStyles(scope);
188
+ let open = false;
189
+ let destroyed = false;
190
+ let view = { kind: "tab", tab: "home" };
191
+ let conversations = [];
192
+ let messages = [];
193
+ let current = null;
194
+ const streaming = /* @__PURE__ */ new Map();
195
+ let sending = false;
196
+ let draft = "";
197
+ let articles = null;
198
+ let articlesLoading = false;
199
+ let threadTimer = null;
200
+ let backgroundTimer = null;
201
+ let threadSeq = 0;
202
+ const pos = `g-pos-${config.launcher.position}`;
203
+ const rootNode = scope.getRootNode();
204
+ const shadowHost = "host" in rootNode ? rootNode.host : null;
205
+ shadowHost?.setAttribute("data-g-messenger", config.launcher.position);
206
+ const root = el(doc, "div", "g-messenger-root");
207
+ const launcher = el(doc, "button", `g-m-launcher ${pos}`);
208
+ launcher.type = "button";
209
+ launcher.setAttribute("aria-label", `Open ${config.name}`);
210
+ launcher.setAttribute("aria-expanded", "false");
211
+ launcher.appendChild(chatIcon(doc));
212
+ const badge = el(doc, "span", "g-m-badge");
213
+ badge.hidden = true;
214
+ launcher.appendChild(badge);
215
+ if (options.hideLauncher) launcher.hidden = true;
216
+ const panel = el(doc, "div", `g-panel g-messenger ${pos}`);
217
+ panel.setAttribute("role", "dialog");
218
+ panel.setAttribute("aria-label", config.name);
219
+ panel.hidden = true;
220
+ const head = el(doc, "div", "g-m-head");
221
+ const main = el(doc, "div", "g-m-main");
222
+ const tabs = el(doc, "div", "g-m-tabs");
223
+ tabs.setAttribute("role", "tablist");
224
+ panel.append(head, main, tabs);
225
+ root.append(launcher, panel);
226
+ scope.appendChild(root);
227
+ const rcExtras = (ids = []) => ids.map((id) => options.guides?.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
228
+ const help = createHelpView({
229
+ doc,
230
+ data: () => ({
231
+ collections: articles?.collections ?? options.resourceCenter?.collections ?? [],
232
+ articles: articles?.articles ?? options.resourceCenter?.articles ?? [],
233
+ recommended: rcExtras(options.resourceCenter?.recommendedGuideIds),
234
+ checklists: rcExtras(options.resourceCenter?.checklistGuideIds),
235
+ faqs: options.resourceCenter?.faqs,
236
+ links: options.resourceCenter?.links
237
+ }),
238
+ search: (q) => api.searchArticles(q),
239
+ loadArticle: (id) => api.getArticle(id),
240
+ onStartGuide: (id) => {
241
+ setOpen(false);
242
+ options.onStartGuide?.(id);
243
+ },
244
+ onOpenChecklist: (id) => {
245
+ setOpen(false);
246
+ options.onOpenChecklist?.(id);
247
+ },
248
+ onArticleView: options.onArticleView
249
+ });
250
+ const unreadTotal = () => conversations.reduce((n, c) => n + (c.unread > 0 ? 1 : 0), 0);
251
+ const updateBadge = () => {
252
+ const n = unreadTotal();
253
+ badge.hidden = n === 0 || open;
254
+ badge.textContent = n > 9 ? "9+" : String(n);
255
+ launcher.setAttribute("aria-label", n ? `Open ${config.name}, ${n} unread` : `Open ${config.name}`);
256
+ };
257
+ const upsertConversation = (c) => {
258
+ conversations = [c, ...conversations.filter((x) => x.id !== c.id)].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
259
+ if (current?.id === c.id) current = c;
260
+ updateBadge();
261
+ };
262
+ const upsertMessage = (m) => {
263
+ const i = messages.findIndex((x) => x.id === m.id);
264
+ if (i >= 0) messages[i] = m;
265
+ else messages.push(m);
266
+ messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
267
+ if (m.status !== "pending") streaming.delete(m.id);
268
+ };
269
+ const loadArticles = () => {
270
+ if (articles || articlesLoading) return;
271
+ articlesLoading = true;
272
+ api.listArticles().then((res) => {
273
+ articles = res;
274
+ if (view.kind === "tab" && view.tab === "help") help.render();
275
+ }).catch(() => {
276
+ }).finally(() => articlesLoading = false);
277
+ };
278
+ const refreshConversations = () => api.listConversations().then((list) => {
279
+ conversations = [...list].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
280
+ updateBadge();
281
+ if (open && view.kind === "tab" && view.tab !== "help") render();
282
+ }).catch(() => {
283
+ });
284
+ function renderHead(title, back) {
285
+ head.textContent = "";
286
+ if (back) {
287
+ const b = el(doc, "button", "g-m-back", "\u2190");
288
+ b.type = "button";
289
+ b.setAttribute("aria-label", "Back");
290
+ b.addEventListener("click", back);
291
+ head.appendChild(b);
292
+ }
293
+ head.appendChild(el(doc, "h2", "", title));
294
+ const close = el(doc, "button", "g-close", "\xD7");
295
+ close.type = "button";
296
+ close.setAttribute("aria-label", "Close");
297
+ close.addEventListener("click", () => setOpen(false));
298
+ head.appendChild(close);
299
+ }
300
+ function renderTabs() {
301
+ tabs.textContent = "";
302
+ tabs.hidden = view.kind === "thread";
303
+ const items = [
304
+ ["home", "\u2302", "Home"],
305
+ ["messages", "\u{1F4AC}", "Messages"],
306
+ ["help", "?", "Help"]
307
+ ];
308
+ for (const [tab, icon, label] of items) {
309
+ const b = el(doc, "button", "g-m-tab");
310
+ b.type = "button";
311
+ b.setAttribute("role", "tab");
312
+ b.setAttribute("aria-selected", String(view.kind === "tab" && view.tab === tab));
313
+ const i = el(doc, "span", "g-m-tab-icon", icon);
314
+ i.setAttribute("aria-hidden", "true");
315
+ b.append(i, el(doc, "span", "", label));
316
+ b.addEventListener("click", () => go({ kind: "tab", tab }));
317
+ tabs.appendChild(b);
318
+ }
319
+ }
320
+ function conversationCard(c) {
321
+ const card = el(doc, "button", "g-m-card");
322
+ card.type = "button";
323
+ const row = el(doc, "div", "g-m-row");
324
+ if (c.unread > 0) row.appendChild(el(doc, "span", "g-m-dot"));
325
+ const text = el(doc, "div", "g-m-grow");
326
+ text.append(el(doc, "div", "g-m-card-title g-m-ellipsis", c.subject || "Conversation"), el(doc, "div", "g-m-card-sub g-m-ellipsis", c.lastMessagePreview));
327
+ row.append(text, el(doc, "span", "g-m-time", relativeTime(c.lastMessageAt)));
328
+ card.appendChild(row);
329
+ card.setAttribute("aria-label", `${c.subject || "Conversation"}${c.unread ? ", unread" : ""}`);
330
+ card.addEventListener("click", () => go({ kind: "thread", conversationId: c.id }));
331
+ return card;
332
+ }
333
+ function renderHome() {
334
+ renderHead(config.name);
335
+ const scroll = el(doc, "div", "g-m-scroll");
336
+ scroll.append(el(doc, "div", "g-m-hero", "Hi there \u{1F44B}"), el(doc, "p", "g-m-greet", config.greeting));
337
+ const ask = el(doc, "button", "g-m-card");
338
+ ask.type = "button";
339
+ ask.append(el(doc, "div", "g-m-card-title", "Ask a question"), el(doc, "div", "g-m-card-sub", config.aiAvailable ? `${config.name} answers right away, and the team can help too` : "The team will reply here"));
340
+ ask.addEventListener("click", () => go({ kind: "thread", conversationId: null }));
341
+ scroll.appendChild(ask);
342
+ const recent = conversations[0];
343
+ if (recent) {
344
+ scroll.appendChild(el(doc, "div", "g-rc-section", "Recent conversation"));
345
+ scroll.appendChild(conversationCard(recent));
346
+ }
347
+ const search = el(doc, "button", "g-m-card");
348
+ search.type = "button";
349
+ search.append(el(doc, "div", "g-m-card-title", "Search for help"), el(doc, "div", "g-m-card-sub", "Browse articles and guides"));
350
+ search.addEventListener("click", () => go({ kind: "tab", tab: "help" }));
351
+ scroll.appendChild(search);
352
+ main.appendChild(scroll);
353
+ }
354
+ function renderMessagesTab() {
355
+ renderHead("Messages");
356
+ const scroll = el(doc, "div", "g-m-scroll");
357
+ if (!conversations.length) scroll.appendChild(el(doc, "div", "g-m-empty", "No conversations yet."));
358
+ conversations.forEach((c) => scroll.appendChild(conversationCard(c)));
359
+ const start = el(doc, "button", "g-btn g-btn-primary", "Send us a message");
360
+ start.type = "button";
361
+ start.style.width = "100%";
362
+ start.addEventListener("click", () => go({ kind: "thread", conversationId: null }));
363
+ scroll.appendChild(start);
364
+ main.appendChild(scroll);
365
+ }
366
+ function renderHelpTab() {
367
+ renderHead("Help");
368
+ loadArticles();
369
+ help.render();
370
+ main.appendChild(help.el);
371
+ }
372
+ let log = null;
373
+ let foot = null;
374
+ let textarea = null;
375
+ let sendButton = null;
376
+ function messageNode(m) {
377
+ const wrap = el(doc, "div", `g-m-msg g-from-${m.author}${m.status === "failed" ? " g-m-failed" : ""}`);
378
+ if (m.author === "system") {
379
+ wrap.textContent = m.body;
380
+ return wrap;
381
+ }
382
+ if (m.author !== "user") {
383
+ const author = el(doc, "div", "g-m-author", m.author === "scout" ? config.name : m.authorName || "Support team");
384
+ if (m.author === "scout") author.appendChild(el(doc, "span", "g-m-ai", "AI"));
385
+ wrap.appendChild(author);
386
+ }
387
+ const bubble = el(doc, "div", "g-m-bubble");
388
+ const live = streaming.get(m.id);
389
+ if (m.status === "pending" && !live) {
390
+ const dots = el(doc, "span", "g-m-typing");
391
+ dots.setAttribute("aria-label", `${config.name} is typing`);
392
+ dots.append(el(doc, "span"), el(doc, "span"), el(doc, "span"));
393
+ bubble.appendChild(dots);
394
+ } else if (m.status === "failed") {
395
+ bubble.textContent = "This reply couldn't be completed.";
396
+ } else {
397
+ bubble.appendChild(renderSafeText(doc, live ?? m.body));
398
+ }
399
+ if (m.status === "pending") bubble.setAttribute("aria-busy", "true");
400
+ bubble.dataset.messageId = m.id;
401
+ wrap.appendChild(bubble);
402
+ if (m.citations.length || m.suggestedGuides.length) {
403
+ const chips = el(doc, "div", "g-m-chips");
404
+ for (const g of m.suggestedGuides) {
405
+ const b = el(doc, "button", "g-m-chip g-m-guide", `\u25B6 Show me: ${g.name}`);
406
+ b.type = "button";
407
+ b.addEventListener("click", () => {
408
+ setOpen(false);
409
+ options.onStartGuide?.(g.key);
410
+ });
411
+ chips.appendChild(b);
412
+ }
413
+ for (const c of m.citations) {
414
+ const b = el(doc, "button", "g-m-chip", `\u{1F4C4} ${c.title}`);
415
+ b.type = "button";
416
+ b.addEventListener("click", () => openArticle(c.articleId));
417
+ chips.appendChild(b);
418
+ }
419
+ wrap.appendChild(chips);
420
+ }
421
+ return wrap;
422
+ }
423
+ function renderLog() {
424
+ if (!log) return;
425
+ const atBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 40;
426
+ log.textContent = "";
427
+ if (!messages.length) {
428
+ const intro = { id: "intro", author: config.aiAvailable ? "scout" : "agent", authorName: "Support team", body: config.greeting, citations: [], suggestedGuides: [], status: "complete", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
429
+ log.appendChild(messageNode(intro));
430
+ }
431
+ messages.forEach((m) => log.appendChild(messageNode(m)));
432
+ if (atBottom || sending) log.scrollTop = log.scrollHeight;
433
+ renderFoot();
434
+ }
435
+ function renderFoot() {
436
+ if (!foot || !textarea || !sendButton) return;
437
+ sendButton.disabled = sending || !textarea.value.trim();
438
+ textarea.disabled = sending;
439
+ for (const extra of Array.from(foot.querySelectorAll(".g-m-human,.g-m-rate,.g-m-note"))) extra.remove();
440
+ if (current?.status === "closed" && current.rating == null) {
441
+ const rate = el(doc, "div", "g-m-rate", "How was this conversation? ");
442
+ for (const [score, face, label] of [
443
+ [1, "\u{1F61E}", "Bad"],
444
+ [3, "\u{1F610}", "OK"],
445
+ [5, "\u{1F600}", "Great"]
446
+ ]) {
447
+ const b = el(doc, "button", "", face);
448
+ b.type = "button";
449
+ b.setAttribute("aria-label", label);
450
+ b.addEventListener("click", () => {
451
+ if (!current) return;
452
+ const id = current.id;
453
+ current = { ...current, rating: score };
454
+ upsertConversation(current);
455
+ renderFoot();
456
+ api.rate(id, score).catch(() => {
457
+ });
458
+ });
459
+ rate.appendChild(b);
460
+ }
461
+ foot.prepend(rate);
462
+ } else if (current && current.rating != null && current.status === "closed") {
463
+ foot.prepend(el(doc, "div", "g-m-note", "Thanks for the feedback!"));
464
+ }
465
+ if (config.handoffEnabled && config.aiAvailable && current?.status === "bot" && !sending) {
466
+ const human = el(doc, "button", "g-m-human", "Talk to a person");
467
+ human.type = "button";
468
+ human.addEventListener("click", () => {
469
+ if (!current) return;
470
+ const id = current.id;
471
+ human.disabled = true;
472
+ api.handoff(id).then((res) => {
473
+ if (current?.id !== id) return;
474
+ upsertConversation(res.conversation);
475
+ res.messages.forEach(upsertMessage);
476
+ renderLog();
477
+ }).catch(() => human.disabled = false);
478
+ });
479
+ foot.appendChild(human);
480
+ }
481
+ }
482
+ function renderThread(conversationId) {
483
+ renderHead(current?.subject || (conversationId ? "Conversation" : "New conversation"), () => go({ kind: "tab", tab: "messages" }));
484
+ log = el(doc, "div", "g-m-log");
485
+ log.setAttribute("role", "log");
486
+ log.setAttribute("aria-live", "polite");
487
+ foot = el(doc, "div", "g-m-foot");
488
+ const compose = el(doc, "form", "g-m-compose");
489
+ textarea = el(doc, "textarea");
490
+ textarea.rows = 1;
491
+ textarea.maxLength = 4e3;
492
+ textarea.placeholder = current?.status === "open" ? "Reply to the team\u2026" : "Ask a question\u2026";
493
+ textarea.setAttribute("aria-label", "Message");
494
+ textarea.value = draft;
495
+ sendButton = el(doc, "button", "g-m-send", "\u2191");
496
+ sendButton.type = "submit";
497
+ sendButton.setAttribute("aria-label", "Send");
498
+ compose.append(textarea, sendButton);
499
+ foot.appendChild(compose);
500
+ textarea.addEventListener("input", () => {
501
+ draft = textarea.value;
502
+ if (sendButton) sendButton.disabled = sending || !draft.trim();
503
+ });
504
+ textarea.addEventListener("keydown", (e) => {
505
+ if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
506
+ e.preventDefault();
507
+ submit();
508
+ }
509
+ });
510
+ compose.addEventListener("submit", (e) => {
511
+ e.preventDefault();
512
+ submit();
513
+ });
514
+ main.append(log, foot);
515
+ renderLog();
516
+ textarea.focus?.({ preventScroll: true });
517
+ }
518
+ function render() {
519
+ if (destroyed) return;
520
+ main.textContent = "";
521
+ log = foot = null;
522
+ textarea = null;
523
+ sendButton = null;
524
+ if (view.kind === "thread") renderThread(view.conversationId);
525
+ else if (view.tab === "home") renderHome();
526
+ else if (view.tab === "messages") renderMessagesTab();
527
+ else renderHelpTab();
528
+ renderTabs();
529
+ }
530
+ const handlers = (seq) => ({
531
+ onConversation(c) {
532
+ if (seq !== threadSeq) return;
533
+ current = c;
534
+ view = { kind: "thread", conversationId: c.id };
535
+ upsertConversation(c);
536
+ },
537
+ onMessage(m) {
538
+ if (seq !== threadSeq) return;
539
+ upsertMessage(m);
540
+ renderLog();
541
+ },
542
+ onDelta(id, text) {
543
+ if (seq !== threadSeq) return;
544
+ streaming.set(id, (streaming.get(id) ?? "") + text);
545
+ const bubble = log?.querySelector(`[data-message-id="${id}"]`);
546
+ if (!bubble) return renderLog();
547
+ bubble.textContent = "";
548
+ bubble.appendChild(renderSafeText(doc, streaming.get(id)));
549
+ if (log) log.scrollTop = log.scrollHeight;
550
+ },
551
+ onDone(m, c) {
552
+ if (seq !== threadSeq) return;
553
+ if (m) {
554
+ upsertMessage(m);
555
+ options.onMessage?.(m, c);
556
+ }
557
+ upsertConversation({ ...c, unread: 0 });
558
+ current = { ...c, unread: 0 };
559
+ },
560
+ onError() {
561
+ }
562
+ });
563
+ function submit() {
564
+ if (!textarea || sending) return;
565
+ const text = textarea.value.trim();
566
+ if (!text) return;
567
+ sending = true;
568
+ draft = "";
569
+ textarea.value = "";
570
+ renderFoot();
571
+ const seq = threadSeq;
572
+ const h = handlers(seq);
573
+ const done = current ? api.send(current.id, text, h) : api.start(text, h);
574
+ done.catch(() => {
575
+ if (seq !== threadSeq) return;
576
+ draft = text;
577
+ if (textarea) textarea.value = text;
578
+ const note = { id: `local_${Date.now()}`, author: "system", body: "Your message couldn't be sent. Please try again.", citations: [], suggestedGuides: [], status: "complete", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
579
+ upsertMessage(note);
580
+ }).finally(() => {
581
+ if (seq !== threadSeq) return;
582
+ sending = false;
583
+ streaming.clear();
584
+ if (view.kind === "thread") {
585
+ if (current) renderHead(current.subject || "Conversation", () => go({ kind: "tab", tab: "messages" }));
586
+ renderLog();
587
+ }
588
+ scheduleThreadPoll();
589
+ });
590
+ }
591
+ function scheduleThreadPoll() {
592
+ if (threadTimer) clearTimeout(threadTimer);
593
+ threadTimer = null;
594
+ if (!open || view.kind !== "thread" || !current || destroyed) return;
595
+ threadTimer = setTimeout(pollThread, poll.thread);
596
+ }
597
+ function pollThread() {
598
+ if (!current || sending) return scheduleThreadPoll();
599
+ const seq = threadSeq;
600
+ const pending = messages.find((m) => m.status === "pending");
601
+ const last = messages.filter((m) => !m.id.startsWith("local_")).at(-1);
602
+ const since = pending ? new Date(Date.parse(pending.createdAt) - 1).toISOString() : last?.createdAt ?? null;
603
+ api.loadMessages(current.id, since).then((res) => {
604
+ if (seq !== threadSeq) return;
605
+ const before = messages.length;
606
+ const changed = res.messages.some((m) => {
607
+ const prev = messages.find((x) => x.id === m.id);
608
+ return !prev || prev.status !== m.status || prev.body !== m.body;
609
+ });
610
+ res.messages.forEach(upsertMessage);
611
+ upsertConversation({ ...res.conversation, unread: 0 });
612
+ current = { ...res.conversation, unread: 0 };
613
+ if (changed || messages.length !== before) {
614
+ renderLog();
615
+ res.messages.filter((m) => m.author !== "user").forEach((m) => options.onMessage?.(m, res.conversation));
616
+ } else renderFoot();
617
+ }).catch(() => {
618
+ }).finally(scheduleThreadPoll);
619
+ }
620
+ function go(next) {
621
+ threadSeq++;
622
+ if (threadTimer) clearTimeout(threadTimer);
623
+ threadTimer = null;
624
+ if (view.kind === "tab" && view.tab === "help" && !(next.kind === "tab" && next.tab === "help")) help.reset();
625
+ view = next;
626
+ if (next.kind === "thread") {
627
+ messages = [];
628
+ streaming.clear();
629
+ sending = false;
630
+ current = next.conversationId ? conversations.find((c) => c.id === next.conversationId) ?? null : null;
631
+ render();
632
+ if (next.conversationId) {
633
+ const seq = threadSeq;
634
+ api.loadMessages(next.conversationId).then((res) => {
635
+ if (seq !== threadSeq) return;
636
+ res.messages.forEach(upsertMessage);
637
+ current = { ...res.conversation, unread: 0 };
638
+ upsertConversation(current);
639
+ renderHead(current.subject || "Conversation", () => go({ kind: "tab", tab: "messages" }));
640
+ renderLog();
641
+ }).catch(() => {
642
+ }).finally(scheduleThreadPoll);
643
+ }
644
+ } else {
645
+ draft = "";
646
+ render();
647
+ }
648
+ }
649
+ function openArticle(id) {
650
+ go({ kind: "tab", tab: "help" });
651
+ help.openArticle(id);
652
+ }
653
+ function startBackgroundPoll() {
654
+ if (backgroundTimer) clearInterval(backgroundTimer);
655
+ backgroundTimer = setInterval(() => {
656
+ if (!open && conversations.length && !destroyed) void refreshConversations();
657
+ }, poll.background);
658
+ }
659
+ const setOpen = (v) => {
660
+ if (open === v || destroyed) return;
661
+ open = v;
662
+ panel.hidden = !v;
663
+ launcher.setAttribute("aria-expanded", String(v));
664
+ launcher.setAttribute("aria-label", v ? `Close ${config.name}` : `Open ${config.name}`);
665
+ updateBadge();
666
+ if (v) {
667
+ render();
668
+ void refreshConversations();
669
+ if (view.kind === "thread") scheduleThreadPoll();
670
+ options.onOpen?.();
671
+ } else {
672
+ if (threadTimer) clearTimeout(threadTimer);
673
+ threadTimer = null;
674
+ options.onClose?.();
675
+ }
676
+ };
677
+ launcher.addEventListener("click", () => setOpen(!open));
678
+ const onKey = (e) => {
679
+ if (e.key === "Escape") {
680
+ e.stopPropagation();
681
+ setOpen(false);
682
+ launcher.focus();
683
+ }
684
+ };
685
+ panel.addEventListener("keydown", onKey);
686
+ void refreshConversations();
687
+ startBackgroundPoll();
688
+ return {
689
+ el: root,
690
+ open(tab) {
691
+ if (tab) view = { kind: "tab", tab };
692
+ if (open && tab) render();
693
+ setOpen(true);
694
+ },
695
+ close: () => setOpen(false),
696
+ toggle: () => setOpen(!open),
697
+ isOpen: () => open,
698
+ showNewMessage(text) {
699
+ setOpen(true);
700
+ go({ kind: "thread", conversationId: null });
701
+ if (text && textarea) {
702
+ textarea.value = text.slice(0, 4e3);
703
+ draft = textarea.value;
704
+ renderFoot();
705
+ }
706
+ },
707
+ openArticle(id) {
708
+ setOpen(true);
709
+ openArticle(id);
710
+ },
711
+ destroy() {
712
+ destroyed = true;
713
+ if (threadTimer) clearTimeout(threadTimer);
714
+ if (backgroundTimer) clearInterval(backgroundTimer);
715
+ panel.removeEventListener("keydown", onKey);
716
+ help.destroy();
717
+ shadowHost?.removeAttribute("data-g-messenger");
718
+ root.remove();
719
+ }
720
+ };
721
+ }
722
+ export {
723
+ relativeTime,
724
+ renderMessenger
725
+ };
726
+ //# sourceMappingURL=messenger.js.map