react-native-chatbot-ai 0.1.30 → 0.1.33

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 (41) hide show
  1. package/lib/module/components/chat/footer/index.js +32 -162
  2. package/lib/module/components/chat/footer/index.js.map +1 -1
  3. package/lib/module/components/chat/index.js +1 -1
  4. package/lib/module/components/chat/item/ChatAIThinkingMessageItem.js +13 -3
  5. package/lib/module/components/chat/item/ChatAIThinkingMessageItem.js.map +1 -1
  6. package/lib/module/components/chat/item/ChatTable.js +6 -1
  7. package/lib/module/components/chat/item/ChatTable.js.map +1 -1
  8. package/lib/module/hooks/upload/useUploadItem.js +171 -0
  9. package/lib/module/hooks/upload/useUploadItem.js.map +1 -0
  10. package/lib/module/store/streamMessage.js +6 -0
  11. package/lib/module/store/streamMessage.js.map +1 -1
  12. package/lib/module/translation/resources/i18n.js +12 -1
  13. package/lib/module/translation/resources/i18n.js.map +1 -1
  14. package/lib/typescript/src/components/chat/footer/index.d.ts +0 -13
  15. package/lib/typescript/src/components/chat/footer/index.d.ts.map +1 -1
  16. package/lib/typescript/src/components/chat/footer/item/UploadFileItem.d.ts +1 -1
  17. package/lib/typescript/src/components/chat/footer/item/UploadFileItem.d.ts.map +1 -1
  18. package/lib/typescript/src/components/chat/footer/item/UploadImageItem.d.ts +1 -1
  19. package/lib/typescript/src/components/chat/footer/item/UploadImageItem.d.ts.map +1 -1
  20. package/lib/typescript/src/components/chat/item/ChatAIThinkingMessageItem.d.ts.map +1 -1
  21. package/lib/typescript/src/components/chat/item/ChatTable.d.ts.map +1 -1
  22. package/lib/typescript/src/hooks/upload/useFileUpload.d.ts +1 -1
  23. package/lib/typescript/src/hooks/upload/useFileUpload.d.ts.map +1 -1
  24. package/lib/typescript/src/hooks/upload/useImageUpload.d.ts +1 -1
  25. package/lib/typescript/src/hooks/upload/useImageUpload.d.ts.map +1 -1
  26. package/lib/typescript/src/hooks/upload/useUploadItem.d.ts +27 -0
  27. package/lib/typescript/src/hooks/upload/useUploadItem.d.ts.map +1 -0
  28. package/lib/typescript/src/store/streamMessage.d.ts.map +1 -1
  29. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  30. package/package.json +2 -2
  31. package/src/components/chat/footer/index.tsx +35 -209
  32. package/src/components/chat/footer/item/UploadFileItem.tsx +1 -1
  33. package/src/components/chat/footer/item/UploadImageItem.tsx +1 -1
  34. package/src/components/chat/index.tsx +1 -1
  35. package/src/components/chat/item/ChatAIThinkingMessageItem.tsx +13 -1
  36. package/src/components/chat/item/ChatTable.tsx +8 -1
  37. package/src/hooks/upload/useFileUpload.ts +1 -1
  38. package/src/hooks/upload/useImageUpload.ts +1 -1
  39. package/src/hooks/upload/useUploadItem.ts +220 -0
  40. package/src/store/streamMessage.ts +6 -0
  41. package/src/translation/resources/i18n.ts +11 -0
@@ -42,7 +42,11 @@ const ChatTable: React.FC<TableProps> = ({
42
42
  >
43
43
  {toArray(cell).map((child, k) => (
44
44
  <React.Fragment key={`header-cell-${j}-${k}`}>
45
- {child}
45
+ {React.isValidElement(child)
46
+ ? React.cloneElement(child as React.ReactElement<any>, {
47
+ style: styles.headerText,
48
+ })
49
+ : child}
46
50
  </React.Fragment>
47
51
  ))}
48
52
  </KContainer.View>
@@ -113,4 +117,7 @@ const styles = StyleSheet.create({
113
117
  firstCell: {
114
118
  width: 160,
115
119
  },
120
+ headerText: {
121
+ fontWeight: 'bold',
122
+ },
116
123
  });
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import ReactNativeBlobUtil from 'react-native-blob-util';
3
- import { FileUpload } from '../../components/chat/footer';
3
+ import { FileUpload } from '../../hooks/upload/useUploadItem';
4
4
  import { useChatContext } from '../../context/ChatContext';
5
5
  import { ENDPOINTS } from '../../services/endpoints';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import ReactNativeBlobUtil from 'react-native-blob-util';
3
- import { ImageUpload } from '../../components/chat/footer';
3
+ import { ImageUpload } from '../../hooks/upload/useUploadItem';
4
4
  import { useChatContext } from '../../context/ChatContext';
5
5
  import { ENDPOINTS } from '../../services/endpoints';
6
6
 
@@ -0,0 +1,220 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { Keyboard } from 'react-native';
3
+ import { DocumentPickerResponse, types } from '@react-native-documents/picker';
4
+ import { Image } from 'react-native-image-crop-picker';
5
+ import UIUtils from '../../utils/ui';
6
+ import { isSameFile, shortenFileName } from '../../utils/common';
7
+ import { trans } from '../../translation';
8
+ import {
9
+ openCameraPicker,
10
+ openDocumentPicker,
11
+ openImageMultiplePicker,
12
+ } from '../../utils/device';
13
+ import { GAEvents } from '../../constants/events';
14
+ import useSessionStore from '../../store/session';
15
+
16
+ export interface ImageUpload extends Image {
17
+ streamId?: string;
18
+ remoteUrl?: string;
19
+ uploadType: 'image';
20
+ }
21
+
22
+ export interface FileUpload extends DocumentPickerResponse {
23
+ streamId?: string;
24
+ remoteUrl?: string;
25
+ uploadType: 'file';
26
+ }
27
+
28
+ export type UploadItem = ImageUpload | FileUpload;
29
+
30
+ const MAX_FILE_UPLOAD = 3;
31
+ const MAX_FILE_SIZE = 30 * 1024 * 1024;
32
+ const MAX_IMAGE_SIZE = 7 * 1024 * 1024;
33
+
34
+ type Props = {
35
+ logGA: (event: string, params?: any) => void;
36
+ };
37
+
38
+ export const useUploadItem = ({ logGA }: Props) => {
39
+ const [documentUpload, setDocumentUpload] = useState<FileUpload[]>([]);
40
+ const [imageUpload, setImageUpload] = useState<ImageUpload[]>([]);
41
+
42
+ const addImages = useCallback((newImages: Image[]) => {
43
+ setImageUpload((prev) => {
44
+ const totalPrevSize = prev.reduce(
45
+ (acc, item) => acc + (item.size || 0),
46
+ 0
47
+ );
48
+
49
+ const uniqueImages = newImages
50
+ .filter((img) => !prev.some((sel) => isSameFile(sel, img)))
51
+ .map((i) => ({ ...i, uploadType: 'image' }))
52
+ .slice(0, MAX_FILE_UPLOAD - prev.length) as ImageUpload[];
53
+
54
+ const totalNewSize = uniqueImages.reduce(
55
+ (acc, item) => acc + (item.size || 0),
56
+ 0
57
+ );
58
+
59
+ if (totalPrevSize + totalNewSize > MAX_IMAGE_SIZE) {
60
+ UIUtils.toast.open({
61
+ title: trans('images_too_large', {
62
+ name: uniqueImages
63
+ .map((i) => shortenFileName(i.filename || ''))
64
+ .join(',\n'),
65
+ }),
66
+ theme: 'danger',
67
+ });
68
+ return prev;
69
+ }
70
+ return [...prev, ...uniqueImages];
71
+ });
72
+ }, []);
73
+
74
+ const addFile = useCallback((newDocument: DocumentPickerResponse) => {
75
+ setDocumentUpload((prev) => {
76
+ const newFileIndex = prev.findIndex(
77
+ (doc) => doc.uri === newDocument?.uri
78
+ );
79
+
80
+ if (newFileIndex !== -1) {
81
+ return prev;
82
+ }
83
+
84
+ const totalPrevSize = prev.reduce(
85
+ (acc, item) => acc + (item.size || 0),
86
+ 0
87
+ );
88
+
89
+ const totalNewSize = newDocument.size || 0;
90
+
91
+ if (totalPrevSize + totalNewSize > MAX_FILE_SIZE) {
92
+ UIUtils.toast.open({
93
+ title: trans('file_too_large', {
94
+ name: shortenFileName(newDocument.name || ''),
95
+ }),
96
+ theme: 'danger',
97
+ });
98
+ return prev;
99
+ }
100
+ return [...prev, { ...newDocument, uploadType: 'file' }];
101
+ });
102
+ }, []);
103
+
104
+ const onPressImagePicker = useCallback(async () => {
105
+ Keyboard.dismiss();
106
+ if (imageUpload.length >= MAX_FILE_UPLOAD) {
107
+ UIUtils.toast.open({
108
+ title: trans('max_image_reached', { number: MAX_FILE_UPLOAD }),
109
+ });
110
+ return;
111
+ }
112
+
113
+ const newImages =
114
+ (await openImageMultiplePicker({
115
+ maxFiles:
116
+ MAX_FILE_UPLOAD - imageUpload.length > 0
117
+ ? MAX_FILE_UPLOAD - imageUpload.length
118
+ : 1,
119
+ })) || [];
120
+
121
+ addImages(newImages);
122
+
123
+ logGA(GAEvents.chatDetailMultimodalTap, {
124
+ conversation_id: useSessionStore.getState().sessionId,
125
+ button: 'image',
126
+ });
127
+ }, [imageUpload, addImages, logGA]);
128
+
129
+ const onPressCameraPicker = useCallback(async () => {
130
+ Keyboard.dismiss();
131
+ if (imageUpload.length >= MAX_FILE_UPLOAD) {
132
+ UIUtils.toast.open({
133
+ title: trans('max_image_reached', { number: MAX_FILE_UPLOAD }),
134
+ });
135
+ return;
136
+ }
137
+ const newImage = await openCameraPicker();
138
+ if (newImage) {
139
+ addImages([newImage]);
140
+ }
141
+
142
+ logGA(GAEvents.chatDetailMultimodalTap, {
143
+ conversation_id: useSessionStore.getState().sessionId,
144
+ button: 'image',
145
+ });
146
+ }, [imageUpload, addImages, logGA]);
147
+
148
+ const handlePressDocumentPicker = useCallback(async () => {
149
+ Keyboard.dismiss();
150
+ if (documentUpload.length >= MAX_FILE_UPLOAD) {
151
+ UIUtils.toast.open({
152
+ title: trans('max_file_reached', { number: MAX_FILE_UPLOAD }),
153
+ });
154
+ return;
155
+ }
156
+
157
+ const newDocument = await openDocumentPicker({
158
+ type: types.pdf,
159
+ });
160
+
161
+ if (newDocument) {
162
+ addFile(newDocument);
163
+ }
164
+
165
+ logGA(GAEvents.chatDetailMultimodalTap, {
166
+ conversation_id: useSessionStore.getState().sessionId,
167
+ button: 'file',
168
+ });
169
+ }, [documentUpload, addFile, logGA]);
170
+
171
+ const handleUploadSuccess = useCallback((item: UploadItem) => {
172
+ if (item.uploadType === 'image') {
173
+ setImageUpload((prev) => {
174
+ const next = [...prev];
175
+ const fileIndex = next.findIndex((i) => i?.path === item?.path);
176
+ if (fileIndex > -1) {
177
+ next[fileIndex] = { ...next[fileIndex], ...item };
178
+ }
179
+ return next;
180
+ });
181
+ } else {
182
+ setDocumentUpload((prev) => {
183
+ const next = [...prev];
184
+ const fileIndex = next.findIndex((i) => i?.uri === item?.uri);
185
+ if (fileIndex > -1) {
186
+ next[fileIndex] = { ...next[fileIndex], ...item };
187
+ }
188
+ return next;
189
+ });
190
+ }
191
+ }, []);
192
+
193
+ const handleRemoveUploadItem = useCallback((item: UploadItem) => {
194
+ if (item.uploadType === 'image') {
195
+ setImageUpload((prev) => prev.filter((i) => i?.path !== item?.path));
196
+ } else {
197
+ setDocumentUpload((prev) => prev.filter((i) => i?.uri !== item?.uri));
198
+ }
199
+ }, []);
200
+
201
+ const clearFileUpload = useCallback(() => {
202
+ setImageUpload([]);
203
+ setDocumentUpload([]);
204
+ }, []);
205
+
206
+ const fileUpload: UploadItem[] = useMemo(
207
+ () => [...imageUpload, ...documentUpload],
208
+ [documentUpload, imageUpload]
209
+ );
210
+
211
+ return {
212
+ fileUpload,
213
+ onPressImagePicker,
214
+ onPressCameraPicker,
215
+ handlePressDocumentPicker,
216
+ handleUploadSuccess,
217
+ handleRemoveUploadItem,
218
+ clearFileUpload,
219
+ };
220
+ };
@@ -1,6 +1,8 @@
1
1
  import { create } from 'zustand';
2
2
  import { StreamMessageStore } from '../types/chat';
3
3
  import { IMessageItem } from '../types';
4
+ import UIUtils from '../utils/ui';
5
+ import { trans } from '../translation';
4
6
 
5
7
  const useStreamMessageStore = create<StreamMessageStore>((set) => ({
6
8
  isStreaming: false,
@@ -27,6 +29,10 @@ const useStreamMessageStore = create<StreamMessageStore>((set) => ({
27
29
  if (state.eventSource) {
28
30
  console.log('🛑 Closing stream');
29
31
  state.eventSource.close();
32
+ UIUtils.toast.open({
33
+ theme: 'light',
34
+ title: trans('stop_stream_alert'),
35
+ });
30
36
  return { isStreaming: false, eventSource: undefined };
31
37
  }
32
38
  return {};
@@ -40,6 +40,17 @@ const i18nKey: Record<string, string> = {
40
40
  delete_session_success: 'Đã xoá phiên chat thành công',
41
41
 
42
42
  recently_chat_history: 'Lịch sử gần đây',
43
+ stop_stream_alert: 'Đã dừng tạo câu trả lời',
44
+ images_too_large: `
45
+ Tổng dung lượng ảnh tải lên có kích thước quá lớn:
46
+ {{name}}
47
+ Vui lòng nén ảnh hoặc tải lên nhiều lần để tiếp tục.`,
48
+ file_too_large: `
49
+ Tổng dung lượng tệp tải lên có kích thước quá lớn:
50
+ {{name}}
51
+ Vui lòng nén tệp hoặc tải lên nhiều lần để tiếp tục.`,
52
+ max_image_reached: 'Chỉ được gửi tối đa {{number}} hình ảnh',
53
+ max_file_reached: 'Chỉ được gửi tối đa {{number}} tệp',
43
54
  };
44
55
 
45
56
  export default {