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.
- package/lib/module/assets/svgIcon/IconChatArrow.js +0 -1
- package/lib/module/assets/svgIcon/IconChatArrow.js.map +1 -1
- package/lib/module/assets/svgIcon/IconPdf.js +27 -0
- package/lib/module/assets/svgIcon/IconPdf.js.map +1 -0
- package/lib/module/assets/svgIcon/IconThinkingStep.js +0 -1
- package/lib/module/assets/svgIcon/IconThinkingStep.js.map +1 -1
- package/lib/module/components/chat/ChatMessageList.js.map +1 -1
- package/lib/module/components/chat/footer/index.js +328 -0
- package/lib/module/components/chat/footer/index.js.map +1 -0
- package/lib/module/components/chat/footer/item/UploadFileItem.js +163 -0
- package/lib/module/components/chat/footer/item/UploadFileItem.js.map +1 -0
- package/lib/module/components/chat/footer/item/UploadImageItem.js +94 -0
- package/lib/module/components/chat/footer/item/UploadImageItem.js.map +1 -0
- package/lib/module/components/chat/index.js +1 -1
- package/lib/module/components/chat/index.js.map +1 -1
- package/lib/module/components/chat/item/ChatAIAnswerMessageItem.js +89 -28
- package/lib/module/components/chat/item/ChatAIAnswerMessageItem.js.map +1 -1
- package/lib/module/components/chat/item/ChatUserMessageItem.js +122 -15
- package/lib/module/components/chat/item/ChatUserMessageItem.js.map +1 -1
- package/lib/module/components/portal/Toast.js +193 -0
- package/lib/module/components/portal/Toast.js.map +1 -0
- package/lib/module/components/portal/index.js +15 -0
- package/lib/module/components/portal/index.js.map +1 -0
- package/lib/module/constants/index.js +9 -0
- package/lib/module/constants/index.js.map +1 -0
- package/lib/module/context/ChatContext.js +8 -5
- package/lib/module/context/ChatContext.js.map +1 -1
- package/lib/module/hooks/message/useMessage.js +0 -1
- package/lib/module/hooks/message/useMessage.js.map +1 -1
- package/lib/module/hooks/message/useSendMessage.js +3 -3
- package/lib/module/hooks/message/useSendMessage.js.map +1 -1
- package/lib/module/hooks/upload/useFileUpload.js +94 -0
- package/lib/module/hooks/upload/useFileUpload.js.map +1 -0
- package/lib/module/hooks/upload/useImageUpload.js +92 -0
- package/lib/module/hooks/upload/useImageUpload.js.map +1 -0
- package/lib/module/hooks/useAndroidBackHandler.js +20 -0
- package/lib/module/hooks/useAndroidBackHandler.js.map +1 -0
- package/lib/module/services/endpoints.js +3 -0
- package/lib/module/services/endpoints.js.map +1 -1
- package/lib/module/types/index.js +1 -0
- package/lib/module/types/index.js.map +1 -1
- package/lib/module/types/ui.js +4 -0
- package/lib/module/types/ui.js.map +1 -0
- package/lib/module/utils/common.js +31 -0
- package/lib/module/utils/common.js.map +1 -0
- package/lib/module/utils/device.js +137 -0
- package/lib/module/utils/device.js.map +1 -0
- package/lib/module/utils/ui.js +28 -0
- package/lib/module/utils/ui.js.map +1 -0
- package/lib/typescript/src/assets/svgIcon/IconChatArrow.d.ts.map +1 -1
- package/lib/typescript/src/assets/svgIcon/IconPdf.d.ts +7 -0
- package/lib/typescript/src/assets/svgIcon/IconPdf.d.ts.map +1 -0
- package/lib/typescript/src/assets/svgIcon/IconThinkingStep.d.ts.map +1 -1
- package/lib/typescript/src/components/chat/ChatMessageList.d.ts.map +1 -1
- package/lib/typescript/src/components/chat/footer/index.d.ts +16 -0
- package/lib/typescript/src/components/chat/footer/index.d.ts.map +1 -0
- package/lib/typescript/src/components/chat/footer/item/UploadFileItem.d.ts +9 -0
- package/lib/typescript/src/components/chat/footer/item/UploadFileItem.d.ts.map +1 -0
- package/lib/typescript/src/components/chat/footer/item/UploadImageItem.d.ts +9 -0
- package/lib/typescript/src/components/chat/footer/item/UploadImageItem.d.ts.map +1 -0
- package/lib/typescript/src/components/chat/item/ChatAIAnswerMessageItem.d.ts +9 -0
- package/lib/typescript/src/components/chat/item/ChatAIAnswerMessageItem.d.ts.map +1 -1
- package/lib/typescript/src/components/chat/item/ChatUserMessageItem.d.ts.map +1 -1
- package/lib/typescript/src/components/portal/Toast.d.ts +4 -0
- package/lib/typescript/src/components/portal/Toast.d.ts.map +1 -0
- package/lib/typescript/src/components/portal/index.d.ts +3 -0
- package/lib/typescript/src/components/portal/index.d.ts.map +1 -0
- package/lib/typescript/src/constants/index.d.ts +6 -0
- package/lib/typescript/src/constants/index.d.ts.map +1 -0
- package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/useMessage.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/useSendMessage.d.ts +2 -1
- package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/src/hooks/upload/useFileUpload.d.ts +15 -0
- package/lib/typescript/src/hooks/upload/useFileUpload.d.ts.map +1 -0
- package/lib/typescript/src/hooks/upload/useImageUpload.d.ts +15 -0
- package/lib/typescript/src/hooks/upload/useImageUpload.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useAndroidBackHandler.d.ts +4 -0
- package/lib/typescript/src/hooks/useAndroidBackHandler.d.ts.map +1 -0
- package/lib/typescript/src/services/endpoints.d.ts +3 -0
- package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
- package/lib/typescript/src/types/chat.d.ts +6 -0
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/types/dto.d.ts +10 -1
- package/lib/typescript/src/types/dto.d.ts.map +1 -1
- package/lib/typescript/src/types/index.d.ts +1 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/types/ui.d.ts +7 -0
- package/lib/typescript/src/types/ui.d.ts.map +1 -0
- package/lib/typescript/src/utils/common.d.ts +10 -0
- package/lib/typescript/src/utils/common.d.ts.map +1 -0
- package/lib/typescript/src/utils/device.d.ts +11 -0
- package/lib/typescript/src/utils/device.d.ts.map +1 -0
- package/lib/typescript/src/utils/ui.d.ts +12 -0
- package/lib/typescript/src/utils/ui.d.ts.map +1 -0
- package/package.json +8 -3
- package/src/assets/svgIcon/IconChatArrow.tsx +1 -7
- package/src/assets/svgIcon/IconPdf.tsx +26 -0
- package/src/assets/svgIcon/IconThinkingStep.tsx +1 -7
- package/src/components/chat/ChatMessageList.tsx +3 -6
- package/src/components/chat/footer/index.tsx +410 -0
- package/src/components/chat/footer/item/UploadFileItem.tsx +181 -0
- package/src/components/chat/footer/item/UploadImageItem.tsx +91 -0
- package/src/components/chat/index.tsx +1 -1
- package/src/components/chat/item/ChatAIAnswerMessageItem.tsx +118 -38
- package/src/components/chat/item/ChatUserMessageItem.tsx +145 -13
- package/src/components/portal/Toast.tsx +315 -0
- package/src/components/portal/index.tsx +13 -0
- package/src/constants/index.ts +9 -0
- package/src/context/ChatContext.tsx +6 -2
- package/src/hooks/message/useMessage.ts +0 -1
- package/src/hooks/message/useSendMessage.ts +4 -4
- package/src/hooks/upload/useFileUpload.ts +126 -0
- package/src/hooks/upload/useImageUpload.ts +123 -0
- package/src/hooks/useAndroidBackHandler.ts +29 -0
- package/src/services/endpoints.ts +3 -0
- package/src/types/chat.ts +2 -0
- package/src/types/dto.ts +16 -1
- package/src/types/index.ts +1 -0
- package/src/types/ui.ts +12 -0
- package/src/utils/common.ts +40 -0
- package/src/utils/device.ts +170 -0
- package/src/utils/ui.tsx +32 -0
- package/lib/module/components/chat/ChatFooter.js +0 -91
- package/lib/module/components/chat/ChatFooter.js.map +0 -1
- package/lib/typescript/src/components/chat/ChatFooter.d.ts +0 -3
- package/lib/typescript/src/components/chat/ChatFooter.d.ts.map +0 -1
- 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.
|
|
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.
|
|
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
|
-
|
|
19
|
-
|
|
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
|
+
});
|