@cimplify/sdk 0.96.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;
@@ -10429,7 +10456,7 @@ function CartLineItemRow({
10429
10456
  });
10430
10457
  const hasComposite = item.compositeSelections && item.compositeSelections.length > 0;
10431
10458
  const hasBundle = item.bundleSelections && item.bundleSelections.length > 0;
10432
- const isOptimistic = item.isOptimistic === true;
10459
+ const isOptimistic2 = item.isOptimistic === true;
10433
10460
  const lineTotal = parsePrice(unitPrice) * item.quantity;
10434
10461
  const imageUrl = item.product.image_url;
10435
10462
  const variantLabel2 = item.variant ? getVariantDisplayName(item.variant, item.product.name) : null;
@@ -10437,10 +10464,10 @@ function CartLineItemRow({
10437
10464
  "div",
10438
10465
  {
10439
10466
  "data-cimplify-cart-item": true,
10440
- "data-optimistic": isOptimistic ? "true" : void 0,
10467
+ "data-optimistic": isOptimistic2 ? "true" : void 0,
10441
10468
  className: cn(
10442
10469
  "group relative flex gap-3 py-4 border-b border-border last:border-b-0 transition-opacity",
10443
- isOptimistic && "opacity-60",
10470
+ isOptimistic2 && "opacity-60",
10444
10471
  classNames?.item
10445
10472
  ),
10446
10473
  children: [
@@ -10607,7 +10634,7 @@ function CartSummary({
10607
10634
  tax,
10608
10635
  total,
10609
10636
  isEmpty,
10610
- isOptimistic,
10637
+ isOptimistic: isOptimistic2,
10611
10638
  pendingOpCount,
10612
10639
  removeItem,
10613
10640
  updateQuantity
@@ -10633,7 +10660,7 @@ function CartSummary({
10633
10660
  "div",
10634
10661
  {
10635
10662
  "data-cimplify-cart-summary": true,
10636
- "data-optimistic": isOptimistic ? "true" : void 0,
10663
+ "data-optimistic": isOptimistic2 ? "true" : void 0,
10637
10664
  "data-pending-ops": pendingOpCount > 0 ? pendingOpCount : void 0,
10638
10665
  className: cn(className, classNames?.root),
10639
10666
  children: isEmpty ? /* @__PURE__ */ jsx("div", { "data-cimplify-cart-empty": true, className: classNames?.empty, children: /* @__PURE__ */ jsx("p", { children: emptyMessage }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -17119,25 +17146,12 @@ var ChatMarkdown = memo(function ChatMarkdown2({
17119
17146
  }
17120
17147
  return /* @__PURE__ */ jsx(SettledMarkdown, { text, className });
17121
17148
  });
17122
-
17123
- // src/react/chat-model.ts
17124
- function partitionMessages(messages) {
17125
- const bubbles = [];
17126
- const reactionsByTarget = /* @__PURE__ */ new Map();
17127
- for (const message of messages) {
17128
- if (message.content_type === "reaction") {
17129
- const targetId = message.reply_to?.id;
17130
- if (!targetId) continue;
17131
- const list = reactionsByTarget.get(targetId);
17132
- if (list) list.push(message);
17133
- else reactionsByTarget.set(targetId, [message]);
17134
- continue;
17135
- }
17136
- bubbles.push(message);
17137
- }
17138
- return { bubbles, reactionsByTarget };
17139
- }
17140
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
+ }
17141
17155
  function ChatWidget({
17142
17156
  client: clientProp,
17143
17157
  businessName = "Support",
@@ -17156,6 +17170,7 @@ function ChatWidget({
17156
17170
  const [open, setOpen] = useState(defaultOpen);
17157
17171
  const [readAt, setReadAt] = useState(null);
17158
17172
  const [attachError, setAttachError] = useState(null);
17173
+ const [replyTo, setReplyTo] = useState(null);
17159
17174
  const {
17160
17175
  messages,
17161
17176
  isLoading,
@@ -17164,11 +17179,15 @@ function ChatWidget({
17164
17179
  seenAt,
17165
17180
  error,
17166
17181
  isActive,
17182
+ humanRequested,
17167
17183
  claimedAs,
17168
17184
  signOut,
17169
17185
  send,
17170
17186
  react,
17187
+ requestHuman,
17171
17188
  startConversation,
17189
+ loadOlder,
17190
+ mayHaveOlder,
17172
17191
  openIfExisting
17173
17192
  } = useChat({ client, pollInterval });
17174
17193
  const showTyping = isSending || awaitingReply;
@@ -17203,9 +17222,11 @@ function ChatWidget({
17203
17222
  const handleSend = useCallback(
17204
17223
  (content) => {
17205
17224
  setAttachError(null);
17206
- return send(content);
17225
+ const target = replyTo;
17226
+ setReplyTo(null);
17227
+ return send(content, target ? { replyTo: target } : void 0);
17207
17228
  },
17208
- [send]
17229
+ [send, replyTo]
17209
17230
  );
17210
17231
  const unread = open ? 0 : messages.filter(
17211
17232
  (m) => m.sender_type !== "customer" && (!readAt || m.created_at > readAt)
@@ -17244,6 +17265,7 @@ function ChatWidget({
17244
17265
  open && /* @__PURE__ */ jsx(
17245
17266
  Panel,
17246
17267
  {
17268
+ client,
17247
17269
  businessName,
17248
17270
  greeting,
17249
17271
  subtitle,
@@ -17257,11 +17279,18 @@ function ChatWidget({
17257
17279
  error: error ?? attachError,
17258
17280
  isActive,
17259
17281
  isLeft,
17282
+ humanRequested,
17283
+ mayHaveOlder,
17284
+ replyTo,
17260
17285
  claimedAs,
17261
17286
  onSignOut: () => void signOut(),
17262
17287
  onClose: handleClose,
17263
17288
  onSend: handleSend,
17264
17289
  onReact: react,
17290
+ onReply: setReplyTo,
17291
+ onCancelReply: () => setReplyTo(null),
17292
+ onRequestHuman: () => void requestHuman(),
17293
+ onLoadOlder: () => void loadOlder(),
17265
17294
  onAttach: attachPhoto,
17266
17295
  onStartConversation: startConversation,
17267
17296
  classNames
@@ -17296,7 +17325,7 @@ function Bubble({
17296
17325
  {
17297
17326
  className: cn(
17298
17327
  "absolute -right-0.5 -top-0.5 flex h-5 w-5 items-center justify-center",
17299
- "rounded-full bg-red-500 text-[10px] font-bold text-white",
17328
+ "rounded-full bg-destructive text-[10px] font-bold text-white",
17300
17329
  "ring-2 ring-background"
17301
17330
  ),
17302
17331
  children: unread > 9 ? "9+" : unread
@@ -17307,6 +17336,7 @@ function Bubble({
17307
17336
  );
17308
17337
  }
17309
17338
  function Panel({
17339
+ client,
17310
17340
  businessName,
17311
17341
  greeting,
17312
17342
  subtitle,
@@ -17320,11 +17350,18 @@ function Panel({
17320
17350
  error,
17321
17351
  isActive,
17322
17352
  isLeft,
17353
+ humanRequested,
17354
+ mayHaveOlder,
17355
+ replyTo,
17323
17356
  claimedAs,
17324
17357
  onSignOut,
17325
17358
  onClose,
17326
17359
  onSend,
17327
17360
  onReact,
17361
+ onReply,
17362
+ onCancelReply,
17363
+ onRequestHuman,
17364
+ onLoadOlder,
17328
17365
  onAttach,
17329
17366
  onStartConversation,
17330
17367
  classNames
@@ -17364,20 +17401,56 @@ function Panel({
17364
17401
  MessageList,
17365
17402
  {
17366
17403
  messages,
17404
+ businessName,
17367
17405
  isLoading,
17368
17406
  isSending: isTyping,
17369
17407
  seenAt,
17408
+ humanRequested,
17409
+ mayHaveOlder,
17370
17410
  onReact,
17411
+ onReply,
17412
+ onRequestHuman,
17413
+ onLoadOlder,
17371
17414
  className: classNames?.messages
17372
17415
  }
17373
17416
  ),
17374
- 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
+ ] }),
17375
17445
  /* @__PURE__ */ jsx(
17376
17446
  ChatInput,
17377
17447
  {
17378
17448
  placeholder,
17379
17449
  isSending,
17380
- onSend: isActive ? onSend : onStartConversation,
17450
+ onSend: isActive ? onSend : async (text) => {
17451
+ await onStartConversation(text);
17452
+ return true;
17453
+ },
17381
17454
  onAttach,
17382
17455
  className: classNames?.input
17383
17456
  }
@@ -17406,10 +17479,10 @@ function Header({
17406
17479
  className
17407
17480
  ),
17408
17481
  children: [
17409
- /* @__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" }) }),
17410
17483
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
17411
17484
  /* @__PURE__ */ jsx("div", { className: "truncate text-[15px] font-semibold leading-tight", children: businessName }),
17412
- 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: [
17413
17486
  /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
17414
17487
  "Chatting as ",
17415
17488
  claimedAs
@@ -17420,11 +17493,11 @@ function Header({
17420
17493
  {
17421
17494
  type: "button",
17422
17495
  onClick: onSignOut,
17423
- className: "flex-shrink-0 underline underline-offset-2 hover:text-white",
17496
+ className: "flex-shrink-0 underline underline-offset-2 hover:text-background",
17424
17497
  children: "Not you?"
17425
17498
  }
17426
17499
  )
17427
- ] }) : /* @__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: [
17428
17501
  /* @__PURE__ */ jsx("span", { className: "h-[7px] w-[7px] flex-shrink-0 rounded-full bg-emerald-400" }),
17429
17502
  "Online"
17430
17503
  ] })
@@ -17437,7 +17510,7 @@ function Header({
17437
17510
  "aria-label": "Close chat",
17438
17511
  className: cn(
17439
17512
  "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg",
17440
- "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"
17441
17514
  ),
17442
17515
  children: /* @__PURE__ */ jsx(CloseIcon, { className: "h-[18px] w-[18px]" })
17443
17516
  }
@@ -17500,32 +17573,73 @@ function lastSeenCustomerMessageId(bubbles, seenAt) {
17500
17573
  }
17501
17574
  return null;
17502
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
+ }
17503
17585
  function MessageList({
17504
17586
  messages,
17587
+ businessName,
17505
17588
  isLoading,
17506
17589
  isSending,
17507
17590
  seenAt,
17591
+ humanRequested,
17592
+ mayHaveOlder,
17508
17593
  onReact,
17594
+ onReply,
17595
+ onRequestHuman,
17596
+ onLoadOlder,
17509
17597
  className
17510
17598
  }) {
17511
- 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;
17512
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
+ };
17513
17612
  return /* @__PURE__ */ jsxs(
17514
17613
  "div",
17515
17614
  {
17615
+ onScroll: handleScroll,
17516
17616
  className: cn(
17517
17617
  "flex flex-1 flex-col gap-1 overflow-y-auto bg-muted/50 px-5 py-5",
17518
17618
  className
17519
17619
  ),
17520
17620
  children: [
17521
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
+ ) }),
17522
17631
  bubbles.map((msg, i) => {
17523
17632
  const isCustomer = msg.sender_type === "customer";
17524
17633
  const prev = bubbles[i - 1];
17525
17634
  const sameSenderAsPrev = prev?.sender_type === msg.sender_type;
17526
- const isOptimistic = msg.id.startsWith("opt_");
17635
+ const optimistic = isOptimistic(msg);
17527
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
+ }
17528
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) }),
17529
17643
  !sameSenderAsPrev && i > 0 && /* @__PURE__ */ jsx("div", { className: "h-3" }),
17530
17644
  /* @__PURE__ */ jsxs(
17531
17645
  "div",
@@ -17535,17 +17649,7 @@ function MessageList({
17535
17649
  isCustomer ? "ml-auto" : "mr-auto"
17536
17650
  ),
17537
17651
  children: [
17538
- !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(
17539
- "button",
17540
- {
17541
- type: "button",
17542
- "aria-label": `React ${emoji}`,
17543
- onClick: () => void onReact(msg, emoji),
17544
- className: "flex h-6 w-6 items-center justify-center rounded-full text-[13px] hover:bg-muted",
17545
- children: emoji
17546
- },
17547
- emoji
17548
- )) }),
17652
+ !isCustomer && !optimistic && /* @__PURE__ */ jsx(HoverRail, { message: msg, onReact, onReply }),
17549
17653
  /* @__PURE__ */ jsxs(
17550
17654
  "div",
17551
17655
  {
@@ -17553,13 +17657,10 @@ function MessageList({
17553
17657
  "rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed",
17554
17658
  isCustomer ? "bg-foreground text-background" : "border border-border bg-background text-foreground",
17555
17659
  isCustomer ? sameSenderAsPrev ? "rounded-tr" : "rounded-br" : sameSenderAsPrev ? "rounded-tl" : "rounded-bl",
17556
- isOptimistic && "opacity-70"
17660
+ optimistic && "opacity-70"
17557
17661
  ),
17558
17662
  children: [
17559
- !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: [
17560
- /* @__PURE__ */ jsx(SparkleIcon, { className: "h-[11px] w-[11px]" }),
17561
- "AI"
17562
- ] }),
17663
+ !sameSenderAsPrev && /* @__PURE__ */ jsx(SenderChip, { sender: msg.sender_type, businessName }),
17563
17664
  msg.reply_to && /* @__PURE__ */ jsxs(
17564
17665
  "div",
17565
17666
  {
@@ -17607,7 +17708,7 @@ function MessageList({
17607
17708
  isCustomer ? msg.content && /* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap", children: msg.content }) : /* @__PURE__ */ jsx(
17608
17709
  ChatMarkdown,
17609
17710
  {
17610
- text: msg.content,
17711
+ text: proseOf(msg.content),
17611
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"
17612
17713
  }
17613
17714
  )
@@ -17629,11 +17730,20 @@ function MessageList({
17629
17730
  ] }, msg.id);
17630
17731
  }),
17631
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
+ ) }),
17632
17742
  /* @__PURE__ */ jsx(
17633
17743
  "div",
17634
17744
  {
17635
17745
  ref: (node) => {
17636
- node?.scrollIntoView({ behavior: "smooth" });
17746
+ if (node && !scrolledUpRef.current) node.scrollIntoView({ behavior: "smooth" });
17637
17747
  }
17638
17748
  },
17639
17749
  `${messages.length}:${isSending}`
@@ -17642,6 +17752,190 @@ function MessageList({
17642
17752
  }
17643
17753
  );
17644
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
+ }
17645
17939
  function ChatInput({
17646
17940
  placeholder,
17647
17941
  isSending,
@@ -17664,14 +17958,15 @@ function ChatInput({
17664
17958
  },
17665
17959
  [onAttach]
17666
17960
  );
17667
- const handleSend = useCallback(() => {
17961
+ const handleSend = useCallback(async () => {
17668
17962
  const trimmed = value.trim();
17669
17963
  if (!trimmed || isSending) return;
17670
17964
  setValue("");
17671
17965
  if (textareaRef.current) {
17672
17966
  textareaRef.current.style.height = "auto";
17673
17967
  }
17674
- onSend(trimmed);
17968
+ const sent = await onSend(trimmed);
17969
+ if (!sent) setValue((current) => current || trimmed);
17675
17970
  }, [value, isSending, onSend]);
17676
17971
  const handleKeyDown = useCallback(
17677
17972
  (e) => {
@@ -17849,6 +18144,24 @@ function ImageIcon({ className }) {
17849
18144
  }
17850
18145
  );
17851
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
+ }
17852
18165
  function SparkleIcon({ className }) {
17853
18166
  return /* @__PURE__ */ jsx(
17854
18167
  "svg",