react-native-chatbot-ai 0.1.1 → 0.1.3

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 +90 -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 +136 -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 +122 -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 +169 -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
@@ -0,0 +1,315 @@
1
+ /* eslint-disable react-native/no-inline-styles */
2
+ import {
3
+ ComponentType,
4
+ forwardRef,
5
+ memo,
6
+ useCallback,
7
+ useEffect,
8
+ useImperativeHandle,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from 'react';
13
+ // @ts-ignore
14
+ import { StyleSheet, StyleProp, ViewStyle } from 'react-native';
15
+ import Animated, {
16
+ FadeInDown,
17
+ FadeInUp,
18
+ FadeOutDown,
19
+ FadeOutUp,
20
+ useAnimatedStyle,
21
+ useSharedValue,
22
+ withSequence,
23
+ withSpring,
24
+ } from 'react-native-reanimated';
25
+ import get from 'lodash/get';
26
+ import { WithToastProps } from '../../types/ui';
27
+ import {
28
+ KButton,
29
+ KContainer,
30
+ KImage,
31
+ KLabel,
32
+ // @ts-ignore
33
+ KToastBarProps,
34
+ KSpacingValue,
35
+ KColors,
36
+ KDims,
37
+ } from '@droppii/libs';
38
+ import { Z_INDEX_PRIORITY } from '../../constants';
39
+
40
+ const Toast = forwardRef<WithToastProps>((_, ref) => {
41
+ const [data, setData] = useState<
42
+ // @ts-ignore
43
+ | (KToastBarProps & { contentContainerStyle?: StyleProp<ViewStyle> })
44
+ | undefined
45
+ >(undefined);
46
+ const scale = useSharedValue(1);
47
+ const containerRef = useRef(null);
48
+
49
+ const timeout = useRef<number>(undefined);
50
+
51
+ const dismiss = useCallback(() => {
52
+ setData(undefined);
53
+ }, []);
54
+
55
+ useImperativeHandle(
56
+ ref,
57
+ () => ({
58
+ open: (payload) => {
59
+ if (data) {
60
+ scale.value = withSequence(
61
+ withSpring(1.1, { mass: 0.3 }),
62
+ withSpring(1, { mass: 0.3 })
63
+ );
64
+ }
65
+ setData(payload);
66
+ },
67
+ dismiss,
68
+ }),
69
+ [dismiss, scale, data]
70
+ );
71
+
72
+ const {
73
+ type = 'full-fill',
74
+ theme = 'light',
75
+ position = 'bottom',
76
+ contentAlignment = 'center',
77
+ textAlign = 'center',
78
+ stretch,
79
+ action,
80
+ description,
81
+ icon,
82
+ showCloseButton,
83
+ title,
84
+ content,
85
+ onPressContent,
86
+ visibleTimeout = 3000,
87
+ br = '2x',
88
+ withTabBar = true,
89
+ contentContainerStyle,
90
+ } = data || {};
91
+
92
+ const colors = useMemo(() => {
93
+ const themeColor =
94
+ theme === 'light'
95
+ ? 'rgba(61,63,64,0.96)'
96
+ : get(KColors, `${theme}.normal`, KColors.primary.normal);
97
+
98
+ return {
99
+ backgroundColor:
100
+ theme === 'light'
101
+ ? themeColor
102
+ : type === 'revert'
103
+ ? KColors.white
104
+ : type === 'full-fill'
105
+ ? themeColor
106
+ : get(
107
+ KColors,
108
+ `palette.${theme}.w25`,
109
+ KColors.palette.primary.w50
110
+ ),
111
+ borderColor:
112
+ theme === 'light' || type !== 'revert' ? undefined : themeColor,
113
+ tintColor:
114
+ theme === 'light'
115
+ ? KColors.border.normal
116
+ : type === 'full-fill'
117
+ ? KColors.white
118
+ : themeColor,
119
+ textColor:
120
+ theme === 'light'
121
+ ? KColors.border.normal
122
+ : type === 'revert'
123
+ ? KColors.gray.dark
124
+ : type === 'full-fill'
125
+ ? KColors.white
126
+ : themeColor,
127
+ closeIconColor:
128
+ theme === 'light'
129
+ ? KColors.border.normal
130
+ : type === 'revert'
131
+ ? KColors.gray.light
132
+ : type === 'full-fill'
133
+ ? KColors.white
134
+ : themeColor,
135
+ actionRevert: type === 'full-fill',
136
+ };
137
+ }, [type, theme]);
138
+
139
+ // @ts-ignore
140
+ useEffect(() => {
141
+ if (data) {
142
+ // @ts-ignore
143
+ timeout.current = setTimeout(() => {
144
+ dismiss();
145
+ }, visibleTimeout);
146
+
147
+ return () => {
148
+ clearTimeout(timeout.current);
149
+ };
150
+ }
151
+ // eslint-disable-next-line react-hooks/exhaustive-deps
152
+ }, [data]);
153
+
154
+ const onPressWrapper = useCallback(
155
+ (onPress?: any) => {
156
+ onPress?.();
157
+ dismiss?.();
158
+ },
159
+ [dismiss]
160
+ );
161
+
162
+ const rStyle = useAnimatedStyle(() => ({
163
+ transform: [{ scale: scale.value }],
164
+ }));
165
+
166
+ const onPressContentWrapper = useCallback(() => {
167
+ onPressContent?.();
168
+ dismiss();
169
+ }, [onPressContent, dismiss]);
170
+
171
+ if (!data) {
172
+ return null;
173
+ }
174
+
175
+ const Wrapper = onPressContent ? KContainer.Touchable : KContainer.View;
176
+
177
+ return (
178
+ <Animated.View
179
+ style={[
180
+ styles.content,
181
+ rStyle,
182
+ position === 'top'
183
+ ? styles.top
184
+ : withTabBar
185
+ ? styles.bottom
186
+ : styles.bottomWithoutTabBar,
187
+ ]}
188
+ // @ts-ignore
189
+ ref={containerRef}
190
+ >
191
+ <Animated.View
192
+ entering={position === 'top' ? FadeInUp : FadeInDown}
193
+ exiting={position === 'top' ? FadeOutUp : FadeOutDown}
194
+ >
195
+ <Wrapper
196
+ onPress={onPressContentWrapper}
197
+ activeOpacity={0.9}
198
+ style={[
199
+ styles.innerContent,
200
+ {
201
+ backgroundColor: colors.backgroundColor,
202
+ borderWidth: colors.borderColor ? 1 : 0,
203
+ borderColor: colors.borderColor,
204
+ },
205
+ ]}
206
+ padding="1rem"
207
+ paddingH={stretch ? '1rem' : '2rem'}
208
+ br={br}
209
+ alignSelf={stretch ? 'stretch' : 'center'}
210
+ >
211
+ {icon && (
212
+ <KContainer.View>
213
+ <KImage.CommonIcon
214
+ {...icon}
215
+ size={
216
+ !icon.size || icon.size > 32
217
+ ? 32
218
+ : icon.size < 16
219
+ ? 16
220
+ : icon.size
221
+ }
222
+ style={styles.leftIcon}
223
+ tintColor={colors.tintColor}
224
+ />
225
+ </KContainer.View>
226
+ )}
227
+ <KContainer.View
228
+ justifyContent
229
+ alignItems={contentAlignment === 'center' ? 'center' : 'flex-start'}
230
+ flex={stretch ? 1 : 0}
231
+ style={contentContainerStyle}
232
+ >
233
+ {!!title && (
234
+ <KLabel.Text
235
+ color={colors.textColor}
236
+ typo="TextNmMedium"
237
+ textAlign={textAlign || 'center'}
238
+ >
239
+ {title}
240
+ </KLabel.Text>
241
+ )}
242
+ {!!description && (
243
+ <KLabel.Text
244
+ color={colors.textColor}
245
+ typo="TextSmNormal"
246
+ textAlign={textAlign || 'center'}
247
+ marginT="0.25rem"
248
+ >
249
+ {description}
250
+ </KLabel.Text>
251
+ )}
252
+ {content}
253
+ </KContainer.View>
254
+ {action && (
255
+ <KContainer.View style={styles.rightIcon}>
256
+ <KButton.Solid
257
+ kind={theme}
258
+ // revert={colors.actionRevert}
259
+ label={action.label}
260
+ onPress={onPressWrapper.bind(null, action.onPress)}
261
+ />
262
+ </KContainer.View>
263
+ )}
264
+ {showCloseButton && (
265
+ <KContainer.View style={styles.rightIcon}>
266
+ <KButton.Close
267
+ size={28}
268
+ color={colors.closeIconColor}
269
+ onPress={dismiss}
270
+ />
271
+ </KContainer.View>
272
+ )}
273
+ </Wrapper>
274
+ </Animated.View>
275
+ </Animated.View>
276
+ );
277
+ });
278
+
279
+ (Toast as ComponentType<any>).displayName = 'Portal.Toast';
280
+
281
+ export default memo(Toast);
282
+
283
+ const styles = StyleSheet.create({
284
+ content: {
285
+ position: 'absolute',
286
+ left: 0,
287
+ right: 0,
288
+ zIndex: Z_INDEX_PRIORITY.toast,
289
+ marginHorizontal: KSpacingValue['1rem'],
290
+ backgroundColor: KColors.transparent,
291
+ },
292
+ innerContent: {
293
+ flexDirection: 'row',
294
+ alignItems: 'center',
295
+ alignSelf: 'center',
296
+ },
297
+ leftIcon: {
298
+ marginRight: KSpacingValue['0.5rem'],
299
+ },
300
+ rightIcon: {
301
+ marginLeft: KSpacingValue['0.5rem'],
302
+ },
303
+ bottom: {
304
+ bottom: KSpacingValue['1.5rem'] + KDims.bottomTabHeightStatic,
305
+ },
306
+ bottomWithoutTabBar: {
307
+ bottom: KSpacingValue['1.5rem'],
308
+ },
309
+ top: {
310
+ top:
311
+ KDims.topBarSafeHeight > 0
312
+ ? KDims.topBarSafeHeight + 25
313
+ : KSpacingValue['2rem'],
314
+ },
315
+ });
@@ -0,0 +1,13 @@
1
+ import { memo } from 'react';
2
+ import ToastBar from './Toast';
3
+ import { toastRef } from '../../constants';
4
+
5
+ const Portal = memo(() => {
6
+ return (
7
+ <>
8
+ <ToastBar ref={toastRef} />
9
+ </>
10
+ );
11
+ });
12
+
13
+ export default Portal;
@@ -0,0 +1,9 @@
1
+ import { createRef } from 'react';
2
+ import { WithToastProps } from '../types';
3
+
4
+ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER + 100;
5
+
6
+ export const toastRef = createRef<WithToastProps>();
7
+ export const Z_INDEX_PRIORITY = {
8
+ toast: MAX_SAFE_INTEGER - 1,
9
+ };
@@ -1,6 +1,7 @@
1
1
  import { createContext, useContext } from 'react';
2
2
  import type { ChatContextType, ChatProviderProps } from '../types/chat';
3
3
  import ChatBotAI from '../components/chat';
4
+ import Portal from '../components/portal';
4
5
 
5
6
  export const ChatContext = createContext<ChatContextType>({
6
7
  apiAddress: '',
@@ -11,10 +12,13 @@ export const ChatContext = createContext<ChatContextType>({
11
12
  export const useChatContext = () => useContext(ChatContext);
12
13
 
13
14
  export const ChatProvider = (props: ChatProviderProps) => {
14
- const { apiAddress, userId, cartButton } = props;
15
+ const { apiAddress, userId, cartButton, openImageViewer } = props;
15
16
  return (
16
- <ChatContext.Provider value={{ apiAddress, userId, cartButton }}>
17
+ <ChatContext.Provider
18
+ value={{ apiAddress, userId, cartButton, openImageViewer }}
19
+ >
17
20
  <ChatBotAI />
21
+ <Portal />
18
22
  </ChatContext.Provider>
19
23
  );
20
24
  };
@@ -59,7 +59,6 @@ export const useMessage = () => {
59
59
  const filtered = messageItems
60
60
  .filter((m) => !existingIds.has(m?.id))
61
61
  .reverse();
62
- console.log('🚀 ~ useEffect ~ filtered:', prev.messages);
63
62
  return {
64
63
  ...prev,
65
64
  messages: [...filtered, ...prev.messages],
@@ -3,7 +3,7 @@ import useSessionStore from '../../store/session';
3
3
  import { useStreamMessage } from './useStreamMessage';
4
4
  import { events } from '../../constants/events';
5
5
  import { DeviceEventEmitter } from 'react-native';
6
- import { IMessageItem, MessageType, RoleType } from '../../types';
6
+ import { IAttachment, IMessageItem, MessageType, RoleType } from '../../types';
7
7
  import dayjs from 'dayjs';
8
8
  import { useCreateSession } from '../session/useCreateSession';
9
9
 
@@ -12,10 +12,10 @@ export const useSendMessage = () => {
12
12
  const { mutateAsync: createSession } = useCreateSession();
13
13
 
14
14
  const onSendMessage = useCallback(
15
- async (message: string) => {
15
+ async (message: string, attachments?: IAttachment[]) => {
16
16
  const messageItem: IMessageItem = {
17
17
  id: dayjs().valueOf().toString(),
18
- attachments: [],
18
+ attachments: attachments || [],
19
19
  type: MessageType.user_message,
20
20
  created_at: dayjs().toISOString(),
21
21
  modified_at: dayjs().toISOString(),
@@ -39,7 +39,7 @@ export const useSendMessage = () => {
39
39
  {
40
40
  content: message,
41
41
  stream: true,
42
- attachments: [],
42
+ attachments: attachments || [],
43
43
  metadata: {
44
44
  source: 'user',
45
45
  },
@@ -0,0 +1,122 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import ReactNativeBlobUtil from 'react-native-blob-util';
3
+ import { FileUpload } from '../../components/chat/footer';
4
+ import { useChatContext } from '../../context/ChatContext';
5
+ import { ENDPOINTS } from '../../services/endpoints';
6
+
7
+ type RNBlobTask = {
8
+ cancel?: () => void;
9
+ uploadProgress?: (
10
+ config: { interval?: number },
11
+ callback: (written: number, total: number) => void
12
+ ) => RNBlobTask;
13
+ then: Promise<any>['then'];
14
+ catch: Promise<any>['catch'];
15
+ };
16
+
17
+ interface UseFileUploadProps {
18
+ file: FileUpload;
19
+ onSuccess?: (data: any) => void;
20
+ onError?: (err: any) => void;
21
+ }
22
+
23
+ export const useFileUpload = ({
24
+ file,
25
+ onSuccess,
26
+ onError,
27
+ }: UseFileUploadProps) => {
28
+ const { apiAddress } = useChatContext();
29
+ const [progress, setProgress] = useState(0);
30
+ const [isUploading, setIsUploading] = useState(false);
31
+ const [isSuccess, setIsSuccess] = useState(false);
32
+ const [isError, setIsError] = useState(false);
33
+ const cancelRef = useRef<RNBlobTask | null>(null);
34
+
35
+ useEffect(() => {
36
+ let isMounted = false;
37
+ if (file && !file.remoteUrl) {
38
+ isMounted = true;
39
+ const doUpload = async () => {
40
+ try {
41
+ setIsUploading(true);
42
+ setIsError(false);
43
+ setIsSuccess(false);
44
+ setProgress(0);
45
+
46
+ const formData = [
47
+ {
48
+ name: 'files',
49
+ filename: file.name ?? `ai_file_${Date.now()}.pdf`,
50
+ type: file.type ?? 'application/pdf',
51
+ data: ReactNativeBlobUtil.wrap(
52
+ decodeURI(file.uri.replace('file://', ''))
53
+ ),
54
+ },
55
+ ];
56
+
57
+ const uploadUrl = `${apiAddress}${ENDPOINTS.uploaderService.upload}?path=chatbot-ai&isPublic=true`;
58
+
59
+ const token = (global as any)?.accessToken;
60
+ const task = ReactNativeBlobUtil.fetch(
61
+ 'POST',
62
+ uploadUrl,
63
+ {
64
+ 'Content-Type': 'multipart/form-data',
65
+ 'Authorization': `Bearer ${token}`,
66
+ },
67
+ formData
68
+ ) as unknown as RNBlobTask;
69
+
70
+ cancelRef.current = task;
71
+
72
+ task
73
+ .uploadProgress?.({ interval: 300 }, (written, total) => {
74
+ if (isMounted && total > 0) {
75
+ setProgress(Math.floor((written / total) * 100));
76
+ }
77
+ })
78
+ .then((res: any) => {
79
+ let response;
80
+ try {
81
+ response = JSON.parse(res.data);
82
+ } catch {
83
+ response = res.data;
84
+ }
85
+ if (!isMounted) return;
86
+ setIsUploading(false);
87
+ setProgress(100);
88
+ setIsSuccess(true);
89
+ onSuccess?.(response?.data);
90
+ })
91
+ .catch((err: any) => {
92
+ if (!isMounted) return;
93
+ setIsUploading(false);
94
+ setIsError(true);
95
+ onError?.(err);
96
+ });
97
+ } catch (err) {
98
+ if (!isMounted) return;
99
+ setIsUploading(false);
100
+ setIsError(true);
101
+ onError?.(err);
102
+ }
103
+ };
104
+
105
+ doUpload();
106
+ }
107
+
108
+ return () => {
109
+ isMounted = false;
110
+ cancelRef.current?.cancel?.();
111
+ };
112
+ // eslint-disable-next-line react-hooks/exhaustive-deps
113
+ }, [file.uri, apiAddress]);
114
+
115
+ return {
116
+ progress,
117
+ isUploading,
118
+ isSuccess,
119
+ isError,
120
+ cancel: () => cancelRef.current?.cancel?.(),
121
+ };
122
+ };
@@ -0,0 +1,123 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import ReactNativeBlobUtil from 'react-native-blob-util';
3
+ import { ImageUpload } from '../../components/chat/footer';
4
+ import { useChatContext } from '../../context/ChatContext';
5
+ import { ENDPOINTS } from '../../services/endpoints';
6
+
7
+ type RNBlobTask = {
8
+ cancel?: () => void;
9
+ uploadProgress?: (
10
+ config: { interval?: number },
11
+ callback: (written: number, total: number) => void
12
+ ) => RNBlobTask;
13
+ then: Promise<any>['then'];
14
+ catch: Promise<any>['catch'];
15
+ };
16
+
17
+ interface UseImageUploadProps {
18
+ file: ImageUpload;
19
+ onSuccess?: (data: any) => void;
20
+ onError?: (err: any) => void;
21
+ }
22
+
23
+ export const useImageUpload = ({
24
+ file,
25
+ onSuccess,
26
+ onError,
27
+ }: UseImageUploadProps) => {
28
+ const { apiAddress } = useChatContext();
29
+ const [progress, setProgress] = useState(0);
30
+ const [isUploading, setIsUploading] = useState(false);
31
+ const [isSuccess, setIsSuccess] = useState(false);
32
+ const [isError, setIsError] = useState(false);
33
+ const cancelRef = useRef<RNBlobTask | null>(null);
34
+
35
+ useEffect(() => {
36
+ let isMounted = false;
37
+ if (file && !file.remoteUrl) {
38
+ isMounted = true;
39
+ const doUpload = async () => {
40
+ try {
41
+ setIsUploading(true);
42
+ setIsError(false);
43
+ setIsSuccess(false);
44
+ setProgress(0);
45
+
46
+ const filePath = file.path?.replace('file://', '');
47
+ if (!filePath) throw new Error('File path not found');
48
+
49
+ const formData = [
50
+ {
51
+ name: 'files',
52
+ filename: file.filename ?? `ai_image_${Date.now()}.jpg`,
53
+ type: file.mime ?? 'image/jpeg',
54
+ data: ReactNativeBlobUtil.wrap(filePath),
55
+ },
56
+ ];
57
+
58
+ const uploadUrl = `${apiAddress}${ENDPOINTS.uploaderService.upload}?path=chatbot-ai&isPublic=true`;
59
+
60
+ const token = (global as any)?.accessToken;
61
+ const task = ReactNativeBlobUtil.fetch(
62
+ 'POST',
63
+ uploadUrl,
64
+ {
65
+ 'Content-Type': 'multipart/form-data',
66
+ 'Authorization': `Bearer ${token}`,
67
+ },
68
+ formData
69
+ ) as unknown as RNBlobTask;
70
+
71
+ cancelRef.current = task;
72
+
73
+ task
74
+ .uploadProgress?.({ interval: 300 }, (written, total) => {
75
+ if (isMounted && total > 0) {
76
+ setProgress(Math.floor((written / total) * 100));
77
+ }
78
+ })
79
+ .then((res: any) => {
80
+ let response;
81
+ try {
82
+ response = JSON.parse(res.data);
83
+ } catch {
84
+ response = res.data;
85
+ }
86
+ if (!isMounted) return;
87
+ setIsUploading(false);
88
+ setProgress(100);
89
+ setIsSuccess(true);
90
+ onSuccess?.(response?.data);
91
+ })
92
+ .catch((err: any) => {
93
+ if (!isMounted) return;
94
+ setIsUploading(false);
95
+ setIsError(true);
96
+ onError?.(err);
97
+ });
98
+ } catch (err) {
99
+ if (!isMounted) return;
100
+ setIsUploading(false);
101
+ setIsError(true);
102
+ onError?.(err);
103
+ }
104
+ };
105
+
106
+ doUpload();
107
+ }
108
+
109
+ return () => {
110
+ isMounted = false;
111
+ cancelRef.current?.cancel?.();
112
+ };
113
+ // eslint-disable-next-line react-hooks/exhaustive-deps
114
+ }, [file.path, apiAddress]);
115
+
116
+ return {
117
+ progress,
118
+ isUploading,
119
+ isSuccess,
120
+ isError,
121
+ cancel: () => cancelRef.current?.cancel?.(),
122
+ };
123
+ };
@@ -0,0 +1,29 @@
1
+ import { useFocusEffect } from '@react-navigation/native';
2
+ import { DependencyList, useCallback } from 'react';
3
+ import { BackHandler } from 'react-native';
4
+
5
+ const useAndroidBackHandler = (
6
+ callback?: () => any,
7
+ deps: DependencyList = []
8
+ ) => {
9
+ const callbackWrapper = useCallback(() => {
10
+ callback?.();
11
+ return true;
12
+ }, [callback]);
13
+
14
+ useFocusEffect(
15
+ useCallback(() => {
16
+ const sub = BackHandler.addEventListener(
17
+ 'hardwareBackPress',
18
+ callbackWrapper
19
+ );
20
+
21
+ return () => {
22
+ sub.remove();
23
+ };
24
+ // eslint-disable-next-line react-hooks/exhaustive-deps
25
+ }, [...deps, callbackWrapper])
26
+ );
27
+ };
28
+
29
+ export default useAndroidBackHandler;
@@ -7,4 +7,7 @@ export const ENDPOINTS = {
7
7
  `/assistant-service/v1/app/chat/sessions/${sessionId}/stream`,
8
8
  createSession: '/assistant-service/v1/app/chat/sessions',
9
9
  },
10
+ uploaderService: {
11
+ upload: '/uploader-service/v1/uploader/permanently',
12
+ },
10
13
  };
package/src/types/chat.ts CHANGED
@@ -5,12 +5,14 @@ export interface ChatContextType {
5
5
  apiAddress: string;
6
6
  userId: string;
7
7
  cartButton?: JSX.Element;
8
+ openImageViewer?: (images: { url: string }[], index: number) => void;
8
9
  }
9
10
 
10
11
  export interface ChatProviderProps {
11
12
  apiAddress: string;
12
13
  userId: string;
13
14
  cartButton?: JSX.Element;
15
+ openImageViewer?: (images: { url: string }[], index: number) => void;
14
16
  }
15
17
 
16
18
  export interface SessionStore {