@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.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;
@@ -10453,7 +10480,7 @@ function CartLineItemRow({
10453
10480
  });
10454
10481
  const hasComposite = item.compositeSelections && item.compositeSelections.length > 0;
10455
10482
  const hasBundle = item.bundleSelections && item.bundleSelections.length > 0;
10456
- const isOptimistic = item.isOptimistic === true;
10483
+ const isOptimistic2 = item.isOptimistic === true;
10457
10484
  const lineTotal = chunkPTPOQVXS_js.parsePrice(unitPrice) * item.quantity;
10458
10485
  const imageUrl = item.product.image_url;
10459
10486
  const variantLabel2 = item.variant ? chunkHKN3YG76_js.getVariantDisplayName(item.variant, item.product.name) : null;
@@ -10461,10 +10488,10 @@ function CartLineItemRow({
10461
10488
  "div",
10462
10489
  {
10463
10490
  "data-cimplify-cart-item": true,
10464
- "data-optimistic": isOptimistic ? "true" : void 0,
10491
+ "data-optimistic": isOptimistic2 ? "true" : void 0,
10465
10492
  className: cn(
10466
10493
  "group relative flex gap-3 py-4 border-b border-border last:border-b-0 transition-opacity",
10467
- isOptimistic && "opacity-60",
10494
+ isOptimistic2 && "opacity-60",
10468
10495
  classNames?.item
10469
10496
  ),
10470
10497
  children: [
@@ -10631,7 +10658,7 @@ function CartSummary({
10631
10658
  tax,
10632
10659
  total,
10633
10660
  isEmpty,
10634
- isOptimistic,
10661
+ isOptimistic: isOptimistic2,
10635
10662
  pendingOpCount,
10636
10663
  removeItem,
10637
10664
  updateQuantity
@@ -10657,7 +10684,7 @@ function CartSummary({
10657
10684
  "div",
10658
10685
  {
10659
10686
  "data-cimplify-cart-summary": true,
10660
- "data-optimistic": isOptimistic ? "true" : void 0,
10687
+ "data-optimistic": isOptimistic2 ? "true" : void 0,
10661
10688
  "data-pending-ops": pendingOpCount > 0 ? pendingOpCount : void 0,
10662
10689
  className: cn(className, classNames?.root),
10663
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: [
@@ -17143,25 +17170,12 @@ var ChatMarkdown = React14.memo(function ChatMarkdown2({
17143
17170
  }
17144
17171
  return /* @__PURE__ */ jsxRuntime.jsx(SettledMarkdown, { text, className });
17145
17172
  });
17146
-
17147
- // src/react/chat-model.ts
17148
- function partitionMessages(messages) {
17149
- const bubbles = [];
17150
- const reactionsByTarget = /* @__PURE__ */ new Map();
17151
- for (const message of messages) {
17152
- if (message.content_type === "reaction") {
17153
- const targetId = message.reply_to?.id;
17154
- if (!targetId) continue;
17155
- const list = reactionsByTarget.get(targetId);
17156
- if (list) list.push(message);
17157
- else reactionsByTarget.set(targetId, [message]);
17158
- continue;
17159
- }
17160
- bubbles.push(message);
17161
- }
17162
- return { bubbles, reactionsByTarget };
17163
- }
17164
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
+ }
17165
17179
  function ChatWidget({
17166
17180
  client: clientProp,
17167
17181
  businessName = "Support",
@@ -17180,6 +17194,7 @@ function ChatWidget({
17180
17194
  const [open, setOpen] = React14.useState(defaultOpen);
17181
17195
  const [readAt, setReadAt] = React14.useState(null);
17182
17196
  const [attachError, setAttachError] = React14.useState(null);
17197
+ const [replyTo, setReplyTo] = React14.useState(null);
17183
17198
  const {
17184
17199
  messages,
17185
17200
  isLoading,
@@ -17188,11 +17203,15 @@ function ChatWidget({
17188
17203
  seenAt,
17189
17204
  error,
17190
17205
  isActive,
17206
+ humanRequested,
17191
17207
  claimedAs,
17192
17208
  signOut,
17193
17209
  send,
17194
17210
  react,
17211
+ requestHuman,
17195
17212
  startConversation,
17213
+ loadOlder,
17214
+ mayHaveOlder,
17196
17215
  openIfExisting
17197
17216
  } = useChat({ client, pollInterval });
17198
17217
  const showTyping = isSending || awaitingReply;
@@ -17227,9 +17246,11 @@ function ChatWidget({
17227
17246
  const handleSend = React14.useCallback(
17228
17247
  (content) => {
17229
17248
  setAttachError(null);
17230
- return send(content);
17249
+ const target = replyTo;
17250
+ setReplyTo(null);
17251
+ return send(content, target ? { replyTo: target } : void 0);
17231
17252
  },
17232
- [send]
17253
+ [send, replyTo]
17233
17254
  );
17234
17255
  const unread = open ? 0 : messages.filter(
17235
17256
  (m) => m.sender_type !== "customer" && (!readAt || m.created_at > readAt)
@@ -17268,6 +17289,7 @@ function ChatWidget({
17268
17289
  open && /* @__PURE__ */ jsxRuntime.jsx(
17269
17290
  Panel,
17270
17291
  {
17292
+ client,
17271
17293
  businessName,
17272
17294
  greeting,
17273
17295
  subtitle,
@@ -17281,11 +17303,18 @@ function ChatWidget({
17281
17303
  error: error ?? attachError,
17282
17304
  isActive,
17283
17305
  isLeft,
17306
+ humanRequested,
17307
+ mayHaveOlder,
17308
+ replyTo,
17284
17309
  claimedAs,
17285
17310
  onSignOut: () => void signOut(),
17286
17311
  onClose: handleClose,
17287
17312
  onSend: handleSend,
17288
17313
  onReact: react,
17314
+ onReply: setReplyTo,
17315
+ onCancelReply: () => setReplyTo(null),
17316
+ onRequestHuman: () => void requestHuman(),
17317
+ onLoadOlder: () => void loadOlder(),
17289
17318
  onAttach: attachPhoto,
17290
17319
  onStartConversation: startConversation,
17291
17320
  classNames
@@ -17320,7 +17349,7 @@ function Bubble({
17320
17349
  {
17321
17350
  className: cn(
17322
17351
  "absolute -right-0.5 -top-0.5 flex h-5 w-5 items-center justify-center",
17323
- "rounded-full bg-red-500 text-[10px] font-bold text-white",
17352
+ "rounded-full bg-destructive text-[10px] font-bold text-white",
17324
17353
  "ring-2 ring-background"
17325
17354
  ),
17326
17355
  children: unread > 9 ? "9+" : unread
@@ -17331,6 +17360,7 @@ function Bubble({
17331
17360
  );
17332
17361
  }
17333
17362
  function Panel({
17363
+ client,
17334
17364
  businessName,
17335
17365
  greeting,
17336
17366
  subtitle,
@@ -17344,11 +17374,18 @@ function Panel({
17344
17374
  error,
17345
17375
  isActive,
17346
17376
  isLeft,
17377
+ humanRequested,
17378
+ mayHaveOlder,
17379
+ replyTo,
17347
17380
  claimedAs,
17348
17381
  onSignOut,
17349
17382
  onClose,
17350
17383
  onSend,
17351
17384
  onReact,
17385
+ onReply,
17386
+ onCancelReply,
17387
+ onRequestHuman,
17388
+ onLoadOlder,
17352
17389
  onAttach,
17353
17390
  onStartConversation,
17354
17391
  classNames
@@ -17388,20 +17425,56 @@ function Panel({
17388
17425
  MessageList,
17389
17426
  {
17390
17427
  messages,
17428
+ businessName,
17391
17429
  isLoading,
17392
17430
  isSending: isTyping,
17393
17431
  seenAt,
17432
+ humanRequested,
17433
+ mayHaveOlder,
17394
17434
  onReact,
17435
+ onReply,
17436
+ onRequestHuman,
17437
+ onLoadOlder,
17395
17438
  className: classNames?.messages
17396
17439
  }
17397
17440
  ),
17398
- 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
+ ] }),
17399
17469
  /* @__PURE__ */ jsxRuntime.jsx(
17400
17470
  ChatInput,
17401
17471
  {
17402
17472
  placeholder,
17403
17473
  isSending,
17404
- onSend: isActive ? onSend : onStartConversation,
17474
+ onSend: isActive ? onSend : async (text) => {
17475
+ await onStartConversation(text);
17476
+ return true;
17477
+ },
17405
17478
  onAttach,
17406
17479
  className: classNames?.input
17407
17480
  }
@@ -17430,10 +17503,10 @@ function Header({
17430
17503
  className
17431
17504
  ),
17432
17505
  children: [
17433
- /* @__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" }) }),
17434
17507
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
17435
17508
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-[15px] font-semibold leading-tight", children: businessName }),
17436
- 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: [
17437
17510
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
17438
17511
  "Chatting as ",
17439
17512
  claimedAs
@@ -17444,11 +17517,11 @@ function Header({
17444
17517
  {
17445
17518
  type: "button",
17446
17519
  onClick: onSignOut,
17447
- className: "flex-shrink-0 underline underline-offset-2 hover:text-white",
17520
+ className: "flex-shrink-0 underline underline-offset-2 hover:text-background",
17448
17521
  children: "Not you?"
17449
17522
  }
17450
17523
  )
17451
- ] }) : /* @__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: [
17452
17525
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-[7px] w-[7px] flex-shrink-0 rounded-full bg-emerald-400" }),
17453
17526
  "Online"
17454
17527
  ] })
@@ -17461,7 +17534,7 @@ function Header({
17461
17534
  "aria-label": "Close chat",
17462
17535
  className: cn(
17463
17536
  "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg",
17464
- "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"
17465
17538
  ),
17466
17539
  children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { className: "h-[18px] w-[18px]" })
17467
17540
  }
@@ -17524,32 +17597,73 @@ function lastSeenCustomerMessageId(bubbles, seenAt) {
17524
17597
  }
17525
17598
  return null;
17526
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
+ }
17527
17609
  function MessageList({
17528
17610
  messages,
17611
+ businessName,
17529
17612
  isLoading,
17530
17613
  isSending,
17531
17614
  seenAt,
17615
+ humanRequested,
17616
+ mayHaveOlder,
17532
17617
  onReact,
17618
+ onReply,
17619
+ onRequestHuman,
17620
+ onLoadOlder,
17533
17621
  className
17534
17622
  }) {
17535
- 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;
17536
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
+ };
17537
17636
  return /* @__PURE__ */ jsxRuntime.jsxs(
17538
17637
  "div",
17539
17638
  {
17639
+ onScroll: handleScroll,
17540
17640
  className: cn(
17541
17641
  "flex flex-1 flex-col gap-1 overflow-y-auto bg-muted/50 px-5 py-5",
17542
17642
  className
17543
17643
  ),
17544
17644
  children: [
17545
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
+ ) }),
17546
17655
  bubbles.map((msg, i) => {
17547
17656
  const isCustomer = msg.sender_type === "customer";
17548
17657
  const prev = bubbles[i - 1];
17549
17658
  const sameSenderAsPrev = prev?.sender_type === msg.sender_type;
17550
- const isOptimistic = msg.id.startsWith("opt_");
17659
+ const optimistic = isOptimistic(msg);
17551
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
+ }
17552
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) }),
17553
17667
  !sameSenderAsPrev && i > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3" }),
17554
17668
  /* @__PURE__ */ jsxRuntime.jsxs(
17555
17669
  "div",
@@ -17559,17 +17673,7 @@ function MessageList({
17559
17673
  isCustomer ? "ml-auto" : "mr-auto"
17560
17674
  ),
17561
17675
  children: [
17562
- !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(
17563
- "button",
17564
- {
17565
- type: "button",
17566
- "aria-label": `React ${emoji}`,
17567
- onClick: () => void onReact(msg, emoji),
17568
- className: "flex h-6 w-6 items-center justify-center rounded-full text-[13px] hover:bg-muted",
17569
- children: emoji
17570
- },
17571
- emoji
17572
- )) }),
17676
+ !isCustomer && !optimistic && /* @__PURE__ */ jsxRuntime.jsx(HoverRail, { message: msg, onReact, onReply }),
17573
17677
  /* @__PURE__ */ jsxRuntime.jsxs(
17574
17678
  "div",
17575
17679
  {
@@ -17577,13 +17681,10 @@ function MessageList({
17577
17681
  "rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed",
17578
17682
  isCustomer ? "bg-foreground text-background" : "border border-border bg-background text-foreground",
17579
17683
  isCustomer ? sameSenderAsPrev ? "rounded-tr" : "rounded-br" : sameSenderAsPrev ? "rounded-tl" : "rounded-bl",
17580
- isOptimistic && "opacity-70"
17684
+ optimistic && "opacity-70"
17581
17685
  ),
17582
17686
  children: [
17583
- !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: [
17584
- /* @__PURE__ */ jsxRuntime.jsx(SparkleIcon, { className: "h-[11px] w-[11px]" }),
17585
- "AI"
17586
- ] }),
17687
+ !sameSenderAsPrev && /* @__PURE__ */ jsxRuntime.jsx(SenderChip, { sender: msg.sender_type, businessName }),
17587
17688
  msg.reply_to && /* @__PURE__ */ jsxRuntime.jsxs(
17588
17689
  "div",
17589
17690
  {
@@ -17631,7 +17732,7 @@ function MessageList({
17631
17732
  isCustomer ? msg.content && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-pre-wrap", children: msg.content }) : /* @__PURE__ */ jsxRuntime.jsx(
17632
17733
  ChatMarkdown,
17633
17734
  {
17634
- text: msg.content,
17735
+ text: proseOf(msg.content),
17635
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"
17636
17737
  }
17637
17738
  )
@@ -17653,11 +17754,20 @@ function MessageList({
17653
17754
  ] }, msg.id);
17654
17755
  }),
17655
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
+ ) }),
17656
17766
  /* @__PURE__ */ jsxRuntime.jsx(
17657
17767
  "div",
17658
17768
  {
17659
17769
  ref: (node) => {
17660
- node?.scrollIntoView({ behavior: "smooth" });
17770
+ if (node && !scrolledUpRef.current) node.scrollIntoView({ behavior: "smooth" });
17661
17771
  }
17662
17772
  },
17663
17773
  `${messages.length}:${isSending}`
@@ -17666,6 +17776,190 @@ function MessageList({
17666
17776
  }
17667
17777
  );
17668
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
+ }
17669
17963
  function ChatInput({
17670
17964
  placeholder,
17671
17965
  isSending,
@@ -17688,14 +17982,15 @@ function ChatInput({
17688
17982
  },
17689
17983
  [onAttach]
17690
17984
  );
17691
- const handleSend = React14.useCallback(() => {
17985
+ const handleSend = React14.useCallback(async () => {
17692
17986
  const trimmed = value.trim();
17693
17987
  if (!trimmed || isSending) return;
17694
17988
  setValue("");
17695
17989
  if (textareaRef.current) {
17696
17990
  textareaRef.current.style.height = "auto";
17697
17991
  }
17698
- onSend(trimmed);
17992
+ const sent = await onSend(trimmed);
17993
+ if (!sent) setValue((current) => current || trimmed);
17699
17994
  }, [value, isSending, onSend]);
17700
17995
  const handleKeyDown = React14.useCallback(
17701
17996
  (e) => {
@@ -17873,6 +18168,24 @@ function ImageIcon({ className }) {
17873
18168
  }
17874
18169
  );
17875
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
+ }
17876
18189
  function SparkleIcon({ className }) {
17877
18190
  return /* @__PURE__ */ jsxRuntime.jsx(
17878
18191
  "svg",