react-native-chatbot-ai 0.1.27 → 0.1.29

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 (50) hide show
  1. package/lib/module/components/chat/footer/SuggestionsBar.js +208 -0
  2. package/lib/module/components/chat/footer/SuggestionsBar.js.map +1 -0
  3. package/lib/module/components/chat/footer/index.js +89 -153
  4. package/lib/module/components/chat/footer/index.js.map +1 -1
  5. package/lib/module/components/chat/item/ChatAIAnswerMessageItem.js +38 -19
  6. package/lib/module/components/chat/item/ChatAIAnswerMessageItem.js.map +1 -1
  7. package/lib/module/components/chat/item/ShimmerBlock.js +52 -0
  8. package/lib/module/components/chat/item/ShimmerBlock.js.map +1 -0
  9. package/lib/module/components/product/CardHorizontal.js +30 -9
  10. package/lib/module/components/product/CardHorizontal.js.map +1 -1
  11. package/lib/module/hooks/message/useSendMessage.js +0 -2
  12. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  13. package/lib/module/hooks/message/useStreamMessage.js +8 -21
  14. package/lib/module/hooks/message/useStreamMessage.js.map +1 -1
  15. package/lib/module/hooks/product/useSearchProduct.js +8 -5
  16. package/lib/module/hooks/product/useSearchProduct.js.map +1 -1
  17. package/lib/module/store/products.js +13 -4
  18. package/lib/module/store/products.js.map +1 -1
  19. package/lib/module/store/streamMessage.js +18 -0
  20. package/lib/module/store/streamMessage.js.map +1 -1
  21. package/lib/module/types/chat.js.map +1 -1
  22. package/lib/typescript/src/components/chat/footer/SuggestionsBar.d.ts +10 -0
  23. package/lib/typescript/src/components/chat/footer/SuggestionsBar.d.ts.map +1 -0
  24. package/lib/typescript/src/components/chat/footer/index.d.ts.map +1 -1
  25. package/lib/typescript/src/components/chat/item/ChatAIAnswerMessageItem.d.ts.map +1 -1
  26. package/lib/typescript/src/components/chat/item/ShimmerBlock.d.ts +9 -0
  27. package/lib/typescript/src/components/chat/item/ShimmerBlock.d.ts.map +1 -0
  28. package/lib/typescript/src/components/product/CardHorizontal.d.ts +2 -2
  29. package/lib/typescript/src/components/product/CardHorizontal.d.ts.map +1 -1
  30. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +0 -1
  31. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  32. package/lib/typescript/src/hooks/message/useStreamMessage.d.ts +0 -1
  33. package/lib/typescript/src/hooks/message/useStreamMessage.d.ts.map +1 -1
  34. package/lib/typescript/src/hooks/product/useSearchProduct.d.ts.map +1 -1
  35. package/lib/typescript/src/store/products.d.ts.map +1 -1
  36. package/lib/typescript/src/store/streamMessage.d.ts.map +1 -1
  37. package/lib/typescript/src/types/chat.d.ts +5 -1
  38. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  39. package/package.json +3 -1
  40. package/src/components/chat/footer/SuggestionsBar.tsx +247 -0
  41. package/src/components/chat/footer/index.tsx +90 -168
  42. package/src/components/chat/item/ChatAIAnswerMessageItem.tsx +56 -20
  43. package/src/components/chat/item/ShimmerBlock.tsx +60 -0
  44. package/src/components/product/CardHorizontal.tsx +333 -305
  45. package/src/hooks/message/useSendMessage.ts +1 -2
  46. package/src/hooks/message/useStreamMessage.ts +9 -24
  47. package/src/hooks/product/useSearchProduct.ts +10 -3
  48. package/src/store/products.ts +8 -2
  49. package/src/store/streamMessage.ts +13 -0
  50. package/src/types/chat.ts +5 -1
@@ -8,7 +8,8 @@ import {
8
8
  KRadiusValue,
9
9
  KSpacingValue,
10
10
  } from '@droppii/libs';
11
- import { useCallback, useState, useRef, useMemo } from 'react';
11
+ import SuggestionsBar from './SuggestionsBar';
12
+ import { useCallback, useState, useRef, useMemo, useEffect } from 'react';
12
13
  import type { ComponentType } from 'react';
13
14
  import {
14
15
  FlatList,
@@ -42,7 +43,6 @@ import UploadFileItem from './item/UploadFileItem';
42
43
  import {
43
44
  IAttachment,
44
45
  IMessageItem,
45
- ISuggestionItem,
46
46
  MessageType,
47
47
  SendActionLogType,
48
48
  } from '../../../types';
@@ -70,28 +70,22 @@ export type UploadItem = ImageUpload | FileUpload;
70
70
  const MAX_FILE_UPLOAD = 3;
71
71
  const MAX_FILE_SIZE = 30 * 1024 * 1024;
72
72
  const MAX_IMAGE_SIZE = 7 * 1024 * 1024;
73
- const COUNTDOWN_MS = 60000;
74
73
 
75
74
  interface IChatFooterProps {
76
75
  lastMessage?: IMessageItem;
77
76
  }
78
77
 
79
78
  const ChatFooter = ({ lastMessage }: IChatFooterProps) => {
80
- const countdownRef = useRef<NodeJS.Timeout | null>(null);
81
- const isInCountdownRef = useRef(false);
82
- const firstVisibleItemRef = useRef<ISuggestionItem | null>(null);
83
-
84
- const viewabilityConfig = useRef({ itemVisiblePercentThreshold: 10 }).current;
85
-
86
79
  const menuRef = useRef(null);
87
80
  const logGA = useChatContext().logGA;
88
- const { onSendMessage, stopStream } = useSendMessage();
81
+ const { onSendMessage } = useSendMessage();
89
82
  const [message, setMessage] = useState('');
90
83
  const isStreaming = useStreamMessageStore((state) => state.isStreaming);
84
+ const stopStream = useStreamMessageStore((state) => state.stopStream);
91
85
  const [fileUpload, setFileUpload] = useState<UploadItem[]>([]);
92
86
 
93
- const debouncedMessage = debounce((message: string) => {
94
- setMessage(message);
87
+ const debouncedMessage = debounce((m: string) => {
88
+ setMessage(m);
95
89
  }, 200);
96
90
 
97
91
  const isDisabledSend = useMemo(() => {
@@ -335,163 +329,101 @@ const ChatFooter = ({ lastMessage }: IChatFooterProps) => {
335
329
  });
336
330
  }, [fileUpload, logGA]);
337
331
 
338
- const onPressItemSuggestion = debounce(
339
- (item: ISuggestionItem, index: number) => {
340
- onSendMessage(
341
- item.content,
342
- undefined,
343
- SendActionLogType.promptSuggestion
344
- );
345
- logGA(GAEvents.chatDetailInConvPrmSuggTap, {
346
- conversation_id: useSessionStore.getState().sessionId,
347
- group_id: lastMessage?.group_suggestion_id,
348
- suggestion_id: item.suggestion_id,
349
- suggestion_content: item.content,
350
- suggestion_cate: item.category,
351
- order: index,
352
- });
353
- },
354
- 200
355
- );
356
-
357
- const renderSuggestionItem = useCallback(
358
- (item: ISuggestionItem, index: number) => {
359
- return (
360
- <KContainer.Touchable
361
- style={styles.suggestionItem}
362
- onPress={() => onPressItemSuggestion(item, index)}
363
- >
364
- <KLabel.Text typo="TextSmNormal">{item?.content || ''}</KLabel.Text>
365
- </KContainer.Touchable>
366
- );
367
- },
368
- [onPressItemSuggestion]
369
- );
370
-
371
- const onViewableItemsChanged = useRef(({ viewableItems }: any) => {
372
- if (viewableItems && viewableItems.length > 0) {
373
- firstVisibleItemRef.current = viewableItems[0].item;
374
- }
375
- }).current;
376
-
377
- const logFirstVisibleItem = useCallback(() => {
378
- if (!firstVisibleItemRef.current) return;
379
-
380
- logGA(GAEvents.chatDetailInConvPrmSuggScroll, {
381
- group_id: lastMessage?.group_suggestion_id,
382
- message_chat_id: lastMessage?.id,
383
- suggestion_id: firstVisibleItemRef.current.suggestion_id,
384
- });
385
-
386
- // Bắt đầu countdown
387
- isInCountdownRef.current = true;
388
- countdownRef.current = setTimeout(() => {
389
- isInCountdownRef.current = false;
390
- countdownRef.current = null;
391
- }, COUNTDOWN_MS);
392
- }, [lastMessage, logGA]);
393
-
394
- const handleMomentumEnd = useCallback(() => {
395
- if (!isInCountdownRef.current) {
396
- logFirstVisibleItem();
397
- }
398
- }, [logFirstVisibleItem]);
332
+ useEffect(() => {
333
+ return () => {
334
+ stopStream();
335
+ };
336
+ }, [stopStream]);
399
337
 
400
338
  return (
401
339
  <KContainer.View style={styles.container}>
402
340
  <KContainer.VisibleView visible={isShowSuggestions}>
403
- <FlatListComponent
404
- data={lastMessage?.suggestions || []}
405
- renderItem={({
406
- item,
407
- index,
408
- }: {
409
- item: ISuggestionItem;
410
- index: number;
411
- }) => renderSuggestionItem(item, index)}
412
- horizontal
413
- keyExtractor={(item: ISuggestionItem) => item.suggestion_id}
414
- contentContainerStyle={styles.suggessionContainer}
415
- onViewableItemsChanged={onViewableItemsChanged}
416
- viewabilityConfig={viewabilityConfig}
417
- scrollEventThrottle={16}
418
- onMomentumScrollEnd={handleMomentumEnd}
341
+ <SuggestionsBar
342
+ suggestions={lastMessage?.suggestions}
343
+ groupSuggestionId={lastMessage?.group_suggestion_id}
344
+ messageId={lastMessage?.id}
345
+ onSendMessage={onSendMessage}
419
346
  />
420
347
  </KContainer.VisibleView>
421
- <KContainer.VisibleView visible={fileUpload.length > 0}>
422
- <FlatListComponent
423
- data={fileUpload}
424
- renderItem={({ item }: { item: UploadItem }) =>
425
- renderUploadItem(item)
426
- }
427
- horizontal
428
- keyExtractor={(item: UploadItem) =>
429
- item.uploadType === 'image' ? item.path : item.uri
430
- }
431
- contentContainerStyle={styles.listUploadItem}
348
+ <KContainer.View style={styles.contentContainer}>
349
+ <KContainer.VisibleView visible={fileUpload.length > 0}>
350
+ <FlatListComponent
351
+ data={fileUpload}
352
+ renderItem={({ item }: { item: UploadItem }) =>
353
+ renderUploadItem(item)
354
+ }
355
+ horizontal
356
+ keyExtractor={(item: UploadItem) =>
357
+ item.uploadType === 'image' ? item.path : item.uri
358
+ }
359
+ contentContainerStyle={styles.listUploadItem}
360
+ />
361
+ </KContainer.VisibleView>
362
+ <KInput.TextArea
363
+ paddingV={0}
364
+ paddingH={'0.25rem'}
365
+ placeholder={trans('input_placeholder')}
366
+ clearButtonMode="hidden"
367
+ onChangeText={debouncedMessage}
368
+ value={message}
369
+ multiline
370
+ style={styles.input}
371
+ blurOnSubmit={false}
372
+ textAlignVertical="top"
432
373
  />
433
- </KContainer.VisibleView>
434
- <KInput.TextArea
435
- paddingV={0}
436
- paddingH={'0.25rem'}
437
- placeholder={trans('input_placeholder')}
438
- clearButtonMode="hidden"
439
- onChangeText={debouncedMessage}
440
- value={message}
441
- multiline
442
- style={styles.input}
443
- blurOnSubmit={false}
444
- textAlignVertical="top"
445
- />
446
- <KContainer.View style={styles.actions}>
447
- <Menu
448
- ref={menuRef}
449
- renderer={Popover}
450
- rendererProps={{ placement: 'top', anchorStyle: { display: 'none' } }}
451
- >
452
- <MenuTrigger
453
- customStyles={{
454
- TriggerTouchableComponent: TouchableWithoutFeedback,
374
+ <KContainer.View style={styles.actions}>
375
+ <Menu
376
+ ref={menuRef}
377
+ renderer={Popover}
378
+ rendererProps={{
379
+ placement: 'top',
380
+ anchorStyle: { display: 'none' },
455
381
  }}
456
382
  >
383
+ <MenuTrigger
384
+ customStyles={{
385
+ TriggerTouchableComponent: TouchableWithoutFeedback,
386
+ }}
387
+ >
388
+ <KImage.VectorIcons
389
+ name="image-o"
390
+ size={24}
391
+ color={KColors.gray.dark}
392
+ />
393
+ </MenuTrigger>
394
+ <MenuOptions optionsContainerStyle={styles.popover}>
395
+ {menuItems.map((i) => (
396
+ <MenuOption
397
+ key={i.icon}
398
+ onSelect={i.onPress}
399
+ style={styles.menuItem}
400
+ >
401
+ <KLabel.Text typo="TextMdMedium">{i.label}</KLabel.Text>
402
+ <KImage.VectorIcons name={i.icon} />
403
+ </MenuOption>
404
+ ))}
405
+ </MenuOptions>
406
+ </Menu>
407
+ <KContainer.Touchable onPress={handlePressDocumentPicker}>
457
408
  <KImage.VectorIcons
458
- name="image-o"
409
+ name="paperclip-o"
459
410
  size={24}
460
411
  color={KColors.gray.dark}
461
412
  />
462
- </MenuTrigger>
463
- <MenuOptions optionsContainerStyle={styles.popover}>
464
- {menuItems.map((i) => (
465
- <MenuOption
466
- key={i.icon}
467
- onSelect={i.onPress}
468
- style={styles.menuItem}
469
- >
470
- <KLabel.Text typo="TextMdMedium">{i.label}</KLabel.Text>
471
- <KImage.VectorIcons name={i.icon} />
472
- </MenuOption>
473
- ))}
474
- </MenuOptions>
475
- </Menu>
476
- <KContainer.Touchable onPress={handlePressDocumentPicker}>
477
- <KImage.VectorIcons
478
- name="paperclip-o"
479
- size={24}
480
- color={KColors.gray.dark}
413
+ </KContainer.Touchable>
414
+ <KContainer.View flex />
415
+ <KButton.Solid
416
+ kind="primary"
417
+ icon={{
418
+ vectorName: isStreaming ? 'square-b' : 'send-b',
419
+ size: 20,
420
+ tintColor: KColors.white,
421
+ }}
422
+ onPress={onPressSend}
423
+ br="round"
424
+ disabled={isDisabledSend}
481
425
  />
482
- </KContainer.Touchable>
483
- <KContainer.View flex />
484
- <KButton.Solid
485
- kind="primary"
486
- icon={{
487
- vectorName: isStreaming ? 'square-b' : 'send-b',
488
- size: 20,
489
- tintColor: KColors.white,
490
- }}
491
- onPress={onPressSend}
492
- br="round"
493
- disabled={isDisabledSend}
494
- />
426
+ </KContainer.View>
495
427
  </KContainer.View>
496
428
  </KContainer.View>
497
429
  );
@@ -501,7 +433,6 @@ export default ChatFooter;
501
433
 
502
434
  const styles = StyleSheet.create({
503
435
  container: {
504
- paddingHorizontal: KSpacingValue['0.75rem'],
505
436
  paddingVertical: KSpacingValue['0.5rem'],
506
437
  borderWidth: 1,
507
438
  borderColor: KColors.hexToRgba(KColors.black, 0.15),
@@ -510,6 +441,9 @@ const styles = StyleSheet.create({
510
441
  borderTopRightRadius: KSpacingValue['1.25rem'],
511
442
  backgroundColor: KColors.white,
512
443
  },
444
+ contentContainer: {
445
+ paddingHorizontal: KSpacingValue['0.75rem'],
446
+ },
513
447
  actions: {
514
448
  flexDirection: 'row',
515
449
  alignItems: 'center',
@@ -542,16 +476,4 @@ const styles = StyleSheet.create({
542
476
  paddingTop: 6,
543
477
  paddingBottom: 8,
544
478
  },
545
- suggessionContainer: {
546
- gap: KSpacingValue['0.25rem'],
547
- paddingBottom: 8,
548
- },
549
- suggestionItem: {
550
- backgroundColor: KColors.palette.gray.w25,
551
- borderRadius: KRadiusValue['4x'],
552
- height: 32,
553
- justifyContent: 'center',
554
- alignItems: 'center',
555
- paddingHorizontal: KSpacingValue['0.75rem'],
556
- },
557
479
  });
@@ -24,6 +24,7 @@ import { useChatContext } from '../../../context/ChatContext';
24
24
  import { trans } from '../../../translation';
25
25
  import MessageActionsBar from './MessageActionsBar';
26
26
  import useProductsStore from '../../../store/products';
27
+ import ShimmerBlock from './ShimmerBlock';
27
28
 
28
29
  interface ChatAIAnswerMessageItemProps {
29
30
  item: IMessageItem;
@@ -220,7 +221,11 @@ class CustomRenderer extends Renderer implements RendererInterface {
220
221
 
221
222
  text(text: string | ReactNode[]): ReactNode {
222
223
  return (
223
- <KLabel.Text key={this.getKey()} typo="TextMdNormal">
224
+ <KLabel.Text
225
+ key={this.getKey()}
226
+ typo="TextMdNormal"
227
+ color={KColors.black}
228
+ >
224
229
  {text}
225
230
  </KLabel.Text>
226
231
  );
@@ -228,7 +233,11 @@ class CustomRenderer extends Renderer implements RendererInterface {
228
233
 
229
234
  codespan(text: string): ReactNode {
230
235
  return (
231
- <KLabel.Text key={this.getKey()} typo="TextMdNormal">
236
+ <KLabel.Text
237
+ key={this.getKey()}
238
+ typo="TextMdNormal"
239
+ color={KColors.black}
240
+ >
232
241
  {text}
233
242
  </KLabel.Text>
234
243
  );
@@ -236,7 +245,12 @@ class CustomRenderer extends Renderer implements RendererInterface {
236
245
 
237
246
  strong(children: string | ReactNode[]): ReactNode {
238
247
  return (
239
- <KLabel.Text key={this.getKey()} typo="TextMdBold">
248
+ <KLabel.Text
249
+ key={this.getKey()}
250
+ typo="TextMdBold"
251
+ style={styles.mdStrong}
252
+ color={KColors.black}
253
+ >
240
254
  {children}
241
255
  </KLabel.Text>
242
256
  );
@@ -246,7 +260,8 @@ class CustomRenderer extends Renderer implements RendererInterface {
246
260
  <KLabel.Text
247
261
  key={this.getKey()}
248
262
  typo="TextMdNormal"
249
- style={{ fontStyle: 'italic' }}
263
+ style={styles.mdEm}
264
+ color={KColors.black}
250
265
  >
251
266
  {children}
252
267
  </KLabel.Text>
@@ -337,7 +352,7 @@ class CustomRenderer extends Renderer implements RendererInterface {
337
352
  <KLabel.Text
338
353
  typo="TextMdNormal"
339
354
  color={KColors.primary.normal}
340
- style={{ textDecorationLine: 'underline' }}
355
+ style={styles.mdLink}
341
356
  >
342
357
  {children}
343
358
  </KLabel.Text>
@@ -366,7 +381,12 @@ class CustomRenderer extends Renderer implements RendererInterface {
366
381
 
367
382
  heading(text: string | ReactNode[]): ReactNode {
368
383
  return (
369
- <KLabel.Text key={this.getKey()} typo="TextMdBold">
384
+ <KLabel.Text
385
+ key={this.getKey()}
386
+ typo="TextMdBold"
387
+ style={styles.mdHeading}
388
+ color={KColors.black}
389
+ >
370
390
  {text}
371
391
  </KLabel.Text>
372
392
  );
@@ -443,7 +463,7 @@ const ChatAIAnswerMessageItem = ({
443
463
  const images: string[] = [];
444
464
 
445
465
  productIds.forEach((productId) => {
446
- const product = products.find((p) => p.id === productId);
466
+ const product = products[productId];
447
467
 
448
468
  if (!product) {
449
469
  return;
@@ -460,19 +480,23 @@ const ChatAIAnswerMessageItem = ({
460
480
  return (
461
481
  <KContainer.View style={styles.container}>
462
482
  {blocks.map((chunk, index) => (
463
- <Markdown
464
- key={index}
465
- renderer={renderer}
466
- value={chunk}
467
- tokenizer={tokenizer}
468
- styles={markedStyles}
469
- flatListProps={{
470
- style: styles.flatList,
471
- removeClippedSubviews: false,
472
- windowSize: 50,
473
- initialNumToRender: 999,
474
- }}
475
- />
483
+ <ShimmerBlock
484
+ key={`chunk-${index}`}
485
+ enable={streamMessage && isStreaming}
486
+ >
487
+ <Markdown
488
+ renderer={renderer}
489
+ value={chunk}
490
+ tokenizer={tokenizer}
491
+ styles={markedStyles}
492
+ flatListProps={{
493
+ style: styles.flatList,
494
+ removeClippedSubviews: false,
495
+ windowSize: 50,
496
+ initialNumToRender: 999,
497
+ }}
498
+ />
499
+ </ShimmerBlock>
476
500
  ))}
477
501
 
478
502
  {/* Message Actions Bar */}
@@ -539,4 +563,16 @@ const styles = StyleSheet.create({
539
563
  borderColor: '#eee',
540
564
  borderBottomWidth: StyleSheet.hairlineWidth,
541
565
  },
566
+ mdStrong: {
567
+ fontWeight: 'bold',
568
+ },
569
+ mdEm: {
570
+ fontStyle: 'italic',
571
+ },
572
+ mdHeading: {
573
+ fontWeight: 'bold',
574
+ },
575
+ mdLink: {
576
+ textDecorationLine: 'underline',
577
+ },
542
578
  });
@@ -0,0 +1,60 @@
1
+ import { KColors } from '@droppii/libs';
2
+ import React, { useEffect, useRef } from 'react';
3
+ import { View, Animated, StyleSheet, useWindowDimensions } from 'react-native';
4
+
5
+ type Props = {
6
+ children: React.ReactNode;
7
+ duration?: number;
8
+ enable?: boolean;
9
+ };
10
+
11
+ export default function ShimmerBlock({
12
+ children,
13
+ duration = 1000,
14
+ enable = false,
15
+ }: Props) {
16
+ const translateX = useRef(new Animated.Value(0)).current;
17
+ const { width: screenWidth } = useWindowDimensions();
18
+
19
+ useEffect(() => {
20
+ if (!enable) return;
21
+ Animated.timing(translateX, {
22
+ toValue: screenWidth,
23
+ duration,
24
+ useNativeDriver: true,
25
+ }).start();
26
+ // eslint-disable-next-line react-hooks/exhaustive-deps
27
+ }, [screenWidth, duration, enable]);
28
+
29
+ return (
30
+ <View style={styles.container}>
31
+ {children}
32
+
33
+ <Animated.View
34
+ pointerEvents="none"
35
+ style={[
36
+ styles.shimmer,
37
+ {
38
+ transform: [{ translateX }],
39
+ },
40
+ ]}
41
+ />
42
+ </View>
43
+ );
44
+ }
45
+
46
+ const styles = StyleSheet.create({
47
+ container: {
48
+ position: 'relative',
49
+ overflow: 'hidden',
50
+ },
51
+ shimmer: {
52
+ position: 'absolute',
53
+ top: 0,
54
+ left: 0,
55
+ width: 24,
56
+ height: '100%',
57
+ backgroundColor: KColors.hexToRgba(KColors.white, 0.4),
58
+ borderRadius: 3,
59
+ },
60
+ });