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.
- package/README.md +153 -0
- package/dist/chat-widget.js +2 -0
- package/dist/chat-widget.js.LICENSE.txt +29 -0
- package/lib/commonjs/api.js +157 -0
- package/lib/commonjs/api.js.map +1 -0
- package/lib/commonjs/components/ChatHeader.js +111 -0
- package/lib/commonjs/components/ChatHeader.js.map +1 -0
- package/lib/commonjs/components/ChatInput.js +144 -0
- package/lib/commonjs/components/ChatInput.js.map +1 -0
- package/lib/commonjs/components/ChatWidget.js +469 -0
- package/lib/commonjs/components/ChatWidget.js.map +1 -0
- package/lib/commonjs/components/MessageBubble.js +122 -0
- package/lib/commonjs/components/MessageBubble.js.map +1 -0
- package/lib/commonjs/components/ProductsList.js +139 -0
- package/lib/commonjs/components/ProductsList.js.map +1 -0
- package/lib/commonjs/components/SuggestionsRow.js +59 -0
- package/lib/commonjs/components/SuggestionsRow.js.map +1 -0
- package/lib/commonjs/components/utils.js +37 -0
- package/lib/commonjs/components/utils.js.map +1 -0
- package/lib/commonjs/index.js +70 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/index.native.js +70 -0
- package/lib/commonjs/index.native.js.map +1 -0
- package/lib/commonjs/index.web.js +30 -0
- package/lib/commonjs/index.web.js.map +1 -0
- package/lib/commonjs/storage.js +136 -0
- package/lib/commonjs/storage.js.map +1 -0
- package/lib/commonjs/theme.js +29 -0
- package/lib/commonjs/theme.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/api.js +146 -0
- package/lib/module/api.js.map +1 -0
- package/lib/module/components/ChatHeader.js +105 -0
- package/lib/module/components/ChatHeader.js.map +1 -0
- package/lib/module/components/ChatInput.js +136 -0
- package/lib/module/components/ChatInput.js.map +1 -0
- package/lib/module/components/ChatWidget.js +461 -0
- package/lib/module/components/ChatWidget.js.map +1 -0
- package/lib/module/components/MessageBubble.js +116 -0
- package/lib/module/components/MessageBubble.js.map +1 -0
- package/lib/module/components/ProductsList.js +133 -0
- package/lib/module/components/ProductsList.js.map +1 -0
- package/lib/module/components/SuggestionsRow.js +52 -0
- package/lib/module/components/SuggestionsRow.js.map +1 -0
- package/lib/module/components/utils.js +29 -0
- package/lib/module/components/utils.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/index.native.js +7 -0
- package/lib/module/index.native.js.map +1 -0
- package/lib/module/index.web.js +23 -0
- package/lib/module/index.web.js.map +1 -0
- package/lib/module/storage.js +129 -0
- package/lib/module/storage.js.map +1 -0
- package/lib/module/theme.js +22 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/api.d.ts +10 -0
- package/lib/typescript/api.d.ts.map +1 -0
- package/lib/typescript/components/ChatHeader.d.ts +6 -0
- package/lib/typescript/components/ChatHeader.d.ts.map +1 -0
- package/lib/typescript/components/ChatInput.d.ts +15 -0
- package/lib/typescript/components/ChatInput.d.ts.map +1 -0
- package/lib/typescript/components/ChatWidget.d.ts +4 -0
- package/lib/typescript/components/ChatWidget.d.ts.map +1 -0
- package/lib/typescript/components/MessageBubble.d.ts +13 -0
- package/lib/typescript/components/MessageBubble.d.ts.map +1 -0
- package/lib/typescript/components/ProductsList.d.ts +9 -0
- package/lib/typescript/components/ProductsList.d.ts.map +1 -0
- package/lib/typescript/components/SuggestionsRow.d.ts +8 -0
- package/lib/typescript/components/SuggestionsRow.d.ts.map +1 -0
- package/lib/typescript/components/utils.d.ts +8 -0
- package/lib/typescript/components/utils.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/index.native.d.ts +6 -0
- package/lib/typescript/index.native.d.ts.map +1 -0
- package/lib/typescript/index.web.d.ts +3 -0
- package/lib/typescript/index.web.d.ts.map +1 -0
- package/lib/typescript/storage.d.ts +28 -0
- package/lib/typescript/storage.d.ts.map +1 -0
- package/lib/typescript/theme.d.ts +4 -0
- package/lib/typescript/theme.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +51 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/package.json +90 -0
- package/src/api.ts +200 -0
- package/src/components/ChatHeader.tsx +114 -0
- package/src/components/ChatInput.tsx +163 -0
- package/src/components/ChatWidget.tsx +679 -0
- package/src/components/MessageBubble.tsx +181 -0
- package/src/components/ProductsList.tsx +160 -0
- package/src/components/SuggestionsRow.tsx +73 -0
- package/src/components/utils.ts +43 -0
- package/src/index.native.tsx +6 -0
- package/src/index.ts +7 -0
- package/src/index.web.tsx +33 -0
- package/src/storage.ts +142 -0
- package/src/theme.ts +21 -0
- 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,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
|
+
}
|