react-native-chatbot-ai 0.1.21 → 0.1.22

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 (141) hide show
  1. package/lib/module/components/Drawer/DeleteSessionPopup.js +19 -5
  2. package/lib/module/components/Drawer/DeleteSessionPopup.js.map +1 -1
  3. package/lib/module/components/Drawer/DrawerContent.js +17 -5
  4. package/lib/module/components/Drawer/DrawerContent.js.map +1 -1
  5. package/lib/module/components/Drawer/RenameSessionPopup.js +1 -1
  6. package/lib/module/components/Drawer/RenameSessionPopup.js.map +1 -1
  7. package/lib/module/components/Drawer/SearchInput.js +10 -3
  8. package/lib/module/components/Drawer/SearchInput.js.map +1 -1
  9. package/lib/module/components/Drawer/SessionItem.js +4 -3
  10. package/lib/module/components/Drawer/SessionItem.js.map +1 -1
  11. package/lib/module/components/Drawer/SessionOptionsBottomSheet.js +10 -3
  12. package/lib/module/components/Drawer/SessionOptionsBottomSheet.js.map +1 -1
  13. package/lib/module/components/Drawer/ShareSessionPopup.js +8 -4
  14. package/lib/module/components/Drawer/ShareSessionPopup.js.map +1 -1
  15. package/lib/module/components/chat/ChatEmpty.js +15 -4
  16. package/lib/module/components/chat/ChatEmpty.js.map +1 -1
  17. package/lib/module/components/chat/ChatHeader.js +10 -4
  18. package/lib/module/components/chat/ChatHeader.js.map +1 -1
  19. package/lib/module/components/chat/ChatMessageList.js +14 -1
  20. package/lib/module/components/chat/ChatMessageList.js.map +1 -1
  21. package/lib/module/components/chat/SuggestionItem.js +2 -1
  22. package/lib/module/components/chat/SuggestionItem.js.map +1 -1
  23. package/lib/module/components/chat/footer/index.js +77 -15
  24. package/lib/module/components/chat/footer/index.js.map +1 -1
  25. package/lib/module/components/chat/footer/item/UploadImageItem.js +21 -1
  26. package/lib/module/components/chat/footer/item/UploadImageItem.js.map +1 -1
  27. package/lib/module/components/chat/index.js +7 -6
  28. package/lib/module/components/chat/index.js.map +1 -1
  29. package/lib/module/components/chat/item/ChatAIAnswerMessageItem.js +7 -4
  30. package/lib/module/components/chat/item/ChatAIAnswerMessageItem.js.map +1 -1
  31. package/lib/module/components/chat/item/DeeplinkItem.js +30 -2
  32. package/lib/module/components/chat/item/DeeplinkItem.js.map +1 -1
  33. package/lib/module/components/chat/item/MessageActionsBar.js +8 -2
  34. package/lib/module/components/chat/item/MessageActionsBar.js.map +1 -1
  35. package/lib/module/components/product/CardHorizontal.js +44 -7
  36. package/lib/module/components/product/CardHorizontal.js.map +1 -1
  37. package/lib/module/constants/events.js +33 -0
  38. package/lib/module/constants/events.js.map +1 -1
  39. package/lib/module/context/ChatContext.js +6 -3
  40. package/lib/module/context/ChatContext.js.map +1 -1
  41. package/lib/module/hooks/message/useSendMessage.js +21 -3
  42. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  43. package/lib/module/hooks/message/useStreamMessage.js +10 -3
  44. package/lib/module/hooks/message/useStreamMessage.js.map +1 -1
  45. package/lib/module/hooks/messageActions/useAudioPlayer.js +30 -4
  46. package/lib/module/hooks/messageActions/useAudioPlayer.js.map +1 -1
  47. package/lib/module/hooks/messageActions/useFeedback.js +9 -1
  48. package/lib/module/hooks/messageActions/useFeedback.js.map +1 -1
  49. package/lib/module/hooks/messageActions/useShareMessage.js +9 -1
  50. package/lib/module/hooks/messageActions/useShareMessage.js.map +1 -1
  51. package/lib/module/hooks/session/useSearchSessions.js +5 -1
  52. package/lib/module/hooks/session/useSearchSessions.js.map +1 -1
  53. package/lib/module/hooks/upload/useImageUpload.js +2 -1
  54. package/lib/module/hooks/upload/useImageUpload.js.map +1 -1
  55. package/lib/module/store/session.js +6 -3
  56. package/lib/module/store/session.js.map +1 -1
  57. package/lib/module/translation/index.js +21 -25
  58. package/lib/module/translation/index.js.map +1 -1
  59. package/lib/module/translation/resources/{vi.js → i18n.js} +5 -2
  60. package/lib/module/translation/resources/i18n.js.map +1 -0
  61. package/lib/module/types/chat.js +12 -1
  62. package/lib/module/types/chat.js.map +1 -1
  63. package/lib/typescript/src/components/Drawer/DeleteSessionPopup.d.ts +1 -1
  64. package/lib/typescript/src/components/Drawer/DeleteSessionPopup.d.ts.map +1 -1
  65. package/lib/typescript/src/components/Drawer/DrawerContent.d.ts.map +1 -1
  66. package/lib/typescript/src/components/Drawer/SearchInput.d.ts.map +1 -1
  67. package/lib/typescript/src/components/Drawer/SessionOptionsBottomSheet.d.ts +1 -1
  68. package/lib/typescript/src/components/Drawer/SessionOptionsBottomSheet.d.ts.map +1 -1
  69. package/lib/typescript/src/components/Drawer/ShareSessionPopup.d.ts.map +1 -1
  70. package/lib/typescript/src/components/chat/ChatEmpty.d.ts.map +1 -1
  71. package/lib/typescript/src/components/chat/ChatHeader.d.ts.map +1 -1
  72. package/lib/typescript/src/components/chat/ChatMessageList.d.ts.map +1 -1
  73. package/lib/typescript/src/components/chat/SuggestionItem.d.ts +3 -2
  74. package/lib/typescript/src/components/chat/SuggestionItem.d.ts.map +1 -1
  75. package/lib/typescript/src/components/chat/footer/index.d.ts.map +1 -1
  76. package/lib/typescript/src/components/chat/footer/item/UploadImageItem.d.ts.map +1 -1
  77. package/lib/typescript/src/components/chat/index.d.ts.map +1 -1
  78. package/lib/typescript/src/components/chat/item/ChatAIAnswerMessageItem.d.ts.map +1 -1
  79. package/lib/typescript/src/components/chat/item/DeeplinkItem.d.ts +2 -1
  80. package/lib/typescript/src/components/chat/item/DeeplinkItem.d.ts.map +1 -1
  81. package/lib/typescript/src/components/chat/item/MessageActionsBar.d.ts.map +1 -1
  82. package/lib/typescript/src/components/product/CardHorizontal.d.ts +3 -2
  83. package/lib/typescript/src/components/product/CardHorizontal.d.ts.map +1 -1
  84. package/lib/typescript/src/constants/events.d.ts +33 -0
  85. package/lib/typescript/src/constants/events.d.ts.map +1 -1
  86. package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
  87. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +2 -2
  88. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  89. package/lib/typescript/src/hooks/message/useStreamMessage.d.ts.map +1 -1
  90. package/lib/typescript/src/hooks/messageActions/useAudioPlayer.d.ts.map +1 -1
  91. package/lib/typescript/src/hooks/messageActions/useFeedback.d.ts.map +1 -1
  92. package/lib/typescript/src/hooks/messageActions/useShareMessage.d.ts.map +1 -1
  93. package/lib/typescript/src/hooks/session/useSearchSessions.d.ts +1 -1
  94. package/lib/typescript/src/hooks/session/useSearchSessions.d.ts.map +1 -1
  95. package/lib/typescript/src/hooks/upload/useImageUpload.d.ts +1 -1
  96. package/lib/typescript/src/hooks/upload/useImageUpload.d.ts.map +1 -1
  97. package/lib/typescript/src/store/session.d.ts.map +1 -1
  98. package/lib/typescript/src/translation/index.d.ts +3 -4
  99. package/lib/typescript/src/translation/index.d.ts.map +1 -1
  100. package/lib/typescript/src/translation/resources/i18n.d.ts +5 -0
  101. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -0
  102. package/lib/typescript/src/types/chat.d.ts +14 -1
  103. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  104. package/package.json +2 -5
  105. package/src/components/Drawer/DeleteSessionPopup.tsx +22 -5
  106. package/src/components/Drawer/DrawerContent.tsx +20 -5
  107. package/src/components/Drawer/RenameSessionPopup.tsx +1 -1
  108. package/src/components/Drawer/SearchInput.tsx +11 -2
  109. package/src/components/Drawer/SessionItem.tsx +3 -3
  110. package/src/components/Drawer/SessionOptionsBottomSheet.tsx +11 -3
  111. package/src/components/Drawer/ShareSessionPopup.tsx +7 -4
  112. package/src/components/chat/ChatEmpty.tsx +15 -5
  113. package/src/components/chat/ChatHeader.tsx +9 -4
  114. package/src/components/chat/ChatMessageList.tsx +16 -1
  115. package/src/components/chat/SuggestionItem.tsx +4 -3
  116. package/src/components/chat/footer/index.tsx +95 -14
  117. package/src/components/chat/footer/item/UploadImageItem.tsx +21 -1
  118. package/src/components/chat/index.tsx +8 -11
  119. package/src/components/chat/item/ChatAIAnswerMessageItem.tsx +17 -6
  120. package/src/components/chat/item/DeeplinkItem.tsx +30 -2
  121. package/src/components/chat/item/MessageActionsBar.tsx +7 -1
  122. package/src/components/chat/item/actions/ActionButton.tsx +0 -1
  123. package/src/components/product/CardHorizontal.tsx +45 -10
  124. package/src/constants/events.ts +34 -0
  125. package/src/context/ChatContext.tsx +3 -0
  126. package/src/hooks/message/useSendMessage.ts +47 -4
  127. package/src/hooks/message/useStreamMessage.ts +15 -4
  128. package/src/hooks/messageActions/useAudioPlayer.ts +31 -2
  129. package/src/hooks/messageActions/useCopyToClipboard.ts +0 -1
  130. package/src/hooks/messageActions/useFeedback.ts +10 -1
  131. package/src/hooks/messageActions/useShareMessage.ts +9 -1
  132. package/src/hooks/session/useSearchSessions.ts +6 -1
  133. package/src/hooks/upload/useImageUpload.ts +6 -2
  134. package/src/store/session.ts +4 -2
  135. package/src/translation/index.ts +27 -19
  136. package/src/translation/resources/{vi.ts → i18n.ts} +5 -1
  137. package/src/types/chat.ts +19 -1
  138. package/src/utils/textCleaner.ts +0 -1
  139. package/lib/module/translation/resources/vi.js.map +0 -1
  140. package/lib/typescript/src/translation/resources/vi.d.ts +0 -31
  141. package/lib/typescript/src/translation/resources/vi.d.ts.map +0 -1
@@ -5,7 +5,9 @@ import ChatEmpty from './ChatEmpty';
5
5
  import ChatItem from './item';
6
6
  import { KSpacingValue, KColors, KImage, KContainer } from '@droppii/libs';
7
7
  import type { IMessageItem } from '../../types';
8
- import { events } from '../../constants/events';
8
+ import { events, GAEvents } from '../../constants/events';
9
+ import { useChatContext } from '../../context/ChatContext';
10
+ import useSessionStore from '../../store/session';
9
11
 
10
12
  const FlatListComponent = FlatList as unknown as ComponentType<any>;
11
13
 
@@ -14,6 +16,9 @@ interface IChatMessageListProps {
14
16
  }
15
17
 
16
18
  const ChatMessageList = ({ messageList = [] }: IChatMessageListProps) => {
19
+ const logGA = useChatContext().logGA;
20
+ const sessionId = useSessionStore((state) => state.sessionId);
21
+
17
22
  const flatListRef = useRef<any>(null);
18
23
  const scrollOffsetRef = useRef(0);
19
24
  const [showScrollToBottom, setShowScrollToBottom] = useState(false);
@@ -53,6 +58,16 @@ const ChatMessageList = ({ messageList = [] }: IChatMessageListProps) => {
53
58
  };
54
59
  }, []);
55
60
 
61
+ useEffect(() => {
62
+ if (!sessionId) {
63
+ logGA(GAEvents.newChatPageView);
64
+ } else {
65
+ logGA(GAEvents.chatDetailPageView, {
66
+ conversation_id: sessionId,
67
+ });
68
+ }
69
+ }, [sessionId, logGA]);
70
+
56
71
  return (
57
72
  <View style={{ flex: 1 }}>
58
73
  <FlatListComponent
@@ -13,10 +13,11 @@ import { StyleSheet } from 'react-native';
13
13
 
14
14
  interface SuggestionItemProps {
15
15
  item: ISuggestionItem;
16
- onPressItem: (item: ISuggestionItem) => void;
16
+ index: number;
17
+ onPressItem: (item: ISuggestionItem, index: number) => void;
17
18
  }
18
19
 
19
- const SuggestionItem = ({ item, onPressItem }: SuggestionItemProps) => {
20
+ const SuggestionItem = ({ item, index, onPressItem }: SuggestionItemProps) => {
20
21
  const icon = useMemo(() => {
21
22
  switch (item.category) {
22
23
  case SuggestionCategory.product:
@@ -81,7 +82,7 @@ const SuggestionItem = ({ item, onPressItem }: SuggestionItemProps) => {
81
82
  return (
82
83
  <KContainer.Touchable
83
84
  style={styles.container}
84
- onPress={() => onPressItem(item)}
85
+ onPress={() => onPressItem(item, index)}
85
86
  >
86
87
  {icon}
87
88
  <KLabel.Text flex typo="TextNmNormal">
@@ -44,9 +44,13 @@ import {
44
44
  IMessageItem,
45
45
  ISuggestionItem,
46
46
  MessageType,
47
+ SendActionLogType,
47
48
  } from '../../../types';
48
49
  import UIUtils from '../../../utils/ui';
49
- import trans from '../../../translation';
50
+ import { trans } from '../../../translation';
51
+ import { useChatContext } from '../../../context/ChatContext';
52
+ import { GAEvents } from '../../../constants/events';
53
+ import useSessionStore from '../../../store/session';
50
54
 
51
55
  const { Popover } = renderers;
52
56
  export interface ImageUpload extends Image {
@@ -66,13 +70,17 @@ export type UploadItem = ImageUpload | FileUpload;
66
70
  const MAX_FILE_UPLOAD = 3;
67
71
  const MAX_FILE_SIZE = 30 * 1024 * 1024;
68
72
  const MAX_IMAGE_SIZE = 7 * 1024 * 1024;
73
+ const SCROLL_IDLE_TIME = 60000;
69
74
 
70
75
  interface IChatFooterProps {
71
76
  lastMessage?: IMessageItem;
72
77
  }
73
78
 
74
79
  const ChatFooter = ({ lastMessage }: IChatFooterProps) => {
80
+ const idleTimerRef = useRef<NodeJS.Timeout | null>(null);
81
+ const hasScrolledRef = useRef(false);
75
82
  const menuRef = useRef(null);
83
+ const logGA = useChatContext().logGA;
76
84
  const { onSendMessage, stopStream } = useSendMessage();
77
85
  const [message, setMessage] = useState('');
78
86
  const isStreaming = useStreamMessageStore((state) => state.isStreaming);
@@ -139,7 +147,12 @@ const ChatFooter = ({ lastMessage }: IChatFooterProps) => {
139
147
  return [...prev, ...uniqueImages];
140
148
  });
141
149
  }
142
- }, [fileUpload]);
150
+
151
+ logGA(GAEvents.chatDetailMultimodalTap, {
152
+ conversation_id: useSessionStore.getState().sessionId,
153
+ button: 'image',
154
+ });
155
+ }, [fileUpload, logGA]);
143
156
 
144
157
  const onPressCameraPicker = useCallback(async () => {
145
158
  Keyboard.dismiss();
@@ -168,7 +181,11 @@ const ChatFooter = ({ lastMessage }: IChatFooterProps) => {
168
181
  return [...prev, { ...newImage, uploadType: 'image' }];
169
182
  });
170
183
  }
171
- }, [fileUpload]);
184
+ logGA(GAEvents.chatDetailMultimodalTap, {
185
+ conversation_id: useSessionStore.getState().sessionId,
186
+ button: 'image',
187
+ });
188
+ }, [fileUpload, logGA]);
172
189
 
173
190
  const menuItems = useMemo(() => {
174
191
  return [
@@ -188,6 +205,9 @@ const ChatFooter = ({ lastMessage }: IChatFooterProps) => {
188
205
  const onPressSend = useCallback(() => {
189
206
  if (isStreaming) {
190
207
  stopStream();
208
+ logGA(GAEvents.chatDetailStopChatBtnTap, {
209
+ conversation_id: useSessionStore.getState().sessionId,
210
+ });
191
211
  return;
192
212
  }
193
213
  const attachments: IAttachment[] = fileUpload.map((i) => ({
@@ -201,10 +221,10 @@ const ChatFooter = ({ lastMessage }: IChatFooterProps) => {
201
221
  : i.type) as IAttachment['mime_type'],
202
222
  }));
203
223
 
204
- onSendMessage(message?.trim(), attachments);
224
+ onSendMessage(message?.trim(), attachments, SendActionLogType.sendBtn);
205
225
  setMessage('');
206
226
  setFileUpload([]);
207
- }, [message, isStreaming, onSendMessage, stopStream, fileUpload]);
227
+ }, [message, isStreaming, onSendMessage, stopStream, fileUpload, logGA]);
208
228
 
209
229
  const handleUploadSuccess = useCallback((item: UploadItem) => {
210
230
  if (item.uploadType === 'image') {
@@ -305,18 +325,37 @@ const ChatFooter = ({ lastMessage }: IChatFooterProps) => {
305
325
  return [...prev, { ...newDocument, uploadType: 'file' }];
306
326
  });
307
327
  }
308
- }, [fileUpload]);
328
+ logGA(GAEvents.chatDetailMultimodalTap, {
329
+ conversation_id: useSessionStore.getState().sessionId,
330
+ button: 'file',
331
+ });
332
+ }, [fileUpload, logGA]);
309
333
 
310
- const onPressItemSuggestion = debounce((item: ISuggestionItem) => {
311
- onSendMessage(item.content);
312
- }, 200);
334
+ const onPressItemSuggestion = debounce(
335
+ (item: ISuggestionItem, index: number) => {
336
+ onSendMessage(
337
+ item.content,
338
+ undefined,
339
+ SendActionLogType.promptSuggestion
340
+ );
341
+ logGA(GAEvents.chatDetailInConvPrmSuggTap, {
342
+ conversation_id: useSessionStore.getState().sessionId,
343
+ group_id: lastMessage?.group_suggestion_id,
344
+ suggestion_id: item.suggestion_id,
345
+ suggestion_content: item.content,
346
+ suggestion_cate: item.category,
347
+ order: index,
348
+ });
349
+ },
350
+ 200
351
+ );
313
352
 
314
353
  const renderSuggestionItem = useCallback(
315
- (item: ISuggestionItem) => {
354
+ (item: ISuggestionItem, index: number) => {
316
355
  return (
317
356
  <KContainer.Touchable
318
357
  style={styles.suggestionItem}
319
- onPress={() => onPressItemSuggestion(item)}
358
+ onPress={() => onPressItemSuggestion(item, index)}
320
359
  >
321
360
  <KLabel.Text typo="TextSmNormal">{item?.content || ''}</KLabel.Text>
322
361
  </KContainer.Touchable>
@@ -325,17 +364,58 @@ const ChatFooter = ({ lastMessage }: IChatFooterProps) => {
325
364
  [onPressItemSuggestion]
326
365
  );
327
366
 
367
+ const clearIdleTimer = () => {
368
+ if (idleTimerRef.current) {
369
+ clearTimeout(idleTimerRef.current);
370
+ idleTimerRef.current = null;
371
+ }
372
+ };
373
+
374
+ const onUserIdleAfterScroll = useCallback(() => {
375
+ logGA(GAEvents.chatDetailInConvPrmSuggScroll, {
376
+ group_id: lastMessage?.group_suggestion_id,
377
+ message_id: lastMessage?.id,
378
+ });
379
+ }, [lastMessage, logGA]);
380
+
381
+ const resetIdleTimer = useCallback(() => {
382
+ clearIdleTimer();
383
+
384
+ hasScrolledRef.current = true;
385
+
386
+ idleTimerRef.current = setTimeout(() => {
387
+ if (hasScrolledRef.current) {
388
+ onUserIdleAfterScroll();
389
+ }
390
+ }, SCROLL_IDLE_TIME);
391
+ }, [onUserIdleAfterScroll]);
392
+
393
+ const handleScroll = () => {
394
+ resetIdleTimer();
395
+ };
396
+
397
+ const handleMomentumEnd = () => {
398
+ resetIdleTimer();
399
+ };
400
+
328
401
  return (
329
402
  <KContainer.View style={styles.container}>
330
403
  <KContainer.VisibleView visible={isShowSuggestions}>
331
404
  <FlatListComponent
332
405
  data={lastMessage?.suggestions || []}
333
- renderItem={({ item }: { item: ISuggestionItem }) =>
334
- renderSuggestionItem(item)
335
- }
406
+ renderItem={({
407
+ item,
408
+ index,
409
+ }: {
410
+ item: ISuggestionItem;
411
+ index: number;
412
+ }) => renderSuggestionItem(item, index)}
336
413
  horizontal
337
414
  keyExtractor={(item: ISuggestionItem) => item.suggestion_id}
338
415
  contentContainerStyle={styles.suggessionContainer}
416
+ onScroll={handleScroll}
417
+ onMomentumScrollEnd={handleMomentumEnd}
418
+ scrollEventThrottle={200}
339
419
  />
340
420
  </KContainer.VisibleView>
341
421
  <KContainer.VisibleView visible={fileUpload.length > 0}>
@@ -429,6 +509,7 @@ const styles = StyleSheet.create({
429
509
  borderBottomWidth: 0,
430
510
  borderTopLeftRadius: KSpacingValue['1.25rem'],
431
511
  borderTopRightRadius: KSpacingValue['1.25rem'],
512
+ backgroundColor: KColors.white,
432
513
  },
433
514
  actions: {
434
515
  flexDirection: 'row',
@@ -9,6 +9,9 @@ import { ImageUpload } from '..';
9
9
  import { StyleSheet } from 'react-native';
10
10
  import { useImageUpload } from '../../../../hooks/upload/useImageUpload';
11
11
  import UIUtils from '../../../../utils/ui';
12
+ import { useChatContext } from '../../../../context/ChatContext';
13
+ import { GAEvents } from '../../../../constants/events';
14
+ import useSessionStore from '../../../../store/session';
12
15
 
13
16
  interface Props {
14
17
  item: ImageUpload;
@@ -17,9 +20,10 @@ interface Props {
17
20
  }
18
21
 
19
22
  const UploadImageItem = ({ item, onSuccess, onRemove }: Props) => {
23
+ const logGA = useChatContext().logGA;
20
24
  const { isSuccess, isUploading, progress } = useImageUpload({
21
25
  file: item,
22
- onSuccess: (data: any) => {
26
+ onSuccess: (data: any, duration?: number) => {
23
27
  const mItem = {
24
28
  ...item,
25
29
  remoteUrl: data?.[0]?.publicUrl,
@@ -27,6 +31,14 @@ const UploadImageItem = ({ item, onSuccess, onRemove }: Props) => {
27
31
  };
28
32
  if (mItem.remoteUrl) {
29
33
  onSuccess?.(mItem);
34
+ logGA(GAEvents.chatDetailMultimodalUpload, {
35
+ conversation_id: useSessionStore.getState().sessionId,
36
+ file_type: 'image',
37
+ duration: duration || 0,
38
+ size_mb: item.size / 1024 / 1024,
39
+ status: 'success',
40
+ format: item.mime,
41
+ });
30
42
  }
31
43
  },
32
44
  onError: () => {
@@ -35,6 +47,14 @@ const UploadImageItem = ({ item, onSuccess, onRemove }: Props) => {
35
47
  theme: 'danger',
36
48
  });
37
49
  onRemove?.(item);
50
+ logGA(GAEvents.chatDetailMultimodalUpload, {
51
+ conversation_id: useSessionStore.getState().sessionId,
52
+ file_type: 'image',
53
+ duration: 0,
54
+ size_mb: item.size / 1024 / 1024,
55
+ status: 'failed',
56
+ format: item.mime,
57
+ });
38
58
  },
39
59
  });
40
60
 
@@ -2,12 +2,8 @@ import { KContainer } from '@droppii/libs';
2
2
  import ChatHeader from './ChatHeader';
3
3
  import ChatMessageList from './ChatMessageList';
4
4
  import ChatFooter from './footer';
5
- import { KeyboardAvoidingView, Platform, StyleSheet } from 'react-native';
6
- import type { ComponentType } from 'react';
7
-
8
- // Type assertion to fix TypeScript issue with React Native components
9
- const KeyboardAvoidingViewComponent =
10
- KeyboardAvoidingView as unknown as ComponentType<any>;
5
+ import { StyleSheet } from 'react-native';
6
+ import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
11
7
  import { useMessage } from '../../hooks/message/useMessage';
12
8
 
13
9
  const ChatBotAI = () => {
@@ -16,15 +12,16 @@ const ChatBotAI = () => {
16
12
  const lastMessage = messageState?.messages?.[0];
17
13
 
18
14
  return (
19
- <KContainer.Page>
20
- <KeyboardAvoidingViewComponent
15
+ <KContainer.Page flex>
16
+ <KeyboardAvoidingView
21
17
  style={styles.container}
22
- behavior={Platform.OS === 'ios' ? 'padding' : undefined}
18
+ behavior={'padding'}
19
+ keyboardVerticalOffset={0}
23
20
  >
24
21
  <ChatHeader />
25
22
  <ChatMessageList messageList={messageState?.messages || []} />
26
23
  <ChatFooter lastMessage={lastMessage} />
27
- </KeyboardAvoidingViewComponent>
24
+ </KeyboardAvoidingView>
28
25
  </KContainer.Page>
29
26
  );
30
27
  };
@@ -32,5 +29,5 @@ const ChatBotAI = () => {
32
29
  export default ChatBotAI;
33
30
 
34
31
  const styles = StyleSheet.create({
35
- container: { flex: 1 },
32
+ container: { flexGrow: 1 },
36
33
  });
@@ -21,7 +21,7 @@ import ProductHorizontalCard from '../../product/CardHorizontal';
21
21
  import ChatTable from './ChatTable';
22
22
  import DeeplinkItem from './DeeplinkItem';
23
23
  import { useChatContext } from '../../../context/ChatContext';
24
- import trans from '../../../translation';
24
+ import { trans } from '../../../translation';
25
25
  import MessageActionsBar from './MessageActionsBar';
26
26
  import useProductsStore from '../../../store/products';
27
27
 
@@ -169,20 +169,27 @@ export function useStreamingMarkdownBlocks(text: string) {
169
169
 
170
170
  class CustomRenderer extends Renderer implements RendererInterface {
171
171
  constructor(
172
- openImageViewer?: (images: { url: string }[], index: number) => void
172
+ openImageViewer?: (images: { url: string }[], index: number) => void,
173
+ messageId?: string
173
174
  ) {
174
175
  super();
175
176
  this.openImageViewer = openImageViewer;
177
+ this.messageId = messageId;
176
178
  }
177
179
 
178
180
  openImageViewer?: (images: { url: string }[], index: number) => void;
181
+ messageId?: string;
179
182
 
180
183
  html(text: string): ReactNode {
181
184
  const match = /^<!--\s*@component:(\w+)\(id:([\w-]+)\)\s*-->/.exec(text);
182
185
  switch (match?.[1]) {
183
186
  case 'product':
184
187
  return (
185
- <ProductHorizontalCard productId={match?.[2]} key={this.getKey()} />
188
+ <ProductHorizontalCard
189
+ productId={match?.[2]}
190
+ key={this.getKey()}
191
+ messageId={this.messageId}
192
+ />
186
193
  );
187
194
  default:
188
195
  return (
@@ -305,7 +312,11 @@ class CustomRenderer extends Renderer implements RendererInterface {
305
312
  link(children: string | ReactNode[], href: string, _styles?: any): ReactNode {
306
313
  if (href?.startsWith('/@component:deeplink')) {
307
314
  return (
308
- <DeeplinkItem key={this.getKey()} href={href}>
315
+ <DeeplinkItem
316
+ key={this.getKey()}
317
+ href={href}
318
+ messageId={this.messageId}
319
+ >
309
320
  {children}
310
321
  </DeeplinkItem>
311
322
  );
@@ -402,8 +413,8 @@ const ChatAIAnswerMessageItem = ({
402
413
  const products = useProductsStore((state) => state.products);
403
414
 
404
415
  const renderer = useMemo(
405
- () => new CustomRenderer(openImageViewer),
406
- [openImageViewer]
416
+ () => new CustomRenderer(openImageViewer, item.id),
417
+ [openImageViewer, item.id]
407
418
  );
408
419
  const tokenizer = useMemo(() => new CustomTokenizer(), []);
409
420
 
@@ -1,10 +1,13 @@
1
1
  import { KContainer, KLabel, KColors } from '@droppii/libs';
2
2
  import { useCallback, useMemo } from 'react';
3
3
  import { useChatContext } from '../../../context/ChatContext';
4
+ import { GAEvents } from '../../../constants/events';
5
+ import useSessionStore from '../../../store/session';
4
6
 
5
7
  interface DeeplinkItemProps {
6
8
  children?: React.ReactNode;
7
9
  href?: string;
10
+ messageId?: string;
8
11
  }
9
12
 
10
13
  export function parseDeepLinkHref(href: string): Record<string, string> | null {
@@ -26,8 +29,9 @@ export function parseDeepLinkHref(href: string): Record<string, string> | null {
26
29
  return result;
27
30
  }
28
31
 
29
- const DeeplinkItem = ({ children, href }: DeeplinkItemProps) => {
32
+ const DeeplinkItem = ({ children, href, messageId }: DeeplinkItemProps) => {
30
33
  const pushLinkTo = useChatContext().pushLinkTo;
34
+ const logGA = useChatContext().logGA;
31
35
  const csTeamId = useChatContext().csTeamId;
32
36
 
33
37
  const linkData = useMemo(() => {
@@ -52,11 +56,35 @@ const DeeplinkItem = ({ children, href }: DeeplinkItemProps) => {
52
56
 
53
57
  const onPressDeeplink = useCallback(() => {
54
58
  if (linkData) {
59
+ const data = parseDeepLinkHref(href || '');
55
60
  pushLinkTo?.(linkData, {
56
61
  source_from: 'chatbot',
57
62
  });
63
+ if (data?.type === 'gift') {
64
+ logGA(GAEvents.chatDetailGiftDetailBtnTap, {
65
+ conversation_id: useSessionStore.getState().sessionId,
66
+ message_id: messageId,
67
+ gift_id: data?.id,
68
+ });
69
+ return;
70
+ }
71
+ if (data?.type === 'faq') {
72
+ logGA(GAEvents.chatDetailFAQTap, {
73
+ conversation_id: useSessionStore.getState().sessionId,
74
+ message_id: messageId,
75
+ deeplink_id: data?.id,
76
+ });
77
+ return;
78
+ }
79
+ if (data?.type === 'cx-support') {
80
+ logGA(GAEvents.chatDetailLivechatBtnTap, {
81
+ conversation_id: useSessionStore.getState().sessionId,
82
+ message_id: messageId,
83
+ });
84
+ return;
85
+ }
58
86
  }
59
- }, [linkData, pushLinkTo]);
87
+ }, [linkData, pushLinkTo, logGA, href, messageId]);
60
88
 
61
89
  return (
62
90
  <KContainer.Touchable onPress={onPressDeeplink}>
@@ -35,6 +35,8 @@ import ActionButton from './actions/ActionButton';
35
35
  const { Popover } = renderers;
36
36
 
37
37
  import type { IMessageItem } from '../../../types/dto';
38
+ import { useChatContext } from '../../../context/ChatContext';
39
+ import { GAEvents } from '../../../constants/events';
38
40
 
39
41
  interface MessageActionsBarProps {
40
42
  messageId: string;
@@ -54,6 +56,7 @@ const MessageActionsBar = ({
54
56
  // Refs
55
57
  const likeMenuRef = useRef(null);
56
58
  const dislikeMenuRef = useRef(null);
59
+ const logGA = useChatContext().logGA;
57
60
 
58
61
  // Hooks
59
62
  const { feedback, isLikeActive, isDislikeActive, selectReason } = useFeedback(
@@ -94,8 +97,11 @@ const MessageActionsBar = ({
94
97
 
95
98
  // Handlers
96
99
  const handleCopy = useCallback(() => {
100
+ logGA(GAEvents.chatDetailCopyBtnTap, {
101
+ message_id: messageId,
102
+ });
97
103
  copyToClipboard(messageContent);
98
- }, [messageContent, copyToClipboard]);
104
+ }, [messageContent, copyToClipboard, logGA, messageId]);
99
105
 
100
106
  // Audio button states
101
107
  const getAudioButtonProps = useCallback(() => {
@@ -63,4 +63,3 @@ const styles = StyleSheet.create({
63
63
  backgroundColor: KColors.hexToRgba(KColors.primary.normal, 0.1),
64
64
  },
65
65
  });
66
-
@@ -11,25 +11,29 @@ import {
11
11
  KPromotionTag,
12
12
  } from '@droppii/libs';
13
13
  import { PTManager } from '../../utils/prototype';
14
- import { useMemo } from 'react';
14
+ import { useCallback, useMemo } from 'react';
15
15
  import { StyleSheet } from 'react-native';
16
16
  import useProductsStore from '../../store/products';
17
17
  import { PRODUCT_STATUSES } from '../../types';
18
18
  import { useChatContext } from '../../context/ChatContext';
19
19
  import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
20
+ import { GAEvents } from '../../constants/events';
21
+ import useSessionStore from '../../store/session';
20
22
 
21
23
  interface IProductHorizontalCardProps {
22
24
  productId?: string;
25
+ messageId?: string;
23
26
  }
24
27
 
25
- const ProducHorizontalCard = (props: IProductHorizontalCardProps) => {
26
- const { productId } = props;
28
+ const ProductHorizontalCard = (props: IProductHorizontalCardProps) => {
29
+ const { productId, messageId } = props;
27
30
  const item = useProductsStore((state) =>
28
31
  state.products.find((p) => p.id === productId)
29
32
  );
30
33
  const onAddToCart = useChatContext()?.onAddToCart;
31
34
  const onBuyNow = useChatContext()?.onBuyNow;
32
35
  const onNavigateToProduct = useChatContext()?.onNavigateToProduct;
36
+ const logGA = useChatContext()?.logGA;
33
37
 
34
38
  const {
35
39
  avgRating,
@@ -132,6 +136,40 @@ const ProducHorizontalCard = (props: IProductHorizontalCardProps) => {
132
136
  return !isSuspended && !isSoldOut && typeof onBuyNow === 'function';
133
137
  }, [isSuspended, isSoldOut, onBuyNow]);
134
138
 
139
+ const onPress = useCallback(() => {
140
+ if (!item) return;
141
+ onNavigateToProduct?.(item);
142
+ logGA(GAEvents.chatDetailProductCardTap, {
143
+ conversation_id: useSessionStore.getState().sessionId,
144
+ product_id: item.id,
145
+ position_in_list: -1, //TODO: fix
146
+ });
147
+ }, [onNavigateToProduct, logGA, item]);
148
+
149
+ const onPressAddToCart = useCallback(() => {
150
+ if (!item) return;
151
+ onAddToCart?.(item);
152
+ logGA(GAEvents.chatDetailAddToCartBtnTap, {
153
+ conversation_id: useSessionStore.getState().sessionId,
154
+ product_id: item.id,
155
+ position_in_list: -1, //TODO: fix
156
+ pdp_id: item.pdpId,
157
+ message_id: messageId,
158
+ });
159
+ }, [onAddToCart, logGA, item, messageId]);
160
+
161
+ const onPressBuyNow = useCallback(() => {
162
+ if (!item) return;
163
+ onBuyNow?.(item);
164
+ logGA(GAEvents.chatDetailBuyNowBtnTap, {
165
+ conversation_id: useSessionStore.getState().sessionId,
166
+ product_id: item.id,
167
+ position_in_list: -1, //TODO: fix
168
+ pdp_id: item.pdpId,
169
+ message_id: messageId,
170
+ });
171
+ }, [onBuyNow, logGA, item, messageId]);
172
+
135
173
  if (!item) {
136
174
  return (
137
175
  <KContainer.View style={styles.container}>
@@ -170,10 +208,7 @@ const ProducHorizontalCard = (props: IProductHorizontalCardProps) => {
170
208
  }
171
209
 
172
210
  return (
173
- <KContainer.Touchable
174
- style={styles.container}
175
- onPress={() => onNavigateToProduct?.(item)}
176
- >
211
+ <KContainer.Touchable style={styles.container} onPress={onPress}>
177
212
  <KContainer.View style={styles.image}>
178
213
  <KImage.Base
179
214
  uri={imageUrl}
@@ -294,7 +329,7 @@ const ProducHorizontalCard = (props: IProductHorizontalCardProps) => {
294
329
  </KLabel.Text>
295
330
  <KContainer.View style={styles.actionsRow}>
296
331
  <KButton.Base
297
- onPress={() => onAddToCart?.(item)}
332
+ onPress={onPressAddToCart}
298
333
  background={KColors.palette.primary.w25}
299
334
  tintColor={KColors.primary.normal}
300
335
  icon={{
@@ -305,7 +340,7 @@ const ProducHorizontalCard = (props: IProductHorizontalCardProps) => {
305
340
  disabled={!isEnabledAddToCart}
306
341
  />
307
342
  <KButton.Solid
308
- onPress={() => onBuyNow?.(item)}
343
+ onPress={onPressBuyNow}
309
344
  size="sm"
310
345
  label="Mua ngay"
311
346
  kind="primary"
@@ -318,7 +353,7 @@ const ProducHorizontalCard = (props: IProductHorizontalCardProps) => {
318
353
  );
319
354
  };
320
355
 
321
- export default ProducHorizontalCard;
356
+ export default ProductHorizontalCard;
322
357
 
323
358
  const styles = StyleSheet.create({
324
359
  abs: {
@@ -5,3 +5,37 @@ export const events = {
5
5
  expandThinkingStep: 'expand_thinking_step',
6
6
  updateMessageError: 'update_message_error',
7
7
  };
8
+
9
+ export const GAEvents = {
10
+ newChatPageView: 'NewChat_Page_View',
11
+ chatDetailPageView: 'ChatDetail_Page_View',
12
+ chatDetailSendBtnTap: 'ChatDetail_SendBtn_Tap',
13
+ chatDetailLikeDislikeBtnTap: 'ChatDetail_LikeDislikeBtn_Tap',
14
+ chatDetailCopyBtnTap: 'ChatDetail_CopyBtn_Tap',
15
+ chatDetailVoiceBtnTap: 'ChatDetail_VoiceBtn_Tap',
16
+ chatDetailExportPDFBtnTap: 'ChatDetail_ExportPDFBtn_Tap',
17
+ chatDetailShareMessBtnTap: 'ChatDetail_ShareMessBtn_Tap',
18
+ chatDetailStopChatBtnTap: 'ChatDetail_StopChatBtn_Tap',
19
+ chatDetailMultimodalTap: 'ChatDetail_Multimodal_Tap',
20
+ chatDetailMultimodalUpload: 'ChatDetail_Multimodal_Upload',
21
+ chatDetailMultimodalSend: 'ChatDetail_Multimodal_Send',
22
+ chatDetailProductCardTap: 'ChatDetail_ProductCard_Tap',
23
+ chatDetailAddToCartBtnTap: 'ChatDetail_AddToCartBtn_Tap',
24
+ chatDetailBuyNowBtnTap: 'ChatDetail_BuyNowBtn_Tap',
25
+ newChatCreateBtnTap: 'NewChat_CreateBtn_Tap',
26
+ chatDetailShoppingCartBtnTap: 'ChatDetail_ShoppingCartBtn_Tap',
27
+ chatDetailShareBtnTap: 'ChatDetail_ShareBtn_Tap',
28
+ shareChatModalCopyBtnTap: 'ShareChatModal_CopyBtn_Tap',
29
+ chatDetailRenameBtnTap: 'ChatDetail_RenameBtn_Tap',
30
+ renameModalSaveBtnTap: 'RenameModal_SaveBtn_Tap',
31
+ deleteChatModalDeleteBtnTap: 'DeleteChatModal_DeleteBtn_Tap',
32
+ sidebarSearchConversationTap: 'Sidebar_SearchConversation_Tap',
33
+ sidebarSearchConversationSend: 'Sidebar_SearchConversation_Send',
34
+ newChatNewChatPrmSuggTap: 'NewChat_NewChatPrmSugg_Tap',
35
+ chatDetailInConvPrmSuggTap: 'ChatDetail_InConvPrmSugg_Tap',
36
+ chatDetailInConvPrmSuggScroll: 'ChatDetail_InConvPrmSugg_Scroll',
37
+ chatDetailInConvPrmSuggReturn: 'ChatDetail_InConvPrmSugg_Return',
38
+ chatDetailLivechatBtnTap: 'ChatDetail_LivechatBtn_Tap',
39
+ chatDetailFAQTap: 'ChatDetail_FAQ_Tap',
40
+ chatDetailGiftDetailBtnTap: 'ChatDetail_GiftDetailBtn_Tap',
41
+ };
@@ -16,6 +16,7 @@ export const ChatContext = createContext<ChatContextType>({
16
16
  openDrawer: () => {},
17
17
  closeDrawer: () => {},
18
18
  chatbotUrl: '',
19
+ logGA: () => {},
19
20
  });
20
21
 
21
22
  export const useChatContext = () => useContext(ChatContext);
@@ -40,6 +41,7 @@ export const ChatProvider = (props: ChatProviderProps) => {
40
41
  csTeamId,
41
42
  pushLinkTo,
42
43
  chatbotUrl,
44
+ logGA,
43
45
  } = props;
44
46
 
45
47
  return (
@@ -57,6 +59,7 @@ export const ChatProvider = (props: ChatProviderProps) => {
57
59
  openDrawer,
58
60
  closeDrawer,
59
61
  chatbotUrl,
62
+ logGA,
60
63
  }}
61
64
  >
62
65
  <ReanimatedDrawerExample ref={ref}>