app-tutor-ai-consumer 1.33.0 → 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 (122) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/config/vitest/__mocks__/sparkie.tsx +1 -1
  3. package/package.json +1 -1
  4. package/src/bootstrap.ts +40 -0
  5. package/src/config/tests/handlers.ts +5 -4
  6. package/src/config/theme/init-theme.ts +11 -5
  7. package/src/index.tsx +22 -12
  8. package/src/lib/components/dropdown-actions/dropdown-actions.tsx +87 -0
  9. package/src/lib/components/dropdown-actions/dropdownActions.builder.ts +58 -0
  10. package/src/lib/components/dropdown-actions/dropdownActions.spec.tsx +76 -0
  11. package/src/lib/components/dropdown-actions/index.ts +1 -0
  12. package/src/lib/components/dropdown-actions/types.ts +16 -0
  13. package/src/lib/components/errors/generic/generic-error.tsx +11 -8
  14. package/src/lib/components/icons/document.svg +3 -0
  15. package/src/lib/components/icons/file.svg +3 -0
  16. package/src/lib/components/icons/icon-names.d.ts +8 -0
  17. package/src/lib/components/icons/image.svg +3 -0
  18. package/src/lib/components/icons/pdf.svg +3 -0
  19. package/src/lib/components/icons/plus.svg +3 -0
  20. package/src/lib/components/icons/retry.svg +3 -0
  21. package/src/lib/components/icons/spreadsheet.svg +3 -0
  22. package/src/lib/components/icons/tutor-logo.svg +9 -0
  23. package/src/lib/components/index.ts +1 -0
  24. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -3
  25. package/src/lib/hooks/index.ts +1 -0
  26. package/src/lib/hooks/use-chat-file-upload/constants.ts +11 -0
  27. package/src/lib/hooks/use-chat-file-upload/index.ts +1 -0
  28. package/src/lib/hooks/use-chat-file-upload/types.ts +14 -0
  29. package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.spec.ts +59 -0
  30. package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.ts +28 -0
  31. package/src/lib/hooks/use-click-outside/index.ts +1 -0
  32. package/src/lib/hooks/use-click-outside/use-click-outside.tsx +23 -0
  33. package/src/lib/hooks/use-click-outside/useClickOutside.spec.ts +102 -0
  34. package/src/lib/utils/index.ts +1 -0
  35. package/src/lib/utils/is-theme-dark.ts +21 -0
  36. package/src/main/hooks/use-initial-store/index.ts +1 -0
  37. package/src/main/hooks/use-initial-store/use-initial-store.tsx +64 -0
  38. package/src/main/hooks/use-initial-tab/index.ts +1 -0
  39. package/src/main/hooks/use-initial-tab/use-initial-tab.tsx +84 -0
  40. package/src/main/index.ts +1 -0
  41. package/src/main/main-content.tsx +14 -0
  42. package/src/main/main-wrapper.tsx +16 -0
  43. package/src/main/main.spec.tsx +5 -3
  44. package/src/main/main.tsx +7 -16
  45. package/src/main/types.ts +5 -0
  46. package/src/modules/global-providers/global-providers.tsx +1 -15
  47. package/src/modules/messages/__tests__/signed-urls.builder.ts +42 -0
  48. package/src/modules/messages/components/chat-file-preview/chat-file-preview.builder.ts +121 -0
  49. package/src/modules/messages/components/chat-file-preview/chat-file-preview.spec.tsx +107 -0
  50. package/src/modules/messages/components/chat-file-preview/chat-file-preview.tsx +45 -0
  51. package/src/modules/messages/components/chat-file-preview/components/close-button/close-button.tsx +31 -0
  52. package/src/modules/messages/components/chat-file-preview/components/close-button/index.ts +1 -0
  53. package/src/modules/messages/components/chat-file-preview/components/close-button/types.ts +4 -0
  54. package/src/modules/messages/components/chat-file-preview/components/document-preview/document-preview.tsx +63 -0
  55. package/src/modules/messages/components/chat-file-preview/components/document-preview/index.ts +1 -0
  56. package/src/modules/messages/components/chat-file-preview/components/document-preview/types.ts +10 -0
  57. package/src/modules/messages/components/chat-file-preview/components/error-preview/error-preview.tsx +37 -0
  58. package/src/modules/messages/components/chat-file-preview/components/error-preview/index.ts +1 -0
  59. package/src/modules/messages/components/chat-file-preview/components/error-preview/types.ts +4 -0
  60. package/src/modules/messages/components/chat-file-preview/components/image-preview/image-preview.tsx +32 -0
  61. package/src/modules/messages/components/chat-file-preview/components/image-preview/index.ts +1 -0
  62. package/src/modules/messages/components/chat-file-preview/components/image-preview/types.ts +6 -0
  63. package/src/modules/messages/components/chat-file-preview/constants.ts +22 -0
  64. package/src/modules/messages/components/chat-file-preview/index.ts +1 -0
  65. package/src/modules/messages/components/chat-file-preview/types.ts +13 -0
  66. package/src/modules/messages/components/chat-file-preview/utils.spec.ts +38 -0
  67. package/src/modules/messages/components/chat-file-preview/utils.ts +13 -0
  68. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.builder.ts +19 -0
  69. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.spec.tsx +58 -0
  70. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.tsx +80 -0
  71. package/src/modules/messages/components/chat-file-uploader/index.ts +1 -0
  72. package/src/modules/messages/components/chat-file-uploader/types.ts +4 -0
  73. package/src/modules/messages/components/chat-input/chat-input.tsx +1 -1
  74. package/src/modules/messages/components/index.ts +1 -0
  75. package/src/modules/messages/components/message-item/message-item.tsx +1 -2
  76. package/src/modules/messages/constants.ts +2 -1
  77. package/src/modules/messages/hooks/use-get-signed-urls/index.ts +1 -0
  78. package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.spec.tsx +27 -0
  79. package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.tsx +38 -0
  80. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +1 -2
  81. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +3 -8
  82. package/src/modules/messages/hooks/use-prefetch-messages/index.ts +1 -0
  83. package/src/modules/messages/hooks/use-prefetch-messages/use-prefetch-messages.tsx +28 -0
  84. package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +54 -119
  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 +18 -0
  89. package/src/modules/messages/service.ts +1 -2
  90. package/src/modules/messages/store/messages-max-count.atom.ts +2 -2
  91. package/src/modules/messages/types.ts +14 -0
  92. package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +7 -6
  93. package/src/modules/sparkie/__tests__/sparkie.mock.ts +1 -4
  94. package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +30 -38
  95. package/src/modules/sparkie/service.ts +2 -1
  96. package/src/modules/widget/__tests__/widget-settings-props.builder.ts +20 -1
  97. package/src/modules/widget/components/ai-disclaimer/ai-disclaimer.tsx +19 -0
  98. package/src/modules/widget/components/ai-disclaimer/index.ts +1 -0
  99. package/src/modules/widget/components/chat-page/chat-page.tsx +30 -71
  100. package/src/modules/widget/components/container/container.tsx +14 -0
  101. package/src/modules/widget/components/greetings-card/greetings-card.tsx +9 -2
  102. package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
  103. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +4 -4
  104. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +3 -2
  105. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +0 -46
  106. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +1 -30
  107. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +8 -4
  108. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +15 -13
  109. package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
  110. package/src/modules/widget/components/starter-page/starter-page.tsx +22 -87
  111. package/src/modules/widget/hooks/index.ts +0 -1
  112. package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
  113. package/src/modules/widget/store/create-store.ts +7 -0
  114. package/src/modules/widget/store/index.ts +1 -0
  115. package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
  116. package/src/modules/widget/store/widget-tabs.atom.ts +18 -37
  117. package/src/types.ts +1 -0
  118. package/src/wrapper.tsx +39 -19
  119. package/src/lib/hooks/use-response-timeout/index.ts +0 -1
  120. package/src/lib/hooks/use-response-timeout/use-response-timeout.tsx +0 -42
  121. package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
  122. package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +0 -56
@@ -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
+ }
@@ -1,25 +1,11 @@
1
- import { type PropsWithChildren, useEffect } from 'react'
2
- import { v4 } from 'uuid'
1
+ import { type PropsWithChildren } from 'react'
3
2
 
4
3
  import { OptimizelyProvider } from '@/src/config/optimizely'
5
- import { useWidgetSettingsAtom } from '@/src/modules/widget'
6
4
  import type { WidgetSettingProps } from '@/src/types'
7
5
 
8
6
  export type GlobalProvidersProps = PropsWithChildren<{ settings: WidgetSettingProps }>
9
7
 
10
8
  function GlobalProviders({ children, settings }: GlobalProvidersProps) {
11
- const [, setWidgetSettings] = useWidgetSettingsAtom()
12
-
13
- useEffect(() => {
14
- if (!settings || !Object.keys(settings)?.length) return
15
-
16
- if (!settings?.sessionId) {
17
- settings.sessionId = v4()
18
- }
19
-
20
- setWidgetSettings(settings)
21
- }, [setWidgetSettings, settings])
22
-
23
9
  return <OptimizelyProvider settings={settings}>{children}</OptimizelyProvider>
24
10
  }
25
11
 
@@ -0,0 +1,42 @@
1
+ import { chance } from '@/src/config/tests'
2
+ import type { IGetSignedUrlsResponse } from '../types'
3
+
4
+ class SignedUrlsResponseBuilder implements IGetSignedUrlsResponse {
5
+ uploadUrl: string
6
+ futureDownloadUrl: string
7
+ imagePreviewUrl: string
8
+ fileNameWithExtension: string
9
+
10
+ constructor() {
11
+ this.uploadUrl = chance.url()
12
+ this.futureDownloadUrl = chance.url()
13
+ this.imagePreviewUrl = chance.url()
14
+ this.fileNameWithExtension = chance.string()
15
+ }
16
+
17
+ withUploadUrl(uploadUrl: typeof this.uploadUrl) {
18
+ this.uploadUrl = uploadUrl
19
+
20
+ return this
21
+ }
22
+
23
+ withFutureDownloadUrl(futureDownloadUrl: typeof this.futureDownloadUrl) {
24
+ this.futureDownloadUrl = futureDownloadUrl
25
+
26
+ return this
27
+ }
28
+
29
+ withImagePreviewUrl(imagePreviewUrl: typeof this.imagePreviewUrl) {
30
+ this.imagePreviewUrl = imagePreviewUrl
31
+
32
+ return this
33
+ }
34
+
35
+ withFileNameWithExtension(fileNameWithExtension: typeof this.fileNameWithExtension) {
36
+ this.fileNameWithExtension = fileNameWithExtension
37
+
38
+ return this
39
+ }
40
+ }
41
+
42
+ export default SignedUrlsResponseBuilder