vdb-ai-chat 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/10.chat-widget.js +2 -0
- package/dist/10.chat-widget.js.LICENSE.txt +1 -0
- package/dist/104.chat-widget.js +1 -0
- package/dist/50.chat-widget.js +1 -0
- package/dist/521.chat-widget.js +1 -0
- package/dist/538.chat-widget.js +1 -1
- package/dist/572.chat-widget.js +1 -0
- package/dist/694.chat-widget.js +1 -0
- package/dist/chat-widget.js +1 -1
- package/lib/commonjs/api.js +4 -3
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/BetaNotice.js +38 -0
- package/lib/commonjs/components/BetaNotice.js.map +1 -0
- package/lib/commonjs/components/ChatHeader.js +27 -20
- package/lib/commonjs/components/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/ChatInput.js +20 -21
- package/lib/commonjs/components/ChatInput.js.map +1 -1
- package/lib/commonjs/components/ChatWidget.js +165 -92
- package/lib/commonjs/components/ChatWidget.js.map +1 -1
- package/lib/commonjs/components/LazyProductsFetcher.js +47 -0
- package/lib/commonjs/components/LazyProductsFetcher.js.map +1 -0
- package/lib/commonjs/components/MessageBubble.js +26 -90
- package/lib/commonjs/components/MessageBubble.js.map +1 -1
- package/lib/commonjs/components/MessageMetaRow.js +113 -0
- package/lib/commonjs/components/MessageMetaRow.js.map +1 -0
- package/lib/commonjs/components/ProductsGrid.js +139 -0
- package/lib/commonjs/components/ProductsGrid.js.map +1 -0
- package/lib/commonjs/components/ProductsList.js +22 -126
- package/lib/commonjs/components/ProductsList.js.map +1 -1
- package/lib/commonjs/components/ProductsListView.js +139 -0
- package/lib/commonjs/components/ProductsListView.js.map +1 -0
- package/lib/commonjs/components/SuggestionsRow.js +50 -27
- package/lib/commonjs/components/SuggestionsRow.js.map +1 -1
- package/lib/commonjs/components/utils.js +4 -3
- package/lib/commonjs/components/utils.js.map +1 -1
- package/lib/commonjs/hooks/useInViewport.js +42 -0
- package/lib/commonjs/hooks/useInViewport.js.map +1 -0
- package/lib/commonjs/index.web.js +86 -29
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/theme.js +4 -4
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/module/api.js +4 -3
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/BetaNotice.js +30 -0
- package/lib/module/components/BetaNotice.js.map +1 -0
- package/lib/module/components/ChatHeader.js +27 -20
- package/lib/module/components/ChatHeader.js.map +1 -1
- package/lib/module/components/ChatInput.js +20 -21
- package/lib/module/components/ChatInput.js.map +1 -1
- package/lib/module/components/ChatWidget.js +166 -93
- package/lib/module/components/ChatWidget.js.map +1 -1
- package/lib/module/components/LazyProductsFetcher.js +40 -0
- package/lib/module/components/LazyProductsFetcher.js.map +1 -0
- package/lib/module/components/MessageBubble.js +26 -92
- package/lib/module/components/MessageBubble.js.map +1 -1
- package/lib/module/components/MessageMetaRow.js +105 -0
- package/lib/module/components/MessageMetaRow.js.map +1 -0
- package/lib/module/components/ProductsGrid.js +133 -0
- package/lib/module/components/ProductsGrid.js.map +1 -0
- package/lib/module/components/ProductsList.js +21 -126
- package/lib/module/components/ProductsList.js.map +1 -1
- package/lib/module/components/ProductsListView.js +132 -0
- package/lib/module/components/ProductsListView.js.map +1 -0
- package/lib/module/components/SuggestionsRow.js +51 -28
- package/lib/module/components/SuggestionsRow.js.map +1 -1
- package/lib/module/components/utils.js +4 -3
- package/lib/module/components/utils.js.map +1 -1
- package/lib/module/hooks/useInViewport.js +36 -0
- package/lib/module/hooks/useInViewport.js.map +1 -0
- package/lib/module/index.web.js +86 -29
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/theme.js +4 -4
- package/lib/module/theme.js.map +1 -1
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/BetaNotice.d.ts +5 -0
- package/lib/typescript/components/BetaNotice.d.ts.map +1 -0
- package/lib/typescript/components/ChatHeader.d.ts +5 -2
- package/lib/typescript/components/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/ChatInput.d.ts.map +1 -1
- package/lib/typescript/components/ChatWidget.d.ts.map +1 -1
- package/lib/typescript/components/LazyProductsFetcher.d.ts +9 -0
- package/lib/typescript/components/LazyProductsFetcher.d.ts.map +1 -0
- package/lib/typescript/components/MessageBubble.d.ts +7 -3
- package/lib/typescript/components/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/components/MessageMetaRow.d.ts +14 -0
- package/lib/typescript/components/MessageMetaRow.d.ts.map +1 -0
- package/lib/typescript/components/ProductsGrid.d.ts +10 -0
- package/lib/typescript/components/ProductsGrid.d.ts.map +1 -0
- package/lib/typescript/components/ProductsList.d.ts +4 -2
- package/lib/typescript/components/ProductsList.d.ts.map +1 -1
- package/lib/typescript/components/ProductsListView.d.ts +10 -0
- package/lib/typescript/components/ProductsListView.d.ts.map +1 -0
- package/lib/typescript/components/SuggestionsRow.d.ts +2 -0
- package/lib/typescript/components/SuggestionsRow.d.ts.map +1 -1
- package/lib/typescript/components/utils.d.ts +1 -0
- package/lib/typescript/components/utils.d.ts.map +1 -1
- package/lib/typescript/hooks/useInViewport.d.ts +5 -0
- package/lib/typescript/hooks/useInViewport.d.ts.map +1 -0
- package/lib/typescript/index.web.d.ts +1 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +3 -1
- package/lib/typescript/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +4 -3
- package/src/components/BetaNotice.tsx +32 -0
- package/src/components/ChatHeader.tsx +32 -18
- package/src/components/ChatInput.tsx +23 -21
- package/src/components/ChatWidget.tsx +249 -159
- package/src/components/LazyProductsFetcher.tsx +41 -0
- package/src/components/MessageBubble.tsx +46 -148
- package/src/components/MessageMetaRow.tsx +199 -0
- package/src/components/ProductsGrid.tsx +163 -0
- package/src/components/ProductsList.tsx +20 -146
- package/src/components/ProductsListView.tsx +149 -0
- package/src/components/SuggestionsRow.tsx +61 -32
- package/src/components/utils.ts +6 -4
- package/src/hooks/useInViewport.ts +38 -0
- package/src/index.web.tsx +87 -32
- package/src/theme.ts +4 -4
- package/src/types.ts +3 -2
- package/dist/751.chat-widget.js +0 -1
- package/lib/commonjs/contexts/SegmentClientContext.js +0 -19
- package/lib/commonjs/contexts/SegmentClientContext.js.map +0 -1
- package/lib/module/contexts/SegmentClientContext.js +0 -10
- package/lib/module/contexts/SegmentClientContext.js.map +0 -1
- package/lib/typescript/contexts/SegmentClientContext.d.ts +0 -9
- package/lib/typescript/contexts/SegmentClientContext.d.ts.map +0 -1
- package/src/contexts/SegmentClientContext.tsx +0 -20
|
@@ -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,9 +31,15 @@ import {
|
|
|
29
31
|
sendUserMessage,
|
|
30
32
|
} from "../api";
|
|
31
33
|
import ChatHeader from "./ChatHeader";
|
|
32
|
-
import
|
|
34
|
+
import BetaNotice from "./BetaNotice";
|
|
33
35
|
import ProductsList from "./ProductsList";
|
|
34
|
-
import
|
|
36
|
+
import LazyProductsFetcher from "./LazyProductsFetcher";
|
|
37
|
+
import {
|
|
38
|
+
FeedbackAction,
|
|
39
|
+
formatToTime,
|
|
40
|
+
getUserDetails,
|
|
41
|
+
UserDetails,
|
|
42
|
+
} from "./utils";
|
|
35
43
|
import { useUserAnalytics } from "../hooks/useAnalytics";
|
|
36
44
|
import { Storage } from "../storage";
|
|
37
45
|
|
|
@@ -39,7 +47,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
39
47
|
(
|
|
40
48
|
{
|
|
41
49
|
apiUrl,
|
|
42
|
-
userId: userIdProp,
|
|
43
50
|
userToken: userTokenProp,
|
|
44
51
|
priceMode: priceModeProp,
|
|
45
52
|
modalHeight,
|
|
@@ -50,6 +57,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
50
57
|
onClearChat,
|
|
51
58
|
onViewAllPress,
|
|
52
59
|
onItemPress: onItemPressProp,
|
|
60
|
+
isBetaMode: isBetaModeProp,
|
|
53
61
|
},
|
|
54
62
|
ref
|
|
55
63
|
) => {
|
|
@@ -64,6 +72,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
64
72
|
const [assistantResponse, setAssistantResponse] = useState<
|
|
65
73
|
ChatMessage | undefined
|
|
66
74
|
>(undefined);
|
|
75
|
+
const [scrollY, setScrollY] = useState(0);
|
|
67
76
|
const [productsByMsg, setProductsByMsg] = useState<Record<string, any>>({});
|
|
68
77
|
const [reloadLoadingIds, setReloadLoadingIds] = useState<Set<string>>(
|
|
69
78
|
new Set()
|
|
@@ -71,26 +80,17 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
71
80
|
const [priceMode, setPriceMode] = useState<string | null>(
|
|
72
81
|
priceModeProp || null
|
|
73
82
|
);
|
|
74
|
-
const [conversationId, setConversationId] = useState<string | null>(null);
|
|
75
|
-
const [userId, setUserId] = useState<string>(userIdProp || "");
|
|
76
83
|
const [userToken, setUserToken] = useState<string>(userTokenProp || "");
|
|
77
84
|
const scrollRef = useRef<ScrollView | null>(null);
|
|
78
85
|
const inputRef = useRef<TextInput | null>(null);
|
|
79
86
|
const theme = useMemo(() => mergeTheme(themeOverrides), [themeOverrides]);
|
|
80
87
|
const { _identify } = useUserAnalytics();
|
|
81
|
-
|
|
88
|
+
const betaActive = Boolean(isBetaModeProp);
|
|
89
|
+
const noResultsText =
|
|
90
|
+
"No results found for your search. Try adjusting your filters or query.";
|
|
82
91
|
useEffect(() => {
|
|
83
92
|
const loadAuthData = async () => {
|
|
84
93
|
try {
|
|
85
|
-
if (!userIdProp) {
|
|
86
|
-
const userData = await Storage.getJSON<{ id?: string }>(
|
|
87
|
-
"userData",
|
|
88
|
-
{}
|
|
89
|
-
);
|
|
90
|
-
if (userData?.id) {
|
|
91
|
-
setUserId(userData.id);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
94
|
if (!userTokenProp) {
|
|
95
95
|
const token = await Storage.getItem("token");
|
|
96
96
|
if (token) {
|
|
@@ -102,7 +102,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
102
102
|
}
|
|
103
103
|
};
|
|
104
104
|
loadAuthData();
|
|
105
|
-
}, [
|
|
105
|
+
}, [userTokenProp]);
|
|
106
106
|
|
|
107
107
|
const onViewAll = useCallback(() => {
|
|
108
108
|
const searchPayload = JSON.stringify(assistantResponse?.search_payload);
|
|
@@ -116,11 +116,9 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
116
116
|
payload.lab_grown === "true" ? "lab_grown_diamond" : "diamond"
|
|
117
117
|
}&fromNewFilterScreen=false&filterSplitStep=1§ionName=Single%20Stones&breadCrumbLabel=Stone%20Search&enterSecondFlow=false&saved_search=${searchPayload}`;
|
|
118
118
|
|
|
119
|
-
// If custom handler is provided (React Native), use it
|
|
120
119
|
if (onViewAllPress) {
|
|
121
120
|
onViewAllPress(deepLinkUrl, payload);
|
|
122
121
|
} else if (Platform.OS === "web") {
|
|
123
|
-
// Default behavior for web
|
|
124
122
|
window.open(deepLinkUrl, "_parent");
|
|
125
123
|
}
|
|
126
124
|
}, [assistantResponse, apiUrl, priceMode, onViewAllPress]);
|
|
@@ -136,27 +134,22 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
136
134
|
item.type
|
|
137
135
|
}&priceMode=${priceMode}&breadCrumbLabel=Stone%20Details`;
|
|
138
136
|
|
|
139
|
-
// If custom handler is provided (React Native), use it
|
|
140
137
|
if (onItemPressProp) {
|
|
141
138
|
onItemPressProp(deepLinkUrl, item);
|
|
142
139
|
} else if (Platform.OS === "web") {
|
|
143
|
-
// Default behavior for web
|
|
144
140
|
window.open(deepLinkUrl, "_parent");
|
|
145
141
|
}
|
|
146
142
|
},
|
|
147
143
|
[apiUrl, priceMode, onItemPressProp]
|
|
148
144
|
);
|
|
149
145
|
|
|
150
|
-
const hasAuth = useMemo(
|
|
151
|
-
() => Boolean(userId || userToken),
|
|
152
|
-
[userId, userToken]
|
|
153
|
-
);
|
|
146
|
+
const hasAuth = useMemo(() => Boolean(userToken), [userToken]);
|
|
154
147
|
|
|
155
148
|
const apiParams: ChatApiParams = useMemo(
|
|
156
149
|
() => ({
|
|
157
|
-
conversationId: userToken
|
|
150
|
+
conversationId: userToken,
|
|
158
151
|
}),
|
|
159
|
-
[
|
|
152
|
+
[userToken]
|
|
160
153
|
);
|
|
161
154
|
|
|
162
155
|
const handleFeedbackAction = useCallback(
|
|
@@ -166,7 +159,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
166
159
|
message_id: string
|
|
167
160
|
) => {
|
|
168
161
|
try {
|
|
169
|
-
// Use functional update to get current messages without dependency
|
|
170
162
|
let currentReaction = "0";
|
|
171
163
|
setMessages((prev) => {
|
|
172
164
|
const msg = prev.find((m) => m.id === message_id);
|
|
@@ -197,7 +189,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
197
189
|
[]
|
|
198
190
|
);
|
|
199
191
|
|
|
200
|
-
// Load initial history on mount
|
|
201
192
|
useEffect(() => {
|
|
202
193
|
let cancelled = false;
|
|
203
194
|
|
|
@@ -210,7 +201,30 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
210
201
|
priceMode
|
|
211
202
|
);
|
|
212
203
|
if (!cancelled) {
|
|
213
|
-
|
|
204
|
+
const normalised = normaliseMessages(initial).reverse();
|
|
205
|
+
if (normalised.length === 0) {
|
|
206
|
+
const initialAssistant: ChatMessage = {
|
|
207
|
+
id: "",
|
|
208
|
+
role: "assistant",
|
|
209
|
+
text: "Hello! How can I help you today?",
|
|
210
|
+
createdAt: Date.now(),
|
|
211
|
+
isLoading: false,
|
|
212
|
+
suggestions: [
|
|
213
|
+
"Search Natural Diamonds",
|
|
214
|
+
"Search Lab-Grown Diamonds",
|
|
215
|
+
],
|
|
216
|
+
reaction: "0",
|
|
217
|
+
};
|
|
218
|
+
setMessages([initialAssistant]);
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
scrollRef.current?.scrollToEnd({ animated: false });
|
|
221
|
+
}, 0);
|
|
222
|
+
} else {
|
|
223
|
+
setMessages(normalised);
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
scrollRef.current?.scrollToEnd({ animated: false });
|
|
226
|
+
}, 0);
|
|
227
|
+
}
|
|
214
228
|
}
|
|
215
229
|
} catch (error) {
|
|
216
230
|
console.error("Failed to fetch initial messages", error);
|
|
@@ -244,6 +258,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
244
258
|
role: "assistant",
|
|
245
259
|
text: "Thinking",
|
|
246
260
|
createdAt: Date.now(),
|
|
261
|
+
reaction: "0",
|
|
247
262
|
isLoading: true,
|
|
248
263
|
};
|
|
249
264
|
|
|
@@ -284,7 +299,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
284
299
|
setAssistantResponse(latestAssistant);
|
|
285
300
|
|
|
286
301
|
if (latestAssistant?.text) {
|
|
287
|
-
// If there is an agent_response, try to fetch products first
|
|
288
302
|
if (
|
|
289
303
|
latestAssistant?.search_payload &&
|
|
290
304
|
typeof latestAssistant.search_payload === "object" &&
|
|
@@ -302,9 +316,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
302
316
|
productsResult.response.body.diamonds.length > 0;
|
|
303
317
|
|
|
304
318
|
if (!hasDiamonds) {
|
|
305
|
-
const noResultsText =
|
|
306
|
-
"No results found for your search. Try adjusting your filters or query.";
|
|
307
|
-
|
|
308
319
|
setLoadingMessageId(null);
|
|
309
320
|
setTypingMessageId(null);
|
|
310
321
|
setTypingFullText("");
|
|
@@ -317,7 +328,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
317
328
|
id: latestAssistant?.id ?? msg.id,
|
|
318
329
|
text: noResultsText,
|
|
319
330
|
isLoading: false,
|
|
320
|
-
suggestions: [],
|
|
321
331
|
}
|
|
322
332
|
: msg
|
|
323
333
|
)
|
|
@@ -325,10 +335,10 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
325
335
|
inputRef.current?.focus();
|
|
326
336
|
return;
|
|
327
337
|
}
|
|
328
|
-
setProductsByMsg({
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
});
|
|
338
|
+
setProductsByMsg((prev) => ({
|
|
339
|
+
...prev,
|
|
340
|
+
[latestAssistant.id]: productsResult,
|
|
341
|
+
}));
|
|
332
342
|
}
|
|
333
343
|
setLoadingMessageId(null);
|
|
334
344
|
|
|
@@ -346,10 +356,10 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
346
356
|
)
|
|
347
357
|
);
|
|
348
358
|
|
|
349
|
-
// Use setTimeout to ensure state is updated before starting typewriter
|
|
350
359
|
setTimeout(() => {
|
|
351
360
|
setTypingMessageId(latestAssistant.id);
|
|
352
361
|
setTypingFullText(latestAssistant.text);
|
|
362
|
+
scrollRef.current?.scrollToEnd({ animated: true });
|
|
353
363
|
}, 50);
|
|
354
364
|
} else {
|
|
355
365
|
setMessages((prev) =>
|
|
@@ -434,7 +444,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
434
444
|
"changePriceMode",
|
|
435
445
|
(data: { priceMode: string }) => changePriceMode(data.priceMode)
|
|
436
446
|
);
|
|
437
|
-
changePriceMode(
|
|
447
|
+
changePriceMode();
|
|
438
448
|
identifySegmentUser();
|
|
439
449
|
|
|
440
450
|
return () => {
|
|
@@ -444,47 +454,30 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
444
454
|
}, []);
|
|
445
455
|
|
|
446
456
|
const changePriceMode = async (_priceMode?: any) => {
|
|
447
|
-
// Priority: passed argument > prop > storage
|
|
448
457
|
if (!_priceMode && priceModeProp) {
|
|
449
458
|
_priceMode = priceModeProp;
|
|
450
459
|
}
|
|
451
460
|
if (!_priceMode) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
);
|
|
456
|
-
_priceMode = userData?.price_mode;
|
|
461
|
+
let userData = await Storage.getJSON("persist:userInfo", {
|
|
462
|
+
user: undefined,
|
|
463
|
+
} as { user?: string });
|
|
464
|
+
userData = userData && userData.user ? JSON.parse(userData.user) : {};
|
|
465
|
+
_priceMode = (userData as UserDetails)?.price_mode;
|
|
457
466
|
}
|
|
458
|
-
setPriceMode(_priceMode
|
|
467
|
+
setPriceMode(_priceMode === undefined ? null : String(_priceMode));
|
|
459
468
|
};
|
|
460
469
|
|
|
461
|
-
// Update conversationId when priceMode changes
|
|
462
|
-
useEffect(() => {
|
|
463
|
-
const loadConversationId = async () => {
|
|
464
|
-
if (!priceMode) {
|
|
465
|
-
setConversationId(null);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
const conversations = await Storage.getJSON<Record<string, any>>(
|
|
469
|
-
"vdbchat_conversations",
|
|
470
|
-
{}
|
|
471
|
-
);
|
|
472
|
-
const storedId = conversations?.[priceMode]?.conversation_id ?? null;
|
|
473
|
-
setConversationId(storedId ? String(storedId) : null);
|
|
474
|
-
};
|
|
475
|
-
loadConversationId();
|
|
476
|
-
}, [priceMode, messages]); // Re-fetch when messages change (new conversation might be created)
|
|
477
|
-
|
|
478
470
|
const handleClearChat = useCallback(async () => {
|
|
479
471
|
try {
|
|
480
472
|
const conversations = await Storage.getJSON<Record<string, any>>(
|
|
481
473
|
"vdbchat_conversations",
|
|
482
474
|
{}
|
|
483
475
|
);
|
|
484
|
-
const storedId =
|
|
485
|
-
conversations
|
|
476
|
+
const storedId = conversations
|
|
477
|
+
? conversations[priceMode as any]?.conversation_id
|
|
478
|
+
: null;
|
|
486
479
|
if (storedId && priceMode) {
|
|
487
|
-
const updatedConversations = conversations
|
|
480
|
+
const updatedConversations = conversations ?? {};
|
|
488
481
|
delete updatedConversations[priceMode];
|
|
489
482
|
await Storage.setJSON("vdbchat_conversations", updatedConversations);
|
|
490
483
|
}
|
|
@@ -498,38 +491,44 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
498
491
|
setTypingFullText("");
|
|
499
492
|
|
|
500
493
|
const fresh = await fetchInitialMessages(apiUrl, undefined, priceMode);
|
|
501
|
-
|
|
494
|
+
const normalised = normaliseMessages(fresh).reverse();
|
|
495
|
+
if (normalised.length === 0) {
|
|
496
|
+
const initialAssistant: ChatMessage = {
|
|
497
|
+
id: "",
|
|
498
|
+
role: "assistant",
|
|
499
|
+
text: "Hello! How can I help you today?",
|
|
500
|
+
createdAt: Date.now(),
|
|
501
|
+
isLoading: false,
|
|
502
|
+
suggestions: [
|
|
503
|
+
"Search Natural Diamonds",
|
|
504
|
+
"Search Lab-Grown Diamonds",
|
|
505
|
+
],
|
|
506
|
+
reaction: "0",
|
|
507
|
+
};
|
|
508
|
+
setMessages([initialAssistant]);
|
|
509
|
+
} else {
|
|
510
|
+
setMessages(normalised);
|
|
511
|
+
}
|
|
512
|
+
setTimeout(() => {
|
|
513
|
+
scrollRef.current?.scrollToEnd({ animated: false });
|
|
514
|
+
}, 500);
|
|
502
515
|
onClearChat?.();
|
|
503
516
|
} catch (err) {
|
|
504
517
|
console.error("Failed to clear chat", err);
|
|
505
518
|
}
|
|
506
|
-
}, [apiUrl,
|
|
507
|
-
|
|
508
|
-
// Expose clearChat method via ref for external use (React Native)
|
|
509
|
-
useImperativeHandle(
|
|
510
|
-
ref,
|
|
511
|
-
() => ({
|
|
512
|
-
clearChat: handleClearChat,
|
|
513
|
-
}),
|
|
514
|
-
[handleClearChat]
|
|
515
|
-
);
|
|
519
|
+
}, [apiUrl, priceMode]);
|
|
516
520
|
|
|
517
|
-
// "Thinking..." dot animation while waiting for the API
|
|
518
521
|
useEffect(() => {
|
|
519
|
-
if (!loadingMessageId || typingMessageId)
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
+
if (!loadingMessageId || typingMessageId) return;
|
|
522
523
|
|
|
523
524
|
let step = 0;
|
|
524
525
|
const base = "Thinking";
|
|
525
|
-
const msgId = loadingMessageId;
|
|
526
|
-
|
|
527
526
|
const interval = setInterval(() => {
|
|
528
527
|
step = (step + 1) % 3;
|
|
529
528
|
const dots = ".".repeat(step + 1);
|
|
530
529
|
setMessages((prev) =>
|
|
531
530
|
prev.map((msg) =>
|
|
532
|
-
msg.id ===
|
|
531
|
+
msg.id === loadingMessageId && msg.isLoading
|
|
533
532
|
? { ...msg, text: `${base}${dots}` }
|
|
534
533
|
: msg
|
|
535
534
|
)
|
|
@@ -541,7 +540,14 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
541
540
|
};
|
|
542
541
|
}, [loadingMessageId, typingMessageId]);
|
|
543
542
|
|
|
544
|
-
|
|
543
|
+
useImperativeHandle(
|
|
544
|
+
ref,
|
|
545
|
+
() => ({
|
|
546
|
+
clearChat: handleClearChat,
|
|
547
|
+
}),
|
|
548
|
+
[handleClearChat]
|
|
549
|
+
);
|
|
550
|
+
|
|
545
551
|
const typingIndexRef = useRef(0);
|
|
546
552
|
|
|
547
553
|
useEffect(() => {
|
|
@@ -593,82 +599,155 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
593
599
|
style={[styles.container, { backgroundColor: theme.backgroundColor }]}
|
|
594
600
|
>
|
|
595
601
|
{Platform.OS === "web" && (
|
|
596
|
-
<ChatHeader
|
|
602
|
+
<ChatHeader
|
|
603
|
+
onClose={onClose}
|
|
604
|
+
onClearChat={handleClearChat}
|
|
605
|
+
isBetaMode={betaActive}
|
|
606
|
+
/>
|
|
597
607
|
)}
|
|
598
|
-
<
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
? { height: modalHeight, backgroundColor: "#f5f5f5" }
|
|
603
|
-
: { backgroundColor: "#f5f5f5" }
|
|
604
|
-
}
|
|
605
|
-
contentContainerStyle={{
|
|
606
|
-
backgroundColor: theme?.listContentBackgroundColor || "#f5f5f5",
|
|
607
|
-
...styles.listContent,
|
|
608
|
-
justifyContent: messages.length === 0 ? "center" : "flex-end",
|
|
609
|
-
minHeight: modalHeight ? modalHeight : undefined,
|
|
610
|
-
}}
|
|
611
|
-
onContentSizeChange={() => {
|
|
612
|
-
scrollRef.current?.scrollToEnd({ animated: false });
|
|
613
|
-
}}
|
|
608
|
+
<KeyboardAvoidingView
|
|
609
|
+
style={{ flex: 1 }}
|
|
610
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
611
|
+
keyboardVerticalOffset={Platform.OS === "ios" ? 100 : -16}
|
|
614
612
|
>
|
|
615
|
-
<
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
613
|
+
<ScrollView
|
|
614
|
+
ref={scrollRef}
|
|
615
|
+
keyboardShouldPersistTaps="handled"
|
|
616
|
+
onScroll={(e) => setScrollY(e.nativeEvent.contentOffset.y)}
|
|
617
|
+
scrollEventThrottle={16}
|
|
618
|
+
style={
|
|
619
|
+
modalHeight
|
|
620
|
+
? { height: modalHeight, backgroundColor: "#FFFFFF" }
|
|
621
|
+
: { backgroundColor: "#FFFFFF" }
|
|
622
|
+
}
|
|
623
|
+
contentContainerStyle={{
|
|
624
|
+
backgroundColor: theme?.listContentBackgroundColor || "#FFFFFF",
|
|
625
|
+
...styles.listContent,
|
|
626
|
+
justifyContent: messages.length === 0 ? "center" : "flex-end",
|
|
627
|
+
minHeight: modalHeight ? modalHeight : undefined,
|
|
628
|
+
}}
|
|
629
|
+
onContentSizeChange={() => {
|
|
630
|
+
scrollRef.current?.scrollToEnd({ animated: false });
|
|
631
|
+
}}
|
|
632
|
+
>
|
|
633
|
+
<View style={styles.emptyContainer}>
|
|
634
|
+
<Text style={styles.emptyText}>
|
|
635
|
+
{messages.length === 0
|
|
636
|
+
? "Start a conversation to Find the Perfect Diamond"
|
|
637
|
+
: `Chat Started at ${formatToTime(messages[0].createdAt)}`}
|
|
638
|
+
</Text>
|
|
639
|
+
</View>
|
|
640
|
+
{(() => {
|
|
641
|
+
const ordered = [...messages];
|
|
642
|
+
return ordered.map((item, index) => {
|
|
643
|
+
const isLatest = index === ordered.length - 1;
|
|
644
|
+
const hasDiamonds = Boolean(
|
|
645
|
+
productsByMsg[item.id]?.response?.body?.diamonds &&
|
|
646
|
+
productsByMsg[item.id]?.response?.body?.diamonds.length > 0
|
|
647
|
+
);
|
|
648
|
+
return (
|
|
649
|
+
<View key={item.id + index} style={{ gap: 12 }}>
|
|
650
|
+
<MessageBubble
|
|
651
|
+
message={item}
|
|
652
|
+
theme={theme}
|
|
653
|
+
priceMode={priceMode as string}
|
|
654
|
+
handleFeedbackAction={handleFeedbackAction}
|
|
655
|
+
onReloadResults={handleReloadResults}
|
|
656
|
+
reloading={reloadLoadingIds.has(item.id)}
|
|
657
|
+
hasResults={hasDiamonds}
|
|
658
|
+
totalResults={
|
|
659
|
+
productsByMsg[item.id]?.response?.header
|
|
660
|
+
?.total_diamonds_found || 0
|
|
661
|
+
}
|
|
662
|
+
shownResults={
|
|
663
|
+
productsByMsg[item.id]?.response?.body?.diamonds
|
|
664
|
+
.length || 0
|
|
665
|
+
}
|
|
666
|
+
onSuggestionSelect={handleSuggestionSelect}
|
|
667
|
+
isLatest={isLatest}
|
|
668
|
+
isTyping={typingMessageId === item.id}
|
|
669
|
+
/>
|
|
670
|
+
{/* Trigger product fetch when this area enters viewport */}
|
|
671
|
+
{item.role === "assistant" &&
|
|
672
|
+
item.search_payload &&
|
|
673
|
+
Object.keys(item.search_payload).length > 0 &&
|
|
674
|
+
!productsByMsg[item.id] && (
|
|
675
|
+
<LazyProductsFetcher
|
|
676
|
+
messageId={item.id}
|
|
677
|
+
payload={item.search_payload}
|
|
678
|
+
onFetched={(id, result) => {
|
|
679
|
+
setProductsByMsg((prev) => ({
|
|
680
|
+
...prev,
|
|
681
|
+
[id]: result,
|
|
682
|
+
}));
|
|
683
|
+
const diamonds = result?.response?.body?.diamonds;
|
|
684
|
+
const hasAny =
|
|
685
|
+
Array.isArray(diamonds) && diamonds.length > 0;
|
|
686
|
+
if (hasAny) {
|
|
687
|
+
try {
|
|
688
|
+
scrollRef.current?.scrollTo({
|
|
689
|
+
x: 0,
|
|
690
|
+
y: scrollY + 120,
|
|
691
|
+
animated: true,
|
|
692
|
+
});
|
|
693
|
+
} catch (_) {}
|
|
694
|
+
} else {
|
|
695
|
+
setMessages((prev) =>
|
|
696
|
+
prev.map((msg) =>
|
|
697
|
+
msg.id === id
|
|
698
|
+
? {
|
|
699
|
+
...msg,
|
|
700
|
+
id: msg.id,
|
|
701
|
+
text: noResultsText,
|
|
702
|
+
isLoading: false,
|
|
703
|
+
}
|
|
704
|
+
: msg
|
|
705
|
+
)
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}}
|
|
709
|
+
/>
|
|
710
|
+
)}
|
|
711
|
+
{item.role === "assistant" && hasDiamonds && (
|
|
643
712
|
<ProductsList
|
|
644
|
-
data={
|
|
713
|
+
data={
|
|
714
|
+
productsByMsg[item.id]?.response?.body?.diamonds || []
|
|
715
|
+
}
|
|
716
|
+
totalResults={
|
|
717
|
+
productsByMsg[item.id]?.response?.header
|
|
718
|
+
?.total_diamonds_found || 0
|
|
719
|
+
}
|
|
645
720
|
onViewAll={onViewAll}
|
|
646
721
|
onItemPress={onItemPress}
|
|
647
722
|
/>
|
|
648
723
|
)}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
724
|
+
{/* Suggestions are now rendered inside MessageBubble */}
|
|
725
|
+
<MessageMetaRow
|
|
726
|
+
message={item}
|
|
727
|
+
priceMode={priceMode || ""}
|
|
728
|
+
handleFeedbackAction={handleFeedbackAction}
|
|
729
|
+
onReloadResults={handleReloadResults}
|
|
730
|
+
reloading={reloadLoadingIds.has(item.id)}
|
|
731
|
+
hasResults={hasDiamonds}
|
|
732
|
+
/>
|
|
733
|
+
</View>
|
|
734
|
+
);
|
|
735
|
+
});
|
|
736
|
+
})()}
|
|
737
|
+
</ScrollView>
|
|
738
|
+
<View style={styles.bottomContainer}>
|
|
739
|
+
<ChatInput
|
|
740
|
+
value={input}
|
|
741
|
+
onChangeText={setInput}
|
|
742
|
+
onSend={handleSend}
|
|
743
|
+
disabled={loading}
|
|
744
|
+
placeholder={placeholder}
|
|
745
|
+
theme={theme}
|
|
746
|
+
inputRef={inputRef}
|
|
747
|
+
/>
|
|
748
|
+
<BetaNotice active={betaActive} />
|
|
749
|
+
</View>
|
|
750
|
+
</KeyboardAvoidingView>
|
|
672
751
|
</View>
|
|
673
752
|
);
|
|
674
753
|
}
|
|
@@ -694,11 +773,22 @@ const styles = StyleSheet.create({
|
|
|
694
773
|
padding: 16,
|
|
695
774
|
alignItems: "center",
|
|
696
775
|
justifyContent: "center",
|
|
697
|
-
backgroundColor: "#
|
|
776
|
+
backgroundColor: "#FFFFFF",
|
|
698
777
|
},
|
|
699
778
|
emptyText: {
|
|
700
|
-
fontSize:
|
|
701
|
-
fontWeight: "
|
|
702
|
-
color: "#
|
|
779
|
+
fontSize: 13,
|
|
780
|
+
fontWeight: "400",
|
|
781
|
+
color: "#4F4E57",
|
|
782
|
+
},
|
|
783
|
+
bottomContainer: {
|
|
784
|
+
paddingVertical: 12,
|
|
785
|
+
paddingHorizontal: 16,
|
|
786
|
+
borderTopColor: "#E0E0E0",
|
|
787
|
+
borderTopWidth: 1,
|
|
788
|
+
flexDirection: "column",
|
|
789
|
+
justifyContent: "space-between",
|
|
790
|
+
alignItems: "center",
|
|
791
|
+
alignSelf: "stretch",
|
|
792
|
+
gap: 12,
|
|
703
793
|
},
|
|
704
794
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { getProducts } from "../api";
|
|
4
|
+
import { useInViewport } from "../hooks/useInViewport";
|
|
5
|
+
|
|
6
|
+
interface LazyProductsFetcherProps {
|
|
7
|
+
messageId: string;
|
|
8
|
+
payload: Record<string, any> | undefined | null;
|
|
9
|
+
onFetched: (messageId: string, data: any) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LazyProductsFetcher: React.FC<LazyProductsFetcherProps> = ({
|
|
13
|
+
messageId,
|
|
14
|
+
payload,
|
|
15
|
+
onFetched,
|
|
16
|
+
}) => {
|
|
17
|
+
const { ref, inView } = useInViewport<HTMLDivElement>();
|
|
18
|
+
const fetchedRef = useRef(false);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (fetchedRef.current) return;
|
|
22
|
+
if (!payload || typeof payload !== "object") return;
|
|
23
|
+
if (!inView) return;
|
|
24
|
+
|
|
25
|
+
fetchedRef.current = true;
|
|
26
|
+
(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const result = await getProducts(payload);
|
|
29
|
+
onFetched(messageId, result);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.error("Lazy fetch products failed", e);
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
}, [inView, messageId, onFetched, payload]);
|
|
36
|
+
|
|
37
|
+
// Sentinel element; no visible UI needed
|
|
38
|
+
return <View ref={ref as any} style={{ height: 1 }} />;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default LazyProductsFetcher;
|