vdb-ai-chat 1.0.0 → 1.0.2

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 (132) hide show
  1. package/dist/10.chat-widget.js +2 -0
  2. package/dist/10.chat-widget.js.LICENSE.txt +1 -0
  3. package/dist/104.chat-widget.js +1 -0
  4. package/dist/248.chat-widget.js +1 -0
  5. package/dist/50.chat-widget.js +1 -0
  6. package/dist/521.chat-widget.js +1 -0
  7. package/dist/538.chat-widget.js +1 -0
  8. package/dist/572.chat-widget.js +1 -0
  9. package/dist/694.chat-widget.js +1 -0
  10. package/dist/chat-widget.js +1 -1
  11. package/dist/chat-widget.js.LICENSE.txt +2 -0
  12. package/lib/commonjs/api.js +6 -8
  13. package/lib/commonjs/api.js.map +1 -1
  14. package/lib/commonjs/components/BetaNotice.js +38 -0
  15. package/lib/commonjs/components/BetaNotice.js.map +1 -0
  16. package/lib/commonjs/components/ChatHeader.js +41 -21
  17. package/lib/commonjs/components/ChatHeader.js.map +1 -1
  18. package/lib/commonjs/components/ChatInput.js +21 -21
  19. package/lib/commonjs/components/ChatInput.js.map +1 -1
  20. package/lib/commonjs/components/ChatWidget.js +162 -90
  21. package/lib/commonjs/components/ChatWidget.js.map +1 -1
  22. package/lib/commonjs/components/MessageBubble.js +28 -68
  23. package/lib/commonjs/components/MessageBubble.js.map +1 -1
  24. package/lib/commonjs/components/MessageMetaRow.js +135 -0
  25. package/lib/commonjs/components/MessageMetaRow.js.map +1 -0
  26. package/lib/commonjs/components/ProductsGrid.js +139 -0
  27. package/lib/commonjs/components/ProductsGrid.js.map +1 -0
  28. package/lib/commonjs/components/ProductsList.js +22 -126
  29. package/lib/commonjs/components/ProductsList.js.map +1 -1
  30. package/lib/commonjs/components/ProductsListView.js +139 -0
  31. package/lib/commonjs/components/ProductsListView.js.map +1 -0
  32. package/lib/commonjs/components/SuggestionsRow.js +41 -23
  33. package/lib/commonjs/components/SuggestionsRow.js.map +1 -1
  34. package/lib/commonjs/components/utils.js +50 -4
  35. package/lib/commonjs/components/utils.js.map +1 -1
  36. package/lib/commonjs/contexts/AnalyticsClientContext.js +19 -0
  37. package/lib/commonjs/contexts/AnalyticsClientContext.js.map +1 -0
  38. package/lib/commonjs/hooks/useAnalytics.js +158 -0
  39. package/lib/commonjs/hooks/useAnalytics.js.map +1 -0
  40. package/lib/commonjs/index.web.js +91 -8
  41. package/lib/commonjs/index.web.js.map +1 -1
  42. package/lib/commonjs/storage.js +5 -5
  43. package/lib/commonjs/storage.js.map +1 -1
  44. package/lib/commonjs/theme.js +4 -4
  45. package/lib/commonjs/theme.js.map +1 -1
  46. package/lib/module/api.js +6 -8
  47. package/lib/module/api.js.map +1 -1
  48. package/lib/module/components/BetaNotice.js +30 -0
  49. package/lib/module/components/BetaNotice.js.map +1 -0
  50. package/lib/module/components/ChatHeader.js +41 -21
  51. package/lib/module/components/ChatHeader.js.map +1 -1
  52. package/lib/module/components/ChatInput.js +21 -21
  53. package/lib/module/components/ChatInput.js.map +1 -1
  54. package/lib/module/components/ChatWidget.js +164 -92
  55. package/lib/module/components/ChatWidget.js.map +1 -1
  56. package/lib/module/components/MessageBubble.js +28 -70
  57. package/lib/module/components/MessageBubble.js.map +1 -1
  58. package/lib/module/components/MessageMetaRow.js +127 -0
  59. package/lib/module/components/MessageMetaRow.js.map +1 -0
  60. package/lib/module/components/ProductsGrid.js +133 -0
  61. package/lib/module/components/ProductsGrid.js.map +1 -0
  62. package/lib/module/components/ProductsList.js +21 -126
  63. package/lib/module/components/ProductsList.js.map +1 -1
  64. package/lib/module/components/ProductsListView.js +132 -0
  65. package/lib/module/components/ProductsListView.js.map +1 -0
  66. package/lib/module/components/SuggestionsRow.js +41 -23
  67. package/lib/module/components/SuggestionsRow.js.map +1 -1
  68. package/lib/module/components/utils.js +46 -3
  69. package/lib/module/components/utils.js.map +1 -1
  70. package/lib/module/contexts/AnalyticsClientContext.js +10 -0
  71. package/lib/module/contexts/AnalyticsClientContext.js.map +1 -0
  72. package/lib/module/hooks/useAnalytics.js +146 -0
  73. package/lib/module/hooks/useAnalytics.js.map +1 -0
  74. package/lib/module/index.native.js +5 -5
  75. package/lib/module/index.web.js +89 -6
  76. package/lib/module/index.web.js.map +1 -1
  77. package/lib/module/storage.js +6 -6
  78. package/lib/module/storage.js.map +1 -1
  79. package/lib/module/theme.js +4 -4
  80. package/lib/module/theme.js.map +1 -1
  81. package/lib/typescript/api.d.ts +1 -1
  82. package/lib/typescript/api.d.ts.map +1 -1
  83. package/lib/typescript/components/BetaNotice.d.ts +5 -0
  84. package/lib/typescript/components/BetaNotice.d.ts.map +1 -0
  85. package/lib/typescript/components/ChatHeader.d.ts +5 -2
  86. package/lib/typescript/components/ChatHeader.d.ts.map +1 -1
  87. package/lib/typescript/components/ChatInput.d.ts.map +1 -1
  88. package/lib/typescript/components/ChatWidget.d.ts.map +1 -1
  89. package/lib/typescript/components/MessageBubble.d.ts +9 -3
  90. package/lib/typescript/components/MessageBubble.d.ts.map +1 -1
  91. package/lib/typescript/components/MessageMetaRow.d.ts +14 -0
  92. package/lib/typescript/components/MessageMetaRow.d.ts.map +1 -0
  93. package/lib/typescript/components/ProductsGrid.d.ts +10 -0
  94. package/lib/typescript/components/ProductsGrid.d.ts.map +1 -0
  95. package/lib/typescript/components/ProductsList.d.ts +4 -2
  96. package/lib/typescript/components/ProductsList.d.ts.map +1 -1
  97. package/lib/typescript/components/ProductsListView.d.ts +10 -0
  98. package/lib/typescript/components/ProductsListView.d.ts.map +1 -0
  99. package/lib/typescript/components/SuggestionsRow.d.ts +2 -0
  100. package/lib/typescript/components/SuggestionsRow.d.ts.map +1 -1
  101. package/lib/typescript/components/utils.d.ts +24 -0
  102. package/lib/typescript/components/utils.d.ts.map +1 -1
  103. package/lib/typescript/contexts/AnalyticsClientContext.d.ts +9 -0
  104. package/lib/typescript/contexts/AnalyticsClientContext.d.ts.map +1 -0
  105. package/lib/typescript/hooks/useAnalytics.d.ts +36 -0
  106. package/lib/typescript/hooks/useAnalytics.d.ts.map +1 -0
  107. package/lib/typescript/index.native.d.ts +5 -5
  108. package/lib/typescript/index.web.d.ts +1 -1
  109. package/lib/typescript/index.web.d.ts.map +1 -1
  110. package/lib/typescript/storage.d.ts.map +1 -1
  111. package/lib/typescript/types.d.ts +4 -2
  112. package/lib/typescript/types.d.ts.map +1 -1
  113. package/package.json +13 -3
  114. package/src/api.ts +26 -14
  115. package/src/components/BetaNotice.tsx +32 -0
  116. package/src/components/ChatHeader.tsx +43 -18
  117. package/src/components/ChatInput.tsx +20 -21
  118. package/src/components/ChatWidget.tsx +282 -219
  119. package/src/components/MessageBubble.tsx +48 -111
  120. package/src/components/MessageMetaRow.tsx +199 -0
  121. package/src/components/ProductsGrid.tsx +163 -0
  122. package/src/components/ProductsList.tsx +20 -144
  123. package/src/components/ProductsListView.tsx +149 -0
  124. package/src/components/SuggestionsRow.tsx +45 -21
  125. package/src/components/utils.ts +66 -5
  126. package/src/contexts/AnalyticsClientContext.tsx +20 -0
  127. package/src/hooks/useAnalytics.tsx +176 -0
  128. package/src/index.native.tsx +5 -5
  129. package/src/index.web.tsx +92 -4
  130. package/src/storage.ts +16 -13
  131. package/src/theme.ts +4 -4
  132. package/src/types.ts +4 -3
@@ -13,11 +13,13 @@ import {
13
13
  ScrollView,
14
14
  Text,
15
15
  TextInput,
16
+ KeyboardAvoidingView,
16
17
  DeviceEventEmitter,
17
18
  Platform,
18
19
  } from "react-native";
19
20
  import { ChatInput } from "./ChatInput";
20
21
  import { MessageBubble } from "./MessageBubble";
22
+ import MessageMetaRow from "./MessageMetaRow";
21
23
  import type { ChatMessage, ChatWidgetProps, ChatWidgetRef } from "../types";
22
24
  import { mergeTheme } from "../theme";
23
25
  import {
@@ -29,16 +31,17 @@ import {
29
31
  sendUserMessage,
30
32
  } from "../api";
31
33
  import ChatHeader from "./ChatHeader";
34
+ import BetaNotice from "./BetaNotice";
32
35
  import SuggestionsRow from "./SuggestionsRow";
33
36
  import ProductsList from "./ProductsList";
34
- import { FeedbackAction, formatToTime } from "./utils";
37
+ import { FeedbackAction, formatToTime, getUserDetails, UserDetails } from "./utils";
38
+ import { useUserAnalytics } from "../hooks/useAnalytics";
35
39
  import { Storage } from "../storage";
36
40
 
37
41
  export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
38
42
  (
39
43
  {
40
44
  apiUrl,
41
- userId: userIdProp,
42
45
  userToken: userTokenProp,
43
46
  priceMode: priceModeProp,
44
47
  modalHeight,
@@ -49,6 +52,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
49
52
  onClearChat,
50
53
  onViewAllPress,
51
54
  onItemPress: onItemPressProp,
55
+ isBetaMode: isBetaModeProp,
52
56
  },
53
57
  ref
54
58
  ) => {
@@ -63,33 +67,23 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
63
67
  const [assistantResponse, setAssistantResponse] = useState<
64
68
  ChatMessage | undefined
65
69
  >(undefined);
66
- const [products, setProducts] = useState<{
67
- messageId: string;
68
- data: any;
69
- } | null>(null);
70
+ const [productsByMsg, setProductsByMsg] = useState<Record<string, any>>({});
71
+ const [reloadLoadingIds, setReloadLoadingIds] = useState<Set<string>>(
72
+ new Set()
73
+ );
70
74
  const [priceMode, setPriceMode] = useState<string | null>(
71
75
  priceModeProp || null
72
76
  );
73
- const [conversationId, setConversationId] = useState<string | null>(null);
74
- const [userId, setUserId] = useState<string>(userIdProp || "");
75
77
  const [userToken, setUserToken] = useState<string>(userTokenProp || "");
76
78
  const scrollRef = useRef<ScrollView | null>(null);
77
79
  const inputRef = useRef<TextInput | null>(null);
78
80
  const theme = useMemo(() => mergeTheme(themeOverrides), [themeOverrides]);
79
-
81
+ const { _identify } = useUserAnalytics();
82
+ const betaActive = Boolean(isBetaModeProp);
80
83
  // Load user auth data from storage on mount
81
84
  useEffect(() => {
82
85
  const loadAuthData = async () => {
83
86
  try {
84
- if (!userIdProp) {
85
- const userData = await Storage.getJSON<{ id?: string }>(
86
- "userData",
87
- {}
88
- );
89
- if (userData?.id) {
90
- setUserId(userData.id);
91
- }
92
- }
93
87
  if (!userTokenProp) {
94
88
  const token = await Storage.getItem("token");
95
89
  if (token) {
@@ -101,13 +95,11 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
101
95
  }
102
96
  };
103
97
  loadAuthData();
104
- }, [userIdProp, userTokenProp]);
98
+ }, [userTokenProp]);
105
99
 
106
100
  const onViewAll = useCallback(() => {
107
- const searchPayload = JSON.stringify(
108
- assistantResponse?.agent_response?.payload
109
- );
110
- const payload = assistantResponse?.agent_response?.payload;
101
+ const searchPayload = JSON.stringify(assistantResponse?.search_payload);
102
+ const payload = assistantResponse?.search_payload;
111
103
  if (!payload) return;
112
104
 
113
105
  const domain = apiUrl.split("v3");
@@ -149,15 +141,15 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
149
141
  );
150
142
 
151
143
  const hasAuth = useMemo(
152
- () => Boolean(userId || userToken),
153
- [userId, userToken]
144
+ () => Boolean(userToken),
145
+ [userToken]
154
146
  );
155
147
 
156
148
  const apiParams: ChatApiParams = useMemo(
157
149
  () => ({
158
- conversationId: userToken || userId,
150
+ conversationId: userToken,
159
151
  }),
160
- [userId, userToken]
152
+ [userToken]
161
153
  );
162
154
 
163
155
  const handleFeedbackAction = useCallback(
@@ -211,7 +203,24 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
211
203
  priceMode
212
204
  );
213
205
  if (!cancelled) {
214
- setMessages(normaliseMessages(initial).reverse());
206
+ const normalised = normaliseMessages(initial).reverse();
207
+ if (normalised.length === 0) {
208
+ const initialAssistant: ChatMessage = {
209
+ id: "",
210
+ role: "assistant",
211
+ text: "Hello! How can I help you today?",
212
+ createdAt: Date.now(),
213
+ isLoading: false,
214
+ suggestions: [
215
+ "Search Natural Diamonds",
216
+ "Search Lab-Grown Diamonds",
217
+ ],
218
+ reaction: "0",
219
+ };
220
+ setMessages([initialAssistant]);
221
+ } else {
222
+ setMessages(normalised);
223
+ }
215
224
  }
216
225
  } catch (error) {
217
226
  console.error("Failed to fetch initial messages", error);
@@ -231,9 +240,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
231
240
  async (rawText: string) => {
232
241
  const trimmed = rawText.trim();
233
242
  if (!trimmed || loading) return;
234
- if (products) {
235
- setProducts(null);
236
- }
243
+
237
244
  const userMessage: ChatMessage = {
238
245
  id: `user-${Date.now()}`,
239
246
  role: "user",
@@ -247,6 +254,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
247
254
  role: "assistant",
248
255
  text: "Thinking",
249
256
  createdAt: Date.now(),
257
+ reaction: "0",
250
258
  isLoading: true,
251
259
  };
252
260
 
@@ -289,12 +297,12 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
289
297
  if (latestAssistant?.text) {
290
298
  // If there is an agent_response, try to fetch products first
291
299
  if (
292
- latestAssistant?.agent_response &&
293
- typeof latestAssistant.agent_response === "object" &&
294
- Object.keys(latestAssistant.agent_response).length > 0
300
+ latestAssistant?.search_payload &&
301
+ typeof latestAssistant.search_payload === "object" &&
302
+ Object.keys(latestAssistant.search_payload).length > 0
295
303
  ) {
296
304
  const productsResult = await getProducts(
297
- latestAssistant.agent_response.payload
305
+ latestAssistant.search_payload
298
306
  );
299
307
 
300
308
  const hasDiamonds =
@@ -311,7 +319,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
311
319
  setLoadingMessageId(null);
312
320
  setTypingMessageId(null);
313
321
  setTypingFullText("");
314
- setProducts(null);
315
322
 
316
323
  setMessages((prev) =>
317
324
  prev.map((msg) =>
@@ -329,10 +336,10 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
329
336
  inputRef.current?.focus();
330
337
  return;
331
338
  }
332
- setProducts({
333
- messageId: latestAssistant.id,
334
- data: productsResult,
335
- });
339
+ setProductsByMsg((prev) => ({
340
+ ...prev,
341
+ [latestAssistant.id]: productsResult,
342
+ }));
336
343
  }
337
344
  setLoadingMessageId(null);
338
345
 
@@ -386,9 +393,19 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
386
393
  inputRef.current?.focus();
387
394
  }
388
395
  },
389
- [apiUrl, loading, messages, apiParams, hasAuth, priceMode, products]
396
+ [apiUrl, loading, messages, apiParams, hasAuth, priceMode, productsByMsg]
390
397
  );
391
398
 
399
+ const identifySegmentUser = async () => {
400
+ const user = await getUserDetails();
401
+ const userId = user?.id;
402
+ const email = JSON.stringify(user?.email) ?? undefined;
403
+ if (userId) {
404
+ _identify(`${userId}`, {
405
+ email: email,
406
+ }).then();
407
+ }
408
+ };
392
409
  const handleSend = useCallback(async () => {
393
410
  const trimmed = input.trim();
394
411
  if (!trimmed) return;
@@ -397,98 +414,134 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
397
414
  }, [input, sendMessage]);
398
415
 
399
416
  const handleSuggestionSelect = useCallback(
400
- (value: string) => {
401
- sendMessage(value);
402
- },
403
- [sendMessage]
404
- );
405
-
406
- const handleReloadResults = useCallback(async (msg: ChatMessage) => {
407
- try {
408
- const payload = msg?.agent_response?.payload;
409
- if (!payload) return;
410
- const productsResult = await getProducts(payload);
411
- setProducts({ messageId: msg.id, data: productsResult });
412
- } catch (e) {
413
- console.error("Reload results failed", e);
414
- }
415
- }, []);
416
-
417
- useEffect(() => {
418
- DeviceEventEmitter.addListener("clearChat", handleClearChat);
419
- DeviceEventEmitter.addListener(
420
- "changePriceMode",
421
- (data: { priceMode: string }) => changePriceMode(data.priceMode)
422
- );
423
- changePriceMode(undefined);
424
-
425
- return () => {
426
- DeviceEventEmitter.removeAllListeners("clearChat");
427
- DeviceEventEmitter.removeAllListeners("changePriceMode");
428
- };
429
- }, []);
430
-
431
- const changePriceMode = async (_priceMode?: any) => {
432
- // Priority: passed argument > prop > storage
433
- if (!_priceMode && priceModeProp) {
434
- _priceMode = priceModeProp;
435
- }
436
- if (!_priceMode) {
437
- const userData = await Storage.getJSON<{ price_mode?: string }>(
438
- "userData",
439
- {}
440
- );
441
- _priceMode = userData?.price_mode;
442
- }
443
- setPriceMode(_priceMode || null);
417
+ (value: string) => {
418
+ sendMessage(value);
419
+ },
420
+ [sendMessage]
421
+ );
422
+
423
+ const handleReloadResults = useCallback(async (msg: ChatMessage) => {
424
+ const id = msg.id;
425
+ try {
426
+ setReloadLoadingIds((prev) => new Set(prev).add(id));
427
+ const payload = msg?.search_payload;
428
+ if (!payload || Object.keys(payload).length === 0) return;
429
+ const productsResult = await getProducts(payload);
430
+ setProductsByMsg((prev) => ({ ...prev, [id]: productsResult }));
431
+ } catch (e) {
432
+ console.error("Reload results failed", e);
433
+ } finally {
434
+ setReloadLoadingIds((prev) => {
435
+ const next = new Set(prev);
436
+ next.delete(id);
437
+ return next;
438
+ });
439
+ }
440
+ }, []);
441
+
442
+ useEffect(() => {
443
+ DeviceEventEmitter.addListener("clearChat", handleClearChat);
444
+ DeviceEventEmitter.addListener("changePriceMode", (data: { priceMode: string; }) => changePriceMode(data.priceMode));
445
+ changePriceMode();
446
+ identifySegmentUser();
447
+
448
+ return () => {
449
+ DeviceEventEmitter.removeAllListeners("clearChat");
450
+ DeviceEventEmitter.removeAllListeners("changePriceMode");
444
451
  };
445
-
446
- // Update conversationId when priceMode changes
447
- useEffect(() => {
448
- const loadConversationId = async () => {
449
- if (!priceMode) {
450
- setConversationId(null);
451
- return;
452
- }
453
- const conversations = await Storage.getJSON<Record<string, any>>(
454
- "vdbchat_conversations",
455
- {}
456
- );
457
- const storedId = conversations?.[priceMode]?.conversation_id ?? null;
458
- setConversationId(storedId ? String(storedId) : null);
459
- };
460
- loadConversationId();
461
- }, [priceMode, messages]); // Re-fetch when messages change (new conversation might be created)
462
-
463
- const handleClearChat = useCallback(async () => {
464
- try {
465
- const conversations = await Storage.getJSON<Record<string, any>>(
452
+ }, []);
453
+
454
+ const changePriceMode = async (_priceMode?: any) => {
455
+ if (!_priceMode && priceModeProp) {
456
+ _priceMode = priceModeProp;
457
+ }
458
+ if (!_priceMode) {
459
+ let userData = await Storage.getJSON(
460
+ "persist:userInfo",
461
+ { user: undefined } as { user?: string }
462
+ );
463
+ userData = userData && userData.user ? JSON.parse(userData.user) : {};
464
+ _priceMode = (userData as UserDetails)?.price_mode;
465
+ }
466
+ setPriceMode(_priceMode === undefined ? null : String(_priceMode));
467
+ };
468
+
469
+ const handleClearChat = useCallback(async () => {
470
+ try {
471
+ const conversations = await Storage.getJSON<Record<string, any>>(
472
+ "vdbchat_conversations",
473
+ {}
474
+ );
475
+ const storedId = conversations ? conversations[priceMode as any]?.conversation_id : null;
476
+ if (storedId && priceMode) {
477
+ const updatedConversations = conversations ?? {};
478
+ delete updatedConversations[priceMode];
479
+ await Storage.setJSON(
466
480
  "vdbchat_conversations",
467
- {}
481
+ updatedConversations
468
482
  );
469
- const storedId =
470
- conversations?.[priceMode as string]?.conversation_id ?? null;
471
- if (storedId && priceMode) {
472
- const updatedConversations = conversations || {};
473
- delete updatedConversations[priceMode];
474
- await Storage.setJSON("vdbchat_conversations", updatedConversations);
483
+ }
484
+ setMessages([]);
485
+ setAssistantResponse(undefined);
486
+ setProductsByMsg({});
487
+ setReloadLoadingIds(new Set());
488
+ setLoading(false);
489
+ setLoadingMessageId(null);
490
+ setTypingMessageId(null);
491
+ setTypingFullText("");
492
+
493
+ const fresh = await fetchInitialMessages(apiUrl, undefined, priceMode);
494
+ {
495
+ const normalised = normaliseMessages(fresh).reverse();
496
+ if (normalised.length === 0) {
497
+ const initialAssistant: ChatMessage = {
498
+ id: "",
499
+ role: "assistant",
500
+ text: "Hello! How can I help you today?",
501
+ createdAt: Date.now(),
502
+ isLoading: false,
503
+ suggestions: [
504
+ "Search Natural Diamonds",
505
+ "Search Lab-Grown Diamonds",
506
+ ],
507
+ reaction: "0",
508
+ };
509
+ setMessages([initialAssistant]);
510
+ } else {
511
+ setMessages(normalised);
475
512
  }
476
- setMessages([]);
477
- setAssistantResponse(undefined);
478
- setProducts(null);
479
- setLoading(false);
480
- setLoadingMessageId(null);
481
- setTypingMessageId(null);
482
- setTypingFullText("");
483
-
484
- const fresh = await fetchInitialMessages(apiUrl, undefined, priceMode);
485
- setMessages(normaliseMessages(fresh).reverse());
486
- onClearChat?.();
487
- } catch (err) {
488
- console.error("Failed to clear chat", err);
489
513
  }
490
- }, [apiUrl, onClearChat, priceMode]);
514
+ onClearChat?.();
515
+ } catch (err) {
516
+ // eslint-disable-next-line no-console
517
+ console.error("Failed to clear chat", err);
518
+ }
519
+ }, [apiUrl, priceMode]);
520
+
521
+ // "Thinking..." dot animation while waiting for the API
522
+ useEffect(() => {
523
+ if (!loadingMessageId || typingMessageId) return;
524
+
525
+ let step = 0;
526
+ const base = "Thinking";
527
+ const interval = setInterval(() => {
528
+ step = (step + 1) % 3;
529
+ const dots = ".".repeat(step + 1);
530
+ setMessages((prev) =>
531
+ prev.map((msg) =>
532
+ msg.id === loadingMessageId && msg.isLoading
533
+ ? { ...msg, text: `${base}${dots}` }
534
+ : msg
535
+ )
536
+ );
537
+ }, 500);
491
538
 
539
+ return () => {
540
+ clearInterval(interval);
541
+ };
542
+ }, [loadingMessageId, typingMessageId]);
543
+
544
+
492
545
  // Expose clearChat method via ref for external use (React Native)
493
546
  useImperativeHandle(
494
547
  ref,
@@ -498,33 +551,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
498
551
  [handleClearChat]
499
552
  );
500
553
 
501
- // "Thinking..." dot animation while waiting for the API
502
- useEffect(() => {
503
- if (!loadingMessageId || typingMessageId) {
504
- return;
505
- }
506
-
507
- let step = 0;
508
- const base = "Thinking";
509
- const msgId = loadingMessageId;
510
-
511
- const interval = setInterval(() => {
512
- step = (step + 1) % 3;
513
- const dots = ".".repeat(step + 1);
514
- setMessages((prev) =>
515
- prev.map((msg) =>
516
- msg.id === msgId && msg.isLoading
517
- ? { ...msg, text: `${base}${dots}` }
518
- : msg
519
- )
520
- );
521
- }, 500);
522
-
523
- return () => {
524
- clearInterval(interval);
525
- };
526
- }, [loadingMessageId, typingMessageId]);
527
-
528
554
  // Typewriter-style animation for assistant reply once it arrives
529
555
  const typingIndexRef = useRef(0);
530
556
 
@@ -577,73 +603,99 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
577
603
  style={[styles.container, { backgroundColor: theme.backgroundColor }]}
578
604
  >
579
605
  {Platform.OS === "web" && (
580
- <ChatHeader onClose={onClose} onClearChat={handleClearChat} />
606
+ <ChatHeader onClose={onClose} onClearChat={handleClearChat} isBetaMode={betaActive} />
581
607
  )}
582
- <ScrollView
583
- ref={scrollRef}
584
- style={modalHeight ? { height: modalHeight } : undefined}
585
- contentContainerStyle={{
586
- backgroundColor: theme?.listContentBackgroundColor || "#f5f5f5",
587
- ...styles.listContent,
588
- justifyContent: messages.length === 0 ? "center" : "flex-end",
589
- minHeight: modalHeight ? modalHeight : undefined,
590
- }}
591
- onContentSizeChange={() => {
592
- scrollRef.current?.scrollToEnd({ animated: false });
593
- }}
594
- >
595
- <View style={styles.emptyContainer}>
596
- <Text style={styles.emptyText}>
597
- {messages.length === 0
598
- ? "Start a conversation to Find the Perfect Diamond"
599
- : `Chat Started at ${formatToTime(messages[0].createdAt)}`}
600
- </Text>
601
- </View>
602
- {(() => {
603
- const ordered = [...messages];
604
- return ordered.map((item, index) => {
605
- const isLatest = index === ordered.length - 1;
606
- return (
607
- <View key={item.id + index}>
608
- <MessageBubble
609
- message={item}
610
- theme={theme}
611
- conversationId={conversationId}
612
- handleFeedbackAction={handleFeedbackAction}
613
- onReloadResults={handleReloadResults}
614
- />
615
- {item.role === "assistant" &&
616
- products &&
617
- products.messageId === item.id && (
608
+ <KeyboardAvoidingView
609
+ style={{ flex: 1 }}
610
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
611
+ keyboardVerticalOffset={-16}
612
+ >
613
+ <ScrollView
614
+ ref={scrollRef}
615
+ keyboardShouldPersistTaps="handled"
616
+ style={
617
+ modalHeight
618
+ ? { height: modalHeight, backgroundColor: "#FFFFFF" }
619
+ : { backgroundColor: "#FFFFFF" }
620
+ }
621
+ contentContainerStyle={{
622
+ backgroundColor: theme?.listContentBackgroundColor || "#FFFFFF",
623
+ ...styles.listContent,
624
+ justifyContent: messages.length === 0 ? "center" : "flex-end",
625
+ minHeight: modalHeight ? modalHeight : undefined,
626
+ }}
627
+ onContentSizeChange={() => {
628
+ scrollRef.current?.scrollToEnd({ animated: false });
629
+ }}
630
+ >
631
+ <View style={styles.emptyContainer}>
632
+ <Text style={styles.emptyText}>
633
+ {messages.length === 0
634
+ ? "Start a conversation to Find the Perfect Diamond"
635
+ : `Chat Started at ${formatToTime(messages[0].createdAt)}`}
636
+ </Text>
637
+ </View>
638
+ {(() => {
639
+ const ordered = [...messages];
640
+ return ordered.map((item, index) => {
641
+ const isLatest = index === ordered.length - 1;
642
+ const hasDiamonds = Boolean(
643
+ productsByMsg[item.id]?.response?.body?.diamonds &&
644
+ productsByMsg[item.id]?.response?.body?.diamonds.length > 0
645
+ );
646
+ return (
647
+ <View key={item.id + index} style={{ gap: 12 }}>
648
+ <MessageBubble
649
+ message={item}
650
+ theme={theme}
651
+ priceMode={priceMode as string}
652
+ handleFeedbackAction={handleFeedbackAction}
653
+ onReloadResults={handleReloadResults}
654
+ reloading={reloadLoadingIds.has(item.id)}
655
+ hasResults={hasDiamonds}
656
+ totalResults={productsByMsg[item.id]?.response?.header?.total_diamonds_found || 0}
657
+ shownResults={productsByMsg[item.id]?.response?.body?.diamonds.length || 0}
658
+ onSuggestionSelect={handleSuggestionSelect}
659
+ isLatest={isLatest}
660
+ isTyping={typingMessageId === item.id}
661
+ />
662
+ {item.role === "assistant" && hasDiamonds && (
618
663
  <ProductsList
619
- data={products?.data?.response?.body?.diamonds}
664
+ data={
665
+ productsByMsg[item.id]?.response?.body?.diamonds || []
666
+ }
667
+ totalResults={productsByMsg[item.id]?.response?.header?.total_diamonds_found || 0}
620
668
  onViewAll={onViewAll}
621
669
  onItemPress={onItemPress}
622
670
  />
623
671
  )}
624
- {item.role === "assistant" &&
625
- item.suggestions &&
626
- item.suggestions.length > 0 &&
627
- isLatest && (
628
- <SuggestionsRow
629
- suggestions={item.suggestions || []}
630
- onSelect={handleSuggestionSelect}
631
- />
632
- )}
633
- </View>
634
- );
635
- });
636
- })()}
637
- </ScrollView>
638
- <ChatInput
639
- value={input}
640
- onChangeText={setInput}
641
- onSend={handleSend}
642
- disabled={loading}
643
- placeholder={placeholder}
644
- theme={theme}
645
- inputRef={inputRef}
646
- />
672
+ {/* Suggestions are now rendered inside MessageBubble */}
673
+ <MessageMetaRow
674
+ message={item}
675
+ priceMode={priceMode || ""}
676
+ handleFeedbackAction={handleFeedbackAction}
677
+ onReloadResults={handleReloadResults}
678
+ reloading={reloadLoadingIds.has(item.id)}
679
+ hasResults={hasDiamonds}
680
+ />
681
+ </View>
682
+ );
683
+ });
684
+ })()}
685
+ </ScrollView>
686
+ <View style={ styles.bottomContainer }>
687
+ <ChatInput
688
+ value={input}
689
+ onChangeText={setInput}
690
+ onSend={handleSend}
691
+ disabled={loading}
692
+ placeholder={placeholder}
693
+ theme={theme}
694
+ inputRef={inputRef}
695
+ />
696
+ <BetaNotice active={betaActive} />
697
+ </View>
698
+ </KeyboardAvoidingView>
647
699
  </View>
648
700
  );
649
701
  }
@@ -669,11 +721,22 @@ const styles = StyleSheet.create({
669
721
  padding: 16,
670
722
  alignItems: "center",
671
723
  justifyContent: "center",
672
- backgroundColor: "#f5f5f5",
724
+ backgroundColor: "#FFFFFF",
673
725
  },
674
726
  emptyText: {
675
- fontSize: 14,
676
- fontWeight: "500",
677
- color: "#666666",
727
+ fontSize: 13,
728
+ fontWeight: "400",
729
+ color: "#4F4E57",
678
730
  },
731
+ bottomContainer: {
732
+ paddingVertical: 12,
733
+ paddingHorizontal: 16,
734
+ borderTopColor: "#E0E0E0",
735
+ borderTopWidth: 1,
736
+ flexDirection: "column",
737
+ justifyContent: "space-between",
738
+ alignItems: "center",
739
+ alignSelf: "stretch",
740
+ gap: 12,
741
+ }
679
742
  });