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
package/CHANGELOG.md CHANGED
@@ -1,3 +1,34 @@
1
+ # [1.34.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.33.1...v1.34.0) (2025-11-04)
2
+
3
+ ### Bug Fixes
4
+
5
+ - fix aria label ([5aa714d](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/5aa714da552e93349c1c030c026ecb4a14c132ba))
6
+ - fix base branch ([96db93a](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/96db93a53c4a93d6e0800af9547ea81d118a6bb5))
7
+ - fix chat input ([d686bc5](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/d686bc530cecb84fc80cbf2a8b9ff029a3572e53))
8
+ - fix file types ([ee12680](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/ee12680db8c36307c4b067cb6460009c0509018e))
9
+ - fix file uploader ([1110f11](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/1110f116a0276f3d34ebec6eea81aaa59e50e993))
10
+ - fix pr request ([6ad4870](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/6ad4870cd2bd03f788eb10f00a65616d48c1087f))
11
+ - fix pr request ([1556154](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/155615476be3fb61994a790dd4464b9f53968f78))
12
+ - fix pr request ([17cd695](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/17cd69506c21b793ea05f634a07f987a125e3102))
13
+ - fix svg ([c9165f7](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/c9165f74d2f592e0fccf27176768e2ae6a782fd0))
14
+ - pr issues ([3f5b8ad](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/3f5b8ad54a657a4d95c66728c12559cd321b088d))
15
+ - refactor code ([1b7b5cf](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/1b7b5cfafe6c8f902bf3514fd4e36996f2e705c8))
16
+ - refactor component name ([a6c2a16](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/a6c2a16ab29733dd831bc88e3a6452c9e225af70))
17
+ - remove console.log ([66645dc](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/66645dc72dac1bd667c5440cf59e45aecc9b321a))
18
+ - update file uploader ([c12f347](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/c12f3472450a4afa214ebf87234e7eb3464c3f7d))
19
+
20
+ ### Features
21
+
22
+ - add chat loading if is user message without response ([8ffe2a7](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/8ffe2a7bbf1d814f341855ec1efebd6e00f5cb65))
23
+ - add default store ([75a07c4](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/75a07c451c99b526aea896b6f94b7deb1f0f1888))
24
+ - change sparkie call to messages history page ([f5f3e73](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/f5f3e73d7ec147b7f5a4a5e76293cf29b2d0dd93))
25
+ - create chat file preview ([885b744](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/885b744d76b62a7e387d2e820e8e8030f6e17f9a))
26
+ - implement signed URLs functionality with hooks and service integration ([ff845fa](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/ff845fa34be2c28f55ec04b9a70522f3100a7bec))
27
+ - refactor dropdown ([88bba02](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/88bba02ec9021f5294450eecfd6d6c92097d6690))
28
+ - release branch for changing widget to support product agent ([ab39962](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/ab39962a5e2a35ee29056c329fe8a4038291ceaa))
29
+ - update feature flag file upload ([bd3313b](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/bd3313b634b53ac5dfda2737155e718d570fb099))
30
+ - update icon size ([2d77927](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/2d7792724ed1a1166b012da118b5ce3c44d9ad74))
31
+
1
32
  ## [1.33.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.33.0...v1.33.1) (2025-10-22)
2
33
 
3
34
  ### Bug Fixes
@@ -4,9 +4,9 @@ import {
4
4
  SparkieMessageServiceMock,
5
5
  SparkieCursorServiceMock
6
6
  } from '@/src/modules/sparkie/__tests__/sparkie.mock'
7
- import MessageService from '@hotmart/sparkie/dist/MessageService'
7
+ import MessageService from '@hotmart-org-ca/sparkie/dist/MessageService'
8
8
 
9
- vi.mock('@hotmart/sparkie', () => ({ default: SparkieMock }))
9
+ vi.mock('@hotmart-org-ca/sparkie', () => ({ default: SparkieMock }))
10
10
 
11
11
  beforeEach(() => {
12
12
  vi.spyOn(SparkieService, 'getMessageService').mockResolvedValue(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.33.1",
3
+ "version": "1.34.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -108,8 +108,8 @@
108
108
  "dependencies": {
109
109
  "@hot-observability-js/react": "~1.1.0",
110
110
  "@hotmart-org-ca/hot-observability-js": "~1.1.0",
111
+ "@hotmart-org-ca/sparkie": "~5.1.4",
111
112
  "@hotmart/event-agent-js": "~1.1.2",
112
- "@hotmart/sparkie": "~5.1.0",
113
113
  "@optimizely/react-sdk": "~3.2.4",
114
114
  "@tanstack/query-sync-storage-persister": "~5.80.7",
115
115
  "@tanstack/react-query": "~5.80.6",
@@ -1,4 +1,4 @@
1
- import type { StartTutorWidgetProps } from '@/src/types'
1
+ import type { StartTutorWidgetProps, WidgetInstance } from '@/src/types'
2
2
 
3
3
  export {}
4
4
 
@@ -13,7 +13,8 @@ declare global {
13
13
  elementId: StartTutorWidgetProps['elementId'],
14
14
  settings: StartTutorWidgetProps['settings']
15
15
  ) => Promise<void>
16
- closeChatWidget: () => void
16
+ closeChatWidget: () => Promise<void>
17
+ __CHAT_WIDGET_INSTANCE__?: WidgetInstance
17
18
  TOKEN: string
18
19
  }
19
20
  }
@@ -0,0 +1,40 @@
1
+ import type { QueryClient } from '@tanstack/react-query'
2
+
3
+ import { initTheme } from '@/src/config/theme'
4
+
5
+ import { DataHubStore } from './config/datahub'
6
+ import { initDayjs } from './config/dayjs'
7
+ import { initLanguage } from './config/i18n'
8
+ import { initAxios } from './config/request/api'
9
+ import { getProfileQuery } from './modules/profile'
10
+ import type { Theme, WidgetSettingProps } from './types'
11
+
12
+ type BootstrapProps = {
13
+ settings: WidgetSettingProps
14
+ queryClient: QueryClient
15
+ }
16
+
17
+ export async function bootstrap({ queryClient, settings }: BootstrapProps) {
18
+ initAxios(settings.hotmartToken)
19
+ initTheme(settings.config?.theme as Theme)
20
+
21
+ const promises = [
22
+ initLanguage(settings.locale),
23
+ queryClient.prefetchQuery(getProfileQuery()),
24
+ initDayjs(settings.locale)
25
+ ] as const
26
+
27
+ await Promise.all(promises)
28
+
29
+ DataHubStore.initData({
30
+ ucode: settings.user?.ucode ?? '',
31
+ membershipId: settings.membershipId ?? '',
32
+ membershipSlug: settings.membershipSlug ?? '',
33
+ sessionId: settings.sessionId,
34
+ userId: Number(settings.userId),
35
+ product: {
36
+ id: Number(settings.productId) || 0,
37
+ category: settings.productType ?? ''
38
+ }
39
+ })
40
+ }
@@ -1,12 +1,16 @@
1
+ import type { QueryClient } from '@tanstack/react-query'
1
2
  import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
2
3
  import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
3
4
  import type { PropsWithChildren } from 'react'
4
5
 
5
- import { persister, queryClient } from './query-client'
6
+ import { persister } from './query-client'
6
7
 
7
- export type QueryProviderProps = PropsWithChildren<{ showDevTools?: boolean }>
8
+ export type QueryProviderProps = PropsWithChildren<{
9
+ showDevTools?: boolean
10
+ queryClient: QueryClient
11
+ }>
8
12
 
9
- function QueryProvider({ children, showDevTools = true }: QueryProviderProps) {
13
+ function QueryProvider({ children, queryClient, showDevTools = true }: QueryProviderProps) {
10
14
  return (
11
15
  <PersistQueryClientProvider client={queryClient} persistOptions={{ persister }}>
12
16
  {children}
@@ -1,8 +1,8 @@
1
1
  import { http, HttpResponse } from 'msw'
2
2
 
3
- import type { IMessageWithSenderData } from '@/src/modules/messages'
4
3
  import { MessagesEndpoints, MSG_MAX_COUNT } from '@/src/modules/messages'
5
4
  import IMessageWithSenderDataMock from '@/src/modules/messages/__tests__/imessage-with-sender-data.mock'
5
+ import SignedUrlsResponseBuilder from '@/src/modules/messages/__tests__/signed-urls.builder'
6
6
  import { ProfileEndpoints } from '@/src/modules/profile'
7
7
  import ProfileAPIPropsBuilder from '@/src/modules/profile/__tests__/profile-api-props.builder'
8
8
 
@@ -19,13 +19,14 @@ export const handlers = [
19
19
  http.all(ProfileEndpoints.getProfile(), () => {
20
20
  return HttpResponse.json(new ProfileAPIPropsBuilder())
21
21
  }),
22
+ http.all(MessagesEndpoints.getSignedUrls(), () => {
23
+ return HttpResponse.json(new SignedUrlsResponseBuilder())
24
+ }),
22
25
  http.all(MessagesEndpoints.getAll(':conversationId'), ({ request }) => {
23
26
  const limit = Number(new URL(request.url)?.searchParams?.get?.('limit'))
24
27
 
25
28
  return HttpResponse.json(
26
- new IMessageWithSenderDataMock().getMany(
27
- isNaN(limit) ? MSG_MAX_COUNT : limit
28
- ) as IMessageWithSenderData[]
29
+ new IMessageWithSenderDataMock().getMany(isNaN(limit) ? MSG_MAX_COUNT : limit)
29
30
  )
30
31
  })
31
32
  ]
@@ -1,3 +1,4 @@
1
+ import { getTheme } from '@/src/lib/utils'
1
2
  import type { Theme } from '@/src/types'
2
3
 
3
4
  export function initTheme(theme: Theme) {
@@ -5,11 +6,16 @@ export function initTheme(theme: Theme) {
5
6
 
6
7
  if (!rootElement) return
7
8
 
8
- if (theme === 'dark') {
9
- rootElement.classList.remove('bg-neutral-100')
10
- return rootElement.classList.add(theme, 'bg-ai-dark')
9
+ const currentTheme = getTheme(theme)
10
+
11
+ const darkClasses = ['dark', 'bg-ai-dark']
12
+ const lightClasses = ['bg-neutral-100']
13
+
14
+ rootElement.classList.remove(...darkClasses, ...lightClasses)
15
+
16
+ if (currentTheme === 'dark') {
17
+ return rootElement.classList.add(...darkClasses)
11
18
  }
12
19
 
13
- rootElement.classList.remove('dark', 'bg-ai-dark')
14
- return rootElement.classList.add('bg-neutral-100')
20
+ return rootElement.classList.add(...lightClasses)
15
21
  }
@@ -0,0 +1,61 @@
1
+ import './config/styles/global.css'
2
+ import './config/styles/index.css'
3
+
4
+ import { StrictMode } from 'react'
5
+ import { createRoot } from 'react-dom/client'
6
+
7
+ import { initTheme } from '@/src/config/theme'
8
+ import { version } from '../package.json'
9
+
10
+ import { initLanguage } from './config/i18n'
11
+ import { devMode, productionMode } from './lib/utils'
12
+ import { Main } from './main'
13
+ import { TutorWidgetEvents } from './modules/widget'
14
+ import type { Theme, WidgetSettingProps } from './types'
15
+
16
+ const loadMainStyles = () => {
17
+ const isProduction = productionMode
18
+ const bundlePath = !isProduction
19
+ ? `${process.env.BUNDLE_PATH}/`
20
+ : `${process.env.BUNDLE_PATH}/${process.env.APP_NAME}/_current/`
21
+
22
+ const cssPath = `${bundlePath}app-tutor-ai-consumer.css?v=${version}`
23
+
24
+ if (!document.querySelector(`link[href="${cssPath}"]`)) {
25
+ const linkElement = document.createElement('link')
26
+ linkElement.rel = 'stylesheet'
27
+ linkElement.href = cssPath
28
+ document.head.appendChild(linkElement)
29
+ }
30
+ }
31
+
32
+ window.startChatWidget = async (
33
+ elementId = 'tutor-chat-app-widget',
34
+ settings: WidgetSettingProps
35
+ ) => {
36
+ if (!devMode) {
37
+ loadMainStyles()
38
+ }
39
+
40
+ const rootElement = document.getElementById(elementId) as HTMLElement
41
+
42
+ if (!rootElement) return
43
+
44
+ rootElement.setAttribute('id', 'hotmart-app-tutor-ai-consumer-root')
45
+ const theme = (rootElement.getAttribute('data-theme') ?? 'dark') as Theme
46
+ const root = createRoot(rootElement)
47
+
48
+ await initLanguage(settings.locale)
49
+ initTheme(theme)
50
+
51
+ if (root) {
52
+ root.render(
53
+ <StrictMode>
54
+ <Main settings={{ ...settings, config: { ...settings.config, theme } }} />
55
+ </StrictMode>
56
+ )
57
+ }
58
+ }
59
+
60
+ window.closeChatWidget = () =>
61
+ Promise.resolve(TutorWidgetEvents['c3po-app-widget-close'].dispatch())
package/src/index.tsx CHANGED
@@ -1,17 +1,18 @@
1
1
  import './config/styles/global.css'
2
2
  import './config/styles/index.css'
3
3
 
4
- import { StrictMode } from 'react'
5
4
  import { createRoot } from 'react-dom/client'
6
5
 
7
- import { initTheme } from '@/src/config/theme'
6
+ import { queryClient } from '@/src/config/tanstack'
8
7
  import { version } from '../package.json'
9
8
 
10
- import { initLanguage } from './config/i18n'
9
+ import { bootstrap } from './bootstrap'
10
+ import { initTheme } from './config/theme'
11
11
  import { devMode, productionMode } from './lib/utils'
12
- import { Main } from './main'
13
- import { TutorWidgetEvents } from './modules/widget'
12
+ import { SparkieService } from './modules/sparkie'
13
+ import { createStore } from './modules/widget/store'
14
14
  import type { Theme, WidgetSettingProps } from './types'
15
+ import Wrapper from './wrapper'
15
16
 
16
17
  const loadMainStyles = () => {
17
18
  const isProduction = productionMode
@@ -33,28 +34,90 @@ window.startChatWidget = async (
33
34
  elementId = 'tutor-chat-app-widget',
34
35
  settings: WidgetSettingProps
35
36
  ) => {
37
+ if (window.__CHAT_WIDGET_INSTANCE__) {
38
+ await window.closeChatWidget()
39
+ }
40
+
36
41
  if (!devMode) {
37
42
  loadMainStyles()
38
43
  }
39
44
 
40
- const rootElement = document.getElementById(elementId) as HTMLElement
41
-
42
- if (!rootElement) return
45
+ const container = document.getElementById(elementId) as HTMLElement
43
46
 
44
- rootElement.setAttribute('id', 'hotmart-app-tutor-ai-consumer-root')
45
- const theme = (rootElement.getAttribute('data-theme') ?? 'dark') as Theme
46
- const root = createRoot(rootElement)
47
+ if (!container) return
47
48
 
48
- await initLanguage(settings.locale)
49
+ container.setAttribute('id', 'hotmart-app-tutor-ai-consumer-root')
50
+ const theme = (container.getAttribute('data-theme') ?? 'dark') as Theme
49
51
  initTheme(theme)
50
52
 
51
- if (root) {
53
+ const root = createRoot(container)
54
+ const widgetSettings = { ...settings, config: { ...settings.config, theme } }
55
+ const store = createStore()
56
+
57
+ root.render(
58
+ <Wrapper settings={widgetSettings} store={store} queryClient={queryClient} state={'LOADING'} />
59
+ )
60
+
61
+ try {
62
+ await bootstrap({ queryClient, settings })
63
+
52
64
  root.render(
53
- <StrictMode>
54
- <Main settings={{ ...settings, config: { ...settings.config, theme } }} />
55
- </StrictMode>
65
+ <Wrapper settings={widgetSettings} store={store} queryClient={queryClient} state={'READY'} />
56
66
  )
67
+
68
+ window.__CHAT_WIDGET_INSTANCE__ = { root, container, queryClient }
69
+ } catch (error) {
70
+ root.render(
71
+ <Wrapper settings={widgetSettings} store={store} queryClient={queryClient} state={'ERROR'} />
72
+ )
73
+
74
+ console.error('ERROR:Initializing Chat Widget', error)
75
+
76
+ window.__CHAT_WIDGET_INSTANCE__ = undefined
57
77
  }
58
78
  }
59
79
 
60
- window.closeChatWidget = () => TutorWidgetEvents['c3po-app-widget-close'].dispatch()
80
+ window.closeChatWidget = async () => {
81
+ const chatWidgetInstance = window.__CHAT_WIDGET_INSTANCE__
82
+
83
+ if (!chatWidgetInstance) return
84
+
85
+ const { root, container, queryClient } = chatWidgetInstance
86
+
87
+ try {
88
+ await queryClient.cancelQueries()
89
+ } catch (err) {
90
+ console.error('Error cancelling queries on widget close', err)
91
+ }
92
+
93
+ try {
94
+ root.unmount()
95
+ } catch (err) {
96
+ console.warn('Error unmounting widget root', err)
97
+ }
98
+
99
+ if (typeof queryClient.clear === 'function') {
100
+ try {
101
+ queryClient.clear()
102
+ } catch {
103
+ queryClient.getQueryCache().clear()
104
+ queryClient.getMutationCache().clear()
105
+ }
106
+ } else {
107
+ queryClient.getQueryCache().clear()
108
+ queryClient.getMutationCache().clear()
109
+ }
110
+
111
+ try {
112
+ container.remove()
113
+ } catch {
114
+ // ignore
115
+ }
116
+
117
+ try {
118
+ await SparkieService.destroySparkie()
119
+ window.__CHAT_WIDGET_INSTANCE__ = undefined
120
+ } catch (err) {
121
+ console.error('Error destroying Sparkie instance', err)
122
+ }
123
+ }
@@ -0,0 +1,87 @@
1
+ import { useRef, useState } from 'react'
2
+ import clsx from 'clsx'
3
+ import { useTranslation } from 'react-i18next'
4
+
5
+ import { Button, Icon } from '@/src/lib/components'
6
+ import ButtonDefault from '@/src/lib/components/button/button-default'
7
+ import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
8
+ import { useClickOutside } from '@/src/lib/hooks'
9
+
10
+ import type { DropdownActionsProps } from './types'
11
+
12
+ function DropdownActions({
13
+ triggerIcon,
14
+ items,
15
+ disabled = false,
16
+ triggerClassName,
17
+ triggerIconClassName
18
+ }: DropdownActionsProps) {
19
+ const { t } = useTranslation()
20
+ const [visible, setVisibile] = useState(false)
21
+ const dropdownRef = useRef<HTMLDivElement>(null)
22
+ useClickOutside(dropdownRef, handleCloseDropdown)
23
+
24
+ function handleCloseDropdown() {
25
+ setVisibile(false)
26
+ }
27
+
28
+ function handleToggleVisibility() {
29
+ setVisibile((prevState) => !prevState)
30
+ }
31
+
32
+ function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
33
+ event.preventDefault()
34
+ event.stopPropagation()
35
+ handleToggleVisibility()
36
+ }
37
+
38
+ function handleDropdownItemClick(
39
+ event: React.MouseEvent<HTMLButtonElement>,
40
+ callback: () => void
41
+ ) {
42
+ event.preventDefault()
43
+ event.stopPropagation()
44
+ handleCloseDropdown()
45
+ callback()
46
+ }
47
+
48
+ return (
49
+ <div ref={dropdownRef} className='relative'>
50
+ <Button
51
+ onClick={handleClick}
52
+ disabled={disabled}
53
+ aria-label={t('general.buttons.open_options')}
54
+ className={clsx(
55
+ triggerClassName,
56
+ 'bg-neutral-100 p-0 hover:bg-neutral-200 focus:bg-neutral-200'
57
+ )}>
58
+ <Icon
59
+ name={triggerIcon as ValidIconNames}
60
+ className={clsx(triggerIconClassName, 'text-neutral-900')}
61
+ />
62
+ </Button>
63
+
64
+ {visible && (
65
+ <div className='absolute bottom-full mb-2 flex flex-col rounded-md border border-neutral-300 bg-neutral-100 shadow-lg'>
66
+ {items.map((item) => {
67
+ if (!item.visible) return null
68
+
69
+ return (
70
+ <ButtonDefault
71
+ key={item.label}
72
+ onClick={(event) => handleDropdownItemClick(event, item.callback)}
73
+ className='flex flex-row items-center justify-start rounded-md px-4 py-3 text-sm font-normal transition-colors duration-150 hover:bg-neutral-200'>
74
+ {item.icon && <Icon name={item.icon} className='mr-4 h-4 w-4 text-neutral-500' />}
75
+ <span className='line-clamp-1 min-w-[5rem] max-w-[10rem] break-words text-left text-neutral-700'>
76
+ {t(item.label)}
77
+ </span>
78
+ </ButtonDefault>
79
+ )
80
+ })}
81
+ </div>
82
+ )}
83
+ </div>
84
+ )
85
+ }
86
+
87
+ export default DropdownActions
@@ -0,0 +1,58 @@
1
+ import { chance } from '@/src/config/tests'
2
+
3
+ import type { DropdownActionsProps } from './types'
4
+
5
+ export class DropdownActionsBuilder implements DropdownActionsProps {
6
+ items: DropdownActionsProps['items']
7
+ triggerIcon?: DropdownActionsProps['triggerIcon']
8
+ disabled?: boolean
9
+ className?: string
10
+ triggerClassName?: string
11
+ dropdownClassName?: string
12
+
13
+ constructor() {
14
+ this.items = [
15
+ {
16
+ label: chance.word(),
17
+ visible: true,
18
+ callback: vi.fn() as () => void
19
+ }
20
+ ]
21
+ this.triggerIcon = 'plus'
22
+ this.disabled = false
23
+ }
24
+
25
+ withTriggerIcon(triggerIcon: DropdownActionsProps['triggerIcon']) {
26
+ this.triggerIcon = triggerIcon
27
+ return this
28
+ }
29
+
30
+ withItems(items: DropdownActionsProps['items']) {
31
+ this.items = items
32
+ return this
33
+ }
34
+
35
+ withDisabled(disabled: boolean) {
36
+ this.disabled = disabled
37
+ return this
38
+ }
39
+
40
+ withClassName(className: string) {
41
+ this.className = className
42
+ return this
43
+ }
44
+
45
+ withTriggerClassName(triggerClassName: string) {
46
+ this.triggerClassName = triggerClassName
47
+ return this
48
+ }
49
+
50
+ withDropdownClassName(dropdownClassName: string) {
51
+ this.dropdownClassName = dropdownClassName
52
+ return this
53
+ }
54
+
55
+ build() {
56
+ return this
57
+ }
58
+ }
@@ -0,0 +1,76 @@
1
+ import { chance, render, screen } from '@/src/config/tests'
2
+ import { DropdownActions } from '../dropdown-actions'
3
+
4
+ import { DropdownActionsBuilder } from './dropdownActions.builder'
5
+ import type { DropdownActionsProps } from './types'
6
+
7
+ const defaultProps = new DropdownActionsBuilder().build()
8
+
9
+ const renderComponent = (props: DropdownActionsProps = defaultProps) => {
10
+ return render(<DropdownActions {...props} />)
11
+ }
12
+
13
+ describe('DropdownActions', () => {
14
+ it('should render with success', async () => {
15
+ const { items } = defaultProps
16
+
17
+ const { user } = renderComponent()
18
+
19
+ const dropdown = screen.getByRole('button', { name: /general.buttons.open_options/i })
20
+
21
+ await user.click(dropdown)
22
+
23
+ expect(screen.getByText(items[0].label)).toBeInTheDocument()
24
+ })
25
+
26
+ describe('when `visible` is `false`', () => {
27
+ it('should not render dropdown item', async () => {
28
+ const label = chance.word()
29
+ const items = [
30
+ {
31
+ label,
32
+ visible: false,
33
+ callback: vi.fn() as () => void
34
+ }
35
+ ]
36
+
37
+ const props = new DropdownActionsBuilder().withItems(items).build()
38
+
39
+ const { user } = renderComponent(props)
40
+
41
+ const dropdown = screen.getByRole('button', { name: /general.buttons.open_options/i })
42
+
43
+ await user.click(dropdown)
44
+
45
+ expect(screen.queryByText(label)).not.toBeInTheDocument()
46
+ })
47
+ })
48
+
49
+ describe('when item has icon', () => {
50
+ it('should render label with icon', async () => {
51
+ const label = chance.word()
52
+ const items = [
53
+ {
54
+ label,
55
+ visible: true,
56
+ icon: 'file' as const,
57
+ callback: vi.fn() as () => void
58
+ }
59
+ ]
60
+
61
+ const props = new DropdownActionsBuilder().withItems(items).build()
62
+
63
+ const { user } = renderComponent(props)
64
+
65
+ const dropdown = screen.getByRole('button', { name: /general.buttons.open_options/i })
66
+
67
+ await user.click(dropdown)
68
+
69
+ expect(screen.getByText(label)).toBeInTheDocument()
70
+
71
+ await user.click(screen.getByText(label))
72
+
73
+ expect(props.items[0].callback).toHaveBeenCalledTimes(1)
74
+ })
75
+ })
76
+ })
@@ -0,0 +1 @@
1
+ export { default as DropdownActions } from './dropdown-actions'
@@ -0,0 +1,16 @@
1
+ import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
2
+
3
+ export type DropdownItem = {
4
+ label: string
5
+ callback: () => void
6
+ icon?: ValidIconNames
7
+ visible?: boolean
8
+ }
9
+
10
+ export type DropdownActionsProps = {
11
+ items: DropdownItem[]
12
+ disabled?: boolean
13
+ triggerClassName?: string
14
+ triggerIcon?: ValidIconNames
15
+ triggerIconClassName?: string
16
+ }
@@ -3,11 +3,12 @@ import { useTranslation } from 'react-i18next'
3
3
  import ErrorDarkSVG from '@/public/assets/svg/error-dark.svg?url'
4
4
  import ErrorLightSVG from '@/public/assets/svg/error-light.svg?url'
5
5
  import { Button } from '@/src/lib/components'
6
- import { PageLayout, TutorWidgetEvents } from '@/src/modules/widget'
6
+ import { PageLayout, TutorWidgetEvents, useIsAgentParentAtomValue } from '@/src/modules/widget'
7
7
  import { WidgetHeader } from '@/src/modules/widget/components/header'
8
8
 
9
9
  function GenericError({ isDarkMode = false }: { isDarkMode?: boolean }) {
10
10
  const { t } = useTranslation()
11
+ const isAgentMode = useIsAgentParentAtomValue()
11
12
 
12
13
  return (
13
14
  <PageLayout className='p-5'>
@@ -36,13 +37,15 @@ function GenericError({ isDarkMode = false }: { isDarkMode?: boolean }) {
36
37
  {t('general.buttons.try_again')}
37
38
  </Button>
38
39
 
39
- <Button
40
- variant='secondary'
41
- className='mx-auto w-full max-w-max rounded-lg !px-9 py-2 !font-light'
42
- onClick={() => TutorWidgetEvents['c3po-app-widget-hide'].dispatch()}
43
- aria-label='Close Button'>
44
- {t('general.buttons.close')}
45
- </Button>
40
+ {!isAgentMode && (
41
+ <Button
42
+ variant='secondary'
43
+ className='mx-auto w-full max-w-max rounded-lg !px-9 py-2 !font-light'
44
+ onClick={() => TutorWidgetEvents['c3po-app-widget-hide'].dispatch()}
45
+ aria-label='Close Button'>
46
+ {t('general.buttons.close')}
47
+ </Button>
48
+ )}
46
49
  </div>
47
50
  </div>
48
51
  </PageLayout>