app-tutor-ai-consumer 1.33.1 → 1.35.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 (129) hide show
  1. package/CHANGELOG.md +37 -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 +15 -4
  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 +19 -10
  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 +48 -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 +6 -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/chat-page/chat-page.tsx +58 -0
  101. package/src/modules/widget/components/constants.tsx +3 -1
  102. package/src/modules/widget/components/error-page/error-page.spec.tsx +17 -0
  103. package/src/modules/widget/components/error-page/error-page.tsx +12 -0
  104. package/src/modules/widget/components/error-page/index.ts +1 -0
  105. package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
  106. package/src/modules/widget/components/starter-page/starter-page-actions/index.ts +1 -0
  107. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +68 -0
  108. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +34 -0
  109. package/src/modules/widget/components/starter-page/starter-page-content/index.ts +1 -0
  110. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +16 -0
  111. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +28 -0
  112. package/src/modules/widget/components/starter-page/starter-page-header/index.ts +1 -0
  113. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +45 -0
  114. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +36 -0
  115. package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
  116. package/src/modules/widget/components/starter-page/starter-page.tsx +15 -109
  117. package/src/modules/widget/hooks/index.ts +1 -1
  118. package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
  119. package/src/modules/widget/hooks/use-retry-last-message/index.ts +1 -0
  120. package/src/modules/widget/hooks/use-retry-last-message/use-retry-last-message.tsx +37 -0
  121. package/src/modules/widget/store/create-store.ts +7 -0
  122. package/src/modules/widget/store/index.ts +2 -0
  123. package/src/modules/widget/store/widget-last-user-message.atom.ts +12 -0
  124. package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
  125. package/src/modules/widget/store/widget-tabs.atom.ts +17 -6
  126. package/src/types.ts +10 -0
  127. package/src/wrapper.tsx +52 -0
  128. package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
  129. package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +0 -56
@@ -0,0 +1,19 @@
1
+ import type { ChatFileUploaderProps } from './types'
2
+
3
+ export class ChatFileUploaderBuilder {
4
+ private props: ChatFileUploaderProps = {}
5
+
6
+ withDisabled(disabled: boolean): ChatFileUploaderBuilder {
7
+ this.props.disabled = disabled
8
+ return this
9
+ }
10
+
11
+ withLoading(loading: boolean): ChatFileUploaderBuilder {
12
+ this.props.loading = loading
13
+ return this
14
+ }
15
+
16
+ build(): ChatFileUploaderProps {
17
+ return this.props
18
+ }
19
+ }
@@ -0,0 +1,58 @@
1
+ import { render, screen } from '@/src/config/tests'
2
+
3
+ import ChatFileUploader from './chat-file-uploader'
4
+ import { ChatFileUploaderBuilder } from './chat-file-uploader.builder'
5
+
6
+ vi.mock('@/src/lib/hooks/use-chat-file-upload', () => ({
7
+ useChatFileUpload: vi.fn(() => ({
8
+ handleSelectFile: vi.fn(),
9
+ selectedFile: null
10
+ }))
11
+ }))
12
+
13
+ const defaultProps = new ChatFileUploaderBuilder().build()
14
+ const renderComponent = (props = defaultProps) => render(<ChatFileUploader {...props} />)
15
+
16
+ describe('ChatFileUploader', () => {
17
+ it('should be disabled when disabled prop is true', () => {
18
+ const props = new ChatFileUploaderBuilder().withDisabled(true).build()
19
+ renderComponent(props)
20
+
21
+ const button = screen.getByRole('button', {
22
+ name: /general.buttons.open_options/i
23
+ })
24
+ expect(button).toBeDisabled()
25
+ })
26
+
27
+ it('should be disabled when loading prop is true', () => {
28
+ const props = new ChatFileUploaderBuilder().withLoading(true).build()
29
+ renderComponent(props)
30
+
31
+ const button = screen.getByRole('button', {
32
+ name: /general.buttons.open_options/i
33
+ })
34
+ expect(button).toBeDisabled()
35
+ })
36
+
37
+ it('should be enabled when both disabled and loading are false', () => {
38
+ const props = new ChatFileUploaderBuilder().withDisabled(false).withLoading(false).build()
39
+ renderComponent(props)
40
+
41
+ const button = screen.getByRole('button', {
42
+ name: /general.buttons.open_options/i
43
+ })
44
+ expect(button).toBeEnabled()
45
+ })
46
+
47
+ it('should trigger file input when clicking file option', async () => {
48
+ const { user } = renderComponent()
49
+
50
+ const button = screen.getByRole('button', {
51
+ name: /general.buttons.open_options/i
52
+ })
53
+ await user.click(button)
54
+
55
+ expect(screen.getByText('general.buttons.file')).toBeInTheDocument()
56
+ expect(screen.getByText('general.buttons.image')).toBeInTheDocument()
57
+ })
58
+ })
@@ -0,0 +1,80 @@
1
+ import { type ChangeEvent, useRef } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ import { DropdownActions } from '@/src/lib/components'
5
+ import { useChatFileUpload } from '@/src/lib/hooks/use-chat-file-upload'
6
+ import { FILE_TYPES, FILE_TYPES_KEYS } from '@/src/lib/hooks/use-chat-file-upload/constants'
7
+ import type { FileType } from '@/src/lib/hooks/use-chat-file-upload/types'
8
+
9
+ import type { ChatFileUploaderProps } from './types'
10
+
11
+ function ChatFileUploader({ disabled = false, loading = false }: ChatFileUploaderProps) {
12
+ const { t } = useTranslation()
13
+ const fileInputRef = useRef<HTMLInputElement>(null)
14
+ const imageInputRef = useRef<HTMLInputElement>(null)
15
+
16
+ const acceptFileTypes = [FILE_TYPES.document, FILE_TYPES.spreadsheet, FILE_TYPES.pdf]
17
+ .flat()
18
+ .join()
19
+ const acceptImageTypes = FILE_TYPES.image.join()
20
+
21
+ const { handleSelectFile, selectedFile } = useChatFileUpload()
22
+
23
+ const triggerFileInput = (type: FileType) => {
24
+ const inputRef = type === FILE_TYPES_KEYS.DOCUMENT ? fileInputRef : imageInputRef
25
+ inputRef.current?.click()
26
+ }
27
+
28
+ const handleFileInputChange = (e: ChangeEvent<HTMLInputElement>, type: FileType) => {
29
+ const file = e.target.files?.[0]
30
+ if (file) {
31
+ handleSelectFile(file, type)
32
+ }
33
+ e.target.value = ''
34
+ }
35
+
36
+ return (
37
+ <>
38
+ <input
39
+ type='file'
40
+ ref={fileInputRef}
41
+ onChange={(e) => handleFileInputChange(e, FILE_TYPES_KEYS.DOCUMENT)}
42
+ accept={acceptFileTypes}
43
+ className='hidden'
44
+ aria-hidden='true'
45
+ aria-label={t('general.buttons.file')}
46
+ />
47
+ <input
48
+ type='file'
49
+ ref={imageInputRef}
50
+ onChange={(e) => handleFileInputChange(e, FILE_TYPES_KEYS.IMAGE)}
51
+ accept={acceptImageTypes}
52
+ className='hidden'
53
+ aria-hidden='true'
54
+ aria-label={t('general.buttons.image')}
55
+ />
56
+ <DropdownActions
57
+ triggerIcon='plus'
58
+ disabled={disabled || loading || selectedFile !== null}
59
+ items={[
60
+ {
61
+ label: 'general.buttons.file',
62
+ icon: 'file',
63
+ visible: true,
64
+ callback: () => triggerFileInput(FILE_TYPES_KEYS.DOCUMENT)
65
+ },
66
+ {
67
+ label: 'general.buttons.image',
68
+ icon: 'image',
69
+ visible: true,
70
+ callback: () => triggerFileInput(FILE_TYPES_KEYS.IMAGE)
71
+ }
72
+ ]}
73
+ triggerClassName='rounded-full border border-neutral-300 h-10 w-10 flex items-center justify-center'
74
+ triggerIconClassName='w-3.5 h-3.5'
75
+ />
76
+ </>
77
+ )
78
+ }
79
+
80
+ export default ChatFileUploader
@@ -0,0 +1 @@
1
+ export { default as ChatFileUploader } from './chat-file-uploader'
@@ -0,0 +1,4 @@
1
+ export type ChatFileUploaderProps = {
2
+ disabled?: boolean
3
+ loading?: boolean
4
+ }
@@ -1,3 +1,4 @@
1
+ export * from './chat-file-uploader'
1
2
  export * from './chat-input'
2
3
  export * from './message-img'
3
4
  export * from './message-item'
@@ -5,5 +5,6 @@ export const MessagesEndpoints = {
5
5
  getAll: (conversationId: string) =>
6
6
  `${process.env.API_CONVERSATION_URL}/v1/conversations/${conversationId}/messages`,
7
7
  create: (conversationId: string) =>
8
- `${process.env.API_CONVERSATION_URL}/v1/conversations/${conversationId}/messages`
8
+ `${process.env.API_CONVERSATION_URL}/v1/conversations/${conversationId}/messages`,
9
+ getSignedUrls: () => `${process.env.API_HOTMART_TUTOR}/api/v1/files/signed-urls`
9
10
  }
@@ -0,0 +1 @@
1
+ export * from './use-get-signed-urls'
@@ -0,0 +1,27 @@
1
+ import { renderHook, waitFor } from '@/src/config/tests'
2
+
3
+ import type { UseGetSignedUrlsProps } from './use-get-signed-urls'
4
+ import { useGetSignedUrls } from './use-get-signed-urls'
5
+
6
+ describe('useGetSignedUrls', () => {
7
+ const props: UseGetSignedUrlsProps = {
8
+ chatId: 'chat-id',
9
+ fileExtension: 'pdf',
10
+ fileNameWithExtension: 'file.pdf',
11
+ productId: 123
12
+ }
13
+ const render = () => renderHook(() => useGetSignedUrls(props))
14
+
15
+ it('should get signed upload url', async () => {
16
+ const { result } = render()
17
+
18
+ await waitFor(() => expect(result.current.isSuccess).toBeTruthy())
19
+
20
+ expect(result.current.data).toMatchObject({
21
+ uploadUrl: expect.any(String) as string,
22
+ futureDownloadUrl: expect.any(String) as string,
23
+ imagePreviewUrl: expect.any(String) as string,
24
+ fileNameWithExtension: expect.any(String) as string
25
+ })
26
+ })
27
+ })
@@ -0,0 +1,38 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+
3
+ import { MessagesEndpoints } from '../../constants'
4
+ import DirectMessagesService from '../../service.direct'
5
+
6
+ export type UseGetSignedUrlsProps = {
7
+ chatId: string
8
+ fileExtension: string
9
+ fileNameWithExtension: string
10
+ productId: number
11
+ enabled?: boolean
12
+ }
13
+
14
+ export function useGetSignedUrls({
15
+ chatId,
16
+ fileExtension,
17
+ fileNameWithExtension,
18
+ productId,
19
+ enabled
20
+ }: UseGetSignedUrlsProps) {
21
+ return useQuery({
22
+ queryKey: [
23
+ MessagesEndpoints.getSignedUrls(),
24
+ chatId,
25
+ fileExtension,
26
+ fileNameWithExtension,
27
+ productId
28
+ ],
29
+ queryFn: () =>
30
+ DirectMessagesService.getSignedUrls({
31
+ chatId,
32
+ fileExtension,
33
+ fileNameWithExtension,
34
+ productId
35
+ }),
36
+ enabled
37
+ })
38
+ }
@@ -1,7 +1,6 @@
1
1
  import { act, MockRequest, renderHook, waitFor } from '@/src/config/tests'
2
2
  import IMessageWithSenderDataMock from '../../__tests__/imessage-with-sender-data.mock'
3
3
  import { MessagesEndpoints, MSG_MAX_COUNT } from '../../constants'
4
- import type { IMessageWithSenderData } from '../../types'
5
4
 
6
5
  import useInfiniteGetMessages from './use-infinite-get-messages'
7
6
 
@@ -34,7 +33,7 @@ describe('useInfiniteGetMessages', () => {
34
33
 
35
34
  MockRequest.mock(
36
35
  MessagesEndpoints.getAll(mockConversationId),
37
- new IMessageWithSenderDataMock().getMany(allMessages) as IMessageWithSenderData[]
36
+ new IMessageWithSenderDataMock().getMany(allMessages)
38
37
  )
39
38
 
40
39
  const { result } = createHook({
@@ -1,5 +1,4 @@
1
- import type { UndefinedInitialDataInfiniteOptions } from '@tanstack/react-query'
2
- import { useInfiniteQuery } from '@tanstack/react-query'
1
+ import { infiniteQueryOptions, useInfiniteQuery } from '@tanstack/react-query'
3
2
 
4
3
  import { formatTime } from '@/src/config/dayjs'
5
4
  import { api } from '@/src/config/request'
@@ -18,7 +17,7 @@ export const getAllMessagesQuery = ({
18
17
  profileId,
19
18
  limit = MSG_MAX_COUNT
20
19
  }: GetAllMessagesQueryProps) =>
21
- ({
20
+ infiniteQueryOptions({
22
21
  queryKey: ['getAllMessagesWithoutSparkie', limit, conversationId],
23
22
  queryFn: async ({ pageParam }: { pageParam?: number }) => {
24
23
  const { data: messages } = await api.get<IMessageWithSenderData[]>(
@@ -45,6 +44,9 @@ export const getAllMessagesQuery = ({
45
44
  return minSentAt
46
45
  },
47
46
  enabled: Boolean(conversationId && profileId) && limit > 0,
47
+ meta: {
48
+ persist: false
49
+ },
48
50
  select(data) {
49
51
  const messages = data.pages?.flatMap((page) => page.messages) ?? []
50
52
  return messagesParser({ messages, profileId: profileId as string }).reduce(
@@ -68,11 +70,7 @@ export const getAllMessagesQuery = ({
68
70
  new Map<string, ParsedMessage[]>()
69
71
  )
70
72
  }
71
- }) as UndefinedInitialDataInfiniteOptions<
72
- FetchMessagesResponse,
73
- Error,
74
- Map<string, ParsedMessage[]>
75
- >
73
+ })
76
74
 
77
75
  function useInfiniteGetMessages({ conversationId, profileId, limit }: GetAllMessagesQueryProps) {
78
76
  const query = getAllMessagesQuery({
@@ -0,0 +1 @@
1
+ export { default as usePrefetchMessages } from './use-prefetch-messages'
@@ -0,0 +1,28 @@
1
+ import { useCallback, useEffect } from 'react'
2
+ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
3
+
4
+ import { getProfileQuery } from '@/src/modules/profile'
5
+ import type { WidgetSettingProps } from '@/src/types'
6
+ import { MSG_MAX_COUNT } from '../../constants'
7
+ import { getAllMessagesQuery } from '../use-infinite-get-messages'
8
+
9
+ function usePrefetchMessages(settings: WidgetSettingProps) {
10
+ const queryClient = useQueryClient()
11
+ const profileQuery = useSuspenseQuery(getProfileQuery())
12
+
13
+ const prefetch = useCallback(async () => {
14
+ await queryClient.prefetchInfiniteQuery(
15
+ getAllMessagesQuery({
16
+ conversationId: settings.conversationId,
17
+ profileId: typeof profileQuery?.data?.id === 'string' ? profileQuery?.data?.id : undefined,
18
+ limit: MSG_MAX_COUNT
19
+ })
20
+ )
21
+ }, [profileQuery?.data?.id, queryClient, settings.conversationId])
22
+
23
+ useEffect(() => {
24
+ void prefetch()
25
+ }, [prefetch])
26
+ }
27
+
28
+ export default usePrefetchMessages
@@ -0,0 +1,2 @@
1
+ export * from './types'
2
+ export { default as useSuspenseMessages } from './use-suspense-messages'
@@ -0,0 +1,4 @@
1
+ export type UseSuspenseMessagesProps = {
2
+ conversationId?: string
3
+ limit?: number
4
+ }
@@ -0,0 +1,21 @@
1
+ import { useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'
2
+
3
+ import { getProfileQuery } from '@/src/modules/profile'
4
+ import { MSG_MAX_COUNT } from '../../constants'
5
+ import { getAllMessagesQuery } from '../use-infinite-get-messages'
6
+
7
+ import type { UseSuspenseMessagesProps } from './types'
8
+
9
+ function useSuspenseMessages({ conversationId, limit = MSG_MAX_COUNT }: UseSuspenseMessagesProps) {
10
+ const profileQuery = useSuspenseQuery(getProfileQuery())
11
+
12
+ return useSuspenseInfiniteQuery(
13
+ getAllMessagesQuery({
14
+ conversationId: conversationId,
15
+ profileId: typeof profileQuery?.data?.id === 'string' ? profileQuery?.data?.id : undefined,
16
+ limit
17
+ })
18
+ )
19
+ }
20
+
21
+ export default useSuspenseMessages
@@ -1,4 +1,4 @@
1
- import type { Message } from '@hotmart/sparkie/dist/MessageService'
1
+ import type { Message } from '@hotmart-org-ca/sparkie/dist/MessageService'
2
2
 
3
3
  import { api } from '@/src/config/request'
4
4
 
@@ -6,6 +6,8 @@ import { MessagesEndpoints } from './constants'
6
6
  import type {
7
7
  DirectMessagesServiceProps,
8
8
  FetchMessagesResponse,
9
+ IGetSignedUrlsPayload,
10
+ IGetSignedUrlsResponse,
9
11
  IMessageWithSenderData
10
12
  } from './types'
11
13
 
@@ -36,6 +38,22 @@ class DirectMessagesService {
36
38
 
37
39
  return data
38
40
  }
41
+
42
+ async getSignedUrls({
43
+ productId,
44
+ chatId,
45
+ fileExtension,
46
+ fileNameWithExtension
47
+ }: IGetSignedUrlsPayload): Promise<IGetSignedUrlsResponse> {
48
+ const { data } = await api.post<IGetSignedUrlsResponse>(MessagesEndpoints.getSignedUrls(), {
49
+ productId,
50
+ chatId,
51
+ fileExtension,
52
+ fileNameWithExtension
53
+ })
54
+
55
+ return data
56
+ }
39
57
  }
40
58
 
41
59
  export default new DirectMessagesService()
@@ -1,4 +1,4 @@
1
- import type { Message } from '@hotmart/sparkie/dist/MessageService'
1
+ import type { Message } from '@hotmart-org-ca/sparkie/dist/MessageService'
2
2
 
3
3
  import { ApiError } from '@/src/config/request'
4
4
  import { HttpCodes } from '@/src/lib/utils'
@@ -2,7 +2,7 @@ import { atom, useAtom, useAtomValue } from 'jotai'
2
2
 
3
3
  const INITIAL_MAX_COUNT = 2
4
4
 
5
- const messagesMaxCountAtom = atom(INITIAL_MAX_COUNT)
5
+ export const messagesMaxCountAtom = atom(INITIAL_MAX_COUNT)
6
6
 
7
7
  const setMessagesMaxCountAtom = atom(
8
8
  (get) => get(messagesMaxCountAtom),
@@ -10,4 +10,4 @@ const setMessagesMaxCountAtom = atom(
10
10
  )
11
11
 
12
12
  export const useSetMessagesMaxCountAtom = () => useAtom(setMessagesMaxCountAtom)
13
- export const useMessagesMaxCount = () => useAtomValue(setMessagesMaxCountAtom)
13
+ export const useMessagesMaxCount = () => useAtomValue(messagesMaxCountAtom)
@@ -2,7 +2,7 @@ import type {
2
2
  Message as SparkieMsg,
3
3
  MessageContent,
4
4
  SenderData
5
- } from '@hotmart/sparkie/dist/MessageService'
5
+ } from '@hotmart-org-ca/sparkie/dist/MessageService'
6
6
 
7
7
  export type IMessage = SparkieMsg & {
8
8
  metadata: {
@@ -19,6 +19,13 @@ export type IGetMessagesPayload = {
19
19
  before?: number
20
20
  }
21
21
 
22
+ export type IGetSignedUrlsPayload = {
23
+ chatId: string
24
+ fileExtension: string
25
+ fileNameWithExtension: string
26
+ productId: number
27
+ }
28
+
22
29
  export type ISendTextMessagePayload = {
23
30
  conversationId: string
24
31
  content: {
@@ -63,6 +70,13 @@ export type MessageParserArgs = {
63
70
  profileId: string
64
71
  }
65
72
 
73
+ export type IGetSignedUrlsResponse = {
74
+ uploadUrl: string
75
+ futureDownloadUrl: string
76
+ imagePreviewUrl: string
77
+ fileNameWithExtension: string
78
+ }
79
+
66
80
  export type ParsedMessage = {
67
81
  from: string
68
82
  id: string
@@ -1,4 +1,4 @@
1
- import type { Message } from '@hotmart/sparkie/dist/MessageService'
1
+ import type { Message } from '@hotmart-org-ca/sparkie/dist/MessageService'
2
2
  import type { InfiniteData, QueryClient } from '@tanstack/react-query'
3
3
  import { produce } from 'immer'
4
4
 
@@ -1,13 +1,14 @@
1
- import { useQuery } from '@tanstack/react-query'
1
+ import { queryOptions, useQuery } from '@tanstack/react-query'
2
2
 
3
3
  import { ProfileEndpoints } from '../../constants'
4
4
  import ProfileService from '../../service'
5
5
 
6
- export const getProfileQuery = (enabled: boolean) => ({
7
- queryKey: [ProfileEndpoints.getProfile()],
8
- queryFn: () => ProfileService.getProfile(),
9
- enabled
10
- })
6
+ export const getProfileQuery = (enabled: boolean = true) =>
7
+ queryOptions({
8
+ queryKey: [ProfileEndpoints.getProfile()],
9
+ queryFn: () => ProfileService.getProfile(),
10
+ enabled
11
+ })
11
12
 
12
13
  export function useGetProfile(enabled: boolean = true) {
13
14
  return useQuery(getProfileQuery(enabled))
@@ -1,11 +1,8 @@
1
1
  import ICursorUpdateBuilder from '../../cursor/__tests__/icursor-update.builder'
2
- import type { IMessageWithSenderData } from '../../messages'
3
2
  import IMessageWithSenderDataMock from '../../messages/__tests__/imessage-with-sender-data.mock'
4
3
 
5
4
  export const SparkieMessageServiceMock = {
6
- getAll: vi
7
- .fn()
8
- .mockReturnValue(new IMessageWithSenderDataMock().getMany(10) as IMessageWithSenderData[]),
5
+ getAll: vi.fn().mockReturnValue(new IMessageWithSenderDataMock().getMany(10)),
9
6
  post: vi.fn(),
10
7
  postDirect: vi.fn(),
11
8
  remove: vi.fn()
@@ -1,39 +1,45 @@
1
- import { useCallback, useEffect, useState } from 'react'
1
+ import { useCallback, useEffect } from 'react'
2
+ import type { useStore } from 'jotai'
2
3
 
3
- import { useWidgetSettingsAtomValue } from '@/src/modules/widget'
4
4
  import { SparkieService } from '../..'
5
+ import { sparkieStateAtom } from '../../store'
5
6
 
6
- function useInitSparkie() {
7
- const [isSuccess, setIsSuccess] = useState(false)
8
- const settings = useWidgetSettingsAtomValue()
7
+ export type UseInitSparkieProps = {
8
+ hotmartToken: string
9
+ store?: ReturnType<typeof useStore>
10
+ }
9
11
 
10
- const init = useCallback(async () => {
11
- if (!settings?.hotmartToken) return
12
+ function useInitSparkie({ hotmartToken, store }: UseInitSparkieProps) {
13
+ const sparkieState = store?.get(sparkieStateAtom)
12
14
 
13
- try {
14
- await SparkieService.initSparkie({
15
- token: settings?.hotmartToken,
16
- skipPresenceSetup: true,
17
- retryOptions: {
18
- maxRetries: 5,
19
- retryDelay: 2000,
20
- backoffMultiplier: 1.5
21
- }
22
- })
23
- await SparkieService.ensureInitialized()
24
- setIsSuccess(true)
25
- } catch {
26
- setIsSuccess(false)
27
- // TODO: Create Error PAGE and setTab
28
- // setTab('information')
15
+ const init = useCallback(async () => {
16
+ if (hotmartToken) {
17
+ try {
18
+ store?.set(sparkieStateAtom, 'initializing')
19
+ await SparkieService.initSparkie({
20
+ token: hotmartToken,
21
+ skipPresenceSetup: true,
22
+ retryOptions: {
23
+ maxRetries: 5,
24
+ retryDelay: 1000,
25
+ backoffMultiplier: 1.5
26
+ }
27
+ })
28
+ await SparkieService.ensureInitialized()
29
+ store?.set(sparkieStateAtom, 'initialized')
30
+ } catch {
31
+ store?.set(sparkieStateAtom, 'failed')
32
+ }
29
33
  }
30
- }, [settings?.hotmartToken])
34
+ }, [hotmartToken, store])
31
35
 
32
- useEffect(() => {
33
- void init()
34
- }, [init])
36
+ const checkState = useCallback(() => {
37
+ if (sparkieState === 'idle') return init()
38
+ }, [init, sparkieState])
35
39
 
36
- return isSuccess
40
+ useEffect(() => {
41
+ void checkState()
42
+ }, [checkState])
37
43
  }
38
44
 
39
45
  export default useInitSparkie
@@ -1,4 +1,4 @@
1
- import Sparkie from '@hotmart/sparkie'
1
+ import Sparkie from '@hotmart-org-ca/sparkie'
2
2
 
3
3
  import { MSG_MAX_COUNT } from '../messages'
4
4
 
@@ -0,0 +1 @@
1
+ export * from './sparkie-state.atom'
@@ -0,0 +1,13 @@
1
+ import { atom, useAtom, useAtomValue } from 'jotai'
2
+
3
+ import type { InitializationState } from '../types'
4
+
5
+ export const sparkieStateAtom = atom<InitializationState>('idle')
6
+
7
+ const setSparkieStateAtom = atom(
8
+ (get) => get(sparkieStateAtom),
9
+ (_, set, sparkieState: InitializationState) => set(sparkieStateAtom, sparkieState)
10
+ )
11
+
12
+ export const useSparkieStateAtom = () => useAtom(setSparkieStateAtom)
13
+ export const useSparkieStateAtomValue = () => useAtomValue(sparkieStateAtom)
@@ -1,6 +1,6 @@
1
1
  import type { ILanguages } from '@/src/config/i18n'
2
2
  import { chance } from '@/src/config/tests'
3
- import type { User, WidgetSettingProps } from '@/src/types'
3
+ import type { Theme, User, WidgetSettingProps } from '@/src/types'
4
4
 
5
5
  class WidgetSettingPropsBuilder implements WidgetSettingProps {
6
6
  hotmartToken: string
@@ -21,6 +21,19 @@ class WidgetSettingPropsBuilder implements WidgetSettingProps {
21
21
  classHashId?: string
22
22
  owner_id?: string
23
23
  current_media_codes?: string
24
+ config?:
25
+ | {
26
+ theme?: Theme
27
+ metadata?: {
28
+ parent?: 'AGENT' | 'TUTOR'
29
+ agentProductId?: number
30
+ agentName?: string
31
+ courseName?: string
32
+ source?: string
33
+ promptId?: string
34
+ }
35
+ }
36
+ | undefined
24
37
 
25
38
  constructor() {
26
39
  this.hotmartToken = chance.guid()
@@ -144,6 +157,12 @@ class WidgetSettingPropsBuilder implements WidgetSettingProps {
144
157
 
145
158
  return this
146
159
  }
160
+
161
+ withTheme(theme: Theme) {
162
+ this.config = { ...(this.config ?? {}), theme }
163
+
164
+ return this
165
+ }
147
166
  }
148
167
 
149
168
  export default WidgetSettingPropsBuilder