vdb-ai-chat 1.0.6 → 1.0.8

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 (71) hide show
  1. package/dist/chat-widget.js +1 -1
  2. package/lib/commonjs/api.js +51 -12
  3. package/lib/commonjs/api.js.map +1 -1
  4. package/lib/commonjs/components/BetaNotice.js +13 -12
  5. package/lib/commonjs/components/BetaNotice.js.map +1 -1
  6. package/lib/commonjs/components/ChatInput.js +59 -49
  7. package/lib/commonjs/components/ChatInput.js.map +1 -1
  8. package/lib/commonjs/components/ChatWidget.js +74 -61
  9. package/lib/commonjs/components/ChatWidget.js.map +1 -1
  10. package/lib/commonjs/components/MessageBubble.js +67 -52
  11. package/lib/commonjs/components/MessageBubble.js.map +1 -1
  12. package/lib/commonjs/components/MessageMetaRow.js +50 -31
  13. package/lib/commonjs/components/MessageMetaRow.js.map +1 -1
  14. package/lib/commonjs/components/ProductsListView.js +232 -153
  15. package/lib/commonjs/components/ProductsListView.js.map +1 -1
  16. package/lib/commonjs/components/SuggestionsRow.js +27 -24
  17. package/lib/commonjs/components/SuggestionsRow.js.map +1 -1
  18. package/lib/commonjs/components/utils.js +15 -4
  19. package/lib/commonjs/components/utils.js.map +1 -1
  20. package/lib/commonjs/contexts/ThemeProvider.js +80 -0
  21. package/lib/commonjs/contexts/ThemeProvider.js.map +1 -0
  22. package/lib/commonjs/contexts/utils.js +142 -0
  23. package/lib/commonjs/contexts/utils.js.map +1 -0
  24. package/lib/module/api.js +51 -12
  25. package/lib/module/api.js.map +1 -1
  26. package/lib/module/components/BetaNotice.js +14 -13
  27. package/lib/module/components/BetaNotice.js.map +1 -1
  28. package/lib/module/components/ChatInput.js +61 -50
  29. package/lib/module/components/ChatInput.js.map +1 -1
  30. package/lib/module/components/ChatWidget.js +78 -63
  31. package/lib/module/components/ChatWidget.js.map +1 -1
  32. package/lib/module/components/MessageBubble.js +69 -52
  33. package/lib/module/components/MessageBubble.js.map +1 -1
  34. package/lib/module/components/MessageMetaRow.js +52 -32
  35. package/lib/module/components/MessageMetaRow.js.map +1 -1
  36. package/lib/module/components/ProductsListView.js +234 -154
  37. package/lib/module/components/ProductsListView.js.map +1 -1
  38. package/lib/module/components/SuggestionsRow.js +29 -25
  39. package/lib/module/components/SuggestionsRow.js.map +1 -1
  40. package/lib/module/components/utils.js +17 -3
  41. package/lib/module/components/utils.js.map +1 -1
  42. package/lib/module/contexts/ThemeProvider.js +75 -0
  43. package/lib/module/contexts/ThemeProvider.js.map +1 -0
  44. package/lib/module/contexts/utils.js +136 -0
  45. package/lib/module/contexts/utils.js.map +1 -0
  46. package/lib/typescript/api.d.ts.map +1 -1
  47. package/lib/typescript/components/BetaNotice.d.ts.map +1 -1
  48. package/lib/typescript/components/ChatInput.d.ts.map +1 -1
  49. package/lib/typescript/components/ChatWidget.d.ts.map +1 -1
  50. package/lib/typescript/components/MessageBubble.d.ts +1 -1
  51. package/lib/typescript/components/MessageBubble.d.ts.map +1 -1
  52. package/lib/typescript/components/MessageMetaRow.d.ts.map +1 -1
  53. package/lib/typescript/components/ProductsListView.d.ts.map +1 -1
  54. package/lib/typescript/components/SuggestionsRow.d.ts.map +1 -1
  55. package/lib/typescript/components/utils.d.ts.map +1 -1
  56. package/lib/typescript/contexts/ThemeProvider.d.ts +7 -0
  57. package/lib/typescript/contexts/ThemeProvider.d.ts.map +1 -0
  58. package/lib/typescript/contexts/utils.d.ts +136 -0
  59. package/lib/typescript/contexts/utils.d.ts.map +1 -0
  60. package/package.json +6 -2
  61. package/src/api.ts +58 -21
  62. package/src/components/BetaNotice.tsx +15 -13
  63. package/src/components/ChatInput.tsx +63 -62
  64. package/src/components/ChatWidget.tsx +238 -206
  65. package/src/components/MessageBubble.tsx +113 -74
  66. package/src/components/MessageMetaRow.tsx +80 -50
  67. package/src/components/ProductsListView.tsx +243 -184
  68. package/src/components/SuggestionsRow.tsx +40 -25
  69. package/src/components/utils.ts +24 -8
  70. package/src/contexts/ThemeProvider.tsx +87 -0
  71. package/src/contexts/utils.ts +135 -0
@@ -30,7 +30,6 @@ import {
30
30
  normaliseMessages,
31
31
  sendUserMessage,
32
32
  } from "../api";
33
- import ChatHeader from "./ChatHeader";
34
33
  import BetaNotice from "./BetaNotice";
35
34
  import ProductsList from "./ProductsList";
36
35
  import LazyProductsFetcher from "./LazyProductsFetcher";
@@ -38,11 +37,15 @@ import {
38
37
  FeedbackAction,
39
38
  formatToTime,
40
39
  getUserDetails,
41
- useDeviceType,
42
40
  UserDetails,
43
41
  } from "./utils";
44
42
  import { useUserAnalytics } from "../hooks/useAnalytics";
45
43
  import { Storage } from "../storage";
44
+ import ThemeProvider from "../contexts/ThemeProvider";
45
+ import { useTheme } from "styled-components/native";
46
+ import styled from "styled-components/native";
47
+ import { DefaultTheme } from "styled-components/native";
48
+ import { css } from "styled-components/native";
46
49
 
47
50
  export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
48
51
  (
@@ -82,10 +85,14 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
82
85
  const [userToken, setUserToken] = useState<string>(userTokenProp || "");
83
86
  const scrollRef = useRef<ScrollView | null>(null);
84
87
  const inputRef = useRef<TextInput | null>(null);
85
- const theme = useMemo(() => mergeTheme(themeOverrides), [themeOverrides]);
88
+ const themeProps = useMemo(
89
+ () => mergeTheme(themeOverrides),
90
+ [themeOverrides]
91
+ );
86
92
  const { _identify } = useUserAnalytics();
87
93
  const betaActive = Boolean(isBetaModeProp);
88
- const { isTablet } = useDeviceType();
94
+ const theme = useTheme();
95
+ const isTablet = theme.isTablet;
89
96
  const noResultsText =
90
97
  "No results found for your search. Try adjusting your filters or query.";
91
98
  useEffect(() => {
@@ -476,18 +483,25 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
476
483
 
477
484
  const handleClearChat = useCallback(async () => {
478
485
  try {
479
- const conversations = await Storage.getJSON<Record<string, any>>(
486
+ interface StoredConversations {
487
+ token: string;
488
+ conversations: Record<string, { conversation_id: string | number }>;
489
+ }
490
+
491
+ const stored = await Storage.getJSON<StoredConversations>(
480
492
  "vdbchat_conversations",
481
- {}
493
+ null
482
494
  );
483
- const storedId = conversations
484
- ? conversations[priceMode as any]?.conversation_id
485
- : null;
486
- if (storedId && priceMode) {
487
- const updatedConversations = conversations ?? {};
495
+
496
+ if (stored && priceMode && stored.conversations?.[priceMode]) {
497
+ const updatedConversations = { ...stored.conversations };
488
498
  delete updatedConversations[priceMode];
489
- await Storage.setJSON("vdbchat_conversations", updatedConversations);
499
+ await Storage.setJSON("vdbchat_conversations", {
500
+ token: stored.token,
501
+ conversations: updatedConversations,
502
+ });
490
503
  }
504
+
491
505
  setMessages([]);
492
506
  setProductsByMsg({});
493
507
  setReloadLoadingIds(new Set());
@@ -602,195 +616,237 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
602
616
  }, [typingMessageId, typingFullText]);
603
617
 
604
618
  return (
605
- <View
606
- style={[styles.container, { backgroundColor: theme.backgroundColor }]}
607
- >
608
- {/* {Platform.OS === "web" && (
619
+ <ThemeProvider>
620
+ <Container theme={theme}>
621
+ {/* {Platform.OS === "web" && (
609
622
  <ChatHeader
610
623
  onClose={onClose}
611
624
  onClearChat={handleClearChat}
612
625
  isBetaMode={betaActive}
613
626
  />
614
627
  )} */}
615
- <KeyboardAvoidingView
616
- style={{ flex: 1 }}
617
- behavior={Platform.OS === "ios" ? "padding" : "height"}
618
- keyboardVerticalOffset={Platform.OS === "ios" ? 100 : -16}
619
- >
620
- <ScrollView
621
- ref={scrollRef}
622
- keyboardShouldPersistTaps="handled"
623
- onScroll={(e) => {
624
- const { contentOffset, contentSize, layoutMeasurement } =
625
- e.nativeEvent;
626
- setScrollY(contentOffset.y);
627
- const distanceFromBottom =
628
- contentSize.height -
629
- (layoutMeasurement.height + contentOffset.y);
630
- // Enable auto-scroll when user is near the bottom; disable when scrolled up
631
- setAutoScroll(distanceFromBottom < 48);
632
- }}
633
- scrollEventThrottle={16}
634
- style={
635
- modalHeight
636
- ? { height: modalHeight, backgroundColor: "#FFFFFF" }
637
- : { backgroundColor: "#FFFFFF" }
638
- }
639
- contentContainerStyle={{
640
- backgroundColor: theme?.listContentBackgroundColor || "#FFFFFF",
641
- ...styles.listContent,
642
- justifyContent: messages.length === 0 ? "center" : "flex-end",
643
- minHeight: modalHeight ? modalHeight : undefined,
644
- ...(isTablet
645
- ? { maxWidth: 608, alignSelf: "center", width: "100%" }
646
- : {}),
647
- }}
648
- onContentSizeChange={() => {
649
- if (autoScroll) {
650
- scrollRef.current?.scrollToEnd({ animated: false });
651
- }
652
- }}
628
+ <KeyboardAvoidingView
629
+ style={{ flex: 1 }}
630
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
631
+ keyboardVerticalOffset={100}
653
632
  >
654
- <View style={styles.emptyContainer}>
655
- <Text style={styles.emptyText}>
656
- {messages.length === 0
657
- ? "Start a conversation to Find the Perfect Diamond"
658
- : `Chat Started at ${formatToTime(messages[0].createdAt)}`}
659
- </Text>
660
- </View>
661
- {(() => {
662
- const ordered = [...messages];
663
- return ordered.map((item, index) => {
664
- const isLatest = index === ordered.length - 1;
665
- const hasDiamonds = Boolean(
666
- productsByMsg[item.id]?.response?.body?.diamonds &&
667
- productsByMsg[item.id]?.response?.body?.diamonds.length > 0
668
- );
669
- return (
670
- <View key={item.id + index} style={{ gap: 12 }}>
671
- <MessageBubble
672
- message={item}
673
- theme={theme}
674
- priceMode={priceMode as string}
675
- handleFeedbackAction={handleFeedbackAction}
676
- onReloadResults={handleReloadResults}
677
- reloading={reloadLoadingIds.has(item.id)}
678
- hasResults={hasDiamonds}
679
- totalResults={
680
- productsByMsg[item.id]?.response?.header
681
- ?.total_diamonds_found || 0
682
- }
683
- shownResults={
684
- productsByMsg[item.id]?.response?.body?.diamonds
685
- .length || 0
686
- }
687
- onSuggestionSelect={handleSuggestionSelect}
688
- isLatest={isLatest}
689
- isTyping={typingMessageId === item.id}
690
- />
691
- {/* Trigger product fetch when this area enters viewport */}
692
- {item.role === "assistant" &&
693
- item.search_payload &&
694
- Object.keys(item.search_payload).length > 0 &&
695
- !productsByMsg[item.id] && (
696
- <LazyProductsFetcher
697
- priceMode={priceMode as string}
698
- messageId={item.id}
699
- payload={item.search_payload}
700
- onFetched={(id, result) => {
701
- setProductsByMsg((prev) => ({
702
- ...prev,
703
- [id]: result,
704
- }));
705
- const diamonds = result?.response?.body?.diamonds;
706
- const hasAny =
707
- Array.isArray(diamonds) && diamonds.length > 0;
708
- if (hasAny) {
709
- try {
710
- scrollRef.current?.scrollTo({
711
- x: 0,
712
- y: scrollY + 120,
713
- animated: true,
714
- });
715
- } catch (_) {}
716
- } else {
717
- setMessages((prev) =>
718
- prev.map((msg) =>
719
- msg.id === id
720
- ? {
721
- ...msg,
722
- id: msg.id,
723
- text: noResultsText,
724
- isLoading: false,
725
- }
726
- : msg
727
- )
728
- );
729
- }
730
- }}
731
- />
732
- )}
733
- {item.role === "assistant" && hasDiamonds && (
734
- <ProductsList
735
- data={
736
- productsByMsg[item.id]?.response?.body?.diamonds || []
737
- }
633
+ <ScrollView
634
+ ref={scrollRef}
635
+ keyboardShouldPersistTaps="handled"
636
+ onScroll={(e) => {
637
+ const { contentOffset, contentSize, layoutMeasurement } =
638
+ e.nativeEvent;
639
+ setScrollY(contentOffset.y);
640
+ const distanceFromBottom =
641
+ contentSize.height -
642
+ (layoutMeasurement.height + contentOffset.y);
643
+ // Enable auto-scroll when user is near the bottom; disable when scrolled up
644
+ setAutoScroll(distanceFromBottom < 48);
645
+ }}
646
+ scrollEventThrottle={16}
647
+ style={
648
+ modalHeight
649
+ ? {
650
+ height: modalHeight,
651
+ backgroundColor: theme["core-01"] || "#FFFFFF",
652
+ }
653
+ : { backgroundColor: theme["core-01"] || "#FFFFFF" }
654
+ }
655
+ contentContainerStyle={{
656
+ backgroundColor:
657
+ theme["core-01"] ||
658
+ themeProps?.listContentBackgroundColor ||
659
+ "#FFFFFF",
660
+ ...styles.listContent,
661
+ justifyContent: messages.length === 0 ? "center" : "flex-end",
662
+ minHeight: modalHeight ? modalHeight : undefined,
663
+ ...(isTablet
664
+ ? { maxWidth: 608, alignSelf: "center", width: "100%" }
665
+ : {}),
666
+ }}
667
+ onContentSizeChange={() => {
668
+ if (autoScroll) {
669
+ scrollRef.current?.scrollToEnd({ animated: false });
670
+ }
671
+ }}
672
+ >
673
+ {messages?.[0]?.createdAt && (
674
+ <EmptyContainer theme={theme}>
675
+ <EmptyText theme={theme}>
676
+ {`Chat Started at ${formatToTime(messages[0].createdAt)}`}
677
+ </EmptyText>
678
+ </EmptyContainer>
679
+ )}
680
+ {(() => {
681
+ const ordered = [...messages];
682
+ return ordered.map((item, index) => {
683
+ const isLatest = index === ordered.length - 1;
684
+ const hasDiamonds = Boolean(
685
+ productsByMsg[item.id]?.response?.body?.diamonds &&
686
+ productsByMsg[item.id]?.response?.body?.diamonds.length >
687
+ 0
688
+ );
689
+ return (
690
+ <View key={item.id + index} style={{ gap: 12 }}>
691
+ <MessageBubble
692
+ message={item}
693
+ userTheme={themeProps}
694
+ priceMode={priceMode as string}
695
+ handleFeedbackAction={handleFeedbackAction}
696
+ onReloadResults={handleReloadResults}
697
+ reloading={reloadLoadingIds.has(item.id)}
698
+ hasResults={hasDiamonds}
738
699
  totalResults={
739
700
  productsByMsg[item.id]?.response?.header
740
701
  ?.total_diamonds_found || 0
741
702
  }
742
- onViewAll={onViewAll}
743
- onItemPress={onItemPress}
744
- item={item}
703
+ shownResults={
704
+ productsByMsg[item.id]?.response?.body?.diamonds
705
+ .length || 0
706
+ }
707
+ onSuggestionSelect={handleSuggestionSelect}
708
+ isLatest={isLatest}
709
+ isTyping={typingMessageId === item.id}
745
710
  />
746
- )}
747
- {/* Suggestions are now rendered inside MessageBubble */}
748
- <MessageMetaRow
749
- message={item}
750
- priceMode={priceMode || ""}
751
- handleFeedbackAction={handleFeedbackAction}
752
- onReloadResults={handleReloadResults}
753
- reloading={reloadLoadingIds.has(item.id)}
754
- hasResults={hasDiamonds}
755
- />
756
- </View>
757
- );
758
- });
759
- })()}
760
- </ScrollView>
761
- <View style={styles.borderTop} />
762
- <View
763
- style={[
764
- styles.bottomContainer,
765
- isTablet
766
- ? { maxWidth: 608, alignSelf: "center", width: "100%" }
767
- : {},
768
- ]}
769
- >
770
- <ChatInput
771
- value={input}
772
- onChangeText={setInput}
773
- onSend={handleSend}
774
- disabled={loading}
775
- placeholder={placeholder}
776
- theme={theme}
777
- inputRef={inputRef}
778
- />
779
- <BetaNotice active={betaActive} />
780
- </View>
781
- </KeyboardAvoidingView>
782
- </View>
711
+ {/* Trigger product fetch when this area enters viewport */}
712
+ {item.role === "assistant" &&
713
+ item.search_payload &&
714
+ Object.keys(item.search_payload).length > 0 &&
715
+ !productsByMsg[item.id] && (
716
+ <LazyProductsFetcher
717
+ priceMode={priceMode as string}
718
+ messageId={item.id}
719
+ payload={item.search_payload}
720
+ onFetched={(id, result) => {
721
+ setProductsByMsg((prev) => ({
722
+ ...prev,
723
+ [id]: result,
724
+ }));
725
+ const diamonds = result?.response?.body?.diamonds;
726
+ const hasAny =
727
+ Array.isArray(diamonds) && diamonds.length > 0;
728
+ if (hasAny) {
729
+ try {
730
+ scrollRef.current?.scrollTo({
731
+ x: 0,
732
+ y: scrollY + 120,
733
+ animated: true,
734
+ });
735
+ } catch (_) {}
736
+ } else {
737
+ setMessages((prev) =>
738
+ prev.map((msg) =>
739
+ msg.id === id
740
+ ? {
741
+ ...msg,
742
+ id: msg.id,
743
+ text: noResultsText,
744
+ isLoading: false,
745
+ }
746
+ : msg
747
+ )
748
+ );
749
+ }
750
+ }}
751
+ />
752
+ )}
753
+ {item.role === "assistant" && hasDiamonds && (
754
+ <ProductsList
755
+ data={
756
+ productsByMsg[item.id]?.response?.body?.diamonds ||
757
+ []
758
+ }
759
+ totalResults={
760
+ productsByMsg[item.id]?.response?.header
761
+ ?.total_diamonds_found || 0
762
+ }
763
+ onViewAll={onViewAll}
764
+ onItemPress={onItemPress}
765
+ item={item}
766
+ />
767
+ )}
768
+ {/* Suggestions are now rendered inside MessageBubble */}
769
+ <MessageMetaRow
770
+ message={item}
771
+ priceMode={priceMode || ""}
772
+ handleFeedbackAction={handleFeedbackAction}
773
+ onReloadResults={handleReloadResults}
774
+ reloading={reloadLoadingIds.has(item.id)}
775
+ hasResults={hasDiamonds}
776
+ />
777
+ </View>
778
+ );
779
+ });
780
+ })()}
781
+ </ScrollView>
782
+ <LineView />
783
+ <BottomContainer theme={theme}>
784
+ <ChatInput
785
+ value={input}
786
+ onChangeText={setInput}
787
+ onSend={handleSend}
788
+ disabled={loading}
789
+ placeholder={placeholder}
790
+ theme={themeProps}
791
+ inputRef={inputRef}
792
+ />
793
+ <BetaNotice active={betaActive} />
794
+ </BottomContainer>
795
+ </KeyboardAvoidingView>
796
+ </Container>
797
+ </ThemeProvider>
783
798
  );
784
799
  }
785
800
  );
786
801
 
787
802
  ChatWidget.displayName = "ChatWidget";
788
803
 
804
+ const Container = styled.View<{ theme: DefaultTheme }>`
805
+ background-color: ${({ theme }: { theme: DefaultTheme }) =>
806
+ theme["core-01"] || "#FFFFFF"};
807
+ flex: 1;
808
+ width: 100%;
809
+ `;
810
+
811
+ const EmptyContainer = styled.View<{ theme: DefaultTheme }>`
812
+ padding: 16px;
813
+ align-items: center;
814
+ justify-content: center;
815
+ background-color: ${({ theme }: { theme: DefaultTheme }) =>
816
+ theme["core-01"] || "#FFFFFF"};
817
+ `;
818
+
819
+ const EmptyText = styled.Text<{ theme: DefaultTheme }>`
820
+ font-size: 13px;
821
+ font-weight: 400;
822
+ color: ${({ theme }: { theme: DefaultTheme }) =>
823
+ theme["core-06"] || "#4F4E57"};
824
+ `;
825
+
826
+ const BottomContainer = styled.View<{ theme: DefaultTheme }>`
827
+ padding-vertical: 12px;
828
+ padding-horizontal: 16px;
829
+ flex-direction: column;
830
+ justify-content: space-between;
831
+ align-items: center;
832
+ align-self: stretch;
833
+ gap: 12px;
834
+ ${({ theme }: { theme: DefaultTheme }) =>
835
+ theme.isTablet &&
836
+ css`
837
+ maxWidth: 608,
838
+ align-self: center;
839
+ width: 100%;
840
+ `}
841
+ `;
842
+
843
+ const LineView = styled.View<{ theme: DefaultTheme }>`
844
+ border-top-color: ${({ theme }: { theme: DefaultTheme }) =>
845
+ theme["core-03"] || "#E0E0E0"};
846
+ border-top-width: 1;
847
+ `;
848
+
789
849
  const styles = StyleSheet.create({
790
- container: {
791
- flex: 1,
792
- width: "100%",
793
- },
794
850
  listContent: {
795
851
  paddingVertical: 8,
796
852
  flexGrow: 1,
@@ -800,28 +856,4 @@ const styles = StyleSheet.create({
800
856
  paddingVertical: 8,
801
857
  alignItems: "center",
802
858
  },
803
- emptyContainer: {
804
- padding: 16,
805
- alignItems: "center",
806
- justifyContent: "center",
807
- backgroundColor: "#FFFFFF",
808
- },
809
- emptyText: {
810
- fontSize: 13,
811
- fontWeight: "400",
812
- color: "#4F4E57",
813
- },
814
- bottomContainer: {
815
- paddingVertical: 12,
816
- paddingHorizontal: 16,
817
- flexDirection: "column",
818
- justifyContent: "space-between",
819
- alignItems: "center",
820
- alignSelf: "stretch",
821
- gap: 12,
822
- },
823
- borderTop: {
824
- borderTopColor: "#E0E0E0",
825
- borderTopWidth: 1,
826
- },
827
859
  });