@sunboard/react 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -26
- package/dist/index.d.mts +299 -47
- package/dist/index.mjs +1196 -264
- package/dist/styles.css +251 -68
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { FloatingArrow, FloatingPortal, arrow, autoUpdate, flip, offset, shift, useFloating } from "@floating-ui/react";
|
|
6
|
-
import { flushSync } from "react-dom";
|
|
7
6
|
//#region src/debug.ts
|
|
8
7
|
const noLog = () => {};
|
|
9
8
|
function consoleLogger() {
|
|
@@ -194,7 +193,9 @@ const defaultTheme = createTheme("Default", {
|
|
|
194
193
|
const defaultComponentSettings = {
|
|
195
194
|
launcher: {
|
|
196
195
|
showBranding: true,
|
|
197
|
-
supportUrl: null
|
|
196
|
+
supportUrl: null,
|
|
197
|
+
talkToPerson: false,
|
|
198
|
+
replyTime: "We reply within a few hours."
|
|
198
199
|
},
|
|
199
200
|
tour: {
|
|
200
201
|
showStepCount: true,
|
|
@@ -220,7 +221,6 @@ const enLabels = {
|
|
|
220
221
|
cancel: "Cancel",
|
|
221
222
|
send: "Send",
|
|
222
223
|
latest: "Latest",
|
|
223
|
-
sourcesCount: (count) => `${count} ${count === 1 ? "source" : "sources"}`,
|
|
224
224
|
greeting: (name) => name ? `Hey ${name},` : "Hey there,",
|
|
225
225
|
guideClosing: "I put together a guide for you that walks you through it.",
|
|
226
226
|
tourFinishedAsk: "Tour finished, how was your experience?",
|
|
@@ -228,7 +228,7 @@ const enLabels = {
|
|
|
228
228
|
rateTour: "Rate this tour",
|
|
229
229
|
stars: (count) => `${count} ${count === 1 ? "star" : "stars"}`,
|
|
230
230
|
ratingThanks: "Thanks for the feedback.",
|
|
231
|
-
|
|
231
|
+
thread: "Conversation",
|
|
232
232
|
writingStatus: "Writing an answer",
|
|
233
233
|
stages: {
|
|
234
234
|
searching: (product) => `Searching the ${product} help docs`,
|
|
@@ -236,7 +236,6 @@ const enLabels = {
|
|
|
236
236
|
checking: "Checking it against the docs",
|
|
237
237
|
slow: "Taking a little longer than usual"
|
|
238
238
|
},
|
|
239
|
-
sources: "Sources",
|
|
240
239
|
commonQuestions: "Common questions",
|
|
241
240
|
showMeHow: "Show me how",
|
|
242
241
|
newTab: "(opens in a new tab)",
|
|
@@ -249,11 +248,39 @@ const enLabels = {
|
|
|
249
248
|
lostBody: "Check your connection and try again.",
|
|
250
249
|
tryAgain: "Try again",
|
|
251
250
|
notCovered: (product) => `The ${product} help docs don't cover this yet.`,
|
|
252
|
-
coveredBy: (article, heading) => heading ? `The section "${heading}" of the article "${article}" covers this.` : `The article "${article}" covers this.`,
|
|
253
|
-
guideFound: "There is a guide for this.",
|
|
254
251
|
closeAria: "Close help",
|
|
255
252
|
poweredBy: "Powered by"
|
|
256
253
|
},
|
|
254
|
+
support: {
|
|
255
|
+
talkToPerson: "Talk to a person",
|
|
256
|
+
title: (company) => `${company} support`,
|
|
257
|
+
back: "Back",
|
|
258
|
+
talking: (company) => `Talking to ${company} support`,
|
|
259
|
+
greeting: (name) => `Hey ${name ?? "there"}, what can we help you with?`,
|
|
260
|
+
from: (company) => `${company} support`,
|
|
261
|
+
boxLabel: (company) => `Write to ${company} support`,
|
|
262
|
+
placeholder: (company) => `Write to ${company} support…`,
|
|
263
|
+
teamMessage: (who, company, time) => [
|
|
264
|
+
who,
|
|
265
|
+
`${company} support`,
|
|
266
|
+
time
|
|
267
|
+
].filter(Boolean).join(" · "),
|
|
268
|
+
asked: (question) => `Asked the assistant: ${question}`,
|
|
269
|
+
closed: (company) => `${company} support closed this conversation`,
|
|
270
|
+
reopen: "Write again to reopen it.",
|
|
271
|
+
conversationWith: (company) => `Conversation with ${company} support`,
|
|
272
|
+
replied: (who) => `${who} replied`,
|
|
273
|
+
waiting: "Waiting for a reply",
|
|
274
|
+
newReply: "New reply",
|
|
275
|
+
openAriaNewReply: "Open help, new reply",
|
|
276
|
+
past: "Past conversations",
|
|
277
|
+
closedWhen: (when, who) => who ? `Closed ${when} · ${who}` : `Closed ${when}`,
|
|
278
|
+
ago: (days) => days < 1 ? "today" : days < 2 ? "yesterday" : days < 14 ? `${days} days ago` : days < 60 ? `${Math.round(days / 7)} weeks ago` : `${Math.round(days / 30)} months ago`,
|
|
279
|
+
tooMany: "Too many messages this hour",
|
|
280
|
+
writeAgainIn: (minutes) => `You can write again in about ${minutes} ${minutes === 1 ? "minute" : "minutes"}.`,
|
|
281
|
+
lost: "Could not send your message",
|
|
282
|
+
lostBody: "Check your connection and try again."
|
|
283
|
+
},
|
|
257
284
|
tour: {
|
|
258
285
|
back: "Back",
|
|
259
286
|
next: "Next",
|
|
@@ -283,6 +310,10 @@ function withLabels(overrides) {
|
|
|
283
310
|
...enLabels.launcher,
|
|
284
311
|
...overrides.launcher
|
|
285
312
|
},
|
|
313
|
+
support: {
|
|
314
|
+
...enLabels.support,
|
|
315
|
+
...overrides.support
|
|
316
|
+
},
|
|
286
317
|
tour: {
|
|
287
318
|
...enLabels.tour,
|
|
288
319
|
...overrides.tour
|
|
@@ -382,6 +413,8 @@ const TOUR_RATINGS = [
|
|
|
382
413
|
4,
|
|
383
414
|
5
|
|
384
415
|
];
|
|
416
|
+
/** One home for the cap, so the composer and the route agree on it. */
|
|
417
|
+
const CONVERSATION_MESSAGE_MAX_LENGTH = 4e3;
|
|
385
418
|
//#endregion
|
|
386
419
|
//#region src/active-tour.ts
|
|
387
420
|
const STORAGE_KEY = "sunboard:active-tour";
|
|
@@ -418,7 +451,7 @@ function stepInRange(guides, key, stepIndex) {
|
|
|
418
451
|
const PREFIX = "sunboard:bootstrap:";
|
|
419
452
|
/** Past this, content the server has since changed is likelier than not. */
|
|
420
453
|
const MAX_AGE_MS = 6048e5;
|
|
421
|
-
const VERSION =
|
|
454
|
+
const VERSION = 5;
|
|
422
455
|
function readCachedBootstrap(key) {
|
|
423
456
|
if (typeof window === "undefined") return null;
|
|
424
457
|
try {
|
|
@@ -1355,15 +1388,221 @@ function useThemeMode(setting) {
|
|
|
1355
1388
|
return setting === "auto" ? detected : setting;
|
|
1356
1389
|
}
|
|
1357
1390
|
//#endregion
|
|
1391
|
+
//#region src/memory-support.ts
|
|
1392
|
+
let counter = 0;
|
|
1393
|
+
const nextId = (kind) => `memory-${kind}-${++counter}`;
|
|
1394
|
+
var MemorySupport = class {
|
|
1395
|
+
script;
|
|
1396
|
+
starts = [];
|
|
1397
|
+
sent = [];
|
|
1398
|
+
reads = [];
|
|
1399
|
+
polls = 0;
|
|
1400
|
+
views = 0;
|
|
1401
|
+
conversations;
|
|
1402
|
+
constructor(script) {
|
|
1403
|
+
this.script = script;
|
|
1404
|
+
this.conversations = (script.conversations ?? []).map(stored);
|
|
1405
|
+
}
|
|
1406
|
+
conversationPoll() {
|
|
1407
|
+
this.polls += 1;
|
|
1408
|
+
const open = this.open();
|
|
1409
|
+
return Promise.resolve({
|
|
1410
|
+
open: open ? summary(open) : null,
|
|
1411
|
+
hasPast: this.conversations.some((conversation) => conversation.view.closed)
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
conversation(id, after) {
|
|
1415
|
+
this.views += 1;
|
|
1416
|
+
const found = this.find(id);
|
|
1417
|
+
if (!found) return Promise.resolve(null);
|
|
1418
|
+
const view = structuredClone(found.view);
|
|
1419
|
+
const at = after === void 0 ? -1 : view.messages.findIndex((message) => message.id === after);
|
|
1420
|
+
return Promise.resolve({
|
|
1421
|
+
...view,
|
|
1422
|
+
messages: view.messages.slice(at + 1)
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
startConversation(start) {
|
|
1426
|
+
this.starts.push(start);
|
|
1427
|
+
if (this.script.refuse) return Promise.resolve({ refused: this.script.refuse });
|
|
1428
|
+
let conversation = this.open();
|
|
1429
|
+
if (!conversation) {
|
|
1430
|
+
conversation = stored({ messages: [] });
|
|
1431
|
+
this.conversations.unshift(conversation);
|
|
1432
|
+
}
|
|
1433
|
+
return Promise.resolve(this.write(conversation, start.text));
|
|
1434
|
+
}
|
|
1435
|
+
sendMessage(id, text) {
|
|
1436
|
+
this.sent.push({
|
|
1437
|
+
id,
|
|
1438
|
+
text
|
|
1439
|
+
});
|
|
1440
|
+
if (this.script.refuse) return Promise.resolve({ refused: this.script.refuse });
|
|
1441
|
+
const conversation = this.find(id);
|
|
1442
|
+
if (!conversation) return Promise.resolve({ refused: "lost" });
|
|
1443
|
+
return Promise.resolve(this.write(conversation, text));
|
|
1444
|
+
}
|
|
1445
|
+
markRead(id) {
|
|
1446
|
+
this.reads.push(id);
|
|
1447
|
+
const conversation = this.find(id);
|
|
1448
|
+
if (conversation) conversation.unread = false;
|
|
1449
|
+
return Promise.resolve();
|
|
1450
|
+
}
|
|
1451
|
+
pastConversations() {
|
|
1452
|
+
const closed = this.conversations.filter((conversation) => conversation.view.closed).sort((a, b) => (b.view.closedAt ?? "").localeCompare(a.view.closedAt ?? ""));
|
|
1453
|
+
return Promise.resolve(closed.map((conversation) => ({
|
|
1454
|
+
id: conversation.view.id,
|
|
1455
|
+
title: conversation.view.messages.find((message) => message.by === "END_USER")?.text ?? "",
|
|
1456
|
+
closedAt: conversation.view.closedAt,
|
|
1457
|
+
closedBy: conversation.closedBy
|
|
1458
|
+
})));
|
|
1459
|
+
}
|
|
1460
|
+
/** The team writes into the open conversation. */
|
|
1461
|
+
teamReplies(text, who) {
|
|
1462
|
+
const conversation = this.open();
|
|
1463
|
+
if (!conversation) return;
|
|
1464
|
+
conversation.view.messages.push(message("TEAM", text, who));
|
|
1465
|
+
conversation.view.status = "PENDING";
|
|
1466
|
+
conversation.unread = true;
|
|
1467
|
+
}
|
|
1468
|
+
/** The team closes the open conversation. */
|
|
1469
|
+
teamCloses(who) {
|
|
1470
|
+
const conversation = this.open();
|
|
1471
|
+
if (!conversation) return;
|
|
1472
|
+
conversation.view.status = "CLOSED";
|
|
1473
|
+
conversation.view.closed = true;
|
|
1474
|
+
conversation.view.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1475
|
+
conversation.closedBy = who;
|
|
1476
|
+
}
|
|
1477
|
+
write(conversation, text) {
|
|
1478
|
+
const sent = message("END_USER", text);
|
|
1479
|
+
const first = conversation.view.messages.length === 0;
|
|
1480
|
+
conversation.view.messages.push(sent);
|
|
1481
|
+
conversation.view.status = "OPEN";
|
|
1482
|
+
conversation.view.closed = false;
|
|
1483
|
+
conversation.view.closedAt = null;
|
|
1484
|
+
conversation.closedBy = null;
|
|
1485
|
+
const { reply } = this.script;
|
|
1486
|
+
if (first && reply) setTimeout(() => this.teamReplies(reply.text, reply.who), reply.afterMs);
|
|
1487
|
+
return {
|
|
1488
|
+
summary: {
|
|
1489
|
+
...summary(conversation),
|
|
1490
|
+
unread: false
|
|
1491
|
+
},
|
|
1492
|
+
message: sent
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
open() {
|
|
1496
|
+
return this.conversations.find((conversation) => !conversation.view.closed);
|
|
1497
|
+
}
|
|
1498
|
+
find(id) {
|
|
1499
|
+
return this.conversations.find((conversation) => conversation.view.id === id);
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
function message(by, text, who) {
|
|
1503
|
+
return {
|
|
1504
|
+
id: nextId("message"),
|
|
1505
|
+
by,
|
|
1506
|
+
who: who ?? null,
|
|
1507
|
+
text,
|
|
1508
|
+
sentAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function folded(questions) {
|
|
1512
|
+
return (questions ?? []).map((question) => ({
|
|
1513
|
+
...question,
|
|
1514
|
+
id: nextId("question"),
|
|
1515
|
+
outcome: "ANSWERED"
|
|
1516
|
+
}));
|
|
1517
|
+
}
|
|
1518
|
+
function stored(script) {
|
|
1519
|
+
const messages = script.messages.map((scripted) => ({
|
|
1520
|
+
id: nextId("message"),
|
|
1521
|
+
by: scripted.by,
|
|
1522
|
+
who: scripted.who ?? null,
|
|
1523
|
+
text: scripted.text,
|
|
1524
|
+
sentAt: scripted.sentAt
|
|
1525
|
+
}));
|
|
1526
|
+
const closed = script.closedAt !== void 0;
|
|
1527
|
+
return {
|
|
1528
|
+
view: {
|
|
1529
|
+
id: nextId("conversation"),
|
|
1530
|
+
status: closed ? "CLOSED" : messages.at(-1)?.by === "TEAM" ? "PENDING" : "OPEN",
|
|
1531
|
+
startedAt: messages[0]?.sentAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1532
|
+
closedAt: script.closedAt ?? null,
|
|
1533
|
+
closed,
|
|
1534
|
+
messages,
|
|
1535
|
+
before: folded(script.before),
|
|
1536
|
+
during: folded(script.during)
|
|
1537
|
+
},
|
|
1538
|
+
unread: script.unread ?? false,
|
|
1539
|
+
closedBy: script.closedBy ?? null
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function summary({ view, unread }) {
|
|
1543
|
+
const last = view.messages.at(-1);
|
|
1544
|
+
return {
|
|
1545
|
+
id: view.id,
|
|
1546
|
+
status: view.status,
|
|
1547
|
+
lastMessageAt: last?.sentAt ?? view.startedAt,
|
|
1548
|
+
lastMessageBy: last?.by ?? "END_USER",
|
|
1549
|
+
lastMessageText: last?.text ?? "",
|
|
1550
|
+
lastMessageWho: last?.by === "TEAM" ? last.who ?? null : null,
|
|
1551
|
+
unread
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
//#endregion
|
|
1555
|
+
//#region src/user-token.ts
|
|
1556
|
+
const NOT_A_TOKEN = "Sunboard: userToken is not a user token. Make it on your server with your project's identity secret.";
|
|
1557
|
+
function decodeSegment(segment) {
|
|
1558
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
1559
|
+
const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
|
|
1560
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Reads the token's claims without checking its signature, which only the runtime can do. Throws
|
|
1564
|
+
* on anything that is not a token with a subject.
|
|
1565
|
+
*/
|
|
1566
|
+
function readUserToken(token) {
|
|
1567
|
+
const [, body, signature, extra] = token.split(".");
|
|
1568
|
+
if (body === void 0 || signature === void 0 || extra !== void 0) throw new Error(NOT_A_TOKEN);
|
|
1569
|
+
let payload;
|
|
1570
|
+
try {
|
|
1571
|
+
payload = decodeSegment(body);
|
|
1572
|
+
} catch {
|
|
1573
|
+
throw new Error(NOT_A_TOKEN);
|
|
1574
|
+
}
|
|
1575
|
+
if (typeof payload !== "object" || payload === null) throw new Error(NOT_A_TOKEN);
|
|
1576
|
+
const sub = "sub" in payload ? payload.sub : void 0;
|
|
1577
|
+
const name = "name" in payload ? payload.name : void 0;
|
|
1578
|
+
if (typeof sub !== "string" || sub === "") throw new Error(NOT_A_TOKEN);
|
|
1579
|
+
return typeof name === "string" ? {
|
|
1580
|
+
sub,
|
|
1581
|
+
name
|
|
1582
|
+
} : { sub };
|
|
1583
|
+
}
|
|
1584
|
+
//#endregion
|
|
1358
1585
|
//#region src/transport.ts
|
|
1359
1586
|
/** Long enough for a slow model, short enough that nobody stares at nothing. */
|
|
1360
1587
|
const ASK_TIMEOUT_MS = 3e4;
|
|
1361
|
-
let
|
|
1588
|
+
let memoryThreads = 0;
|
|
1362
1589
|
const STAGES$1 = [
|
|
1363
1590
|
"searching",
|
|
1364
1591
|
"writing",
|
|
1365
1592
|
"checking"
|
|
1366
1593
|
];
|
|
1594
|
+
/** Sunboard switched off: what a connection whose token cannot be read gets, so the host's page stays up. */
|
|
1595
|
+
function offTransport() {
|
|
1596
|
+
const nothing = () => Promise.resolve();
|
|
1597
|
+
return {
|
|
1598
|
+
bootstrap: () => Promise.resolve({ off: true }),
|
|
1599
|
+
mutate: nothing,
|
|
1600
|
+
reportAnchorMiss: nothing,
|
|
1601
|
+
reportAnchorFound: nothing,
|
|
1602
|
+
stepCheck: nothing,
|
|
1603
|
+
recordEvents: nothing
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1367
1606
|
var MemoryTransport = class {
|
|
1368
1607
|
data;
|
|
1369
1608
|
misses = [];
|
|
@@ -1377,10 +1616,12 @@ var MemoryTransport = class {
|
|
|
1377
1616
|
user;
|
|
1378
1617
|
ask;
|
|
1379
1618
|
reportPage;
|
|
1619
|
+
support;
|
|
1380
1620
|
constructor(data, options) {
|
|
1381
1621
|
this.data = data;
|
|
1382
1622
|
this.cacheKey = options?.cacheKey;
|
|
1383
1623
|
this.user = options?.user;
|
|
1624
|
+
if (options?.support) this.support = new MemorySupport(options.support);
|
|
1384
1625
|
if (options?.census) this.reportPage = (report) => {
|
|
1385
1626
|
this.pages.push(report);
|
|
1386
1627
|
return Promise.resolve();
|
|
@@ -1388,7 +1629,7 @@ var MemoryTransport = class {
|
|
|
1388
1629
|
const script = options?.ask;
|
|
1389
1630
|
if (!script) return;
|
|
1390
1631
|
const gapMs = options?.gapMs ?? 0;
|
|
1391
|
-
const
|
|
1632
|
+
const threadId = `memory-thread-${++memoryThreads}`;
|
|
1392
1633
|
this.ask = async (asked, signal, onStage) => {
|
|
1393
1634
|
this.questions.push(asked);
|
|
1394
1635
|
const scripted = script(asked);
|
|
@@ -1409,8 +1650,8 @@ var MemoryTransport = class {
|
|
|
1409
1650
|
kind: "answer",
|
|
1410
1651
|
reply: {
|
|
1411
1652
|
...scripted.reply,
|
|
1412
|
-
|
|
1413
|
-
firstTurn: scripted.reply.firstTurn ?? asked.
|
|
1653
|
+
threadId: scripted.reply.threadId ?? threadId,
|
|
1654
|
+
firstTurn: scripted.reply.firstTurn ?? asked.threadId === void 0
|
|
1414
1655
|
}
|
|
1415
1656
|
};
|
|
1416
1657
|
};
|
|
@@ -1521,89 +1762,114 @@ var HttpTransport = class {
|
|
|
1521
1762
|
previewKey;
|
|
1522
1763
|
cacheKey;
|
|
1523
1764
|
user;
|
|
1765
|
+
support;
|
|
1524
1766
|
constructor(config) {
|
|
1525
1767
|
this.config = config;
|
|
1768
|
+
const { sub, name } = readUserToken(config.userToken);
|
|
1526
1769
|
this.baseUrl = config.baseUrl ?? "https://api.sunboardhq.com";
|
|
1527
|
-
this.user =
|
|
1770
|
+
this.user = name === void 0 ? {} : { firstName: name };
|
|
1528
1771
|
this.previewKey = config.previewKey ?? storedPreviewKey();
|
|
1529
|
-
this.cacheKey = this.previewKey ? void 0 : `${config.publishableKey}:${
|
|
1772
|
+
this.cacheKey = this.previewKey ? void 0 : `${config.publishableKey}:${sub}`;
|
|
1773
|
+
this.support = {
|
|
1774
|
+
conversationPoll: () => this.getJson("/runtime/conversation"),
|
|
1775
|
+
conversation: async (id, after) => {
|
|
1776
|
+
const response = await this.send(`/runtime/conversation/${encodeURIComponent(id)}?${this.identity(after ? { after } : {})}`);
|
|
1777
|
+
if (response.status === 404) return null;
|
|
1778
|
+
return await ok(response, "/runtime/conversation/:id").json();
|
|
1779
|
+
},
|
|
1780
|
+
startConversation: ({ threadId, path, text }) => this.write("/runtime/conversation", {
|
|
1781
|
+
threadId,
|
|
1782
|
+
path,
|
|
1783
|
+
text
|
|
1784
|
+
}),
|
|
1785
|
+
sendMessage: (id, text) => this.write(`/runtime/conversation/${encodeURIComponent(id)}/messages`, { text }),
|
|
1786
|
+
markRead: async (id) => {
|
|
1787
|
+
await this.post(`/runtime/conversation/${encodeURIComponent(id)}/read`, {});
|
|
1788
|
+
},
|
|
1789
|
+
pastConversations: () => this.getJson("/runtime/conversations")
|
|
1790
|
+
};
|
|
1530
1791
|
}
|
|
1531
|
-
|
|
1792
|
+
identity(extra = {}) {
|
|
1793
|
+
return new URLSearchParams({
|
|
1794
|
+
userToken: this.config.userToken,
|
|
1795
|
+
...extra
|
|
1796
|
+
}).toString();
|
|
1797
|
+
}
|
|
1798
|
+
signed(body) {
|
|
1532
1799
|
return {
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
...this.previewKey ? { "x-sunboard-preview-key": this.previewKey } : {}
|
|
1800
|
+
...body,
|
|
1801
|
+
userToken: this.config.userToken
|
|
1536
1802
|
};
|
|
1537
1803
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1804
|
+
send(path, init) {
|
|
1805
|
+
return fetch(`${this.baseUrl}${path}`, {
|
|
1540
1806
|
...init,
|
|
1541
1807
|
headers: {
|
|
1542
1808
|
...this.headers(),
|
|
1543
1809
|
...init?.headers
|
|
1544
1810
|
}
|
|
1545
1811
|
});
|
|
1546
|
-
if (!response.ok) throw new Error(`sunboard runtime request failed: ${response.status} for ${path}`);
|
|
1547
|
-
return response;
|
|
1548
1812
|
}
|
|
1549
|
-
async
|
|
1550
|
-
return await (await this.request(
|
|
1813
|
+
async getJson(path) {
|
|
1814
|
+
return await (await this.request(`${path}?${this.identity()}`)).json();
|
|
1551
1815
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1816
|
+
post(path, body, init) {
|
|
1817
|
+
return this.request(path, {
|
|
1818
|
+
...init,
|
|
1554
1819
|
method: "POST",
|
|
1555
|
-
body: JSON.stringify(
|
|
1556
|
-
user: this.config.userId,
|
|
1557
|
-
...action
|
|
1558
|
-
})
|
|
1820
|
+
body: JSON.stringify(this.signed(body))
|
|
1559
1821
|
});
|
|
1560
1822
|
}
|
|
1561
|
-
async
|
|
1562
|
-
await this.
|
|
1823
|
+
async write(path, body) {
|
|
1824
|
+
const response = await this.send(path, {
|
|
1563
1825
|
method: "POST",
|
|
1564
|
-
body: JSON.stringify(
|
|
1565
|
-
user: this.config.userId,
|
|
1566
|
-
...report
|
|
1567
|
-
})
|
|
1826
|
+
body: JSON.stringify(this.signed(body))
|
|
1568
1827
|
});
|
|
1828
|
+
if (response.status === 429) return {
|
|
1829
|
+
refused: "too-many",
|
|
1830
|
+
retryAt: refusalIn(await response.text())?.retryAt
|
|
1831
|
+
};
|
|
1832
|
+
if (response.status === 409) return { refused: "another-open" };
|
|
1833
|
+
if (response.status === 404) return { refused: "lost" };
|
|
1834
|
+
return await ok(response, path).json();
|
|
1835
|
+
}
|
|
1836
|
+
headers() {
|
|
1837
|
+
return {
|
|
1838
|
+
"content-type": "application/json",
|
|
1839
|
+
"x-sunboard-key": this.config.publishableKey,
|
|
1840
|
+
...this.previewKey ? { "x-sunboard-preview-key": this.previewKey } : {}
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
async request(path, init) {
|
|
1844
|
+
return ok(await this.send(path, init), path);
|
|
1845
|
+
}
|
|
1846
|
+
async bootstrap() {
|
|
1847
|
+
return await (await this.request(`/runtime/bootstrap?${this.identity()}`)).json();
|
|
1848
|
+
}
|
|
1849
|
+
async mutate(action) {
|
|
1850
|
+
await this.post("/runtime/state", action);
|
|
1851
|
+
}
|
|
1852
|
+
async reportAnchorMiss(report) {
|
|
1853
|
+
await this.post("/runtime/anchor-miss", report);
|
|
1569
1854
|
}
|
|
1570
1855
|
async reportAnchorFound(report) {
|
|
1571
|
-
await this.
|
|
1572
|
-
method: "POST",
|
|
1573
|
-
body: JSON.stringify(report)
|
|
1574
|
-
});
|
|
1856
|
+
await this.post("/runtime/anchor-found", report);
|
|
1575
1857
|
}
|
|
1576
1858
|
async stepCheck(report) {
|
|
1577
|
-
await this.
|
|
1578
|
-
method: "POST",
|
|
1579
|
-
body: JSON.stringify(report)
|
|
1580
|
-
});
|
|
1859
|
+
await this.post("/runtime/step-check", report);
|
|
1581
1860
|
}
|
|
1582
1861
|
async recordEvents(events) {
|
|
1583
1862
|
if (events.length === 0) return;
|
|
1584
|
-
await this.
|
|
1585
|
-
method: "POST",
|
|
1586
|
-
body: JSON.stringify({
|
|
1587
|
-
user: this.config.userId,
|
|
1588
|
-
events
|
|
1589
|
-
}),
|
|
1590
|
-
keepalive: true
|
|
1591
|
-
});
|
|
1863
|
+
await this.post("/runtime/events", { events }, { keepalive: true });
|
|
1592
1864
|
}
|
|
1593
1865
|
async reportPage(report) {
|
|
1594
|
-
await this.
|
|
1595
|
-
method: "POST",
|
|
1596
|
-
body: JSON.stringify(report)
|
|
1597
|
-
});
|
|
1866
|
+
await this.post("/runtime/page", report);
|
|
1598
1867
|
}
|
|
1599
1868
|
async ask(asked, signal, onStage) {
|
|
1600
1869
|
const response = await fetch(`${this.baseUrl}/runtime/ask`, {
|
|
1601
1870
|
method: "POST",
|
|
1602
1871
|
headers: this.headers(),
|
|
1603
|
-
body: JSON.stringify(
|
|
1604
|
-
user: this.config.userId,
|
|
1605
|
-
...asked
|
|
1606
|
-
}),
|
|
1872
|
+
body: JSON.stringify(this.signed(asked)),
|
|
1607
1873
|
signal: AbortSignal.any([signal, AbortSignal.timeout(ASK_TIMEOUT_MS)])
|
|
1608
1874
|
});
|
|
1609
1875
|
if (response.status === 400 || response.status === 429) {
|
|
@@ -1620,6 +1886,10 @@ var HttpTransport = class {
|
|
|
1620
1886
|
};
|
|
1621
1887
|
}
|
|
1622
1888
|
};
|
|
1889
|
+
function ok(response, path) {
|
|
1890
|
+
if (!response.ok) throw new Error(`sunboard runtime request failed: ${response.status} for ${path}`);
|
|
1891
|
+
return response;
|
|
1892
|
+
}
|
|
1623
1893
|
/**
|
|
1624
1894
|
* The sentence a 400 or a 429 carries, and the time the cap lifts. Read as
|
|
1625
1895
|
* text and parsed loosely: a proxy in front of the API answers those codes
|
|
@@ -1654,39 +1924,20 @@ function stringOrUndefined(value) {
|
|
|
1654
1924
|
function optionalString(value) {
|
|
1655
1925
|
return value === void 0 || typeof value === "string";
|
|
1656
1926
|
}
|
|
1657
|
-
function parseSource(value) {
|
|
1658
|
-
if (!isRecord$1(value)) return void 0;
|
|
1659
|
-
const { id, title, url, heading } = value;
|
|
1660
|
-
if (typeof id !== "string" || typeof title !== "string") return void 0;
|
|
1661
|
-
if (!optionalString(url) || !optionalString(heading)) return void 0;
|
|
1662
|
-
return {
|
|
1663
|
-
id,
|
|
1664
|
-
title,
|
|
1665
|
-
...url === void 0 ? {} : { url },
|
|
1666
|
-
...heading === void 0 ? {} : { heading }
|
|
1667
|
-
};
|
|
1668
|
-
}
|
|
1669
1927
|
/** A reply as the server sends it, or as the thread kept it in session storage. */
|
|
1670
1928
|
function parseReply(value) {
|
|
1671
1929
|
if (!isRecord$1(value)) return void 0;
|
|
1672
|
-
const { id,
|
|
1673
|
-
if (typeof id !== "string" || typeof
|
|
1930
|
+
const { id, threadId, firstTurn, outcome, answer, guideKey, supportUrl } = value;
|
|
1931
|
+
if (typeof id !== "string" || typeof threadId !== "string") return void 0;
|
|
1674
1932
|
if (typeof firstTurn !== "boolean") return void 0;
|
|
1675
1933
|
if (!isAskOutcome(outcome) || typeof answer !== "string") return void 0;
|
|
1676
|
-
if (!
|
|
1677
|
-
const parsed = [];
|
|
1678
|
-
for (const source of sources) {
|
|
1679
|
-
const article = parseSource(source);
|
|
1680
|
-
if (!article) return void 0;
|
|
1681
|
-
parsed.push(article);
|
|
1682
|
-
}
|
|
1934
|
+
if (!optionalString(guideKey) || !optionalString(supportUrl)) return void 0;
|
|
1683
1935
|
return {
|
|
1684
1936
|
id,
|
|
1685
|
-
|
|
1937
|
+
threadId,
|
|
1686
1938
|
firstTurn,
|
|
1687
1939
|
outcome,
|
|
1688
1940
|
answer,
|
|
1689
|
-
sources: parsed,
|
|
1690
1941
|
...guideKey === void 0 ? {} : { guideKey },
|
|
1691
1942
|
...supportUrl === void 0 ? {} : { supportUrl }
|
|
1692
1943
|
};
|
|
@@ -1750,8 +2001,13 @@ function SunboardProvider(props) {
|
|
|
1750
2001
|
const connectionKey = connection ? JSON.stringify(connection) : null;
|
|
1751
2002
|
const resolvedTransport = useMemo(() => {
|
|
1752
2003
|
if (transport) return transport;
|
|
1753
|
-
if (connectionKey) return
|
|
1754
|
-
|
|
2004
|
+
if (!connectionKey) return null;
|
|
2005
|
+
try {
|
|
2006
|
+
return new HttpTransport(JSON.parse(connectionKey));
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2009
|
+
return offTransport();
|
|
2010
|
+
}
|
|
1755
2011
|
}, [transport, connectionKey]);
|
|
1756
2012
|
if (!resolvedTransport) throw new Error("SunboardProvider needs either a connection or a transport");
|
|
1757
2013
|
const [answer, setAnswer] = useState(null);
|
|
@@ -1985,6 +2241,7 @@ function SunboardProvider(props) {
|
|
|
1985
2241
|
if (!transport.ask) throw new Error("this transport cannot ask questions");
|
|
1986
2242
|
return transport.ask(asked, signal, onStage);
|
|
1987
2243
|
},
|
|
2244
|
+
support: off ? void 0 : resolvedTransport.support,
|
|
1988
2245
|
track,
|
|
1989
2246
|
navigate
|
|
1990
2247
|
};
|
|
@@ -2123,7 +2380,7 @@ function threadReducer(thread, action) {
|
|
|
2123
2380
|
} : response);
|
|
2124
2381
|
return next === thread ? thread : {
|
|
2125
2382
|
...next,
|
|
2126
|
-
|
|
2383
|
+
threadId: action.reply.threadId
|
|
2127
2384
|
};
|
|
2128
2385
|
}
|
|
2129
2386
|
case "failed": return replaceResponse(thread, action.questionId, (response) => response.kind === "waiting" ? {
|
|
@@ -2303,14 +2560,14 @@ function parseThread(value) {
|
|
|
2303
2560
|
if (!message) return null;
|
|
2304
2561
|
messages.push(message);
|
|
2305
2562
|
}
|
|
2306
|
-
const
|
|
2563
|
+
const threadId = typeof value.threadId === "string" ? value.threadId : void 0;
|
|
2307
2564
|
const handoff = parseHandoff(value.handoff);
|
|
2308
|
-
return
|
|
2565
|
+
return threadId === void 0 ? {
|
|
2309
2566
|
messages,
|
|
2310
2567
|
handoff
|
|
2311
2568
|
} : {
|
|
2312
2569
|
messages,
|
|
2313
|
-
|
|
2570
|
+
threadId,
|
|
2314
2571
|
handoff
|
|
2315
2572
|
};
|
|
2316
2573
|
}
|
|
@@ -2399,6 +2656,162 @@ function parseFailure(value) {
|
|
|
2399
2656
|
return null;
|
|
2400
2657
|
}
|
|
2401
2658
|
//#endregion
|
|
2659
|
+
//#region src/support.ts
|
|
2660
|
+
const emptySupport = {
|
|
2661
|
+
off: false,
|
|
2662
|
+
open: null,
|
|
2663
|
+
hasPast: false,
|
|
2664
|
+
view: "assistant",
|
|
2665
|
+
conversation: null,
|
|
2666
|
+
readOnly: false,
|
|
2667
|
+
past: null,
|
|
2668
|
+
sending: false,
|
|
2669
|
+
failure: null
|
|
2670
|
+
};
|
|
2671
|
+
function supportReducer(state, action) {
|
|
2672
|
+
switch (action.type) {
|
|
2673
|
+
case "off": return {
|
|
2674
|
+
...emptySupport,
|
|
2675
|
+
off: true
|
|
2676
|
+
};
|
|
2677
|
+
case "polled": return {
|
|
2678
|
+
...state,
|
|
2679
|
+
open: action.poll.open,
|
|
2680
|
+
hasPast: action.poll.hasPast
|
|
2681
|
+
};
|
|
2682
|
+
case "talk": {
|
|
2683
|
+
const keep = state.conversation !== null && state.conversation.id === state.open?.id;
|
|
2684
|
+
return {
|
|
2685
|
+
...state,
|
|
2686
|
+
view: "conversation",
|
|
2687
|
+
readOnly: false,
|
|
2688
|
+
failure: null,
|
|
2689
|
+
conversation: keep ? state.conversation : null
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
case "loaded": {
|
|
2693
|
+
const loaded = action.conversation;
|
|
2694
|
+
if (!canWrite(state)) return state;
|
|
2695
|
+
if (state.conversation !== null && state.conversation.id !== loaded.id) return state;
|
|
2696
|
+
const conversation = merged(state.conversation, loaded);
|
|
2697
|
+
return {
|
|
2698
|
+
...state,
|
|
2699
|
+
conversation,
|
|
2700
|
+
...state.open?.id === loaded.id ? summarized(state, conversation) : {}
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
case "sending": return {
|
|
2704
|
+
...state,
|
|
2705
|
+
sending: true,
|
|
2706
|
+
failure: null
|
|
2707
|
+
};
|
|
2708
|
+
case "sent": {
|
|
2709
|
+
const { summary, message } = action.sent;
|
|
2710
|
+
const current = state.conversation?.id === summary.id ? state.conversation : null;
|
|
2711
|
+
const conversation = current ? {
|
|
2712
|
+
...current,
|
|
2713
|
+
status: summary.status,
|
|
2714
|
+
closed: false,
|
|
2715
|
+
closedAt: null,
|
|
2716
|
+
messages: withMessages(current.messages, [message])
|
|
2717
|
+
} : {
|
|
2718
|
+
id: summary.id,
|
|
2719
|
+
status: summary.status,
|
|
2720
|
+
startedAt: message.sentAt,
|
|
2721
|
+
closedAt: null,
|
|
2722
|
+
closed: false,
|
|
2723
|
+
messages: [message],
|
|
2724
|
+
before: [],
|
|
2725
|
+
during: []
|
|
2726
|
+
};
|
|
2727
|
+
return {
|
|
2728
|
+
...state,
|
|
2729
|
+
sending: false,
|
|
2730
|
+
open: summary,
|
|
2731
|
+
conversation
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
case "refused": return {
|
|
2735
|
+
...state,
|
|
2736
|
+
sending: false,
|
|
2737
|
+
failure: action.refused
|
|
2738
|
+
};
|
|
2739
|
+
case "read": return state.open?.id === action.id && state.open.unread ? {
|
|
2740
|
+
...state,
|
|
2741
|
+
open: {
|
|
2742
|
+
...state.open,
|
|
2743
|
+
unread: false
|
|
2744
|
+
}
|
|
2745
|
+
} : state;
|
|
2746
|
+
case "showPast": return {
|
|
2747
|
+
...state,
|
|
2748
|
+
view: "past",
|
|
2749
|
+
readOnly: false,
|
|
2750
|
+
failure: null
|
|
2751
|
+
};
|
|
2752
|
+
case "pastLoaded": return {
|
|
2753
|
+
...state,
|
|
2754
|
+
past: action.past,
|
|
2755
|
+
hasPast: state.hasPast || action.past.length > 0
|
|
2756
|
+
};
|
|
2757
|
+
case "openPast": return {
|
|
2758
|
+
...state,
|
|
2759
|
+
view: "conversation",
|
|
2760
|
+
readOnly: true,
|
|
2761
|
+
conversation: action.conversation,
|
|
2762
|
+
failure: null
|
|
2763
|
+
};
|
|
2764
|
+
case "back": return {
|
|
2765
|
+
...state,
|
|
2766
|
+
view: state.view === "conversation" && state.readOnly ? "past" : "assistant",
|
|
2767
|
+
readOnly: false,
|
|
2768
|
+
failure: null
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
/** What the bar and the start screen row say, brought up to the messages on screen. */
|
|
2773
|
+
function summarized({ open, hasPast }, conversation) {
|
|
2774
|
+
if (conversation.closed) return {
|
|
2775
|
+
open: null,
|
|
2776
|
+
hasPast: true
|
|
2777
|
+
};
|
|
2778
|
+
const last = conversation.messages.at(-1);
|
|
2779
|
+
if (!open || !last) return {
|
|
2780
|
+
open,
|
|
2781
|
+
hasPast
|
|
2782
|
+
};
|
|
2783
|
+
return {
|
|
2784
|
+
open: {
|
|
2785
|
+
...open,
|
|
2786
|
+
status: conversation.status,
|
|
2787
|
+
lastMessageAt: last.sentAt,
|
|
2788
|
+
lastMessageBy: last.by,
|
|
2789
|
+
lastMessageText: last.text
|
|
2790
|
+
},
|
|
2791
|
+
hasPast
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
/** Talk to a person is offered: the project's switch is on and the runtime has not said off. */
|
|
2795
|
+
function offersTalk(switchOn, hasTransport, support) {
|
|
2796
|
+
return switchOn && hasTransport && !support.off;
|
|
2797
|
+
}
|
|
2798
|
+
/** A full view replaces what is held; a view fetched with `after` adds its messages. */
|
|
2799
|
+
function merged(current, next) {
|
|
2800
|
+
if (current === null || current.id !== next.id) return next;
|
|
2801
|
+
return {
|
|
2802
|
+
...next,
|
|
2803
|
+
messages: withMessages(current.messages, next.messages)
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
function withMessages(current, next) {
|
|
2807
|
+
const seen = new Set(current.map((message) => message.id));
|
|
2808
|
+
return [...current, ...next.filter((message) => !seen.has(message.id))];
|
|
2809
|
+
}
|
|
2810
|
+
/** The conversation on screen can take a message: not one opened from the past list. */
|
|
2811
|
+
function canWrite(support) {
|
|
2812
|
+
return support.view === "conversation" && !support.readOnly;
|
|
2813
|
+
}
|
|
2814
|
+
//#endregion
|
|
2402
2815
|
//#region src/hooks.ts
|
|
2403
2816
|
function useGuidesApi() {
|
|
2404
2817
|
const api = useContext(GuidesContext);
|
|
@@ -2518,7 +2931,7 @@ function useLauncher({ remember = true } = {}) {
|
|
|
2518
2931
|
const asked = {
|
|
2519
2932
|
question,
|
|
2520
2933
|
pathname,
|
|
2521
|
-
|
|
2934
|
+
threadId: threadRef.current.threadId
|
|
2522
2935
|
};
|
|
2523
2936
|
run(transportAsk, asked, questionId, controller.signal, dispatch);
|
|
2524
2937
|
};
|
|
@@ -2528,6 +2941,7 @@ function useLauncher({ remember = true } = {}) {
|
|
|
2528
2941
|
ready: api.ready,
|
|
2529
2942
|
tourRunning: api.activeTour !== null,
|
|
2530
2943
|
messages: thread.messages,
|
|
2944
|
+
threadId: thread.threadId,
|
|
2531
2945
|
waiting: isWaiting(thread),
|
|
2532
2946
|
rateLimitedUntil: cappedUntil,
|
|
2533
2947
|
handedOff: thread.handoff !== null,
|
|
@@ -2600,6 +3014,157 @@ function useLauncher({ remember = true } = {}) {
|
|
|
2600
3014
|
}
|
|
2601
3015
|
};
|
|
2602
3016
|
}
|
|
3017
|
+
/** How often the end user's open conversation is checked, and the conversation on screen. */
|
|
3018
|
+
const SUPPORT_POLL_MS = 3e4;
|
|
3019
|
+
const CONVERSATION_POLL_MS = 5e3;
|
|
3020
|
+
function isOff(value) {
|
|
3021
|
+
return typeof value === "object" && value !== null && "off" in value;
|
|
3022
|
+
}
|
|
3023
|
+
function useTabVisible() {
|
|
3024
|
+
const [visible, setVisible] = useState(() => typeof document === "undefined" || document.visibilityState === "visible");
|
|
3025
|
+
useEffect(() => {
|
|
3026
|
+
const onChange = () => setVisible(document.visibilityState === "visible");
|
|
3027
|
+
document.addEventListener("visibilitychange", onChange);
|
|
3028
|
+
return () => document.removeEventListener("visibilitychange", onChange);
|
|
3029
|
+
}, []);
|
|
3030
|
+
return visible;
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Talk to a person. Polls the runtime only while it can matter: once on load,
|
|
3034
|
+
* then every 30 seconds while the end user has an open conversation, and the
|
|
3035
|
+
* conversation itself every 5 seconds while it is on screen. Nothing polls
|
|
3036
|
+
* while the tab is hidden.
|
|
3037
|
+
*/
|
|
3038
|
+
function useSupport({ onScreen, threadId }) {
|
|
3039
|
+
const api = useGuidesApi();
|
|
3040
|
+
const switchOn = useComponentSettings().launcher.talkToPerson;
|
|
3041
|
+
const transport = api.support;
|
|
3042
|
+
const [state, dispatch] = useReducer(supportReducer, emptySupport);
|
|
3043
|
+
const stateRef = useRef(state);
|
|
3044
|
+
stateRef.current = state;
|
|
3045
|
+
const visible = useTabVisible();
|
|
3046
|
+
const available = offersTalk(switchOn, transport !== void 0, state);
|
|
3047
|
+
const live = available ? transport : void 0;
|
|
3048
|
+
const hasOpen = state.open !== null;
|
|
3049
|
+
const poll = useCallback(() => {
|
|
3050
|
+
if (!live) return;
|
|
3051
|
+
live.conversationPoll().then((answer) => dispatch(isOff(answer) ? { type: "off" } : {
|
|
3052
|
+
type: "polled",
|
|
3053
|
+
poll: answer
|
|
3054
|
+
}), () => void 0);
|
|
3055
|
+
}, [live]);
|
|
3056
|
+
useEffect(() => {
|
|
3057
|
+
poll();
|
|
3058
|
+
}, [poll]);
|
|
3059
|
+
const wasVisible = useRef(visible);
|
|
3060
|
+
useEffect(() => {
|
|
3061
|
+
const returned = visible && !wasVisible.current;
|
|
3062
|
+
wasVisible.current = visible;
|
|
3063
|
+
if (!hasOpen || !visible) return;
|
|
3064
|
+
if (returned) poll();
|
|
3065
|
+
const timer = setInterval(poll, SUPPORT_POLL_MS);
|
|
3066
|
+
return () => clearInterval(timer);
|
|
3067
|
+
}, [
|
|
3068
|
+
poll,
|
|
3069
|
+
hasOpen,
|
|
3070
|
+
visible
|
|
3071
|
+
]);
|
|
3072
|
+
const watchedId = onScreen && visible && canWrite(state) ? state.conversation?.id ?? state.open?.id : void 0;
|
|
3073
|
+
useEffect(() => {
|
|
3074
|
+
if (!live || watchedId === void 0) return;
|
|
3075
|
+
const fetchView = () => {
|
|
3076
|
+
const held = stateRef.current.conversation;
|
|
3077
|
+
const after = held?.id === watchedId ? held.messages.at(-1)?.id : void 0;
|
|
3078
|
+
live.conversation(watchedId, after).then((view) => {
|
|
3079
|
+
if (view === null) return;
|
|
3080
|
+
if (isOff(view)) {
|
|
3081
|
+
dispatch({ type: "off" });
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
dispatch({
|
|
3085
|
+
type: "loaded",
|
|
3086
|
+
conversation: view
|
|
3087
|
+
});
|
|
3088
|
+
if (after !== void 0 && view.messages.some((message) => message.by === "TEAM")) live.markRead(watchedId).catch(() => void 0);
|
|
3089
|
+
}, () => void 0);
|
|
3090
|
+
};
|
|
3091
|
+
fetchView();
|
|
3092
|
+
const timer = setInterval(fetchView, CONVERSATION_POLL_MS);
|
|
3093
|
+
return () => clearInterval(timer);
|
|
3094
|
+
}, [live, watchedId]);
|
|
3095
|
+
const unreadOnScreen = state.open?.unread === true && state.open.id === watchedId;
|
|
3096
|
+
useEffect(() => {
|
|
3097
|
+
if (!live || !unreadOnScreen || watchedId === void 0) return;
|
|
3098
|
+
dispatch({
|
|
3099
|
+
type: "read",
|
|
3100
|
+
id: watchedId
|
|
3101
|
+
});
|
|
3102
|
+
live.markRead(watchedId).catch(() => void 0);
|
|
3103
|
+
}, [
|
|
3104
|
+
live,
|
|
3105
|
+
unreadOnScreen,
|
|
3106
|
+
watchedId
|
|
3107
|
+
]);
|
|
3108
|
+
return {
|
|
3109
|
+
available,
|
|
3110
|
+
state,
|
|
3111
|
+
talk: () => dispatch({ type: "talk" }),
|
|
3112
|
+
send: async (typed) => {
|
|
3113
|
+
const text = typed.trim();
|
|
3114
|
+
const current = stateRef.current;
|
|
3115
|
+
if (!live || text === "" || current.sending || !canWrite(current)) return false;
|
|
3116
|
+
dispatch({ type: "sending" });
|
|
3117
|
+
stateRef.current = {
|
|
3118
|
+
...current,
|
|
3119
|
+
sending: true
|
|
3120
|
+
};
|
|
3121
|
+
let answer;
|
|
3122
|
+
try {
|
|
3123
|
+
answer = current.conversation ? await live.sendMessage(current.conversation.id, text) : await live.startConversation({
|
|
3124
|
+
threadId: threadId ?? null,
|
|
3125
|
+
path: window.location.pathname,
|
|
3126
|
+
text
|
|
3127
|
+
});
|
|
3128
|
+
} catch {
|
|
3129
|
+
answer = { refused: "lost" };
|
|
3130
|
+
}
|
|
3131
|
+
if (isOff(answer)) {
|
|
3132
|
+
dispatch({ type: "off" });
|
|
3133
|
+
return false;
|
|
3134
|
+
}
|
|
3135
|
+
if ("refused" in answer) {
|
|
3136
|
+
const refused = answer.refused === "another-open" ? { refused: "lost" } : answer;
|
|
3137
|
+
dispatch({
|
|
3138
|
+
type: "refused",
|
|
3139
|
+
refused
|
|
3140
|
+
});
|
|
3141
|
+
return false;
|
|
3142
|
+
}
|
|
3143
|
+
dispatch({
|
|
3144
|
+
type: "sent",
|
|
3145
|
+
sent: answer
|
|
3146
|
+
});
|
|
3147
|
+
return true;
|
|
3148
|
+
},
|
|
3149
|
+
showPast: () => {
|
|
3150
|
+
dispatch({ type: "showPast" });
|
|
3151
|
+
live?.pastConversations().then((past) => dispatch(isOff(past) ? { type: "off" } : {
|
|
3152
|
+
type: "pastLoaded",
|
|
3153
|
+
past
|
|
3154
|
+
}), () => void 0);
|
|
3155
|
+
},
|
|
3156
|
+
openPast: (id) => {
|
|
3157
|
+
live?.conversation(id).then((view) => {
|
|
3158
|
+
if (view === null) return;
|
|
3159
|
+
dispatch(isOff(view) ? { type: "off" } : {
|
|
3160
|
+
type: "openPast",
|
|
3161
|
+
conversation: view
|
|
3162
|
+
});
|
|
3163
|
+
}, () => void 0);
|
|
3164
|
+
},
|
|
3165
|
+
back: () => dispatch({ type: "back" })
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
2603
3168
|
/** One guide's content plus launchTour; call with no key for launch-only. */
|
|
2604
3169
|
function useGuide(key) {
|
|
2605
3170
|
const api = useGuidesApi();
|
|
@@ -2720,12 +3285,6 @@ function PlayIcon() {
|
|
|
2720
3285
|
children: /* @__PURE__ */ jsx("path", { d: "M7 4.5v15l12-7.5z" })
|
|
2721
3286
|
});
|
|
2722
3287
|
}
|
|
2723
|
-
function DocIcon() {
|
|
2724
|
-
return /* @__PURE__ */ jsxs(Glyph, {
|
|
2725
|
-
width: 1.8,
|
|
2726
|
-
children: [/* @__PURE__ */ jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ jsx("path", { d: "M14 3v5h5M9 13h6M9 17h4" })]
|
|
2727
|
-
});
|
|
2728
|
-
}
|
|
2729
3288
|
function ExternalIcon() {
|
|
2730
3289
|
return /* @__PURE__ */ jsx(Glyph, {
|
|
2731
3290
|
width: 1.8,
|
|
@@ -2763,12 +3322,6 @@ function RestartIcon() {
|
|
|
2763
3322
|
function SendIcon() {
|
|
2764
3323
|
return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M12 19V5M5 12l7-7 7 7" }) });
|
|
2765
3324
|
}
|
|
2766
|
-
function ChevronDownIcon() {
|
|
2767
|
-
return /* @__PURE__ */ jsx(Glyph, {
|
|
2768
|
-
width: 1.8,
|
|
2769
|
-
children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
|
|
2770
|
-
});
|
|
2771
|
-
}
|
|
2772
3325
|
function CheckIcon() {
|
|
2773
3326
|
return /* @__PURE__ */ jsx(Glyph, {
|
|
2774
3327
|
width: 1.8,
|
|
@@ -2800,6 +3353,28 @@ function StarIcon() {
|
|
|
2800
3353
|
children: /* @__PURE__ */ jsx("path", { d: "M11.53 2.3a.5.5 0 0 1 .94 0l2.31 4.68a2 2 0 0 0 1.5 1.1l5.17.75a.5.5 0 0 1 .28.86l-3.74 3.64a2 2 0 0 0-.58 1.77l.88 5.14a.5.5 0 0 1-.73.53l-4.62-2.43a2 2 0 0 0-1.86 0l-4.62 2.43a.5.5 0 0 1-.73-.53l.88-5.14a2 2 0 0 0-.57-1.77L2.3 9.69a.5.5 0 0 1 .28-.86l5.16-.75a2 2 0 0 0 1.52-1.1z" })
|
|
2801
3354
|
});
|
|
2802
3355
|
}
|
|
3356
|
+
function ChevronLeftIcon() {
|
|
3357
|
+
return /* @__PURE__ */ jsx(Glyph, {
|
|
3358
|
+
width: 1.8,
|
|
3359
|
+
children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" })
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
function HeadsetIcon() {
|
|
3363
|
+
return /* @__PURE__ */ jsxs(Glyph, {
|
|
3364
|
+
width: 1.8,
|
|
3365
|
+
children: [/* @__PURE__ */ jsx("path", { d: "M3 11h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-5Zm0 0a9 9 0 1 1 18 0m0 0v5a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3Z" }), /* @__PURE__ */ jsx("path", { d: "M21 16v2a4 4 0 0 1-4 4h-5" })]
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
function HistoryIcon() {
|
|
3369
|
+
return /* @__PURE__ */ jsxs(Glyph, {
|
|
3370
|
+
width: 1.8,
|
|
3371
|
+
children: [
|
|
3372
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
3373
|
+
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" }),
|
|
3374
|
+
/* @__PURE__ */ jsx("path", { d: "M12 7v5l4 2" })
|
|
3375
|
+
]
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
2803
3378
|
//#endregion
|
|
2804
3379
|
//#region src/overlay/CardMedia.tsx
|
|
2805
3380
|
const VIDEO_URL = /\.(mp4|webm|m4v|mov|ogg)([?#]|$)/i;
|
|
@@ -3887,10 +4462,387 @@ function SunboardLogo({ width = 75 }) {
|
|
|
3887
4462
|
}) })]
|
|
3888
4463
|
});
|
|
3889
4464
|
}
|
|
4465
|
+
const FOCUSABLE = "a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled])";
|
|
4466
|
+
/**
|
|
4467
|
+
* On a phone the panel is a bottom sheet, and focusing the box raises the
|
|
4468
|
+
* soft keyboard over what the visitor came to read. The panel is fixed, so
|
|
4469
|
+
* focus never needs to scroll, and in an iframe it would scroll the parent.
|
|
4470
|
+
*/
|
|
4471
|
+
function useOpeningFocus(inline, focusBox, boxRef, panelRef) {
|
|
4472
|
+
useEffect(() => {
|
|
4473
|
+
if (inline) return;
|
|
4474
|
+
const phone = window.matchMedia?.(`(max-width: 640px)`).matches ?? false;
|
|
4475
|
+
const box = boxRef.current;
|
|
4476
|
+
if ((focusBox || !phone) && box && !box.disabled) box.focus({ preventScroll: true });
|
|
4477
|
+
else panelRef.current?.focus({ preventScroll: true });
|
|
4478
|
+
}, [inline]);
|
|
4479
|
+
}
|
|
4480
|
+
/**
|
|
4481
|
+
* Bound to the document rather than the panel: disabling the box the visitor
|
|
4482
|
+
* was typing in hands focus back to the page, and a listener on the panel
|
|
4483
|
+
* would never hear the Tab that follows.
|
|
4484
|
+
*/
|
|
4485
|
+
function useTabTrap(inline, panelRef) {
|
|
4486
|
+
useEffect(() => {
|
|
4487
|
+
if (inline) return;
|
|
4488
|
+
const onKeyDown = (event) => {
|
|
4489
|
+
const panel = panelRef.current;
|
|
4490
|
+
if (event.key !== "Tab" || event.defaultPrevented || !panel) return;
|
|
4491
|
+
const stops = [...panel.querySelectorAll(FOCUSABLE)];
|
|
4492
|
+
if (stops.length === 0) return;
|
|
4493
|
+
const first = stops[0];
|
|
4494
|
+
const last = stops[stops.length - 1];
|
|
4495
|
+
const inside = panel.contains(document.activeElement);
|
|
4496
|
+
const leaving = document.activeElement === (event.shiftKey ? first : last);
|
|
4497
|
+
if (inside && !leaving) return;
|
|
4498
|
+
event.preventDefault();
|
|
4499
|
+
(event.shiftKey ? last : first).focus();
|
|
4500
|
+
};
|
|
4501
|
+
document.addEventListener("keydown", onKeyDown);
|
|
4502
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
4503
|
+
}, [inline, panelRef]);
|
|
4504
|
+
}
|
|
4505
|
+
function clockTime(iso) {
|
|
4506
|
+
const at = new Date(iso);
|
|
4507
|
+
if (Number.isNaN(at.getTime())) return iso;
|
|
4508
|
+
return at.toLocaleTimeString(void 0, {
|
|
4509
|
+
hour: "2-digit",
|
|
4510
|
+
minute: "2-digit"
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
/** Whole calendar days from `iso` to `now`, in the visitor's own time zone. */
|
|
4514
|
+
function daysAgo(iso, now) {
|
|
4515
|
+
const at = new Date(iso);
|
|
4516
|
+
if (Number.isNaN(at.getTime())) return 0;
|
|
4517
|
+
const midnight = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
4518
|
+
return Math.max(0, Math.round((midnight(now) - midnight(at)) / 864e5));
|
|
4519
|
+
}
|
|
4520
|
+
function minutesUntil(iso) {
|
|
4521
|
+
const at = new Date(iso).getTime();
|
|
4522
|
+
if (Number.isNaN(at)) return 1;
|
|
4523
|
+
return Math.max(1, Math.round((at - Date.now()) / 6e4));
|
|
4524
|
+
}
|
|
4525
|
+
//#endregion
|
|
4526
|
+
//#region src/components/Conversation.tsx
|
|
4527
|
+
/** "Sean replied" or "Waiting for a reply", for the bar and the start screen row. */
|
|
4528
|
+
function supportStatus(open, company, labels) {
|
|
4529
|
+
if (open.lastMessageBy !== "TEAM") return labels.waiting;
|
|
4530
|
+
return labels.replied(open.lastMessageWho ?? labels.from(company));
|
|
4531
|
+
}
|
|
4532
|
+
function when(iso, labels, now) {
|
|
4533
|
+
const days = daysAgo(iso, now);
|
|
4534
|
+
return days === 0 ? clockTime(iso) : labels.ago(days);
|
|
4535
|
+
}
|
|
4536
|
+
function PanelHead({ title, onBack, close }) {
|
|
4537
|
+
const labels = useLabels();
|
|
4538
|
+
return /* @__PURE__ */ jsxs("header", {
|
|
4539
|
+
className: "sb-head sb-head-back",
|
|
4540
|
+
children: [
|
|
4541
|
+
/* @__PURE__ */ jsx("button", {
|
|
4542
|
+
type: "button",
|
|
4543
|
+
className: "sb-panel-close",
|
|
4544
|
+
"aria-label": labels.support.back,
|
|
4545
|
+
onClick: onBack,
|
|
4546
|
+
children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
|
|
4547
|
+
}),
|
|
4548
|
+
/* @__PURE__ */ jsx("h2", {
|
|
4549
|
+
className: "sb-title",
|
|
4550
|
+
children: title
|
|
4551
|
+
}),
|
|
4552
|
+
/* @__PURE__ */ jsx("button", {
|
|
4553
|
+
type: "button",
|
|
4554
|
+
className: "sb-panel-close",
|
|
4555
|
+
"aria-label": labels.launcher.closeAria,
|
|
4556
|
+
onClick: close,
|
|
4557
|
+
children: /* @__PURE__ */ jsx(XIcon, {})
|
|
4558
|
+
})
|
|
4559
|
+
]
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4562
|
+
function Branding() {
|
|
4563
|
+
const L = useLabels().launcher;
|
|
4564
|
+
if (!useComponentSettings().launcher.showBranding) return null;
|
|
4565
|
+
return /* @__PURE__ */ jsxs("a", {
|
|
4566
|
+
className: "sb-brand",
|
|
4567
|
+
href: "https://sunboardhq.com",
|
|
4568
|
+
target: "_blank",
|
|
4569
|
+
rel: "noreferrer",
|
|
4570
|
+
children: [L.poweredBy, /* @__PURE__ */ jsx(SunboardLogo, { width: 62 })]
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
/** The messages and the questions asked of the assistant meanwhile, in the order they happened. */
|
|
4574
|
+
function items(conversation) {
|
|
4575
|
+
return [...conversation.messages.map((message) => ({
|
|
4576
|
+
kind: "message",
|
|
4577
|
+
at: message.sentAt,
|
|
4578
|
+
message
|
|
4579
|
+
})), ...conversation.during.map((question) => ({
|
|
4580
|
+
kind: "fold",
|
|
4581
|
+
at: question.askedAt,
|
|
4582
|
+
question
|
|
4583
|
+
}))].sort((a, b) => a.at.localeCompare(b.at));
|
|
4584
|
+
}
|
|
4585
|
+
/** The assistant's turns the conversation follows on from, dimmed. */
|
|
4586
|
+
function AssistantTurns({ messages }) {
|
|
4587
|
+
const replies = new Map(messages.flatMap((message) => message.kind === "reply" ? [[message.questionId, message.reply.answer]] : []));
|
|
4588
|
+
return /* @__PURE__ */ jsx("div", {
|
|
4589
|
+
className: "sb-dim",
|
|
4590
|
+
children: messages.map((message) => {
|
|
4591
|
+
if (message.kind !== "question") return null;
|
|
4592
|
+
const answer = replies.get(message.id);
|
|
4593
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4594
|
+
className: "sb-exchange",
|
|
4595
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
4596
|
+
className: "sb-asked",
|
|
4597
|
+
children: /* @__PURE__ */ jsx("p", { children: message.text })
|
|
4598
|
+
}), answer && /* @__PURE__ */ jsx("div", {
|
|
4599
|
+
className: "sb-reply-text",
|
|
4600
|
+
children: /* @__PURE__ */ jsx(AnswerText, { text: answer })
|
|
4601
|
+
})]
|
|
4602
|
+
}, message.id);
|
|
4603
|
+
})
|
|
4604
|
+
});
|
|
4605
|
+
}
|
|
4606
|
+
function Fold({ question, labels }) {
|
|
4607
|
+
const [open, setOpen] = useState(false);
|
|
4608
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
4609
|
+
type: "button",
|
|
4610
|
+
className: `sb-fold${open ? " sb-fold-open" : ""}`,
|
|
4611
|
+
"aria-expanded": open,
|
|
4612
|
+
onClick: () => setOpen((now) => !now),
|
|
4613
|
+
children: [/* @__PURE__ */ jsx(ChevronRightIcon, {}), /* @__PURE__ */ jsx("span", {
|
|
4614
|
+
className: "sb-fold-text",
|
|
4615
|
+
children: labels.asked(question.text)
|
|
4616
|
+
})]
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
function Line({ children }) {
|
|
4620
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4621
|
+
className: "sb-line",
|
|
4622
|
+
role: "note",
|
|
4623
|
+
children: [
|
|
4624
|
+
/* @__PURE__ */ jsx("span", { className: "sb-line-rule" }),
|
|
4625
|
+
/* @__PURE__ */ jsx("span", {
|
|
4626
|
+
className: "sb-line-text",
|
|
4627
|
+
children
|
|
4628
|
+
}),
|
|
4629
|
+
/* @__PURE__ */ jsx("span", { className: "sb-line-rule" })
|
|
4630
|
+
]
|
|
4631
|
+
});
|
|
4632
|
+
}
|
|
4633
|
+
function TeamMessage({ who, text }) {
|
|
4634
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4635
|
+
className: "sb-team",
|
|
4636
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
4637
|
+
className: "sb-team-who",
|
|
4638
|
+
children: who
|
|
4639
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
4640
|
+
className: "sb-team-text",
|
|
4641
|
+
children: text
|
|
4642
|
+
})]
|
|
4643
|
+
});
|
|
4644
|
+
}
|
|
4645
|
+
function SendFailure({ failure, labels }) {
|
|
4646
|
+
const capped = failure.refused === "too-many";
|
|
4647
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
4648
|
+
className: "sb-note sb-arrive",
|
|
4649
|
+
role: capped ? "status" : "alert",
|
|
4650
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
4651
|
+
className: "sb-note-mark",
|
|
4652
|
+
children: capped ? /* @__PURE__ */ jsx(ClockIcon, {}) : /* @__PURE__ */ jsx(NoSignalIcon, {})
|
|
4653
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
4654
|
+
className: "sb-note-text",
|
|
4655
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
4656
|
+
className: "sb-note-words",
|
|
4657
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
4658
|
+
className: "sb-note-title",
|
|
4659
|
+
children: capped ? labels.tooMany : labels.lost
|
|
4660
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
4661
|
+
className: "sb-note-body",
|
|
4662
|
+
children: capped && failure.retryAt ? labels.writeAgainIn(minutesUntil(failure.retryAt)) : labels.lostBody
|
|
4663
|
+
})]
|
|
4664
|
+
})
|
|
4665
|
+
})]
|
|
4666
|
+
});
|
|
4667
|
+
}
|
|
4668
|
+
function ConversationPanel({ launcher, support, close, focusBox }) {
|
|
4669
|
+
const api = useGuidesApi();
|
|
4670
|
+
const { support: S, launcher: L } = useLabels();
|
|
4671
|
+
const replyTime = useComponentSettings().launcher.replyTime.trim();
|
|
4672
|
+
const company = api.productName;
|
|
4673
|
+
const { conversation, readOnly, sending, failure } = support.state;
|
|
4674
|
+
const panelRef = useRef(null);
|
|
4675
|
+
const boxRef = useRef(null);
|
|
4676
|
+
const [draft, setDraft] = useState("");
|
|
4677
|
+
const now = /* @__PURE__ */ new Date();
|
|
4678
|
+
useOpeningFocus(false, focusBox, boxRef, panelRef);
|
|
4679
|
+
useTabTrap(false, panelRef);
|
|
4680
|
+
useLayoutEffect(() => {
|
|
4681
|
+
const box = boxRef.current;
|
|
4682
|
+
if (!box) return;
|
|
4683
|
+
box.style.height = "";
|
|
4684
|
+
box.style.height = `${String(box.scrollHeight)}px`;
|
|
4685
|
+
box.style.overflowY = box.scrollHeight > box.clientHeight + 1 ? "auto" : "hidden";
|
|
4686
|
+
}, [draft]);
|
|
4687
|
+
const send = async (event) => {
|
|
4688
|
+
event?.preventDefault();
|
|
4689
|
+
if (await support.send(draft)) setDraft("");
|
|
4690
|
+
};
|
|
4691
|
+
const onBoxKeyDown = (event) => {
|
|
4692
|
+
if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return;
|
|
4693
|
+
event.preventDefault();
|
|
4694
|
+
send();
|
|
4695
|
+
};
|
|
4696
|
+
const questions = launcher.messages.filter((message) => message.kind === "question" || message.kind === "reply");
|
|
4697
|
+
const showTurns = !readOnly && questions.length > 0 && (conversation === null || conversation.before.length > 0);
|
|
4698
|
+
const firstFromEndUser = conversation?.messages.find((message) => message.by === "END_USER")?.id;
|
|
4699
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
4700
|
+
className: "sb-panel",
|
|
4701
|
+
ref: panelRef,
|
|
4702
|
+
role: "dialog",
|
|
4703
|
+
"aria-label": S.title(company),
|
|
4704
|
+
tabIndex: -1,
|
|
4705
|
+
children: [
|
|
4706
|
+
/* @__PURE__ */ jsx(PanelHead, {
|
|
4707
|
+
title: S.title(company),
|
|
4708
|
+
onBack: support.back,
|
|
4709
|
+
close
|
|
4710
|
+
}),
|
|
4711
|
+
/* @__PURE__ */ jsx("div", {
|
|
4712
|
+
className: "sb-thread",
|
|
4713
|
+
role: "log",
|
|
4714
|
+
"aria-label": S.conversationWith(company),
|
|
4715
|
+
"aria-live": "polite",
|
|
4716
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
4717
|
+
className: "sb-thread-scroll sb-anchored",
|
|
4718
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
4719
|
+
className: "sb-thread-list sb-conversation",
|
|
4720
|
+
children: [
|
|
4721
|
+
showTurns && /* @__PURE__ */ jsx(AssistantTurns, { messages: questions }),
|
|
4722
|
+
!showTurns && conversation?.before.map((question) => /* @__PURE__ */ jsx(Fold, {
|
|
4723
|
+
question,
|
|
4724
|
+
labels: S
|
|
4725
|
+
}, question.id)),
|
|
4726
|
+
/* @__PURE__ */ jsx(Line, { children: S.talking(company) }),
|
|
4727
|
+
/* @__PURE__ */ jsx(TeamMessage, {
|
|
4728
|
+
who: S.from(company),
|
|
4729
|
+
text: S.greeting(api.firstName)
|
|
4730
|
+
}),
|
|
4731
|
+
conversation && items(conversation).map((item) => item.kind === "fold" ? /* @__PURE__ */ jsx(Fold, {
|
|
4732
|
+
question: item.question,
|
|
4733
|
+
labels: S
|
|
4734
|
+
}, item.question.id) : item.message.by === "TEAM" ? /* @__PURE__ */ jsx(TeamMessage, {
|
|
4735
|
+
who: S.teamMessage(item.message.who, company, when(item.message.sentAt, S, now)),
|
|
4736
|
+
text: item.message.text
|
|
4737
|
+
}, item.message.id) : /* @__PURE__ */ jsxs("div", {
|
|
4738
|
+
className: "sb-asked sb-sent",
|
|
4739
|
+
children: [/* @__PURE__ */ jsx("p", { children: item.message.text }), item.message.id === firstFromEndUser && replyTime !== "" && /* @__PURE__ */ jsx("span", {
|
|
4740
|
+
className: "sb-reply-time",
|
|
4741
|
+
children: replyTime
|
|
4742
|
+
})]
|
|
4743
|
+
}, item.message.id)),
|
|
4744
|
+
conversation?.closed && /* @__PURE__ */ jsxs("div", {
|
|
4745
|
+
className: "sb-closed",
|
|
4746
|
+
children: [/* @__PURE__ */ jsx(Line, { children: S.closed(company) }), !readOnly && /* @__PURE__ */ jsx("p", {
|
|
4747
|
+
className: "sb-reopen",
|
|
4748
|
+
children: S.reopen
|
|
4749
|
+
})]
|
|
4750
|
+
}),
|
|
4751
|
+
failure && /* @__PURE__ */ jsx(SendFailure, {
|
|
4752
|
+
failure,
|
|
4753
|
+
labels: S
|
|
4754
|
+
})
|
|
4755
|
+
]
|
|
4756
|
+
})
|
|
4757
|
+
})
|
|
4758
|
+
}),
|
|
4759
|
+
readOnly ? /* @__PURE__ */ jsx("div", {
|
|
4760
|
+
className: "sb-foot",
|
|
4761
|
+
children: /* @__PURE__ */ jsx(Branding, {})
|
|
4762
|
+
}) : /* @__PURE__ */ jsxs("form", {
|
|
4763
|
+
className: "sb-composer",
|
|
4764
|
+
onSubmit: (event) => void send(event),
|
|
4765
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
4766
|
+
className: "sb-field",
|
|
4767
|
+
children: [
|
|
4768
|
+
/* @__PURE__ */ jsx("span", {
|
|
4769
|
+
className: "sb-sr",
|
|
4770
|
+
children: S.boxLabel(company)
|
|
4771
|
+
}),
|
|
4772
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
4773
|
+
className: "sb-box",
|
|
4774
|
+
ref: boxRef,
|
|
4775
|
+
rows: 1,
|
|
4776
|
+
value: draft,
|
|
4777
|
+
onChange: (event) => setDraft(event.target.value),
|
|
4778
|
+
onKeyDown: onBoxKeyDown,
|
|
4779
|
+
placeholder: S.placeholder(company),
|
|
4780
|
+
maxLength: CONVERSATION_MESSAGE_MAX_LENGTH,
|
|
4781
|
+
autoComplete: "off"
|
|
4782
|
+
}),
|
|
4783
|
+
/* @__PURE__ */ jsx("button", {
|
|
4784
|
+
type: "submit",
|
|
4785
|
+
className: "sb-send",
|
|
4786
|
+
"aria-label": L.send,
|
|
4787
|
+
disabled: sending || draft.trim() === "",
|
|
4788
|
+
children: /* @__PURE__ */ jsx(SendIcon, {})
|
|
4789
|
+
})
|
|
4790
|
+
]
|
|
4791
|
+
}), /* @__PURE__ */ jsx(Branding, {})]
|
|
4792
|
+
})
|
|
4793
|
+
]
|
|
4794
|
+
});
|
|
4795
|
+
}
|
|
4796
|
+
function PastPanel({ support, close }) {
|
|
4797
|
+
const S = useLabels().support;
|
|
4798
|
+
const panelRef = useRef(null);
|
|
4799
|
+
const now = /* @__PURE__ */ new Date();
|
|
4800
|
+
useTabTrap(false, panelRef);
|
|
4801
|
+
useLayoutEffect(() => {
|
|
4802
|
+
panelRef.current?.focus({ preventScroll: true });
|
|
4803
|
+
}, []);
|
|
4804
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
4805
|
+
className: "sb-panel",
|
|
4806
|
+
ref: panelRef,
|
|
4807
|
+
role: "dialog",
|
|
4808
|
+
"aria-label": S.past,
|
|
4809
|
+
tabIndex: -1,
|
|
4810
|
+
children: [
|
|
4811
|
+
/* @__PURE__ */ jsx(PanelHead, {
|
|
4812
|
+
title: S.past,
|
|
4813
|
+
onBack: support.back,
|
|
4814
|
+
close
|
|
4815
|
+
}),
|
|
4816
|
+
/* @__PURE__ */ jsx("div", {
|
|
4817
|
+
className: "sb-thread",
|
|
4818
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
4819
|
+
className: "sb-thread-scroll",
|
|
4820
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
4821
|
+
className: "sb-past-list",
|
|
4822
|
+
children: (support.state.past ?? []).map((past) => /* @__PURE__ */ jsxs("button", {
|
|
4823
|
+
type: "button",
|
|
4824
|
+
className: "sb-past",
|
|
4825
|
+
onClick: () => support.openPast(past.id),
|
|
4826
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
4827
|
+
className: "sb-past-title",
|
|
4828
|
+
children: past.title
|
|
4829
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
4830
|
+
className: "sb-past-meta",
|
|
4831
|
+
children: S.closedWhen(S.ago(daysAgo(past.closedAt ?? now.toISOString(), now)), past.closedBy)
|
|
4832
|
+
})]
|
|
4833
|
+
}, past.id))
|
|
4834
|
+
})
|
|
4835
|
+
})
|
|
4836
|
+
}),
|
|
4837
|
+
/* @__PURE__ */ jsx("div", {
|
|
4838
|
+
className: "sb-foot",
|
|
4839
|
+
children: /* @__PURE__ */ jsx(Branding, {})
|
|
4840
|
+
})
|
|
4841
|
+
]
|
|
4842
|
+
});
|
|
4843
|
+
}
|
|
3890
4844
|
//#endregion
|
|
3891
4845
|
//#region src/components/Launcher.tsx
|
|
3892
|
-
/** The width below which the panel is a bottom sheet; see styles.css. */
|
|
3893
|
-
const PHONE_WIDTH = 640;
|
|
3894
4846
|
/** How long the stage labels stay before the wait owns up to being slow. */
|
|
3895
4847
|
const SLOW_MS = 6e3;
|
|
3896
4848
|
const STAGE_AT = {
|
|
@@ -3898,14 +4850,19 @@ const STAGE_AT = {
|
|
|
3898
4850
|
writing: 1,
|
|
3899
4851
|
checking: 2
|
|
3900
4852
|
};
|
|
3901
|
-
const FOCUSABLE = "a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled])";
|
|
3902
4853
|
function Launcher({ inline } = {}) {
|
|
3903
4854
|
const launcher = useLauncher({ remember: !inline });
|
|
3904
4855
|
const { ready, onThisPage, elsewhere, tourRunning, opened, handedOff } = launcher;
|
|
3905
4856
|
const [open, setOpen] = useState(false);
|
|
4857
|
+
const support = useSupport({
|
|
4858
|
+
onScreen: open !== false,
|
|
4859
|
+
threadId: launcher.threadId
|
|
4860
|
+
});
|
|
3906
4861
|
const { off } = useGuidesApi();
|
|
3907
4862
|
const canAsk = launcher.ask !== void 0;
|
|
3908
4863
|
const L = useLabels().launcher;
|
|
4864
|
+
const S = useLabels().support;
|
|
4865
|
+
const unread = support.available && support.state.open?.unread === true;
|
|
3909
4866
|
const pillRef = useRef(null);
|
|
3910
4867
|
const wasOpen = useRef(false);
|
|
3911
4868
|
const wasHandedOff = useRef(handedOff);
|
|
@@ -3932,31 +4889,49 @@ function Launcher({ inline } = {}) {
|
|
|
3932
4889
|
if (inline) return /* @__PURE__ */ jsx(Scope, { children: /* @__PURE__ */ jsx(QuestionPanel, {
|
|
3933
4890
|
inline: true,
|
|
3934
4891
|
launcher,
|
|
4892
|
+
support,
|
|
3935
4893
|
close: () => setOpen(false),
|
|
3936
4894
|
focusBox: false
|
|
3937
4895
|
}) });
|
|
3938
4896
|
const floating = (surface) => /* @__PURE__ */ jsx(FloatingPortal, { children: /* @__PURE__ */ jsx(Scope, { children: surface }) });
|
|
3939
4897
|
if (!ready) return null;
|
|
3940
4898
|
if (tourRunning) return floating(/* @__PURE__ */ jsx(TourPill, {}));
|
|
3941
|
-
if (!canAsk && onThisPage.length + elsewhere.length === 0) return null;
|
|
3942
|
-
if (open)
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
4899
|
+
if (!canAsk && onThisPage.length + elsewhere.length === 0 && !support.state.open) return null;
|
|
4900
|
+
if (open) {
|
|
4901
|
+
const close = () => setOpen(false);
|
|
4902
|
+
const view = support.available ? support.state.view : "assistant";
|
|
4903
|
+
return floating(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
4904
|
+
className: "sb-panel-scrim",
|
|
4905
|
+
onClick: close
|
|
4906
|
+
}), view === "conversation" ? /* @__PURE__ */ jsx(ConversationPanel, {
|
|
4907
|
+
launcher,
|
|
4908
|
+
support,
|
|
4909
|
+
close,
|
|
4910
|
+
focusBox: open === "back"
|
|
4911
|
+
}) : view === "past" ? /* @__PURE__ */ jsx(PastPanel, {
|
|
4912
|
+
support,
|
|
4913
|
+
close
|
|
4914
|
+
}) : /* @__PURE__ */ jsx(QuestionPanel, {
|
|
4915
|
+
launcher,
|
|
4916
|
+
support,
|
|
4917
|
+
close,
|
|
4918
|
+
focusBox: open === "back"
|
|
4919
|
+
})] }));
|
|
4920
|
+
}
|
|
3950
4921
|
return floating(/* @__PURE__ */ jsxs("button", {
|
|
3951
4922
|
type: "button",
|
|
3952
4923
|
className: "sb-launcher",
|
|
3953
4924
|
ref: pillRef,
|
|
3954
|
-
"aria-label": L.openAria,
|
|
4925
|
+
"aria-label": unread ? S.openAriaNewReply : L.openAria,
|
|
3955
4926
|
onClick: () => {
|
|
3956
4927
|
setOpen("opened");
|
|
3957
4928
|
opened();
|
|
3958
4929
|
},
|
|
3959
|
-
children: [
|
|
4930
|
+
children: [
|
|
4931
|
+
/* @__PURE__ */ jsx(HelpIcon, {}),
|
|
4932
|
+
L.open,
|
|
4933
|
+
unread && /* @__PURE__ */ jsx("span", { className: "sb-dot sb-launcher-dot" })
|
|
4934
|
+
]
|
|
3960
4935
|
}));
|
|
3961
4936
|
}
|
|
3962
4937
|
/**
|
|
@@ -3964,10 +4939,14 @@ function Launcher({ inline } = {}) {
|
|
|
3964
4939
|
* tour taking its place, loses nothing; a question still out lands while the
|
|
3965
4940
|
* panel is away and is there when it comes back.
|
|
3966
4941
|
*/
|
|
3967
|
-
function QuestionPanel({ inline, launcher, close, focusBox }) {
|
|
4942
|
+
function QuestionPanel({ inline, launcher, support, close, focusBox }) {
|
|
3968
4943
|
const { onThisPage, elsewhere, messages, waiting, rateLimitedUntil, ask } = launcher;
|
|
3969
4944
|
const api = useGuidesApi();
|
|
3970
4945
|
const L = useLabels().launcher;
|
|
4946
|
+
const S = useLabels().support;
|
|
4947
|
+
const company = api.productName;
|
|
4948
|
+
const talk = support.available && !inline ? support.talk : void 0;
|
|
4949
|
+
const openConversation = support.available ? support.state.open : null;
|
|
3971
4950
|
const showBranding = useComponentSettings().launcher.showBranding;
|
|
3972
4951
|
const panelRef = useRef(null);
|
|
3973
4952
|
const scrollRef = useRef(null);
|
|
@@ -4022,16 +5001,6 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
|
|
|
4022
5001
|
followRef.current = false;
|
|
4023
5002
|
measure();
|
|
4024
5003
|
}, [messages]);
|
|
4025
|
-
const keepInPlace = (change) => {
|
|
4026
|
-
const box = scrollRef.current;
|
|
4027
|
-
const before = box ? {
|
|
4028
|
-
height: box.scrollHeight,
|
|
4029
|
-
top: box.scrollTop
|
|
4030
|
-
} : null;
|
|
4031
|
-
flushSync(change);
|
|
4032
|
-
if (box && before && anchored) box.scrollTop = before.top - (box.scrollHeight - before.height);
|
|
4033
|
-
measure();
|
|
4034
|
-
};
|
|
4035
5004
|
useEffect(() => {
|
|
4036
5005
|
const seen = seenRef.current;
|
|
4037
5006
|
let finished = null;
|
|
@@ -4040,7 +5009,7 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
|
|
|
4040
5009
|
if (message.kind === "waiting") continue;
|
|
4041
5010
|
seen.add(message.id);
|
|
4042
5011
|
if (message.kind === "reply") {
|
|
4043
|
-
const { greeting, closing } = replyWords(message,
|
|
5012
|
+
const { greeting, closing } = replyWords(message, api.firstName, L);
|
|
4044
5013
|
finished = [
|
|
4045
5014
|
greeting,
|
|
4046
5015
|
plainAnswer(message.reply.answer),
|
|
@@ -4050,31 +5019,8 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
|
|
|
4050
5019
|
}
|
|
4051
5020
|
if (finished !== null) setAnnouncement(finished);
|
|
4052
5021
|
}, [messages]);
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
const phone = window.matchMedia?.(`(max-width: ${PHONE_WIDTH}px)`).matches ?? false;
|
|
4056
|
-
const box = boxRef.current;
|
|
4057
|
-
if ((focusBox || !phone) && box && !box.disabled) box.focus({ preventScroll: true });
|
|
4058
|
-
else panelRef.current?.focus({ preventScroll: true });
|
|
4059
|
-
}, [inline]);
|
|
4060
|
-
useEffect(() => {
|
|
4061
|
-
if (inline) return;
|
|
4062
|
-
const onKeyDown = (event) => {
|
|
4063
|
-
const panel = panelRef.current;
|
|
4064
|
-
if (event.key !== "Tab" || event.defaultPrevented || !panel) return;
|
|
4065
|
-
const stops = [...panel.querySelectorAll(FOCUSABLE)];
|
|
4066
|
-
if (stops.length === 0) return;
|
|
4067
|
-
const first = stops[0];
|
|
4068
|
-
const last = stops[stops.length - 1];
|
|
4069
|
-
const inside = panel.contains(document.activeElement);
|
|
4070
|
-
const leaving = document.activeElement === (event.shiftKey ? first : last);
|
|
4071
|
-
if (inside && !leaving) return;
|
|
4072
|
-
event.preventDefault();
|
|
4073
|
-
(event.shiftKey ? last : first).focus();
|
|
4074
|
-
};
|
|
4075
|
-
document.addEventListener("keydown", onKeyDown);
|
|
4076
|
-
return () => document.removeEventListener("keydown", onKeyDown);
|
|
4077
|
-
}, [inline]);
|
|
5022
|
+
useOpeningFocus(inline, focusBox, boxRef, panelRef);
|
|
5023
|
+
useTabTrap(inline, panelRef);
|
|
4078
5024
|
useEffect(() => {
|
|
4079
5025
|
if (!confirming) return;
|
|
4080
5026
|
cancelRef.current?.focus();
|
|
@@ -4151,6 +5097,13 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
|
|
|
4151
5097
|
onClick: () => setConfirming((now) => !now),
|
|
4152
5098
|
children: [/* @__PURE__ */ jsx(RestartIcon, {}), L.startOver]
|
|
4153
5099
|
}),
|
|
5100
|
+
!inline && support.available && support.state.hasPast && /* @__PURE__ */ jsx("button", {
|
|
5101
|
+
type: "button",
|
|
5102
|
+
className: "sb-panel-close",
|
|
5103
|
+
"aria-label": S.past,
|
|
5104
|
+
onClick: support.showPast,
|
|
5105
|
+
children: /* @__PURE__ */ jsx(HistoryIcon, {})
|
|
5106
|
+
}),
|
|
4154
5107
|
!inline && /* @__PURE__ */ jsx("button", {
|
|
4155
5108
|
type: "button",
|
|
4156
5109
|
className: "sb-panel-close",
|
|
@@ -4193,10 +5146,30 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
|
|
|
4193
5146
|
})
|
|
4194
5147
|
]
|
|
4195
5148
|
}),
|
|
5149
|
+
openConversation && messages.length > 0 && /* @__PURE__ */ jsxs("button", {
|
|
5150
|
+
type: "button",
|
|
5151
|
+
className: "sb-convbar",
|
|
5152
|
+
onClick: talk,
|
|
5153
|
+
children: [
|
|
5154
|
+
openConversation.unread && /* @__PURE__ */ jsx("span", { className: "sb-dot" }),
|
|
5155
|
+
openConversation.unread && /* @__PURE__ */ jsx("span", {
|
|
5156
|
+
className: "sb-sr",
|
|
5157
|
+
children: S.newReply
|
|
5158
|
+
}),
|
|
5159
|
+
/* @__PURE__ */ jsxs("span", {
|
|
5160
|
+
className: "sb-convbar-text",
|
|
5161
|
+
children: [S.conversationWith(company), /* @__PURE__ */ jsxs("span", {
|
|
5162
|
+
className: "sb-convbar-status",
|
|
5163
|
+
children: [" · ", supportStatus(openConversation, company, S)]
|
|
5164
|
+
})]
|
|
5165
|
+
}),
|
|
5166
|
+
/* @__PURE__ */ jsx(ChevronRightIcon, {})
|
|
5167
|
+
]
|
|
5168
|
+
}),
|
|
4196
5169
|
/* @__PURE__ */ jsxs("div", {
|
|
4197
5170
|
className: "sb-thread",
|
|
4198
5171
|
role: "log",
|
|
4199
|
-
"aria-label": L.
|
|
5172
|
+
"aria-label": L.thread,
|
|
4200
5173
|
"aria-live": "off",
|
|
4201
5174
|
children: [/* @__PURE__ */ jsx("div", {
|
|
4202
5175
|
className: `sb-thread-scroll${anchored ? " sb-anchored" : ""}`,
|
|
@@ -4207,7 +5180,30 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
|
|
|
4207
5180
|
children: messages.length === 0 ? /* @__PURE__ */ jsx(Suggestions, {
|
|
4208
5181
|
labels: L,
|
|
4209
5182
|
guides: listed,
|
|
4210
|
-
onRow: inline ? void 0 : (guide) => start(guide.key)
|
|
5183
|
+
onRow: inline ? void 0 : (guide) => start(guide.key),
|
|
5184
|
+
conversation: openConversation && /* @__PURE__ */ jsxs("button", {
|
|
5185
|
+
type: "button",
|
|
5186
|
+
className: "sb-conv",
|
|
5187
|
+
onClick: talk,
|
|
5188
|
+
children: [
|
|
5189
|
+
openConversation.unread && /* @__PURE__ */ jsx("span", { className: "sb-dot" }),
|
|
5190
|
+
openConversation.unread && /* @__PURE__ */ jsx("span", {
|
|
5191
|
+
className: "sb-sr",
|
|
5192
|
+
children: S.newReply
|
|
5193
|
+
}),
|
|
5194
|
+
/* @__PURE__ */ jsxs("span", {
|
|
5195
|
+
className: "sb-conv-text",
|
|
5196
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
5197
|
+
className: "sb-conv-title",
|
|
5198
|
+
children: supportStatus(openConversation, company, S)
|
|
5199
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
5200
|
+
className: "sb-conv-line",
|
|
5201
|
+
children: openConversation.lastMessageText
|
|
5202
|
+
})]
|
|
5203
|
+
}),
|
|
5204
|
+
/* @__PURE__ */ jsx(ChevronRightIcon, {})
|
|
5205
|
+
]
|
|
5206
|
+
})
|
|
4211
5207
|
}) : threadItems(messages).map((item) => item.kind === "line" ? /* @__PURE__ */ jsx(TourLine, {
|
|
4212
5208
|
line: item.line,
|
|
4213
5209
|
labels: L,
|
|
@@ -4230,10 +5226,13 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
|
|
|
4230
5226
|
latest: item.question.id === latestQuestion,
|
|
4231
5227
|
fresh: !seenRef.current.has(item.response.id),
|
|
4232
5228
|
guide: guideFor(item.response, listed),
|
|
4233
|
-
words: replyWords(item.response,
|
|
5229
|
+
words: replyWords(item.response, api.firstName, L),
|
|
4234
5230
|
labels: L,
|
|
4235
5231
|
onShowMe: inline ? void 0 : start,
|
|
4236
|
-
|
|
5232
|
+
talk: support.available ? {
|
|
5233
|
+
labels: S,
|
|
5234
|
+
onTalk: talk
|
|
5235
|
+
} : void 0
|
|
4237
5236
|
}, item.response.id),
|
|
4238
5237
|
item.response?.kind === "error" && /* @__PURE__ */ jsx(FailureNote, {
|
|
4239
5238
|
message: item.response,
|
|
@@ -4311,30 +5310,33 @@ function guideFor(message, listed) {
|
|
|
4311
5310
|
return key === void 0 ? void 0 : listed.find((guide) => guide.key === key);
|
|
4312
5311
|
}
|
|
4313
5312
|
/**
|
|
4314
|
-
* The assistant's own lines around a
|
|
4315
|
-
*
|
|
4316
|
-
*
|
|
5313
|
+
* The assistant's own lines around a reply: the greeting on the first reply
|
|
5314
|
+
* of a conversation that offers a guide or an answer, and the closing under
|
|
5315
|
+
* any reply that offers a guide.
|
|
4317
5316
|
*/
|
|
4318
|
-
function replyWords(
|
|
4319
|
-
const
|
|
4320
|
-
if (reply.outcome === "NONE") return {};
|
|
5317
|
+
function replyWords({ reply }, name, labels) {
|
|
5318
|
+
const offers = reply.outcome === "GUIDE" || reply.outcome === "ANSWERED";
|
|
4321
5319
|
return {
|
|
4322
|
-
greeting: reply.firstTurn ? labels.greeting(name) : void 0,
|
|
4323
|
-
closing:
|
|
5320
|
+
greeting: reply.firstTurn && offers ? labels.greeting(name) : void 0,
|
|
5321
|
+
closing: reply.guideKey !== void 0 ? labels.guideClosing : void 0
|
|
4324
5322
|
};
|
|
4325
5323
|
}
|
|
4326
5324
|
const SUGGESTION_LIMIT = 4;
|
|
4327
|
-
function Suggestions({ labels, guides, onRow }) {
|
|
5325
|
+
function Suggestions({ labels, guides, onRow, conversation }) {
|
|
4328
5326
|
return /* @__PURE__ */ jsxs("div", {
|
|
4329
5327
|
className: "sb-start",
|
|
4330
|
-
children: [
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
5328
|
+
children: [
|
|
5329
|
+
conversation,
|
|
5330
|
+
/* @__PURE__ */ jsx("h3", {
|
|
5331
|
+
className: "sb-start-title",
|
|
5332
|
+
children: labels.emptyTitle
|
|
5333
|
+
}),
|
|
5334
|
+
guides.length > 0 && /* @__PURE__ */ jsx(Group, {
|
|
5335
|
+
label: labels.commonQuestions,
|
|
5336
|
+
guides: guides.slice(0, SUGGESTION_LIMIT),
|
|
5337
|
+
onRow
|
|
5338
|
+
})
|
|
5339
|
+
]
|
|
4338
5340
|
});
|
|
4339
5341
|
}
|
|
4340
5342
|
function Group({ label, guides, onRow }) {
|
|
@@ -4414,8 +5416,9 @@ function Waiting({ stage, since, labels, productName }) {
|
|
|
4414
5416
|
]
|
|
4415
5417
|
});
|
|
4416
5418
|
}
|
|
4417
|
-
function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe,
|
|
5419
|
+
function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe, talk }) {
|
|
4418
5420
|
const { reply } = message;
|
|
5421
|
+
const notCovered = reply.outcome === "NONE" || reply.outcome === "UNSUPPORTED";
|
|
4419
5422
|
const primary = latest && !message.toured;
|
|
4420
5423
|
const [arrived] = useState(fresh);
|
|
4421
5424
|
return /* @__PURE__ */ jsxs("article", {
|
|
@@ -4428,7 +5431,7 @@ function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe,
|
|
|
4428
5431
|
className: "sb-reply-greeting",
|
|
4429
5432
|
children: words.greeting
|
|
4430
5433
|
}),
|
|
4431
|
-
/* @__PURE__ */ jsx(AnswerText, { text: reply.answer }),
|
|
5434
|
+
reply.answer !== "" && /* @__PURE__ */ jsx(AnswerText, { text: reply.answer }),
|
|
4432
5435
|
words.closing && /* @__PURE__ */ jsx("p", {
|
|
4433
5436
|
className: "sb-reply-closing",
|
|
4434
5437
|
children: words.closing
|
|
@@ -4444,7 +5447,21 @@ function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe,
|
|
|
4444
5447
|
children: [/* @__PURE__ */ jsx(PlayIcon, {}), labels.showMeHow]
|
|
4445
5448
|
})
|
|
4446
5449
|
}),
|
|
4447
|
-
|
|
5450
|
+
talk && (notCovered ? /* @__PURE__ */ jsx("div", {
|
|
5451
|
+
className: "sb-reply-action",
|
|
5452
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
5453
|
+
type: "button",
|
|
5454
|
+
className: "sb-secondary sb-talk-button",
|
|
5455
|
+
onClick: talk.onTalk,
|
|
5456
|
+
children: [/* @__PURE__ */ jsx(HeadsetIcon, {}), talk.labels.talkToPerson]
|
|
5457
|
+
})
|
|
5458
|
+
}) : /* @__PURE__ */ jsxs("button", {
|
|
5459
|
+
type: "button",
|
|
5460
|
+
className: "sb-talk",
|
|
5461
|
+
onClick: talk.onTalk,
|
|
5462
|
+
children: [/* @__PURE__ */ jsx(HeadsetIcon, {}), talk.labels.talkToPerson]
|
|
5463
|
+
})),
|
|
5464
|
+
!talk && reply.supportUrl && /* @__PURE__ */ jsxs("div", {
|
|
4448
5465
|
className: "sb-support",
|
|
4449
5466
|
children: [/* @__PURE__ */ jsx("span", { children: labels.supportOffer }), /* @__PURE__ */ jsxs("a", {
|
|
4450
5467
|
className: "sb-support-link",
|
|
@@ -4460,82 +5477,10 @@ function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe,
|
|
|
4460
5477
|
})
|
|
4461
5478
|
]
|
|
4462
5479
|
})]
|
|
4463
|
-
})
|
|
4464
|
-
reply.sources.length > 0 && (latest ? /* @__PURE__ */ jsx(Sources, {
|
|
4465
|
-
sources: reply.sources,
|
|
4466
|
-
labels
|
|
4467
|
-
}) : /* @__PURE__ */ jsx(FoldedSources, {
|
|
4468
|
-
sources: reply.sources,
|
|
4469
|
-
labels,
|
|
4470
|
-
keepInPlace
|
|
4471
|
-
}))
|
|
5480
|
+
})
|
|
4472
5481
|
]
|
|
4473
5482
|
});
|
|
4474
5483
|
}
|
|
4475
|
-
function Sources({ sources, labels }) {
|
|
4476
|
-
return /* @__PURE__ */ jsx("div", {
|
|
4477
|
-
className: "sb-sources",
|
|
4478
|
-
role: "group",
|
|
4479
|
-
"aria-label": labels.sources,
|
|
4480
|
-
children: /* @__PURE__ */ jsx(SourceRows, {
|
|
4481
|
-
sources,
|
|
4482
|
-
labels
|
|
4483
|
-
})
|
|
4484
|
-
});
|
|
4485
|
-
}
|
|
4486
|
-
function FoldedSources({ sources, labels, keepInPlace }) {
|
|
4487
|
-
const [open, setOpen] = useState(false);
|
|
4488
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
4489
|
-
className: "sb-sources",
|
|
4490
|
-
role: "group",
|
|
4491
|
-
"aria-label": labels.sources,
|
|
4492
|
-
children: [/* @__PURE__ */ jsxs("button", {
|
|
4493
|
-
type: "button",
|
|
4494
|
-
className: "sb-sources-toggle",
|
|
4495
|
-
"aria-expanded": open,
|
|
4496
|
-
onClick: () => keepInPlace(() => setOpen((now) => !now)),
|
|
4497
|
-
children: [
|
|
4498
|
-
/* @__PURE__ */ jsx(DocIcon, {}),
|
|
4499
|
-
/* @__PURE__ */ jsx("span", { children: labels.sourcesCount(sources.length) }),
|
|
4500
|
-
/* @__PURE__ */ jsx(ChevronDownIcon, {})
|
|
4501
|
-
]
|
|
4502
|
-
}), open && /* @__PURE__ */ jsx(SourceRows, {
|
|
4503
|
-
sources,
|
|
4504
|
-
labels
|
|
4505
|
-
})]
|
|
4506
|
-
});
|
|
4507
|
-
}
|
|
4508
|
-
function SourceRows({ sources, labels }) {
|
|
4509
|
-
return /* @__PURE__ */ jsx(Fragment, { children: sources.map((source) => {
|
|
4510
|
-
const name = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(DocIcon, {}), /* @__PURE__ */ jsxs("span", {
|
|
4511
|
-
className: "sb-doc-text",
|
|
4512
|
-
children: [source.heading !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
4513
|
-
className: "sb-doc-trail",
|
|
4514
|
-
children: `${source.title} › `
|
|
4515
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
4516
|
-
className: "sb-doc-title",
|
|
4517
|
-
children: source.heading ?? source.title
|
|
4518
|
-
})]
|
|
4519
|
-
})] });
|
|
4520
|
-
return source.url === void 0 ? /* @__PURE__ */ jsx("div", {
|
|
4521
|
-
className: "sb-doc",
|
|
4522
|
-
children: name
|
|
4523
|
-
}, source.id) : /* @__PURE__ */ jsxs("a", {
|
|
4524
|
-
className: "sb-doc",
|
|
4525
|
-
href: source.url,
|
|
4526
|
-
target: "_blank",
|
|
4527
|
-
rel: "noreferrer",
|
|
4528
|
-
children: [
|
|
4529
|
-
name,
|
|
4530
|
-
/* @__PURE__ */ jsx(ExternalIcon, {}),
|
|
4531
|
-
/* @__PURE__ */ jsx("span", {
|
|
4532
|
-
className: "sb-sr",
|
|
4533
|
-
children: labels.newTab
|
|
4534
|
-
})
|
|
4535
|
-
]
|
|
4536
|
-
}, source.id);
|
|
4537
|
-
}) });
|
|
4538
|
-
}
|
|
4539
5484
|
function FailureNote({ message, fresh, labels, onRetry }) {
|
|
4540
5485
|
const { failure } = message;
|
|
4541
5486
|
const lost = failure.kind === "lost";
|
|
@@ -4641,19 +5586,6 @@ function Stars({ labels, onPick }) {
|
|
|
4641
5586
|
}, rating))
|
|
4642
5587
|
});
|
|
4643
5588
|
}
|
|
4644
|
-
function clockTime(iso) {
|
|
4645
|
-
const at = new Date(iso);
|
|
4646
|
-
if (Number.isNaN(at.getTime())) return iso;
|
|
4647
|
-
return at.toLocaleTimeString(void 0, {
|
|
4648
|
-
hour: "2-digit",
|
|
4649
|
-
minute: "2-digit"
|
|
4650
|
-
});
|
|
4651
|
-
}
|
|
4652
|
-
function minutesUntil(iso) {
|
|
4653
|
-
const at = new Date(iso).getTime();
|
|
4654
|
-
if (Number.isNaN(at)) return 1;
|
|
4655
|
-
return Math.max(1, Math.round((at - Date.now()) / 6e4));
|
|
4656
|
-
}
|
|
4657
5589
|
//#endregion
|
|
4658
5590
|
//#region src/components/SunboardOverlay.tsx
|
|
4659
5591
|
function SunboardOverlay({ launcher = true } = {}) {
|
|
@@ -4665,4 +5597,4 @@ function SunboardOverlay({ launcher = true } = {}) {
|
|
|
4665
5597
|
] });
|
|
4666
5598
|
}
|
|
4667
5599
|
//#endregion
|
|
4668
|
-
export { DEFAULT_SUNBOARD_API_URL, HttpTransport, LAUNCH_PARAM, Launcher, MemoryTransport, PREVIEW_EXPIRY_STORAGE, PREVIEW_KEY_STORAGE, PREVIEW_PARAM, SunboardOverlay, SunboardProvider, cooldownActive, detectThemeMode, enLabels, hasSeen, isBootstrapOff, markSeen, pickPushGuide, recordDismissal, resolveAnchor, themeToCssVars, useAnchorElement, useComponentSettings, useCurrentPathname, useGuide, useGuidesApi, useLabels, useLauncher, useTheme, useThemeMode, useTrack };
|
|
5600
|
+
export { DEFAULT_SUNBOARD_API_URL, HttpTransport, LAUNCH_PARAM, Launcher, MemoryTransport, PREVIEW_EXPIRY_STORAGE, PREVIEW_KEY_STORAGE, PREVIEW_PARAM, SunboardOverlay, SunboardProvider, cooldownActive, detectThemeMode, enLabels, hasSeen, isBootstrapOff, markSeen, pickPushGuide, recordDismissal, resolveAnchor, themeToCssVars, useAnchorElement, useComponentSettings, useCurrentPathname, useGuide, useGuidesApi, useLabels, useLauncher, useSupport, useTheme, useThemeMode, useTrack };
|