vdb-ai-chat 1.0.14 → 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 +44 -3
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/ChatWidget.js +129 -34
- package/lib/commonjs/components/ChatWidget.js.map +1 -1
- package/lib/module/api.js +43 -3
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/ChatWidget.js +131 -36
- package/lib/module/components/ChatWidget.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/package.json +1 -1
- package/src/api.ts +69 -7
- package/src/components/ChatWidget.tsx +187 -36
|
@@ -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,
|
|
@@ -75,8 +77,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
75
77
|
);
|
|
76
78
|
const [typingMessageId, setTypingMessageId] = useState<string | null>(null);
|
|
77
79
|
const [typingFullText, setTypingFullText] = useState<string>("");
|
|
78
|
-
const [scrollY, setScrollY] = useState(0);
|
|
79
|
-
const [autoScroll, setAutoScroll] = useState(true);
|
|
80
80
|
const [productsByMsg, setProductsByMsg] = useState<Record<string, any>>({});
|
|
81
81
|
const [reloadLoadingIds, setReloadLoadingIds] = useState<Set<string>>(
|
|
82
82
|
new Set()
|
|
@@ -85,8 +85,21 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
85
85
|
priceModeProp || null
|
|
86
86
|
);
|
|
87
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);
|
|
88
95
|
const scrollRef = useRef<ScrollView | null>(null);
|
|
89
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);
|
|
90
103
|
const themeProps = useMemo(
|
|
91
104
|
() => mergeTheme(themeOverrides),
|
|
92
105
|
[themeOverrides]
|
|
@@ -123,17 +136,24 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
123
136
|
if (activeProductType) {
|
|
124
137
|
await Storage.setJSON("external_context", activeProductType);
|
|
125
138
|
}
|
|
126
|
-
}
|
|
139
|
+
};
|
|
127
140
|
setActiveProduct();
|
|
128
141
|
}, [activeProductType]);
|
|
129
142
|
|
|
130
143
|
const onViewAll = (item: any) => {
|
|
131
144
|
let searchPayload = item?.search_payload;
|
|
132
|
-
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
|
+
) {
|
|
133
153
|
searchPayload.cutGrades = [];
|
|
134
154
|
searchPayload.polishes = [];
|
|
135
155
|
searchPayload.symmetries = [];
|
|
136
|
-
if(!searchPayload?.shapes) {
|
|
156
|
+
if (!searchPayload?.shapes) {
|
|
137
157
|
searchPayload.shapes = [];
|
|
138
158
|
}
|
|
139
159
|
}
|
|
@@ -155,14 +175,19 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
155
175
|
if (searchPayload?.symmetry_to) {
|
|
156
176
|
searchPayload.symmetries.push(searchPayload?.symmetry_to);
|
|
157
177
|
}
|
|
158
|
-
if(searchPayload?.three_x) {
|
|
178
|
+
if (searchPayload?.three_x) {
|
|
159
179
|
delete searchPayload.three_x;
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
const stringifiedSearchPayload = JSON.stringify(searchPayload);
|
|
163
183
|
const payload = item?.search_payload;
|
|
164
184
|
if (!payload) return;
|
|
165
|
-
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;
|
|
166
191
|
const domain = apiUrl.split("v3");
|
|
167
192
|
const deepLinkUrl = `${domain[0]}webapp/${
|
|
168
193
|
isLabgrown ? "lab-grown-diamonds" : "natural-diamonds"
|
|
@@ -258,11 +283,23 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
258
283
|
);
|
|
259
284
|
if (!cancelled) {
|
|
260
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
|
+
|
|
261
298
|
if (normalised.length === 0) {
|
|
262
299
|
const initialAssistant: ChatMessage = {
|
|
263
300
|
id: "",
|
|
264
301
|
role: "assistant",
|
|
265
|
-
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.',
|
|
266
303
|
createdAt: Date.now(),
|
|
267
304
|
isLoading: false,
|
|
268
305
|
suggestions: [],
|
|
@@ -270,15 +307,11 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
270
307
|
reaction: "0",
|
|
271
308
|
};
|
|
272
309
|
setMessages([initialAssistant]);
|
|
273
|
-
|
|
274
|
-
scrollRef.current?.scrollToEnd({ animated: false });
|
|
275
|
-
}, 0);
|
|
310
|
+
setHasMoreMessages(false);
|
|
276
311
|
} else {
|
|
277
312
|
setMessages(normalised);
|
|
278
|
-
setTimeout(() => {
|
|
279
|
-
scrollRef.current?.scrollToEnd({ animated: false });
|
|
280
|
-
}, 0);
|
|
281
313
|
}
|
|
314
|
+
// onContentSizeChange will handle scrollToEnd
|
|
282
315
|
}
|
|
283
316
|
} catch (error) {
|
|
284
317
|
console.error("Failed to fetch initial messages", error);
|
|
@@ -298,14 +331,16 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
298
331
|
async (rawText: string) => {
|
|
299
332
|
const trimmed = rawText.trim();
|
|
300
333
|
if (!trimmed || loading) return;
|
|
301
|
-
const external_context = await Storage.getJSON<string>(
|
|
334
|
+
const external_context = await Storage.getJSON<string>(
|
|
335
|
+
"external_context"
|
|
336
|
+
);
|
|
302
337
|
|
|
303
338
|
const userMessage: ChatMessage = {
|
|
304
339
|
id: `user-${Date.now()}`,
|
|
305
340
|
role: "user",
|
|
306
341
|
text: trimmed,
|
|
307
342
|
createdAt: Date.now(),
|
|
308
|
-
external_context: external_context || null
|
|
343
|
+
external_context: external_context || null,
|
|
309
344
|
};
|
|
310
345
|
|
|
311
346
|
const loadingId = `bot-loading-${Date.now()}`;
|
|
@@ -496,6 +531,71 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
496
531
|
}
|
|
497
532
|
}, []);
|
|
498
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
|
+
|
|
499
599
|
useEffect(() => {
|
|
500
600
|
const scrollToBottom = () => {
|
|
501
601
|
setTimeout(() => {
|
|
@@ -505,8 +605,10 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
505
605
|
}, 100);
|
|
506
606
|
};
|
|
507
607
|
|
|
508
|
-
const showEvent =
|
|
509
|
-
|
|
608
|
+
const showEvent =
|
|
609
|
+
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
610
|
+
const hideEvent =
|
|
611
|
+
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
|
|
510
612
|
|
|
511
613
|
const subShow = Keyboard.addListener(showEvent, scrollToBottom);
|
|
512
614
|
const subHide = Keyboard.addListener(hideEvent, scrollToBottom);
|
|
@@ -574,6 +676,14 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
574
676
|
setLoadingMessageId(null);
|
|
575
677
|
setTypingMessageId(null);
|
|
576
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;
|
|
577
687
|
|
|
578
688
|
const fresh = await fetchInitialMessages(apiUrl, undefined, priceMode);
|
|
579
689
|
const normalised = normaliseMessages(fresh).reverse();
|
|
@@ -581,7 +691,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
581
691
|
const initialAssistant: ChatMessage = {
|
|
582
692
|
id: "",
|
|
583
693
|
role: "assistant",
|
|
584
|
-
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.',
|
|
585
695
|
createdAt: Date.now(),
|
|
586
696
|
isLoading: false,
|
|
587
697
|
suggestions: [],
|
|
@@ -592,9 +702,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
592
702
|
} else {
|
|
593
703
|
setMessages(normalised);
|
|
594
704
|
}
|
|
595
|
-
|
|
596
|
-
scrollRef.current?.scrollToEnd({ animated: false });
|
|
597
|
-
}, 500);
|
|
705
|
+
// onContentSizeChange will handle scrollToEnd
|
|
598
706
|
onClearChat?.();
|
|
599
707
|
} catch (err) {
|
|
600
708
|
console.error("Failed to clear chat", err);
|
|
@@ -698,12 +806,22 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
698
806
|
onScroll={(e) => {
|
|
699
807
|
const { contentOffset, contentSize, layoutMeasurement } =
|
|
700
808
|
e.nativeEvent;
|
|
701
|
-
|
|
809
|
+
scrollYRef.current = contentOffset.y;
|
|
702
810
|
const distanceFromBottom =
|
|
703
811
|
contentSize.height -
|
|
704
812
|
(layoutMeasurement.height + contentOffset.y);
|
|
705
|
-
//
|
|
706
|
-
|
|
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
|
+
}
|
|
707
825
|
}}
|
|
708
826
|
scrollEventThrottle={16}
|
|
709
827
|
style={
|
|
@@ -726,12 +844,38 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
726
844
|
? { maxWidth: 608, alignSelf: "center", width: "100%" }
|
|
727
845
|
: {}),
|
|
728
846
|
}}
|
|
729
|
-
onContentSizeChange={() => {
|
|
730
|
-
|
|
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
|
|
731
863
|
scrollRef.current?.scrollToEnd({ animated: false });
|
|
732
864
|
}
|
|
733
865
|
}}
|
|
734
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
|
+
)}
|
|
735
879
|
{messages?.[0]?.createdAt && (
|
|
736
880
|
<EmptyContainer theme={theme}>
|
|
737
881
|
<EmptyText theme={theme}>
|
|
@@ -787,15 +931,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
787
931
|
const diamonds = result?.response?.body?.diamonds;
|
|
788
932
|
const hasAny =
|
|
789
933
|
Array.isArray(diamonds) && diamonds.length > 0;
|
|
790
|
-
if (hasAny) {
|
|
791
|
-
try {
|
|
792
|
-
scrollRef.current?.scrollTo({
|
|
793
|
-
x: 0,
|
|
794
|
-
y: scrollY + 120,
|
|
795
|
-
animated: true,
|
|
796
|
-
});
|
|
797
|
-
} catch (_) {}
|
|
798
|
-
} else {
|
|
934
|
+
if (!hasAny) {
|
|
799
935
|
setMessages((prev) =>
|
|
800
936
|
prev.map((msg) =>
|
|
801
937
|
msg.id === id
|
|
@@ -809,6 +945,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
809
945
|
)
|
|
810
946
|
);
|
|
811
947
|
}
|
|
948
|
+
// Let onContentSizeChange handle scrolling naturally
|
|
812
949
|
}}
|
|
813
950
|
/>
|
|
814
951
|
)}
|
|
@@ -908,6 +1045,20 @@ const LineView = styled.View<{ theme: DefaultTheme }>`
|
|
|
908
1045
|
border-top-width: 1;
|
|
909
1046
|
`;
|
|
910
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
|
+
|
|
911
1062
|
const styles = StyleSheet.create({
|
|
912
1063
|
listContent: {
|
|
913
1064
|
paddingVertical: 8,
|