vdb-ai-chat 1.0.0 → 1.0.1
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/248.chat-widget.js +1 -0
- package/dist/538.chat-widget.js +1 -0
- package/dist/751.chat-widget.js +1 -0
- package/dist/chat-widget.js +1 -1
- package/dist/chat-widget.js.LICENSE.txt +2 -0
- package/lib/commonjs/api.js +2 -5
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/ChatHeader.js +15 -2
- package/lib/commonjs/components/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/ChatWidget.js +49 -24
- package/lib/commonjs/components/ChatWidget.js.map +1 -1
- package/lib/commonjs/components/MessageBubble.js +33 -9
- package/lib/commonjs/components/MessageBubble.js.map +1 -1
- package/lib/commonjs/components/ProductsList.js +2 -2
- package/lib/commonjs/components/ProductsList.js.map +1 -1
- package/lib/commonjs/components/utils.js +46 -1
- package/lib/commonjs/components/utils.js.map +1 -1
- package/lib/commonjs/contexts/AnalyticsClientContext.js +19 -0
- package/lib/commonjs/contexts/AnalyticsClientContext.js.map +1 -0
- package/lib/commonjs/contexts/SegmentClientContext.js +19 -0
- package/lib/commonjs/contexts/SegmentClientContext.js.map +1 -0
- package/lib/commonjs/hooks/useAnalytics.js +158 -0
- package/lib/commonjs/hooks/useAnalytics.js.map +1 -0
- package/lib/commonjs/index.web.js +29 -3
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/storage.js +5 -5
- package/lib/commonjs/storage.js.map +1 -1
- package/lib/module/api.js +2 -5
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/ChatHeader.js +15 -2
- package/lib/module/components/ChatHeader.js.map +1 -1
- package/lib/module/components/ChatWidget.js +50 -25
- package/lib/module/components/ChatWidget.js.map +1 -1
- package/lib/module/components/MessageBubble.js +34 -10
- package/lib/module/components/MessageBubble.js.map +1 -1
- package/lib/module/components/ProductsList.js +2 -2
- package/lib/module/components/ProductsList.js.map +1 -1
- package/lib/module/components/utils.js +42 -0
- package/lib/module/components/utils.js.map +1 -1
- package/lib/module/contexts/AnalyticsClientContext.js +10 -0
- package/lib/module/contexts/AnalyticsClientContext.js.map +1 -0
- package/lib/module/contexts/SegmentClientContext.js +10 -0
- package/lib/module/contexts/SegmentClientContext.js.map +1 -0
- package/lib/module/hooks/useAnalytics.js +146 -0
- package/lib/module/hooks/useAnalytics.js.map +1 -0
- package/lib/module/index.native.js +5 -5
- package/lib/module/index.web.js +30 -4
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/storage.js +6 -6
- package/lib/module/storage.js.map +1 -1
- package/lib/typescript/api.d.ts +1 -1
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/ChatWidget.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble.d.ts +2 -0
- package/lib/typescript/components/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/components/ProductsList.d.ts.map +1 -1
- package/lib/typescript/components/utils.d.ts +23 -0
- package/lib/typescript/components/utils.d.ts.map +1 -1
- package/lib/typescript/contexts/AnalyticsClientContext.d.ts +9 -0
- package/lib/typescript/contexts/AnalyticsClientContext.d.ts.map +1 -0
- package/lib/typescript/contexts/SegmentClientContext.d.ts +9 -0
- package/lib/typescript/contexts/SegmentClientContext.d.ts.map +1 -0
- package/lib/typescript/hooks/useAnalytics.d.ts +36 -0
- package/lib/typescript/hooks/useAnalytics.d.ts.map +1 -0
- package/lib/typescript/index.native.d.ts +5 -5
- package/lib/typescript/index.web.d.ts +1 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/storage.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +1 -1
- package/lib/typescript/types.d.ts.map +1 -1
- package/package.json +13 -3
- package/src/api.ts +22 -11
- package/src/components/ChatHeader.tsx +13 -2
- package/src/components/ChatWidget.tsx +53 -28
- package/src/components/MessageBubble.tsx +52 -10
- package/src/components/ProductsList.tsx +8 -6
- package/src/components/utils.ts +60 -1
- package/src/contexts/AnalyticsClientContext.tsx +20 -0
- package/src/contexts/SegmentClientContext.tsx +20 -0
- package/src/hooks/useAnalytics.tsx +176 -0
- package/src/index.native.tsx +5 -5
- package/src/index.web.tsx +36 -3
- package/src/storage.ts +16 -13
- package/src/types.ts +1 -1
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
Image,
|
|
8
8
|
} from "react-native";
|
|
9
9
|
import React from "react";
|
|
10
|
+
import { useUserAnalytics } from "../hooks/useAnalytics";
|
|
11
|
+
import { AnalyticsEventNames } from "./utils";
|
|
10
12
|
|
|
11
13
|
// Use expo-image on native if available, fallback to RN Image
|
|
12
14
|
let ImageComponent: typeof Image = Image;
|
|
@@ -38,6 +40,7 @@ const ChatHeader = ({
|
|
|
38
40
|
onClose?: () => void;
|
|
39
41
|
onClearChat?: () => void;
|
|
40
42
|
}) => {
|
|
43
|
+
const { trackEvent } = useUserAnalytics();
|
|
41
44
|
return (
|
|
42
45
|
<View style={styles.container}>
|
|
43
46
|
<View style={styles.logoContainer}>
|
|
@@ -49,12 +52,20 @@ const ChatHeader = ({
|
|
|
49
52
|
<Text style={styles.title}>AI Search</Text>
|
|
50
53
|
</View>
|
|
51
54
|
<View style={styles.buttonContainer}>
|
|
52
|
-
<TouchableOpacity onPress={() =>
|
|
55
|
+
<TouchableOpacity onPress={() => {
|
|
56
|
+
trackEvent?.(AnalyticsEventNames.CHAT_CLEARED, {});
|
|
57
|
+
console.log("Segment: Clear Chat tracked");
|
|
58
|
+
onClearChat?.();
|
|
59
|
+
}}>
|
|
53
60
|
<Text style={styles.clearChatText}>Clear Chat</Text>
|
|
54
61
|
</TouchableOpacity>
|
|
55
62
|
<TouchableOpacity
|
|
56
63
|
style={styles.closeButton}
|
|
57
|
-
onPress={() =>
|
|
64
|
+
onPress={() => {
|
|
65
|
+
trackEvent?.(AnalyticsEventNames.WIDGET_CLOSED, {});
|
|
66
|
+
console.log("Segment: Widget Close tracked");
|
|
67
|
+
onClose?.();
|
|
68
|
+
}}
|
|
58
69
|
>
|
|
59
70
|
<CloseIcon />
|
|
60
71
|
</TouchableOpacity>
|
|
@@ -31,7 +31,8 @@ import {
|
|
|
31
31
|
import ChatHeader from "./ChatHeader";
|
|
32
32
|
import SuggestionsRow from "./SuggestionsRow";
|
|
33
33
|
import ProductsList from "./ProductsList";
|
|
34
|
-
import { FeedbackAction, formatToTime } from "./utils";
|
|
34
|
+
import { FeedbackAction, formatToTime, getUserDetails } from "./utils";
|
|
35
|
+
import { useUserAnalytics } from "../hooks/useAnalytics";
|
|
35
36
|
import { Storage } from "../storage";
|
|
36
37
|
|
|
37
38
|
export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
@@ -63,10 +64,10 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
63
64
|
const [assistantResponse, setAssistantResponse] = useState<
|
|
64
65
|
ChatMessage | undefined
|
|
65
66
|
>(undefined);
|
|
66
|
-
const [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
const [productsByMsg, setProductsByMsg] = useState<Record<string, any>>({});
|
|
68
|
+
const [reloadLoadingIds, setReloadLoadingIds] = useState<Set<string>>(
|
|
69
|
+
new Set()
|
|
70
|
+
);
|
|
70
71
|
const [priceMode, setPriceMode] = useState<string | null>(
|
|
71
72
|
priceModeProp || null
|
|
72
73
|
);
|
|
@@ -76,7 +77,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
76
77
|
const scrollRef = useRef<ScrollView | null>(null);
|
|
77
78
|
const inputRef = useRef<TextInput | null>(null);
|
|
78
79
|
const theme = useMemo(() => mergeTheme(themeOverrides), [themeOverrides]);
|
|
79
|
-
|
|
80
|
+
const { _identify } = useUserAnalytics();
|
|
80
81
|
// Load user auth data from storage on mount
|
|
81
82
|
useEffect(() => {
|
|
82
83
|
const loadAuthData = async () => {
|
|
@@ -104,10 +105,8 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
104
105
|
}, [userIdProp, userTokenProp]);
|
|
105
106
|
|
|
106
107
|
const onViewAll = useCallback(() => {
|
|
107
|
-
const searchPayload = JSON.stringify(
|
|
108
|
-
|
|
109
|
-
);
|
|
110
|
-
const payload = assistantResponse?.agent_response?.payload;
|
|
108
|
+
const searchPayload = JSON.stringify(assistantResponse?.search_payload);
|
|
109
|
+
const payload = assistantResponse?.search_payload;
|
|
111
110
|
if (!payload) return;
|
|
112
111
|
|
|
113
112
|
const domain = apiUrl.split("v3");
|
|
@@ -231,9 +230,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
231
230
|
async (rawText: string) => {
|
|
232
231
|
const trimmed = rawText.trim();
|
|
233
232
|
if (!trimmed || loading) return;
|
|
234
|
-
|
|
235
|
-
setProducts(null);
|
|
236
|
-
}
|
|
233
|
+
|
|
237
234
|
const userMessage: ChatMessage = {
|
|
238
235
|
id: `user-${Date.now()}`,
|
|
239
236
|
role: "user",
|
|
@@ -289,12 +286,12 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
289
286
|
if (latestAssistant?.text) {
|
|
290
287
|
// If there is an agent_response, try to fetch products first
|
|
291
288
|
if (
|
|
292
|
-
latestAssistant?.
|
|
293
|
-
typeof latestAssistant.
|
|
294
|
-
Object.keys(latestAssistant.
|
|
289
|
+
latestAssistant?.search_payload &&
|
|
290
|
+
typeof latestAssistant.search_payload === "object" &&
|
|
291
|
+
Object.keys(latestAssistant.search_payload).length > 0
|
|
295
292
|
) {
|
|
296
293
|
const productsResult = await getProducts(
|
|
297
|
-
latestAssistant.
|
|
294
|
+
latestAssistant.search_payload
|
|
298
295
|
);
|
|
299
296
|
|
|
300
297
|
const hasDiamonds =
|
|
@@ -311,7 +308,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
311
308
|
setLoadingMessageId(null);
|
|
312
309
|
setTypingMessageId(null);
|
|
313
310
|
setTypingFullText("");
|
|
314
|
-
setProducts(null);
|
|
315
311
|
|
|
316
312
|
setMessages((prev) =>
|
|
317
313
|
prev.map((msg) =>
|
|
@@ -329,7 +325,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
329
325
|
inputRef.current?.focus();
|
|
330
326
|
return;
|
|
331
327
|
}
|
|
332
|
-
|
|
328
|
+
setProductsByMsg({
|
|
333
329
|
messageId: latestAssistant.id,
|
|
334
330
|
data: productsResult,
|
|
335
331
|
});
|
|
@@ -386,9 +382,19 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
386
382
|
inputRef.current?.focus();
|
|
387
383
|
}
|
|
388
384
|
},
|
|
389
|
-
[apiUrl, loading, messages, apiParams, hasAuth, priceMode,
|
|
385
|
+
[apiUrl, loading, messages, apiParams, hasAuth, priceMode, productsByMsg]
|
|
390
386
|
);
|
|
391
387
|
|
|
388
|
+
const identifySegmentUser = async () => {
|
|
389
|
+
const user = await getUserDetails();
|
|
390
|
+
const userId = user?.id;
|
|
391
|
+
const email = JSON.stringify(user?.email) ?? undefined;
|
|
392
|
+
if (userId) {
|
|
393
|
+
_identify(`${userId}`, {
|
|
394
|
+
email: email,
|
|
395
|
+
}).then();
|
|
396
|
+
}
|
|
397
|
+
};
|
|
392
398
|
const handleSend = useCallback(async () => {
|
|
393
399
|
const trimmed = input.trim();
|
|
394
400
|
if (!trimmed) return;
|
|
@@ -404,13 +410,21 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
404
410
|
);
|
|
405
411
|
|
|
406
412
|
const handleReloadResults = useCallback(async (msg: ChatMessage) => {
|
|
413
|
+
const id = msg.id;
|
|
407
414
|
try {
|
|
408
|
-
|
|
409
|
-
|
|
415
|
+
setReloadLoadingIds((prev) => new Set(prev).add(id));
|
|
416
|
+
const payload = msg?.search_payload;
|
|
417
|
+
if (!payload || Object.keys(payload).length === 0) return;
|
|
410
418
|
const productsResult = await getProducts(payload);
|
|
411
|
-
|
|
419
|
+
setProductsByMsg((prev) => ({ ...prev, [id]: productsResult }));
|
|
412
420
|
} catch (e) {
|
|
413
421
|
console.error("Reload results failed", e);
|
|
422
|
+
} finally {
|
|
423
|
+
setReloadLoadingIds((prev) => {
|
|
424
|
+
const next = new Set(prev);
|
|
425
|
+
next.delete(id);
|
|
426
|
+
return next;
|
|
427
|
+
});
|
|
414
428
|
}
|
|
415
429
|
}, []);
|
|
416
430
|
|
|
@@ -421,6 +435,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
421
435
|
(data: { priceMode: string }) => changePriceMode(data.priceMode)
|
|
422
436
|
);
|
|
423
437
|
changePriceMode(undefined);
|
|
438
|
+
identifySegmentUser();
|
|
424
439
|
|
|
425
440
|
return () => {
|
|
426
441
|
DeviceEventEmitter.removeAllListeners("clearChat");
|
|
@@ -475,7 +490,8 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
475
490
|
}
|
|
476
491
|
setMessages([]);
|
|
477
492
|
setAssistantResponse(undefined);
|
|
478
|
-
|
|
493
|
+
setProductsByMsg({});
|
|
494
|
+
setReloadLoadingIds(new Set());
|
|
479
495
|
setLoading(false);
|
|
480
496
|
setLoadingMessageId(null);
|
|
481
497
|
setTypingMessageId(null);
|
|
@@ -581,7 +597,11 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
581
597
|
)}
|
|
582
598
|
<ScrollView
|
|
583
599
|
ref={scrollRef}
|
|
584
|
-
style={
|
|
600
|
+
style={
|
|
601
|
+
modalHeight
|
|
602
|
+
? { height: modalHeight, backgroundColor: "#f5f5f5" }
|
|
603
|
+
: { backgroundColor: "#f5f5f5" }
|
|
604
|
+
}
|
|
585
605
|
contentContainerStyle={{
|
|
586
606
|
backgroundColor: theme?.listContentBackgroundColor || "#f5f5f5",
|
|
587
607
|
...styles.listContent,
|
|
@@ -611,12 +631,17 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
|
|
|
611
631
|
conversationId={conversationId}
|
|
612
632
|
handleFeedbackAction={handleFeedbackAction}
|
|
613
633
|
onReloadResults={handleReloadResults}
|
|
634
|
+
reloading={reloadLoadingIds.has(item.id)}
|
|
635
|
+
hasResults={Boolean(
|
|
636
|
+
productsByMsg[item.id]?.response?.body?.diamonds &&
|
|
637
|
+
productsByMsg[item.id].response.body.diamonds.length > 0
|
|
638
|
+
)}
|
|
614
639
|
/>
|
|
615
640
|
{item.role === "assistant" &&
|
|
616
|
-
|
|
617
|
-
|
|
641
|
+
productsByMsg &&
|
|
642
|
+
productsByMsg.messageId === item.id && (
|
|
618
643
|
<ProductsList
|
|
619
|
-
data={
|
|
644
|
+
data={productsByMsg?.data?.response?.body?.diamonds}
|
|
620
645
|
onViewAll={onViewAll}
|
|
621
646
|
onItemPress={onItemPress}
|
|
622
647
|
/>
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import React, { memo } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Image,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
Platform,
|
|
9
|
+
ActivityIndicator,
|
|
10
|
+
} from "react-native";
|
|
3
11
|
import type { ChatMessage, ChatTheme } from "../types";
|
|
4
12
|
import { FeedbackAction, formatToTime } from "./utils";
|
|
5
13
|
|
|
@@ -24,6 +32,8 @@ interface Props {
|
|
|
24
32
|
message_id: string
|
|
25
33
|
) => void;
|
|
26
34
|
onReloadResults?: (message: ChatMessage) => void;
|
|
35
|
+
reloading?: boolean;
|
|
36
|
+
hasResults?: boolean;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
const MessageBubbleComponent: React.FC<Props> = ({
|
|
@@ -32,6 +42,8 @@ const MessageBubbleComponent: React.FC<Props> = ({
|
|
|
32
42
|
conversationId,
|
|
33
43
|
handleFeedbackAction,
|
|
34
44
|
onReloadResults,
|
|
45
|
+
reloading,
|
|
46
|
+
hasResults,
|
|
35
47
|
}) => {
|
|
36
48
|
const isUser = message.role === "user";
|
|
37
49
|
const isValidMessageId =
|
|
@@ -75,18 +87,48 @@ const MessageBubbleComponent: React.FC<Props> = ({
|
|
|
75
87
|
</Text>
|
|
76
88
|
</View>
|
|
77
89
|
{(canFeedback ||
|
|
78
|
-
(message.
|
|
79
|
-
typeof message.
|
|
90
|
+
(message.search_payload &&
|
|
91
|
+
typeof message.search_payload === "object" &&
|
|
92
|
+
Object.keys(message.search_payload).length > 0)) && (
|
|
80
93
|
<View style={styles.rowContainer}>
|
|
81
94
|
<View style={styles.time}>
|
|
82
95
|
<Text>{formatToTime(message.createdAt)}</Text>
|
|
83
96
|
</View>
|
|
84
|
-
{message.
|
|
85
|
-
typeof message.
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
{message.search_payload &&
|
|
98
|
+
typeof message.search_payload === "object" &&
|
|
99
|
+
Object.keys(message.search_payload).length > 0 &&
|
|
100
|
+
!hasResults &&
|
|
101
|
+
(reloading ? (
|
|
102
|
+
<View
|
|
103
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 6 }}
|
|
104
|
+
>
|
|
105
|
+
<ActivityIndicator size="small" color="#1a73e8" />
|
|
106
|
+
<Text
|
|
107
|
+
style={{
|
|
108
|
+
fontSize: 12,
|
|
109
|
+
color: "#1a73e8",
|
|
110
|
+
textDecorationLine: "underline",
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
Loading…
|
|
114
|
+
</Text>
|
|
115
|
+
</View>
|
|
116
|
+
) : (
|
|
117
|
+
<TouchableOpacity
|
|
118
|
+
onPress={() => onReloadResults?.(message)}
|
|
119
|
+
disabled={reloading}
|
|
120
|
+
>
|
|
121
|
+
<Text
|
|
122
|
+
style={{
|
|
123
|
+
fontSize: 12,
|
|
124
|
+
color: "#1a73e8",
|
|
125
|
+
textDecorationLine: "underline",
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
Reload Results
|
|
129
|
+
</Text>
|
|
88
130
|
</TouchableOpacity>
|
|
89
|
-
)}
|
|
131
|
+
))}
|
|
90
132
|
<View style={styles.likeDislikeContainer}>
|
|
91
133
|
<TouchableOpacity
|
|
92
134
|
onPress={() =>
|
|
@@ -151,12 +193,12 @@ const styles = StyleSheet.create({
|
|
|
151
193
|
justifyContent: "space-between",
|
|
152
194
|
alignItems: "center",
|
|
153
195
|
gap: 12,
|
|
196
|
+
marginTop: 8,
|
|
197
|
+
marginBottom: 8,
|
|
154
198
|
},
|
|
155
199
|
likeDislikeContainer: {
|
|
156
200
|
flexDirection: "row",
|
|
157
201
|
gap: 12,
|
|
158
|
-
marginTop: 4,
|
|
159
|
-
marginBottom: 4,
|
|
160
202
|
},
|
|
161
203
|
alignRight: {
|
|
162
204
|
alignItems: "flex-end",
|
|
@@ -48,17 +48,19 @@ const ProductsListComponent: React.FC<ProductsListProps> = ({
|
|
|
48
48
|
}}
|
|
49
49
|
>
|
|
50
50
|
<View key={item.id} style={styles.card}>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
{item.image_thumb_url ? (
|
|
52
|
+
<ImageComponent
|
|
53
|
+
style={styles.image}
|
|
54
|
+
source={{ uri: item.image_thumb_url }}
|
|
55
|
+
/>
|
|
56
|
+
) : null}
|
|
55
57
|
|
|
56
58
|
<View style={styles.content}>
|
|
57
59
|
<Text numberOfLines={2} style={styles.title}>
|
|
58
60
|
{item.short_title}
|
|
59
61
|
</Text>
|
|
60
62
|
<Text style={styles.price}>${item.total_sales_price}</Text>
|
|
61
|
-
|
|
63
|
+
</View>
|
|
62
64
|
</View>
|
|
63
65
|
</TouchableOpacity>
|
|
64
66
|
))}
|
|
@@ -72,7 +74,7 @@ const ProductsListComponent: React.FC<ProductsListProps> = ({
|
|
|
72
74
|
>
|
|
73
75
|
<Text style={styles.buttonText}>View All {">>"}</Text>
|
|
74
76
|
</TouchableOpacity>
|
|
75
|
-
|
|
77
|
+
</View>
|
|
76
78
|
);
|
|
77
79
|
};
|
|
78
80
|
|
package/src/components/utils.ts
CHANGED
|
@@ -38,6 +38,65 @@ export const fetchConversationId = async (
|
|
|
38
38
|
);
|
|
39
39
|
priceMode = userData?.price_mode || "";
|
|
40
40
|
}
|
|
41
|
-
|
|
42
41
|
return conversations[priceMode]?.conversation_id || null;
|
|
43
42
|
};
|
|
43
|
+
|
|
44
|
+
export interface UserDetails {
|
|
45
|
+
id?: string | number;
|
|
46
|
+
email?: string;
|
|
47
|
+
first_name?: string;
|
|
48
|
+
last_name?: string;
|
|
49
|
+
company?: string;
|
|
50
|
+
company_id?: string | number;
|
|
51
|
+
country?: string;
|
|
52
|
+
price_mode?: string | number;
|
|
53
|
+
plan_details?: any;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const getUserDetails = async (): Promise<UserDetails | null> => {
|
|
57
|
+
try {
|
|
58
|
+
const stored = await Storage.getJSON<{ user?: string | UserDetails }>(
|
|
59
|
+
"persist:userInfo",
|
|
60
|
+
{}
|
|
61
|
+
);
|
|
62
|
+
if (!stored?.user) return null;
|
|
63
|
+
|
|
64
|
+
// If user is stored as a JSON string, parse it
|
|
65
|
+
if (typeof stored.user === "string") {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(stored.user) as UserDetails;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return stored.user as UserDetails;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export enum DeviceType {
|
|
80
|
+
WEB = "web",
|
|
81
|
+
IOS = "ios",
|
|
82
|
+
ANDROID = "android",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const getDeviceType = (platform?: string) => {
|
|
86
|
+
switch (platform) {
|
|
87
|
+
case DeviceType.WEB:
|
|
88
|
+
return 0;
|
|
89
|
+
case DeviceType.IOS:
|
|
90
|
+
return 1;
|
|
91
|
+
case DeviceType.ANDROID:
|
|
92
|
+
return 2;
|
|
93
|
+
default:
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export enum AnalyticsEventNames {
|
|
99
|
+
WIDGET_OPENED = "ai_chat_widget_opened",
|
|
100
|
+
WIDGET_CLOSED = "ai_chat_widget_closed",
|
|
101
|
+
CHAT_CLEARED = "ai_chat_cleared",
|
|
102
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import type { Analytics } from "@segment/analytics-next";
|
|
3
|
+
|
|
4
|
+
export const AnalyticsClientContext = createContext<Analytics | undefined>(
|
|
5
|
+
undefined
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export const useAnalyticsClient = () => useContext(AnalyticsClientContext);
|
|
9
|
+
|
|
10
|
+
export const AnalyticsClientProvider = ({
|
|
11
|
+
client,
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
client: Analytics;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}) => (
|
|
17
|
+
<AnalyticsClientContext.Provider value={client}>
|
|
18
|
+
{children}
|
|
19
|
+
</AnalyticsClientContext.Provider>
|
|
20
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import { SegmentClient } from "@segment/analytics-react-native";
|
|
3
|
+
|
|
4
|
+
export const SegmentClientContext = createContext<SegmentClient | undefined>(
|
|
5
|
+
undefined
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export const useSegmentClient = () => useContext(SegmentClientContext);
|
|
9
|
+
|
|
10
|
+
export const SegmentClientProvider = ({
|
|
11
|
+
client,
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
client: SegmentClient;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}) => (
|
|
17
|
+
<SegmentClientContext.Provider value={client}>
|
|
18
|
+
{children}
|
|
19
|
+
</SegmentClientContext.Provider>
|
|
20
|
+
);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
type JsonMap = Record<string, unknown>;
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import { DeviceType, getDeviceType, getUserDetails } from "../components/utils";
|
|
4
|
+
import { useAnalyticsClient } from "../contexts/AnalyticsClientContext";
|
|
5
|
+
|
|
6
|
+
const getSystemTypeEnum = (deviceType: any) => {
|
|
7
|
+
let type: string = "web";
|
|
8
|
+
if (deviceType) {
|
|
9
|
+
switch (deviceType) {
|
|
10
|
+
case "Linux":
|
|
11
|
+
type = "android";
|
|
12
|
+
break;
|
|
13
|
+
case "iPhone":
|
|
14
|
+
type = "ios";
|
|
15
|
+
break;
|
|
16
|
+
default:
|
|
17
|
+
type = "web";
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
type = String(Platform.OS);
|
|
22
|
+
}
|
|
23
|
+
return getDeviceType(type).toString();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const formatOSName = (rawOsName: string) => {
|
|
27
|
+
if (!rawOsName) return "";
|
|
28
|
+
|
|
29
|
+
const osName = rawOsName.toLowerCase();
|
|
30
|
+
|
|
31
|
+
const osMap = {
|
|
32
|
+
android: "Android",
|
|
33
|
+
ios: "iOS",
|
|
34
|
+
ipad: "iPadOS",
|
|
35
|
+
ipados: "iPadOS",
|
|
36
|
+
windows: "Windows",
|
|
37
|
+
macos: "macOS",
|
|
38
|
+
mac: "macOS",
|
|
39
|
+
linux: "Linux",
|
|
40
|
+
web: "Web",
|
|
41
|
+
tizen: "Tizen",
|
|
42
|
+
harmonyos: "HarmonyOS",
|
|
43
|
+
fireos: "Fire OS",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (const key of Object.keys(osMap) as Array<keyof typeof osMap>) {
|
|
47
|
+
if (osName.includes(key)) {
|
|
48
|
+
return osMap[key];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return rawOsName.charAt(0).toUpperCase() + rawOsName.slice(1);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const getUserAnalyticsPayload = async () => {
|
|
56
|
+
const user = await getUserDetails();
|
|
57
|
+
const deviceDetails =
|
|
58
|
+
Platform.OS === "web"
|
|
59
|
+
? {
|
|
60
|
+
system_type_enum: getSystemTypeEnum(null),
|
|
61
|
+
platform_action_enum: "0",
|
|
62
|
+
}
|
|
63
|
+
: {};
|
|
64
|
+
return {
|
|
65
|
+
user_company_id: user?.company_id?.toString(),
|
|
66
|
+
user_company_name: user?.company,
|
|
67
|
+
user_email: user?.email,
|
|
68
|
+
user_id: user?.id?.toString(),
|
|
69
|
+
user_name: user?.first_name
|
|
70
|
+
? `${user?.first_name} ${user?.last_name}`
|
|
71
|
+
: undefined,
|
|
72
|
+
user_price_mode: user?.price_mode?.toString(),
|
|
73
|
+
price_mode: user?.price_mode?.toString(),
|
|
74
|
+
has_plan_details: user?.plan_details ? true : false,
|
|
75
|
+
userAgent: Platform.OS === "web" ? navigator.userAgent : "",
|
|
76
|
+
...deviceDetails,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const isSegmentUserAgent = (userAgent: string) => {
|
|
81
|
+
if (!userAgent) return false;
|
|
82
|
+
|
|
83
|
+
// Regex to match Segment's specific Safari-like pattern
|
|
84
|
+
const segmentPattern =
|
|
85
|
+
/^Mozilla\/5\.0 \([^;]+; CPU [^)]+ like Mac OS X\) AppleWebKit\/600\.1\.4 \(KHTML, like Gecko\) Version\/\d\.0 Mobile\/10B329 Safari\/8536\.25$/;
|
|
86
|
+
|
|
87
|
+
return segmentPattern.test(userAgent);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function isBot(userAgent = "") {
|
|
91
|
+
if (!userAgent && Platform.OS === "web") return true;
|
|
92
|
+
|
|
93
|
+
const botPatterns = [
|
|
94
|
+
/bot/i, // "Googlebot", "Bingbot"
|
|
95
|
+
/crawl/i, // "crawler"
|
|
96
|
+
/spider/i, // "spider"
|
|
97
|
+
/HeadlessChrome/i, // Puppeteer/Headless
|
|
98
|
+
/PhantomJS/i, // Old automation
|
|
99
|
+
/slurp/i, // Yahoo Slurp bot
|
|
100
|
+
/curl/i, // CLI traffic
|
|
101
|
+
/wget/i, // CLI downloads
|
|
102
|
+
/httpclient/i, // Generic clients
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
return botPatterns.some((pattern) => pattern.test(userAgent));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const useUserAnalytics = () => {
|
|
109
|
+
// This will return undefined if not wrapped in provider, which is fine
|
|
110
|
+
const analyticsClient = useAnalyticsClient();
|
|
111
|
+
|
|
112
|
+
const _track = async (eventName: string, options: any) => {
|
|
113
|
+
const userAnalyticsPayload = await getUserAnalyticsPayload();
|
|
114
|
+
if (isSegmentUserAgent(userAnalyticsPayload?.userAgent)) {
|
|
115
|
+
console.warn(
|
|
116
|
+
"Skipping tracking for Segment user agent:",
|
|
117
|
+
userAnalyticsPayload.userAgent
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (isBot(userAnalyticsPayload?.userAgent)) {
|
|
122
|
+
console.warn(
|
|
123
|
+
"Skipping tracking for bot user agent:",
|
|
124
|
+
userAnalyticsPayload.userAgent
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const client = analyticsClient || (window as any).analytics;
|
|
129
|
+
if (!client) return;
|
|
130
|
+
|
|
131
|
+
const user = await getUserDetails();
|
|
132
|
+
if (!user?.id) {
|
|
133
|
+
console.error(
|
|
134
|
+
"Track Event tracked without user:",
|
|
135
|
+
eventName,
|
|
136
|
+
JSON.stringify(options)
|
|
137
|
+
);
|
|
138
|
+
console.error(
|
|
139
|
+
"Can't identify as user is not logged in",
|
|
140
|
+
JSON.stringify(user)
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
await client.identify(`${user.id}`, {
|
|
144
|
+
name: `${user.first_name} ${user.last_name}`,
|
|
145
|
+
email: user.email,
|
|
146
|
+
country: user.country,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
await client.track(eventName, {
|
|
150
|
+
...userAnalyticsPayload,
|
|
151
|
+
...options,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const _identify = async (userId?: string, userTraits?: JsonMap) => {
|
|
156
|
+
const client = analyticsClient || (window as any).analytics;
|
|
157
|
+
if (!client) return;
|
|
158
|
+
await client.identify(userId, userTraits);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const trackEvent = async (eventName: string, options: any) => {
|
|
162
|
+
const userAnalyticsPayload = await getUserAnalyticsPayload();
|
|
163
|
+
const client = analyticsClient || (window as any).analytics;
|
|
164
|
+
if (!client) return;
|
|
165
|
+
await client.track(eventName, {
|
|
166
|
+
...userAnalyticsPayload,
|
|
167
|
+
...options,
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
trackEvent,
|
|
173
|
+
_identify,
|
|
174
|
+
_track,
|
|
175
|
+
};
|
|
176
|
+
};
|
package/src/index.native.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// React Native entry point - re-export everything from main index
|
|
2
|
-
export { ChatWidget } from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export { normaliseMessages } from
|
|
6
|
-
export { initStorage, isStorageInitialized, Storage } from
|
|
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";
|