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.
- package/CHANGELOG.md +37 -0
- package/config/vitest/__mocks__/sparkie.tsx +1 -1
- package/package.json +1 -1
- package/src/bootstrap.ts +40 -0
- package/src/config/tests/handlers.ts +5 -4
- package/src/config/theme/init-theme.ts +11 -5
- package/src/index.tsx +22 -12
- package/src/lib/components/dropdown-actions/dropdown-actions.tsx +87 -0
- package/src/lib/components/dropdown-actions/dropdownActions.builder.ts +58 -0
- package/src/lib/components/dropdown-actions/dropdownActions.spec.tsx +76 -0
- package/src/lib/components/dropdown-actions/index.ts +1 -0
- package/src/lib/components/dropdown-actions/types.ts +16 -0
- package/src/lib/components/errors/generic/generic-error.tsx +11 -8
- package/src/lib/components/icons/document.svg +3 -0
- package/src/lib/components/icons/file.svg +3 -0
- package/src/lib/components/icons/icon-names.d.ts +8 -0
- package/src/lib/components/icons/image.svg +3 -0
- package/src/lib/components/icons/pdf.svg +3 -0
- package/src/lib/components/icons/plus.svg +3 -0
- package/src/lib/components/icons/retry.svg +3 -0
- package/src/lib/components/icons/spreadsheet.svg +3 -0
- package/src/lib/components/icons/tutor-logo.svg +9 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -3
- package/src/lib/hooks/index.ts +1 -0
- package/src/lib/hooks/use-chat-file-upload/constants.ts +11 -0
- package/src/lib/hooks/use-chat-file-upload/index.ts +1 -0
- package/src/lib/hooks/use-chat-file-upload/types.ts +14 -0
- package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.spec.ts +59 -0
- package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.ts +28 -0
- package/src/lib/hooks/use-click-outside/index.ts +1 -0
- package/src/lib/hooks/use-click-outside/use-click-outside.tsx +23 -0
- package/src/lib/hooks/use-click-outside/useClickOutside.spec.ts +102 -0
- package/src/lib/utils/index.ts +1 -0
- package/src/lib/utils/is-theme-dark.ts +21 -0
- package/src/main/hooks/use-initial-store/index.ts +1 -0
- package/src/main/hooks/use-initial-store/use-initial-store.tsx +64 -0
- package/src/main/hooks/use-initial-tab/index.ts +1 -0
- package/src/main/hooks/use-initial-tab/use-initial-tab.tsx +84 -0
- package/src/main/index.ts +1 -0
- package/src/main/main-content.tsx +14 -0
- package/src/main/main-wrapper.tsx +16 -0
- package/src/main/main.spec.tsx +5 -3
- package/src/main/main.tsx +7 -16
- package/src/main/types.ts +5 -0
- package/src/modules/global-providers/global-providers.tsx +1 -15
- package/src/modules/messages/__tests__/signed-urls.builder.ts +42 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.builder.ts +121 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.spec.tsx +107 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.tsx +45 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/close-button.tsx +31 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/types.ts +4 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/document-preview.tsx +63 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/types.ts +10 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/error-preview.tsx +37 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/types.ts +4 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/image-preview.tsx +32 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/types.ts +6 -0
- package/src/modules/messages/components/chat-file-preview/constants.ts +22 -0
- package/src/modules/messages/components/chat-file-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/types.ts +13 -0
- package/src/modules/messages/components/chat-file-preview/utils.spec.ts +38 -0
- package/src/modules/messages/components/chat-file-preview/utils.ts +13 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.builder.ts +19 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.spec.tsx +58 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.tsx +80 -0
- package/src/modules/messages/components/chat-file-uploader/index.ts +1 -0
- package/src/modules/messages/components/chat-file-uploader/types.ts +4 -0
- package/src/modules/messages/components/chat-input/chat-input.tsx +1 -1
- package/src/modules/messages/components/index.ts +1 -0
- package/src/modules/messages/components/message-item/message-item.tsx +1 -2
- package/src/modules/messages/constants.ts +2 -1
- package/src/modules/messages/hooks/use-get-signed-urls/index.ts +1 -0
- package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.spec.tsx +27 -0
- package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.tsx +38 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +1 -2
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +3 -8
- package/src/modules/messages/hooks/use-prefetch-messages/index.ts +1 -0
- package/src/modules/messages/hooks/use-prefetch-messages/use-prefetch-messages.tsx +28 -0
- package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +54 -119
- package/src/modules/messages/hooks/use-suspense-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-suspense-messages/types.ts +4 -0
- package/src/modules/messages/hooks/use-suspense-messages/use-suspense-messages.tsx +21 -0
- package/src/modules/messages/service.direct.ts +18 -0
- package/src/modules/messages/service.ts +1 -2
- package/src/modules/messages/store/messages-max-count.atom.ts +2 -2
- package/src/modules/messages/types.ts +14 -0
- package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +7 -6
- package/src/modules/sparkie/__tests__/sparkie.mock.ts +1 -4
- package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +30 -38
- package/src/modules/sparkie/service.ts +2 -1
- package/src/modules/widget/__tests__/widget-settings-props.builder.ts +20 -1
- package/src/modules/widget/components/ai-disclaimer/ai-disclaimer.tsx +19 -0
- package/src/modules/widget/components/ai-disclaimer/index.ts +1 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +30 -71
- package/src/modules/widget/components/container/container.tsx +14 -0
- package/src/modules/widget/components/greetings-card/greetings-card.tsx +9 -2
- package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +4 -4
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +3 -2
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +0 -46
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +1 -30
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +8 -4
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +15 -13
- package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
- package/src/modules/widget/components/starter-page/starter-page.tsx +22 -87
- package/src/modules/widget/hooks/index.ts +0 -1
- package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
- package/src/modules/widget/store/create-store.ts +7 -0
- package/src/modules/widget/store/index.ts +1 -0
- package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
- package/src/modules/widget/store/widget-tabs.atom.ts +18 -37
- package/src/types.ts +1 -0
- package/src/wrapper.tsx +39 -19
- package/src/lib/hooks/use-response-timeout/index.ts +0 -1
- package/src/lib/hooks/use-response-timeout/use-response-timeout.tsx +0 -42
- package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
- 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
|
+
})
|
package/src/lib/utils/index.ts
CHANGED
|
@@ -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
|
@@ -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
|
package/src/main/main.spec.tsx
CHANGED
|
@@ -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 =
|
|
21
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
useAppLang(settings.locale)
|
|
17
|
-
useListenToThemeChangeEvent()
|
|
10
|
+
useInitialStore({ settings })
|
|
18
11
|
|
|
19
12
|
return (
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
</GlobalProviders>
|
|
24
|
-
</ErrorBoundary>
|
|
13
|
+
<GlobalProviders settings={settings}>
|
|
14
|
+
<MainWrapper settings={settings} />
|
|
15
|
+
</GlobalProviders>
|
|
25
16
|
)
|
|
26
17
|
}
|
|
27
18
|
|
|
@@ -1,25 +1,11 @@
|
|
|
1
|
-
import { type PropsWithChildren
|
|
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
|