react-native-chatbot-ai 0.1.1 → 0.1.2

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 (128) hide show
  1. package/lib/module/assets/svgIcon/IconChatArrow.js +0 -1
  2. package/lib/module/assets/svgIcon/IconChatArrow.js.map +1 -1
  3. package/lib/module/assets/svgIcon/IconPdf.js +27 -0
  4. package/lib/module/assets/svgIcon/IconPdf.js.map +1 -0
  5. package/lib/module/assets/svgIcon/IconThinkingStep.js +0 -1
  6. package/lib/module/assets/svgIcon/IconThinkingStep.js.map +1 -1
  7. package/lib/module/components/chat/ChatMessageList.js.map +1 -1
  8. package/lib/module/components/chat/footer/index.js +328 -0
  9. package/lib/module/components/chat/footer/index.js.map +1 -0
  10. package/lib/module/components/chat/footer/item/UploadFileItem.js +163 -0
  11. package/lib/module/components/chat/footer/item/UploadFileItem.js.map +1 -0
  12. package/lib/module/components/chat/footer/item/UploadImageItem.js +94 -0
  13. package/lib/module/components/chat/footer/item/UploadImageItem.js.map +1 -0
  14. package/lib/module/components/chat/index.js +1 -1
  15. package/lib/module/components/chat/index.js.map +1 -1
  16. package/lib/module/components/chat/item/ChatAIAnswerMessageItem.js +89 -28
  17. package/lib/module/components/chat/item/ChatAIAnswerMessageItem.js.map +1 -1
  18. package/lib/module/components/chat/item/ChatUserMessageItem.js +122 -15
  19. package/lib/module/components/chat/item/ChatUserMessageItem.js.map +1 -1
  20. package/lib/module/components/portal/Toast.js +193 -0
  21. package/lib/module/components/portal/Toast.js.map +1 -0
  22. package/lib/module/components/portal/index.js +15 -0
  23. package/lib/module/components/portal/index.js.map +1 -0
  24. package/lib/module/constants/index.js +9 -0
  25. package/lib/module/constants/index.js.map +1 -0
  26. package/lib/module/context/ChatContext.js +8 -5
  27. package/lib/module/context/ChatContext.js.map +1 -1
  28. package/lib/module/hooks/message/useMessage.js +0 -1
  29. package/lib/module/hooks/message/useMessage.js.map +1 -1
  30. package/lib/module/hooks/message/useSendMessage.js +3 -3
  31. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  32. package/lib/module/hooks/upload/useFileUpload.js +94 -0
  33. package/lib/module/hooks/upload/useFileUpload.js.map +1 -0
  34. package/lib/module/hooks/upload/useImageUpload.js +92 -0
  35. package/lib/module/hooks/upload/useImageUpload.js.map +1 -0
  36. package/lib/module/hooks/useAndroidBackHandler.js +20 -0
  37. package/lib/module/hooks/useAndroidBackHandler.js.map +1 -0
  38. package/lib/module/services/endpoints.js +3 -0
  39. package/lib/module/services/endpoints.js.map +1 -1
  40. package/lib/module/types/index.js +1 -0
  41. package/lib/module/types/index.js.map +1 -1
  42. package/lib/module/types/ui.js +4 -0
  43. package/lib/module/types/ui.js.map +1 -0
  44. package/lib/module/utils/common.js +31 -0
  45. package/lib/module/utils/common.js.map +1 -0
  46. package/lib/module/utils/device.js +137 -0
  47. package/lib/module/utils/device.js.map +1 -0
  48. package/lib/module/utils/ui.js +28 -0
  49. package/lib/module/utils/ui.js.map +1 -0
  50. package/lib/typescript/src/assets/svgIcon/IconChatArrow.d.ts.map +1 -1
  51. package/lib/typescript/src/assets/svgIcon/IconPdf.d.ts +7 -0
  52. package/lib/typescript/src/assets/svgIcon/IconPdf.d.ts.map +1 -0
  53. package/lib/typescript/src/assets/svgIcon/IconThinkingStep.d.ts.map +1 -1
  54. package/lib/typescript/src/components/chat/ChatMessageList.d.ts.map +1 -1
  55. package/lib/typescript/src/components/chat/footer/index.d.ts +16 -0
  56. package/lib/typescript/src/components/chat/footer/index.d.ts.map +1 -0
  57. package/lib/typescript/src/components/chat/footer/item/UploadFileItem.d.ts +9 -0
  58. package/lib/typescript/src/components/chat/footer/item/UploadFileItem.d.ts.map +1 -0
  59. package/lib/typescript/src/components/chat/footer/item/UploadImageItem.d.ts +9 -0
  60. package/lib/typescript/src/components/chat/footer/item/UploadImageItem.d.ts.map +1 -0
  61. package/lib/typescript/src/components/chat/item/ChatAIAnswerMessageItem.d.ts +9 -0
  62. package/lib/typescript/src/components/chat/item/ChatAIAnswerMessageItem.d.ts.map +1 -1
  63. package/lib/typescript/src/components/chat/item/ChatUserMessageItem.d.ts.map +1 -1
  64. package/lib/typescript/src/components/portal/Toast.d.ts +4 -0
  65. package/lib/typescript/src/components/portal/Toast.d.ts.map +1 -0
  66. package/lib/typescript/src/components/portal/index.d.ts +3 -0
  67. package/lib/typescript/src/components/portal/index.d.ts.map +1 -0
  68. package/lib/typescript/src/constants/index.d.ts +6 -0
  69. package/lib/typescript/src/constants/index.d.ts.map +1 -0
  70. package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
  71. package/lib/typescript/src/hooks/message/useMessage.d.ts.map +1 -1
  72. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +2 -1
  73. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  74. package/lib/typescript/src/hooks/upload/useFileUpload.d.ts +15 -0
  75. package/lib/typescript/src/hooks/upload/useFileUpload.d.ts.map +1 -0
  76. package/lib/typescript/src/hooks/upload/useImageUpload.d.ts +15 -0
  77. package/lib/typescript/src/hooks/upload/useImageUpload.d.ts.map +1 -0
  78. package/lib/typescript/src/hooks/useAndroidBackHandler.d.ts +4 -0
  79. package/lib/typescript/src/hooks/useAndroidBackHandler.d.ts.map +1 -0
  80. package/lib/typescript/src/services/endpoints.d.ts +3 -0
  81. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  82. package/lib/typescript/src/types/chat.d.ts +6 -0
  83. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  84. package/lib/typescript/src/types/dto.d.ts +10 -1
  85. package/lib/typescript/src/types/dto.d.ts.map +1 -1
  86. package/lib/typescript/src/types/index.d.ts +1 -0
  87. package/lib/typescript/src/types/index.d.ts.map +1 -1
  88. package/lib/typescript/src/types/ui.d.ts +7 -0
  89. package/lib/typescript/src/types/ui.d.ts.map +1 -0
  90. package/lib/typescript/src/utils/common.d.ts +10 -0
  91. package/lib/typescript/src/utils/common.d.ts.map +1 -0
  92. package/lib/typescript/src/utils/device.d.ts +11 -0
  93. package/lib/typescript/src/utils/device.d.ts.map +1 -0
  94. package/lib/typescript/src/utils/ui.d.ts +12 -0
  95. package/lib/typescript/src/utils/ui.d.ts.map +1 -0
  96. package/package.json +8 -3
  97. package/src/assets/svgIcon/IconChatArrow.tsx +1 -7
  98. package/src/assets/svgIcon/IconPdf.tsx +26 -0
  99. package/src/assets/svgIcon/IconThinkingStep.tsx +1 -7
  100. package/src/components/chat/ChatMessageList.tsx +3 -6
  101. package/src/components/chat/footer/index.tsx +410 -0
  102. package/src/components/chat/footer/item/UploadFileItem.tsx +181 -0
  103. package/src/components/chat/footer/item/UploadImageItem.tsx +91 -0
  104. package/src/components/chat/index.tsx +1 -1
  105. package/src/components/chat/item/ChatAIAnswerMessageItem.tsx +118 -38
  106. package/src/components/chat/item/ChatUserMessageItem.tsx +145 -13
  107. package/src/components/portal/Toast.tsx +315 -0
  108. package/src/components/portal/index.tsx +13 -0
  109. package/src/constants/index.ts +9 -0
  110. package/src/context/ChatContext.tsx +6 -2
  111. package/src/hooks/message/useMessage.ts +0 -1
  112. package/src/hooks/message/useSendMessage.ts +4 -4
  113. package/src/hooks/upload/useFileUpload.ts +126 -0
  114. package/src/hooks/upload/useImageUpload.ts +123 -0
  115. package/src/hooks/useAndroidBackHandler.ts +29 -0
  116. package/src/services/endpoints.ts +3 -0
  117. package/src/types/chat.ts +2 -0
  118. package/src/types/dto.ts +16 -1
  119. package/src/types/index.ts +1 -0
  120. package/src/types/ui.ts +12 -0
  121. package/src/utils/common.ts +40 -0
  122. package/src/utils/device.ts +170 -0
  123. package/src/utils/ui.tsx +32 -0
  124. package/lib/module/components/chat/ChatFooter.js +0 -91
  125. package/lib/module/components/chat/ChatFooter.js.map +0 -1
  126. package/lib/typescript/src/components/chat/ChatFooter.d.ts +0 -3
  127. package/lib/typescript/src/components/chat/ChatFooter.d.ts.map +0 -1
  128. package/src/components/chat/ChatFooter.tsx +0 -99
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-chatbot-ai",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@commitlint/config-conventional": "^19.8.1",
63
- "@droppii/libs": "git+ssh://git@github.com:droppii/mobile-components.git#v1.0.80",
63
+ "@droppii/libs": "git+ssh://git@github.com:droppii/mobile-components.git#v1.0.81",
64
64
  "@eslint/compat": "^1.3.2",
65
65
  "@eslint/eslintrc": "^3.3.1",
66
66
  "@eslint/js": "^9.35.0",
@@ -69,6 +69,7 @@
69
69
  "@react-native/eslint-config": "^0.81.1",
70
70
  "@release-it/conventional-changelog": "^10.0.1",
71
71
  "@types/jest": "^29.5.14",
72
+ "@types/lodash": "^4.17.20",
72
73
  "@types/react": "^19.1.12",
73
74
  "axios": "^1.12.2",
74
75
  "commitlint": "^19.8.1",
@@ -81,6 +82,7 @@
81
82
  "lodash": "^4.17.21",
82
83
  "prettier": "^3.6.2",
83
84
  "react-native-builder-bob": "^0.40.13",
85
+ "react-native-image-crop-picker": "^0.51.1",
84
86
  "react-native-marked": "^7.0.2",
85
87
  "react-native-sse": "^1.2.1",
86
88
  "release-it": "^19.0.4",
@@ -91,7 +93,8 @@
91
93
  "@droppii/libs": "*",
92
94
  "@tanstack/react-query": "*",
93
95
  "react": "*",
94
- "react-native": "*"
96
+ "react-native": "*",
97
+ "react-native-blob-util": "*"
95
98
  },
96
99
  "workspaces": [
97
100
  "example"
@@ -160,6 +163,8 @@
160
163
  },
161
164
  "dependencies": {
162
165
  "@babel/runtime": "^7.28.4",
166
+ "@react-native-documents/picker": "^11.0.0",
167
+ "@react-native-documents/viewer": "^2.0.0",
163
168
  "axios": "^1.12.2",
164
169
  "dayjs": "^1.11.18",
165
170
  "lodash": "^4.17.21",
@@ -10,13 +10,7 @@ interface Props {
10
10
  export const IconChatArrow = (props: Props) => {
11
11
  const { width = 6, height = 18, fill = '#07F', opacity = 0.1 } = props || {};
12
12
  return (
13
- <Svg
14
- width={width}
15
- height={height}
16
- viewBox="0 0 6 18"
17
- fill="none"
18
- {...props}
19
- >
13
+ <Svg width={width} height={height} viewBox="0 0 6 18" fill="none">
20
14
  <Path
21
15
  d="M4.024 0H0v18c.545-4.5 2.546-9.873 4.956-12.5C6.332 4 6.65 0 4.024 0z"
22
16
  fill={fill}
@@ -0,0 +1,26 @@
1
+ import Svg, { Path } from 'react-native-svg';
2
+
3
+ interface Props {
4
+ width?: number;
5
+ height?: number;
6
+ }
7
+
8
+ export const IconPdf = (props: Props) => {
9
+ const { width = 20, height = 20 } = props || {};
10
+ return (
11
+ <Svg width={width} height={height} viewBox="0 0 20 20" fill="none">
12
+ <Path
13
+ d="M2.5 0h10.62l5.63 5.608V18.75c0 .69-.56 1.25-1.25 1.25h-15c-.69 0-1.25-.56-1.25-1.25V1.25C1.25.56 1.81 0 2.5 0z"
14
+ fill="#E2574C"
15
+ />
16
+ <Path
17
+ d="M18.732 5.625h-4.357c-.69 0-1.25-.56-1.25-1.25V.013l5.607 5.612z"
18
+ fill="#B53629"
19
+ />
20
+ <Path
21
+ d="M12.314 9.642c.012-.19.09-.392.234-.607.147-.218.336-.386.566-.505.23-.12.45-.181.664-.185h1.513c.296-.004.476.123.542.38v.168c-.066.257-.246.384-.542.38h-1.267a.498.498 0 00-.32.15.448.448 0 00-.148.327v.618h1.625c.295-.004.475.123.541.381v.179c-.066.258-.246.385-.541.38h-1.624v1.548a.468.468 0 01-.148.334.512.512 0 01-.32.142h-.314a.506.506 0 01-.314-.143.468.468 0 01-.147-.333V9.642zM10.516 9.75a.448.448 0 00-.148-.328.499.499 0 00-.32-.149h-.344c-.143-.004-.221.048-.234.155v2.821c.013.107.09.159.234.155h.345a.512.512 0 00.32-.143.468.468 0 00.147-.333V9.75zM8.24 9.047c.004-.21.107-.38.308-.512.2-.134.447-.198.738-.19h.941c.258.004.502.065.732.184.23.12.416.288.56.506.147.215.227.417.24.607v2.393c-.013.19-.093.395-.24.613-.144.214-.33.38-.56.5-.23.119-.474.18-.732.184h-.941c-.291.008-.538-.053-.738-.184-.201-.135-.304-.308-.308-.518V9.047zM4.627 13.332a.506.506 0 01-.313-.143.468.468 0 01-.148-.333V9.63c.012-.19.09-.392.234-.607.147-.218.336-.386.566-.505.23-.12.45-.181.664-.185h.523c.258.004.502.065.732.184.23.12.416.288.56.506.148.215.228.417.24.607v.381c-.012.19-.092.395-.24.613-.144.215-.33.381-.56.5-.23.12-.47.18-.72.185h-.769v1.547a.468.468 0 01-.147.333.512.512 0 01-.32.143h-.302zm1.815-3.594a.448.448 0 00-.147-.328.499.499 0 00-.32-.149h-.111a.499.499 0 00-.32.15.448.448 0 00-.148.327v.63h.579a.51.51 0 00.32-.142.468.468 0 00.147-.334v-.154z"
22
+ fill="#fff"
23
+ />
24
+ </Svg>
25
+ );
26
+ };
@@ -8,13 +8,7 @@ interface Props {
8
8
  export const IconThinkingStep = (props: Props) => {
9
9
  const { width = 24, height = 24 } = props || {};
10
10
  return (
11
- <Svg
12
- width={width}
13
- height={height}
14
- viewBox="0 0 24 24"
15
- fill="none"
16
- {...props}
17
- >
11
+ <Svg width={width} height={height} viewBox="0 0 24 24" fill="none">
18
12
  <Mask
19
13
  id="a"
20
14
  maskUnits="userSpaceOnUse"
@@ -14,12 +14,9 @@ const ChatMessageList = () => {
14
14
  const flatListRef = useRef<any>(null);
15
15
  const scrollOffsetRef = useRef(0);
16
16
 
17
- const onScroll = useCallback(
18
- (event: any) => {
19
- scrollOffsetRef.current = event?.nativeEvent?.contentOffset?.y;
20
- },
21
- []
22
- );
17
+ const onScroll = useCallback((event: any) => {
18
+ scrollOffsetRef.current = event?.nativeEvent?.contentOffset?.y;
19
+ }, []);
23
20
 
24
21
  useEffect(() => {
25
22
  const subExpandThinkingStep = DeviceEventEmitter.addListener(
@@ -0,0 +1,410 @@
1
+ import {
2
+ KButton,
3
+ KColors,
4
+ KContainer,
5
+ KImage,
6
+ KInput,
7
+ KLabel,
8
+ KRadiusValue,
9
+ KSpacingValue,
10
+ } from '@droppii/libs';
11
+ import { useCallback, useState, useRef, useMemo } from 'react';
12
+ import { FlatList, StyleSheet, TouchableWithoutFeedback } from 'react-native';
13
+ import debounce from 'lodash/debounce';
14
+ import { useSendMessage } from '../../../hooks/message/useSendMessage';
15
+ import useStreamMessageStore from '../../../store/streamMessage';
16
+ import {
17
+ Menu,
18
+ MenuTrigger,
19
+ MenuOption,
20
+ MenuOptions,
21
+ renderers,
22
+ } from 'react-native-popup-menu';
23
+ import {
24
+ openCameraPicker,
25
+ openDocumentPicker,
26
+ openImageMultiplePicker,
27
+ } from '../../../utils/device';
28
+ import type { Image } from 'react-native-image-crop-picker';
29
+ import UploadImageItem from './item/UploadImageItem';
30
+ import { isSameFile, shortenFileName } from '../../../utils/common';
31
+ import { DocumentPickerResponse, types } from '@react-native-documents/picker';
32
+ import UploadFileItem from './item/UploadFileItem';
33
+ import { IAttachment } from '../../../types';
34
+ import UIUtils from '../../../utils/ui';
35
+
36
+ const { Popover } = renderers;
37
+ export interface ImageUpload extends Image {
38
+ streamId?: string;
39
+ remoteUrl?: string;
40
+ uploadType: 'image';
41
+ }
42
+
43
+ export interface FileUpload extends DocumentPickerResponse {
44
+ streamId?: string;
45
+ remoteUrl?: string;
46
+ uploadType: 'file';
47
+ }
48
+
49
+ export type UploadItem = ImageUpload | FileUpload;
50
+
51
+ const MAX_FILE_UPLOAD = 3;
52
+ const MAX_FILE_SIZE = 30 * 1024 * 1024;
53
+ const MAX_IMAGE_SIZE = 7 * 1024 * 1024;
54
+
55
+ const ChatFooter = () => {
56
+ const menuRef = useRef(null);
57
+ const { onSendMessage, stopStream } = useSendMessage();
58
+ const [message, setMessage] = useState('');
59
+ const isStreaming = useStreamMessageStore((state) => state.isStreaming);
60
+ const [fileUpload, setFileUpload] = useState<UploadItem[]>([]);
61
+
62
+ const debouncedMessage = debounce((message: string) => {
63
+ setMessage(message);
64
+ }, 200);
65
+
66
+ const isDisabledSend = useMemo(() => {
67
+ return (
68
+ isStreaming ||
69
+ message.trim() === '' ||
70
+ (fileUpload.length > 0 && fileUpload.some((i) => !i.remoteUrl))
71
+ );
72
+ }, [isStreaming, message, fileUpload]);
73
+
74
+ const onPressImagePicker = useCallback(async () => {
75
+ if (
76
+ fileUpload.filter((i) => i.uploadType === 'image').length >=
77
+ MAX_FILE_UPLOAD
78
+ ) {
79
+ UIUtils.toast.open({
80
+ title: `Chỉ được gửi tối đa ${MAX_FILE_UPLOAD} hình ảnh`,
81
+ });
82
+ return;
83
+ }
84
+ const newImages =
85
+ (await openImageMultiplePicker({
86
+ maxFiles:
87
+ MAX_FILE_UPLOAD - fileUpload.length > 0
88
+ ? MAX_FILE_UPLOAD - fileUpload.length
89
+ : 1,
90
+ })) || [];
91
+ const uniqueImages = newImages
92
+ .filter((img) => !fileUpload.some((sel) => isSameFile(sel, img)))
93
+ .map((i) => ({ ...i, uploadType: 'image' }))
94
+ .slice(0, MAX_FILE_UPLOAD - fileUpload.length) as ImageUpload[];
95
+
96
+ if (uniqueImages?.length > 0) {
97
+ setFileUpload((prev) => {
98
+ const totalImages = prev.filter((i) => i.uploadType === 'image');
99
+ const totalPrevSize = totalImages.reduce(
100
+ (acc, item) => acc + (item.size || 0),
101
+ 0
102
+ );
103
+ const totalNewSize = uniqueImages.reduce(
104
+ (acc, item) => acc + (item.size || 0),
105
+ 0
106
+ );
107
+ if (totalPrevSize + totalNewSize > MAX_IMAGE_SIZE) {
108
+ UIUtils.toast.open({
109
+ title: `Tổng dung lượng ảnh tải lên có kích thước quá lớn:\n ${uniqueImages.map((i) => shortenFileName(i.filename || '')).join(',\n')}, \nVui lòng nén ảnh hoặc tải lên nhiều lần để tiếp tục.`,
110
+ theme: 'danger',
111
+ });
112
+ return prev;
113
+ }
114
+ return [...prev, ...uniqueImages];
115
+ });
116
+ }
117
+ }, [fileUpload]);
118
+
119
+ const onPressCameraPicker = useCallback(async () => {
120
+ if (
121
+ fileUpload.filter((i) => i.uploadType === 'image').length >=
122
+ MAX_FILE_UPLOAD
123
+ ) {
124
+ UIUtils.toast.open({
125
+ title: `Chỉ được gửi tối đa ${MAX_FILE_UPLOAD} hình ảnh`,
126
+ });
127
+ return;
128
+ }
129
+ const newImage = await openCameraPicker();
130
+ if (newImage) {
131
+ setFileUpload((prev) => {
132
+ const totalImages = prev.filter((i) => i.uploadType === 'image');
133
+ const totalPrevSize = totalImages.reduce(
134
+ (acc, item) => acc + (item.size || 0),
135
+ 0
136
+ );
137
+ const totalNewSize = newImage.size || 0;
138
+ if (totalPrevSize + totalNewSize > MAX_IMAGE_SIZE) {
139
+ UIUtils.toast.open({
140
+ title: `Tổng dung lượng ảnh tải lên có kích thước quá lớn:\n ${shortenFileName(newImage.filename || '')},\n Vui lòng nén ảnh hoặc tải lên nhiều lần để tiếp tục.`,
141
+ theme: 'danger',
142
+ });
143
+ return prev;
144
+ }
145
+ return [...prev, { ...newImage, uploadType: 'image' }];
146
+ });
147
+ }
148
+ }, [fileUpload]);
149
+
150
+ const menuItems = useMemo(() => {
151
+ return [
152
+ {
153
+ icon: 'image-o',
154
+ onPress: onPressImagePicker,
155
+ label: 'Thư viện ảnh',
156
+ },
157
+ {
158
+ icon: 'camera-o',
159
+ onPress: onPressCameraPicker,
160
+ label: 'Chụp ảnh',
161
+ },
162
+ ];
163
+ }, [onPressImagePicker, onPressCameraPicker]);
164
+
165
+ const onPressSend = useCallback(() => {
166
+ if (isStreaming) {
167
+ stopStream();
168
+ return;
169
+ }
170
+ const attachments: IAttachment[] = fileUpload.map((i) => ({
171
+ type: i.uploadType,
172
+ source_type: 'url',
173
+ data: i.remoteUrl || '',
174
+ name: (i.uploadType === 'image' ? i.filename : i.name) as string,
175
+ size: (i.size || null) as number,
176
+ mime_type: (i.uploadType === 'image'
177
+ ? i.mime
178
+ : i.type) as IAttachment['mime_type'],
179
+ }));
180
+
181
+ onSendMessage(message?.trim(), attachments);
182
+ setMessage('');
183
+ setFileUpload([]);
184
+ }, [message, isStreaming, onSendMessage, stopStream, fileUpload]);
185
+
186
+ const handleUploadSuccess = useCallback((item: UploadItem) => {
187
+ if (item.uploadType === 'image') {
188
+ setFileUpload((prev) => {
189
+ const next = [...prev];
190
+ const fileIndex = next.findIndex(
191
+ (i) => i.uploadType === 'image' && i?.path === item?.path
192
+ );
193
+ if (fileIndex > -1) {
194
+ next[fileIndex] = { ...next[fileIndex], ...item };
195
+ }
196
+ return next;
197
+ });
198
+ } else {
199
+ setFileUpload((prev) => {
200
+ const next = [...prev];
201
+ const fileIndex = next.findIndex(
202
+ (i) => i.uploadType === 'file' && i?.uri === item?.uri
203
+ );
204
+ if (fileIndex > -1) {
205
+ next[fileIndex] = { ...next[fileIndex], ...item };
206
+ }
207
+ return next;
208
+ });
209
+ }
210
+ }, []);
211
+
212
+ const handleRemoveUploadItem = useCallback((item: UploadItem) => {
213
+ if (item.uploadType === 'image') {
214
+ setFileUpload((prev) =>
215
+ prev.filter((i) => i.uploadType !== 'image' || i.path !== item.path)
216
+ );
217
+ } else {
218
+ setFileUpload((prev) =>
219
+ prev.filter((i) => i.uploadType !== 'file' || i.uri !== item.uri)
220
+ );
221
+ }
222
+ }, []);
223
+
224
+ const renderUploadItem = useCallback(
225
+ (item: UploadItem) => {
226
+ switch (item.uploadType) {
227
+ case 'image':
228
+ return (
229
+ <UploadImageItem
230
+ item={item}
231
+ onRemove={handleRemoveUploadItem}
232
+ onSuccess={handleUploadSuccess}
233
+ />
234
+ );
235
+ case 'file':
236
+ return (
237
+ <UploadFileItem
238
+ item={item}
239
+ onRemove={handleRemoveUploadItem}
240
+ onSuccess={handleUploadSuccess}
241
+ />
242
+ );
243
+ default:
244
+ return null;
245
+ }
246
+ },
247
+ [handleRemoveUploadItem, handleUploadSuccess]
248
+ );
249
+
250
+ const handlePressDocumentPicker = useCallback(async () => {
251
+ const listFiles = fileUpload.filter((i) => i.uploadType === 'file');
252
+ if (listFiles.length >= MAX_FILE_UPLOAD) {
253
+ UIUtils.toast.open({
254
+ title: `Chỉ được gửi tối đa ${MAX_FILE_UPLOAD} tệp`,
255
+ });
256
+ return;
257
+ }
258
+ const newDocument = await openDocumentPicker({
259
+ type: types.pdf,
260
+ });
261
+ const isUnique = listFiles.some(
262
+ (doc) => doc.uploadType === 'file' && doc.uri !== newDocument?.uri
263
+ );
264
+
265
+ if (!!newDocument && isUnique) {
266
+ setFileUpload((prev) => {
267
+ const totalPrevSize = prev.reduce(
268
+ (acc, item) => acc + (item.size || 0),
269
+ 0
270
+ );
271
+ const totalNewSize = newDocument.size || 0;
272
+ if (totalPrevSize + totalNewSize > MAX_FILE_SIZE) {
273
+ UIUtils.toast.open({
274
+ title: `Tổng dung lượng tệp tải lên có kích thước quá lớn:\n ${shortenFileName(
275
+ newDocument.name || ''
276
+ )},\n Vui lòng nén tệp hoặc tải lên nhiều lần để tiếp tục.`,
277
+ theme: 'danger',
278
+ });
279
+ return prev;
280
+ }
281
+ return [...prev, { ...newDocument, uploadType: 'file' }];
282
+ });
283
+ }
284
+ }, [fileUpload]);
285
+
286
+ return (
287
+ <KContainer.View style={styles.container}>
288
+ <KContainer.VisibleView visible={fileUpload.length > 0}>
289
+ <FlatList
290
+ data={fileUpload}
291
+ renderItem={({ item }: { item: UploadItem }) =>
292
+ renderUploadItem(item)
293
+ }
294
+ horizontal
295
+ keyExtractor={(item: UploadItem) =>
296
+ item.uploadType === 'image' ? item.path : item.uri
297
+ }
298
+ contentContainerStyle={styles.listUploadItem}
299
+ />
300
+ </KContainer.VisibleView>
301
+ <KInput.TextArea
302
+ paddingV={'0.25rem'}
303
+ paddingH={'0.25rem'}
304
+ placeholder="Bạn muốn hỏi gì hôm nay?"
305
+ clearButtonMode="hidden"
306
+ onChangeText={debouncedMessage}
307
+ value={message}
308
+ multiline
309
+ style={styles.input}
310
+ blurOnSubmit={false}
311
+ textAlignVertical="top"
312
+ />
313
+ <KContainer.View style={styles.actions}>
314
+ <Menu
315
+ ref={menuRef}
316
+ renderer={Popover}
317
+ rendererProps={{ placement: 'top', anchorStyle: { display: 'none' } }}
318
+ >
319
+ <MenuTrigger
320
+ customStyles={{
321
+ TriggerTouchableComponent: TouchableWithoutFeedback,
322
+ }}
323
+ >
324
+ <KImage.VectorIcons
325
+ name="image-o"
326
+ size={24}
327
+ color={KColors.gray.dark}
328
+ />
329
+ </MenuTrigger>
330
+ <MenuOptions optionsContainerStyle={styles.popover}>
331
+ {menuItems.map((i) => (
332
+ <MenuOption
333
+ key={i.icon}
334
+ onSelect={i.onPress}
335
+ style={styles.menuItem}
336
+ >
337
+ <KLabel.Text typo="TextMdMedium">{i.label}</KLabel.Text>
338
+ <KImage.VectorIcons name={i.icon} />
339
+ </MenuOption>
340
+ ))}
341
+ </MenuOptions>
342
+ </Menu>
343
+ <KContainer.Touchable onPress={handlePressDocumentPicker}>
344
+ <KImage.VectorIcons
345
+ name="paperclip-o"
346
+ size={24}
347
+ color={KColors.gray.dark}
348
+ />
349
+ </KContainer.Touchable>
350
+ <KContainer.View flex />
351
+ <KButton.Solid
352
+ kind="primary"
353
+ icon={{
354
+ vectorName: isStreaming ? 'square-b' : 'send-b',
355
+ size: 20,
356
+ tintColor: KColors.white,
357
+ }}
358
+ onPress={onPressSend}
359
+ br="round"
360
+ disabled={isDisabledSend}
361
+ />
362
+ </KContainer.View>
363
+ </KContainer.View>
364
+ );
365
+ };
366
+
367
+ export default ChatFooter;
368
+
369
+ const styles = StyleSheet.create({
370
+ container: {
371
+ paddingHorizontal: KSpacingValue['0.75rem'],
372
+ paddingVertical: KSpacingValue['0.5rem'],
373
+ gap: KSpacingValue['0.5rem'],
374
+ borderWidth: 1,
375
+ borderColor: KColors.hexToRgba(KColors.black, 0.15),
376
+ borderBottomWidth: 0,
377
+ borderTopLeftRadius: KSpacingValue['1.25rem'],
378
+ borderTopRightRadius: KSpacingValue['1.25rem'],
379
+ },
380
+ actions: {
381
+ flexDirection: 'row',
382
+ alignItems: 'center',
383
+ gap: KSpacingValue['1rem'],
384
+ },
385
+ sendButton: {
386
+ alignSelf: 'flex-end',
387
+ },
388
+ input: {
389
+ maxHeight: 100,
390
+ },
391
+ popover: {
392
+ borderRadius: KRadiusValue['4x'],
393
+ backgroundColor: KColors.white,
394
+ width: 200,
395
+ },
396
+ menuItem: {
397
+ paddingHorizontal: KSpacingValue['1rem'],
398
+ flexDirection: 'row',
399
+ alignItems: 'center',
400
+ paddingVertical: KSpacingValue['0.75rem'],
401
+ borderBottomWidth: 1,
402
+ borderBottomColor: KColors.border.normal,
403
+ gap: KSpacingValue['0.5rem'],
404
+ justifyContent: 'space-between',
405
+ },
406
+ listUploadItem: {
407
+ gap: KSpacingValue['0.75rem'],
408
+ paddingTop: 6,
409
+ },
410
+ });