@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.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { DURATION_UNIT, getVariantDisplayName, INPUT_FIELD_TYPE, isMultiDayService, serviceBillingMultiplier, PRODUCT_TYPE, BOUNDARY_KIND, resolveGallery, RENDER_HINT, DEFAULT_CDN_BASE_URL } from './chunk-RRBY35TJ.mjs';
3
3
  export { getVariantDisplayName } from './chunk-RRBY35TJ.mjs';
4
- import { messagePreview } from './chunk-U4MW6PYU.mjs';
4
+ import { messagePreview, splitGenUIContent } from './chunk-U4MW6PYU.mjs';
5
5
  export { messagePreview, splitGenUIContent } from './chunk-U4MW6PYU.mjs';
6
6
  import { SESSION_CLAIMED, SESSION_CLEARED, createCimplifyClient, MUTATION_ERROR } from './chunk-XMDLF5Y6.mjs';
7
7
  import { generateIdempotencyKey, ELEMENT_TYPES, EVENT_TYPES, startSignIn } from './chunk-VUSHG3HA.mjs';
@@ -3091,8 +3091,8 @@ function fmtScheduleWindow(start, end) {
3091
3091
  const startStr = format(s, "EEE, MMM d \xB7 h:mm a");
3092
3092
  const e = end ? parseISO(end) : null;
3093
3093
  if (e && isValid(e)) {
3094
- const sameDay = s.toDateString() === e.toDateString();
3095
- return `${startStr} \u2013 ${format(e, sameDay ? "h:mm a" : "EEE, MMM d \xB7 h:mm a")}`;
3094
+ const sameDay2 = s.toDateString() === e.toDateString();
3095
+ return `${startStr} \u2013 ${format(e, sameDay2 ? "h:mm a" : "EEE, MMM d \xB7 h:mm a")}`;
3096
3096
  }
3097
3097
  return startStr;
3098
3098
  }
@@ -5883,6 +5883,33 @@ function useBootstrap(options = {}) {
5883
5883
  const { data, isLoading, error, refetch } = bootstrapQuery(options);
5884
5884
  return { bootstrap: data, isLoading, error, refetch };
5885
5885
  }
5886
+
5887
+ // src/react/chat-model.ts
5888
+ var OPTIMISTIC_ID_PREFIX = "opt_";
5889
+ function optimisticId(clientId) {
5890
+ return `${OPTIMISTIC_ID_PREFIX}${clientId}`;
5891
+ }
5892
+ function isOptimistic(message) {
5893
+ return message.id.startsWith(OPTIMISTIC_ID_PREFIX);
5894
+ }
5895
+ function partitionMessages(messages) {
5896
+ const bubbles = [];
5897
+ const reactionsByTarget = /* @__PURE__ */ new Map();
5898
+ for (const message of messages) {
5899
+ if (message.content_type === "reaction") {
5900
+ const targetId = message.reply_to?.id;
5901
+ if (!targetId) continue;
5902
+ const list = reactionsByTarget.get(targetId);
5903
+ if (list) list.push(message);
5904
+ else reactionsByTarget.set(targetId, [message]);
5905
+ continue;
5906
+ }
5907
+ bubbles.push(message);
5908
+ }
5909
+ return { bubbles, reactionsByTarget };
5910
+ }
5911
+
5912
+ // src/react/hooks/use-chat.ts
5886
5913
  var POLL_INTERVAL_MS = 3e3;
5887
5914
  var POLL_IDLE_INTERVAL_MS = 15e3;
5888
5915
  var IDLE_TIMEOUT_MS = 6e4;
@@ -5897,10 +5924,10 @@ function mergeMessages(prev, incoming) {
5897
5924
  (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
5898
5925
  );
5899
5926
  const confirmed = new Set(
5900
- merged.filter((m) => !m.id.startsWith("opt_") && m.client_id).map((m) => m.client_id)
5927
+ merged.filter((m) => !isOptimistic(m) && m.client_id).map((m) => m.client_id)
5901
5928
  );
5902
5929
  return merged.filter(
5903
- (m) => !(m.id.startsWith("opt_") && m.client_id && confirmed.has(m.client_id))
5930
+ (m) => !(isOptimistic(m) && m.client_id && confirmed.has(m.client_id))
5904
5931
  );
5905
5932
  }
5906
5933
  function useChat(options = {}) {
@@ -5963,7 +5990,7 @@ function useChat(options = {}) {
5963
5990
  const lastConfirmedMessage = useCallback(() => {
5964
5991
  const list = messagesRef.current;
5965
5992
  for (let i = list.length - 1; i >= 0; i--) {
5966
- if (!list[i].id.startsWith("opt_")) return list[i];
5993
+ if (!isOptimistic(list[i])) return list[i];
5967
5994
  }
5968
5995
  return void 0;
5969
5996
  }, []);
@@ -6011,7 +6038,7 @@ function useChat(options = {}) {
6011
6038
  const snapshotIds = new Set(snapshot.map((m) => m.id));
6012
6039
  const tail = snapshot.length > 0 ? new Date(snapshot[snapshot.length - 1].created_at).getTime() : 0;
6013
6040
  const extras = prev.filter(
6014
- (m) => !snapshotIds.has(m.id) && (m.id.startsWith("opt_") || new Date(m.created_at).getTime() > tail)
6041
+ (m) => !snapshotIds.has(m.id) && (isOptimistic(m) || new Date(m.created_at).getTime() > tail)
6015
6042
  );
6016
6043
  return mergeMessages(snapshot, extras);
6017
6044
  });
@@ -6197,9 +6224,9 @@ function useChat(options = {}) {
6197
6224
  }
6198
6225
  const replyTo = opts?.replyTo;
6199
6226
  const clientId = generateIdempotencyKey();
6200
- const optimisticId = `opt_${clientId}`;
6227
+ const localId = optimisticId(clientId);
6201
6228
  const optimistic = {
6202
- id: optimisticId,
6229
+ id: localId,
6203
6230
  sender_type: "customer",
6204
6231
  content: content.trim(),
6205
6232
  content_type: attachments.length > 0 ? "image" : "text",
@@ -6229,14 +6256,14 @@ function useChat(options = {}) {
6229
6256
  // The websocket may have already streamed the confirmed message
6230
6257
  // in while the POST was in flight; drop the optimistic bubble
6231
6258
  // instead of inserting a duplicate.
6232
- prev.some((m) => m.id === confirmed.id) ? prev.filter((m) => m.id !== optimisticId) : prev.map((m) => m.id === optimisticId ? confirmed : m)
6259
+ prev.some((m) => m.id === confirmed.id) ? prev.filter((m) => m.id !== localId) : prev.map((m) => m.id === localId ? confirmed : m)
6233
6260
  )
6234
6261
  );
6235
6262
  if (aiEnabledRef.current && !humanRequestedRef.current) {
6236
6263
  startAwaitingReply();
6237
6264
  }
6238
6265
  } else {
6239
- setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
6266
+ setMessages((prev) => prev.filter((m) => m.id !== localId));
6240
6267
  setError(result.error);
6241
6268
  }
6242
6269
  setIsSending(false);
@@ -6255,7 +6282,7 @@ function useChat(options = {}) {
6255
6282
  );
6256
6283
  const react = useCallback(
6257
6284
  async (message, emoji) => {
6258
- if (!client || message.id.startsWith("opt_")) return false;
6285
+ if (!client || isOptimistic(message)) return false;
6259
6286
  const result = await client.support.reactToMessage(message.id, emoji);
6260
6287
  if (result.ok) appendMessages([result.value]);
6261
6288
  return result.ok;
@@ -9239,6 +9266,7 @@ function SlotPicker({
9239
9266
  hideElapsedSlots = true,
9240
9267
  minLeadMinutes = 0,
9241
9268
  emptyMessage = "No available slots",
9269
+ contactPhone,
9242
9270
  className,
9243
9271
  classNames
9244
9272
  }) {
@@ -9271,13 +9299,20 @@ function SlotPicker({
9271
9299
  );
9272
9300
  }
9273
9301
  if (slots.length === 0) {
9274
- return /* @__PURE__ */ jsx(
9302
+ return /* @__PURE__ */ jsxs(
9275
9303
  "div",
9276
9304
  {
9277
9305
  "data-cimplify-slot-picker": true,
9278
9306
  "data-empty": true,
9279
9307
  className: cn(className, classNames?.root, classNames?.empty),
9280
- children: /* @__PURE__ */ jsx("p", { children: emptyMessage })
9308
+ children: [
9309
+ /* @__PURE__ */ jsx("p", { children: emptyMessage }),
9310
+ contactPhone ? /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsxs("a", { href: `tel:${contactPhone.replace(/\s+/g, "")}`, children: [
9311
+ "Call ",
9312
+ contactPhone,
9313
+ " to book"
9314
+ ] }) }) : null
9315
+ ]
9281
9316
  }
9282
9317
  );
9283
9318
  }
@@ -9404,6 +9439,7 @@ function DateSlotPicker({
9404
9439
  durationValue,
9405
9440
  hideElapsedSlots = true,
9406
9441
  minLeadMinutes = 0,
9442
+ contactPhone,
9407
9443
  className,
9408
9444
  classNames
9409
9445
  }) {
@@ -9553,7 +9589,8 @@ function DateSlotPicker({
9553
9589
  durationUnit,
9554
9590
  durationValue,
9555
9591
  hideElapsedSlots,
9556
- minLeadMinutes
9592
+ minLeadMinutes,
9593
+ contactPhone
9557
9594
  }
9558
9595
  ) })
9559
9596
  ]
@@ -10419,7 +10456,7 @@ function CartLineItemRow({
10419
10456
  });
10420
10457
  const hasComposite = item.compositeSelections && item.compositeSelections.length > 0;
10421
10458
  const hasBundle = item.bundleSelections && item.bundleSelections.length > 0;
10422
- const isOptimistic = item.isOptimistic === true;
10459
+ const isOptimistic2 = item.isOptimistic === true;
10423
10460
  const lineTotal = parsePrice(unitPrice) * item.quantity;
10424
10461
  const imageUrl = item.product.image_url;
10425
10462
  const variantLabel2 = item.variant ? getVariantDisplayName(item.variant, item.product.name) : null;
@@ -10427,10 +10464,10 @@ function CartLineItemRow({
10427
10464
  "div",
10428
10465
  {
10429
10466
  "data-cimplify-cart-item": true,
10430
- "data-optimistic": isOptimistic ? "true" : void 0,
10467
+ "data-optimistic": isOptimistic2 ? "true" : void 0,
10431
10468
  className: cn(
10432
10469
  "group relative flex gap-3 py-4 border-b border-border last:border-b-0 transition-opacity",
10433
- isOptimistic && "opacity-60",
10470
+ isOptimistic2 && "opacity-60",
10434
10471
  classNames?.item
10435
10472
  ),
10436
10473
  children: [
@@ -10597,7 +10634,7 @@ function CartSummary({
10597
10634
  tax,
10598
10635
  total,
10599
10636
  isEmpty,
10600
- isOptimistic,
10637
+ isOptimistic: isOptimistic2,
10601
10638
  pendingOpCount,
10602
10639
  removeItem,
10603
10640
  updateQuantity
@@ -10623,7 +10660,7 @@ function CartSummary({
10623
10660
  "div",
10624
10661
  {
10625
10662
  "data-cimplify-cart-summary": true,
10626
- "data-optimistic": isOptimistic ? "true" : void 0,
10663
+ "data-optimistic": isOptimistic2 ? "true" : void 0,
10627
10664
  "data-pending-ops": pendingOpCount > 0 ? pendingOpCount : void 0,
10628
10665
  className: cn(className, classNames?.root),
10629
10666
  children: isEmpty ? /* @__PURE__ */ jsx("div", { "data-cimplify-cart-empty": true, className: classNames?.empty, children: /* @__PURE__ */ jsx("p", { children: emptyMessage }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -17109,25 +17146,12 @@ var ChatMarkdown = memo(function ChatMarkdown2({
17109
17146
  }
17110
17147
  return /* @__PURE__ */ jsx(SettledMarkdown, { text, className });
17111
17148
  });
17112
-
17113
- // src/react/chat-model.ts
17114
- function partitionMessages(messages) {
17115
- const bubbles = [];
17116
- const reactionsByTarget = /* @__PURE__ */ new Map();
17117
- for (const message of messages) {
17118
- if (message.content_type === "reaction") {
17119
- const targetId = message.reply_to?.id;
17120
- if (!targetId) continue;
17121
- const list = reactionsByTarget.get(targetId);
17122
- if (list) list.push(message);
17123
- else reactionsByTarget.set(targetId, [message]);
17124
- continue;
17125
- }
17126
- bubbles.push(message);
17127
- }
17128
- return { bubbles, reactionsByTarget };
17129
- }
17130
17149
  var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
17150
+ var KEEP_DISMISSED_KEY = "cimplify_keep_chat_dismissed";
17151
+ var AT_BOTTOM_SLACK_PX = 48;
17152
+ function proseOf(content) {
17153
+ return splitGenUIContent(content).filter((segment) => segment.kind === "prose").map((segment) => segment.text).join("\n\n").trim();
17154
+ }
17131
17155
  function ChatWidget({
17132
17156
  client: clientProp,
17133
17157
  businessName = "Support",
@@ -17146,6 +17170,7 @@ function ChatWidget({
17146
17170
  const [open, setOpen] = useState(defaultOpen);
17147
17171
  const [readAt, setReadAt] = useState(null);
17148
17172
  const [attachError, setAttachError] = useState(null);
17173
+ const [replyTo, setReplyTo] = useState(null);
17149
17174
  const {
17150
17175
  messages,
17151
17176
  isLoading,
@@ -17154,11 +17179,15 @@ function ChatWidget({
17154
17179
  seenAt,
17155
17180
  error,
17156
17181
  isActive,
17182
+ humanRequested,
17157
17183
  claimedAs,
17158
17184
  signOut,
17159
17185
  send,
17160
17186
  react,
17187
+ requestHuman,
17161
17188
  startConversation,
17189
+ loadOlder,
17190
+ mayHaveOlder,
17162
17191
  openIfExisting
17163
17192
  } = useChat({ client, pollInterval });
17164
17193
  const showTyping = isSending || awaitingReply;
@@ -17193,9 +17222,11 @@ function ChatWidget({
17193
17222
  const handleSend = useCallback(
17194
17223
  (content) => {
17195
17224
  setAttachError(null);
17196
- return send(content);
17225
+ const target = replyTo;
17226
+ setReplyTo(null);
17227
+ return send(content, target ? { replyTo: target } : void 0);
17197
17228
  },
17198
- [send]
17229
+ [send, replyTo]
17199
17230
  );
17200
17231
  const unread = open ? 0 : messages.filter(
17201
17232
  (m) => m.sender_type !== "customer" && (!readAt || m.created_at > readAt)
@@ -17234,6 +17265,7 @@ function ChatWidget({
17234
17265
  open && /* @__PURE__ */ jsx(
17235
17266
  Panel,
17236
17267
  {
17268
+ client,
17237
17269
  businessName,
17238
17270
  greeting,
17239
17271
  subtitle,
@@ -17247,11 +17279,18 @@ function ChatWidget({
17247
17279
  error: error ?? attachError,
17248
17280
  isActive,
17249
17281
  isLeft,
17282
+ humanRequested,
17283
+ mayHaveOlder,
17284
+ replyTo,
17250
17285
  claimedAs,
17251
17286
  onSignOut: () => void signOut(),
17252
17287
  onClose: handleClose,
17253
17288
  onSend: handleSend,
17254
17289
  onReact: react,
17290
+ onReply: setReplyTo,
17291
+ onCancelReply: () => setReplyTo(null),
17292
+ onRequestHuman: () => void requestHuman(),
17293
+ onLoadOlder: () => void loadOlder(),
17255
17294
  onAttach: attachPhoto,
17256
17295
  onStartConversation: startConversation,
17257
17296
  classNames
@@ -17286,7 +17325,7 @@ function Bubble({
17286
17325
  {
17287
17326
  className: cn(
17288
17327
  "absolute -right-0.5 -top-0.5 flex h-5 w-5 items-center justify-center",
17289
- "rounded-full bg-red-500 text-[10px] font-bold text-white",
17328
+ "rounded-full bg-destructive text-[10px] font-bold text-white",
17290
17329
  "ring-2 ring-background"
17291
17330
  ),
17292
17331
  children: unread > 9 ? "9+" : unread
@@ -17297,6 +17336,7 @@ function Bubble({
17297
17336
  );
17298
17337
  }
17299
17338
  function Panel({
17339
+ client,
17300
17340
  businessName,
17301
17341
  greeting,
17302
17342
  subtitle,
@@ -17310,11 +17350,18 @@ function Panel({
17310
17350
  error,
17311
17351
  isActive,
17312
17352
  isLeft,
17353
+ humanRequested,
17354
+ mayHaveOlder,
17355
+ replyTo,
17313
17356
  claimedAs,
17314
17357
  onSignOut,
17315
17358
  onClose,
17316
17359
  onSend,
17317
17360
  onReact,
17361
+ onReply,
17362
+ onCancelReply,
17363
+ onRequestHuman,
17364
+ onLoadOlder,
17318
17365
  onAttach,
17319
17366
  onStartConversation,
17320
17367
  classNames
@@ -17354,20 +17401,56 @@ function Panel({
17354
17401
  MessageList,
17355
17402
  {
17356
17403
  messages,
17404
+ businessName,
17357
17405
  isLoading,
17358
17406
  isSending: isTyping,
17359
17407
  seenAt,
17408
+ humanRequested,
17409
+ mayHaveOlder,
17360
17410
  onReact,
17411
+ onReply,
17412
+ onRequestHuman,
17413
+ onLoadOlder,
17361
17414
  className: classNames?.messages
17362
17415
  }
17363
17416
  ),
17364
- error != null && /* @__PURE__ */ 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." }),
17417
+ isActive && !claimedAs && client && /* @__PURE__ */ jsx(KeepConversation, { client }),
17418
+ humanRequested && isActive && /* @__PURE__ */ 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: [
17419
+ /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 flex-shrink-0 animate-pulse rounded-full bg-amber-500" }),
17420
+ "Waiting for ",
17421
+ businessName,
17422
+ " \u2014 usually replies in minutes"
17423
+ ] }),
17424
+ error != null && /* @__PURE__ */ 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." }),
17425
+ replyTo && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 border-t border-border bg-muted/50 px-4 py-2", children: [
17426
+ /* @__PURE__ */ jsx("span", { className: "w-[3px] shrink-0 self-stretch rounded-full bg-foreground/40" }),
17427
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
17428
+ /* @__PURE__ */ jsxs("div", { className: "text-[11px] font-semibold text-foreground", children: [
17429
+ "Replying to ",
17430
+ replyTo.sender_type === "customer" ? "yourself" : businessName
17431
+ ] }),
17432
+ /* @__PURE__ */ jsx("div", { className: "truncate text-[11px] text-muted-foreground", children: proseOf(replyTo.content) || "Photo" })
17433
+ ] }),
17434
+ /* @__PURE__ */ jsx(
17435
+ "button",
17436
+ {
17437
+ type: "button",
17438
+ "aria-label": "Cancel reply",
17439
+ onClick: onCancelReply,
17440
+ className: "flex-shrink-0 p-0.5 text-muted-foreground hover:text-foreground",
17441
+ children: /* @__PURE__ */ jsx(CloseIcon, { className: "h-3.5 w-3.5" })
17442
+ }
17443
+ )
17444
+ ] }),
17365
17445
  /* @__PURE__ */ jsx(
17366
17446
  ChatInput,
17367
17447
  {
17368
17448
  placeholder,
17369
17449
  isSending,
17370
- onSend: isActive ? onSend : onStartConversation,
17450
+ onSend: isActive ? onSend : async (text) => {
17451
+ await onStartConversation(text);
17452
+ return true;
17453
+ },
17371
17454
  onAttach,
17372
17455
  className: classNames?.input
17373
17456
  }
@@ -17396,10 +17479,10 @@ function Header({
17396
17479
  className
17397
17480
  ),
17398
17481
  children: [
17399
- /* @__PURE__ */ jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] bg-white/15", children: /* @__PURE__ */ jsx(ChatIcon, { className: "h-5 w-5" }) }),
17482
+ /* @__PURE__ */ jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] bg-background/15", children: /* @__PURE__ */ jsx(ChatIcon, { className: "h-5 w-5" }) }),
17400
17483
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
17401
17484
  /* @__PURE__ */ jsx("div", { className: "truncate text-[15px] font-semibold leading-tight", children: businessName }),
17402
- claimedAs ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 truncate text-xs text-white/60", children: [
17485
+ claimedAs ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 truncate text-xs text-background/60", children: [
17403
17486
  /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
17404
17487
  "Chatting as ",
17405
17488
  claimedAs
@@ -17410,11 +17493,11 @@ function Header({
17410
17493
  {
17411
17494
  type: "button",
17412
17495
  onClick: onSignOut,
17413
- className: "flex-shrink-0 underline underline-offset-2 hover:text-white",
17496
+ className: "flex-shrink-0 underline underline-offset-2 hover:text-background",
17414
17497
  children: "Not you?"
17415
17498
  }
17416
17499
  )
17417
- ] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-white/60", children: [
17500
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-background/60", children: [
17418
17501
  /* @__PURE__ */ jsx("span", { className: "h-[7px] w-[7px] flex-shrink-0 rounded-full bg-emerald-400" }),
17419
17502
  "Online"
17420
17503
  ] })
@@ -17427,7 +17510,7 @@ function Header({
17427
17510
  "aria-label": "Close chat",
17428
17511
  className: cn(
17429
17512
  "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg",
17430
- "bg-white/10 text-white/70 transition-colors hover:bg-white/20 hover:text-white"
17513
+ "bg-background/10 text-background/70 transition-colors hover:bg-background/20 hover:text-background"
17431
17514
  ),
17432
17515
  children: /* @__PURE__ */ jsx(CloseIcon, { className: "h-[18px] w-[18px]" })
17433
17516
  }
@@ -17490,32 +17573,73 @@ function lastSeenCustomerMessageId(bubbles, seenAt) {
17490
17573
  }
17491
17574
  return null;
17492
17575
  }
17576
+ function dayLabel(at) {
17577
+ const date = new Date(at);
17578
+ const today = /* @__PURE__ */ new Date();
17579
+ if (date.toDateString() === today.toDateString()) return "Today";
17580
+ const yesterday = new Date(today);
17581
+ yesterday.setDate(today.getDate() - 1);
17582
+ if (date.toDateString() === yesterday.toDateString()) return "Yesterday";
17583
+ return date.toLocaleDateString([], { weekday: "short", day: "numeric", month: "short" });
17584
+ }
17493
17585
  function MessageList({
17494
17586
  messages,
17587
+ businessName,
17495
17588
  isLoading,
17496
17589
  isSending,
17497
17590
  seenAt,
17591
+ humanRequested,
17592
+ mayHaveOlder,
17498
17593
  onReact,
17594
+ onReply,
17595
+ onRequestHuman,
17596
+ onLoadOlder,
17499
17597
  className
17500
17598
  }) {
17501
- const { bubbles, reactionsByTarget } = partitionMessages(messages);
17599
+ const scrolledUpRef = useRef(false);
17600
+ const partitioned = partitionMessages(messages);
17601
+ const bubbles = partitioned.bubbles.filter(
17602
+ (m) => m.sender_type === "system" || m.attachments.length > 0 || (m.sender_type === "customer" ? m.content.trim() !== "" : proseOf(m.content) !== "")
17603
+ );
17604
+ const reactionsByTarget = partitioned.reactionsByTarget;
17502
17605
  const seenMessageId = lastSeenCustomerMessageId(bubbles, seenAt);
17606
+ const lastBubble = bubbles[bubbles.length - 1];
17607
+ const offerHuman = !humanRequested && lastBubble?.sender_type === "bot" && !isSending;
17608
+ const handleScroll = (event) => {
17609
+ const node = event.currentTarget;
17610
+ scrolledUpRef.current = node.scrollHeight - node.scrollTop - node.clientHeight >= AT_BOTTOM_SLACK_PX;
17611
+ };
17503
17612
  return /* @__PURE__ */ jsxs(
17504
17613
  "div",
17505
17614
  {
17615
+ onScroll: handleScroll,
17506
17616
  className: cn(
17507
17617
  "flex flex-1 flex-col gap-1 overflow-y-auto bg-muted/50 px-5 py-5",
17508
17618
  className
17509
17619
  ),
17510
17620
  children: [
17511
17621
  isLoading && messages.length === 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center justify-center", children: /* @__PURE__ */ jsx(TypingDots, {}) }),
17622
+ mayHaveOlder && /* @__PURE__ */ jsx("div", { className: "flex justify-center pb-2", children: /* @__PURE__ */ jsx(
17623
+ "button",
17624
+ {
17625
+ type: "button",
17626
+ onClick: onLoadOlder,
17627
+ className: "rounded-full border border-border bg-background px-3 py-1 text-[11px] font-semibold text-muted-foreground hover:text-foreground",
17628
+ children: "Show earlier messages"
17629
+ }
17630
+ ) }),
17512
17631
  bubbles.map((msg, i) => {
17513
17632
  const isCustomer = msg.sender_type === "customer";
17514
17633
  const prev = bubbles[i - 1];
17515
17634
  const sameSenderAsPrev = prev?.sender_type === msg.sender_type;
17516
- const isOptimistic = msg.id.startsWith("opt_");
17635
+ const optimistic = isOptimistic(msg);
17517
17636
  const reactions = reactionsByTarget.get(msg.id);
17637
+ const newDay = !prev || !sameDay(prev.created_at, msg.created_at);
17638
+ if (msg.sender_type === "system") {
17639
+ return /* @__PURE__ */ jsx("div", { className: "my-2 text-center text-[11px] font-medium text-muted-foreground", children: msg.content }, msg.id);
17640
+ }
17518
17641
  return /* @__PURE__ */ jsxs("div", { children: [
17642
+ newDay && /* @__PURE__ */ jsx("div", { className: "my-2 text-center text-[10px] font-semibold tracking-wide text-muted-foreground", children: dayLabel(msg.created_at) }),
17519
17643
  !sameSenderAsPrev && i > 0 && /* @__PURE__ */ jsx("div", { className: "h-3" }),
17520
17644
  /* @__PURE__ */ jsxs(
17521
17645
  "div",
@@ -17525,17 +17649,7 @@ function MessageList({
17525
17649
  isCustomer ? "ml-auto" : "mr-auto"
17526
17650
  ),
17527
17651
  children: [
17528
- !isCustomer && !isOptimistic && /* @__PURE__ */ 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__ */ jsx(
17529
- "button",
17530
- {
17531
- type: "button",
17532
- "aria-label": `React ${emoji}`,
17533
- onClick: () => void onReact(msg, emoji),
17534
- className: "flex h-6 w-6 items-center justify-center rounded-full text-[13px] hover:bg-muted",
17535
- children: emoji
17536
- },
17537
- emoji
17538
- )) }),
17652
+ !isCustomer && !optimistic && /* @__PURE__ */ jsx(HoverRail, { message: msg, onReact, onReply }),
17539
17653
  /* @__PURE__ */ jsxs(
17540
17654
  "div",
17541
17655
  {
@@ -17543,13 +17657,10 @@ function MessageList({
17543
17657
  "rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed",
17544
17658
  isCustomer ? "bg-foreground text-background" : "border border-border bg-background text-foreground",
17545
17659
  isCustomer ? sameSenderAsPrev ? "rounded-tr" : "rounded-br" : sameSenderAsPrev ? "rounded-tl" : "rounded-bl",
17546
- isOptimistic && "opacity-70"
17660
+ optimistic && "opacity-70"
17547
17661
  ),
17548
17662
  children: [
17549
- !isCustomer && !sameSenderAsPrev && /* @__PURE__ */ 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: [
17550
- /* @__PURE__ */ jsx(SparkleIcon, { className: "h-[11px] w-[11px]" }),
17551
- "AI"
17552
- ] }),
17663
+ !sameSenderAsPrev && /* @__PURE__ */ jsx(SenderChip, { sender: msg.sender_type, businessName }),
17553
17664
  msg.reply_to && /* @__PURE__ */ jsxs(
17554
17665
  "div",
17555
17666
  {
@@ -17597,7 +17708,7 @@ function MessageList({
17597
17708
  isCustomer ? msg.content && /* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap", children: msg.content }) : /* @__PURE__ */ jsx(
17598
17709
  ChatMarkdown,
17599
17710
  {
17600
- text: msg.content,
17711
+ text: proseOf(msg.content),
17601
17712
  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"
17602
17713
  }
17603
17714
  )
@@ -17619,11 +17730,20 @@ function MessageList({
17619
17730
  ] }, msg.id);
17620
17731
  }),
17621
17732
  isSending && /* @__PURE__ */ jsx("div", { className: "mr-auto mt-1", children: /* @__PURE__ */ jsx(TypingDots, {}) }),
17733
+ offerHuman && /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(
17734
+ "button",
17735
+ {
17736
+ type: "button",
17737
+ onClick: onRequestHuman,
17738
+ className: "rounded-full border border-border bg-background px-3 py-1.5 text-xs font-semibold text-foreground hover:border-foreground",
17739
+ children: "Talk to a person"
17740
+ }
17741
+ ) }),
17622
17742
  /* @__PURE__ */ jsx(
17623
17743
  "div",
17624
17744
  {
17625
17745
  ref: (node) => {
17626
- node?.scrollIntoView({ behavior: "smooth" });
17746
+ if (node && !scrolledUpRef.current) node.scrollIntoView({ behavior: "smooth" });
17627
17747
  }
17628
17748
  },
17629
17749
  `${messages.length}:${isSending}`
@@ -17632,6 +17752,190 @@ function MessageList({
17632
17752
  }
17633
17753
  );
17634
17754
  }
17755
+ function sameDay(a, b) {
17756
+ return new Date(a).toDateString() === new Date(b).toDateString();
17757
+ }
17758
+ function SenderChip({
17759
+ sender,
17760
+ businessName
17761
+ }) {
17762
+ if (sender === "bot") {
17763
+ return /* @__PURE__ */ 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: [
17764
+ /* @__PURE__ */ jsx(SparkleIcon, { className: "h-[11px] w-[11px]" }),
17765
+ "AI"
17766
+ ] });
17767
+ }
17768
+ if (sender === "agent") {
17769
+ return /* @__PURE__ */ 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 });
17770
+ }
17771
+ return null;
17772
+ }
17773
+ function HoverRail({
17774
+ message,
17775
+ onReact,
17776
+ onReply
17777
+ }) {
17778
+ return /* @__PURE__ */ 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: [
17779
+ QUICK_REACTIONS.map((emoji) => /* @__PURE__ */ jsx(
17780
+ "button",
17781
+ {
17782
+ type: "button",
17783
+ "aria-label": `React ${emoji}`,
17784
+ onClick: () => void onReact(message, emoji),
17785
+ className: "flex h-6 w-6 items-center justify-center rounded-full text-[13px] hover:bg-muted",
17786
+ children: emoji
17787
+ },
17788
+ emoji
17789
+ )),
17790
+ /* @__PURE__ */ jsx(
17791
+ "button",
17792
+ {
17793
+ type: "button",
17794
+ "aria-label": "Reply",
17795
+ onClick: () => onReply(message),
17796
+ className: "flex h-6 w-6 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground",
17797
+ children: /* @__PURE__ */ jsx(ReplyIcon, { className: "h-3.5 w-3.5" })
17798
+ }
17799
+ )
17800
+ ] });
17801
+ }
17802
+ function readKeepDismissed() {
17803
+ try {
17804
+ return typeof window !== "undefined" && window.localStorage.getItem(KEEP_DISMISSED_KEY) === "1";
17805
+ } catch {
17806
+ return false;
17807
+ }
17808
+ }
17809
+ function writeKeepDismissed() {
17810
+ try {
17811
+ window.localStorage.setItem(KEEP_DISMISSED_KEY, "1");
17812
+ } catch {
17813
+ }
17814
+ }
17815
+ function KeepConversation({ client }) {
17816
+ const [step, setStep] = useState("row");
17817
+ const [dismissed, setDismissed] = useState(readKeepDismissed);
17818
+ const [contact, setContact] = useState("");
17819
+ const [code, setCode] = useState("");
17820
+ const [busy, setBusy] = useState(false);
17821
+ const [verifyError, setVerifyError] = useState(null);
17822
+ if (dismissed) return null;
17823
+ const contactType = contact.includes("@") ? "email" : "phone";
17824
+ const requestCode = async () => {
17825
+ const trimmed = contact.trim();
17826
+ if (!trimmed || busy) return;
17827
+ setBusy(true);
17828
+ setVerifyError(null);
17829
+ const result = await client.link.requestOtp({ contact: trimmed, contact_type: contactType });
17830
+ setBusy(false);
17831
+ if (result.ok) {
17832
+ setCode("");
17833
+ setStep("code");
17834
+ } else {
17835
+ setVerifyError(result.error.userMessage || "Couldn't send a code \u2014 try again.");
17836
+ }
17837
+ };
17838
+ const verify = async (value) => {
17839
+ if (value.length !== 6 || busy) return;
17840
+ setBusy(true);
17841
+ setVerifyError(null);
17842
+ const result = await client.link.verifyOtp({
17843
+ contact: contact.trim(),
17844
+ contact_type: contactType,
17845
+ otp_code: value
17846
+ });
17847
+ setBusy(false);
17848
+ if (!result.ok) {
17849
+ setCode("");
17850
+ setVerifyError(result.error.userMessage || "Invalid or expired code.");
17851
+ }
17852
+ };
17853
+ return /* @__PURE__ */ jsxs("div", { className: "flex-shrink-0 border-t border-border bg-muted/50 px-4 py-2.5", children: [
17854
+ step === "row" && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [
17855
+ /* @__PURE__ */ jsxs(
17856
+ "button",
17857
+ {
17858
+ type: "button",
17859
+ onClick: () => setStep("contact"),
17860
+ className: "flex min-w-0 flex-1 flex-col text-left",
17861
+ children: [
17862
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-foreground", children: "Keep this conversation" }),
17863
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: "Verify your number and it follows you \u2014 any device" })
17864
+ ]
17865
+ }
17866
+ ),
17867
+ /* @__PURE__ */ jsx(
17868
+ "button",
17869
+ {
17870
+ type: "button",
17871
+ "aria-label": "Dismiss",
17872
+ onClick: () => {
17873
+ writeKeepDismissed();
17874
+ setDismissed(true);
17875
+ },
17876
+ className: "flex-shrink-0 p-1 text-muted-foreground hover:text-foreground",
17877
+ children: /* @__PURE__ */ jsx(CloseIcon, { className: "h-3.5 w-3.5" })
17878
+ }
17879
+ )
17880
+ ] }),
17881
+ step === "contact" && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
17882
+ /* @__PURE__ */ jsx(
17883
+ "input",
17884
+ {
17885
+ type: "text",
17886
+ inputMode: "email",
17887
+ value: contact,
17888
+ onChange: (e) => setContact(e.target.value),
17889
+ onKeyDown: (e) => e.key === "Enter" && void requestCode(),
17890
+ placeholder: "Phone or email",
17891
+ autoFocus: true,
17892
+ 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"
17893
+ }
17894
+ ),
17895
+ /* @__PURE__ */ jsx(
17896
+ "button",
17897
+ {
17898
+ type: "button",
17899
+ onClick: () => void requestCode(),
17900
+ disabled: !contact.trim() || busy,
17901
+ className: "h-9 flex-shrink-0 rounded-lg bg-foreground px-3 text-xs font-semibold text-background disabled:opacity-50",
17902
+ children: busy ? "Sending\u2026" : "Send code"
17903
+ }
17904
+ )
17905
+ ] }),
17906
+ step === "code" && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
17907
+ /* @__PURE__ */ jsx(
17908
+ "input",
17909
+ {
17910
+ type: "text",
17911
+ inputMode: "numeric",
17912
+ autoComplete: "one-time-code",
17913
+ maxLength: 6,
17914
+ value: code,
17915
+ onChange: (e) => {
17916
+ const digits = e.target.value.replace(/\D/g, "").slice(0, 6);
17917
+ setCode(digits);
17918
+ if (digits.length === 6) void verify(digits);
17919
+ },
17920
+ placeholder: "6-digit code",
17921
+ autoFocus: true,
17922
+ 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"
17923
+ }
17924
+ ),
17925
+ /* @__PURE__ */ jsx(
17926
+ "button",
17927
+ {
17928
+ type: "button",
17929
+ onClick: () => void requestCode(),
17930
+ disabled: busy,
17931
+ className: "h-9 flex-shrink-0 rounded-lg px-2 text-[11px] font-medium text-muted-foreground hover:text-foreground disabled:opacity-50",
17932
+ children: "Resend"
17933
+ }
17934
+ )
17935
+ ] }),
17936
+ verifyError && /* @__PURE__ */ jsx("p", { className: "mt-1.5 text-[11px] text-destructive", children: verifyError })
17937
+ ] });
17938
+ }
17635
17939
  function ChatInput({
17636
17940
  placeholder,
17637
17941
  isSending,
@@ -17654,14 +17958,15 @@ function ChatInput({
17654
17958
  },
17655
17959
  [onAttach]
17656
17960
  );
17657
- const handleSend = useCallback(() => {
17961
+ const handleSend = useCallback(async () => {
17658
17962
  const trimmed = value.trim();
17659
17963
  if (!trimmed || isSending) return;
17660
17964
  setValue("");
17661
17965
  if (textareaRef.current) {
17662
17966
  textareaRef.current.style.height = "auto";
17663
17967
  }
17664
- onSend(trimmed);
17968
+ const sent = await onSend(trimmed);
17969
+ if (!sent) setValue((current) => current || trimmed);
17665
17970
  }, [value, isSending, onSend]);
17666
17971
  const handleKeyDown = useCallback(
17667
17972
  (e) => {
@@ -17839,6 +18144,24 @@ function ImageIcon({ className }) {
17839
18144
  }
17840
18145
  );
17841
18146
  }
18147
+ function ReplyIcon({ className }) {
18148
+ return /* @__PURE__ */ jsxs(
18149
+ "svg",
18150
+ {
18151
+ className,
18152
+ viewBox: "0 0 24 24",
18153
+ fill: "none",
18154
+ stroke: "currentColor",
18155
+ strokeWidth: 2,
18156
+ strokeLinecap: "round",
18157
+ strokeLinejoin: "round",
18158
+ children: [
18159
+ /* @__PURE__ */ jsx("polyline", { points: "9 17 4 12 9 7" }),
18160
+ /* @__PURE__ */ jsx("path", { d: "M20 18v-2a4 4 0 0 0-4-4H4" })
18161
+ ]
18162
+ }
18163
+ );
18164
+ }
17842
18165
  function SparkleIcon({ className }) {
17843
18166
  return /* @__PURE__ */ jsx(
17844
18167
  "svg",