@sunboard/react 1.1.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/dist/index.mjs CHANGED
@@ -193,7 +193,9 @@ const defaultTheme = createTheme("Default", {
193
193
  const defaultComponentSettings = {
194
194
  launcher: {
195
195
  showBranding: true,
196
- supportUrl: null
196
+ supportUrl: null,
197
+ talkToPerson: false,
198
+ replyTime: "We reply within a few hours."
197
199
  },
198
200
  tour: {
199
201
  showStepCount: true,
@@ -226,7 +228,7 @@ const enLabels = {
226
228
  rateTour: "Rate this tour",
227
229
  stars: (count) => `${count} ${count === 1 ? "star" : "stars"}`,
228
230
  ratingThanks: "Thanks for the feedback.",
229
- conversation: "Conversation",
231
+ thread: "Conversation",
230
232
  writingStatus: "Writing an answer",
231
233
  stages: {
232
234
  searching: (product) => `Searching the ${product} help docs`,
@@ -249,6 +251,36 @@ const enLabels = {
249
251
  closeAria: "Close help",
250
252
  poweredBy: "Powered by"
251
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
+ },
252
284
  tour: {
253
285
  back: "Back",
254
286
  next: "Next",
@@ -278,6 +310,10 @@ function withLabels(overrides) {
278
310
  ...enLabels.launcher,
279
311
  ...overrides.launcher
280
312
  },
313
+ support: {
314
+ ...enLabels.support,
315
+ ...overrides.support
316
+ },
281
317
  tour: {
282
318
  ...enLabels.tour,
283
319
  ...overrides.tour
@@ -377,6 +413,8 @@ const TOUR_RATINGS = [
377
413
  4,
378
414
  5
379
415
  ];
416
+ /** One home for the cap, so the composer and the route agree on it. */
417
+ const CONVERSATION_MESSAGE_MAX_LENGTH = 4e3;
380
418
  //#endregion
381
419
  //#region src/active-tour.ts
382
420
  const STORAGE_KEY = "sunboard:active-tour";
@@ -413,7 +451,7 @@ function stepInRange(guides, key, stepIndex) {
413
451
  const PREFIX = "sunboard:bootstrap:";
414
452
  /** Past this, content the server has since changed is likelier than not. */
415
453
  const MAX_AGE_MS = 6048e5;
416
- const VERSION = 4;
454
+ const VERSION = 5;
417
455
  function readCachedBootstrap(key) {
418
456
  if (typeof window === "undefined") return null;
419
457
  try {
@@ -1350,15 +1388,221 @@ function useThemeMode(setting) {
1350
1388
  return setting === "auto" ? detected : setting;
1351
1389
  }
1352
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
1353
1585
  //#region src/transport.ts
1354
1586
  /** Long enough for a slow model, short enough that nobody stares at nothing. */
1355
1587
  const ASK_TIMEOUT_MS = 3e4;
1356
- let memoryConversations = 0;
1588
+ let memoryThreads = 0;
1357
1589
  const STAGES$1 = [
1358
1590
  "searching",
1359
1591
  "writing",
1360
1592
  "checking"
1361
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
+ }
1362
1606
  var MemoryTransport = class {
1363
1607
  data;
1364
1608
  misses = [];
@@ -1372,10 +1616,12 @@ var MemoryTransport = class {
1372
1616
  user;
1373
1617
  ask;
1374
1618
  reportPage;
1619
+ support;
1375
1620
  constructor(data, options) {
1376
1621
  this.data = data;
1377
1622
  this.cacheKey = options?.cacheKey;
1378
1623
  this.user = options?.user;
1624
+ if (options?.support) this.support = new MemorySupport(options.support);
1379
1625
  if (options?.census) this.reportPage = (report) => {
1380
1626
  this.pages.push(report);
1381
1627
  return Promise.resolve();
@@ -1383,7 +1629,7 @@ var MemoryTransport = class {
1383
1629
  const script = options?.ask;
1384
1630
  if (!script) return;
1385
1631
  const gapMs = options?.gapMs ?? 0;
1386
- const conversationId = `memory-conversation-${++memoryConversations}`;
1632
+ const threadId = `memory-thread-${++memoryThreads}`;
1387
1633
  this.ask = async (asked, signal, onStage) => {
1388
1634
  this.questions.push(asked);
1389
1635
  const scripted = script(asked);
@@ -1404,8 +1650,8 @@ var MemoryTransport = class {
1404
1650
  kind: "answer",
1405
1651
  reply: {
1406
1652
  ...scripted.reply,
1407
- conversationId: scripted.reply.conversationId ?? conversationId,
1408
- firstTurn: scripted.reply.firstTurn ?? asked.conversationId === void 0
1653
+ threadId: scripted.reply.threadId ?? threadId,
1654
+ firstTurn: scripted.reply.firstTurn ?? asked.threadId === void 0
1409
1655
  }
1410
1656
  };
1411
1657
  };
@@ -1516,89 +1762,114 @@ var HttpTransport = class {
1516
1762
  previewKey;
1517
1763
  cacheKey;
1518
1764
  user;
1765
+ support;
1519
1766
  constructor(config) {
1520
1767
  this.config = config;
1768
+ const { sub, name } = readUserToken(config.userToken);
1521
1769
  this.baseUrl = config.baseUrl ?? "https://api.sunboardhq.com";
1522
- this.user = config.user;
1770
+ this.user = name === void 0 ? {} : { firstName: name };
1523
1771
  this.previewKey = config.previewKey ?? storedPreviewKey();
1524
- this.cacheKey = this.previewKey ? void 0 : `${config.publishableKey}:${config.userId}`;
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
+ };
1525
1791
  }
1526
- headers() {
1792
+ identity(extra = {}) {
1793
+ return new URLSearchParams({
1794
+ userToken: this.config.userToken,
1795
+ ...extra
1796
+ }).toString();
1797
+ }
1798
+ signed(body) {
1527
1799
  return {
1528
- "content-type": "application/json",
1529
- "x-sunboard-key": this.config.publishableKey,
1530
- ...this.previewKey ? { "x-sunboard-preview-key": this.previewKey } : {}
1800
+ ...body,
1801
+ userToken: this.config.userToken
1531
1802
  };
1532
1803
  }
1533
- async request(path, init) {
1534
- const response = await fetch(`${this.baseUrl}${path}`, {
1804
+ send(path, init) {
1805
+ return fetch(`${this.baseUrl}${path}`, {
1535
1806
  ...init,
1536
1807
  headers: {
1537
1808
  ...this.headers(),
1538
1809
  ...init?.headers
1539
1810
  }
1540
1811
  });
1541
- if (!response.ok) throw new Error(`sunboard runtime request failed: ${response.status} for ${path}`);
1542
- return response;
1543
1812
  }
1544
- async bootstrap() {
1545
- return await (await this.request(`/runtime/bootstrap?user=${encodeURIComponent(this.config.userId)}`)).json();
1813
+ async getJson(path) {
1814
+ return await (await this.request(`${path}?${this.identity()}`)).json();
1546
1815
  }
1547
- async mutate(action) {
1548
- await this.request("/runtime/state", {
1816
+ post(path, body, init) {
1817
+ return this.request(path, {
1818
+ ...init,
1549
1819
  method: "POST",
1550
- body: JSON.stringify({
1551
- user: this.config.userId,
1552
- ...action
1553
- })
1820
+ body: JSON.stringify(this.signed(body))
1554
1821
  });
1555
1822
  }
1556
- async reportAnchorMiss(report) {
1557
- await this.request("/runtime/anchor-miss", {
1823
+ async write(path, body) {
1824
+ const response = await this.send(path, {
1558
1825
  method: "POST",
1559
- body: JSON.stringify({
1560
- user: this.config.userId,
1561
- ...report
1562
- })
1826
+ body: JSON.stringify(this.signed(body))
1563
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);
1564
1854
  }
1565
1855
  async reportAnchorFound(report) {
1566
- await this.request("/runtime/anchor-found", {
1567
- method: "POST",
1568
- body: JSON.stringify(report)
1569
- });
1856
+ await this.post("/runtime/anchor-found", report);
1570
1857
  }
1571
1858
  async stepCheck(report) {
1572
- await this.request("/runtime/step-check", {
1573
- method: "POST",
1574
- body: JSON.stringify(report)
1575
- });
1859
+ await this.post("/runtime/step-check", report);
1576
1860
  }
1577
1861
  async recordEvents(events) {
1578
1862
  if (events.length === 0) return;
1579
- await this.request("/runtime/events", {
1580
- method: "POST",
1581
- body: JSON.stringify({
1582
- user: this.config.userId,
1583
- events
1584
- }),
1585
- keepalive: true
1586
- });
1863
+ await this.post("/runtime/events", { events }, { keepalive: true });
1587
1864
  }
1588
1865
  async reportPage(report) {
1589
- await this.request("/runtime/page", {
1590
- method: "POST",
1591
- body: JSON.stringify(report)
1592
- });
1866
+ await this.post("/runtime/page", report);
1593
1867
  }
1594
1868
  async ask(asked, signal, onStage) {
1595
1869
  const response = await fetch(`${this.baseUrl}/runtime/ask`, {
1596
1870
  method: "POST",
1597
1871
  headers: this.headers(),
1598
- body: JSON.stringify({
1599
- user: this.config.userId,
1600
- ...asked
1601
- }),
1872
+ body: JSON.stringify(this.signed(asked)),
1602
1873
  signal: AbortSignal.any([signal, AbortSignal.timeout(ASK_TIMEOUT_MS)])
1603
1874
  });
1604
1875
  if (response.status === 400 || response.status === 429) {
@@ -1615,6 +1886,10 @@ var HttpTransport = class {
1615
1886
  };
1616
1887
  }
1617
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
+ }
1618
1893
  /**
1619
1894
  * The sentence a 400 or a 429 carries, and the time the cap lifts. Read as
1620
1895
  * text and parsed loosely: a proxy in front of the API answers those codes
@@ -1652,14 +1927,14 @@ function optionalString(value) {
1652
1927
  /** A reply as the server sends it, or as the thread kept it in session storage. */
1653
1928
  function parseReply(value) {
1654
1929
  if (!isRecord$1(value)) return void 0;
1655
- const { id, conversationId, firstTurn, outcome, answer, guideKey, supportUrl } = value;
1656
- if (typeof id !== "string" || typeof conversationId !== "string") return void 0;
1930
+ const { id, threadId, firstTurn, outcome, answer, guideKey, supportUrl } = value;
1931
+ if (typeof id !== "string" || typeof threadId !== "string") return void 0;
1657
1932
  if (typeof firstTurn !== "boolean") return void 0;
1658
1933
  if (!isAskOutcome(outcome) || typeof answer !== "string") return void 0;
1659
1934
  if (!optionalString(guideKey) || !optionalString(supportUrl)) return void 0;
1660
1935
  return {
1661
1936
  id,
1662
- conversationId,
1937
+ threadId,
1663
1938
  firstTurn,
1664
1939
  outcome,
1665
1940
  answer,
@@ -1726,8 +2001,13 @@ function SunboardProvider(props) {
1726
2001
  const connectionKey = connection ? JSON.stringify(connection) : null;
1727
2002
  const resolvedTransport = useMemo(() => {
1728
2003
  if (transport) return transport;
1729
- if (connectionKey) return new HttpTransport(JSON.parse(connectionKey));
1730
- return null;
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
+ }
1731
2011
  }, [transport, connectionKey]);
1732
2012
  if (!resolvedTransport) throw new Error("SunboardProvider needs either a connection or a transport");
1733
2013
  const [answer, setAnswer] = useState(null);
@@ -1961,6 +2241,7 @@ function SunboardProvider(props) {
1961
2241
  if (!transport.ask) throw new Error("this transport cannot ask questions");
1962
2242
  return transport.ask(asked, signal, onStage);
1963
2243
  },
2244
+ support: off ? void 0 : resolvedTransport.support,
1964
2245
  track,
1965
2246
  navigate
1966
2247
  };
@@ -2099,7 +2380,7 @@ function threadReducer(thread, action) {
2099
2380
  } : response);
2100
2381
  return next === thread ? thread : {
2101
2382
  ...next,
2102
- conversationId: action.reply.conversationId
2383
+ threadId: action.reply.threadId
2103
2384
  };
2104
2385
  }
2105
2386
  case "failed": return replaceResponse(thread, action.questionId, (response) => response.kind === "waiting" ? {
@@ -2279,14 +2560,14 @@ function parseThread(value) {
2279
2560
  if (!message) return null;
2280
2561
  messages.push(message);
2281
2562
  }
2282
- const conversationId = typeof value.conversationId === "string" ? value.conversationId : void 0;
2563
+ const threadId = typeof value.threadId === "string" ? value.threadId : void 0;
2283
2564
  const handoff = parseHandoff(value.handoff);
2284
- return conversationId === void 0 ? {
2565
+ return threadId === void 0 ? {
2285
2566
  messages,
2286
2567
  handoff
2287
2568
  } : {
2288
2569
  messages,
2289
- conversationId,
2570
+ threadId,
2290
2571
  handoff
2291
2572
  };
2292
2573
  }
@@ -2375,6 +2656,162 @@ function parseFailure(value) {
2375
2656
  return null;
2376
2657
  }
2377
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
2378
2815
  //#region src/hooks.ts
2379
2816
  function useGuidesApi() {
2380
2817
  const api = useContext(GuidesContext);
@@ -2494,7 +2931,7 @@ function useLauncher({ remember = true } = {}) {
2494
2931
  const asked = {
2495
2932
  question,
2496
2933
  pathname,
2497
- conversationId: threadRef.current.conversationId
2934
+ threadId: threadRef.current.threadId
2498
2935
  };
2499
2936
  run(transportAsk, asked, questionId, controller.signal, dispatch);
2500
2937
  };
@@ -2504,6 +2941,7 @@ function useLauncher({ remember = true } = {}) {
2504
2941
  ready: api.ready,
2505
2942
  tourRunning: api.activeTour !== null,
2506
2943
  messages: thread.messages,
2944
+ threadId: thread.threadId,
2507
2945
  waiting: isWaiting(thread),
2508
2946
  rateLimitedUntil: cappedUntil,
2509
2947
  handedOff: thread.handoff !== null,
@@ -2576,6 +3014,157 @@ function useLauncher({ remember = true } = {}) {
2576
3014
  }
2577
3015
  };
2578
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
+ }
2579
3168
  /** One guide's content plus launchTour; call with no key for launch-only. */
2580
3169
  function useGuide(key) {
2581
3170
  const api = useGuidesApi();
@@ -2764,6 +3353,28 @@ function StarIcon() {
2764
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" })
2765
3354
  });
2766
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
+ }
2767
3378
  //#endregion
2768
3379
  //#region src/overlay/CardMedia.tsx
2769
3380
  const VIDEO_URL = /\.(mp4|webm|m4v|mov|ogg)([?#]|$)/i;
@@ -3851,10 +4462,387 @@ function SunboardLogo({ width = 75 }) {
3851
4462
  }) })]
3852
4463
  });
3853
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
+ }
3854
4844
  //#endregion
3855
4845
  //#region src/components/Launcher.tsx
3856
- /** The width below which the panel is a bottom sheet; see styles.css. */
3857
- const PHONE_WIDTH = 640;
3858
4846
  /** How long the stage labels stay before the wait owns up to being slow. */
3859
4847
  const SLOW_MS = 6e3;
3860
4848
  const STAGE_AT = {
@@ -3862,14 +4850,19 @@ const STAGE_AT = {
3862
4850
  writing: 1,
3863
4851
  checking: 2
3864
4852
  };
3865
- const FOCUSABLE = "a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled])";
3866
4853
  function Launcher({ inline } = {}) {
3867
4854
  const launcher = useLauncher({ remember: !inline });
3868
4855
  const { ready, onThisPage, elsewhere, tourRunning, opened, handedOff } = launcher;
3869
4856
  const [open, setOpen] = useState(false);
4857
+ const support = useSupport({
4858
+ onScreen: open !== false,
4859
+ threadId: launcher.threadId
4860
+ });
3870
4861
  const { off } = useGuidesApi();
3871
4862
  const canAsk = launcher.ask !== void 0;
3872
4863
  const L = useLabels().launcher;
4864
+ const S = useLabels().support;
4865
+ const unread = support.available && support.state.open?.unread === true;
3873
4866
  const pillRef = useRef(null);
3874
4867
  const wasOpen = useRef(false);
3875
4868
  const wasHandedOff = useRef(handedOff);
@@ -3896,31 +4889,49 @@ function Launcher({ inline } = {}) {
3896
4889
  if (inline) return /* @__PURE__ */ jsx(Scope, { children: /* @__PURE__ */ jsx(QuestionPanel, {
3897
4890
  inline: true,
3898
4891
  launcher,
4892
+ support,
3899
4893
  close: () => setOpen(false),
3900
4894
  focusBox: false
3901
4895
  }) });
3902
4896
  const floating = (surface) => /* @__PURE__ */ jsx(FloatingPortal, { children: /* @__PURE__ */ jsx(Scope, { children: surface }) });
3903
4897
  if (!ready) return null;
3904
4898
  if (tourRunning) return floating(/* @__PURE__ */ jsx(TourPill, {}));
3905
- if (!canAsk && onThisPage.length + elsewhere.length === 0) return null;
3906
- if (open) return floating(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
3907
- className: "sb-panel-scrim",
3908
- onClick: () => setOpen(false)
3909
- }), /* @__PURE__ */ jsx(QuestionPanel, {
3910
- launcher,
3911
- close: () => setOpen(false),
3912
- focusBox: open === "back"
3913
- })] }));
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
+ }
3914
4921
  return floating(/* @__PURE__ */ jsxs("button", {
3915
4922
  type: "button",
3916
4923
  className: "sb-launcher",
3917
4924
  ref: pillRef,
3918
- "aria-label": L.openAria,
4925
+ "aria-label": unread ? S.openAriaNewReply : L.openAria,
3919
4926
  onClick: () => {
3920
4927
  setOpen("opened");
3921
4928
  opened();
3922
4929
  },
3923
- children: [/* @__PURE__ */ jsx(HelpIcon, {}), L.open]
4930
+ children: [
4931
+ /* @__PURE__ */ jsx(HelpIcon, {}),
4932
+ L.open,
4933
+ unread && /* @__PURE__ */ jsx("span", { className: "sb-dot sb-launcher-dot" })
4934
+ ]
3924
4935
  }));
3925
4936
  }
3926
4937
  /**
@@ -3928,10 +4939,14 @@ function Launcher({ inline } = {}) {
3928
4939
  * tour taking its place, loses nothing; a question still out lands while the
3929
4940
  * panel is away and is there when it comes back.
3930
4941
  */
3931
- function QuestionPanel({ inline, launcher, close, focusBox }) {
4942
+ function QuestionPanel({ inline, launcher, support, close, focusBox }) {
3932
4943
  const { onThisPage, elsewhere, messages, waiting, rateLimitedUntil, ask } = launcher;
3933
4944
  const api = useGuidesApi();
3934
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;
3935
4950
  const showBranding = useComponentSettings().launcher.showBranding;
3936
4951
  const panelRef = useRef(null);
3937
4952
  const scrollRef = useRef(null);
@@ -4004,31 +5019,8 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
4004
5019
  }
4005
5020
  if (finished !== null) setAnnouncement(finished);
4006
5021
  }, [messages]);
4007
- useEffect(() => {
4008
- if (inline) return;
4009
- const phone = window.matchMedia?.(`(max-width: ${PHONE_WIDTH}px)`).matches ?? false;
4010
- const box = boxRef.current;
4011
- if ((focusBox || !phone) && box && !box.disabled) box.focus({ preventScroll: true });
4012
- else panelRef.current?.focus({ preventScroll: true });
4013
- }, [inline]);
4014
- useEffect(() => {
4015
- if (inline) return;
4016
- const onKeyDown = (event) => {
4017
- const panel = panelRef.current;
4018
- if (event.key !== "Tab" || event.defaultPrevented || !panel) return;
4019
- const stops = [...panel.querySelectorAll(FOCUSABLE)];
4020
- if (stops.length === 0) return;
4021
- const first = stops[0];
4022
- const last = stops[stops.length - 1];
4023
- const inside = panel.contains(document.activeElement);
4024
- const leaving = document.activeElement === (event.shiftKey ? first : last);
4025
- if (inside && !leaving) return;
4026
- event.preventDefault();
4027
- (event.shiftKey ? last : first).focus();
4028
- };
4029
- document.addEventListener("keydown", onKeyDown);
4030
- return () => document.removeEventListener("keydown", onKeyDown);
4031
- }, [inline]);
5022
+ useOpeningFocus(inline, focusBox, boxRef, panelRef);
5023
+ useTabTrap(inline, panelRef);
4032
5024
  useEffect(() => {
4033
5025
  if (!confirming) return;
4034
5026
  cancelRef.current?.focus();
@@ -4105,6 +5097,13 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
4105
5097
  onClick: () => setConfirming((now) => !now),
4106
5098
  children: [/* @__PURE__ */ jsx(RestartIcon, {}), L.startOver]
4107
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
+ }),
4108
5107
  !inline && /* @__PURE__ */ jsx("button", {
4109
5108
  type: "button",
4110
5109
  className: "sb-panel-close",
@@ -4147,10 +5146,30 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
4147
5146
  })
4148
5147
  ]
4149
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
+ }),
4150
5169
  /* @__PURE__ */ jsxs("div", {
4151
5170
  className: "sb-thread",
4152
5171
  role: "log",
4153
- "aria-label": L.conversation,
5172
+ "aria-label": L.thread,
4154
5173
  "aria-live": "off",
4155
5174
  children: [/* @__PURE__ */ jsx("div", {
4156
5175
  className: `sb-thread-scroll${anchored ? " sb-anchored" : ""}`,
@@ -4161,7 +5180,30 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
4161
5180
  children: messages.length === 0 ? /* @__PURE__ */ jsx(Suggestions, {
4162
5181
  labels: L,
4163
5182
  guides: listed,
4164
- 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
+ })
4165
5207
  }) : threadItems(messages).map((item) => item.kind === "line" ? /* @__PURE__ */ jsx(TourLine, {
4166
5208
  line: item.line,
4167
5209
  labels: L,
@@ -4186,7 +5228,11 @@ function QuestionPanel({ inline, launcher, close, focusBox }) {
4186
5228
  guide: guideFor(item.response, listed),
4187
5229
  words: replyWords(item.response, api.firstName, L),
4188
5230
  labels: L,
4189
- onShowMe: inline ? void 0 : start
5231
+ onShowMe: inline ? void 0 : start,
5232
+ talk: support.available ? {
5233
+ labels: S,
5234
+ onTalk: talk
5235
+ } : void 0
4190
5236
  }, item.response.id),
4191
5237
  item.response?.kind === "error" && /* @__PURE__ */ jsx(FailureNote, {
4192
5238
  message: item.response,
@@ -4276,17 +5322,21 @@ function replyWords({ reply }, name, labels) {
4276
5322
  };
4277
5323
  }
4278
5324
  const SUGGESTION_LIMIT = 4;
4279
- function Suggestions({ labels, guides, onRow }) {
5325
+ function Suggestions({ labels, guides, onRow, conversation }) {
4280
5326
  return /* @__PURE__ */ jsxs("div", {
4281
5327
  className: "sb-start",
4282
- children: [/* @__PURE__ */ jsx("h3", {
4283
- className: "sb-start-title",
4284
- children: labels.emptyTitle
4285
- }), guides.length > 0 && /* @__PURE__ */ jsx(Group, {
4286
- label: labels.commonQuestions,
4287
- guides: guides.slice(0, SUGGESTION_LIMIT),
4288
- onRow
4289
- })]
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
+ ]
4290
5340
  });
4291
5341
  }
4292
5342
  function Group({ label, guides, onRow }) {
@@ -4366,8 +5416,9 @@ function Waiting({ stage, since, labels, productName }) {
4366
5416
  ]
4367
5417
  });
4368
5418
  }
4369
- function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe }) {
5419
+ function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe, talk }) {
4370
5420
  const { reply } = message;
5421
+ const notCovered = reply.outcome === "NONE" || reply.outcome === "UNSUPPORTED";
4371
5422
  const primary = latest && !message.toured;
4372
5423
  const [arrived] = useState(fresh);
4373
5424
  return /* @__PURE__ */ jsxs("article", {
@@ -4396,7 +5447,21 @@ function ReplyArticle({ message, latest, fresh, guide, words, labels, onShowMe }
4396
5447
  children: [/* @__PURE__ */ jsx(PlayIcon, {}), labels.showMeHow]
4397
5448
  })
4398
5449
  }),
4399
- reply.supportUrl && /* @__PURE__ */ jsxs("div", {
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", {
4400
5465
  className: "sb-support",
4401
5466
  children: [/* @__PURE__ */ jsx("span", { children: labels.supportOffer }), /* @__PURE__ */ jsxs("a", {
4402
5467
  className: "sb-support-link",
@@ -4521,19 +5586,6 @@ function Stars({ labels, onPick }) {
4521
5586
  }, rating))
4522
5587
  });
4523
5588
  }
4524
- function clockTime(iso) {
4525
- const at = new Date(iso);
4526
- if (Number.isNaN(at.getTime())) return iso;
4527
- return at.toLocaleTimeString(void 0, {
4528
- hour: "2-digit",
4529
- minute: "2-digit"
4530
- });
4531
- }
4532
- function minutesUntil(iso) {
4533
- const at = new Date(iso).getTime();
4534
- if (Number.isNaN(at)) return 1;
4535
- return Math.max(1, Math.round((at - Date.now()) / 6e4));
4536
- }
4537
5589
  //#endregion
4538
5590
  //#region src/components/SunboardOverlay.tsx
4539
5591
  function SunboardOverlay({ launcher = true } = {}) {
@@ -4545,4 +5597,4 @@ function SunboardOverlay({ launcher = true } = {}) {
4545
5597
  ] });
4546
5598
  }
4547
5599
  //#endregion
4548
- 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 };