@cimplify/sdk 0.95.0 → 0.97.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/react.js CHANGED
@@ -3115,8 +3115,8 @@ function fmtScheduleWindow(start, end) {
3115
3115
  const startStr = dateFns.format(s, "EEE, MMM d \xB7 h:mm a");
3116
3116
  const e = end ? dateFns.parseISO(end) : null;
3117
3117
  if (e && dateFns.isValid(e)) {
3118
- const sameDay = s.toDateString() === e.toDateString();
3119
- return `${startStr} \u2013 ${dateFns.format(e, sameDay ? "h:mm a" : "EEE, MMM d \xB7 h:mm a")}`;
3118
+ const sameDay2 = s.toDateString() === e.toDateString();
3119
+ return `${startStr} \u2013 ${dateFns.format(e, sameDay2 ? "h:mm a" : "EEE, MMM d \xB7 h:mm a")}`;
3120
3120
  }
3121
3121
  return startStr;
3122
3122
  }
@@ -5907,6 +5907,33 @@ function useBootstrap(options = {}) {
5907
5907
  const { data, isLoading, error, refetch } = bootstrapQuery(options);
5908
5908
  return { bootstrap: data, isLoading, error, refetch };
5909
5909
  }
5910
+
5911
+ // src/react/chat-model.ts
5912
+ var OPTIMISTIC_ID_PREFIX = "opt_";
5913
+ function optimisticId(clientId) {
5914
+ return `${OPTIMISTIC_ID_PREFIX}${clientId}`;
5915
+ }
5916
+ function isOptimistic(message) {
5917
+ return message.id.startsWith(OPTIMISTIC_ID_PREFIX);
5918
+ }
5919
+ function partitionMessages(messages) {
5920
+ const bubbles = [];
5921
+ const reactionsByTarget = /* @__PURE__ */ new Map();
5922
+ for (const message of messages) {
5923
+ if (message.content_type === "reaction") {
5924
+ const targetId = message.reply_to?.id;
5925
+ if (!targetId) continue;
5926
+ const list = reactionsByTarget.get(targetId);
5927
+ if (list) list.push(message);
5928
+ else reactionsByTarget.set(targetId, [message]);
5929
+ continue;
5930
+ }
5931
+ bubbles.push(message);
5932
+ }
5933
+ return { bubbles, reactionsByTarget };
5934
+ }
5935
+
5936
+ // src/react/hooks/use-chat.ts
5910
5937
  var POLL_INTERVAL_MS = 3e3;
5911
5938
  var POLL_IDLE_INTERVAL_MS = 15e3;
5912
5939
  var IDLE_TIMEOUT_MS = 6e4;
@@ -5921,10 +5948,10 @@ function mergeMessages(prev, incoming) {
5921
5948
  (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
5922
5949
  );
5923
5950
  const confirmed = new Set(
5924
- merged.filter((m) => !m.id.startsWith("opt_") && m.client_id).map((m) => m.client_id)
5951
+ merged.filter((m) => !isOptimistic(m) && m.client_id).map((m) => m.client_id)
5925
5952
  );
5926
5953
  return merged.filter(
5927
- (m) => !(m.id.startsWith("opt_") && m.client_id && confirmed.has(m.client_id))
5954
+ (m) => !(isOptimistic(m) && m.client_id && confirmed.has(m.client_id))
5928
5955
  );
5929
5956
  }
5930
5957
  function useChat(options = {}) {
@@ -5987,7 +6014,7 @@ function useChat(options = {}) {
5987
6014
  const lastConfirmedMessage = React14.useCallback(() => {
5988
6015
  const list = messagesRef.current;
5989
6016
  for (let i = list.length - 1; i >= 0; i--) {
5990
- if (!list[i].id.startsWith("opt_")) return list[i];
6017
+ if (!isOptimistic(list[i])) return list[i];
5991
6018
  }
5992
6019
  return void 0;
5993
6020
  }, []);
@@ -6035,7 +6062,7 @@ function useChat(options = {}) {
6035
6062
  const snapshotIds = new Set(snapshot.map((m) => m.id));
6036
6063
  const tail = snapshot.length > 0 ? new Date(snapshot[snapshot.length - 1].created_at).getTime() : 0;
6037
6064
  const extras = prev.filter(
6038
- (m) => !snapshotIds.has(m.id) && (m.id.startsWith("opt_") || new Date(m.created_at).getTime() > tail)
6065
+ (m) => !snapshotIds.has(m.id) && (isOptimistic(m) || new Date(m.created_at).getTime() > tail)
6039
6066
  );
6040
6067
  return mergeMessages(snapshot, extras);
6041
6068
  });
@@ -6221,9 +6248,9 @@ function useChat(options = {}) {
6221
6248
  }
6222
6249
  const replyTo = opts?.replyTo;
6223
6250
  const clientId = chunkVEOAMBEX_js.generateIdempotencyKey();
6224
- const optimisticId = `opt_${clientId}`;
6251
+ const localId = optimisticId(clientId);
6225
6252
  const optimistic = {
6226
- id: optimisticId,
6253
+ id: localId,
6227
6254
  sender_type: "customer",
6228
6255
  content: content.trim(),
6229
6256
  content_type: attachments.length > 0 ? "image" : "text",
@@ -6253,14 +6280,14 @@ function useChat(options = {}) {
6253
6280
  // The websocket may have already streamed the confirmed message
6254
6281
  // in while the POST was in flight; drop the optimistic bubble
6255
6282
  // instead of inserting a duplicate.
6256
- prev.some((m) => m.id === confirmed.id) ? prev.filter((m) => m.id !== optimisticId) : prev.map((m) => m.id === optimisticId ? confirmed : m)
6283
+ prev.some((m) => m.id === confirmed.id) ? prev.filter((m) => m.id !== localId) : prev.map((m) => m.id === localId ? confirmed : m)
6257
6284
  )
6258
6285
  );
6259
6286
  if (aiEnabledRef.current && !humanRequestedRef.current) {
6260
6287
  startAwaitingReply();
6261
6288
  }
6262
6289
  } else {
6263
- setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
6290
+ setMessages((prev) => prev.filter((m) => m.id !== localId));
6264
6291
  setError(result.error);
6265
6292
  }
6266
6293
  setIsSending(false);
@@ -6279,7 +6306,7 @@ function useChat(options = {}) {
6279
6306
  );
6280
6307
  const react = React14.useCallback(
6281
6308
  async (message, emoji) => {
6282
- if (!client || message.id.startsWith("opt_")) return false;
6309
+ if (!client || isOptimistic(message)) return false;
6283
6310
  const result = await client.support.reactToMessage(message.id, emoji);
6284
6311
  if (result.ok) appendMessages([result.value]);
6285
6312
  return result.ok;
@@ -9263,6 +9290,7 @@ function SlotPicker({
9263
9290
  hideElapsedSlots = true,
9264
9291
  minLeadMinutes = 0,
9265
9292
  emptyMessage = "No available slots",
9293
+ contactPhone,
9266
9294
  className,
9267
9295
  classNames
9268
9296
  }) {
@@ -9295,13 +9323,20 @@ function SlotPicker({
9295
9323
  );
9296
9324
  }
9297
9325
  if (slots.length === 0) {
9298
- return /* @__PURE__ */ jsxRuntime.jsx(
9326
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9299
9327
  "div",
9300
9328
  {
9301
9329
  "data-cimplify-slot-picker": true,
9302
9330
  "data-empty": true,
9303
9331
  className: cn(className, classNames?.root, classNames?.empty),
9304
- children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: emptyMessage })
9332
+ children: [
9333
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: emptyMessage }),
9334
+ contactPhone ? /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsxs("a", { href: `tel:${contactPhone.replace(/\s+/g, "")}`, children: [
9335
+ "Call ",
9336
+ contactPhone,
9337
+ " to book"
9338
+ ] }) }) : null
9339
+ ]
9305
9340
  }
9306
9341
  );
9307
9342
  }
@@ -9428,6 +9463,7 @@ function DateSlotPicker({
9428
9463
  durationValue,
9429
9464
  hideElapsedSlots = true,
9430
9465
  minLeadMinutes = 0,
9466
+ contactPhone,
9431
9467
  className,
9432
9468
  classNames
9433
9469
  }) {
@@ -9577,7 +9613,8 @@ function DateSlotPicker({
9577
9613
  durationUnit,
9578
9614
  durationValue,
9579
9615
  hideElapsedSlots,
9580
- minLeadMinutes
9616
+ minLeadMinutes,
9617
+ contactPhone
9581
9618
  }
9582
9619
  ) })
9583
9620
  ]
@@ -10443,7 +10480,7 @@ function CartLineItemRow({
10443
10480
  });
10444
10481
  const hasComposite = item.compositeSelections && item.compositeSelections.length > 0;
10445
10482
  const hasBundle = item.bundleSelections && item.bundleSelections.length > 0;
10446
- const isOptimistic = item.isOptimistic === true;
10483
+ const isOptimistic2 = item.isOptimistic === true;
10447
10484
  const lineTotal = chunkPTPOQVXS_js.parsePrice(unitPrice) * item.quantity;
10448
10485
  const imageUrl = item.product.image_url;
10449
10486
  const variantLabel2 = item.variant ? chunkHKN3YG76_js.getVariantDisplayName(item.variant, item.product.name) : null;
@@ -10451,10 +10488,10 @@ function CartLineItemRow({
10451
10488
  "div",
10452
10489
  {
10453
10490
  "data-cimplify-cart-item": true,
10454
- "data-optimistic": isOptimistic ? "true" : void 0,
10491
+ "data-optimistic": isOptimistic2 ? "true" : void 0,
10455
10492
  className: cn(
10456
10493
  "group relative flex gap-3 py-4 border-b border-border last:border-b-0 transition-opacity",
10457
- isOptimistic && "opacity-60",
10494
+ isOptimistic2 && "opacity-60",
10458
10495
  classNames?.item
10459
10496
  ),
10460
10497
  children: [
@@ -10621,7 +10658,7 @@ function CartSummary({
10621
10658
  tax,
10622
10659
  total,
10623
10660
  isEmpty,
10624
- isOptimistic,
10661
+ isOptimistic: isOptimistic2,
10625
10662
  pendingOpCount,
10626
10663
  removeItem,
10627
10664
  updateQuantity
@@ -10647,7 +10684,7 @@ function CartSummary({
10647
10684
  "div",
10648
10685
  {
10649
10686
  "data-cimplify-cart-summary": true,
10650
- "data-optimistic": isOptimistic ? "true" : void 0,
10687
+ "data-optimistic": isOptimistic2 ? "true" : void 0,
10651
10688
  "data-pending-ops": pendingOpCount > 0 ? pendingOpCount : void 0,
10652
10689
  className: cn(className, classNames?.root),
10653
10690
  children: isEmpty ? /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cimplify-cart-empty": true, className: classNames?.empty, children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: emptyMessage }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -17133,25 +17170,12 @@ var ChatMarkdown = React14.memo(function ChatMarkdown2({
17133
17170
  }
17134
17171
  return /* @__PURE__ */ jsxRuntime.jsx(SettledMarkdown, { text, className });
17135
17172
  });
17136
-
17137
- // src/react/chat-model.ts
17138
- function partitionMessages(messages) {
17139
- const bubbles = [];
17140
- const reactionsByTarget = /* @__PURE__ */ new Map();
17141
- for (const message of messages) {
17142
- if (message.content_type === "reaction") {
17143
- const targetId = message.reply_to?.id;
17144
- if (!targetId) continue;
17145
- const list = reactionsByTarget.get(targetId);
17146
- if (list) list.push(message);
17147
- else reactionsByTarget.set(targetId, [message]);
17148
- continue;
17149
- }
17150
- bubbles.push(message);
17151
- }
17152
- return { bubbles, reactionsByTarget };
17153
- }
17154
17173
  var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
17174
+ var KEEP_DISMISSED_KEY = "cimplify_keep_chat_dismissed";
17175
+ var AT_BOTTOM_SLACK_PX = 48;
17176
+ function proseOf(content) {
17177
+ return chunkXREIZ56K_js.splitGenUIContent(content).filter((segment) => segment.kind === "prose").map((segment) => segment.text).join("\n\n").trim();
17178
+ }
17155
17179
  function ChatWidget({
17156
17180
  client: clientProp,
17157
17181
  businessName = "Support",
@@ -17170,6 +17194,7 @@ function ChatWidget({
17170
17194
  const [open, setOpen] = React14.useState(defaultOpen);
17171
17195
  const [readAt, setReadAt] = React14.useState(null);
17172
17196
  const [attachError, setAttachError] = React14.useState(null);
17197
+ const [replyTo, setReplyTo] = React14.useState(null);
17173
17198
  const {
17174
17199
  messages,
17175
17200
  isLoading,
@@ -17178,11 +17203,15 @@ function ChatWidget({
17178
17203
  seenAt,
17179
17204
  error,
17180
17205
  isActive,
17206
+ humanRequested,
17181
17207
  claimedAs,
17182
17208
  signOut,
17183
17209
  send,
17184
17210
  react,
17211
+ requestHuman,
17185
17212
  startConversation,
17213
+ loadOlder,
17214
+ mayHaveOlder,
17186
17215
  openIfExisting
17187
17216
  } = useChat({ client, pollInterval });
17188
17217
  const showTyping = isSending || awaitingReply;
@@ -17217,9 +17246,11 @@ function ChatWidget({
17217
17246
  const handleSend = React14.useCallback(
17218
17247
  (content) => {
17219
17248
  setAttachError(null);
17220
- return send(content);
17249
+ const target = replyTo;
17250
+ setReplyTo(null);
17251
+ return send(content, target ? { replyTo: target } : void 0);
17221
17252
  },
17222
- [send]
17253
+ [send, replyTo]
17223
17254
  );
17224
17255
  const unread = open ? 0 : messages.filter(
17225
17256
  (m) => m.sender_type !== "customer" && (!readAt || m.created_at > readAt)
@@ -17258,6 +17289,7 @@ function ChatWidget({
17258
17289
  open && /* @__PURE__ */ jsxRuntime.jsx(
17259
17290
  Panel,
17260
17291
  {
17292
+ client,
17261
17293
  businessName,
17262
17294
  greeting,
17263
17295
  subtitle,
@@ -17271,11 +17303,18 @@ function ChatWidget({
17271
17303
  error: error ?? attachError,
17272
17304
  isActive,
17273
17305
  isLeft,
17306
+ humanRequested,
17307
+ mayHaveOlder,
17308
+ replyTo,
17274
17309
  claimedAs,
17275
17310
  onSignOut: () => void signOut(),
17276
17311
  onClose: handleClose,
17277
17312
  onSend: handleSend,
17278
17313
  onReact: react,
17314
+ onReply: setReplyTo,
17315
+ onCancelReply: () => setReplyTo(null),
17316
+ onRequestHuman: () => void requestHuman(),
17317
+ onLoadOlder: () => void loadOlder(),
17279
17318
  onAttach: attachPhoto,
17280
17319
  onStartConversation: startConversation,
17281
17320
  classNames
@@ -17310,7 +17349,7 @@ function Bubble({
17310
17349
  {
17311
17350
  className: cn(
17312
17351
  "absolute -right-0.5 -top-0.5 flex h-5 w-5 items-center justify-center",
17313
- "rounded-full bg-red-500 text-[10px] font-bold text-white",
17352
+ "rounded-full bg-destructive text-[10px] font-bold text-white",
17314
17353
  "ring-2 ring-background"
17315
17354
  ),
17316
17355
  children: unread > 9 ? "9+" : unread
@@ -17321,6 +17360,7 @@ function Bubble({
17321
17360
  );
17322
17361
  }
17323
17362
  function Panel({
17363
+ client,
17324
17364
  businessName,
17325
17365
  greeting,
17326
17366
  subtitle,
@@ -17334,11 +17374,18 @@ function Panel({
17334
17374
  error,
17335
17375
  isActive,
17336
17376
  isLeft,
17377
+ humanRequested,
17378
+ mayHaveOlder,
17379
+ replyTo,
17337
17380
  claimedAs,
17338
17381
  onSignOut,
17339
17382
  onClose,
17340
17383
  onSend,
17341
17384
  onReact,
17385
+ onReply,
17386
+ onCancelReply,
17387
+ onRequestHuman,
17388
+ onLoadOlder,
17342
17389
  onAttach,
17343
17390
  onStartConversation,
17344
17391
  classNames
@@ -17378,20 +17425,56 @@ function Panel({
17378
17425
  MessageList,
17379
17426
  {
17380
17427
  messages,
17428
+ businessName,
17381
17429
  isLoading,
17382
17430
  isSending: isTyping,
17383
17431
  seenAt,
17432
+ humanRequested,
17433
+ mayHaveOlder,
17384
17434
  onReact,
17435
+ onReply,
17436
+ onRequestHuman,
17437
+ onLoadOlder,
17385
17438
  className: classNames?.messages
17386
17439
  }
17387
17440
  ),
17388
- error != null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-red-200 bg-red-50 px-4 py-2 text-xs text-red-600", children: error.userMessage || "Something went wrong. Please try again." }),
17441
+ isActive && !claimedAs && client && /* @__PURE__ */ jsxRuntime.jsx(KeepConversation, { client }),
17442
+ humanRequested && isActive && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border-t border-border bg-muted/50 px-4 py-2 text-[11px] text-muted-foreground", children: [
17443
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-1.5 w-1.5 flex-shrink-0 animate-pulse rounded-full bg-amber-500" }),
17444
+ "Waiting for ",
17445
+ businessName,
17446
+ " \u2014 usually replies in minutes"
17447
+ ] }),
17448
+ error != null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-destructive/20 bg-destructive/10 px-4 py-2 text-xs text-destructive", children: error.userMessage || "Something went wrong. Please try again." }),
17449
+ replyTo && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 border-t border-border bg-muted/50 px-4 py-2", children: [
17450
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-[3px] shrink-0 self-stretch rounded-full bg-foreground/40" }),
17451
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
17452
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-[11px] font-semibold text-foreground", children: [
17453
+ "Replying to ",
17454
+ replyTo.sender_type === "customer" ? "yourself" : businessName
17455
+ ] }),
17456
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-[11px] text-muted-foreground", children: proseOf(replyTo.content) || "Photo" })
17457
+ ] }),
17458
+ /* @__PURE__ */ jsxRuntime.jsx(
17459
+ "button",
17460
+ {
17461
+ type: "button",
17462
+ "aria-label": "Cancel reply",
17463
+ onClick: onCancelReply,
17464
+ className: "flex-shrink-0 p-0.5 text-muted-foreground hover:text-foreground",
17465
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { className: "h-3.5 w-3.5" })
17466
+ }
17467
+ )
17468
+ ] }),
17389
17469
  /* @__PURE__ */ jsxRuntime.jsx(
17390
17470
  ChatInput,
17391
17471
  {
17392
17472
  placeholder,
17393
17473
  isSending,
17394
- onSend: isActive ? onSend : onStartConversation,
17474
+ onSend: isActive ? onSend : async (text) => {
17475
+ await onStartConversation(text);
17476
+ return true;
17477
+ },
17395
17478
  onAttach,
17396
17479
  className: classNames?.input
17397
17480
  }
@@ -17420,10 +17503,10 @@ function Header({
17420
17503
  className
17421
17504
  ),
17422
17505
  children: [
17423
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] bg-white/15", children: /* @__PURE__ */ jsxRuntime.jsx(ChatIcon, { className: "h-5 w-5" }) }),
17506
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] bg-background/15", children: /* @__PURE__ */ jsxRuntime.jsx(ChatIcon, { className: "h-5 w-5" }) }),
17424
17507
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
17425
17508
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-[15px] font-semibold leading-tight", children: businessName }),
17426
- claimedAs ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 truncate text-xs text-white/60", children: [
17509
+ claimedAs ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 truncate text-xs text-background/60", children: [
17427
17510
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
17428
17511
  "Chatting as ",
17429
17512
  claimedAs
@@ -17434,11 +17517,11 @@ function Header({
17434
17517
  {
17435
17518
  type: "button",
17436
17519
  onClick: onSignOut,
17437
- className: "flex-shrink-0 underline underline-offset-2 hover:text-white",
17520
+ className: "flex-shrink-0 underline underline-offset-2 hover:text-background",
17438
17521
  children: "Not you?"
17439
17522
  }
17440
17523
  )
17441
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-white/60", children: [
17524
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-background/60", children: [
17442
17525
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-[7px] w-[7px] flex-shrink-0 rounded-full bg-emerald-400" }),
17443
17526
  "Online"
17444
17527
  ] })
@@ -17451,7 +17534,7 @@ function Header({
17451
17534
  "aria-label": "Close chat",
17452
17535
  className: cn(
17453
17536
  "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg",
17454
- "bg-white/10 text-white/70 transition-colors hover:bg-white/20 hover:text-white"
17537
+ "bg-background/10 text-background/70 transition-colors hover:bg-background/20 hover:text-background"
17455
17538
  ),
17456
17539
  children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { className: "h-[18px] w-[18px]" })
17457
17540
  }
@@ -17514,32 +17597,73 @@ function lastSeenCustomerMessageId(bubbles, seenAt) {
17514
17597
  }
17515
17598
  return null;
17516
17599
  }
17600
+ function dayLabel(at) {
17601
+ const date = new Date(at);
17602
+ const today = /* @__PURE__ */ new Date();
17603
+ if (date.toDateString() === today.toDateString()) return "Today";
17604
+ const yesterday = new Date(today);
17605
+ yesterday.setDate(today.getDate() - 1);
17606
+ if (date.toDateString() === yesterday.toDateString()) return "Yesterday";
17607
+ return date.toLocaleDateString([], { weekday: "short", day: "numeric", month: "short" });
17608
+ }
17517
17609
  function MessageList({
17518
17610
  messages,
17611
+ businessName,
17519
17612
  isLoading,
17520
17613
  isSending,
17521
17614
  seenAt,
17615
+ humanRequested,
17616
+ mayHaveOlder,
17522
17617
  onReact,
17618
+ onReply,
17619
+ onRequestHuman,
17620
+ onLoadOlder,
17523
17621
  className
17524
17622
  }) {
17525
- const { bubbles, reactionsByTarget } = partitionMessages(messages);
17623
+ const scrolledUpRef = React14.useRef(false);
17624
+ const partitioned = partitionMessages(messages);
17625
+ const bubbles = partitioned.bubbles.filter(
17626
+ (m) => m.sender_type === "system" || m.attachments.length > 0 || (m.sender_type === "customer" ? m.content.trim() !== "" : proseOf(m.content) !== "")
17627
+ );
17628
+ const reactionsByTarget = partitioned.reactionsByTarget;
17526
17629
  const seenMessageId = lastSeenCustomerMessageId(bubbles, seenAt);
17630
+ const lastBubble = bubbles[bubbles.length - 1];
17631
+ const offerHuman = !humanRequested && lastBubble?.sender_type === "bot" && !isSending;
17632
+ const handleScroll = (event) => {
17633
+ const node = event.currentTarget;
17634
+ scrolledUpRef.current = node.scrollHeight - node.scrollTop - node.clientHeight >= AT_BOTTOM_SLACK_PX;
17635
+ };
17527
17636
  return /* @__PURE__ */ jsxRuntime.jsxs(
17528
17637
  "div",
17529
17638
  {
17639
+ onScroll: handleScroll,
17530
17640
  className: cn(
17531
17641
  "flex flex-1 flex-col gap-1 overflow-y-auto bg-muted/50 px-5 py-5",
17532
17642
  className
17533
17643
  ),
17534
17644
  children: [
17535
17645
  isLoading && messages.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-1 items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(TypingDots, {}) }),
17646
+ mayHaveOlder && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center pb-2", children: /* @__PURE__ */ jsxRuntime.jsx(
17647
+ "button",
17648
+ {
17649
+ type: "button",
17650
+ onClick: onLoadOlder,
17651
+ className: "rounded-full border border-border bg-background px-3 py-1 text-[11px] font-semibold text-muted-foreground hover:text-foreground",
17652
+ children: "Show earlier messages"
17653
+ }
17654
+ ) }),
17536
17655
  bubbles.map((msg, i) => {
17537
17656
  const isCustomer = msg.sender_type === "customer";
17538
17657
  const prev = bubbles[i - 1];
17539
17658
  const sameSenderAsPrev = prev?.sender_type === msg.sender_type;
17540
- const isOptimistic = msg.id.startsWith("opt_");
17659
+ const optimistic = isOptimistic(msg);
17541
17660
  const reactions = reactionsByTarget.get(msg.id);
17661
+ const newDay = !prev || !sameDay(prev.created_at, msg.created_at);
17662
+ if (msg.sender_type === "system") {
17663
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-2 text-center text-[11px] font-medium text-muted-foreground", children: msg.content }, msg.id);
17664
+ }
17542
17665
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
17666
+ newDay && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-2 text-center text-[10px] font-semibold tracking-wide text-muted-foreground", children: dayLabel(msg.created_at) }),
17543
17667
  !sameSenderAsPrev && i > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3" }),
17544
17668
  /* @__PURE__ */ jsxRuntime.jsxs(
17545
17669
  "div",
@@ -17549,17 +17673,7 @@ function MessageList({
17549
17673
  isCustomer ? "ml-auto" : "mr-auto"
17550
17674
  ),
17551
17675
  children: [
17552
- !isCustomer && !isOptimistic && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -top-3 right-0 z-10 hidden gap-0.5 rounded-full border border-border bg-background px-1 py-0.5 shadow-sm group-hover:flex", children: QUICK_REACTIONS.map((emoji) => /* @__PURE__ */ jsxRuntime.jsx(
17553
- "button",
17554
- {
17555
- type: "button",
17556
- "aria-label": `React ${emoji}`,
17557
- onClick: () => void onReact(msg, emoji),
17558
- className: "flex h-6 w-6 items-center justify-center rounded-full text-[13px] hover:bg-muted",
17559
- children: emoji
17560
- },
17561
- emoji
17562
- )) }),
17676
+ !isCustomer && !optimistic && /* @__PURE__ */ jsxRuntime.jsx(HoverRail, { message: msg, onReact, onReply }),
17563
17677
  /* @__PURE__ */ jsxRuntime.jsxs(
17564
17678
  "div",
17565
17679
  {
@@ -17567,13 +17681,10 @@ function MessageList({
17567
17681
  "rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed",
17568
17682
  isCustomer ? "bg-foreground text-background" : "border border-border bg-background text-foreground",
17569
17683
  isCustomer ? sameSenderAsPrev ? "rounded-tr" : "rounded-br" : sameSenderAsPrev ? "rounded-tl" : "rounded-bl",
17570
- isOptimistic && "opacity-70"
17684
+ optimistic && "opacity-70"
17571
17685
  ),
17572
17686
  children: [
17573
- !isCustomer && !sameSenderAsPrev && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mb-1 inline-flex items-center gap-1 rounded bg-violet-100 px-1.5 py-0.5 text-[10px] font-semibold text-violet-600", children: [
17574
- /* @__PURE__ */ jsxRuntime.jsx(SparkleIcon, { className: "h-[11px] w-[11px]" }),
17575
- "AI"
17576
- ] }),
17687
+ !sameSenderAsPrev && /* @__PURE__ */ jsxRuntime.jsx(SenderChip, { sender: msg.sender_type, businessName }),
17577
17688
  msg.reply_to && /* @__PURE__ */ jsxRuntime.jsxs(
17578
17689
  "div",
17579
17690
  {
@@ -17621,7 +17732,7 @@ function MessageList({
17621
17732
  isCustomer ? msg.content && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-pre-wrap", children: msg.content }) : /* @__PURE__ */ jsxRuntime.jsx(
17622
17733
  ChatMarkdown,
17623
17734
  {
17624
- text: msg.content,
17735
+ text: proseOf(msg.content),
17625
17736
  className: "[&_p]:my-0 [&_p+p]:mt-1.5 [&_ul]:my-1 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:my-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:my-0.5 [&_h1]:text-sm [&_h1]:font-bold [&_h2]:text-sm [&_h2]:font-bold [&_h3]:text-sm [&_h3]:font-semibold [&_hr]:my-2 [&_table]:my-1 [&_table]:w-full [&_th]:pb-1 [&_th]:text-left [&_th]:text-[10px] [&_th]:font-semibold [&_th]:uppercase [&_td]:py-1 [&_td:last-child]:text-right [&_td:last-child]:font-semibold [&_td:last-child]:tabular-nums [&_a]:underline"
17626
17737
  }
17627
17738
  )
@@ -17643,11 +17754,20 @@ function MessageList({
17643
17754
  ] }, msg.id);
17644
17755
  }),
17645
17756
  isSending && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mr-auto mt-1", children: /* @__PURE__ */ jsxRuntime.jsx(TypingDots, {}) }),
17757
+ offerHuman && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsxRuntime.jsx(
17758
+ "button",
17759
+ {
17760
+ type: "button",
17761
+ onClick: onRequestHuman,
17762
+ className: "rounded-full border border-border bg-background px-3 py-1.5 text-xs font-semibold text-foreground hover:border-foreground",
17763
+ children: "Talk to a person"
17764
+ }
17765
+ ) }),
17646
17766
  /* @__PURE__ */ jsxRuntime.jsx(
17647
17767
  "div",
17648
17768
  {
17649
17769
  ref: (node) => {
17650
- node?.scrollIntoView({ behavior: "smooth" });
17770
+ if (node && !scrolledUpRef.current) node.scrollIntoView({ behavior: "smooth" });
17651
17771
  }
17652
17772
  },
17653
17773
  `${messages.length}:${isSending}`
@@ -17656,6 +17776,190 @@ function MessageList({
17656
17776
  }
17657
17777
  );
17658
17778
  }
17779
+ function sameDay(a, b) {
17780
+ return new Date(a).toDateString() === new Date(b).toDateString();
17781
+ }
17782
+ function SenderChip({
17783
+ sender,
17784
+ businessName
17785
+ }) {
17786
+ if (sender === "bot") {
17787
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mb-1 inline-flex items-center gap-1 rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold text-primary", children: [
17788
+ /* @__PURE__ */ jsxRuntime.jsx(SparkleIcon, { className: "h-[11px] w-[11px]" }),
17789
+ "AI"
17790
+ ] });
17791
+ }
17792
+ if (sender === "agent") {
17793
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mb-1 inline-flex items-center rounded bg-muted px-1.5 py-0.5 text-[10px] font-semibold text-muted-foreground", children: businessName });
17794
+ }
17795
+ return null;
17796
+ }
17797
+ function HoverRail({
17798
+ message,
17799
+ onReact,
17800
+ onReply
17801
+ }) {
17802
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute -top-3 right-0 z-10 hidden gap-0.5 rounded-full border border-border bg-background px-1 py-0.5 shadow-sm group-hover:flex", children: [
17803
+ QUICK_REACTIONS.map((emoji) => /* @__PURE__ */ jsxRuntime.jsx(
17804
+ "button",
17805
+ {
17806
+ type: "button",
17807
+ "aria-label": `React ${emoji}`,
17808
+ onClick: () => void onReact(message, emoji),
17809
+ className: "flex h-6 w-6 items-center justify-center rounded-full text-[13px] hover:bg-muted",
17810
+ children: emoji
17811
+ },
17812
+ emoji
17813
+ )),
17814
+ /* @__PURE__ */ jsxRuntime.jsx(
17815
+ "button",
17816
+ {
17817
+ type: "button",
17818
+ "aria-label": "Reply",
17819
+ onClick: () => onReply(message),
17820
+ className: "flex h-6 w-6 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground",
17821
+ children: /* @__PURE__ */ jsxRuntime.jsx(ReplyIcon, { className: "h-3.5 w-3.5" })
17822
+ }
17823
+ )
17824
+ ] });
17825
+ }
17826
+ function readKeepDismissed() {
17827
+ try {
17828
+ return typeof window !== "undefined" && window.localStorage.getItem(KEEP_DISMISSED_KEY) === "1";
17829
+ } catch {
17830
+ return false;
17831
+ }
17832
+ }
17833
+ function writeKeepDismissed() {
17834
+ try {
17835
+ window.localStorage.setItem(KEEP_DISMISSED_KEY, "1");
17836
+ } catch {
17837
+ }
17838
+ }
17839
+ function KeepConversation({ client }) {
17840
+ const [step, setStep] = React14.useState("row");
17841
+ const [dismissed, setDismissed] = React14.useState(readKeepDismissed);
17842
+ const [contact, setContact] = React14.useState("");
17843
+ const [code, setCode] = React14.useState("");
17844
+ const [busy, setBusy] = React14.useState(false);
17845
+ const [verifyError, setVerifyError] = React14.useState(null);
17846
+ if (dismissed) return null;
17847
+ const contactType = contact.includes("@") ? "email" : "phone";
17848
+ const requestCode = async () => {
17849
+ const trimmed = contact.trim();
17850
+ if (!trimmed || busy) return;
17851
+ setBusy(true);
17852
+ setVerifyError(null);
17853
+ const result = await client.link.requestOtp({ contact: trimmed, contact_type: contactType });
17854
+ setBusy(false);
17855
+ if (result.ok) {
17856
+ setCode("");
17857
+ setStep("code");
17858
+ } else {
17859
+ setVerifyError(result.error.userMessage || "Couldn't send a code \u2014 try again.");
17860
+ }
17861
+ };
17862
+ const verify = async (value) => {
17863
+ if (value.length !== 6 || busy) return;
17864
+ setBusy(true);
17865
+ setVerifyError(null);
17866
+ const result = await client.link.verifyOtp({
17867
+ contact: contact.trim(),
17868
+ contact_type: contactType,
17869
+ otp_code: value
17870
+ });
17871
+ setBusy(false);
17872
+ if (!result.ok) {
17873
+ setCode("");
17874
+ setVerifyError(result.error.userMessage || "Invalid or expired code.");
17875
+ }
17876
+ };
17877
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-shrink-0 border-t border-border bg-muted/50 px-4 py-2.5", children: [
17878
+ step === "row" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5", children: [
17879
+ /* @__PURE__ */ jsxRuntime.jsxs(
17880
+ "button",
17881
+ {
17882
+ type: "button",
17883
+ onClick: () => setStep("contact"),
17884
+ className: "flex min-w-0 flex-1 flex-col text-left",
17885
+ children: [
17886
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-foreground", children: "Keep this conversation" }),
17887
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: "Verify your number and it follows you \u2014 any device" })
17888
+ ]
17889
+ }
17890
+ ),
17891
+ /* @__PURE__ */ jsxRuntime.jsx(
17892
+ "button",
17893
+ {
17894
+ type: "button",
17895
+ "aria-label": "Dismiss",
17896
+ onClick: () => {
17897
+ writeKeepDismissed();
17898
+ setDismissed(true);
17899
+ },
17900
+ className: "flex-shrink-0 p-1 text-muted-foreground hover:text-foreground",
17901
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { className: "h-3.5 w-3.5" })
17902
+ }
17903
+ )
17904
+ ] }),
17905
+ step === "contact" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
17906
+ /* @__PURE__ */ jsxRuntime.jsx(
17907
+ "input",
17908
+ {
17909
+ type: "text",
17910
+ inputMode: "email",
17911
+ value: contact,
17912
+ onChange: (e) => setContact(e.target.value),
17913
+ onKeyDown: (e) => e.key === "Enter" && void requestCode(),
17914
+ placeholder: "Phone or email",
17915
+ autoFocus: true,
17916
+ className: "h-9 min-w-0 flex-1 rounded-lg border border-border bg-background px-3 text-xs text-foreground outline-none placeholder:text-muted-foreground focus:border-foreground"
17917
+ }
17918
+ ),
17919
+ /* @__PURE__ */ jsxRuntime.jsx(
17920
+ "button",
17921
+ {
17922
+ type: "button",
17923
+ onClick: () => void requestCode(),
17924
+ disabled: !contact.trim() || busy,
17925
+ className: "h-9 flex-shrink-0 rounded-lg bg-foreground px-3 text-xs font-semibold text-background disabled:opacity-50",
17926
+ children: busy ? "Sending\u2026" : "Send code"
17927
+ }
17928
+ )
17929
+ ] }),
17930
+ step === "code" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
17931
+ /* @__PURE__ */ jsxRuntime.jsx(
17932
+ "input",
17933
+ {
17934
+ type: "text",
17935
+ inputMode: "numeric",
17936
+ autoComplete: "one-time-code",
17937
+ maxLength: 6,
17938
+ value: code,
17939
+ onChange: (e) => {
17940
+ const digits = e.target.value.replace(/\D/g, "").slice(0, 6);
17941
+ setCode(digits);
17942
+ if (digits.length === 6) void verify(digits);
17943
+ },
17944
+ placeholder: "6-digit code",
17945
+ autoFocus: true,
17946
+ className: "h-9 min-w-0 flex-1 rounded-lg border border-border bg-background px-3 text-xs tracking-[0.2em] text-foreground outline-none placeholder:tracking-normal placeholder:text-muted-foreground focus:border-foreground"
17947
+ }
17948
+ ),
17949
+ /* @__PURE__ */ jsxRuntime.jsx(
17950
+ "button",
17951
+ {
17952
+ type: "button",
17953
+ onClick: () => void requestCode(),
17954
+ disabled: busy,
17955
+ className: "h-9 flex-shrink-0 rounded-lg px-2 text-[11px] font-medium text-muted-foreground hover:text-foreground disabled:opacity-50",
17956
+ children: "Resend"
17957
+ }
17958
+ )
17959
+ ] }),
17960
+ verifyError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1.5 text-[11px] text-destructive", children: verifyError })
17961
+ ] });
17962
+ }
17659
17963
  function ChatInput({
17660
17964
  placeholder,
17661
17965
  isSending,
@@ -17678,14 +17982,15 @@ function ChatInput({
17678
17982
  },
17679
17983
  [onAttach]
17680
17984
  );
17681
- const handleSend = React14.useCallback(() => {
17985
+ const handleSend = React14.useCallback(async () => {
17682
17986
  const trimmed = value.trim();
17683
17987
  if (!trimmed || isSending) return;
17684
17988
  setValue("");
17685
17989
  if (textareaRef.current) {
17686
17990
  textareaRef.current.style.height = "auto";
17687
17991
  }
17688
- onSend(trimmed);
17992
+ const sent = await onSend(trimmed);
17993
+ if (!sent) setValue((current) => current || trimmed);
17689
17994
  }, [value, isSending, onSend]);
17690
17995
  const handleKeyDown = React14.useCallback(
17691
17996
  (e) => {
@@ -17863,6 +18168,24 @@ function ImageIcon({ className }) {
17863
18168
  }
17864
18169
  );
17865
18170
  }
18171
+ function ReplyIcon({ className }) {
18172
+ return /* @__PURE__ */ jsxRuntime.jsxs(
18173
+ "svg",
18174
+ {
18175
+ className,
18176
+ viewBox: "0 0 24 24",
18177
+ fill: "none",
18178
+ stroke: "currentColor",
18179
+ strokeWidth: 2,
18180
+ strokeLinecap: "round",
18181
+ strokeLinejoin: "round",
18182
+ children: [
18183
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 17 4 12 9 7" }),
18184
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 18v-2a4 4 0 0 0-4-4H4" })
18185
+ ]
18186
+ }
18187
+ );
18188
+ }
17866
18189
  function SparkleIcon({ className }) {
17867
18190
  return /* @__PURE__ */ jsxRuntime.jsx(
17868
18191
  "svg",