app-tutor-ai-consumer 1.33.1 → 1.34.0

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 (125) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/config/vitest/__mocks__/sparkie.tsx +2 -2
  3. package/package.json +2 -2
  4. package/src/@types/index.d.ts +3 -2
  5. package/src/bootstrap.ts +40 -0
  6. package/src/config/tanstack/query-provider.tsx +7 -3
  7. package/src/config/tests/handlers.ts +5 -4
  8. package/src/config/theme/init-theme.ts +11 -5
  9. package/src/index.backup.tsx +61 -0
  10. package/src/index.tsx +80 -17
  11. package/src/lib/components/dropdown-actions/dropdown-actions.tsx +87 -0
  12. package/src/lib/components/dropdown-actions/dropdownActions.builder.ts +58 -0
  13. package/src/lib/components/dropdown-actions/dropdownActions.spec.tsx +76 -0
  14. package/src/lib/components/dropdown-actions/index.ts +1 -0
  15. package/src/lib/components/dropdown-actions/types.ts +16 -0
  16. package/src/lib/components/errors/generic/generic-error.tsx +11 -8
  17. package/src/lib/components/icons/document.svg +3 -0
  18. package/src/lib/components/icons/file.svg +3 -0
  19. package/src/lib/components/icons/icon-names.d.ts +7 -0
  20. package/src/lib/components/icons/image.svg +3 -0
  21. package/src/lib/components/icons/pdf.svg +3 -0
  22. package/src/lib/components/icons/plus.svg +3 -0
  23. package/src/lib/components/icons/retry.svg +3 -0
  24. package/src/lib/components/icons/spreadsheet.svg +3 -0
  25. package/src/lib/components/index.ts +1 -0
  26. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -3
  27. package/src/lib/hooks/index.ts +1 -0
  28. package/src/lib/hooks/use-chat-file-upload/constants.ts +11 -0
  29. package/src/lib/hooks/use-chat-file-upload/index.ts +1 -0
  30. package/src/lib/hooks/use-chat-file-upload/types.ts +14 -0
  31. package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.spec.ts +59 -0
  32. package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.ts +28 -0
  33. package/src/lib/hooks/use-click-outside/index.ts +1 -0
  34. package/src/lib/hooks/use-click-outside/use-click-outside.tsx +23 -0
  35. package/src/lib/hooks/use-click-outside/useClickOutside.spec.ts +102 -0
  36. package/src/lib/utils/index.ts +1 -0
  37. package/src/lib/utils/is-theme-dark.ts +21 -0
  38. package/src/main/hooks/use-initial-store/index.ts +1 -0
  39. package/src/main/hooks/use-initial-store/use-initial-store.tsx +64 -0
  40. package/src/main/hooks/use-initial-tab/index.ts +1 -0
  41. package/src/main/hooks/use-initial-tab/use-initial-tab.tsx +84 -0
  42. package/src/main/index.ts +1 -0
  43. package/src/main/main-content.tsx +14 -0
  44. package/src/main/main-wrapper.tsx +16 -0
  45. package/src/main/main.spec.tsx +5 -3
  46. package/src/main/main.tsx +7 -16
  47. package/src/main/types.ts +5 -0
  48. package/src/modules/global-providers/global-providers.tsx +2 -21
  49. package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +1 -1
  50. package/src/modules/messages/__tests__/signed-urls.builder.ts +42 -0
  51. package/src/modules/messages/components/chat-file-preview/chat-file-preview.builder.ts +121 -0
  52. package/src/modules/messages/components/chat-file-preview/chat-file-preview.spec.tsx +107 -0
  53. package/src/modules/messages/components/chat-file-preview/chat-file-preview.tsx +45 -0
  54. package/src/modules/messages/components/chat-file-preview/components/close-button/close-button.tsx +31 -0
  55. package/src/modules/messages/components/chat-file-preview/components/close-button/index.ts +1 -0
  56. package/src/modules/messages/components/chat-file-preview/components/close-button/types.ts +4 -0
  57. package/src/modules/messages/components/chat-file-preview/components/document-preview/document-preview.tsx +63 -0
  58. package/src/modules/messages/components/chat-file-preview/components/document-preview/index.ts +1 -0
  59. package/src/modules/messages/components/chat-file-preview/components/document-preview/types.ts +10 -0
  60. package/src/modules/messages/components/chat-file-preview/components/error-preview/error-preview.tsx +37 -0
  61. package/src/modules/messages/components/chat-file-preview/components/error-preview/index.ts +1 -0
  62. package/src/modules/messages/components/chat-file-preview/components/error-preview/types.ts +4 -0
  63. package/src/modules/messages/components/chat-file-preview/components/image-preview/image-preview.tsx +32 -0
  64. package/src/modules/messages/components/chat-file-preview/components/image-preview/index.ts +1 -0
  65. package/src/modules/messages/components/chat-file-preview/components/image-preview/types.ts +6 -0
  66. package/src/modules/messages/components/chat-file-preview/constants.ts +22 -0
  67. package/src/modules/messages/components/chat-file-preview/index.ts +1 -0
  68. package/src/modules/messages/components/chat-file-preview/types.ts +13 -0
  69. package/src/modules/messages/components/chat-file-preview/utils.spec.ts +38 -0
  70. package/src/modules/messages/components/chat-file-preview/utils.ts +13 -0
  71. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.builder.ts +19 -0
  72. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.spec.tsx +58 -0
  73. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.tsx +80 -0
  74. package/src/modules/messages/components/chat-file-uploader/index.ts +1 -0
  75. package/src/modules/messages/components/chat-file-uploader/types.ts +4 -0
  76. package/src/modules/messages/components/index.ts +1 -0
  77. package/src/modules/messages/constants.ts +2 -1
  78. package/src/modules/messages/hooks/use-get-signed-urls/index.ts +1 -0
  79. package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.spec.tsx +27 -0
  80. package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.tsx +38 -0
  81. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +1 -2
  82. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +3 -8
  83. package/src/modules/messages/hooks/use-prefetch-messages/index.ts +1 -0
  84. package/src/modules/messages/hooks/use-prefetch-messages/use-prefetch-messages.tsx +28 -0
  85. package/src/modules/messages/hooks/use-suspense-messages/index.ts +2 -0
  86. package/src/modules/messages/hooks/use-suspense-messages/types.ts +4 -0
  87. package/src/modules/messages/hooks/use-suspense-messages/use-suspense-messages.tsx +21 -0
  88. package/src/modules/messages/service.direct.ts +19 -1
  89. package/src/modules/messages/service.ts +1 -1
  90. package/src/modules/messages/store/messages-max-count.atom.ts +2 -2
  91. package/src/modules/messages/types.ts +15 -1
  92. package/src/modules/messages/utils/set-messages-cache/utils.ts +1 -1
  93. package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +7 -6
  94. package/src/modules/sparkie/__tests__/sparkie.mock.ts +1 -4
  95. package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +34 -28
  96. package/src/modules/sparkie/service.ts +1 -1
  97. package/src/modules/sparkie/store/index.ts +1 -0
  98. package/src/modules/sparkie/store/sparkie-state.atom.ts +13 -0
  99. package/src/modules/widget/__tests__/widget-settings-props.builder.ts +20 -1
  100. package/src/modules/widget/components/constants.tsx +3 -1
  101. package/src/modules/widget/components/error-page/error-page.spec.tsx +17 -0
  102. package/src/modules/widget/components/error-page/error-page.tsx +10 -0
  103. package/src/modules/widget/components/error-page/index.ts +1 -0
  104. package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
  105. package/src/modules/widget/components/starter-page/starter-page-actions/index.ts +1 -0
  106. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +68 -0
  107. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +34 -0
  108. package/src/modules/widget/components/starter-page/starter-page-content/index.ts +1 -0
  109. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +16 -0
  110. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +28 -0
  111. package/src/modules/widget/components/starter-page/starter-page-header/index.ts +1 -0
  112. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +45 -0
  113. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +36 -0
  114. package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
  115. package/src/modules/widget/components/starter-page/starter-page.tsx +15 -109
  116. package/src/modules/widget/hooks/index.ts +0 -1
  117. package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
  118. package/src/modules/widget/store/create-store.ts +7 -0
  119. package/src/modules/widget/store/index.ts +1 -0
  120. package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
  121. package/src/modules/widget/store/widget-tabs.atom.ts +17 -6
  122. package/src/types.ts +10 -0
  123. package/src/wrapper.tsx +52 -0
  124. package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
  125. package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +0 -56
@@ -0,0 +1,3 @@
1
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M13 16C13.5312 16 14 15.5625 14 15V7H10.5C9.65625 7 9 6.34375 9 5.5V2H5C4.4375 2 4 2.46875 4 3V15C4 15.5625 4.4375 16 5 16H13ZM10.5 6H13.9688C13.9375 5.9375 13.9062 5.84375 13.8438 5.78125L10.2188 2.15625C10.1562 2.09375 10.0625 2.0625 10 2.03125V5.5C10 5.78125 10.2188 6 10.5 6ZM5 1H9.875C10.25 1 10.6562 1.1875 10.9375 1.46875L14.5312 5.0625C14.8125 5.34375 15 5.75 15 6.125V15C15 16.125 14.0938 17 13 17H5C3.875 17 3 16.125 3 15V3C3 1.90625 3.875 1 5 1ZM6.5 9H11.5C11.75 9 12 9.25 12 9.5C12 9.78125 11.75 10 11.5 10H6.5C6.21875 10 6 9.78125 6 9.5C6 9.25 6.21875 9 6.5 9ZM6.5 11H11.5C11.75 11 12 11.25 12 11.5C12 11.7812 11.75 12 11.5 12H6.5C6.21875 12 6 11.7812 6 11.5C6 11.25 6.21875 11 6.5 11ZM6.5 13H11.5C11.75 13 12 13.25 12 13.5C12 13.7812 11.75 14 11.5 14H6.5C6.21875 14 6 13.7812 6 13.5C6 13.25 6.21875 13 6.5 13Z" fill="#F5F3EF"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M13 14.5V6.5H9.5C8.65625 6.5 8 5.84375 8 5V1.5H4C3.4375 1.5 3 1.96875 3 2.5V14.5C3 15.0625 3.4375 15.5 4 15.5H12C12.5312 15.5 13 15.0625 13 14.5ZM12.9688 5.5C12.9375 5.4375 12.9062 5.34375 12.8438 5.28125L9.21875 1.65625C9.15625 1.59375 9.0625 1.5625 9 1.53125V5C9 5.28125 9.21875 5.5 9.5 5.5H12.9688ZM2 2.5C2 1.40625 2.875 0.5 4 0.5H8.875C9.25 0.5 9.65625 0.6875 9.9375 0.96875L13.5312 4.5625C13.8125 4.84375 14 5.25 14 5.625V14.5C14 15.625 13.0938 16.5 12 16.5H4C2.875 16.5 2 15.625 2 14.5V2.5Z" fill="currentColor"/>
3
+ </svg>
@@ -6,22 +6,29 @@ export type ValidIconNames =
6
6
  | 'arrow-left'
7
7
  | 'arrow-up'
8
8
  | 'book'
9
+ | 'retry'
9
10
  | 'chevron-down'
10
11
  | 'clone'
11
12
  | 'close'
12
13
  | 'copy-solid'
13
14
  | 'copy'
15
+ | 'document'
14
16
  | 'double-check'
15
17
  | 'expand'
18
+ | 'file'
16
19
  | 'gallery'
20
+ | 'image'
17
21
  | 'info'
18
22
  | 'interrogation'
19
23
  | 'like-solid'
20
24
  | 'like'
21
25
  | 'paste'
26
+ | 'pdf'
27
+ | 'plus'
22
28
  | 'send'
23
29
  | 'sparkle-tutor-light'
24
30
  | 'sparkle-tutor'
31
+ | 'spreadsheet'
25
32
  | 'stop'
26
33
  | 'tutor-logo'
27
34
  | 'warning'
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M2 2.5C1.4375 2.5 1 2.96875 1 3.5V10.8125L3.09375 8.6875C3.59375 8.21875 4.375 8.21875 4.875 8.6875L7 10.8125L11.0938 6.6875C11.5938 6.21875 12.375 6.21875 12.875 6.6875L15 8.8125V3.5C15 2.96875 14.5312 2.5 14 2.5H2ZM1 12.2188V13.5C1 14.0625 1.4375 14.5 2 14.5H3.28125L6.28125 11.5L4.15625 9.40625C4.0625 9.3125 3.90625 9.3125 3.8125 9.40625L1 12.2188ZM12.1562 7.40625C12.0625 7.3125 11.9062 7.3125 11.8125 7.40625L4.6875 14.5H14C14.5312 14.5 15 14.0625 15 13.5V10.2188L12.1562 7.40625ZM0 3.5C0 2.40625 0.875 1.5 2 1.5H14C15.0938 1.5 16 2.40625 16 3.5V13.5C16 14.625 15.0938 15.5 14 15.5H2C0.875 15.5 0 14.625 0 13.5V3.5ZM5 5C5 4.75 4.75 4.5 4.5 4.5C4.21875 4.5 4 4.75 4 5C4 5.28125 4.21875 5.5 4.5 5.5C4.75 5.5 5 5.28125 5 5ZM3 5C3 4.46875 3.28125 4 3.75 3.71875C4.1875 3.4375 4.78125 3.4375 5.25 3.71875C5.6875 4 6 4.46875 6 5C6 5.5625 5.6875 6.03125 5.25 6.3125C4.78125 6.59375 4.1875 6.59375 3.75 6.3125C3.28125 6.03125 3 5.5625 3 5Z" fill="currentColor"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M14 15V7H10.5C9.65625 7 9 6.34375 9 5.5V2H5C4.4375 2 4 2.46875 4 3V15C4 15.5625 4.4375 16 5 16H13C13.5312 16 14 15.5625 14 15ZM13.9688 6C13.9375 5.9375 13.9062 5.84375 13.8438 5.78125L10.2188 2.15625C10.1562 2.09375 10.0625 2.0625 10 2.03125V5.5C10 5.78125 10.2188 6 10.5 6H13.9688ZM3 3C3 1.90625 3.875 1 5 1H9.875C10.25 1 10.6562 1.1875 10.9375 1.46875L14.5312 5.0625C14.8125 5.34375 15 5.75 15 6.125V15C15 16.125 14.0938 17 13 17H5C3.875 17 3 16.125 3 15V3Z" fill="#F5F3EF"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M7.5 1.17993V6.67993H13C13.25 6.67993 13.5 6.92993 13.5 7.17993C13.5 7.46118 13.25 7.67993 13 7.67993H7.5V13.1799C7.5 13.4612 7.25 13.6799 7 13.6799C6.71875 13.6799 6.5 13.4612 6.5 13.1799V7.67993H1C0.71875 7.67993 0.5 7.46118 0.5 7.17993C0.5 6.92993 0.71875 6.67993 1 6.67993H6.5V1.17993C6.5 0.929932 6.71875 0.679932 7 0.679932C7.25 0.679932 7.5 0.929932 7.5 1.17993Z" fill="currentColor"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="19" viewBox="0 0 14 19" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M12.5312 4.1875C12.6719 4.1875 12.8125 4.32812 12.8125 4.46875V8.21875C12.8125 8.38281 12.6719 8.5 12.5312 8.5H8.78125C8.61719 8.5 8.5 8.38281 8.5 8.21875V7.98438C8.5 7.84375 8.61719 7.70312 8.78125 7.70312H11.4531C10.6094 6.10938 8.92188 4.98438 7 4.98438C4.21094 4.98438 1.98438 7.25781 1.98438 10C1.98438 12.7891 4.23438 15.0156 7 15.0156C8.28906 15.0156 9.46094 14.5469 10.3516 13.75C10.4453 13.6328 10.6328 13.6562 10.7266 13.75L10.8906 13.9141C11.0078 14.0312 11.0078 14.2188 10.8906 14.3359C9.85938 15.25 8.47656 15.8125 7 15.8125C3.78906 15.8125 1.1875 13.2344 1.1875 10.0234C1.16406 6.83594 3.76562 4.21094 6.97656 4.21094C9.10938 4.1875 10.9844 5.35938 12.0156 7.07031V4.46875C12.0156 4.32812 12.1328 4.1875 12.2969 4.1875H12.5312Z" fill="currentColor"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M3 3C2.4375 3 2 3.46875 2 4V6H16V4C16 3.46875 15.5312 3 15 3H3ZM2 7V10.5H8.5V7H2ZM9.5 7V10.5H16V7H9.5ZM8.5 11.5H2V14C2 14.5625 2.4375 15 3 15H8.5V11.5ZM9.5 15H15C15.5312 15 16 14.5625 16 14V11.5H9.5V15ZM1 4C1 2.90625 1.875 2 3 2H15C16.0938 2 17 2.90625 17 4V14C17 15.125 16.0938 16 15 16H3C1.875 16 1 15.125 1 14V4Z" fill="#F5F3EF"/>
3
+ </svg>
@@ -1,4 +1,5 @@
1
1
  export * from './button'
2
+ export * from './dropdown-actions'
2
3
  export * from './errors'
3
4
  export * from './horizontal-draggable-scroll'
4
5
  export * from './icons'
@@ -71,9 +71,7 @@ const mdComponents: Partial<Components> = {
71
71
  },
72
72
  blockquote({ children, ...props }) {
73
73
  return (
74
- <blockquote
75
- className='my-2 border-l-4 border-primary-500 pl-4 italic text-neutral-100'
76
- {...props}>
74
+ <blockquote className='my-2 border-l-4 border-primary-500 pl-4 italic' {...props}>
77
75
  {children}
78
76
  </blockquote>
79
77
  )
@@ -1,3 +1,4 @@
1
+ export * from './use-click-outside'
1
2
  export * from './use-intersection-observer-reverse-scroll'
2
3
  export * from './use-media-query'
3
4
  export * from './use-ref-client-height'
@@ -0,0 +1,11 @@
1
+ export const FILE_TYPES = {
2
+ document: ['.doc', '.docx', '.txt'],
3
+ spreadsheet: ['.xls', '.xlsx', '.csv'],
4
+ pdf: ['.pdf'],
5
+ image: ['.jpg', '.jpeg', '.png']
6
+ } as const
7
+
8
+ export const FILE_TYPES_KEYS = {
9
+ DOCUMENT: 'document',
10
+ IMAGE: 'image'
11
+ } as const
@@ -0,0 +1 @@
1
+ export { useChatFileUpload } from './use-chat-file-upload'
@@ -0,0 +1,14 @@
1
+ export type FileType = 'document' | 'image'
2
+
3
+ export type SelectedFile = {
4
+ file: File
5
+ type: FileType
6
+ name: string
7
+ size: number
8
+ previewUrl?: string
9
+ }
10
+
11
+ export type UseFileUploadReturn = {
12
+ selectedFile: SelectedFile | null
13
+ handleSelectFile: (file: File, type: FileType) => void
14
+ }
@@ -0,0 +1,59 @@
1
+ import { act, renderHook } from '@/src/config/tests'
2
+
3
+ import { useChatFileUpload } from './use-chat-file-upload'
4
+
5
+ describe('useChatFileUpload', () => {
6
+ beforeEach(() => {
7
+ global.URL.createObjectURL = vi.fn(() => 'blob:mock-url')
8
+ })
9
+
10
+ afterEach(() => {
11
+ vi.clearAllMocks()
12
+ })
13
+
14
+ it('should initialize with default values', () => {
15
+ const { result } = renderHook(() => useChatFileUpload())
16
+
17
+ expect(result.current.selectedFile).toBeNull()
18
+ })
19
+
20
+ describe('when selecting a document file', () => {
21
+ it('should store the file with correct properties', () => {
22
+ const { result } = renderHook(() => useChatFileUpload())
23
+
24
+ const mockFile = new File(['content'], 'document.pdf', { type: 'application/pdf' })
25
+
26
+ act(() => {
27
+ result.current.handleSelectFile(mockFile, 'document')
28
+ })
29
+
30
+ expect(result.current.selectedFile).toEqual({
31
+ file: mockFile,
32
+ type: 'document',
33
+ name: 'document.pdf',
34
+ size: mockFile.size
35
+ })
36
+ })
37
+ })
38
+
39
+ describe('when selecting an image file', () => {
40
+ it('should create a preview URL', () => {
41
+ const { result } = renderHook(() => useChatFileUpload())
42
+
43
+ const mockImage = new File(['image'], 'photo.jpg', { type: 'image/jpeg' })
44
+
45
+ act(() => {
46
+ result.current.handleSelectFile(mockImage, 'image')
47
+ })
48
+
49
+ expect(result.current.selectedFile).toEqual({
50
+ file: mockImage,
51
+ type: 'image',
52
+ name: 'photo.jpg',
53
+ size: mockImage.size,
54
+ previewUrl: 'blob:mock-url'
55
+ })
56
+ expect(global.URL.createObjectURL).toHaveBeenCalledWith(mockImage)
57
+ })
58
+ })
59
+ })
@@ -0,0 +1,28 @@
1
+ import { useState } from 'react'
2
+
3
+ import { FILE_TYPES_KEYS } from './constants'
4
+ import type { FileType, SelectedFile, UseFileUploadReturn } from './types'
5
+
6
+ export function useChatFileUpload(): UseFileUploadReturn {
7
+ const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null)
8
+
9
+ const handleSelectFile = (file: File, type: FileType) => {
10
+ const newFile: SelectedFile = {
11
+ file,
12
+ type,
13
+ name: file.name,
14
+ size: file.size
15
+ }
16
+
17
+ if (type === FILE_TYPES_KEYS.IMAGE) {
18
+ newFile.previewUrl = URL.createObjectURL(file)
19
+ }
20
+
21
+ setSelectedFile(newFile)
22
+ }
23
+
24
+ return {
25
+ selectedFile,
26
+ handleSelectFile
27
+ }
28
+ }
@@ -0,0 +1 @@
1
+ export { default as useClickOutside } from './use-click-outside'
@@ -0,0 +1,23 @@
1
+ import { type RefObject, useEffect } from 'react'
2
+
3
+ function useClickOutside(
4
+ ref: RefObject<HTMLElement | null>,
5
+ onClickOutside: (event: MouseEvent | TouchEvent) => void
6
+ ) {
7
+ useEffect(() => {
8
+ const listener = (event: MouseEvent | TouchEvent) => {
9
+ if (!ref.current || ref.current.contains(event.target as Node)) {
10
+ return
11
+ }
12
+ onClickOutside(event)
13
+ }
14
+ document.addEventListener('mousedown', listener)
15
+ document.addEventListener('touchstart', listener)
16
+ return () => {
17
+ document.removeEventListener('mousedown', listener)
18
+ document.removeEventListener('touchstart', listener)
19
+ }
20
+ }, [ref, onClickOutside])
21
+ }
22
+
23
+ export default useClickOutside
@@ -0,0 +1,102 @@
1
+ import { fireEvent } from '@testing-library/react'
2
+
3
+ import { renderHook } from '@/src/config/tests'
4
+
5
+ import useClickOutside from './use-click-outside'
6
+
7
+ describe('useClickOutside', () => {
8
+ const mockOnClickOutside = vi.fn()
9
+
10
+ beforeEach(() => {
11
+ vi.clearAllMocks()
12
+ })
13
+
14
+ describe('when clicking outside the referenced element', () => {
15
+ it('should call onClickOutside', () => {
16
+ const ref = { current: document.createElement('div') }
17
+ document.body.appendChild(ref.current)
18
+
19
+ renderHook(() => useClickOutside(ref, mockOnClickOutside))
20
+
21
+ fireEvent.mouseDown(document.body)
22
+
23
+ expect(mockOnClickOutside).toHaveBeenCalledTimes(1)
24
+ })
25
+ })
26
+
27
+ describe('when clicking inside the referenced element', () => {
28
+ it('should not call onClickOutside', () => {
29
+ const ref = { current: document.createElement('div') }
30
+ document.body.appendChild(ref.current)
31
+
32
+ renderHook(() => useClickOutside(ref, mockOnClickOutside))
33
+
34
+ fireEvent.mouseDown(ref.current)
35
+
36
+ expect(mockOnClickOutside).not.toHaveBeenCalled()
37
+ })
38
+ })
39
+
40
+ describe('when ref is null', () => {
41
+ it('should not call onClickOutside', () => {
42
+ const ref = { current: null }
43
+
44
+ renderHook(() => useClickOutside(ref, mockOnClickOutside))
45
+
46
+ fireEvent.mouseDown(document.body)
47
+
48
+ expect(mockOnClickOutside).not.toHaveBeenCalled()
49
+ })
50
+ })
51
+
52
+ describe('when ref is undefined', () => {
53
+ it('should not call onClickOutside', () => {
54
+ const ref = { current: null }
55
+
56
+ renderHook(() => useClickOutside(ref, mockOnClickOutside))
57
+
58
+ fireEvent.mouseDown(document.body)
59
+
60
+ expect(mockOnClickOutside).not.toHaveBeenCalled()
61
+ })
62
+ })
63
+
64
+ describe('touch events', () => {
65
+ it('should call onClickOutside on touchstart outside element', () => {
66
+ const ref = { current: document.createElement('div') }
67
+ document.body.appendChild(ref.current)
68
+
69
+ renderHook(() => useClickOutside(ref, mockOnClickOutside))
70
+
71
+ fireEvent.touchStart(document.body)
72
+
73
+ expect(mockOnClickOutside).toHaveBeenCalledTimes(1)
74
+ })
75
+
76
+ it('should not call onClickOutside on touchstart inside element', () => {
77
+ const ref = { current: document.createElement('div') }
78
+ document.body.appendChild(ref.current)
79
+
80
+ renderHook(() => useClickOutside(ref, mockOnClickOutside))
81
+
82
+ fireEvent.touchStart(ref.current)
83
+
84
+ expect(mockOnClickOutside).not.toHaveBeenCalled()
85
+ })
86
+ })
87
+
88
+ describe('cleanup', () => {
89
+ it('should remove event listeners on unmount', () => {
90
+ const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener')
91
+ const ref = { current: document.createElement('div') }
92
+ document.body.appendChild(ref.current)
93
+
94
+ const { unmount } = renderHook(() => useClickOutside(ref, mockOnClickOutside))
95
+
96
+ unmount()
97
+
98
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('mousedown', expect.any(Function))
99
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('touchstart', expect.any(Function))
100
+ })
101
+ })
102
+ })
@@ -2,6 +2,7 @@ export * from './constants'
2
2
  export * from './copy-text-to-clipboard'
3
3
  export * from './extract-text-from-react-nodes'
4
4
  export { default as HttpCodes } from './http-codes'
5
+ export * from './is-theme-dark'
5
6
  export * from './languages'
6
7
  export * from './message-types'
7
8
  export * from './toast'
@@ -0,0 +1,21 @@
1
+ import type { Theme } from '@/src/types'
2
+
3
+ export function isThemeDark() {
4
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
5
+ return true
6
+ }
7
+
8
+ return false
9
+ }
10
+
11
+ export const getTheme = (theme?: Theme): Theme => {
12
+ if (theme) return theme
13
+
14
+ const container = document.getElementById('hotmart-app-tutor-ai-consumer-root')
15
+
16
+ const newTheme = container?.getAttribute('data-theme')
17
+
18
+ if (newTheme) return newTheme as Theme
19
+
20
+ return isThemeDark() ? 'dark' : 'light'
21
+ }
@@ -0,0 +1 @@
1
+ export { default as useInitialStore } from './use-initial-store'
@@ -0,0 +1,64 @@
1
+ import { useMemo } from 'react'
2
+ import { produce } from 'immer'
3
+ import { useStore } from 'jotai'
4
+ import { useHydrateAtoms } from 'jotai/utils'
5
+ import { v4 } from 'uuid'
6
+
7
+ import { getTheme } from '@/src/lib/utils'
8
+ import { MSG_MAX_COUNT } from '@/src/modules/messages'
9
+ import { messagesMaxCountAtom } from '@/src/modules/messages/store'
10
+ import type { WidgetTabsProps } from '@/src/modules/widget'
11
+ import {
12
+ widgetSettingsAtom,
13
+ widgetSettingsConfigAgentParentAtom,
14
+ widgetTabsAtom
15
+ } from '@/src/modules/widget'
16
+ import type { WidgetSettingProps } from '@/src/types'
17
+
18
+ const getDefaultSettings = (settings: WidgetSettingProps) => {
19
+ return produce(settings, (draft) => {
20
+ draft.config = { ...draft.config, theme: getTheme(draft.config?.theme) }
21
+
22
+ draft.sessionId = settings?.sessionId ?? v4()
23
+
24
+ return draft
25
+ })
26
+ }
27
+
28
+ export type UseInitialStoreProps = { settings: WidgetSettingProps }
29
+
30
+ const useInitialStore = ({ settings }: UseInitialStoreProps) => {
31
+ const store = useStore()
32
+
33
+ const initialTab: WidgetTabsProps = useMemo(() => {
34
+ const isAgentMode = settings.config?.metadata?.parent === 'AGENT'
35
+
36
+ if (isAgentMode)
37
+ return {
38
+ currentTab: 'loading',
39
+ history: new Set(['loading'])
40
+ }
41
+
42
+ return {
43
+ currentTab: 'starter',
44
+ history: new Set(['starter'])
45
+ }
46
+ }, [settings.config?.metadata?.parent])
47
+
48
+ const isAgentMode = useMemo(
49
+ () => settings?.config?.metadata?.parent === 'AGENT',
50
+ [settings?.config?.metadata?.parent]
51
+ )
52
+
53
+ useHydrateAtoms(
54
+ [
55
+ [widgetSettingsAtom, getDefaultSettings(settings)],
56
+ [messagesMaxCountAtom, MSG_MAX_COUNT],
57
+ [widgetTabsAtom, initialTab],
58
+ [widgetSettingsConfigAgentParentAtom, isAgentMode]
59
+ ],
60
+ { store }
61
+ )
62
+ }
63
+
64
+ export default useInitialStore
@@ -0,0 +1 @@
1
+ export { default as useInitialTab } from './use-initial-tab'
@@ -0,0 +1,84 @@
1
+ import { useEffect, useMemo, useRef } from 'react'
2
+
3
+ import { useSuspenseMessages } from '@/src/modules/messages/hooks/use-suspense-messages'
4
+ import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
5
+ import { useWidgetLoadingAtom, useWidgetTabsAtom } from '@/src/modules/widget'
6
+ import type { MainProps } from '../../types'
7
+
8
+ function useInitialTab({ settings }: MainProps) {
9
+ const [, setTab] = useWidgetTabsAtom()
10
+ const [, setWidgetLoading] = useWidgetLoadingAtom()
11
+ const sparkieState = useSparkieStateAtomValue()
12
+
13
+ const suspenseMessagesQuery = useSuspenseMessages({ conversationId: settings.conversationId })
14
+
15
+ const isAgentMode = useMemo(
16
+ () => settings.config?.metadata?.parent === 'AGENT',
17
+ [settings.config?.metadata?.parent]
18
+ )
19
+
20
+ const msgCount = useMemo(
21
+ () => Number(suspenseMessagesQuery.data.size),
22
+ [suspenseMessagesQuery.data.size]
23
+ )
24
+
25
+ const hasUserMessageWithoutResponse = useMemo(() => {
26
+ if (!isAgentMode || !suspenseMessagesQuery.data || msgCount === 0) return false
27
+
28
+ const allMessages = Array.from(suspenseMessagesQuery.data.values()).flat()
29
+ const userMessages = allMessages.filter((msg) => msg.metadata.author === 'user')
30
+ const aiMessages = allMessages.filter((msg) => msg.metadata.author !== 'user')
31
+
32
+ return userMessages.length > aiMessages.length && aiMessages.length === 0
33
+ }, [isAgentMode, suspenseMessagesQuery.data, msgCount])
34
+
35
+ const showStarterPage = useMemo(
36
+ () => !isAgentMode && sparkieState === 'initialized' && msgCount === 0,
37
+ [isAgentMode, sparkieState, msgCount]
38
+ )
39
+
40
+ const showChatPage = useMemo(
41
+ () => isAgentMode && sparkieState === 'initialized',
42
+ [isAgentMode, sparkieState]
43
+ )
44
+
45
+ const showErrorPage = useMemo(
46
+ () => suspenseMessagesQuery.isError,
47
+ [suspenseMessagesQuery.isError]
48
+ )
49
+
50
+ useEffect(() => {
51
+ if (showErrorPage) return setTab('error')
52
+
53
+ if (showStarterPage) return setTab('starter')
54
+
55
+ if (showChatPage) return setTab('chat')
56
+ }, [setTab, showChatPage, showErrorPage, showStarterPage])
57
+
58
+ const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
59
+
60
+ useEffect(() => {
61
+ if (hasUserMessageWithoutResponse && showChatPage) {
62
+ setWidgetLoading(true)
63
+
64
+ loadingTimeoutRef.current = setTimeout(() => {
65
+ setWidgetLoading(false)
66
+ setTab('error')
67
+ }, 60000)
68
+ } else {
69
+ if (loadingTimeoutRef.current) {
70
+ clearTimeout(loadingTimeoutRef.current)
71
+ loadingTimeoutRef.current = null
72
+ }
73
+ }
74
+
75
+ return () => {
76
+ if (loadingTimeoutRef.current) {
77
+ clearTimeout(loadingTimeoutRef.current)
78
+ loadingTimeoutRef.current = null
79
+ }
80
+ }
81
+ }, [hasUserMessageWithoutResponse, showChatPage, setWidgetLoading, setTab])
82
+ }
83
+
84
+ export default useInitialTab
package/src/main/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { default as Main } from './main'
2
+ export * from './types'
@@ -0,0 +1,14 @@
1
+ import { useAppLang } from '../config/i18n'
2
+ import { WidgetContainer } from '../modules/widget'
3
+
4
+ import { useInitialTab } from './hooks/use-initial-tab'
5
+ import type { MainProps } from './types'
6
+
7
+ function MainContent({ settings }: MainProps) {
8
+ useAppLang(settings.locale)
9
+ useInitialTab({ settings })
10
+
11
+ return <WidgetContainer />
12
+ }
13
+
14
+ export default MainContent
@@ -0,0 +1,16 @@
1
+ import { useStore } from 'jotai'
2
+
3
+ import { useInitSparkie } from '../modules/sparkie/hooks'
4
+
5
+ import MainContent from './main-content'
6
+ import type { MainProps } from './types'
7
+
8
+ function MainWrapper({ settings }: MainProps) {
9
+ const store = useStore()
10
+
11
+ useInitSparkie({ hotmartToken: settings.hotmartToken, store })
12
+
13
+ return <MainContent settings={settings} />
14
+ }
15
+
16
+ export default MainWrapper
@@ -9,7 +9,7 @@ vi.mock('@/src/modules/widget/store/widget-settings.atom', async (importOriginal
9
9
  }))
10
10
 
11
11
  describe('Main', () => {
12
- const defaultProps = new WidgetSettingPropsBuilder()
12
+ const defaultProps = JSON.parse(JSON.stringify(new WidgetSettingPropsBuilder().withTheme('dark')))
13
13
  const renderComponent = (props = { settings: defaultProps }) => render(<Main {...props} />)
14
14
 
15
15
  beforeEach(() => {
@@ -17,8 +17,10 @@ describe('Main', () => {
17
17
  })
18
18
 
19
19
  it('should render without errors', async () => {
20
- const props = new WidgetSettingPropsBuilder().withTutorName(chance.name())
21
- vi.mocked(useWidgetSettingsAtom).mockReturnValue([props, vi.fn()])
20
+ const props = JSON.parse(
21
+ JSON.stringify(new WidgetSettingPropsBuilder().withTheme('dark').withTutorName(chance.name()))
22
+ )
23
+ vi.mocked(useWidgetSettingsAtom).mockReturnValue([props, vi.fn()] as never)
22
24
 
23
25
  renderComponent({ settings: props })
24
26
 
package/src/main/main.tsx CHANGED
@@ -1,27 +1,18 @@
1
1
  import '@/config/styles/index.css'
2
2
 
3
- import { ErrorBoundary, GenericError } from '@/src/lib/components/errors'
4
- import { useAppLang } from '../config/i18n'
5
3
  import { GlobalProviders } from '../modules/global-providers'
6
- import { WidgetContainer } from '../modules/widget'
7
- import { useInitWidget, useListenToThemeChangeEvent } from '../modules/widget/hooks'
8
- import type { WidgetSettingProps } from '../types'
9
4
 
10
- export type MainProps = {
11
- settings: WidgetSettingProps
12
- }
5
+ import { useInitialStore } from './hooks/use-initial-store'
6
+ import MainWrapper from './main-wrapper'
7
+ import type { MainProps } from './types'
13
8
 
14
9
  function Main({ settings }: MainProps) {
15
- useInitWidget(settings)
16
- useAppLang(settings.locale)
17
- useListenToThemeChangeEvent()
10
+ useInitialStore({ settings })
18
11
 
19
12
  return (
20
- <ErrorBoundary fallback={<GenericError isDarkMode={settings.config?.theme === 'dark'} />}>
21
- <GlobalProviders settings={settings}>
22
- <WidgetContainer />
23
- </GlobalProviders>
24
- </ErrorBoundary>
13
+ <GlobalProviders settings={settings}>
14
+ <MainWrapper settings={settings} />
15
+ </GlobalProviders>
25
16
  )
26
17
  }
27
18
 
@@ -0,0 +1,5 @@
1
+ import type { WidgetSettingProps } from '../types'
2
+
3
+ export type MainProps = {
4
+ settings: WidgetSettingProps
5
+ }