@cimplify/sdk 0.90.0 → 0.93.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.
Files changed (67) hide show
  1. package/dist/advanced.d.mts +3 -3
  2. package/dist/advanced.d.ts +3 -3
  3. package/dist/advanced.js +20 -20
  4. package/dist/advanced.mjs +1 -1
  5. package/dist/{chunk-KJP4U6L3.js → chunk-6Y7KC5YZ.js} +23 -0
  6. package/dist/{chunk-JVWFIFQ7.js → chunk-AWNWP5J3.js} +4 -4
  7. package/dist/{chunk-2UDEWILL.mjs → chunk-FKARGZK3.mjs} +307 -90
  8. package/dist/{chunk-T7Z25EY4.js → chunk-HKN3YG76.js} +84 -0
  9. package/dist/{chunk-BN4V4ONE.js → chunk-QLGZIBRO.js} +348 -128
  10. package/dist/{chunk-NGE37REW.mjs → chunk-QXWX7CD2.mjs} +2 -2
  11. package/dist/{chunk-OZDDHSIB.mjs → chunk-RRBY35TJ.mjs} +80 -1
  12. package/dist/{chunk-GHFDYM5K.js → chunk-VEOAMBEX.js} +11 -4
  13. package/dist/{chunk-ZU4ZRYUR.mjs → chunk-VUSHG3HA.mjs} +11 -4
  14. package/dist/{chunk-NNGKC3MM.mjs → chunk-X36FZZT6.mjs} +23 -0
  15. package/dist/{client-YiSelM7x.d.mts → client-BTL9d_nN.d.mts} +117 -11
  16. package/dist/{client-CR1f94wY.d.ts → client-BX30FAWZ.d.ts} +117 -11
  17. package/dist/{client-DMttx1xk.d.mts → client-Bhe2TnFW.d.mts} +2 -2
  18. package/dist/{client-CqpOcbin.d.ts → client-DrkuEK_I.d.ts} +2 -2
  19. package/dist/{index-DD9z396M.d.mts → index-BuNjyfaV.d.mts} +3 -3
  20. package/dist/{index-CxTVu0HZ.d.ts → index-D-w5h82t.d.ts} +3 -3
  21. package/dist/index.d.mts +49 -7
  22. package/dist/index.d.ts +49 -7
  23. package/dist/index.js +103 -83
  24. package/dist/index.mjs +5 -5
  25. package/dist/mock/cli.mjs +23 -0
  26. package/dist/mock/library.js +23 -0
  27. package/dist/mock/library.mjs +23 -0
  28. package/dist/mock/msw.js +23 -0
  29. package/dist/mock/msw.mjs +23 -0
  30. package/dist/{payment-BX7ccIKY.d.ts → payment-By0JidTe.d.ts} +1 -1
  31. package/dist/{payment-5d8FTA1q.d.mts → payment-pKPpaORI.d.mts} +1 -1
  32. package/dist/{price-DtfP_Xfq.d.ts → price-23RNXftz.d.ts} +1 -1
  33. package/dist/{price-arl_4Rzn.d.mts → price-BvRtp6ak.d.mts} +1 -1
  34. package/dist/{product-DZX4QuKS.d.ts → product-O8-wcHSe.d.mts} +49 -0
  35. package/dist/{product-DZX4QuKS.d.mts → product-O8-wcHSe.d.ts} +49 -0
  36. package/dist/react.d.mts +79 -6
  37. package/dist/react.d.ts +79 -6
  38. package/dist/react.js +607 -379
  39. package/dist/react.mjs +539 -315
  40. package/dist/{server-BceZMxcK.d.mts → server-BN9ZCAKT.d.mts} +5 -1
  41. package/dist/{server-DoJGSC7L.d.ts → server-DDGbfW_S.d.ts} +5 -1
  42. package/dist/server.d.mts +4 -4
  43. package/dist/server.d.ts +4 -4
  44. package/dist/server.js +7 -7
  45. package/dist/server.mjs +2 -2
  46. package/dist/styles.css +1 -1
  47. package/dist/testing/msw.d.mts +2 -2
  48. package/dist/testing/msw.d.ts +2 -2
  49. package/dist/testing/msw.js +2 -2
  50. package/dist/testing/msw.mjs +1 -1
  51. package/dist/testing/suite.d.mts +6 -6
  52. package/dist/testing/suite.d.ts +6 -6
  53. package/dist/testing/suite.js +23 -23
  54. package/dist/testing/suite.mjs +4 -4
  55. package/dist/testing.d.mts +5 -5
  56. package/dist/testing.d.ts +5 -5
  57. package/dist/testing.js +79 -79
  58. package/dist/testing.mjs +5 -5
  59. package/dist/utils.d.mts +4 -4
  60. package/dist/utils.d.ts +4 -4
  61. package/package.json +1 -1
  62. package/registry/booking-page.json +1 -1
  63. package/registry/chat-widget.json +1 -1
  64. package/registry/customer-input-fields.json +1 -1
  65. package/registry/date-slot-picker.json +1 -1
  66. package/registry/product-customizer.json +1 -1
  67. package/registry/rental-service-card.json +1 -1
package/dist/react.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  "use client";
2
- import { DURATION_UNIT, getVariantDisplayName, INPUT_FIELD_TYPE, PRODUCT_TYPE, BOUNDARY_KIND, resolveGallery, RENDER_HINT, DEFAULT_CDN_BASE_URL } from './chunk-OZDDHSIB.mjs';
3
- export { getVariantDisplayName } from './chunk-OZDDHSIB.mjs';
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
+ export { getVariantDisplayName } from './chunk-RRBY35TJ.mjs';
4
4
  import { messagePreview } from './chunk-U4MW6PYU.mjs';
5
5
  export { messagePreview, splitGenUIContent } from './chunk-U4MW6PYU.mjs';
6
- import { createCimplifyClient } from './chunk-2UDEWILL.mjs';
7
- import { ELEMENT_TYPES, EVENT_TYPES, startSignIn } from './chunk-ZU4ZRYUR.mjs';
6
+ import { SESSION_CLAIMED, SESSION_CLEARED, createCimplifyClient, MUTATION_ERROR } from './chunk-FKARGZK3.mjs';
7
+ import { generateIdempotencyKey, ELEMENT_TYPES, EVENT_TYPES, startSignIn } from './chunk-VUSHG3HA.mjs';
8
8
  import { parsePrice, formatMoney, formatPrice, getProductCurrency, getUnitPriceAtQuantity, isOnSale, getDiscountPercentage, getBasePrice, getPriceRange, formatPriceRange } from './chunk-KFLGQGMC.mjs';
9
- import { isSupportedCurrency } from './chunk-K5EN2W2Q.mjs';
9
+ import { isSupportedCurrency, CimplifyError } from './chunk-K5EN2W2Q.mjs';
10
10
  import './chunk-3G6RQLXK.mjs';
11
11
  import React14, { createContext, lazy, memo, Suspense, useContext, useMemo, useRef, useState, useCallback, useEffect, useId, useSyncExternalStore } from 'react';
12
12
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
@@ -723,6 +723,7 @@ function mapServerCart(serverCart, fallbackCurrency) {
723
723
  scheduledStart: item.scheduled_start || void 0,
724
724
  scheduledEnd: item.scheduled_end || void 0,
725
725
  staffId: item.staff_id || void 0,
726
+ units: item.units || void 0,
726
727
  lineType: item.line_type,
727
728
  confirmationCode: item.confirmation_code || void 0,
728
729
  serviceStatus: item.service_status || void 0,
@@ -781,7 +782,7 @@ async function syncFromServer(entry) {
781
782
  return next;
782
783
  }
783
784
  function emitMutationError(entry, operation, opId, idempotencyKey, error) {
784
- entry.client.events.emit("mutation:error", {
785
+ entry.client.events.emit(MUTATION_ERROR, {
785
786
  service: "cart",
786
787
  operation,
787
788
  opId,
@@ -821,6 +822,12 @@ function getOrCreateStore(params) {
821
822
  syncTail: Promise.resolve(),
822
823
  failedOpRemovers: /* @__PURE__ */ new Set()
823
824
  };
825
+ client.events.on(SESSION_CLAIMED, () => {
826
+ if (entry.initialized) void syncFromServer(entry);
827
+ });
828
+ client.events.on(SESSION_CLEARED, () => {
829
+ if (entry.initialized) void syncFromServer(entry);
830
+ });
824
831
  cartStores.set(storeKey, entry);
825
832
  return entry;
826
833
  }
@@ -879,7 +886,8 @@ async function performAddItem(entry, product, quantity, options) {
879
886
  specialInstructions: normalizedOptions.specialInstructions,
880
887
  scheduledStart: normalizedOptions.scheduledStart,
881
888
  scheduledEnd: normalizedOptions.scheduledEnd,
882
- staffId: normalizedOptions.staffId
889
+ staffId: normalizedOptions.staffId,
890
+ units: normalizedOptions.units
883
891
  });
884
892
  const idempotencyKey = `idem_${typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`}`;
885
893
  const patch = { kind: "add_item", tempItem, lineKey };
@@ -901,6 +909,7 @@ async function performAddItem(entry, product, quantity, options) {
901
909
  scheduled_end: normalizedOptions.scheduledEnd,
902
910
  staff_id: normalizedOptions.staffId,
903
911
  resource_id: normalizedOptions.resourceId,
912
+ units: normalizedOptions.units,
904
913
  customer_inputs: normalizedOptions.customerInputs
905
914
  },
906
915
  { idempotencyKey }
@@ -5454,14 +5463,24 @@ function useServices(options = {}) {
5454
5463
  }
5455
5464
 
5456
5465
  // src/react/hooks/use-available-slots.ts
5466
+ function narrowingUnits(units) {
5467
+ return units != null && units > 1 ? units : void 0;
5468
+ }
5457
5469
  var slotsQuery = createCachedQuery({
5458
5470
  name: "useAvailableSlots",
5459
- key: (client, options) => cacheKey(client.getPublicKey(), options.serviceId, options.date, options.participantCount),
5471
+ key: (client, options) => cacheKey(
5472
+ client.getPublicKey(),
5473
+ options.serviceId,
5474
+ options.date,
5475
+ options.participantCount,
5476
+ narrowingUnits(options.units)
5477
+ ),
5460
5478
  fetch: async (client, options) => {
5461
5479
  const result = await client.scheduling.getAvailableSlots({
5462
5480
  service_id: options.serviceId,
5463
5481
  date: options.date,
5464
- participant_count: options.participantCount
5482
+ participant_count: options.participantCount,
5483
+ units: narrowingUnits(options.units)
5465
5484
  });
5466
5485
  if (!result.ok) throw result.error;
5467
5486
  return result.value;
@@ -5478,6 +5497,9 @@ function useAvailableSlots(serviceId, date, options = {}) {
5478
5497
  }
5479
5498
 
5480
5499
  // src/react/hooks/use-service-availability.ts
5500
+ function narrowingUnits2(units) {
5501
+ return units != null && units > 1 ? units : void 0;
5502
+ }
5481
5503
  var serviceAvailabilityQuery = createCachedQuery({
5482
5504
  name: "useServiceAvailability",
5483
5505
  key: (client, options) => cacheKey(
@@ -5485,7 +5507,8 @@ var serviceAvailabilityQuery = createCachedQuery({
5485
5507
  options.serviceId,
5486
5508
  options.startDate,
5487
5509
  options.endDate,
5488
- options.participantCount
5510
+ options.participantCount,
5511
+ narrowingUnits2(options.units)
5489
5512
  ),
5490
5513
  fetch: async (client, options) => {
5491
5514
  const result = await client.scheduling.getServiceAvailability({
@@ -5493,7 +5516,8 @@ var serviceAvailabilityQuery = createCachedQuery({
5493
5516
  start_date: options.startDate,
5494
5517
  end_date: options.endDate,
5495
5518
  location_id: options.locationId,
5496
- participant_count: options.participantCount
5519
+ participant_count: options.participantCount,
5520
+ units: narrowingUnits2(options.units)
5497
5521
  });
5498
5522
  if (!result.ok) throw result.error;
5499
5523
  return result.value.availability;
@@ -5865,6 +5889,20 @@ var IDLE_TIMEOUT_MS = 6e4;
5865
5889
  var WS_MAX_RECONNECT_ATTEMPTS = 10;
5866
5890
  var WS_PATH = "/api/v1/support/conversation/ws";
5867
5891
  var AWAIT_REPLY_TIMEOUT_MS = 3e4;
5892
+ function mergeMessages(prev, incoming) {
5893
+ const ids = new Set(prev.map((m) => m.id));
5894
+ const fresh = incoming.filter((m) => !ids.has(m.id));
5895
+ if (fresh.length === 0) return prev;
5896
+ const merged = [...prev, ...fresh].sort(
5897
+ (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
5898
+ );
5899
+ const confirmed = new Set(
5900
+ merged.filter((m) => !m.id.startsWith("opt_") && m.client_id).map((m) => m.client_id)
5901
+ );
5902
+ return merged.filter(
5903
+ (m) => !(m.id.startsWith("opt_") && m.client_id && confirmed.has(m.client_id))
5904
+ );
5905
+ }
5868
5906
  function useChat(options = {}) {
5869
5907
  const context = useOptionalCimplifyClient();
5870
5908
  const client = options.client ?? context?.client;
@@ -5877,6 +5915,9 @@ function useChat(options = {}) {
5877
5915
  const [awaitingReply, setAwaitingReply] = useState(false);
5878
5916
  const [seenAt, setSeenAt] = useState(null);
5879
5917
  const [mayHaveOlder, setMayHaveOlder] = useState(false);
5918
+ const [claimedAs, setClaimedAs] = useState(
5919
+ () => client?.support.claimedCustomerName ?? null
5920
+ );
5880
5921
  const pollRef = useRef(null);
5881
5922
  const lastActivityRef = useRef(Date.now());
5882
5923
  const openedRef = useRef(false);
@@ -5888,6 +5929,7 @@ function useChat(options = {}) {
5888
5929
  const aiEnabledRef = useRef(false);
5889
5930
  const humanRequestedRef = useRef(false);
5890
5931
  const awaitReplyTimerRef = useRef(null);
5932
+ const claimUnsubRef = useRef(null);
5891
5933
  messagesRef.current = messages;
5892
5934
  clientRef.current = client;
5893
5935
  humanRequestedRef.current = humanRequested;
@@ -5913,18 +5955,22 @@ function useChat(options = {}) {
5913
5955
  if (incoming.some((m) => m.sender_type === "bot" || m.sender_type === "agent")) {
5914
5956
  stopAwaitingReply();
5915
5957
  }
5916
- setMessages((prev) => {
5917
- const ids = new Set(prev.map((m) => m.id));
5918
- const fresh = incoming.filter((m) => !ids.has(m.id));
5919
- return fresh.length > 0 ? [...prev, ...fresh] : prev;
5920
- });
5958
+ if (incoming.length > 0) lastActivityRef.current = Date.now();
5959
+ setMessages((prev) => mergeMessages(prev, incoming));
5921
5960
  },
5922
5961
  [stopAwaitingReply]
5923
5962
  );
5963
+ const lastConfirmedMessage = useCallback(() => {
5964
+ const list = messagesRef.current;
5965
+ for (let i = list.length - 1; i >= 0; i--) {
5966
+ if (!list[i].id.startsWith("opt_")) return list[i];
5967
+ }
5968
+ return void 0;
5969
+ }, []);
5924
5970
  const fetchNewMessages = useCallback(async () => {
5925
5971
  const c = clientRef.current;
5926
5972
  if (!c) return;
5927
- const last = messagesRef.current[messagesRef.current.length - 1];
5973
+ const last = lastConfirmedMessage();
5928
5974
  const result = await c.support.getMessages({
5929
5975
  after: last?.created_at,
5930
5976
  limit: 50
@@ -5932,29 +5978,61 @@ function useChat(options = {}) {
5932
5978
  if (result.ok && result.value.length > 0) {
5933
5979
  appendMessages(result.value);
5934
5980
  }
5935
- }, [appendMessages]);
5936
- const pollMessages = useCallback(async () => {
5937
- if (Date.now() - lastActivityRef.current > IDLE_TIMEOUT_MS) return;
5938
- await fetchNewMessages();
5939
- }, [fetchNewMessages]);
5981
+ }, [appendMessages, lastConfirmedMessage]);
5940
5982
  const startPolling = useCallback(() => {
5941
5983
  if (pollRef.current) clearInterval(pollRef.current);
5942
- const interval = Date.now() - lastActivityRef.current > IDLE_TIMEOUT_MS ? pollIdleInterval : pollInterval;
5943
- pollRef.current = setInterval(pollMessages, interval);
5944
- }, [pollMessages, pollInterval, pollIdleInterval]);
5984
+ const idle = () => Date.now() - lastActivityRef.current > IDLE_TIMEOUT_MS;
5985
+ let current = idle() ? pollIdleInterval : pollInterval;
5986
+ const tick = () => {
5987
+ void fetchNewMessages().then(() => {
5988
+ const desired = idle() ? pollIdleInterval : pollInterval;
5989
+ if (desired !== current && pollRef.current) {
5990
+ current = desired;
5991
+ clearInterval(pollRef.current);
5992
+ pollRef.current = setInterval(tick, current);
5993
+ }
5994
+ });
5995
+ };
5996
+ pollRef.current = setInterval(tick, current);
5997
+ }, [fetchNewMessages, pollInterval, pollIdleInterval]);
5945
5998
  const stopPolling = useCallback(() => {
5946
5999
  if (pollRef.current) {
5947
6000
  clearInterval(pollRef.current);
5948
6001
  pollRef.current = null;
5949
6002
  }
5950
6003
  }, []);
6004
+ const resyncConversation = useCallback(async () => {
6005
+ const c = clientRef.current;
6006
+ if (!c || !openedRef.current) return;
6007
+ const result = await c.support.openConversation();
6008
+ if (!result.ok) return;
6009
+ const snapshot = result.value.messages;
6010
+ setMessages((prev) => {
6011
+ const snapshotIds = new Set(snapshot.map((m) => m.id));
6012
+ const tail = snapshot.length > 0 ? new Date(snapshot[snapshot.length - 1].created_at).getTime() : 0;
6013
+ const extras = prev.filter(
6014
+ (m) => !snapshotIds.has(m.id) && (m.id.startsWith("opt_") || new Date(m.created_at).getTime() > tail)
6015
+ );
6016
+ return mergeMessages(snapshot, extras);
6017
+ });
6018
+ aiEnabledRef.current = result.value.conversation.ai_enabled;
6019
+ setHumanRequested(result.value.conversation.needs_human_review ?? false);
6020
+ setSeenAt(result.value.conversation.team_read_at ?? null);
6021
+ setMayHaveOlder(result.value.messages.length >= 50);
6022
+ }, []);
5951
6023
  const connectWebSocket = useCallback(() => {
5952
6024
  const c = clientRef.current;
5953
6025
  if (!c || typeof WebSocket === "undefined") {
5954
6026
  startPolling();
5955
6027
  return;
5956
6028
  }
6029
+ if (wsReconnectTimerRef.current) {
6030
+ clearTimeout(wsReconnectTimerRef.current);
6031
+ wsReconnectTimerRef.current = null;
6032
+ }
5957
6033
  if (wsRef.current) {
6034
+ wsRef.current.onclose = null;
6035
+ wsRef.current.onmessage = null;
5958
6036
  wsRef.current.close();
5959
6037
  wsRef.current = null;
5960
6038
  }
@@ -5978,10 +6056,12 @@ function useChat(options = {}) {
5978
6056
  else stopAwaitingReply();
5979
6057
  } else if (envelope.type === "resync") {
5980
6058
  const latest = envelope.latest_message_at;
5981
- const last = messagesRef.current[messagesRef.current.length - 1];
6059
+ const last = lastConfirmedMessage();
5982
6060
  if (latest && (!last || new Date(last.created_at).getTime() < new Date(latest).getTime())) {
5983
6061
  fetchNewMessages();
5984
6062
  }
6063
+ } else if (envelope.type === "rekey") {
6064
+ void resyncConversation();
5985
6065
  }
5986
6066
  } catch {
5987
6067
  }
@@ -5993,6 +6073,7 @@ function useChat(options = {}) {
5993
6073
  if (wsReconnectAttemptRef.current < WS_MAX_RECONNECT_ATTEMPTS) {
5994
6074
  const attempt = wsReconnectAttemptRef.current++;
5995
6075
  const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 3e4);
6076
+ if (wsReconnectTimerRef.current) clearTimeout(wsReconnectTimerRef.current);
5996
6077
  wsReconnectTimerRef.current = setTimeout(connectWebSocket, delay);
5997
6078
  } else {
5998
6079
  startPolling();
@@ -6001,14 +6082,91 @@ function useChat(options = {}) {
6001
6082
  ws.onerror = () => {
6002
6083
  };
6003
6084
  wsRef.current = ws;
6004
- }, [appendMessages, startPolling, stopPolling, fetchNewMessages]);
6085
+ }, [appendMessages, startPolling, stopPolling, fetchNewMessages, resyncConversation, lastConfirmedMessage]);
6086
+ const resetConversation = useCallback(() => {
6087
+ stopPolling();
6088
+ if (wsReconnectTimerRef.current) {
6089
+ clearTimeout(wsReconnectTimerRef.current);
6090
+ wsReconnectTimerRef.current = null;
6091
+ }
6092
+ if (wsRef.current) {
6093
+ wsRef.current.onclose = null;
6094
+ wsRef.current.close();
6095
+ wsRef.current = null;
6096
+ }
6097
+ wsReconnectAttemptRef.current = 0;
6098
+ openedRef.current = false;
6099
+ stopAwaitingReply();
6100
+ setMessages([]);
6101
+ setConversationOpened(false);
6102
+ setHumanRequested(false);
6103
+ setSeenAt(null);
6104
+ setMayHaveOlder(false);
6105
+ setError(null);
6106
+ setClaimedAs(null);
6107
+ }, [stopPolling, stopAwaitingReply]);
6108
+ const hydrateConversation = useCallback(
6109
+ (value) => {
6110
+ const c = clientRef.current;
6111
+ setMessages(value.messages);
6112
+ setConversationOpened(true);
6113
+ aiEnabledRef.current = value.conversation.ai_enabled;
6114
+ setHumanRequested(value.conversation.needs_human_review ?? false);
6115
+ setSeenAt(value.conversation.team_read_at ?? null);
6116
+ setMayHaveOlder(value.messages.length >= 50);
6117
+ setClaimedAs(c?.support.claimedCustomerName ?? null);
6118
+ connectWebSocket();
6119
+ },
6120
+ [connectWebSocket]
6121
+ );
6122
+ const openIfExisting = useCallback(async () => {
6123
+ const c = clientRef.current;
6124
+ if (!c || openedRef.current) return;
6125
+ openedRef.current = true;
6126
+ setIsLoading(true);
6127
+ const result = await c.support.peekConversation();
6128
+ if (result.ok && result.value) {
6129
+ hydrateConversation(result.value);
6130
+ } else {
6131
+ openedRef.current = false;
6132
+ }
6133
+ setIsLoading(false);
6134
+ }, [hydrateConversation]);
6135
+ const watchSessionClaims = useCallback(() => {
6136
+ const c = clientRef.current;
6137
+ if (!c || claimUnsubRef.current) return;
6138
+ const unsubClaimed = c.events.on(SESSION_CLAIMED, (event) => {
6139
+ setClaimedAs(event.customerName || c.support.claimedCustomerName || null);
6140
+ if (!openedRef.current) {
6141
+ void openIfExisting();
6142
+ return;
6143
+ }
6144
+ connectWebSocket();
6145
+ void resyncConversation();
6146
+ });
6147
+ const unsubCleared = c.events.on(SESSION_CLEARED, resetConversation);
6148
+ claimUnsubRef.current = () => {
6149
+ unsubClaimed();
6150
+ unsubCleared();
6151
+ };
6152
+ }, [connectWebSocket, resyncConversation, resetConversation, openIfExisting]);
6153
+ const signOut = useCallback(async () => {
6154
+ const c = clientRef.current;
6155
+ if (!c) return false;
6156
+ const result = await c.link.logout();
6157
+ return result.ok;
6158
+ }, []);
6005
6159
  useEffect(() => {
6160
+ watchSessionClaims();
6006
6161
  return () => {
6007
6162
  if (pollRef.current) clearInterval(pollRef.current);
6008
6163
  if (wsReconnectTimerRef.current) clearTimeout(wsReconnectTimerRef.current);
6009
6164
  if (awaitReplyTimerRef.current) clearTimeout(awaitReplyTimerRef.current);
6165
+ claimUnsubRef.current?.();
6166
+ claimUnsubRef.current = null;
6010
6167
  if (wsRef.current) {
6011
6168
  wsRef.current.onclose = null;
6169
+ wsRef.current.onmessage = null;
6012
6170
  wsRef.current.close();
6013
6171
  }
6014
6172
  };
@@ -6020,19 +6178,13 @@ function useChat(options = {}) {
6020
6178
  setError(null);
6021
6179
  const result = await client.support.openConversation();
6022
6180
  if (result.ok) {
6023
- setMessages(result.value.messages);
6024
- setConversationOpened(true);
6025
- aiEnabledRef.current = result.value.conversation.ai_enabled;
6026
- setHumanRequested(result.value.conversation.needs_human_review ?? false);
6027
- setSeenAt(result.value.conversation.team_read_at ?? null);
6028
- setMayHaveOlder(result.value.messages.length >= 50);
6029
- connectWebSocket();
6181
+ hydrateConversation(result.value);
6030
6182
  } else {
6031
6183
  openedRef.current = false;
6032
6184
  setError(result.error);
6033
6185
  }
6034
6186
  setIsLoading(false);
6035
- }, [client, connectWebSocket]);
6187
+ }, [client, hydrateConversation]);
6036
6188
  const send = useCallback(
6037
6189
  async (content, opts) => {
6038
6190
  const attachments = opts?.attachments ?? [];
@@ -6044,7 +6196,8 @@ function useChat(options = {}) {
6044
6196
  if (!openedRef.current) return false;
6045
6197
  }
6046
6198
  const replyTo = opts?.replyTo;
6047
- const optimisticId = `opt_${Date.now()}`;
6199
+ const clientId = generateIdempotencyKey();
6200
+ const optimisticId = `opt_${clientId}`;
6048
6201
  const optimistic = {
6049
6202
  id: optimisticId,
6050
6203
  sender_type: "customer",
@@ -6052,6 +6205,7 @@ function useChat(options = {}) {
6052
6205
  content_type: attachments.length > 0 ? "image" : "text",
6053
6206
  attachments,
6054
6207
  metadata: {},
6208
+ client_id: clientId,
6055
6209
  // Same shape the server echoes back, so the bubble doesn't reflow
6056
6210
  // when the confirmed message replaces it.
6057
6211
  reply_to: replyTo ? {
@@ -6064,6 +6218,7 @@ function useChat(options = {}) {
6064
6218
  setMessages((prev) => [...prev, optimistic]);
6065
6219
  setIsSending(true);
6066
6220
  const result = await client.support.sendMessage(content.trim(), {
6221
+ idempotencyKey: clientId,
6067
6222
  replyToMessageId: replyTo?.id,
6068
6223
  attachments: attachments.length > 0 ? attachments : void 0
6069
6224
  });
@@ -6134,11 +6289,7 @@ function useChat(options = {}) {
6134
6289
  const older = result.value;
6135
6290
  setMayHaveOlder(older.length >= 50);
6136
6291
  if (older.length === 0) return false;
6137
- setMessages((prev) => {
6138
- const ids = new Set(prev.map((m) => m.id));
6139
- const fresh = older.filter((m) => !ids.has(m.id));
6140
- return fresh.length > 0 ? [...fresh, ...prev] : prev;
6141
- });
6292
+ setMessages((prev) => mergeMessages(prev, older));
6142
6293
  return true;
6143
6294
  }, []);
6144
6295
  const agentArrived = messages.some((m) => m.sender_type === "agent");
@@ -6152,11 +6303,14 @@ function useChat(options = {}) {
6152
6303
  awaitingReply,
6153
6304
  seenAt,
6154
6305
  mayHaveOlder,
6306
+ claimedAs,
6307
+ signOut,
6155
6308
  send,
6156
6309
  startConversation,
6157
6310
  react,
6158
6311
  requestHuman,
6159
- loadOlder
6312
+ loadOlder,
6313
+ openIfExisting
6160
6314
  };
6161
6315
  }
6162
6316
  var ElementsContext = createContext({
@@ -7886,6 +8040,177 @@ function VolumePricing({
7886
8040
  }
7887
8041
  );
7888
8042
  }
8043
+ function createPlacesSessionToken() {
8044
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
8045
+ return crypto.randomUUID();
8046
+ }
8047
+ return `places_${Date.now()}_${Math.random().toString(16).slice(2)}`;
8048
+ }
8049
+ var MIN_QUERY_LENGTH = 3;
8050
+ var DEBOUNCE_MS = 300;
8051
+ var BLUR_CLOSE_MS = 120;
8052
+ function AddressAutocomplete({
8053
+ value,
8054
+ onSelect,
8055
+ onDirty,
8056
+ placeholder,
8057
+ required,
8058
+ autoFocus,
8059
+ id,
8060
+ className,
8061
+ classNames
8062
+ }) {
8063
+ const clientContext = useOptionalCimplifyClient();
8064
+ const [inputText, setInputText] = React14.useState(value?.formatted_address ?? "");
8065
+ const [suggestions, setSuggestions] = React14.useState([]);
8066
+ const [isOpen, setIsOpen] = React14.useState(false);
8067
+ const [highlightedIndex, setHighlightedIndex] = React14.useState(-1);
8068
+ const debounceRef = React14.useRef(null);
8069
+ const seqRef = React14.useRef(0);
8070
+ const sessionTokenRef = React14.useRef(createPlacesSessionToken());
8071
+ const blurTimeoutRef = React14.useRef(null);
8072
+ React14.useEffect(() => {
8073
+ return () => {
8074
+ if (debounceRef.current) clearTimeout(debounceRef.current);
8075
+ if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8076
+ seqRef.current += 1;
8077
+ };
8078
+ }, []);
8079
+ const fetchSuggestions = React14.useCallback(
8080
+ async (query) => {
8081
+ const client = clientContext?.client;
8082
+ if (!client || query.length < MIN_QUERY_LENGTH) {
8083
+ setSuggestions([]);
8084
+ setIsOpen(false);
8085
+ return;
8086
+ }
8087
+ const seq = ++seqRef.current;
8088
+ const result = await client.places.autocomplete(query, sessionTokenRef.current);
8089
+ if (seqRef.current !== seq) return;
8090
+ if (result.ok && result.value.predictions.length > 0) {
8091
+ setSuggestions(result.value.predictions);
8092
+ setHighlightedIndex(0);
8093
+ setIsOpen(true);
8094
+ } else {
8095
+ setSuggestions([]);
8096
+ setIsOpen(false);
8097
+ }
8098
+ },
8099
+ [clientContext]
8100
+ );
8101
+ const handleInputChange = (nextValue) => {
8102
+ setInputText(nextValue);
8103
+ onDirty?.();
8104
+ if (debounceRef.current) clearTimeout(debounceRef.current);
8105
+ debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), DEBOUNCE_MS);
8106
+ };
8107
+ const selectSuggestion = React14.useCallback(
8108
+ async (suggestion) => {
8109
+ if (debounceRef.current) clearTimeout(debounceRef.current);
8110
+ if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8111
+ const fallback = suggestion.description.split(",")[0]?.trim() || suggestion.description;
8112
+ setInputText(fallback);
8113
+ setSuggestions([]);
8114
+ setIsOpen(false);
8115
+ const client = clientContext?.client;
8116
+ if (!client) return;
8117
+ const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);
8118
+ sessionTokenRef.current = createPlacesSessionToken();
8119
+ if (result.ok) {
8120
+ const d = result.value;
8121
+ const address = {
8122
+ formatted_address: d.formatted_address || fallback,
8123
+ street_address: d.street_address,
8124
+ apartment: d.apartment,
8125
+ city: d.city,
8126
+ region: d.region,
8127
+ postal_code: d.postal_code,
8128
+ country: d.country,
8129
+ latitude: d.latitude,
8130
+ longitude: d.longitude,
8131
+ place_id: d.place_id
8132
+ };
8133
+ setInputText(address.formatted_address);
8134
+ onSelect(address);
8135
+ }
8136
+ },
8137
+ [clientContext, onSelect]
8138
+ );
8139
+ return /* @__PURE__ */ jsxs("div", { "data-cimplify-address-autocomplete": true, className: cn("relative", className, classNames?.root), children: [
8140
+ /* @__PURE__ */ jsx(
8141
+ "input",
8142
+ {
8143
+ id,
8144
+ type: "text",
8145
+ value: inputText,
8146
+ onChange: (e) => handleInputChange(e.target.value),
8147
+ onFocus: () => {
8148
+ if (suggestions.length > 0) setIsOpen(true);
8149
+ },
8150
+ onBlur: () => {
8151
+ blurTimeoutRef.current = setTimeout(() => setIsOpen(false), BLUR_CLOSE_MS);
8152
+ },
8153
+ onKeyDown: (e) => {
8154
+ if (e.key === "Escape") {
8155
+ setIsOpen(false);
8156
+ return;
8157
+ }
8158
+ if (!isOpen || suggestions.length === 0) return;
8159
+ if (e.key === "ArrowDown") {
8160
+ e.preventDefault();
8161
+ setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0);
8162
+ } else if (e.key === "ArrowUp") {
8163
+ e.preventDefault();
8164
+ setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1);
8165
+ } else if (e.key === "Enter" && highlightedIndex >= 0) {
8166
+ e.preventDefault();
8167
+ void selectSuggestion(suggestions[highlightedIndex]);
8168
+ }
8169
+ },
8170
+ placeholder: placeholder ?? "Search for an address...",
8171
+ required,
8172
+ autoFocus,
8173
+ autoComplete: "off",
8174
+ "aria-autocomplete": "list",
8175
+ "aria-expanded": isOpen && suggestions.length > 0,
8176
+ "data-cimplify-address-input": true,
8177
+ className: classNames?.input
8178
+ }
8179
+ ),
8180
+ isOpen && suggestions.length > 0 && /* @__PURE__ */ jsx(
8181
+ "div",
8182
+ {
8183
+ role: "listbox",
8184
+ "data-cimplify-address-list": true,
8185
+ className: cn(
8186
+ "absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg",
8187
+ classNames?.list
8188
+ ),
8189
+ children: suggestions.map((s, i) => /* @__PURE__ */ jsx(
8190
+ "button",
8191
+ {
8192
+ type: "button",
8193
+ role: "option",
8194
+ "aria-selected": i === highlightedIndex,
8195
+ onMouseDown: (e) => {
8196
+ e.preventDefault();
8197
+ void selectSuggestion(s);
8198
+ },
8199
+ "data-cimplify-address-option": true,
8200
+ className: cn(
8201
+ "block w-full px-3 py-2.5 text-left text-sm transition-colors",
8202
+ i === highlightedIndex ? "bg-muted text-foreground" : "text-foreground hover:bg-muted/70",
8203
+ classNames?.option,
8204
+ i === highlightedIndex && classNames?.activeOption
8205
+ ),
8206
+ children: s.description
8207
+ },
8208
+ s.place_id
8209
+ ))
8210
+ }
8211
+ )
8212
+ ] });
8213
+ }
7889
8214
  function parseValue(value) {
7890
8215
  if (!value) return void 0;
7891
8216
  const date = parseISO(value);
@@ -8646,129 +8971,18 @@ function AddressInput({
8646
8971
  classNames,
8647
8972
  inputClass
8648
8973
  }) {
8649
- const clientContext = useOptionalCimplifyClient();
8650
8974
  const addr = value && typeof value === "object" ? value : null;
8651
- const [inputText, setInputText] = React14.useState(addr?.formatted_address ?? "");
8652
- const [suggestions, setSuggestions] = React14.useState([]);
8653
- const [isOpen, setIsOpen] = React14.useState(false);
8654
- const [highlightedIndex, setHighlightedIndex] = React14.useState(-1);
8655
- const debounceRef = React14.useRef(null);
8656
- const seqRef = React14.useRef(0);
8657
- const sessionTokenRef = React14.useRef(createSessionToken());
8658
- const blurTimeoutRef = React14.useRef(null);
8659
- React14.useEffect(() => {
8660
- return () => {
8661
- if (debounceRef.current) clearTimeout(debounceRef.current);
8662
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8663
- seqRef.current += 1;
8664
- };
8665
- }, []);
8666
- const fetchSuggestions = React14.useCallback(async (query) => {
8667
- const client = clientContext?.client;
8668
- if (!client || query.length < 3) {
8669
- setSuggestions([]);
8670
- setIsOpen(false);
8671
- return;
8672
- }
8673
- const seq = ++seqRef.current;
8674
- const result = await client.places.autocomplete(query, sessionTokenRef.current);
8675
- if (seqRef.current !== seq) return;
8676
- if (result.ok && result.value.predictions.length > 0) {
8677
- setSuggestions(result.value.predictions);
8678
- setHighlightedIndex(0);
8679
- setIsOpen(true);
8680
- } else {
8681
- setSuggestions([]);
8682
- setIsOpen(false);
8683
- }
8684
- }, [clientContext]);
8685
- const handleInputChange = (nextValue) => {
8686
- setInputText(nextValue);
8687
- if (debounceRef.current) clearTimeout(debounceRef.current);
8688
- debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);
8689
- };
8690
- const selectSuggestion = React14.useCallback(async (suggestion) => {
8691
- if (debounceRef.current) clearTimeout(debounceRef.current);
8692
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8693
- const fallback = suggestion.description.split(",")[0]?.trim() || suggestion.description;
8694
- setInputText(fallback);
8695
- setSuggestions([]);
8696
- setIsOpen(false);
8697
- const client = clientContext?.client;
8698
- if (!client) return;
8699
- const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);
8700
- sessionTokenRef.current = createSessionToken();
8701
- if (result.ok) {
8702
- const d = result.value;
8703
- const addressValue = {
8704
- formatted_address: d.formatted_address || fallback,
8705
- street_address: d.street_address,
8706
- apartment: d.apartment,
8707
- city: d.city,
8708
- region: d.region,
8709
- postal_code: d.postal_code,
8710
- country: d.country,
8711
- latitude: d.latitude,
8712
- longitude: d.longitude,
8713
- place_id: d.place_id
8714
- };
8715
- setInputText(addressValue.formatted_address);
8716
- onValueChange(addressValue);
8717
- }
8718
- }, [clientContext, onValueChange]);
8719
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2 relative", classNames?.addressInput), children: [
8975
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", classNames?.addressInput), children: [
8720
8976
  /* @__PURE__ */ jsx(
8721
- "input",
8977
+ AddressAutocomplete,
8722
8978
  {
8723
- type: "text",
8724
- value: inputText,
8725
- onChange: (e) => handleInputChange(e.target.value),
8726
- onFocus: () => {
8727
- if (suggestions.length > 0) setIsOpen(true);
8728
- },
8729
- onBlur: () => {
8730
- blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120);
8731
- },
8732
- onKeyDown: (e) => {
8733
- if (e.key === "Escape") {
8734
- setIsOpen(false);
8735
- return;
8736
- }
8737
- if (!isOpen || suggestions.length === 0) return;
8738
- if (e.key === "ArrowDown") {
8739
- e.preventDefault();
8740
- setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0);
8741
- } else if (e.key === "ArrowUp") {
8742
- e.preventDefault();
8743
- setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1);
8744
- } else if (e.key === "Enter" && highlightedIndex >= 0) {
8745
- e.preventDefault();
8746
- void selectSuggestion(suggestions[highlightedIndex]);
8747
- }
8748
- },
8979
+ value: addr,
8980
+ onSelect: onValueChange,
8749
8981
  placeholder: field.placeholder || "Search for an address...",
8750
8982
  required: field.is_required,
8751
- autoComplete: "off",
8752
- "aria-autocomplete": "list",
8753
- "aria-expanded": isOpen && suggestions.length > 0,
8754
- className: inputClass
8983
+ classNames: { input: inputClass }
8755
8984
  }
8756
8985
  ),
8757
- isOpen && suggestions.length > 0 && /* @__PURE__ */ jsx("div", { role: "listbox", className: "absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg", children: suggestions.map((s, i) => /* @__PURE__ */ jsx(
8758
- "button",
8759
- {
8760
- type: "button",
8761
- role: "option",
8762
- "aria-selected": i === highlightedIndex,
8763
- onMouseDown: (e) => {
8764
- e.preventDefault();
8765
- void selectSuggestion(s);
8766
- },
8767
- className: cn("block w-full px-3 py-2.5 text-left text-sm transition-colors", i === highlightedIndex ? "bg-muted text-foreground" : "text-foreground hover:bg-muted/70"),
8768
- children: s.description
8769
- },
8770
- s.place_id
8771
- )) }),
8772
8986
  addr && /* @__PURE__ */ jsx(
8773
8987
  "input",
8774
8988
  {
@@ -8788,121 +9002,22 @@ function LocationInput({
8788
9002
  classNames,
8789
9003
  inputClass
8790
9004
  }) {
8791
- const clientContext = useOptionalCimplifyClient();
8792
9005
  const loc = value && typeof value === "object" ? value : null;
8793
- const [inputText, setInputText] = React14.useState(loc?.label ?? "");
8794
- const [suggestions, setSuggestions] = React14.useState([]);
8795
- const [isOpen, setIsOpen] = React14.useState(false);
8796
- const [highlightedIndex, setHighlightedIndex] = React14.useState(-1);
8797
- const debounceRef = React14.useRef(null);
8798
- const seqRef = React14.useRef(0);
8799
- const sessionTokenRef = React14.useRef(createSessionToken());
8800
- const blurTimeoutRef = React14.useRef(null);
8801
- React14.useEffect(() => {
8802
- return () => {
8803
- if (debounceRef.current) clearTimeout(debounceRef.current);
8804
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8805
- seqRef.current += 1;
8806
- };
8807
- }, []);
8808
- const fetchSuggestions = React14.useCallback(async (query) => {
8809
- const client = clientContext?.client;
8810
- if (!client || query.length < 3) {
8811
- setSuggestions([]);
8812
- setIsOpen(false);
8813
- return;
8814
- }
8815
- const seq = ++seqRef.current;
8816
- const result = await client.places.autocomplete(query, sessionTokenRef.current);
8817
- if (seqRef.current !== seq) return;
8818
- if (result.ok && result.value.predictions.length > 0) {
8819
- setSuggestions(result.value.predictions);
8820
- setHighlightedIndex(0);
8821
- setIsOpen(true);
8822
- } else {
8823
- setSuggestions([]);
8824
- setIsOpen(false);
8825
- }
8826
- }, [clientContext]);
8827
- const handleInputChange = (nextValue) => {
8828
- setInputText(nextValue);
8829
- if (debounceRef.current) clearTimeout(debounceRef.current);
8830
- debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);
8831
- };
8832
- const selectSuggestion = React14.useCallback(async (suggestion) => {
8833
- if (debounceRef.current) clearTimeout(debounceRef.current);
8834
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8835
- const label = suggestion.description.split(",")[0]?.trim() || suggestion.description;
8836
- setInputText(label);
8837
- setSuggestions([]);
8838
- setIsOpen(false);
8839
- const client = clientContext?.client;
8840
- if (!client) return;
8841
- const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);
8842
- sessionTokenRef.current = createSessionToken();
8843
- if (result.ok) {
8844
- const d = result.value;
8845
- onValueChange({
8846
- latitude: d.latitude,
8847
- longitude: d.longitude,
8848
- label: d.formatted_address || label
8849
- });
8850
- setInputText(d.formatted_address || label);
8851
- }
8852
- }, [clientContext, onValueChange]);
8853
- return /* @__PURE__ */ jsxs("div", { className: cn("relative", classNames?.locationInput), children: [
9006
+ return /* @__PURE__ */ jsxs("div", { className: cn(classNames?.locationInput), children: [
8854
9007
  /* @__PURE__ */ jsx(
8855
- "input",
9008
+ AddressAutocomplete,
8856
9009
  {
8857
- type: "text",
8858
- value: inputText,
8859
- onChange: (e) => handleInputChange(e.target.value),
8860
- onFocus: () => {
8861
- if (suggestions.length > 0) setIsOpen(true);
8862
- },
8863
- onBlur: () => {
8864
- blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120);
8865
- },
8866
- onKeyDown: (e) => {
8867
- if (e.key === "Escape") {
8868
- setIsOpen(false);
8869
- return;
8870
- }
8871
- if (!isOpen || suggestions.length === 0) return;
8872
- if (e.key === "ArrowDown") {
8873
- e.preventDefault();
8874
- setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0);
8875
- } else if (e.key === "ArrowUp") {
8876
- e.preventDefault();
8877
- setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1);
8878
- } else if (e.key === "Enter" && highlightedIndex >= 0) {
8879
- e.preventDefault();
8880
- void selectSuggestion(suggestions[highlightedIndex]);
8881
- }
8882
- },
9010
+ value: loc ? { formatted_address: loc.label ?? "", latitude: loc.latitude, longitude: loc.longitude } : null,
9011
+ onSelect: (address) => onValueChange({
9012
+ latitude: address.latitude,
9013
+ longitude: address.longitude,
9014
+ label: address.formatted_address
9015
+ }),
8883
9016
  placeholder: field.placeholder || "Search for a location...",
8884
9017
  required: field.is_required,
8885
- autoComplete: "off",
8886
- "aria-autocomplete": "list",
8887
- "aria-expanded": isOpen && suggestions.length > 0,
8888
- className: inputClass
9018
+ classNames: { input: inputClass }
8889
9019
  }
8890
9020
  ),
8891
- isOpen && suggestions.length > 0 && /* @__PURE__ */ jsx("div", { role: "listbox", className: "absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg", children: suggestions.map((s, i) => /* @__PURE__ */ jsx(
8892
- "button",
8893
- {
8894
- type: "button",
8895
- role: "option",
8896
- "aria-selected": i === highlightedIndex,
8897
- onMouseDown: (e) => {
8898
- e.preventDefault();
8899
- void selectSuggestion(s);
8900
- },
8901
- className: cn("block w-full px-3 py-2.5 text-left text-sm transition-colors", i === highlightedIndex ? "bg-muted text-foreground" : "text-foreground hover:bg-muted/70"),
8902
- children: s.description
8903
- },
8904
- s.place_id
8905
- )) }),
8906
9021
  loc && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: [
8907
9022
  loc.latitude.toFixed(4),
8908
9023
  ", ",
@@ -9033,12 +9148,6 @@ function SignatureInput({
9033
9148
  !sig?.data_url && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: field.placeholder || "Draw your signature above" })
9034
9149
  ] });
9035
9150
  }
9036
- function createSessionToken() {
9037
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
9038
- return crypto.randomUUID();
9039
- }
9040
- return `places_${Date.now()}_${Math.random().toString(16).slice(2)}`;
9041
- }
9042
9151
  function getTimeOfDay(timeStr) {
9043
9152
  const hour = parseInt(timeStr.split("T").pop()?.split(":")[0] ?? timeStr.split(":")[0], 10);
9044
9153
  if (hour < 12) return "morning";
@@ -9284,6 +9393,7 @@ function DateSlotPicker({
9284
9393
  serviceId,
9285
9394
  daysToShow = 7,
9286
9395
  participantCount,
9396
+ units,
9287
9397
  selectedSlot,
9288
9398
  onSlotSelect,
9289
9399
  availability: availabilityProp,
@@ -9318,6 +9428,7 @@ function DateSlotPicker({
9318
9428
  dateRange.endDate,
9319
9429
  {
9320
9430
  participantCount,
9431
+ units,
9321
9432
  enabled: availabilityProp === void 0
9322
9433
  }
9323
9434
  );
@@ -9475,6 +9586,7 @@ function ProductCustomizer({
9475
9586
  const [specialInstructions, setSpecialInstructions] = useState("");
9476
9587
  const [selectedSlot, setSelectedSlot] = useState(null);
9477
9588
  const [selectedStaffId, setSelectedStaffId] = useState(null);
9589
+ const [unitsCount, setUnitsCount] = useState(1);
9478
9590
  const cart = useCart();
9479
9591
  const currency = getProductCurrency(product);
9480
9592
  const productType = product.type || "product";
@@ -9482,6 +9594,7 @@ function ProductCustomizer({
9482
9594
  const isBundle = productType === "bundle";
9483
9595
  const isDigital = productType === "digital";
9484
9596
  const isService = productType === "service";
9597
+ const offersUnits = isService && isMultiDayService(product);
9485
9598
  const isStandard = !isComposite && !isBundle;
9486
9599
  const hasVariants = isStandard && product.variants && product.variants.length > 0;
9487
9600
  const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;
@@ -9503,6 +9616,7 @@ function ProductCustomizer({
9503
9616
  setSpecialInstructions("");
9504
9617
  setSelectedSlot(null);
9505
9618
  setSelectedStaffId(null);
9619
+ setUnitsCount(1);
9506
9620
  }, [product.id, product.min_order_quantity]);
9507
9621
  const slotStaff = useMemo(
9508
9622
  () => (selectedSlot?.available_staff ?? []).map((member) => ({
@@ -9592,7 +9706,17 @@ function ProductCustomizer({
9592
9706
  if (perUnit === void 0 || perUnit === null) return void 0;
9593
9707
  return parsePrice(perUnit) * quantity;
9594
9708
  }, [quote, quantity]);
9595
- const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;
9709
+ const displayTotalPrice = useMemo(() => {
9710
+ const total = quotedTotalPrice ?? localTotalPrice;
9711
+ const multiplier = serviceBillingMultiplier(product, {
9712
+ quantity,
9713
+ units: unitsCount,
9714
+ scheduledStart: selectedSlot?.start_time,
9715
+ scheduledEnd: selectedSlot?.end_time
9716
+ });
9717
+ if (multiplier === null) return total;
9718
+ return total / Math.max(1, quantity) * multiplier;
9719
+ }, [quotedTotalPrice, localTotalPrice, product, quantity, unitsCount, selectedSlot]);
9596
9720
  const handleVariantChange = useCallback(
9597
9721
  (variantId, variant) => {
9598
9722
  setSelectedVariantId(variantId);
@@ -9624,7 +9748,8 @@ function ProductCustomizer({
9624
9748
  scheduledStart: selectedSlot.start_time,
9625
9749
  scheduledEnd: selectedSlot.end_time,
9626
9750
  staffId: selectedStaffId ?? selectedSlot.available_staff?.[0]?.staff_id,
9627
- resourceId: selectedSlot.available_resources?.[0]?.resource_id
9751
+ resourceId: selectedSlot.available_resources?.[0]?.resource_id,
9752
+ units: unitsCount > 1 ? unitsCount : void 0
9628
9753
  } : {},
9629
9754
  customerInputs: Object.keys(customerInputValues).length > 0 ? Object.entries(customerInputValues).filter(([, v]) => v !== void 0 && v !== "").map(([fieldId, value]) => ({ field_id: fieldId, value })) : void 0,
9630
9755
  specialInstructions: specialInstructions.trim() || void 0
@@ -9645,7 +9770,7 @@ function ProductCustomizer({
9645
9770
  } finally {
9646
9771
  setIsSubmitting(false);
9647
9772
  }
9648
- }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, selectedStaffId, isSubmitting, onAddToCart, cart]);
9773
+ }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, selectedStaffId, unitsCount, isSubmitting, onAddToCart, cart]);
9649
9774
  return /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer": true, className: cn("space-y-6", className, classNames?.root), children: [
9650
9775
  isComposite && product.groups && product.composite_id && /* @__PURE__ */ jsx(
9651
9776
  CompositeSelector,
@@ -9682,9 +9807,17 @@ function ProductCustomizer({
9682
9807
  setSelectedStaffId(null);
9683
9808
  },
9684
9809
  participantCount: quantity,
9685
- currency
9810
+ units: offersUnits ? unitsCount : void 0,
9811
+ currency,
9812
+ schedulingMode: product.scheduling_mode ?? (offersUnits ? "multi_day" : "intraday"),
9813
+ durationUnit: product.duration_unit,
9814
+ durationValue: product.duration_value
9686
9815
  }
9687
9816
  ),
9817
+ offersUnits && /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer-units": true, className: classNames?.units, children: [
9818
+ /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", children: "Units" }),
9819
+ /* @__PURE__ */ jsx(QuantitySelector, { value: unitsCount, onChange: setUnitsCount, min: 1 })
9820
+ ] }),
9688
9821
  isService && slotStaff.length > 0 && /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer-staff": true, children: [
9689
9822
  /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", children: "Choose your specialist" }),
9690
9823
  /* @__PURE__ */ jsx(
@@ -11738,18 +11871,18 @@ function RentalServiceCard({
11738
11871
  const currency = getProductCurrency(product);
11739
11872
  const hasDeposit = product.deposit_type && product.deposit_type !== "none" && product.deposit_amount;
11740
11873
  const unit = product.duration_unit ? UNIT_LABELS2[product.duration_unit] || product.duration_unit : "day";
11874
+ const priceUnit = product.price_basis === "per_person" ? "guest" : product.price_basis === "flat" ? "" : unit;
11741
11875
  const status = product.inventory_status;
11742
11876
  return /* @__PURE__ */ jsxs(CardShell, { product, renderLink, className, children: [
11743
11877
  /* @__PURE__ */ jsxs(CardImage, { src: image, alt: product.name, aspectRatio: "16/9", renderImage, children: [
11744
11878
  /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" }),
11745
- /* @__PURE__ */ jsxs("span", { className: "absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm", children: [
11879
+ priceUnit && /* @__PURE__ */ jsxs("span", { className: "absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm", children: [
11746
11880
  "Per ",
11747
- unit
11881
+ priceUnit
11748
11882
  ] }),
11749
11883
  /* @__PURE__ */ jsxs("span", { className: "absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm", children: [
11750
11884
  /* @__PURE__ */ jsx(Price, { amount: product.default_price, currency }),
11751
- "/",
11752
- unit
11885
+ priceUnit && `/${priceUnit}`
11753
11886
  ] }),
11754
11887
  product.general_service_capacity != null && product.general_service_capacity > 1 && /* @__PURE__ */ jsxs("span", { className: "absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-blue-50/90 text-blue-700 border border-blue-200/50 backdrop-blur px-2 py-0.5 rounded-md", children: [
11755
11888
  "Up to ",
@@ -12343,6 +12476,18 @@ function ProductGrid({
12343
12476
  ] });
12344
12477
  }
12345
12478
 
12479
+ // src/react/utils/attachment.ts
12480
+ var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".svg"];
12481
+ function safeAttachmentUrl(url) {
12482
+ return /^https?:\/\//i.test(url) ? url : null;
12483
+ }
12484
+ function isImageAttachment(attachment) {
12485
+ if (attachment.mime_type?.startsWith("image/")) return true;
12486
+ if (attachment.mime_type) return false;
12487
+ const path = attachment.url.split("?")[0].split("#")[0].toLowerCase();
12488
+ return IMAGE_EXTENSIONS.some((extension) => path.endsWith(extension));
12489
+ }
12490
+
12346
12491
  // src/react/utils/boundary-labels.ts
12347
12492
  var PRESET_LABELS = {
12348
12493
  check_in_out: { start: "Check-in", end: "Check-out" },
@@ -13524,6 +13669,7 @@ function BookingPage({
13524
13669
  const [selectedDate, setSelectedDate] = useState(null);
13525
13670
  const [selectedStaffId, setSelectedStaffId] = useState(null);
13526
13671
  const [selectedResourceId, setSelectedResourceId] = useState(null);
13672
+ const [bookingUnits, setBookingUnits] = useState(1);
13527
13673
  const [isSubmitting, setIsSubmitting] = useState(false);
13528
13674
  const [error, setError] = useState(null);
13529
13675
  const hasResourceStep = resources && resources.length > 0;
@@ -13617,7 +13763,8 @@ function BookingPage({
13617
13763
  scheduledStart: selectedSlot.start_time,
13618
13764
  scheduledEnd: selectedSlot.end_time,
13619
13765
  staffId: selectedStaffId || void 0,
13620
- resourceId: selectedResourceId || void 0
13766
+ resourceId: selectedResourceId || void 0,
13767
+ units: bookingUnits > 1 ? bookingUnits : void 0
13621
13768
  });
13622
13769
  onBooked?.(selectedSlot, selectedStaffId);
13623
13770
  }
@@ -13713,6 +13860,31 @@ function BookingPage({
13713
13860
  ] }),
13714
13861
  step === STEP.CONFIRM && selectedSlot && /* @__PURE__ */ jsxs("div", { "data-cimplify-booking-step": "confirm", className: classNames?.step, children: [
13715
13862
  /* @__PURE__ */ jsx("h2", { "data-cimplify-booking-step-title": true, className: classNames?.stepTitle, children: "Confirm your booking" }),
13863
+ /* @__PURE__ */ jsxs("div", { "data-cimplify-booking-units": true, className: classNames?.summaryRow, children: [
13864
+ /* @__PURE__ */ jsx("span", { children: "Units" }),
13865
+ /* @__PURE__ */ jsxs("div", { "data-cimplify-booking-units-stepper": true, children: [
13866
+ /* @__PURE__ */ jsx(
13867
+ "button",
13868
+ {
13869
+ type: "button",
13870
+ "data-cimplify-booking-units-decrement": true,
13871
+ onClick: () => setBookingUnits((current) => Math.max(1, current - 1)),
13872
+ disabled: bookingUnits <= 1,
13873
+ children: "\u2212"
13874
+ }
13875
+ ),
13876
+ /* @__PURE__ */ jsx("span", { "data-cimplify-booking-units-value": true, children: bookingUnits }),
13877
+ /* @__PURE__ */ jsx(
13878
+ "button",
13879
+ {
13880
+ type: "button",
13881
+ "data-cimplify-booking-units-increment": true,
13882
+ onClick: () => setBookingUnits((current) => current + 1),
13883
+ children: "+"
13884
+ }
13885
+ )
13886
+ ] })
13887
+ ] }),
13716
13888
  /* @__PURE__ */ jsxs("div", { "data-cimplify-booking-summary": true, className: classNames?.summary, children: [
13717
13889
  /* @__PURE__ */ jsxs("div", { "data-cimplify-booking-summary-row": true, className: classNames?.summaryRow, children: [
13718
13890
  /* @__PURE__ */ jsx("span", { children: "Service" }),
@@ -16955,6 +17127,7 @@ function partitionMessages(messages) {
16955
17127
  }
16956
17128
  return { bubbles, reactionsByTarget };
16957
17129
  }
17130
+ var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
16958
17131
  function ChatWidget({
16959
17132
  client: clientProp,
16960
17133
  businessName = "Support",
@@ -16971,7 +17144,8 @@ function ChatWidget({
16971
17144
  const context = useOptionalCimplifyClient();
16972
17145
  const client = clientProp ?? context?.client;
16973
17146
  const [open, setOpen] = useState(defaultOpen);
16974
- const [unread, setUnread] = useState(0);
17147
+ const [readAt, setReadAt] = useState(null);
17148
+ const [attachError, setAttachError] = useState(null);
16975
17149
  const {
16976
17150
  messages,
16977
17151
  isLoading,
@@ -16980,16 +17154,31 @@ function ChatWidget({
16980
17154
  seenAt,
16981
17155
  error,
16982
17156
  isActive,
17157
+ claimedAs,
17158
+ signOut,
16983
17159
  send,
16984
17160
  react,
16985
- startConversation
17161
+ startConversation,
17162
+ openIfExisting
16986
17163
  } = useChat({ client, pollInterval });
16987
17164
  const showTyping = isSending || awaitingReply;
16988
17165
  const attachPhoto = useCallback(
16989
17166
  async (file) => {
16990
- if (!client || !file.type.startsWith("image/") || file.size > 5 * 1024 * 1024) return;
17167
+ if (!client) return;
17168
+ setAttachError(null);
17169
+ if (!file.type.startsWith("image/")) {
17170
+ setAttachError(new CimplifyError("invalid_attachment", "Only photos can be attached."));
17171
+ return;
17172
+ }
17173
+ if (file.size > MAX_ATTACHMENT_BYTES) {
17174
+ setAttachError(new CimplifyError("attachment_too_large", "Photos must be under 5MB."));
17175
+ return;
17176
+ }
16991
17177
  const uploaded = await client.uploads.upload(file);
16992
- if (!uploaded.ok) return;
17178
+ if (!uploaded.ok) {
17179
+ setAttachError(uploaded.error);
17180
+ return;
17181
+ }
16993
17182
  const attachment = {
16994
17183
  upload_id: uploaded.value.id,
16995
17184
  url: uploaded.value.url,
@@ -17001,22 +17190,28 @@ function ChatWidget({
17001
17190
  },
17002
17191
  [client, send]
17003
17192
  );
17004
- const prevCountRef = useRef(messages.length);
17005
- useEffect(() => {
17006
- if (!open && messages.length > prevCountRef.current) {
17007
- const newMessages = messages.slice(prevCountRef.current);
17008
- const incomingCount = newMessages.filter(
17009
- (m) => m.sender_type !== "customer"
17010
- ).length;
17011
- if (incomingCount > 0) setUnread((n) => n + incomingCount);
17012
- }
17013
- prevCountRef.current = messages.length;
17014
- }, [messages, open]);
17193
+ const handleSend = useCallback(
17194
+ (content) => {
17195
+ setAttachError(null);
17196
+ return send(content);
17197
+ },
17198
+ [send]
17199
+ );
17200
+ const unread = open ? 0 : messages.filter(
17201
+ (m) => m.sender_type !== "customer" && (!readAt || m.created_at > readAt)
17202
+ ).length;
17203
+ useState(() => {
17204
+ if (defaultOpen) void openIfExisting();
17205
+ });
17015
17206
  const handleOpen = useCallback(() => {
17016
17207
  setOpen(true);
17017
- setUnread(0);
17018
- }, []);
17019
- const handleClose = useCallback(() => setOpen(false), []);
17208
+ void openIfExisting();
17209
+ }, [openIfExisting]);
17210
+ const handleClose = useCallback(() => {
17211
+ const newest = messages[messages.length - 1];
17212
+ setReadAt(newest?.created_at ?? null);
17213
+ setOpen(false);
17214
+ }, [messages]);
17020
17215
  const isLeft = position === "bottom-left";
17021
17216
  return /* @__PURE__ */ jsxs(
17022
17217
  "div",
@@ -17049,11 +17244,13 @@ function ChatWidget({
17049
17244
  isSending,
17050
17245
  isTyping: showTyping,
17051
17246
  seenAt,
17052
- error,
17247
+ error: error ?? attachError,
17053
17248
  isActive,
17054
17249
  isLeft,
17250
+ claimedAs,
17251
+ onSignOut: () => void signOut(),
17055
17252
  onClose: handleClose,
17056
- onSend: send,
17253
+ onSend: handleSend,
17057
17254
  onReact: react,
17058
17255
  onAttach: attachPhoto,
17059
17256
  onStartConversation: startConversation,
@@ -17113,6 +17310,8 @@ function Panel({
17113
17310
  error,
17114
17311
  isActive,
17115
17312
  isLeft,
17313
+ claimedAs,
17314
+ onSignOut,
17116
17315
  onClose,
17117
17316
  onSend,
17118
17317
  onReact,
@@ -17136,6 +17335,8 @@ function Panel({
17136
17335
  Header,
17137
17336
  {
17138
17337
  businessName,
17338
+ claimedAs,
17339
+ onSignOut,
17139
17340
  onClose,
17140
17341
  className: classNames?.header
17141
17342
  }
@@ -17160,7 +17361,7 @@ function Panel({
17160
17361
  className: classNames?.messages
17161
17362
  }
17162
17363
  ),
17163
- error != null && /* @__PURE__ */ jsx("div", { className: "border-t border-red-200 bg-red-50 px-4 py-2 text-xs text-red-600", children: "Something went wrong. Please try again." }),
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." }),
17164
17365
  (isActive || isLoading) && /* @__PURE__ */ jsx(
17165
17366
  ChatInput,
17166
17367
  {
@@ -17182,6 +17383,8 @@ function Panel({
17182
17383
  }
17183
17384
  function Header({
17184
17385
  businessName,
17386
+ claimedAs,
17387
+ onSignOut,
17185
17388
  onClose,
17186
17389
  className
17187
17390
  }) {
@@ -17196,7 +17399,22 @@ function Header({
17196
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" }) }),
17197
17400
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
17198
17401
  /* @__PURE__ */ jsx("div", { className: "truncate text-[15px] font-semibold leading-tight", children: businessName }),
17199
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-white/60", children: [
17402
+ claimedAs ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 truncate text-xs text-white/60", children: [
17403
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
17404
+ "Chatting as ",
17405
+ claimedAs
17406
+ ] }),
17407
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
17408
+ /* @__PURE__ */ jsx(
17409
+ "button",
17410
+ {
17411
+ type: "button",
17412
+ onClick: onSignOut,
17413
+ className: "flex-shrink-0 underline underline-offset-2 hover:text-white",
17414
+ children: "Not you?"
17415
+ }
17416
+ )
17417
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-white/60", children: [
17200
17418
  /* @__PURE__ */ jsx("span", { className: "h-[7px] w-[7px] flex-shrink-0 rounded-full bg-emerald-400" }),
17201
17419
  "Online"
17202
17420
  ] })
@@ -17280,10 +17498,6 @@ function MessageList({
17280
17498
  onReact,
17281
17499
  className
17282
17500
  }) {
17283
- const bottomRef = useRef(null);
17284
- useEffect(() => {
17285
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
17286
- }, [messages.length, isSending]);
17287
17501
  const { bubbles, reactionsByTarget } = partitionMessages(messages);
17288
17502
  const seenMessageId = lastSeenCustomerMessageId(bubbles, seenAt);
17289
17503
  return /* @__PURE__ */ jsxs(
@@ -17357,11 +17571,13 @@ function MessageList({
17357
17571
  ]
17358
17572
  }
17359
17573
  ),
17360
- msg.attachments.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("flex flex-wrap gap-1.5", msg.content && "mb-1.5"), children: msg.attachments.map(
17361
- (attachment, index) => attachment.mime_type?.startsWith("image/") ? /* @__PURE__ */ jsx("a", { href: attachment.url, target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ jsx(
17574
+ msg.attachments.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("flex flex-wrap gap-1.5", msg.content && "mb-1.5"), children: msg.attachments.map((attachment, index) => {
17575
+ const url = safeAttachmentUrl(attachment.url);
17576
+ if (!url) return null;
17577
+ return attachment.mime_type?.startsWith("image/") ? /* @__PURE__ */ jsx("a", { href: url, target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ jsx(
17362
17578
  "img",
17363
17579
  {
17364
- src: attachment.url,
17580
+ src: url,
17365
17581
  alt: attachment.filename ?? "Photo",
17366
17582
  loading: "lazy",
17367
17583
  className: "max-h-48 max-w-full rounded-lg object-cover"
@@ -17369,15 +17585,15 @@ function MessageList({
17369
17585
  ) }, index) : /* @__PURE__ */ jsx(
17370
17586
  "a",
17371
17587
  {
17372
- href: attachment.url,
17588
+ href: url,
17373
17589
  target: "_blank",
17374
17590
  rel: "noreferrer",
17375
17591
  className: "truncate text-xs underline",
17376
17592
  children: attachment.filename ?? "Attachment"
17377
17593
  },
17378
17594
  index
17379
- )
17380
- ) }),
17595
+ );
17596
+ }) }),
17381
17597
  isCustomer ? msg.content && /* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap", children: msg.content }) : /* @__PURE__ */ jsx(
17382
17598
  ChatMarkdown,
17383
17599
  {
@@ -17403,7 +17619,15 @@ function MessageList({
17403
17619
  ] }, msg.id);
17404
17620
  }),
17405
17621
  isSending && /* @__PURE__ */ jsx("div", { className: "mr-auto mt-1", children: /* @__PURE__ */ jsx(TypingDots, {}) }),
17406
- /* @__PURE__ */ jsx("div", { ref: bottomRef })
17622
+ /* @__PURE__ */ jsx(
17623
+ "div",
17624
+ {
17625
+ ref: (node) => {
17626
+ node?.scrollIntoView({ behavior: "smooth" });
17627
+ }
17628
+ },
17629
+ `${messages.length}:${isSending}`
17630
+ )
17407
17631
  ]
17408
17632
  }
17409
17633
  );
@@ -19450,4 +19674,4 @@ async function genUIFingerprint(manifest) {
19450
19674
  return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 16);
19451
19675
  }
19452
19676
 
19453
- export { AccommodationCard, AccountContent, AccountDashboardPage, AccountHero, AccountIdentityStrip, AccountLineItem, AccountLineItems, AccountNav, AccountOrderDetailPage, AccountOrderView, AccountOrdersPage, AccountShell, AccountSidebar, AccountSignedOutPrompt, AccountSubscriptionDetailPage, AccountSubscriptionsPage, Ad, AdProvider, AddOnSelector, AddressElement, AuthElement, AvailabilityBadge, BackLink, BillingPlanSelector, BookingCard, BookingList, BookingPage, BookingSummary, BookingsPage, BoxIcon24, BundleProductCard, BundleProductLayout, BundleSelector, Button, CIMPLIFY_GENUI_REGISTRY, CardImage, CardShell, CardVariant, CartDrawer, CartDrawerProvider, CartPage, CartSummary, CartTemplate, CatalogueCollectionLayout, CataloguePage, CatalogueTemplate, CategoryFilter, CategoryGrid, ChatMarkdown, ChatWidget, CheckIcon, CheckoutPage, CimplifyCheckout, CimplifyProvider, CimplifySignInButton, CimplifyUserButton, CollectionPage, CollectionTemplate, CompactCartLayout, CompactCatalogueLayout, CompactSearchLayout, CompactServiceCard, CompositeProductCard, CompositeProductLayout, CompositeSelector, ContextCard, ContinuityCard, CurrencySelector, CustomAttributesTable, CustomerInputFields, DashboardIcon, DashboardIcon20, DatePicker, DateSlotPicker, DealBanner, DealsPage, DefaultCartLayout, DefaultCatalogueLayout, DefaultCollectionLayout, DefaultProductLayout, DefaultSearchLayout, DeliveryEstimate, DeliveryTracker, DeliveryTruckIcon, DigitalProductCard, DigitalProductLayout, DiscountInput, ElementsProvider, EmptyState, ErrorState, FeaturedCollectionLayout, FilterTabs, FoodProductCard, FoodProductLayout, GenUIView, HappeningCard, IdentityIcon, IdentityOfframp, InventoryBadge, ItemsCard, LeaseServiceCard, LineItemDetails, LocationPicker, MediaGallery, MetadataStringList, MobileAccountShell, MobileBackButton, MobileMoreButton, MobileTabBar, NextDeliveryCard, OrderDetailHeader, OrderDetailMetaPipe, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderList, OrderRow, OrderSummary, OrderTimeline, OrderTotals, OrdersIcon, OrdersIcon20, OvenIcon, PackageIcon, PaymentElement, Price, PriceRange, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductModel3D, ProductPage, ProductSheet, ProductTemplate, PropertiesTable, QuantitySelector, QuickAddButton, RecentlyViewed, RecommendationCarousel, RelatedProductsSection, RentalServiceCard, ResourcePicker, RetailProductCard, RichText, SaleBadge, SalePrice, SaleTeaser, ScheduleServiceCard, SearchInput, SearchPage, SearchTemplate, Section, SectionMore, ServiceProductLayout, SessionMessageBanner, SignOutIcon, Skeleton, SlotPicker, SoldOutOverlay, StaffPicker, StandardServiceCard, Status, StoreNav, StoreVideo, SubscriptionCard, SubscriptionList, SubscriptionRow, SubscriptionsIcon, SubscriptionsIcon20, TagPills, TimePicker, Toast, TwoColumnGrid, VariantSelector, VolumePricing, WelcomeCard, WholesaleProductCard, WholesaleProductLayout, WishlistButton, clearOrdersCache, cn, defaultBoundaryKind, genUICatalog, genUIFingerprint, genUIManifest, genUIManifestJson, getBoundaryLabels, isTerminalOrderStatus, parse as parseGenUIProgram, partitionMessages, prefetchProduct, prefetchProducts, resolveBoundaryKind, roomToResource, sanitizeRichTextHtml, stripHtmlToText, toAccountLineItem, toAccountLineItems, toAccountOrderView, useActivityState, useAds, useAttributeDefinitions, useAvailableSlots, useBillingPlans, useBookings, useBootstrap, useBundles, useCart, useCartDrawer, useCategories, useChat, useCheckout, useCimplify, useCimplifyClient, useCimplifySession, useCollection, useCollections, useDeals, useDeliveryFee, useDeliveryOptions, useElements, useElementsReady, useFxRate, useGenUIProgram, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, usePropertyFacets, useQuote, useRecommendations, useSearch, useServiceAvailability, useServices, useStoreCredit, useSubscription, useSubscriptions, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount, useVariantSelector };
19677
+ export { AccommodationCard, AccountContent, AccountDashboardPage, AccountHero, AccountIdentityStrip, AccountLineItem, AccountLineItems, AccountNav, AccountOrderDetailPage, AccountOrderView, AccountOrdersPage, AccountShell, AccountSidebar, AccountSignedOutPrompt, AccountSubscriptionDetailPage, AccountSubscriptionsPage, Ad, AdProvider, AddOnSelector, AddressAutocomplete, AddressElement, AuthElement, AvailabilityBadge, BackLink, BillingPlanSelector, BookingCard, BookingList, BookingPage, BookingSummary, BookingsPage, BoxIcon24, BundleProductCard, BundleProductLayout, BundleSelector, Button, CIMPLIFY_GENUI_REGISTRY, CardImage, CardShell, CardVariant, CartDrawer, CartDrawerProvider, CartPage, CartSummary, CartTemplate, CatalogueCollectionLayout, CataloguePage, CatalogueTemplate, CategoryFilter, CategoryGrid, ChatMarkdown, ChatWidget, CheckIcon, CheckoutPage, CimplifyCheckout, CimplifyProvider, CimplifySignInButton, CimplifyUserButton, CollectionPage, CollectionTemplate, CompactCartLayout, CompactCatalogueLayout, CompactSearchLayout, CompactServiceCard, CompositeProductCard, CompositeProductLayout, CompositeSelector, ContextCard, ContinuityCard, CurrencySelector, CustomAttributesTable, CustomerInputFields, DashboardIcon, DashboardIcon20, DatePicker, DateSlotPicker, DealBanner, DealsPage, DefaultCartLayout, DefaultCatalogueLayout, DefaultCollectionLayout, DefaultProductLayout, DefaultSearchLayout, DeliveryEstimate, DeliveryTracker, DeliveryTruckIcon, DigitalProductCard, DigitalProductLayout, DiscountInput, ElementsProvider, EmptyState, ErrorState, FeaturedCollectionLayout, FilterTabs, FoodProductCard, FoodProductLayout, GenUIView, HappeningCard, IdentityIcon, IdentityOfframp, InventoryBadge, ItemsCard, LeaseServiceCard, LineItemDetails, LocationPicker, MediaGallery, MetadataStringList, MobileAccountShell, MobileBackButton, MobileMoreButton, MobileTabBar, NextDeliveryCard, OrderDetailHeader, OrderDetailMetaPipe, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderList, OrderRow, OrderSummary, OrderTimeline, OrderTotals, OrdersIcon, OrdersIcon20, OvenIcon, PackageIcon, PaymentElement, Price, PriceRange, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductModel3D, ProductPage, ProductSheet, ProductTemplate, PropertiesTable, QuantitySelector, QuickAddButton, RecentlyViewed, RecommendationCarousel, RelatedProductsSection, RentalServiceCard, ResourcePicker, RetailProductCard, RichText, SaleBadge, SalePrice, SaleTeaser, ScheduleServiceCard, SearchInput, SearchPage, SearchTemplate, Section, SectionMore, ServiceProductLayout, SessionMessageBanner, SignOutIcon, Skeleton, SlotPicker, SoldOutOverlay, StaffPicker, StandardServiceCard, Status, StoreNav, StoreVideo, SubscriptionCard, SubscriptionList, SubscriptionRow, SubscriptionsIcon, SubscriptionsIcon20, TagPills, TimePicker, Toast, TwoColumnGrid, VariantSelector, VolumePricing, WelcomeCard, WholesaleProductCard, WholesaleProductLayout, WishlistButton, clearOrdersCache, cn, createPlacesSessionToken, defaultBoundaryKind, genUICatalog, genUIFingerprint, genUIManifest, genUIManifestJson, getBoundaryLabels, isImageAttachment, isTerminalOrderStatus, parse as parseGenUIProgram, partitionMessages, prefetchProduct, prefetchProducts, resolveBoundaryKind, roomToResource, safeAttachmentUrl, sanitizeRichTextHtml, stripHtmlToText, toAccountLineItem, toAccountLineItems, toAccountOrderView, useActivityState, useAds, useAttributeDefinitions, useAvailableSlots, useBillingPlans, useBookings, useBootstrap, useBundles, useCart, useCartDrawer, useCategories, useChat, useCheckout, useCimplify, useCimplifyClient, useCimplifySession, useCollection, useCollections, useDeals, useDeliveryFee, useDeliveryOptions, useElements, useElementsReady, useFxRate, useGenUIProgram, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, usePropertyFacets, useQuote, useRecommendations, useSearch, useServiceAvailability, useServices, useStoreCredit, useSubscription, useSubscriptions, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount, useVariantSelector };