vdb-ai-chat 1.0.13 → 1.0.15
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/chat-widget.js +1 -1
- package/lib/commonjs/api.js +47 -1
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/BetaNotice.js +1 -1
- package/lib/commonjs/components/BetaNotice.js.map +1 -1
- package/lib/commonjs/components/ChatInput.js +1 -1
- package/lib/commonjs/components/ChatInput.js.map +1 -1
- package/lib/commonjs/components/ChatWidget.js +142 -36
- package/lib/commonjs/components/ChatWidget.js.map +1 -1
- package/lib/commonjs/components/ProductsListView.js +26 -20
- package/lib/commonjs/components/ProductsListView.js.map +1 -1
- package/lib/commonjs/components/utils.js +37 -1
- package/lib/commonjs/components/utils.js.map +1 -1
- package/lib/module/api.js +46 -1
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/BetaNotice.js +1 -1
- package/lib/module/components/BetaNotice.js.map +1 -1
- package/lib/module/components/ChatInput.js +1 -1
- package/lib/module/components/ChatInput.js.map +1 -1
- package/lib/module/components/ChatWidget.js +144 -38
- package/lib/module/components/ChatWidget.js.map +1 -1
- package/lib/module/components/ProductsListView.js +27 -21
- package/lib/module/components/ProductsListView.js.map +1 -1
- package/lib/module/components/utils.js +33 -0
- package/lib/module/components/utils.js.map +1 -1
- package/lib/typescript/api.d.ts +12 -0
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/ChatWidget.d.ts.map +1 -1
- package/lib/typescript/components/ProductsListView.d.ts.map +1 -1
- package/lib/typescript/components/utils.d.ts +3 -0
- package/lib/typescript/components/utils.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +2 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +71 -3
- package/src/components/BetaNotice.tsx +1 -1
- package/src/components/ChatInput.tsx +1 -1
- package/src/components/ChatWidget.tsx +196 -33
- package/src/components/ProductsListView.tsx +39 -32
- package/src/components/utils.ts +36 -0
- package/src/types.ts +2 -0
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
Keyboard,
|
|
18
18
|
DeviceEventEmitter,
|
|
19
19
|
Platform,
|
|
20
|
+
ActivityIndicator,
|
|
20
21
|
} from "react-native";
|
|
21
22
|
import { ChatInput } from "./ChatInput";
|
|
22
23
|
import { MessageBubble } from "./MessageBubble";
|
|
@@ -26,6 +27,7 @@ import { mergeTheme } from "../theme";
|
|
|
26
27
|
import {
|
|
27
28
|
ChatApiParams,
|
|
28
29
|
fetchInitialMessages,
|
|
30
|
+
fetchOlderMessages,
|
|
29
31
|
getProducts,
|
|
30
32
|
handleFeedbackActionApi,
|
|
31
33
|
normaliseMessages,
|
|
@@ -63,6 +65,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
63
65
|
onViewAllPress,
|
|
64
66
|
onItemPress: onItemPressProp,
|
|
65
67
|
isBetaMode: isBetaModeProp,
|
|
68
|
+
activeProductType,
|
|
66
69
|
},
|
|
67
70
|
ref
|
|
68
71
|
) => {
|
|
@@ -74,8 +77,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
74
77
|
);
|
|
75
78
|
const [typingMessageId, setTypingMessageId] = useState<string | null>(null);
|
|
76
79
|
const [typingFullText, setTypingFullText] = useState<string>("");
|
|
77
|
-
const [scrollY, setScrollY] = useState(0);
|
|
78
|
-
const [autoScroll, setAutoScroll] = useState(true);
|
|
79
80
|
const [productsByMsg, setProductsByMsg] = useState<Record<string, any>>({});
|
|
80
81
|
const [reloadLoadingIds, setReloadLoadingIds] = useState<Set<string>>(
|
|
81
82
|
new Set()
|
|
@@ -84,8 +85,21 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
84
85
|
priceModeProp || null
|
|
85
86
|
);
|
|
86
87
|
const [userToken, setUserToken] = useState<string>(userTokenProp || "");
|
|
88
|
+
// Pagination state
|
|
89
|
+
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
90
|
+
const [hasMoreMessages, setHasMoreMessages] = useState(true);
|
|
91
|
+
const [nextCursor, setNextCursor] = useState<string | number | undefined>(
|
|
92
|
+
undefined
|
|
93
|
+
);
|
|
94
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
87
95
|
const scrollRef = useRef<ScrollView | null>(null);
|
|
88
96
|
const inputRef = useRef<TextInput | null>(null);
|
|
97
|
+
// Scroll management refs - using refs instead of state to avoid re-renders
|
|
98
|
+
const scrollYRef = useRef(0);
|
|
99
|
+
const contentHeightRef = useRef(0);
|
|
100
|
+
const isUserScrollingRef = useRef(false);
|
|
101
|
+
const isPaginatingRef = useRef(false);
|
|
102
|
+
const initialScrollDoneRef = useRef(false);
|
|
89
103
|
const themeProps = useMemo(
|
|
90
104
|
() => mergeTheme(themeOverrides),
|
|
91
105
|
[themeOverrides]
|
|
@@ -117,13 +131,29 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
117
131
|
loadAuthData();
|
|
118
132
|
}, [userTokenProp]);
|
|
119
133
|
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const setActiveProduct = async () => {
|
|
136
|
+
if (activeProductType) {
|
|
137
|
+
await Storage.setJSON("external_context", activeProductType);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
setActiveProduct();
|
|
141
|
+
}, [activeProductType]);
|
|
142
|
+
|
|
120
143
|
const onViewAll = (item: any) => {
|
|
121
144
|
let searchPayload = item?.search_payload;
|
|
122
|
-
if(
|
|
145
|
+
if (
|
|
146
|
+
searchPayload?.cut_from ||
|
|
147
|
+
searchPayload?.cut_to ||
|
|
148
|
+
searchPayload?.polish_from ||
|
|
149
|
+
searchPayload?.polish_to ||
|
|
150
|
+
searchPayload?.symmetry_from ||
|
|
151
|
+
searchPayload?.symmetry_to
|
|
152
|
+
) {
|
|
123
153
|
searchPayload.cutGrades = [];
|
|
124
154
|
searchPayload.polishes = [];
|
|
125
155
|
searchPayload.symmetries = [];
|
|
126
|
-
if(!searchPayload?.shapes) {
|
|
156
|
+
if (!searchPayload?.shapes) {
|
|
127
157
|
searchPayload.shapes = [];
|
|
128
158
|
}
|
|
129
159
|
}
|
|
@@ -145,14 +175,19 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
145
175
|
if (searchPayload?.symmetry_to) {
|
|
146
176
|
searchPayload.symmetries.push(searchPayload?.symmetry_to);
|
|
147
177
|
}
|
|
148
|
-
if(searchPayload?.three_x) {
|
|
178
|
+
if (searchPayload?.three_x) {
|
|
149
179
|
delete searchPayload.three_x;
|
|
150
180
|
}
|
|
151
181
|
|
|
152
182
|
const stringifiedSearchPayload = JSON.stringify(searchPayload);
|
|
153
183
|
const payload = item?.search_payload;
|
|
154
184
|
if (!payload) return;
|
|
155
|
-
const isLabgrown =
|
|
185
|
+
const isLabgrown =
|
|
186
|
+
typeof payload.lab_grown === "string"
|
|
187
|
+
? payload.lab_grown === "true"
|
|
188
|
+
: typeof payload.lab_grown === "boolean"
|
|
189
|
+
? payload.lab_grown
|
|
190
|
+
: false;
|
|
156
191
|
const domain = apiUrl.split("v3");
|
|
157
192
|
const deepLinkUrl = `${domain[0]}webapp/${
|
|
158
193
|
isLabgrown ? "lab-grown-diamonds" : "natural-diamonds"
|
|
@@ -248,11 +283,23 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
248
283
|
);
|
|
249
284
|
if (!cancelled) {
|
|
250
285
|
const normalised = normaliseMessages(initial).reverse();
|
|
286
|
+
// Capture pagination info from initial response
|
|
287
|
+
if (initial?.has_more !== undefined) {
|
|
288
|
+
setHasMoreMessages(initial.has_more);
|
|
289
|
+
}
|
|
290
|
+
if (initial?.next_cursor !== undefined) {
|
|
291
|
+
setNextCursor(initial.next_cursor);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Reset scroll tracking for fresh load
|
|
295
|
+
initialScrollDoneRef.current = false;
|
|
296
|
+
isUserScrollingRef.current = false;
|
|
297
|
+
|
|
251
298
|
if (normalised.length === 0) {
|
|
252
299
|
const initialAssistant: ChatMessage = {
|
|
253
300
|
id: "",
|
|
254
301
|
role: "assistant",
|
|
255
|
-
text:
|
|
302
|
+
text: 'Describe the diamond you\'re looking for — for example, "2ct F VS2 under $10,000" or "1–2ct D–F VS pear" — and I\'ll take it from there.',
|
|
256
303
|
createdAt: Date.now(),
|
|
257
304
|
isLoading: false,
|
|
258
305
|
suggestions: [],
|
|
@@ -260,15 +307,11 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
260
307
|
reaction: "0",
|
|
261
308
|
};
|
|
262
309
|
setMessages([initialAssistant]);
|
|
263
|
-
|
|
264
|
-
scrollRef.current?.scrollToEnd({ animated: false });
|
|
265
|
-
}, 0);
|
|
310
|
+
setHasMoreMessages(false);
|
|
266
311
|
} else {
|
|
267
312
|
setMessages(normalised);
|
|
268
|
-
setTimeout(() => {
|
|
269
|
-
scrollRef.current?.scrollToEnd({ animated: false });
|
|
270
|
-
}, 0);
|
|
271
313
|
}
|
|
314
|
+
// onContentSizeChange will handle scrollToEnd
|
|
272
315
|
}
|
|
273
316
|
} catch (error) {
|
|
274
317
|
console.error("Failed to fetch initial messages", error);
|
|
@@ -288,12 +331,16 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
288
331
|
async (rawText: string) => {
|
|
289
332
|
const trimmed = rawText.trim();
|
|
290
333
|
if (!trimmed || loading) return;
|
|
334
|
+
const external_context = await Storage.getJSON<string>(
|
|
335
|
+
"external_context"
|
|
336
|
+
);
|
|
291
337
|
|
|
292
338
|
const userMessage: ChatMessage = {
|
|
293
339
|
id: `user-${Date.now()}`,
|
|
294
340
|
role: "user",
|
|
295
341
|
text: trimmed,
|
|
296
342
|
createdAt: Date.now(),
|
|
343
|
+
external_context: external_context || null,
|
|
297
344
|
};
|
|
298
345
|
|
|
299
346
|
const loadingId = `bot-loading-${Date.now()}`;
|
|
@@ -484,6 +531,71 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
484
531
|
}
|
|
485
532
|
}, []);
|
|
486
533
|
|
|
534
|
+
// Load older messages when user scrolls to top
|
|
535
|
+
const loadOlderMessages = useCallback(async () => {
|
|
536
|
+
if (loadingOlder || !hasMoreMessages || !hasAuth) return;
|
|
537
|
+
|
|
538
|
+
// Mark that we're paginating - this prevents auto-scroll to bottom
|
|
539
|
+
isPaginatingRef.current = true;
|
|
540
|
+
const prevHeight = contentHeightRef.current;
|
|
541
|
+
setLoadingOlder(true);
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
const result = await fetchOlderMessages(
|
|
545
|
+
apiUrl,
|
|
546
|
+
priceMode,
|
|
547
|
+
nextCursor,
|
|
548
|
+
currentPage + 1
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const olderMessages = normaliseMessages(result);
|
|
552
|
+
if (olderMessages.length > 0) {
|
|
553
|
+
// Prepend older messages to the beginning
|
|
554
|
+
setMessages((prev) => {
|
|
555
|
+
// Filter out duplicates by id
|
|
556
|
+
const existingIds = new Set(prev.map((m) => m.id));
|
|
557
|
+
const newMessages = olderMessages.filter(
|
|
558
|
+
(m) => !existingIds.has(m.id)
|
|
559
|
+
);
|
|
560
|
+
return [...newMessages, ...prev];
|
|
561
|
+
});
|
|
562
|
+
setCurrentPage((prev) => prev + 1);
|
|
563
|
+
|
|
564
|
+
// After state update, adjust scroll to maintain position
|
|
565
|
+
// Use requestAnimationFrame to wait for layout
|
|
566
|
+
requestAnimationFrame(() => {
|
|
567
|
+
const newHeight = contentHeightRef.current;
|
|
568
|
+
const heightDiff = newHeight - prevHeight;
|
|
569
|
+
if (heightDiff > 0 && scrollRef.current) {
|
|
570
|
+
scrollRef.current.scrollTo({
|
|
571
|
+
y: scrollYRef.current + heightDiff,
|
|
572
|
+
animated: false,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
isPaginatingRef.current = false;
|
|
576
|
+
});
|
|
577
|
+
} else {
|
|
578
|
+
isPaginatingRef.current = false;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
setHasMoreMessages(result.has_more ?? olderMessages.length > 0);
|
|
582
|
+
setNextCursor(result.next_cursor);
|
|
583
|
+
} catch (e) {
|
|
584
|
+
console.error("Failed to load older messages", e);
|
|
585
|
+
isPaginatingRef.current = false;
|
|
586
|
+
} finally {
|
|
587
|
+
setLoadingOlder(false);
|
|
588
|
+
}
|
|
589
|
+
}, [
|
|
590
|
+
loadingOlder,
|
|
591
|
+
hasMoreMessages,
|
|
592
|
+
hasAuth,
|
|
593
|
+
apiUrl,
|
|
594
|
+
priceMode,
|
|
595
|
+
nextCursor,
|
|
596
|
+
currentPage,
|
|
597
|
+
]);
|
|
598
|
+
|
|
487
599
|
useEffect(() => {
|
|
488
600
|
const scrollToBottom = () => {
|
|
489
601
|
setTimeout(() => {
|
|
@@ -493,8 +605,10 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
493
605
|
}, 100);
|
|
494
606
|
};
|
|
495
607
|
|
|
496
|
-
const showEvent =
|
|
497
|
-
|
|
608
|
+
const showEvent =
|
|
609
|
+
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
610
|
+
const hideEvent =
|
|
611
|
+
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
|
|
498
612
|
|
|
499
613
|
const subShow = Keyboard.addListener(showEvent, scrollToBottom);
|
|
500
614
|
const subHide = Keyboard.addListener(hideEvent, scrollToBottom);
|
|
@@ -562,6 +676,14 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
562
676
|
setLoadingMessageId(null);
|
|
563
677
|
setTypingMessageId(null);
|
|
564
678
|
setTypingFullText("");
|
|
679
|
+
// Reset pagination state
|
|
680
|
+
setHasMoreMessages(true);
|
|
681
|
+
setNextCursor(undefined);
|
|
682
|
+
setCurrentPage(1);
|
|
683
|
+
// Reset scroll tracking
|
|
684
|
+
initialScrollDoneRef.current = false;
|
|
685
|
+
isUserScrollingRef.current = false;
|
|
686
|
+
isPaginatingRef.current = false;
|
|
565
687
|
|
|
566
688
|
const fresh = await fetchInitialMessages(apiUrl, undefined, priceMode);
|
|
567
689
|
const normalised = normaliseMessages(fresh).reverse();
|
|
@@ -569,7 +691,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
569
691
|
const initialAssistant: ChatMessage = {
|
|
570
692
|
id: "",
|
|
571
693
|
role: "assistant",
|
|
572
|
-
text:
|
|
694
|
+
text: 'Describe the diamond you\'re looking for — for example, "2ct F VS2 under $10,000" or "1–2ct D–F VS pear" — and I\'ll take it from there.',
|
|
573
695
|
createdAt: Date.now(),
|
|
574
696
|
isLoading: false,
|
|
575
697
|
suggestions: [],
|
|
@@ -580,9 +702,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
580
702
|
} else {
|
|
581
703
|
setMessages(normalised);
|
|
582
704
|
}
|
|
583
|
-
|
|
584
|
-
scrollRef.current?.scrollToEnd({ animated: false });
|
|
585
|
-
}, 500);
|
|
705
|
+
// onContentSizeChange will handle scrollToEnd
|
|
586
706
|
onClearChat?.();
|
|
587
707
|
} catch (err) {
|
|
588
708
|
console.error("Failed to clear chat", err);
|
|
@@ -686,12 +806,22 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
686
806
|
onScroll={(e) => {
|
|
687
807
|
const { contentOffset, contentSize, layoutMeasurement } =
|
|
688
808
|
e.nativeEvent;
|
|
689
|
-
|
|
809
|
+
scrollYRef.current = contentOffset.y;
|
|
690
810
|
const distanceFromBottom =
|
|
691
811
|
contentSize.height -
|
|
692
812
|
(layoutMeasurement.height + contentOffset.y);
|
|
693
|
-
//
|
|
694
|
-
|
|
813
|
+
// Track if user has scrolled away from bottom
|
|
814
|
+
isUserScrollingRef.current = distanceFromBottom > 100;
|
|
815
|
+
|
|
816
|
+
// Load older messages when scrolled near the top (within 50px)
|
|
817
|
+
if (
|
|
818
|
+
contentOffset.y < 50 &&
|
|
819
|
+
hasMoreMessages &&
|
|
820
|
+
!loadingOlder &&
|
|
821
|
+
!isPaginatingRef.current
|
|
822
|
+
) {
|
|
823
|
+
loadOlderMessages();
|
|
824
|
+
}
|
|
695
825
|
}}
|
|
696
826
|
scrollEventThrottle={16}
|
|
697
827
|
style={
|
|
@@ -714,12 +844,38 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
714
844
|
? { maxWidth: 608, alignSelf: "center", width: "100%" }
|
|
715
845
|
: {}),
|
|
716
846
|
}}
|
|
717
|
-
onContentSizeChange={() => {
|
|
718
|
-
|
|
847
|
+
onContentSizeChange={(_width, height) => {
|
|
848
|
+
contentHeightRef.current = height;
|
|
849
|
+
|
|
850
|
+
// Skip auto-scroll during pagination - scroll is handled in loadOlderMessages
|
|
851
|
+
if (isPaginatingRef.current || loadingOlder) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Only auto-scroll to bottom if:
|
|
856
|
+
// 1. Initial scroll hasn't been done yet, OR
|
|
857
|
+
// 2. User is near the bottom (not scrolled up to view history)
|
|
858
|
+
if (!initialScrollDoneRef.current) {
|
|
859
|
+
scrollRef.current?.scrollToEnd({ animated: false });
|
|
860
|
+
initialScrollDoneRef.current = true;
|
|
861
|
+
} else if (!isUserScrollingRef.current) {
|
|
862
|
+
// User is near bottom, auto-scroll for new messages
|
|
719
863
|
scrollRef.current?.scrollToEnd({ animated: false });
|
|
720
864
|
}
|
|
721
865
|
}}
|
|
722
866
|
>
|
|
867
|
+
{/* Loading indicator for pagination */}
|
|
868
|
+
{loadingOlder && (
|
|
869
|
+
<LoadingOlderContainer>
|
|
870
|
+
<ActivityIndicator
|
|
871
|
+
size="small"
|
|
872
|
+
color={theme["core-06"] || "#4F4E57"}
|
|
873
|
+
/>
|
|
874
|
+
<LoadingOlderText theme={theme}>
|
|
875
|
+
Loading older messages...
|
|
876
|
+
</LoadingOlderText>
|
|
877
|
+
</LoadingOlderContainer>
|
|
878
|
+
)}
|
|
723
879
|
{messages?.[0]?.createdAt && (
|
|
724
880
|
<EmptyContainer theme={theme}>
|
|
725
881
|
<EmptyText theme={theme}>
|
|
@@ -775,15 +931,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
775
931
|
const diamonds = result?.response?.body?.diamonds;
|
|
776
932
|
const hasAny =
|
|
777
933
|
Array.isArray(diamonds) && diamonds.length > 0;
|
|
778
|
-
if (hasAny) {
|
|
779
|
-
try {
|
|
780
|
-
scrollRef.current?.scrollTo({
|
|
781
|
-
x: 0,
|
|
782
|
-
y: scrollY + 120,
|
|
783
|
-
animated: true,
|
|
784
|
-
});
|
|
785
|
-
} catch (_) {}
|
|
786
|
-
} else {
|
|
934
|
+
if (!hasAny) {
|
|
787
935
|
setMessages((prev) =>
|
|
788
936
|
prev.map((msg) =>
|
|
789
937
|
msg.id === id
|
|
@@ -797,6 +945,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
797
945
|
)
|
|
798
946
|
);
|
|
799
947
|
}
|
|
948
|
+
// Let onContentSizeChange handle scrolling naturally
|
|
800
949
|
}}
|
|
801
950
|
/>
|
|
802
951
|
)}
|
|
@@ -896,6 +1045,20 @@ const LineView = styled.View<{ theme: DefaultTheme }>`
|
|
|
896
1045
|
border-top-width: 1;
|
|
897
1046
|
`;
|
|
898
1047
|
|
|
1048
|
+
const LoadingOlderContainer = styled.View`
|
|
1049
|
+
padding: 12px;
|
|
1050
|
+
align-items: center;
|
|
1051
|
+
justify-content: center;
|
|
1052
|
+
flex-direction: row;
|
|
1053
|
+
gap: 8px;
|
|
1054
|
+
`;
|
|
1055
|
+
|
|
1056
|
+
const LoadingOlderText = styled.Text<{ theme: DefaultTheme }>`
|
|
1057
|
+
font-size: 12px;
|
|
1058
|
+
color: ${({ theme }: { theme: DefaultTheme }) =>
|
|
1059
|
+
theme["core-06"] || "#4F4E57"};
|
|
1060
|
+
`;
|
|
1061
|
+
|
|
899
1062
|
const styles = StyleSheet.create({
|
|
900
1063
|
listContent: {
|
|
901
1064
|
paddingVertical: 8,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
Image,
|
|
10
10
|
Pressable,
|
|
11
11
|
} from "react-native";
|
|
12
|
-
import { getUserCurrencySymbol } from "./utils";
|
|
12
|
+
import { getUserCurrencySymbol, formatPriceValue, roundUpPrices } from "./utils";
|
|
13
13
|
import { useTheme } from "styled-components/native";
|
|
14
14
|
import { DefaultTheme } from "styled-components/native";
|
|
15
15
|
import styled from "styled-components/native";
|
|
@@ -128,7 +128,8 @@ const renderToken = (
|
|
|
128
128
|
token: TokenConfig,
|
|
129
129
|
item: any,
|
|
130
130
|
userCurrency: string | null,
|
|
131
|
-
index: number
|
|
131
|
+
index: number,
|
|
132
|
+
shouldRound: boolean = false
|
|
132
133
|
): React.ReactNode => {
|
|
133
134
|
const TextView = token.style === "secondary" ? TextStyleTwo : TextStyleOne;
|
|
134
135
|
const PrefixTextView = token.prefixStyle === "secondary" ? TextStyleTwo : TextStyleOne;
|
|
@@ -176,11 +177,14 @@ const renderToken = (
|
|
|
176
177
|
|
|
177
178
|
case "priceField": {
|
|
178
179
|
const value = item[token.field!];
|
|
180
|
+
const formattedPrice = formatPriceValue(token.field!, value, shouldRound);
|
|
181
|
+
console.log("formattedPrice", formattedPrice);
|
|
182
|
+
|
|
179
183
|
return (
|
|
180
184
|
<View key={index} style={styles.rowCenter}>
|
|
181
185
|
{token.prefix && <TextView theme={theme}>{token.prefix}</TextView>}
|
|
182
186
|
{value && <TextView theme={theme}>{currency}</TextView>}
|
|
183
|
-
<TextView theme={theme}>{
|
|
187
|
+
<TextView theme={theme}>{formattedPrice ?? "-"}</TextView>
|
|
184
188
|
{token.suffix && <SuffixTextView theme={theme}>{token.suffix}</SuffixTextView>}
|
|
185
189
|
</View>
|
|
186
190
|
);
|
|
@@ -190,7 +194,7 @@ const renderToken = (
|
|
|
190
194
|
return (
|
|
191
195
|
<View key={index} style={styles.rowCenter}>
|
|
192
196
|
{token.items?.map((subToken, subIndex) =>
|
|
193
|
-
renderToken(subToken, item, userCurrency, subIndex)
|
|
197
|
+
renderToken(subToken, item, userCurrency, subIndex, shouldRound)
|
|
194
198
|
)}
|
|
195
199
|
</View>
|
|
196
200
|
);
|
|
@@ -210,13 +214,20 @@ const ProductsListViewComponent: React.FC<ProductsListViewProps> = ({
|
|
|
210
214
|
if (!data || !data.length) return null;
|
|
211
215
|
const theme = useTheme();
|
|
212
216
|
const [userCurrency, setUserCurrency] = useState<string | null>(null);
|
|
217
|
+
const [shouldRound, setShouldRound] = useState<boolean>(false);
|
|
213
218
|
|
|
214
219
|
useEffect(() => {
|
|
215
220
|
const fetchCurrency = async () => {
|
|
216
221
|
const currency = await getUserCurrencySymbol();
|
|
217
222
|
setUserCurrency(currency);
|
|
218
223
|
};
|
|
224
|
+
|
|
225
|
+
const getRoundOffPrices = async () => {
|
|
226
|
+
const roundOff = await roundUpPrices();
|
|
227
|
+
setShouldRound(roundOff);
|
|
228
|
+
};
|
|
219
229
|
fetchCurrency();
|
|
230
|
+
getRoundOffPrices();
|
|
220
231
|
}, []);
|
|
221
232
|
|
|
222
233
|
return (
|
|
@@ -229,28 +240,28 @@ const ProductsListViewComponent: React.FC<ProductsListViewProps> = ({
|
|
|
229
240
|
<Pressable
|
|
230
241
|
key={dataItem.id}
|
|
231
242
|
onPress={() => onItemPress?.(dataItem)}
|
|
232
|
-
// @ts-ignore - hovered is available on web
|
|
233
|
-
style={({ hovered }) => [
|
|
234
|
-
styles.itemContainer,
|
|
235
|
-
hovered && styles.itemHover,
|
|
236
|
-
]}
|
|
237
243
|
>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
{(state) => (
|
|
245
|
+
// @ts-ignore
|
|
246
|
+
<ListItemParent theme={theme} hovered={state.hovered}>
|
|
247
|
+
<View style={styles.left}>
|
|
248
|
+
<View style={styles.serialContainer}>
|
|
249
|
+
<Serial>{index + 1}.</Serial>
|
|
250
|
+
</View>
|
|
251
|
+
<View style={styles.tokens}>
|
|
252
|
+
{DIAMOND_TOKENS.map((token, tokenIndex) =>
|
|
253
|
+
renderToken(token, dataItem, userCurrency, tokenIndex, shouldRound)
|
|
254
|
+
)}
|
|
255
|
+
</View>
|
|
256
|
+
</View>
|
|
257
|
+
</ListItemParent>
|
|
258
|
+
)}
|
|
248
259
|
</Pressable>
|
|
249
260
|
))}
|
|
250
261
|
</ScrollView>
|
|
251
262
|
|
|
252
263
|
<ButtonView activeOpacity={0.8} onPress={() => onViewAll(item)}>
|
|
253
|
-
<ButtonText>{`View All ${totalResults}
|
|
264
|
+
<ButtonText>{`View All ${totalResults} diamonds`}</ButtonText>
|
|
254
265
|
<ImageComponent
|
|
255
266
|
source={{
|
|
256
267
|
uri: "https://cdn.vdbapp.com/ai/chat-widget/assets/img/right.svg",
|
|
@@ -320,6 +331,14 @@ const ButtonText = styled.Text<{ theme: DefaultTheme }>`
|
|
|
320
331
|
font-weight: 600;
|
|
321
332
|
`;
|
|
322
333
|
|
|
334
|
+
const ListItemParent = styled.Pressable<{ theme: DefaultTheme, hovered?: boolean }>`
|
|
335
|
+
padding-vertical: 2px;
|
|
336
|
+
background-color: ${({ theme, hovered }: { theme: DefaultTheme, hovered?: boolean }) =>
|
|
337
|
+
hovered
|
|
338
|
+
? theme["core-02"] || "#EDEDF2"
|
|
339
|
+
: "transparent"};
|
|
340
|
+
`;
|
|
341
|
+
|
|
323
342
|
const styles = StyleSheet.create({
|
|
324
343
|
listContent: {
|
|
325
344
|
gap: 8,
|
|
@@ -351,18 +370,6 @@ const styles = StyleSheet.create({
|
|
|
351
370
|
flexDirection: "row",
|
|
352
371
|
alignItems: "center",
|
|
353
372
|
},
|
|
354
|
-
priceContainer: {
|
|
355
|
-
alignItems: "flex-start",
|
|
356
|
-
justifyContent: "center",
|
|
357
|
-
},
|
|
358
|
-
|
|
359
|
-
itemContainer: {
|
|
360
|
-
paddingVertical: 2,
|
|
361
|
-
},
|
|
362
|
-
|
|
363
|
-
itemHover: {
|
|
364
|
-
backgroundColor: "#EDEDF2",
|
|
365
|
-
},
|
|
366
373
|
});
|
|
367
374
|
|
|
368
375
|
const ProductsListView = memo(ProductsListViewComponent);
|
package/src/components/utils.ts
CHANGED
|
@@ -68,6 +68,42 @@ export const getUserCurrencySymbol = async (): Promise<string | null> => {
|
|
|
68
68
|
return userData.currency_symbol || "$";
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
|
|
72
|
+
export const roundUpPrices = async (): Promise<boolean> => {
|
|
73
|
+
const userInfo = await Storage.getJSON<UserDetails>("persist:userInfo", {});
|
|
74
|
+
const userData = userInfo?.user as UserDetails;
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
return true; // org 1
|
|
77
|
+
// return !!userData?.feature_settings?.round_up_prices;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Helper to add commas to a number
|
|
81
|
+
export function formatWithCommas(value: number | string): string {
|
|
82
|
+
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
83
|
+
if (isNaN(num)) return String(value);
|
|
84
|
+
return num.toLocaleString();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Format price values according to requirements
|
|
88
|
+
export function formatPriceValue(key: string, value: any, shouldRound: boolean) {
|
|
89
|
+
console.log("formatPriceValue", key, value);
|
|
90
|
+
|
|
91
|
+
if (key === 'price_per_carat') {
|
|
92
|
+
// Remove decimals, round, add commas
|
|
93
|
+
const num = Math.round(Number(value));
|
|
94
|
+
return formatWithCommas(num);
|
|
95
|
+
}
|
|
96
|
+
if (key === 'total_sales_price') {
|
|
97
|
+
let num = Number(value);
|
|
98
|
+
if (shouldRound) {
|
|
99
|
+
num = Math.round(num);
|
|
100
|
+
}
|
|
101
|
+
return formatWithCommas(num);
|
|
102
|
+
}
|
|
103
|
+
// fallback: just add commas if number
|
|
104
|
+
return formatWithCommas(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
71
107
|
export interface UserDetails {
|
|
72
108
|
id?: string | number;
|
|
73
109
|
email?: string;
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface ChatMessage {
|
|
|
9
9
|
suggestions?: string[];
|
|
10
10
|
reaction?: string;
|
|
11
11
|
search_payload?: Record<string, any>;
|
|
12
|
+
external_context?: string | null;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface ChatTheme {
|
|
@@ -45,6 +46,7 @@ export interface ChatWidgetProps {
|
|
|
45
46
|
onItemPress?: (deepLinkUrl: string, item: any) => void;
|
|
46
47
|
/** When true, shows beta labels and disclaimer. */
|
|
47
48
|
isBetaMode?: boolean;
|
|
49
|
+
activeProductType?: string;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface ChatWidgetRef {
|