react-native-chatbot-ai 0.1.47 → 0.1.49

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 (55) hide show
  1. package/lib/module/components/chat/ChatEmpty.js +7 -13
  2. package/lib/module/components/chat/ChatEmpty.js.map +1 -1
  3. package/lib/module/components/chat/ChatMessageList.js +27 -12
  4. package/lib/module/components/chat/ChatMessageList.js.map +1 -1
  5. package/lib/module/components/chat/SuggestionItem.js +16 -8
  6. package/lib/module/components/chat/SuggestionItem.js.map +1 -1
  7. package/lib/module/components/chat/footer/SuggestionsBar.js +1 -1
  8. package/lib/module/components/chat/footer/SuggestionsBar.js.map +1 -1
  9. package/lib/module/components/chat/footer/item/UploadFileItem.js +21 -1
  10. package/lib/module/components/chat/footer/item/UploadFileItem.js.map +1 -1
  11. package/lib/module/components/chat/item/ChatAIAnswerMessageItem.js +11 -138
  12. package/lib/module/components/chat/item/ChatAIAnswerMessageItem.js.map +1 -1
  13. package/lib/module/components/chat/item/ChatAIThinkingMessageItem.js +2 -1
  14. package/lib/module/components/chat/item/ChatAIThinkingMessageItem.js.map +1 -1
  15. package/lib/module/components/chat/item/ShimmerBlock.js +7 -1
  16. package/lib/module/components/chat/item/ShimmerBlock.js.map +1 -1
  17. package/lib/module/components/chat/item/index.js +3 -2
  18. package/lib/module/components/chat/item/index.js.map +1 -1
  19. package/lib/module/hooks/message/useMessage.js +2 -0
  20. package/lib/module/hooks/message/useMessage.js.map +1 -1
  21. package/lib/module/hooks/upload/useFileUpload.js +2 -1
  22. package/lib/module/hooks/upload/useFileUpload.js.map +1 -1
  23. package/lib/module/store/streamMessage.js +11 -0
  24. package/lib/module/store/streamMessage.js.map +1 -1
  25. package/lib/typescript/src/components/chat/ChatEmpty.d.ts +5 -1
  26. package/lib/typescript/src/components/chat/ChatEmpty.d.ts.map +1 -1
  27. package/lib/typescript/src/components/chat/ChatMessageList.d.ts.map +1 -1
  28. package/lib/typescript/src/components/chat/SuggestionItem.d.ts.map +1 -1
  29. package/lib/typescript/src/components/chat/footer/item/UploadFileItem.d.ts.map +1 -1
  30. package/lib/typescript/src/components/chat/item/ChatAIAnswerMessageItem.d.ts +0 -2
  31. package/lib/typescript/src/components/chat/item/ChatAIAnswerMessageItem.d.ts.map +1 -1
  32. package/lib/typescript/src/components/chat/item/ChatAIThinkingMessageItem.d.ts.map +1 -1
  33. package/lib/typescript/src/components/chat/item/ShimmerBlock.d.ts.map +1 -1
  34. package/lib/typescript/src/components/chat/item/index.d.ts +1 -1
  35. package/lib/typescript/src/components/chat/item/index.d.ts.map +1 -1
  36. package/lib/typescript/src/hooks/message/useMessage.d.ts.map +1 -1
  37. package/lib/typescript/src/hooks/upload/useFileUpload.d.ts +1 -1
  38. package/lib/typescript/src/hooks/upload/useFileUpload.d.ts.map +1 -1
  39. package/lib/typescript/src/store/streamMessage.d.ts.map +1 -1
  40. package/lib/typescript/src/types/chat.d.ts +1 -0
  41. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  42. package/package.json +3 -1
  43. package/src/components/chat/ChatEmpty.tsx +9 -13
  44. package/src/components/chat/ChatMessageList.tsx +24 -8
  45. package/src/components/chat/SuggestionItem.tsx +15 -8
  46. package/src/components/chat/footer/SuggestionsBar.tsx +1 -1
  47. package/src/components/chat/footer/item/UploadFileItem.tsx +21 -1
  48. package/src/components/chat/item/ChatAIAnswerMessageItem.tsx +15 -168
  49. package/src/components/chat/item/ChatAIThinkingMessageItem.tsx +1 -0
  50. package/src/components/chat/item/ShimmerBlock.tsx +4 -1
  51. package/src/components/chat/item/index.tsx +20 -12
  52. package/src/hooks/message/useMessage.ts +4 -0
  53. package/src/hooks/upload/useFileUpload.ts +6 -2
  54. package/src/store/streamMessage.ts +7 -0
  55. package/src/types/chat.ts +1 -0
@@ -1,7 +1,7 @@
1
1
  import { FileUpload } from '../../hooks/upload/useUploadItem';
2
2
  interface UseFileUploadProps {
3
3
  file: FileUpload;
4
- onSuccess?: (data: any) => void;
4
+ onSuccess?: (data: any, duration?: number) => void;
5
5
  onError?: (err: any) => void;
6
6
  }
7
7
  export declare const useFileUpload: ({ file, onSuccess, onError, }: UseFileUploadProps) => {
@@ -1 +1 @@
1
- {"version":3,"file":"useFileUpload.d.ts","sourceRoot":"","sources":["../../../../../src/hooks/upload/useFileUpload.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAc9D,UAAU,kBAAkB;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,aAAa,GAAI,+BAI3B,kBAAkB;;;;;;CA+FpB,CAAC"}
1
+ {"version":3,"file":"useFileUpload.d.ts","sourceRoot":"","sources":["../../../../../src/hooks/upload/useFileUpload.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAc9D,UAAU,kBAAkB;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,aAAa,GAAI,+BAI3B,kBAAkB;;;;;;CAmGpB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"streamMessage.d.ts","sourceRoot":"","sources":["../../../../src/store/streamMessage.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAKnD,QAAA,MAAM,qBAAqB,iFAkCxB,CAAC;AAEJ,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"streamMessage.d.ts","sourceRoot":"","sources":["../../../../src/store/streamMessage.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAKnD,QAAA,MAAM,qBAAqB,iFAyCxB,CAAC;AAEJ,eAAe,qBAAqB,CAAC"}
@@ -65,6 +65,7 @@ export interface StreamMessageStore {
65
65
  eventSource?: EventSource;
66
66
  setEventSource: (es?: EventSource) => void;
67
67
  stopStream: () => void;
68
+ clearStreamMessageRecordById: (ids: string[]) => void;
68
69
  }
69
70
  export interface ProductsStore {
70
71
  products: Record<string, IProductItem | undefined>;
@@ -1 +1 @@
1
- {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../../src/types/chat.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../../src/types/chat.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-chatbot-ai",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "description": "React Native library for Chatbot AI",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -96,6 +96,7 @@
96
96
  "@d11/react-native-fast-image": "*",
97
97
  "@droppii/libs": "*",
98
98
  "@react-native-documents/picker": "*",
99
+ "@shopify/flash-list": "*",
99
100
  "@tanstack/react-query": "*",
100
101
  "react": "*",
101
102
  "react-native": "*",
@@ -179,6 +180,7 @@
179
180
  "axios": "^1.12.2",
180
181
  "dayjs": "^1.11.18",
181
182
  "lodash": "^4.17.21",
183
+ "react-native-animatable": "^1.4.0",
182
184
  "react-native-marked": "^7.0.2",
183
185
  "react-native-skeleton-placeholder": "^5.2.4",
184
186
  "react-native-sse": "^1.2.1",
@@ -1,6 +1,5 @@
1
1
  import { KColors, KContainer, KLabel, KSpacingValue } from '@droppii/libs';
2
2
  import { StyleSheet } from 'react-native';
3
- import { useFetchSuggestions } from '../../hooks/suggestions/useFetchSuggestions';
4
3
  import SuggestionItem from './SuggestionItem';
5
4
  import { useSendMessage } from '../../hooks/message/useSendMessage';
6
5
  import debounce from 'lodash/debounce';
@@ -9,15 +8,18 @@ import { trans } from '../../translation';
9
8
  import { useChatContext } from '../../context/ChatContext';
10
9
  import { GAEvents } from '../../constants/events';
11
10
 
12
- const ChatEmpty = () => {
13
- const { data } = useFetchSuggestions();
11
+ interface IChatEmptyProps {
12
+ suggestions?: ISuggestionItem[];
13
+ }
14
+
15
+ const ChatEmpty = ({ suggestions }: IChatEmptyProps) => {
14
16
  const { onSendMessage } = useSendMessage();
15
17
  const logGA = useChatContext().logGA;
16
18
 
17
19
  const onPress = debounce((item: ISuggestionItem, index: number) => {
18
20
  logGA(GAEvents.newChatNewChatPrmSuggTap, {
19
21
  suggestion_id: item.suggestion_id,
20
- suggestion_content: item.content,
22
+ suggestion_content: item?.content?.slice?.(0, 100) || '',
21
23
  suggestion_cate: item.category,
22
24
  order: index,
23
25
  });
@@ -25,10 +27,7 @@ const ChatEmpty = () => {
25
27
  }, 200);
26
28
 
27
29
  return (
28
- <KContainer.ScrollView
29
- contentContainerStyle={styles.container}
30
- style={styles.transformStyle}
31
- >
30
+ <KContainer.View style={styles.container}>
32
31
  <KLabel.Text typo="TextXLgMedium" center>
33
32
  {trans('chat_empty_title')}
34
33
  </KLabel.Text>
@@ -40,7 +39,7 @@ const ChatEmpty = () => {
40
39
  {trans('chat_empty_description_2')}
41
40
  </KLabel.Text>
42
41
  </KContainer.View>
43
- {data?.suggestions?.map((item: ISuggestionItem, index: number) => (
42
+ {suggestions?.map((item: ISuggestionItem, index: number) => (
44
43
  <SuggestionItem
45
44
  key={item.suggestion_id}
46
45
  item={item}
@@ -57,7 +56,7 @@ const ChatEmpty = () => {
57
56
  >
58
57
  {trans('ai_answer_note')}
59
58
  </KLabel.Text>
60
- </KContainer.ScrollView>
59
+ </KContainer.View>
61
60
  );
62
61
  };
63
62
 
@@ -71,9 +70,6 @@ const styles = StyleSheet.create({
71
70
  padding: KSpacingValue['0.75rem'],
72
71
  gap: KSpacingValue['0.5rem'],
73
72
  },
74
- transformStyle: {
75
- // transform: [Platform.OS === 'android' ? { scale: -1 } : { scaleY: -1 }],
76
- },
77
73
  disclaimer: {
78
74
  fontSize: 11,
79
75
  paddingHorizontal: KSpacingValue['0.5rem'],
@@ -1,4 +1,4 @@
1
- import { FlatList, StyleSheet, View } from 'react-native';
1
+ import { StyleSheet, View } from 'react-native';
2
2
  import type { ComponentType } from 'react';
3
3
  import { useCallback, useRef, useEffect, useState } from 'react';
4
4
  import ChatEmpty from './ChatEmpty';
@@ -8,8 +8,10 @@ import type { IMessageItem } from '../../types';
8
8
  import { GAEvents } from '../../constants/events';
9
9
  import { useChatContext } from '../../context/ChatContext';
10
10
  import useSessionStore from '../../store/session';
11
+ import { FlashList } from '@shopify/flash-list';
12
+ import { useFetchSuggestions } from '../../hooks/suggestions/useFetchSuggestions';
11
13
 
12
- const FlatListComponent = FlatList as unknown as ComponentType<any>;
14
+ const FlashListComponent = FlashList as unknown as ComponentType<any>;
13
15
 
14
16
  interface IChatMessageListProps {
15
17
  messageList?: IMessageItem[];
@@ -18,9 +20,12 @@ interface IChatMessageListProps {
18
20
  const ChatMessageList = ({ messageList = [] }: IChatMessageListProps) => {
19
21
  const logGA = useChatContext().logGA;
20
22
  const sessionId = useSessionStore((state) => state.sessionId);
23
+ const { data: suggestions } = useFetchSuggestions();
21
24
 
22
25
  const flatListRef = useRef<any>(null);
23
26
  const scrollOffsetRef = useRef(0);
27
+ const contentHeightRef = useRef(0);
28
+ const viewportHeightRef = useRef(0);
24
29
  const [showScrollToBottom, setShowScrollToBottom] = useState(false);
25
30
 
26
31
  const onScroll = useCallback((event: any) => {
@@ -36,7 +41,11 @@ const ChatMessageList = ({ messageList = [] }: IChatMessageListProps) => {
36
41
 
37
42
  const scrollToBottom = useCallback(async () => {
38
43
  await new Promise((resolve) => setTimeout(resolve, 300));
39
- flatListRef.current?.scrollToEnd({ animated: true });
44
+ const offset = Math.max(
45
+ 0,
46
+ contentHeightRef.current - viewportHeightRef.current
47
+ );
48
+ flatListRef.current?.scrollToOffset({ animated: true, offset });
40
49
  }, []);
41
50
 
42
51
  useEffect(() => {
@@ -53,11 +62,11 @@ const ChatMessageList = ({ messageList = [] }: IChatMessageListProps) => {
53
62
  if (messageList.length > 0) {
54
63
  scrollToBottom();
55
64
  }
56
- }, [messageList, scrollToBottom]);
65
+ }, [messageList.length, scrollToBottom]);
57
66
 
58
67
  return (
59
68
  <View style={{ flex: 1 }}>
60
- <FlatListComponent
69
+ <FlashListComponent
61
70
  ref={flatListRef}
62
71
  data={messageList}
63
72
  renderItem={({
@@ -71,10 +80,18 @@ const ChatMessageList = ({ messageList = [] }: IChatMessageListProps) => {
71
80
  )}
72
81
  keyExtractor={(item: IMessageItem) => item.id}
73
82
  contentContainerStyle={styles.container}
74
- // inverted
75
- ListEmptyComponent={<ChatEmpty />}
83
+ ListEmptyComponent={
84
+ <ChatEmpty suggestions={suggestions?.suggestions || []} />
85
+ }
76
86
  onScroll={onScroll}
77
87
  scrollEventThrottle={16}
88
+ estimatedItemSize={60}
89
+ onLayout={(e: any) => {
90
+ viewportHeightRef.current = e?.nativeEvent?.layout?.height || 0;
91
+ }}
92
+ onContentSizeChange={(_: any, height: number) => {
93
+ contentHeightRef.current = height;
94
+ }}
78
95
  />
79
96
 
80
97
  {showScrollToBottom && (
@@ -100,7 +117,6 @@ const styles = StyleSheet.create({
100
117
  flexGrow: 1,
101
118
  gap: KSpacingValue['0.5rem'],
102
119
  paddingVertical: KSpacingValue['1rem'],
103
- // justifyContent: 'flex-end',
104
120
  },
105
121
  scrollButton: {
106
122
  position: 'absolute',
@@ -10,6 +10,7 @@ import {
10
10
  } from '@droppii/libs';
11
11
  import { SuggestionCategory } from '../../types/common';
12
12
  import { StyleSheet } from 'react-native';
13
+ import * as Animatable from 'react-native-animatable';
13
14
 
14
15
  interface SuggestionItemProps {
15
16
  item: ISuggestionItem;
@@ -80,15 +81,21 @@ const SuggestionItem = ({ item, index, onPressItem }: SuggestionItemProps) => {
80
81
  }, [item.category]);
81
82
 
82
83
  return (
83
- <KContainer.Touchable
84
- style={styles.container}
85
- onPress={() => onPressItem(item, index)}
84
+ <Animatable.View
85
+ animation="fadeInUp"
86
+ duration={500}
87
+ style={{ width: '100%' }}
86
88
  >
87
- {icon}
88
- <KLabel.Text flex typo="TextNmNormal">
89
- {item?.content || ''}
90
- </KLabel.Text>
91
- </KContainer.Touchable>
89
+ <KContainer.Touchable
90
+ style={styles.container}
91
+ onPress={() => onPressItem(item, index)}
92
+ >
93
+ {icon}
94
+ <KLabel.Text flex typo="TextNmNormal">
95
+ {item?.content || ''}
96
+ </KLabel.Text>
97
+ </KContainer.Touchable>
98
+ </Animatable.View>
92
99
  );
93
100
  };
94
101
 
@@ -77,7 +77,7 @@ const SuggestionsBar = ({
77
77
  conversation_id: useSessionStore.getState().sessionId,
78
78
  group_id: groupSuggestionId,
79
79
  suggestion_id: item.suggestion_id,
80
- suggestion_content: item.content,
80
+ suggestion_content: item?.content?.slice?.(0, 100) || '',
81
81
  suggestion_cate: item.category,
82
82
  order: index,
83
83
  });
@@ -13,6 +13,9 @@ import { formatFileSize, shortenFileName } from '../../../../utils/common';
13
13
  import { useFileUpload } from '../../../../hooks/upload/useFileUpload';
14
14
  import UIUtils from '../../../../utils/ui';
15
15
  import { useEffect, useRef } from 'react';
16
+ import { useChatContext } from 'src/context/ChatContext';
17
+ import { GAEvents } from 'src/constants/events';
18
+ import useSessionStore from 'src/store/session';
16
19
 
17
20
  interface Props {
18
21
  item: FileUpload;
@@ -20,9 +23,10 @@ interface Props {
20
23
  onSuccess?: (item: FileUpload) => void;
21
24
  }
22
25
  const UploadFileItem = ({ item, onRemove, onSuccess }: Props) => {
26
+ const logGA = useChatContext().logGA;
23
27
  const { isUploading, cancel } = useFileUpload({
24
28
  file: item,
25
- onSuccess: (data: any) => {
29
+ onSuccess: (data: any, duration?: number) => {
26
30
  const mItem = {
27
31
  ...item,
28
32
  remoteUrl: data?.[0]?.publicUrl,
@@ -30,6 +34,14 @@ const UploadFileItem = ({ item, onRemove, onSuccess }: Props) => {
30
34
  };
31
35
  if (mItem.remoteUrl) {
32
36
  onSuccess?.(mItem);
37
+ logGA(GAEvents.chatDetailMultimodalUpload, {
38
+ conversation_id: useSessionStore.getState().sessionId,
39
+ file_type: 'file',
40
+ duration: duration || 0,
41
+ size_mb: (item?.size || 0) / 1024 / 1024,
42
+ status: 'success',
43
+ format: item.type,
44
+ });
33
45
  }
34
46
  },
35
47
  onError: () => {
@@ -38,6 +50,14 @@ const UploadFileItem = ({ item, onRemove, onSuccess }: Props) => {
38
50
  theme: 'danger',
39
51
  });
40
52
  onRemove?.(item);
53
+ logGA(GAEvents.chatDetailMultimodalUpload, {
54
+ conversation_id: useSessionStore.getState().sessionId,
55
+ file_type: 'file',
56
+ duration: 0,
57
+ size_mb: (item.size || 0) / 1024 / 1024,
58
+ status: 'failed',
59
+ format: item.type,
60
+ });
41
61
  },
42
62
  });
43
63
 
@@ -9,12 +9,13 @@ import {
9
9
  import { IMessageItem } from '../../../types';
10
10
  import { StyleSheet, Linking } from 'react-native';
11
11
  import useStreamMessageStore from '../../../store/streamMessage';
12
- import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
13
- import Markdown, {
12
+ import React, { Fragment, ReactNode, useMemo } from 'react';
13
+ import {
14
14
  Renderer,
15
15
  RendererInterface,
16
16
  MarkedTokenizer,
17
17
  MarkedStyles,
18
+ useMarkdown,
18
19
  } from 'react-native-marked';
19
20
  import { useSearchProduct } from '../../../hooks/product/useSearchProduct';
20
21
  import ProductHorizontalCard from '../../product/CardHorizontal';
@@ -33,141 +34,6 @@ interface ChatAIAnswerMessageItemProps {
33
34
 
34
35
  class CustomTokenizer extends MarkedTokenizer {}
35
36
 
36
- export function splitMarkdownBlocks(text: string): string[] {
37
- if (typeof text !== 'string' || !text.length) return [];
38
-
39
- const lines = (text + '\n').split(/\r?\n/);
40
- const blocks: string[] = [];
41
- let buffer: string[] = [];
42
- let insideCode = false;
43
- let insideTable = false;
44
-
45
- for (let i = 0; i < lines.length; i++) {
46
- const rawLine = lines[i] ?? '';
47
- const line = rawLine.trimEnd();
48
-
49
- // --- code block ---
50
- if (/^```|^~~~/.test(line)) {
51
- insideCode = !insideCode;
52
- buffer.push(rawLine);
53
- if (!insideCode) {
54
- blocks.push(buffer.join('\n'));
55
- buffer = [];
56
- }
57
- continue;
58
- }
59
- if (insideCode) {
60
- buffer.push(rawLine);
61
- continue;
62
- }
63
-
64
- // --- table block ---
65
- if (/^\|.*\|/.test(line)) {
66
- // nếu bắt đầu table mới
67
- if (!insideTable && buffer.length > 0) {
68
- blocks.push(buffer.join('\n'));
69
- buffer = [];
70
- }
71
-
72
- insideTable = true;
73
- buffer.push(rawLine);
74
-
75
- // nếu dòng kế tiếp không phải bảng -> kết thúc bảng
76
- const nextLine = lines[i + 1];
77
- if (!nextLine || !/^\|.*\|/.test(nextLine)) {
78
- insideTable = false;
79
- blocks.push(buffer.join('\n'));
80
- buffer = [];
81
- }
82
- continue;
83
- }
84
-
85
- // --- component marker ---
86
- // ❗ nếu đang trong bảng, giữ nguyên marker trong buffer, không cắt block
87
- if (line.includes('<!-- @component:') && !insideTable) {
88
- const parts = rawLine
89
- .split(/(<!--\s*@component:[\w-]+\([^)]+\)\s*-->)/g)
90
- .filter(Boolean);
91
-
92
- for (const part of parts) {
93
- if (/^<!--\s*@component:[\w-]+\(/.test(part.trim())) {
94
- if (buffer.length > 0) {
95
- blocks.push(buffer.join('\n'));
96
- buffer = [];
97
- }
98
- blocks.push(part.trim());
99
- } else if (part.trim().length > 0) {
100
- buffer.push(part);
101
- }
102
- }
103
- continue;
104
- }
105
-
106
- // --- block starter (heading, list, quote, v.v.) ---
107
- if (
108
- /^(\s*#{1,6}\s+|\s*>|\s*[-*+]\s+|\s*\d+\.\s+)/.test(line) &&
109
- !insideTable
110
- ) {
111
- if (buffer.length > 0) {
112
- blocks.push(buffer.join('\n'));
113
- buffer = [];
114
- }
115
- }
116
-
117
- buffer.push(rawLine);
118
- }
119
-
120
- if (buffer.length > 0) {
121
- blocks.push(buffer.join('\n'));
122
- }
123
-
124
- return blocks.map((b) => b.trim()).filter(Boolean);
125
- }
126
-
127
- export function useStreamingMarkdownBlocks(text: string) {
128
- const [blocks, setBlocks] = useState<string[]>([]);
129
- const prevBlocksRef = useRef<string[]>([]);
130
-
131
- useEffect(() => {
132
- if (!text) {
133
- setBlocks([]);
134
- prevBlocksRef.current = [];
135
- return;
136
- }
137
-
138
- const newBlocks = splitMarkdownBlocks(text);
139
- const prevBlocks = prevBlocksRef.current;
140
-
141
- // ✅ Merge thông minh, không làm mất chữ
142
- let merged = [...prevBlocks];
143
-
144
- for (let i = 0; i < newBlocks.length; i++) {
145
- const newBlock = newBlocks[i];
146
- const oldBlock = prevBlocks[i];
147
-
148
- // nếu block chưa có hoặc thay đổi nội dung → cập nhật
149
- if (oldBlock === undefined || newBlock !== oldBlock) {
150
- merged[i] = newBlock || '';
151
- }
152
- }
153
-
154
- // nếu có thêm block mới (VD: text dài hơn)
155
- if (newBlocks.length > merged.length) {
156
- merged = [...merged, ...newBlocks.slice(merged.length)];
157
- }
158
-
159
- // nếu markdown bị rút ngắn (xóa bớt)
160
- if (newBlocks.length < merged.length) {
161
- merged = merged.slice(0, newBlocks.length);
162
- }
163
-
164
- prevBlocksRef.current = merged;
165
- setBlocks(merged);
166
- }, [text]);
167
-
168
- return blocks;
169
- }
170
-
171
37
  class CustomRenderer extends Renderer implements RendererInterface {
172
38
  constructor(
173
39
  openImageViewer?: (images: { url: string }[], index: number) => void,
@@ -199,11 +65,7 @@ class CustomRenderer extends Renderer implements RendererInterface {
199
65
  );
200
66
  default:
201
67
  return (
202
- <KLabel.Text
203
- typo="TextMdNormal"
204
- key={this.getKey()}
205
- selectable={true}
206
- >
68
+ <KLabel.Text typo="TextMdNormal" key={this.getKey()}>
207
69
  {match?.[2] || ''}
208
70
  </KLabel.Text>
209
71
  );
@@ -229,7 +91,6 @@ class CustomRenderer extends Renderer implements RendererInterface {
229
91
  key={this.getKey()}
230
92
  typo="TextMdNormal"
231
93
  color={KColors.black}
232
- selectable={true}
233
94
  >
234
95
  {text}
235
96
  </KLabel.Text>
@@ -242,7 +103,6 @@ class CustomRenderer extends Renderer implements RendererInterface {
242
103
  key={this.getKey()}
243
104
  typo="TextMdNormal"
244
105
  color={KColors.black}
245
- selectable={true}
246
106
  >
247
107
  {text}
248
108
  </KLabel.Text>
@@ -256,7 +116,6 @@ class CustomRenderer extends Renderer implements RendererInterface {
256
116
  typo="TextMdBold"
257
117
  style={styles.mdStrong}
258
118
  color={KColors.black}
259
- selectable={true}
260
119
  >
261
120
  {children}
262
121
  </KLabel.Text>
@@ -269,7 +128,6 @@ class CustomRenderer extends Renderer implements RendererInterface {
269
128
  typo="TextMdNormal"
270
129
  style={styles.mdEm}
271
130
  color={KColors.black}
272
- selectable={true}
273
131
  >
274
132
  {children}
275
133
  </KLabel.Text>
@@ -294,7 +152,6 @@ class CustomRenderer extends Renderer implements RendererInterface {
294
152
  typo="TextMdNormal"
295
153
  style={textStyle}
296
154
  color={KColors.gray.dark}
297
- selectable={true}
298
155
  >
299
156
  {text}
300
157
  </KLabel.Text>
@@ -425,10 +282,6 @@ const ChatAIAnswerMessageItem = ({
425
282
  );
426
283
  const isStreaming = useStreamMessageStore((state) => state.isStreaming);
427
284
 
428
- const blocks = useStreamingMarkdownBlocks(
429
- streamMessage?.content || item.content
430
- );
431
-
432
285
  const openImageViewer = useChatContext()?.openImageViewer;
433
286
 
434
287
  const productIds = useMemo(() => {
@@ -463,6 +316,12 @@ const ChatAIAnswerMessageItem = ({
463
316
  );
464
317
  const tokenizer = useMemo(() => new CustomTokenizer(), []);
465
318
 
319
+ const blocks = useMarkdown(streamMessage?.content || item.content, {
320
+ renderer: renderer,
321
+ tokenizer: tokenizer,
322
+ styles: markedStyles,
323
+ });
324
+
466
325
  // Extract all product images for sharing
467
326
  const productImages = useMemo(() => {
468
327
  if (productIds.length === 0) {
@@ -489,23 +348,11 @@ const ChatAIAnswerMessageItem = ({
489
348
  return (
490
349
  <KContainer.View style={styles.container}>
491
350
  {blocks.map((chunk, index) => (
492
- <ShimmerBlock
493
- key={`chunk-${index}`}
494
- enable={streamMessage && isStreaming}
495
- >
496
- <Markdown
497
- renderer={renderer}
498
- value={chunk}
499
- tokenizer={tokenizer}
500
- styles={markedStyles}
501
- flatListProps={{
502
- style: styles.flatList,
503
- removeClippedSubviews: false,
504
- windowSize: 50,
505
- initialNumToRender: 999,
506
- }}
507
- />
508
- </ShimmerBlock>
351
+ <Fragment key={`chunk-${index}`}>
352
+ <ShimmerBlock enable={streamMessage && isStreaming}>
353
+ {chunk}
354
+ </ShimmerBlock>
355
+ </Fragment>
509
356
  ))}
510
357
 
511
358
  {/* Message Actions Bar */}
@@ -137,6 +137,7 @@ const ChatAIThinkingMessageItem = ({
137
137
  value={display}
138
138
  flatListProps={{
139
139
  style: styles.flatList,
140
+ scrollEnabled: false,
140
141
  }}
141
142
  />
142
143
  </KContainer.View>
@@ -1,6 +1,7 @@
1
1
  import { KColors } from '@droppii/libs';
2
2
  import React, { useEffect, useRef } from 'react';
3
3
  import { View, Animated, StyleSheet, useWindowDimensions } from 'react-native';
4
+ import * as Animatable from 'react-native-animatable';
4
5
 
5
6
  type Props = {
6
7
  children: React.ReactNode;
@@ -28,7 +29,9 @@ export default function ShimmerBlock({
28
29
 
29
30
  return (
30
31
  <View style={styles.container}>
31
- {children}
32
+ <Animatable.View animation="fadeIn" duration={duration} easing="ease-in">
33
+ {children}
34
+ </Animatable.View>
32
35
 
33
36
  {enable && (
34
37
  <Animated.View
@@ -1,3 +1,4 @@
1
+ import { memo } from 'react';
1
2
  import { IMessageItem, MessageType } from '../../../types';
2
3
  import ChatAIAnswerMessageItem from './ChatAIAnswerMessageItem';
3
4
  import ChatAIThinkingMessageItem from './ChatAIThinkingMessageItem';
@@ -7,17 +8,24 @@ interface ChatItemProps {
7
8
  item: IMessageItem;
8
9
  isLastItem: boolean;
9
10
  }
10
- const ChatItem = ({ item, isLastItem = false }: ChatItemProps) => {
11
- switch (item.type) {
12
- case MessageType.user_message:
13
- return <ChatUserMessageItem item={item} isLast={isLastItem} />;
14
- case MessageType.ai_answer:
15
- return <ChatAIAnswerMessageItem item={item} isLast={isLastItem} />;
16
- case MessageType.ai_thinking:
17
- return <ChatAIThinkingMessageItem item={item} isLast={isLastItem} />;
18
- default:
19
- return <ChatUserMessageItem item={item} isLast={isLastItem} />;
20
- }
21
- };
11
+
12
+ const ChatItem = memo(
13
+ ({ item, isLastItem = false }: ChatItemProps) => {
14
+ switch (item.type) {
15
+ case MessageType.user_message:
16
+ return <ChatUserMessageItem item={item} isLast={isLastItem} />;
17
+ case MessageType.ai_answer:
18
+ return <ChatAIAnswerMessageItem item={item} isLast={isLastItem} />;
19
+ case MessageType.ai_thinking:
20
+ return <ChatAIThinkingMessageItem item={item} isLast={isLastItem} />;
21
+ default:
22
+ return <ChatUserMessageItem item={item} isLast={isLastItem} />;
23
+ }
24
+ },
25
+ (prev, next) =>
26
+ prev.isLastItem === next.isLastItem &&
27
+ prev.item?.id === next.item?.id &&
28
+ prev.item?.content === next.item?.content
29
+ );
22
30
 
23
31
  export default ChatItem;
@@ -4,6 +4,7 @@ import { useFetchSessionById } from '../session/useFetchSessionById';
4
4
  import { IMessageItem, SessionDetailResponse } from '../../types/dto';
5
5
  import { DeviceEventEmitter } from 'react-native';
6
6
  import { events } from '../../constants/events';
7
+ import useStreamMessageStore from '../../store/streamMessage';
7
8
 
8
9
  const initialState = {
9
10
  id: '',
@@ -107,6 +108,9 @@ export const useMessage = () => {
107
108
  messages: [...merged, ...newItems],
108
109
  };
109
110
  });
111
+ useStreamMessageStore
112
+ .getState()
113
+ .clearStreamMessageRecordById(messages.map((m) => m.id));
110
114
  }
111
115
  );
112
116
  const subUpdateMessageError = DeviceEventEmitter.addListener(