vdb-ai-chat 1.0.0

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 (102) hide show
  1. package/README.md +153 -0
  2. package/dist/chat-widget.js +2 -0
  3. package/dist/chat-widget.js.LICENSE.txt +29 -0
  4. package/lib/commonjs/api.js +157 -0
  5. package/lib/commonjs/api.js.map +1 -0
  6. package/lib/commonjs/components/ChatHeader.js +111 -0
  7. package/lib/commonjs/components/ChatHeader.js.map +1 -0
  8. package/lib/commonjs/components/ChatInput.js +144 -0
  9. package/lib/commonjs/components/ChatInput.js.map +1 -0
  10. package/lib/commonjs/components/ChatWidget.js +469 -0
  11. package/lib/commonjs/components/ChatWidget.js.map +1 -0
  12. package/lib/commonjs/components/MessageBubble.js +122 -0
  13. package/lib/commonjs/components/MessageBubble.js.map +1 -0
  14. package/lib/commonjs/components/ProductsList.js +139 -0
  15. package/lib/commonjs/components/ProductsList.js.map +1 -0
  16. package/lib/commonjs/components/SuggestionsRow.js +59 -0
  17. package/lib/commonjs/components/SuggestionsRow.js.map +1 -0
  18. package/lib/commonjs/components/utils.js +37 -0
  19. package/lib/commonjs/components/utils.js.map +1 -0
  20. package/lib/commonjs/index.js +70 -0
  21. package/lib/commonjs/index.js.map +1 -0
  22. package/lib/commonjs/index.native.js +70 -0
  23. package/lib/commonjs/index.native.js.map +1 -0
  24. package/lib/commonjs/index.web.js +30 -0
  25. package/lib/commonjs/index.web.js.map +1 -0
  26. package/lib/commonjs/storage.js +136 -0
  27. package/lib/commonjs/storage.js.map +1 -0
  28. package/lib/commonjs/theme.js +29 -0
  29. package/lib/commonjs/theme.js.map +1 -0
  30. package/lib/commonjs/types.js +2 -0
  31. package/lib/commonjs/types.js.map +1 -0
  32. package/lib/module/api.js +146 -0
  33. package/lib/module/api.js.map +1 -0
  34. package/lib/module/components/ChatHeader.js +105 -0
  35. package/lib/module/components/ChatHeader.js.map +1 -0
  36. package/lib/module/components/ChatInput.js +136 -0
  37. package/lib/module/components/ChatInput.js.map +1 -0
  38. package/lib/module/components/ChatWidget.js +461 -0
  39. package/lib/module/components/ChatWidget.js.map +1 -0
  40. package/lib/module/components/MessageBubble.js +116 -0
  41. package/lib/module/components/MessageBubble.js.map +1 -0
  42. package/lib/module/components/ProductsList.js +133 -0
  43. package/lib/module/components/ProductsList.js.map +1 -0
  44. package/lib/module/components/SuggestionsRow.js +52 -0
  45. package/lib/module/components/SuggestionsRow.js.map +1 -0
  46. package/lib/module/components/utils.js +29 -0
  47. package/lib/module/components/utils.js.map +1 -0
  48. package/lib/module/index.js +7 -0
  49. package/lib/module/index.js.map +1 -0
  50. package/lib/module/index.native.js +7 -0
  51. package/lib/module/index.native.js.map +1 -0
  52. package/lib/module/index.web.js +23 -0
  53. package/lib/module/index.web.js.map +1 -0
  54. package/lib/module/storage.js +129 -0
  55. package/lib/module/storage.js.map +1 -0
  56. package/lib/module/theme.js +22 -0
  57. package/lib/module/theme.js.map +1 -0
  58. package/lib/module/types.js +2 -0
  59. package/lib/module/types.js.map +1 -0
  60. package/lib/typescript/api.d.ts +10 -0
  61. package/lib/typescript/api.d.ts.map +1 -0
  62. package/lib/typescript/components/ChatHeader.d.ts +6 -0
  63. package/lib/typescript/components/ChatHeader.d.ts.map +1 -0
  64. package/lib/typescript/components/ChatInput.d.ts +15 -0
  65. package/lib/typescript/components/ChatInput.d.ts.map +1 -0
  66. package/lib/typescript/components/ChatWidget.d.ts +4 -0
  67. package/lib/typescript/components/ChatWidget.d.ts.map +1 -0
  68. package/lib/typescript/components/MessageBubble.d.ts +13 -0
  69. package/lib/typescript/components/MessageBubble.d.ts.map +1 -0
  70. package/lib/typescript/components/ProductsList.d.ts +9 -0
  71. package/lib/typescript/components/ProductsList.d.ts.map +1 -0
  72. package/lib/typescript/components/SuggestionsRow.d.ts +8 -0
  73. package/lib/typescript/components/SuggestionsRow.d.ts.map +1 -0
  74. package/lib/typescript/components/utils.d.ts +8 -0
  75. package/lib/typescript/components/utils.d.ts.map +1 -0
  76. package/lib/typescript/index.d.ts +6 -0
  77. package/lib/typescript/index.d.ts.map +1 -0
  78. package/lib/typescript/index.native.d.ts +6 -0
  79. package/lib/typescript/index.native.d.ts.map +1 -0
  80. package/lib/typescript/index.web.d.ts +3 -0
  81. package/lib/typescript/index.web.d.ts.map +1 -0
  82. package/lib/typescript/storage.d.ts +28 -0
  83. package/lib/typescript/storage.d.ts.map +1 -0
  84. package/lib/typescript/theme.d.ts +4 -0
  85. package/lib/typescript/theme.d.ts.map +1 -0
  86. package/lib/typescript/types.d.ts +51 -0
  87. package/lib/typescript/types.d.ts.map +1 -0
  88. package/package.json +90 -0
  89. package/src/api.ts +200 -0
  90. package/src/components/ChatHeader.tsx +114 -0
  91. package/src/components/ChatInput.tsx +163 -0
  92. package/src/components/ChatWidget.tsx +679 -0
  93. package/src/components/MessageBubble.tsx +181 -0
  94. package/src/components/ProductsList.tsx +160 -0
  95. package/src/components/SuggestionsRow.tsx +73 -0
  96. package/src/components/utils.ts +43 -0
  97. package/src/index.native.tsx +6 -0
  98. package/src/index.ts +7 -0
  99. package/src/index.web.tsx +33 -0
  100. package/src/storage.ts +142 -0
  101. package/src/theme.ts +21 -0
  102. package/src/types.ts +56 -0
@@ -0,0 +1,181 @@
1
+ import React, { memo } from "react";
2
+ import { View, Text, StyleSheet, Image, TouchableOpacity, Platform } from "react-native";
3
+ import type { ChatMessage, ChatTheme } from "../types";
4
+ import { FeedbackAction, formatToTime } from "./utils";
5
+
6
+ // Use expo-image on native if available, fallback to RN Image
7
+ let ImageComponent: typeof Image = Image;
8
+ if (Platform.OS !== "web") {
9
+ try {
10
+ const ExpoImage = require("expo-image").Image;
11
+ if (ExpoImage) ImageComponent = ExpoImage;
12
+ } catch {
13
+ // expo-image not installed, use React Native Image
14
+ }
15
+ }
16
+
17
+ interface Props {
18
+ message: ChatMessage;
19
+ theme: ChatTheme;
20
+ conversationId: string | null;
21
+ handleFeedbackAction: (
22
+ action: FeedbackAction,
23
+ conversation_id: string,
24
+ message_id: string
25
+ ) => void;
26
+ onReloadResults?: (message: ChatMessage) => void;
27
+ }
28
+
29
+ const MessageBubbleComponent: React.FC<Props> = ({
30
+ message,
31
+ theme,
32
+ conversationId,
33
+ handleFeedbackAction,
34
+ onReloadResults,
35
+ }) => {
36
+ const isUser = message.role === "user";
37
+ const isValidMessageId =
38
+ typeof message.id === "string" &&
39
+ message.id.length > 0 &&
40
+ !message.id.startsWith("bot-loading-");
41
+ const canFeedback =
42
+ message.role === "assistant" &&
43
+ !message.isLoading &&
44
+ isValidMessageId &&
45
+ !!conversationId;
46
+
47
+ return (
48
+ <View
49
+ style={[styles.container, isUser ? styles.alignRight : styles.alignLeft]}
50
+ >
51
+ <View
52
+ style={[
53
+ styles.bubble,
54
+ {
55
+ backgroundColor: isUser
56
+ ? theme.userBubbleColor
57
+ : theme.botBubbleColor,
58
+ borderRadius: theme.borderRadius || 4,
59
+ borderTopRightRadius: isUser ? 4 : theme.borderRadius,
60
+ borderTopLeftRadius: isUser ? theme.borderRadius : 4,
61
+ },
62
+ ]}
63
+ >
64
+ <Text
65
+ style={[
66
+ styles.text,
67
+ {
68
+ color: isUser ? theme.userTextColor : theme.botTextColor,
69
+ fontFamily: theme.fontFamily,
70
+ fontSize: theme.fontSize,
71
+ },
72
+ ]}
73
+ >
74
+ {message.text || ""}
75
+ </Text>
76
+ </View>
77
+ {(canFeedback ||
78
+ (message.agent_response &&
79
+ typeof message.agent_response === "object")) && (
80
+ <View style={styles.rowContainer}>
81
+ <View style={styles.time}>
82
+ <Text>{formatToTime(message.createdAt)}</Text>
83
+ </View>
84
+ {message.agent_response &&
85
+ typeof message.agent_response === "object" && (
86
+ <TouchableOpacity onPress={() => onReloadResults?.(message)}>
87
+ <Text>Reload Results</Text>
88
+ </TouchableOpacity>
89
+ )}
90
+ <View style={styles.likeDislikeContainer}>
91
+ <TouchableOpacity
92
+ onPress={() =>
93
+ handleFeedbackAction(
94
+ FeedbackAction.LIKE,
95
+ String(conversationId),
96
+ message.id
97
+ )
98
+ }
99
+ disabled={!canFeedback}
100
+ >
101
+ <ImageComponent
102
+ source={{
103
+ uri:
104
+ message.reaction === FeedbackAction.LIKE
105
+ ? "https://cdn.vdbapp.com/ai/chat-widget/assets/img/like-filled.svg"
106
+ : "https://cdn.vdbapp.com/ai/chat-widget/assets/img/like.svg",
107
+ }}
108
+ resizeMode="contain"
109
+ style={{ width: 16, height: 16 }}
110
+ />
111
+ </TouchableOpacity>
112
+ <TouchableOpacity
113
+ onPress={() =>
114
+ handleFeedbackAction(
115
+ FeedbackAction.DISLIKE,
116
+ String(conversationId),
117
+ message.id
118
+ )
119
+ }
120
+ disabled={!canFeedback}
121
+ >
122
+ <ImageComponent
123
+ source={{
124
+ uri:
125
+ message.reaction === FeedbackAction.DISLIKE
126
+ ? "https://cdn.vdbapp.com/ai/chat-widget/assets/img/dislike-filled.svg"
127
+ : "https://cdn.vdbapp.com/ai/chat-widget/assets/img/dislike.svg",
128
+ }}
129
+ resizeMode="contain"
130
+ style={{ width: 16, height: 16 }}
131
+ />
132
+ </TouchableOpacity>
133
+ </View>
134
+ </View>
135
+ )}
136
+ </View>
137
+ );
138
+ };
139
+
140
+ // Memoize to prevent re-renders when parent re-renders
141
+ export const MessageBubble = memo(MessageBubbleComponent);
142
+
143
+ const styles = StyleSheet.create({
144
+ container: {
145
+ paddingHorizontal: 8,
146
+ marginVertical: 4,
147
+ width: "100%",
148
+ },
149
+ rowContainer: {
150
+ flexDirection: "row",
151
+ justifyContent: "space-between",
152
+ alignItems: "center",
153
+ gap: 12,
154
+ },
155
+ likeDislikeContainer: {
156
+ flexDirection: "row",
157
+ gap: 12,
158
+ marginTop: 4,
159
+ marginBottom: 4,
160
+ },
161
+ alignRight: {
162
+ alignItems: "flex-end",
163
+ },
164
+ alignLeft: {
165
+ alignItems: "flex-start",
166
+ },
167
+ bubble: {
168
+ maxWidth: "80%",
169
+ paddingHorizontal: 12,
170
+ paddingVertical: 8,
171
+ },
172
+ text: {
173
+ lineHeight: 20,
174
+ },
175
+ time: {
176
+ fontSize: 12,
177
+ color: "#666",
178
+ marginTop: 4,
179
+ marginLeft: 8,
180
+ },
181
+ });
@@ -0,0 +1,160 @@
1
+ import {
2
+ View,
3
+ Image,
4
+ StyleSheet,
5
+ Text,
6
+ ScrollView,
7
+ TouchableOpacity,
8
+ Platform,
9
+ } from "react-native";
10
+ import React, { memo } from "react";
11
+
12
+ // Use expo-image on native if available, fallback to RN Image
13
+ let ImageComponent: typeof Image = Image;
14
+ if (Platform.OS !== "web") {
15
+ try {
16
+ const ExpoImage = require("expo-image").Image;
17
+ if (ExpoImage) ImageComponent = ExpoImage;
18
+ } catch {
19
+ // expo-image not installed, use React Native Image
20
+ }
21
+ }
22
+
23
+ interface ProductsListProps {
24
+ data: any;
25
+ onViewAll?: () => void;
26
+ onItemPress?: (item: any) => void;
27
+ }
28
+
29
+ const ProductsListComponent: React.FC<ProductsListProps> = ({
30
+ data,
31
+ onViewAll,
32
+ onItemPress,
33
+ }) => {
34
+ if (!data || !data.length) return null;
35
+
36
+ return (
37
+ <View style={styles.wrapper}>
38
+ <ScrollView
39
+ horizontal
40
+ showsHorizontalScrollIndicator={false}
41
+ contentContainerStyle={styles.listContent}
42
+ >
43
+ {data.map((item: any) => (
44
+ <TouchableOpacity
45
+ key={item.id}
46
+ onPress={() => {
47
+ onItemPress?.(item);
48
+ }}
49
+ >
50
+ <View key={item.id} style={styles.card}>
51
+ <ImageComponent
52
+ style={styles.image}
53
+ source={{ uri: item.image_thumb_url }}
54
+ />
55
+
56
+ <View style={styles.content}>
57
+ <Text numberOfLines={2} style={styles.title}>
58
+ {item.short_title}
59
+ </Text>
60
+ <Text style={styles.price}>${item.total_sales_price}</Text>
61
+ </View>
62
+ </View>
63
+ </TouchableOpacity>
64
+ ))}
65
+ </ScrollView>
66
+
67
+ {/* View All Button */}
68
+ <TouchableOpacity
69
+ style={styles.button}
70
+ activeOpacity={0.8}
71
+ onPress={onViewAll}
72
+ >
73
+ <Text style={styles.buttonText}>View All {">>"}</Text>
74
+ </TouchableOpacity>
75
+ </View>
76
+ );
77
+ };
78
+
79
+ const styles = StyleSheet.create({
80
+ wrapper: {
81
+ paddingHorizontal: 12,
82
+ marginHorizontal: 12,
83
+ marginBottom: 12,
84
+ paddingTop: 6,
85
+ paddingBottom: 14,
86
+ backgroundColor: "#fff",
87
+ borderRadius: 8,
88
+ borderWidth: 1,
89
+ borderColor: "#e8e8e8",
90
+ elevation: 3,
91
+ shadowColor: "#000",
92
+ shadowOpacity: 0.08,
93
+ shadowOffset: { width: 0, height: 2 },
94
+ shadowRadius: 6,
95
+ },
96
+
97
+ listContent: {
98
+ gap: 12,
99
+ paddingVertical: 6,
100
+ },
101
+
102
+ card: {
103
+ width: 150,
104
+ backgroundColor: "#fff",
105
+ borderRadius: 14,
106
+ overflow: "hidden",
107
+ borderColor: "#e8e8e8",
108
+ borderWidth: 1,
109
+ elevation: 3,
110
+ shadowColor: "#000",
111
+ shadowOpacity: 0.08,
112
+ shadowOffset: { width: 0, height: 2 },
113
+ shadowRadius: 6,
114
+ },
115
+
116
+ image: {
117
+ width: "100%",
118
+ height: 120,
119
+ borderBottomWidth: 1,
120
+ borderBottomColor: "#e8e8e8",
121
+ },
122
+
123
+ content: {
124
+ padding: 10,
125
+ gap: 4,
126
+ },
127
+
128
+ title: {
129
+ fontSize: 13,
130
+ color: "#222",
131
+ fontWeight: "500",
132
+ },
133
+
134
+ price: {
135
+ marginTop: 4,
136
+ fontSize: 14,
137
+ fontWeight: "700",
138
+ color: "#000",
139
+ },
140
+
141
+ button: {
142
+ marginTop: 12,
143
+ alignSelf: "center",
144
+ paddingHorizontal: 20,
145
+ paddingVertical: 8,
146
+ backgroundColor: "#804195",
147
+ borderRadius: 20,
148
+ width: 300,
149
+ alignItems: "center",
150
+ },
151
+
152
+ buttonText: {
153
+ color: "#fff",
154
+ fontSize: 14,
155
+ fontWeight: "600",
156
+ },
157
+ });
158
+
159
+ const ProductsList = memo(ProductsListComponent);
160
+ export default ProductsList;
@@ -0,0 +1,73 @@
1
+ import React, { memo } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ ScrollView,
8
+ } from "react-native";
9
+
10
+ interface SuggestionsRowProps {
11
+ suggestions: string[];
12
+ onSelect: (value: string) => void;
13
+ }
14
+
15
+ const SuggestionsRowComponent: React.FC<SuggestionsRowProps> = ({
16
+ suggestions,
17
+ onSelect,
18
+ }) => {
19
+ if (!suggestions.length) return null;
20
+
21
+ return (
22
+ <ScrollView
23
+ horizontal
24
+ showsHorizontalScrollIndicator={false}
25
+ contentContainerStyle={styles.container}
26
+ >
27
+ {suggestions.map((s) => (
28
+ <TouchableOpacity
29
+ key={s}
30
+ style={styles.chip}
31
+ onPress={() => onSelect(s)}
32
+ activeOpacity={0.75}
33
+ >
34
+ <Text style={styles.chipText}>{s}</Text>
35
+ </TouchableOpacity>
36
+ ))}
37
+ </ScrollView>
38
+ );
39
+ };
40
+
41
+ const styles = StyleSheet.create({
42
+ container: {
43
+ paddingHorizontal: 12,
44
+ paddingBottom: 8,
45
+ flexDirection: "row",
46
+ gap: 10,
47
+ },
48
+
49
+ chip: {
50
+ backgroundColor: "#ffffff",
51
+ paddingHorizontal: 16,
52
+ paddingVertical: 8,
53
+ borderRadius: 18,
54
+
55
+ // Modern soft shadow (Material 3 / iOS style)
56
+ shadowColor: "#000",
57
+ shadowOpacity: 0.07,
58
+ shadowRadius: 4,
59
+ shadowOffset: { width: 0, height: 2 },
60
+
61
+ elevation: 2,
62
+ },
63
+
64
+ chipText: {
65
+ fontSize: 14,
66
+ color: "#1A1A1A",
67
+ fontWeight: "500",
68
+ letterSpacing: 0.3,
69
+ },
70
+ });
71
+
72
+ const SuggestionsRow = memo(SuggestionsRowComponent);
73
+ export default SuggestionsRow;
@@ -0,0 +1,43 @@
1
+ import { Storage } from "../storage";
2
+
3
+ export function formatToTime(timestamp: number): string {
4
+ const date = new Date(timestamp);
5
+
6
+ let hours = date.getHours();
7
+ const minutes = date.getMinutes();
8
+ const ampm = hours >= 12 ? "pm" : "am";
9
+
10
+ hours = hours % 12;
11
+ hours = hours ? hours : 12; // 0 → 12
12
+
13
+ const mins = minutes < 10 ? `0${minutes}` : minutes;
14
+
15
+ return `${hours}:${mins} ${ampm}`;
16
+ }
17
+
18
+ export enum FeedbackAction {
19
+ LIKE = "1",
20
+ DISLIKE = "2",
21
+ UNSET = "0",
22
+ }
23
+
24
+ export const fetchConversationId = async (
25
+ _priceMode: string
26
+ ): Promise<string | null> => {
27
+ const conversations = await Storage.getJSON<Record<string, any>>(
28
+ "vdbchat_conversations",
29
+ {}
30
+ );
31
+ if (!conversations) return null;
32
+
33
+ let priceMode = _priceMode;
34
+ if (!priceMode) {
35
+ const userData = await Storage.getJSON<{ price_mode?: string }>(
36
+ "userData",
37
+ {}
38
+ );
39
+ priceMode = userData?.price_mode || "";
40
+ }
41
+
42
+ return conversations[priceMode]?.conversation_id || null;
43
+ };
@@ -0,0 +1,6 @@
1
+ // React Native entry point - re-export everything from main index
2
+ export { ChatWidget } from './components/ChatWidget';
3
+ export * from './types';
4
+ export * from './theme';
5
+ export { normaliseMessages } from './api';
6
+ export { initStorage, isStorageInitialized, Storage } from './storage';
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // Main entry for package consumers
2
+ export { ChatWidget } from './components/ChatWidget';
3
+ export * from './types';
4
+ export * from './theme';
5
+ export { normaliseMessages } from './api';
6
+ export { initStorage, isStorageInitialized, Storage } from './storage';
7
+
@@ -0,0 +1,33 @@
1
+ import * as React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { View, StyleSheet } from "react-native";
4
+ import { ChatWidget } from "./components/ChatWidget";
5
+ import type { ChatTheme } from "./types";
6
+
7
+ export function renderChatApp(
8
+ domElement: HTMLElement,
9
+ apiUrl: string,
10
+ theme?: Partial<ChatTheme>,
11
+ onClose?: () => void,
12
+ onClearChat?: () => void
13
+ ) {
14
+ const root = createRoot(domElement);
15
+ root.render(
16
+ <View style={styles.root}>
17
+ <ChatWidget
18
+ apiUrl={apiUrl}
19
+ theme={theme}
20
+ onClose={onClose}
21
+ onClearChat={onClearChat}
22
+ />
23
+ </View>
24
+ );
25
+ }
26
+
27
+ const styles = StyleSheet.create({
28
+ root: {
29
+ flex: 1,
30
+ height: "100%",
31
+ width: "100%",
32
+ },
33
+ });
package/src/storage.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { Platform } from 'react-native';
2
+
3
+ // Storage interface that works across platforms
4
+ interface StorageInterface {
5
+ getItem: (key: string) => Promise<string | null>;
6
+ setItem: (key: string, value: string) => Promise<void>;
7
+ removeItem: (key: string) => Promise<void>;
8
+ }
9
+
10
+ // Helper to safely get localStorage (only available on web)
11
+ function getLocalStorage(): Storage | null {
12
+ if (typeof window !== 'undefined' && window.localStorage) {
13
+ return window.localStorage;
14
+ }
15
+ return null;
16
+ }
17
+
18
+ // Web storage implementation using localStorage (lazy access)
19
+ const webStorage: StorageInterface = {
20
+ getItem: async (key: string) => {
21
+ try {
22
+ const storage = getLocalStorage();
23
+ return storage ? storage.getItem(key) : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ },
28
+ setItem: async (key: string, value: string) => {
29
+ try {
30
+ const storage = getLocalStorage();
31
+ if (storage) {
32
+ storage.setItem(key, value);
33
+ }
34
+ } catch {
35
+ // ignore
36
+ }
37
+ },
38
+ removeItem: async (key: string) => {
39
+ try {
40
+ const storage = getLocalStorage();
41
+ if (storage) {
42
+ storage.removeItem(key);
43
+ }
44
+ } catch {
45
+ // ignore
46
+ }
47
+ },
48
+ };
49
+
50
+ // Native storage - will be set by the app
51
+ let nativeStorage: StorageInterface | null = null;
52
+ let hasWarnedAboutStorage = false;
53
+
54
+ /**
55
+ * Initialize storage with AsyncStorage for React Native.
56
+ * Call this once at app startup before using ChatWidget.
57
+ *
58
+ * @example
59
+ * import AsyncStorage from '@react-native-async-storage/async-storage';
60
+ * import { initStorage } from 'vdb-ai-chat';
61
+ * initStorage(AsyncStorage);
62
+ */
63
+ export function initStorage(asyncStorage: StorageInterface): void {
64
+ nativeStorage = asyncStorage;
65
+ hasWarnedAboutStorage = false;
66
+ }
67
+
68
+ /**
69
+ * Check if storage is properly initialized for the current platform
70
+ */
71
+ export function isStorageInitialized(): boolean {
72
+ if (Platform.OS === 'web') {
73
+ return getLocalStorage() !== null;
74
+ }
75
+ return nativeStorage !== null;
76
+ }
77
+
78
+ // No-op storage for when native storage isn't initialized
79
+ const noopStorage: StorageInterface = {
80
+ getItem: async () => null,
81
+ setItem: async () => {},
82
+ removeItem: async () => {},
83
+ };
84
+
85
+ // Get the appropriate storage based on platform
86
+ function getStorage(): StorageInterface {
87
+ if (Platform.OS === 'web') {
88
+ return webStorage;
89
+ }
90
+
91
+ if (!nativeStorage) {
92
+ // Only warn once to avoid console spam
93
+ if (!hasWarnedAboutStorage) {
94
+ console.warn(
95
+ '[vdb-ai-chat] AsyncStorage not initialized. Call initStorage(AsyncStorage) at app startup.'
96
+ );
97
+ hasWarnedAboutStorage = true;
98
+ }
99
+ return noopStorage;
100
+ }
101
+
102
+ return nativeStorage;
103
+ }
104
+
105
+ // Exported storage methods
106
+ export const Storage = {
107
+ getItem: async (key: string): Promise<string | null> => {
108
+ return getStorage().getItem(key);
109
+ },
110
+
111
+ setItem: async (key: string, value: string): Promise<void> => {
112
+ return getStorage().setItem(key, value);
113
+ },
114
+
115
+ removeItem: async (key: string): Promise<void> => {
116
+ return getStorage().removeItem(key);
117
+ },
118
+
119
+ // Convenience method to get and parse JSON
120
+ getJSON: async <T = any>(key: string, defaultValue: T | null = null): Promise<T | null> => {
121
+ try {
122
+ const value = await getStorage().getItem(key);
123
+ if (value) {
124
+ return JSON.parse(value) as T;
125
+ }
126
+ return defaultValue;
127
+ } catch {
128
+ return defaultValue;
129
+ }
130
+ },
131
+
132
+ // Convenience method to stringify and set JSON
133
+ setJSON: async (key: string, value: any): Promise<void> => {
134
+ try {
135
+ await getStorage().setItem(key, JSON.stringify(value));
136
+ } catch {
137
+ // ignore
138
+ }
139
+ },
140
+ };
141
+
142
+ export default Storage;
package/src/theme.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { ChatTheme } from "./types";
2
+
3
+ export const defaultTheme: ChatTheme = {
4
+ primaryColor: "#0b93f6",
5
+ backgroundColor: "#ffffff",
6
+ inputColor: "#FFF",
7
+ inputBackgroundColor: "#f5f5f5",
8
+ inputBorderRadius: 8,
9
+ inputTextColor: "#000000",
10
+ userBubbleColor: "#804195",
11
+ userTextColor: "#ffffff",
12
+ botBubbleColor: "#e5e5ea",
13
+ botTextColor: "#000000",
14
+ borderRadius: 18,
15
+ fontFamily: undefined,
16
+ fontSize: 16,
17
+ };
18
+
19
+ export function mergeTheme(overrides?: Partial<ChatTheme>): ChatTheme {
20
+ return { ...defaultTheme, ...(overrides || {}) };
21
+ }