app-tutor-ai-consumer 1.8.1 → 1.9.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 +11 -0
- package/config/rspack/rspack.config.js +4 -6
- package/config/rspack/utils/plugins.js +2 -2
- package/config/vitest/__mocks__/sparkie.tsx +6 -1
- package/config/vitest/vitest.config.mts +0 -1
- package/package.json +1 -1
- package/src/development-bootstrap.tsx +16 -1
- package/src/index.tsx +34 -15
- package/src/lib/contexts/index.ts +1 -0
- package/src/lib/contexts/shared-ref/index.ts +1 -0
- package/src/lib/contexts/shared-ref/shared-ref.tsx +26 -0
- package/src/main/main.spec.tsx +6 -3
- package/src/main/main.tsx +19 -1
- package/src/modules/cursor/service.ts +20 -2
- package/src/modules/global-providers/global-providers.tsx +5 -0
- package/src/modules/messages/components/messages-list/messages-list.tsx +12 -12
- package/src/modules/messages/events.ts +25 -0
- package/src/modules/messages/hooks/use-send-text-message/use-send-text-message.spec.tsx +21 -0
- package/src/modules/messages/hooks/use-send-text-message/use-send-text-message.tsx +2 -0
- package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +16 -1
- package/src/modules/messages/store/index.ts +1 -0
- package/src/modules/messages/store/unread-messages-set.atom.ts +21 -0
- package/src/modules/messages/types.ts +4 -0
- package/src/modules/sparkie/service.ts +14 -4
- package/src/modules/thread/hooks/index.ts +1 -0
- package/src/modules/thread/hooks/use-subscribe-thread-closed-event/index.ts +2 -0
- package/src/modules/thread/hooks/use-subscribe-thread-closed-event/use-subscribe-thread-closed-event.tsx +22 -0
- package/src/modules/thread/index.ts +1 -0
- package/src/modules/widget/components/container/container.tsx +2 -0
- package/src/modules/widget/components/page-layout/constants.tsx +8 -0
- package/src/modules/widget/components/page-layout/index.ts +1 -0
- package/src/modules/widget/components/page-layout/page-layout.tsx +18 -3
- package/src/modules/widget/components/scroll-to-bottom-button/scroll-to-bottom-button.tsx +3 -4
- package/src/modules/widget/events.ts +53 -12
- package/src/modules/widget/hooks/index.ts +1 -1
- package/src/modules/widget/hooks/use-listen-to-visibility-events/index.ts +1 -0
- package/src/modules/widget/hooks/use-listen-to-visibility-events/use-listen-to-visibility-events.tsx +20 -0
- package/src/modules/widget/store/widget-tabs.atom.ts +2 -2
- package/src/modules/widget/types.ts +3 -3
- package/src/types.ts +6 -0
- package/config/vitest/__mocks__/use-init-sparkie.tsx +0 -14
- package/src/modules/widget/hooks/use-init-sparkie/index.ts +0 -1
- package/src/modules/widget/hooks/use-init-sparkie/use-init-sparkie.tsx +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
# [1.9.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.8.2...v1.9.0) (2025-07-17)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- add on close listener ([c41e700](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/c41e700241ac7a3f31d8091eb4e03141fd75c83e))
|
|
6
|
+
- add thread event listner ([904b78d](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/904b78dd3271552d2167d467814aa4add1550201))
|
|
7
|
+
- add useSubscribeThreadClosed event listener ([edfa1ee](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/edfa1ee8cd7296292ad485d283f3675d6a626ac1))
|
|
8
|
+
- add useSubscribeThreadClosed to container.tsx ([53aa939](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/53aa939561a1ad3415ba7735344632390015764b))
|
|
9
|
+
|
|
10
|
+
## [1.8.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.8.1...v1.8.2) (2025-07-16)
|
|
11
|
+
|
|
1
12
|
## [1.8.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.8.0...v1.8.1) (2025-07-15)
|
|
2
13
|
|
|
3
14
|
# [1.8.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.7.0...v1.8.0) (2025-07-14)
|
|
@@ -11,6 +11,7 @@ require('dotenv').config({
|
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
module.exports = async function (env) {
|
|
14
|
+
const standaloneMode = env?.local
|
|
14
15
|
const productionMode = env?.production || env?.staging
|
|
15
16
|
const releaseFullName = `app-tutor-ai-consumer_v${packageJSON.version}`
|
|
16
17
|
const fileVersion = `${productionMode ? 'prod' : 'dev'}-${packageJSON?.version}`
|
|
@@ -23,7 +24,7 @@ module.exports = async function (env) {
|
|
|
23
24
|
*/
|
|
24
25
|
const config = {
|
|
25
26
|
mode: productionMode ? 'production' : 'development',
|
|
26
|
-
entry:
|
|
27
|
+
entry: standaloneMode ? paths.DEV_BOOTSTRAP : paths.INDEX,
|
|
27
28
|
...(productionMode ? {} : require('./utils/devserver.config')),
|
|
28
29
|
optimization: {
|
|
29
30
|
usedExports: true,
|
|
@@ -159,17 +160,14 @@ module.exports = async function (env) {
|
|
|
159
160
|
new rspack.DefinePlugin({
|
|
160
161
|
'process.env.PROJECT_VERSION': JSON.stringify(packageJSON.version),
|
|
161
162
|
'process.env.TARGET_ENV': JSON.stringify(process.env.NODE_ENV),
|
|
162
|
-
'process.env.RELEASE_FULL_NAME': JSON.stringify(releaseFullName)
|
|
163
|
+
'process.env.RELEASE_FULL_NAME': JSON.stringify(releaseFullName),
|
|
164
|
+
'process.env.STANDALONE_MODE': JSON.stringify(standaloneMode)
|
|
163
165
|
}),
|
|
164
166
|
new rspack.ProgressPlugin(),
|
|
165
167
|
new TsCheckerRspackPlugin(),
|
|
166
168
|
new rspack.HtmlRspackPlugin({
|
|
167
169
|
template: path.resolve(paths.PUBLIC, 'index.html'),
|
|
168
170
|
favicon: path.resolve(paths.PUBLIC, 'favicon.ico')
|
|
169
|
-
}),
|
|
170
|
-
new rspack.CssExtractRspackPlugin({
|
|
171
|
-
filename: productionMode ? `app-tutor-ai-consumer.css` : '[name].[contenthash].css',
|
|
172
|
-
chunkFilename: productionMode ? '[id].[contenthash].css' : '[id].[contenthash].css'
|
|
173
171
|
})
|
|
174
172
|
].concat(await getEnvironmentPlugins(!productionMode)),
|
|
175
173
|
watchOptions: {
|
|
@@ -27,8 +27,8 @@ async function getEnvironmentPlugins(isDevelopment) {
|
|
|
27
27
|
return [
|
|
28
28
|
new rspack.EnvironmentPlugin(allEnvs),
|
|
29
29
|
new rspack.CssExtractRspackPlugin({
|
|
30
|
-
filename: '
|
|
31
|
-
chunkFilename: '[id]
|
|
30
|
+
filename: 'app-tutor-ai-consumer.css',
|
|
31
|
+
chunkFilename: '[id].[contenthash].css'
|
|
32
32
|
}),
|
|
33
33
|
new CompressionPlugin({
|
|
34
34
|
algorithm: 'gzip'
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import SparkieMock from '@/src/modules/sparkie/__tests__/sparkie.mock'
|
|
2
2
|
import { SparkieService } from '@/src/modules/sparkie'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
SparkieMessageServiceMock,
|
|
5
|
+
SparkieCursorServiceMock
|
|
6
|
+
} from '@/src/modules/sparkie/__tests__/sparkie.mock'
|
|
4
7
|
import MessageService from '@hotmart/sparkie/dist/MessageService'
|
|
5
8
|
|
|
6
9
|
vi.mock('@hotmart/sparkie', () => ({ default: SparkieMock }))
|
|
@@ -9,4 +12,6 @@ beforeEach(() => {
|
|
|
9
12
|
vi.spyOn(SparkieService, 'getMessageService').mockResolvedValue(
|
|
10
13
|
SparkieMessageServiceMock as unknown as MessageService
|
|
11
14
|
)
|
|
15
|
+
|
|
16
|
+
vi.spyOn(SparkieService, 'getCursorService').mockResolvedValue(SparkieCursorServiceMock as never)
|
|
12
17
|
})
|
|
@@ -14,7 +14,6 @@ export default defineConfig({
|
|
|
14
14
|
'./config/vitest/__mocks__/sparkie.tsx',
|
|
15
15
|
'./config/vitest/__mocks__/icons.tsx',
|
|
16
16
|
'./config/vitest/__mocks__/intersection-observer.ts',
|
|
17
|
-
'./config/vitest/__mocks__/use-init-sparkie.tsx',
|
|
18
17
|
'./config/vitest/polyfills/global.js'
|
|
19
18
|
],
|
|
20
19
|
coverage: {
|
package/package.json
CHANGED
|
@@ -5,10 +5,25 @@ import { v4 } from 'uuid'
|
|
|
5
5
|
import { LANGUAGES } from './config/i18n'
|
|
6
6
|
import { devMode } from './lib/utils'
|
|
7
7
|
|
|
8
|
+
const rootId = 'app-tutor-ai-widget'
|
|
9
|
+
|
|
8
10
|
if (devMode) {
|
|
9
11
|
window.TOKEN = process.env.TOKEN ?? ''
|
|
10
12
|
void (async () => {
|
|
11
|
-
|
|
13
|
+
// TODO: Remove after sidebar implementation
|
|
14
|
+
// Add Local popup css config
|
|
15
|
+
const root = document.getElementById('c3po-app-widget')
|
|
16
|
+
const container = document.createElement('div')
|
|
17
|
+
|
|
18
|
+
container.setAttribute('id', rootId)
|
|
19
|
+
container.setAttribute(
|
|
20
|
+
'class',
|
|
21
|
+
'bg-ai-dark fixed bottom-5 right-5 w-[27rem] h-[min(43rem,calc(100vh-2.5rem))] max-h-[calc(100vh-2.5rem)] z-10 rounded-[0.625rem] border border-neutral-800 shadow-lg overflow-hidden flex flex-col'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
root?.appendChild(container)
|
|
25
|
+
|
|
26
|
+
await window.startChatWidget(rootId, {
|
|
12
27
|
hotmartToken: window.TOKEN,
|
|
13
28
|
locale: LANGUAGES.PT_BR,
|
|
14
29
|
conversationId: '21506473-a93c-4b38-9c32-68a5ca37ce73', // OWNER
|
package/src/index.tsx
CHANGED
|
@@ -7,10 +7,10 @@ import { createRoot } from 'react-dom/client'
|
|
|
7
7
|
import { initDayjs } from './config/dayjs'
|
|
8
8
|
import { initLanguage } from './config/i18n'
|
|
9
9
|
import { initAxios } from './config/request/api'
|
|
10
|
-
import { productionMode } from './lib/utils'
|
|
10
|
+
import { devMode, productionMode } from './lib/utils'
|
|
11
11
|
import { Main } from './main'
|
|
12
12
|
import { SparkieService } from './modules/sparkie'
|
|
13
|
-
import { TutorWidgetEvents
|
|
13
|
+
import { TutorWidgetEvents } from './modules/widget'
|
|
14
14
|
import type { WidgetSettingProps } from './types'
|
|
15
15
|
|
|
16
16
|
const loadMainStyles = () => {
|
|
@@ -33,30 +33,49 @@ window.startChatWidget = async (
|
|
|
33
33
|
elementId = 'tutor-chat-app-widget',
|
|
34
34
|
settings: WidgetSettingProps
|
|
35
35
|
) => {
|
|
36
|
-
|
|
36
|
+
if (!devMode) {
|
|
37
|
+
loadMainStyles()
|
|
38
|
+
}
|
|
37
39
|
|
|
38
40
|
const rootElement = document.getElementById(elementId) as HTMLElement
|
|
39
41
|
const root = createRoot(rootElement)
|
|
40
42
|
|
|
41
|
-
await SparkieService.initSparkie({
|
|
42
|
-
token: settings.hotmartToken,
|
|
43
|
-
skipPresenceSetup: true,
|
|
44
|
-
retryOptions: {
|
|
45
|
-
maxRetries: 5,
|
|
46
|
-
retryDelay: 2000,
|
|
47
|
-
backoffMultiplier: 1.5
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
43
|
initAxios(settings.hotmartToken)
|
|
51
44
|
await initLanguage(settings.locale)
|
|
52
45
|
await initDayjs(settings.locale)
|
|
53
46
|
|
|
54
|
-
|
|
47
|
+
let isLoadingSparkie: boolean = false
|
|
48
|
+
let initSparkieError: unknown = null
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
isLoadingSparkie = true
|
|
52
|
+
await SparkieService.initSparkie({
|
|
53
|
+
token: settings?.hotmartToken,
|
|
54
|
+
skipPresenceSetup: true,
|
|
55
|
+
retryOptions: {
|
|
56
|
+
maxRetries: 5,
|
|
57
|
+
retryDelay: 2000,
|
|
58
|
+
backoffMultiplier: 1.5
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
await SparkieService.ensureInitialized()
|
|
62
|
+
TutorWidgetEvents['tutor-app-widget-loaded'].dispatch()
|
|
63
|
+
} catch (error) {
|
|
64
|
+
initSparkieError = error
|
|
65
|
+
console.error(error)
|
|
66
|
+
TutorWidgetEvents['tutor-app-widget-loaded'].dispatch({ detail: { isSuccess: false } })
|
|
67
|
+
} finally {
|
|
68
|
+
isLoadingSparkie = false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (root) {
|
|
72
|
+
TutorWidgetEvents['c3po-app-widget-open'].dispatch()
|
|
55
73
|
root.render(
|
|
56
74
|
<StrictMode>
|
|
57
|
-
<Main settings={settings} />
|
|
75
|
+
<Main settings={settings} metadata={{ initSparkieError, isLoadingSparkie }} />
|
|
58
76
|
</StrictMode>
|
|
59
77
|
)
|
|
78
|
+
}
|
|
60
79
|
}
|
|
61
80
|
|
|
62
|
-
window.closeChatWidget = () => TutorWidgetEvents.
|
|
81
|
+
window.closeChatWidget = () => TutorWidgetEvents['c3po-app-widget-close'].dispatch()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './shared-ref'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as createSharedRefContext } from './shared-ref'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createContext, useContext, useRef } from 'react'
|
|
2
|
+
import type { PropsWithChildren, RefObject } from 'react'
|
|
3
|
+
|
|
4
|
+
function createSharedRefContext<T extends HTMLElement>() {
|
|
5
|
+
const SharedRefContext = createContext<RefObject<T | null> | null>(null)
|
|
6
|
+
|
|
7
|
+
const SharedRefContextProvider = ({ children }: PropsWithChildren) => {
|
|
8
|
+
const sharedRef = useRef<T>(null)
|
|
9
|
+
return <SharedRefContext.Provider value={sharedRef}>{children}</SharedRefContext.Provider>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const useSharedRefContext = () => {
|
|
13
|
+
const ctx = useContext(SharedRefContext)
|
|
14
|
+
|
|
15
|
+
if (!ctx) throw new Error('SharedRefContext must be used inside a SharedRefContextProvider')
|
|
16
|
+
return ctx
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
SharedRefContext,
|
|
21
|
+
SharedRefContextProvider,
|
|
22
|
+
useSharedRefContext
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default createSharedRefContext
|
package/src/main/main.spec.tsx
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { chance, render, screen, waitFor } from '@/config/tests'
|
|
2
|
+
import { useWidgetSettingsAtom } from '@/src/modules/widget/store/widget-settings.atom'
|
|
2
3
|
import WidgetSettingPropsBuilder from '../modules/widget/__tests__/widget-settings-props.builder'
|
|
3
|
-
import { useWidgetSettingsAtom } from '../modules/widget/store/widget-settings.atom'
|
|
4
4
|
import { Main } from '.'
|
|
5
5
|
|
|
6
|
-
vi.mock('
|
|
6
|
+
vi.mock('@/src/modules/widget/store/widget-settings.atom', async (importOriginal) => ({
|
|
7
|
+
...(await importOriginal()),
|
|
8
|
+
useWidgetSettingsAtom: vi.fn()
|
|
9
|
+
}))
|
|
7
10
|
|
|
8
11
|
describe('Main', () => {
|
|
9
12
|
const defaultProps = new WidgetSettingPropsBuilder()
|
|
@@ -20,7 +23,7 @@ describe('Main', () => {
|
|
|
20
23
|
renderComponent({ settings: props })
|
|
21
24
|
|
|
22
25
|
await waitFor(() => {
|
|
23
|
-
expect(screen.getByText(/
|
|
26
|
+
expect(screen.getByText(/send/i)).toBeInTheDocument()
|
|
24
27
|
})
|
|
25
28
|
})
|
|
26
29
|
})
|
package/src/main/main.tsx
CHANGED
|
@@ -3,13 +3,31 @@ import '@/config/styles/index.css'
|
|
|
3
3
|
import { ErrorBoundary, GenericError } from '@/src/lib/components/errors'
|
|
4
4
|
import { useDefaultId } from '@/src/lib/hooks'
|
|
5
5
|
import { useAppLang } from '../config/i18n'
|
|
6
|
+
import { Spinner } from '../lib/components'
|
|
6
7
|
import { GlobalProviders } from '../modules/global-providers'
|
|
7
8
|
import { WidgetContainer } from '../modules/widget'
|
|
9
|
+
import { useListenToVisibilityEvents } from '../modules/widget/hooks'
|
|
8
10
|
import type { WidgetSettingProps } from '../types'
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
export type MainProps = {
|
|
13
|
+
settings: WidgetSettingProps
|
|
14
|
+
metadata?: { isLoadingSparkie: boolean; initSparkieError: unknown }
|
|
15
|
+
}
|
|
16
|
+
function Main({
|
|
17
|
+
settings,
|
|
18
|
+
metadata = { initSparkieError: null, isLoadingSparkie: false }
|
|
19
|
+
}: MainProps) {
|
|
11
20
|
useDefaultId()
|
|
12
21
|
useAppLang(settings.locale)
|
|
22
|
+
useListenToVisibilityEvents()
|
|
23
|
+
|
|
24
|
+
if (metadata.isLoadingSparkie) {
|
|
25
|
+
return (
|
|
26
|
+
<div className='flex h-full w-full flex-col items-center justify-center'>
|
|
27
|
+
<Spinner className='h-10 w-10 text-neutral-500' />
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
13
31
|
|
|
14
32
|
return (
|
|
15
33
|
<ErrorBoundary fallback={<GenericError />}>
|
|
@@ -1,12 +1,30 @@
|
|
|
1
|
+
import { ApiError } from '@/src/config/request'
|
|
2
|
+
import { HttpCodes } from '@/src/lib/utils'
|
|
1
3
|
import { SparkieService } from '../sparkie'
|
|
2
4
|
|
|
3
5
|
import type { ICursorUpdate } from './types'
|
|
4
6
|
|
|
5
7
|
class CursorService {
|
|
6
|
-
|
|
8
|
+
async getSparkieCursorService() {
|
|
9
|
+
try {
|
|
10
|
+
const messageService = await SparkieService.getCursorService()
|
|
11
|
+
|
|
12
|
+
if (!messageService) throw new Error()
|
|
13
|
+
|
|
14
|
+
return messageService
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new ApiError({
|
|
17
|
+
statusCode: HttpCodes.UNPROCESSABLE_ENTITY,
|
|
18
|
+
message: 'sparkie.cursorService not defined',
|
|
19
|
+
extra: { error }
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
|
|
8
24
|
async updateCursor(conversationId: string): Promise<ICursorUpdate | null> {
|
|
9
|
-
const
|
|
25
|
+
const cursorService = await this.getSparkieCursorService()
|
|
26
|
+
|
|
27
|
+
const data = await cursorService?.update(conversationId)
|
|
10
28
|
|
|
11
29
|
return data ?? null
|
|
12
30
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PropsWithChildren, useEffect } from 'react'
|
|
2
|
+
import { v4 } from 'uuid'
|
|
2
3
|
|
|
3
4
|
import { OptimizelyProvider } from '@/src/config/optimizely'
|
|
4
5
|
import { QueryProvider } from '@/src/config/tanstack'
|
|
@@ -13,6 +14,10 @@ function GlobalProviders({ children, settings }: GlobalProvidersProps) {
|
|
|
13
14
|
useEffect(() => {
|
|
14
15
|
if (!settings || !Object.keys(settings)?.length) return
|
|
15
16
|
|
|
17
|
+
if (!settings?.sessionId) {
|
|
18
|
+
settings.sessionId = v4()
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
setWidgetSettings(settings)
|
|
17
22
|
}, [setWidgetSettings, settings])
|
|
18
23
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { lazy, useCallback, useRef } from 'react'
|
|
2
2
|
import clsx from 'clsx'
|
|
3
|
+
import { createPortal } from 'react-dom'
|
|
3
4
|
|
|
4
|
-
import { useWidgetLoadingAtomValue } from '@/src/modules/widget'
|
|
5
|
+
import { usePageLayoutMainRefContext, useWidgetLoadingAtomValue } from '@/src/modules/widget'
|
|
5
6
|
import { useAllMessages, useManageScroll } from '../../hooks'
|
|
6
7
|
import { useSkeletonRef } from '../../hooks/use-skeleton-ref'
|
|
7
8
|
import { MessageItem } from '../message-item'
|
|
@@ -26,6 +27,7 @@ function MessagesList() {
|
|
|
26
27
|
const widgetIsLoading = useWidgetLoadingAtomValue()
|
|
27
28
|
const skeletonRef = useSkeletonRef()
|
|
28
29
|
const { showScrollButton } = useManageScroll(scrollerRef)
|
|
30
|
+
const mainLayoutRef = usePageLayoutMainRefContext()
|
|
29
31
|
|
|
30
32
|
const scrollToBottom = useCallback(() => {
|
|
31
33
|
const { current: scroller } = scrollerRef
|
|
@@ -39,23 +41,21 @@ function MessagesList() {
|
|
|
39
41
|
}, [])
|
|
40
42
|
|
|
41
43
|
return (
|
|
42
|
-
<div ref={scrollerRef} className='
|
|
44
|
+
<div ref={scrollerRef} className='mx-2 my-4 flex flex-col gap-2 overflow-auto px-4'>
|
|
43
45
|
<MessageItemLoading show={messagesQuery.isFetching} />
|
|
44
46
|
|
|
45
47
|
<MessageItemEndOfScroll
|
|
46
48
|
show={!messagesQuery.isFetching && !messagesQuery.hasNextPage && allMessages.length > 0}
|
|
47
49
|
/>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
{mainLayoutRef.current &&
|
|
51
|
+
createPortal(
|
|
52
|
+
<ScrollToBottomButton
|
|
53
|
+
ref={scrollToButtonRef}
|
|
54
|
+
show={showScrollButton}
|
|
55
|
+
onClick={scrollToBottom}
|
|
56
|
+
/>,
|
|
57
|
+
mainLayoutRef.current
|
|
55
58
|
)}
|
|
56
|
-
show={showScrollButton}
|
|
57
|
-
onClick={scrollToBottom}
|
|
58
|
-
/>
|
|
59
59
|
|
|
60
60
|
{allMessages?.map(([publishingDate, messages], i) => (
|
|
61
61
|
<div key={i} className='flex flex-1 flex-col justify-center gap-6'>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ICustomEvent } from '@/src/types'
|
|
2
|
+
|
|
3
|
+
import type { SubmitQuestionEventDetail } from './types'
|
|
4
|
+
|
|
5
|
+
export const MessagesEventTypes = {
|
|
6
|
+
SUBMIT_QUESTION: 'c3po-chat:questionSubmitted'
|
|
7
|
+
} as const
|
|
8
|
+
|
|
9
|
+
const MessagesEventsList: Array<ICustomEvent<typeof MessagesEventTypes>> = [
|
|
10
|
+
{
|
|
11
|
+
name: MessagesEventTypes.SUBMIT_QUESTION,
|
|
12
|
+
handler: () => () => undefined,
|
|
13
|
+
dispatch: () => {
|
|
14
|
+
const event: CustomEventInit<SubmitQuestionEventDetail> = {
|
|
15
|
+
detail: {
|
|
16
|
+
timestamp: Date.now()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
window.dispatchEvent(new CustomEvent(MessagesEventTypes.SUBMIT_QUESTION, event))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
] as const
|
|
24
|
+
|
|
25
|
+
export const MessagesEvents = new Map(MessagesEventsList.map((e) => [e.name, e]))
|
|
@@ -3,6 +3,7 @@ import { MessagesService } from '@/src/modules/messages'
|
|
|
3
3
|
import { useGetProfile } from '@/src/modules/profile'
|
|
4
4
|
import * as Store from '@/src/modules/widget'
|
|
5
5
|
import WidgetSettingPropsBuilder from '@/src/modules/widget/__tests__/widget-settings-props.builder'
|
|
6
|
+
import { MessagesEventTypes } from '../../events'
|
|
6
7
|
|
|
7
8
|
import useSendTextMessage from './use-send-text-message'
|
|
8
9
|
|
|
@@ -83,4 +84,24 @@ describe('useSendTextMessage', () => {
|
|
|
83
84
|
}
|
|
84
85
|
})
|
|
85
86
|
})
|
|
87
|
+
|
|
88
|
+
it('should dispatch window custom event when mutating', async () => {
|
|
89
|
+
const num = chance.integer()
|
|
90
|
+
vi.spyOn(globalThis.window, 'dispatchEvent')
|
|
91
|
+
vi.spyOn(Date, 'now').mockReturnValueOnce(num)
|
|
92
|
+
|
|
93
|
+
const { result } = render()
|
|
94
|
+
|
|
95
|
+
await waitFor(() => result.current.mutateAsync(message))
|
|
96
|
+
|
|
97
|
+
expect(globalThis.window.dispatchEvent).toHaveBeenCalledTimes(1)
|
|
98
|
+
expect(globalThis.window.dispatchEvent).toHaveBeenNthCalledWith(
|
|
99
|
+
1,
|
|
100
|
+
new CustomEvent(MessagesEventTypes.SUBMIT_QUESTION, {
|
|
101
|
+
detail: {
|
|
102
|
+
timestamp: num
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
})
|
|
86
107
|
})
|
|
@@ -4,6 +4,7 @@ import { v4 } from 'uuid'
|
|
|
4
4
|
import { MessagesService } from '@/src/modules/messages'
|
|
5
5
|
import { useGetProfile } from '@/src/modules/profile'
|
|
6
6
|
import { useWidgetLoadingAtom, useWidgetSettingsAtomValue } from '@/src/modules/widget'
|
|
7
|
+
import { MessagesEvents } from '../../events'
|
|
7
8
|
|
|
8
9
|
function useSendTextMessage() {
|
|
9
10
|
const settings = useWidgetSettingsAtomValue()
|
|
@@ -53,6 +54,7 @@ function useSendTextMessage() {
|
|
|
53
54
|
},
|
|
54
55
|
onMutate: () => {
|
|
55
56
|
setWidgetLoading(true)
|
|
57
|
+
MessagesEvents.get('c3po-chat:questionSubmitted')?.dispatch()
|
|
56
58
|
}
|
|
57
59
|
})
|
|
58
60
|
}
|
|
@@ -3,9 +3,11 @@ import type { InfiniteData } from '@tanstack/react-query'
|
|
|
3
3
|
import { useQueryClient } from '@tanstack/react-query'
|
|
4
4
|
import { produce } from 'immer'
|
|
5
5
|
|
|
6
|
+
import { useUpdateCursor } from '@/src/modules/cursor/hooks'
|
|
6
7
|
import { useGetProfile } from '@/src/modules/profile'
|
|
7
8
|
import { SparkieService } from '@/src/modules/sparkie'
|
|
8
9
|
import { useWidgetLoadingAtom, useWidgetSettingsAtom } from '@/src/modules/widget'
|
|
10
|
+
import { useUnreadMessagesSetAtom } from '../../store'
|
|
9
11
|
import type { FetchMessagesResponse, IMessageWithSenderData } from '../../types'
|
|
10
12
|
import { getMessagesInfiniteQuery } from '../use-infinite-get-messages'
|
|
11
13
|
|
|
@@ -14,6 +16,8 @@ const useSubscribeMessageReceivedEvent = () => {
|
|
|
14
16
|
const profileQuery = useGetProfile()
|
|
15
17
|
const queryClient = useQueryClient()
|
|
16
18
|
const [, setWidgetLoading] = useWidgetLoadingAtom()
|
|
19
|
+
const [, addUnreadMessagesToSet] = useUnreadMessagesSetAtom()
|
|
20
|
+
const useUpdateCursorMutation = useUpdateCursor()
|
|
17
21
|
|
|
18
22
|
const conversationId = useMemo(() => String(settings?.conversationId), [settings?.conversationId])
|
|
19
23
|
const profileId = useMemo(() => String(profileQuery?.data?.id), [profileQuery?.data?.id])
|
|
@@ -61,7 +65,11 @@ const useSubscribeMessageReceivedEvent = () => {
|
|
|
61
65
|
const isMine = data.contactId === profileId
|
|
62
66
|
|
|
63
67
|
if (!isMine) {
|
|
68
|
+
addUnreadMessagesToSet({ itemId: data.id })
|
|
64
69
|
setTimeout(() => setWidgetLoading(false), 100)
|
|
70
|
+
} else {
|
|
71
|
+
// The cursor should update only with my messages
|
|
72
|
+
useUpdateCursorMutation.mutate(data.conversationId)
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -74,7 +82,14 @@ const useSubscribeMessageReceivedEvent = () => {
|
|
|
74
82
|
messageReceived
|
|
75
83
|
})
|
|
76
84
|
}
|
|
77
|
-
}, [
|
|
85
|
+
}, [
|
|
86
|
+
addUnreadMessagesToSet,
|
|
87
|
+
profileId,
|
|
88
|
+
query.queryKey,
|
|
89
|
+
queryClient,
|
|
90
|
+
setWidgetLoading,
|
|
91
|
+
useUpdateCursorMutation
|
|
92
|
+
])
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
export default useSubscribeMessageReceivedEvent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './unread-messages-set.atom'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { atom, useAtom, useAtomValue } from 'jotai'
|
|
2
|
+
|
|
3
|
+
const unreadMessagesSetAtom = atom(new Set<string>())
|
|
4
|
+
|
|
5
|
+
const setUnreadMessagesSetAtom = atom(
|
|
6
|
+
(get) => get(unreadMessagesSetAtom),
|
|
7
|
+
(_, set, { itemId = '', clear = false }: { itemId?: string; clear?: boolean }) =>
|
|
8
|
+
set(unreadMessagesSetAtom, (p) => {
|
|
9
|
+
if (clear) return new Set<string>()
|
|
10
|
+
|
|
11
|
+
const previousItems = Array.from(p)
|
|
12
|
+
|
|
13
|
+
return new Set([...previousItems, itemId])
|
|
14
|
+
})
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const unreadMessagesAtom = atom((get) => get(unreadMessagesSetAtom).size)
|
|
18
|
+
|
|
19
|
+
export const useUnreadMessagesSetAtom = () => useAtom(setUnreadMessagesSetAtom)
|
|
20
|
+
export const useUnreadMessagesSetAtomValue = () => useAtomValue(setUnreadMessagesSetAtom)
|
|
21
|
+
export const useUnreadMessagesCount = () => useAtomValue(unreadMessagesAtom)
|
|
@@ -153,7 +153,7 @@ class SparkieService {
|
|
|
153
153
|
return false
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
async ensureInitialized(): Promise<void> {
|
|
157
157
|
if (this.isInitialized) return
|
|
158
158
|
|
|
159
159
|
switch (this.initializationState) {
|
|
@@ -178,6 +178,16 @@ class SparkieService {
|
|
|
178
178
|
return messageService
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
async getCursorService() {
|
|
182
|
+
await this.ensureInitialized()
|
|
183
|
+
|
|
184
|
+
const cursorService = this.sparkieInstance.cursorService
|
|
185
|
+
|
|
186
|
+
if (!cursorService) throw new Error('CursorService not available after initialization')
|
|
187
|
+
|
|
188
|
+
return cursorService
|
|
189
|
+
}
|
|
190
|
+
|
|
181
191
|
async updateToken(token: string, reinitialize = true): Promise<boolean> {
|
|
182
192
|
this.sparkieInstance.setAPIToken(token)
|
|
183
193
|
|
|
@@ -225,11 +235,11 @@ class SparkieService {
|
|
|
225
235
|
|
|
226
236
|
async destroySparkie(): Promise<void> {
|
|
227
237
|
try {
|
|
228
|
-
if (this.sparkie
|
|
238
|
+
if (this.sparkie) {
|
|
229
239
|
await this.sparkieInstance.destroy({ skipSignOut: true })
|
|
230
240
|
}
|
|
231
|
-
} catch
|
|
232
|
-
console.
|
|
241
|
+
} catch {
|
|
242
|
+
console.warn('Sparkie instance not available for destruction')
|
|
233
243
|
} finally {
|
|
234
244
|
this.initializationState = 'idle'
|
|
235
245
|
this.initializationPromise = null
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './use-subscribe-thread-closed-event'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useCallback, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useUnreadMessagesSetAtom } from '@/src/modules/messages/store'
|
|
4
|
+
import { SparkieService } from '@/src/modules/sparkie'
|
|
5
|
+
|
|
6
|
+
function useSubscribeThreadClosedEvent() {
|
|
7
|
+
const [, setUnreadMessagesSet] = useUnreadMessagesSetAtom()
|
|
8
|
+
|
|
9
|
+
const threadClosed = useCallback(() => {
|
|
10
|
+
setUnreadMessagesSet({ clear: true })
|
|
11
|
+
}, [setUnreadMessagesSet])
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
SparkieService.subscribeEvents({ threadClosed })
|
|
15
|
+
|
|
16
|
+
return () => {
|
|
17
|
+
SparkieService.removeEventSubscription({ threadClosed })
|
|
18
|
+
}
|
|
19
|
+
}, [threadClosed])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default useSubscribeThreadClosedEvent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './hooks'
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useSubscribeMessageReceivedEvent } from '@/src/modules/messages/hooks'
|
|
2
|
+
import { useSubscribeThreadClosedEvent } from '@/src/modules/thread/hooks'
|
|
2
3
|
import { useWidgetTabsValueAtom } from '../../store'
|
|
3
4
|
import { WIDGET_TABS } from '../constants'
|
|
4
5
|
|
|
5
6
|
function WidgetContainer() {
|
|
6
7
|
const widgetTabs = useWidgetTabsValueAtom()
|
|
7
8
|
useSubscribeMessageReceivedEvent()
|
|
9
|
+
useSubscribeThreadClosedEvent()
|
|
8
10
|
|
|
9
11
|
return (
|
|
10
12
|
<div className='flex h-full flex-col items-center justify-stretch overflow-hidden'>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createSharedRefContext } from '@/src/lib/contexts'
|
|
2
|
+
|
|
3
|
+
const { useSharedRefContext, SharedRefContext, SharedRefContextProvider } =
|
|
4
|
+
createSharedRefContext<HTMLDivElement>()
|
|
5
|
+
|
|
6
|
+
export const usePageLayoutMainRefContext = useSharedRefContext
|
|
7
|
+
export const PageLayoutMainRefContext = SharedRefContext
|
|
8
|
+
export const PageLayoutMainRefContextProvider = SharedRefContextProvider
|
|
@@ -1,18 +1,25 @@
|
|
|
1
|
+
import { type PropsWithChildren, type ReactNode } from 'react'
|
|
1
2
|
import clsx from 'clsx'
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
import { PageLayoutMainRefContextProvider, usePageLayoutMainRefContext } from './constants'
|
|
3
5
|
|
|
4
6
|
export type PageLayoutProps = PropsWithChildren<{
|
|
5
7
|
asideChild?: ReactNode
|
|
6
8
|
className?: string
|
|
7
9
|
}>
|
|
10
|
+
|
|
8
11
|
function PageLayout({ asideChild, children, className }: PageLayoutProps) {
|
|
12
|
+
const mainLayoutRef = usePageLayoutMainRefContext()
|
|
13
|
+
|
|
9
14
|
return (
|
|
10
15
|
<div
|
|
11
16
|
className={clsx(
|
|
12
17
|
'grid-areas-[main_aside] grid h-full min-h-0 w-full grid-cols-1 grid-rows-[1fr_auto]',
|
|
13
18
|
className
|
|
14
19
|
)}>
|
|
15
|
-
<div
|
|
20
|
+
<div
|
|
21
|
+
ref={mainLayoutRef}
|
|
22
|
+
className='grid-area-[main] relative flex min-h-0 flex-col overflow-y-auto overflow-x-hidden'>
|
|
16
23
|
{children}
|
|
17
24
|
</div>
|
|
18
25
|
{asideChild && (
|
|
@@ -24,4 +31,12 @@ function PageLayout({ asideChild, children, className }: PageLayoutProps) {
|
|
|
24
31
|
)
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
function PageLayoutWrapper(props: PageLayoutProps) {
|
|
35
|
+
return (
|
|
36
|
+
<PageLayoutMainRefContextProvider>
|
|
37
|
+
<PageLayout {...props} />
|
|
38
|
+
</PageLayoutMainRefContextProvider>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default PageLayoutWrapper
|
|
@@ -10,14 +10,13 @@ export interface IScrollToBottomButtonProps
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const ScrollToBottomButton = forwardRef<HTMLButtonElement, IScrollToBottomButtonProps>(
|
|
13
|
-
({ show = false,
|
|
13
|
+
({ show = false, onClick, className, ...props }, ref) => (
|
|
14
14
|
<button
|
|
15
15
|
{...props}
|
|
16
|
-
style={isNaN(Number(top)) ? undefined : { top }}
|
|
17
16
|
ref={ref}
|
|
18
17
|
className={clsx(
|
|
19
|
-
'
|
|
20
|
-
{ 'pointer-events-none opacity-0': !show },
|
|
18
|
+
'absolute bottom-4 left-1/2 flex size-7 cursor-pointer flex-col items-center justify-center rounded-full bg-neutral-600 text-sm text-neutral-50 outline-none transition-colors duration-300 ease-in hover:scale-110 hover:bg-neutral-700 focus:outline-none focus:ring-neutral-500 focus:ring-offset-2 focus-visible:ring-2 active:ring-2',
|
|
19
|
+
{ 'opacity-85': show, 'pointer-events-none opacity-0': !show },
|
|
21
20
|
className
|
|
22
21
|
)}
|
|
23
22
|
onClick={onClick}
|
|
@@ -1,24 +1,65 @@
|
|
|
1
1
|
import type { ITutorWidgetEvent } from './types'
|
|
2
2
|
|
|
3
3
|
export const TutorWidgetEventTypes = {
|
|
4
|
-
OPEN: '
|
|
5
|
-
CLOSE: '
|
|
4
|
+
OPEN: 'c3po-app-widget-open',
|
|
5
|
+
CLOSE: 'c3po-app-widget-close',
|
|
6
|
+
LOADED: 'tutor-app-widget-loaded'
|
|
6
7
|
} as const
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
{
|
|
9
|
+
const TutorWidgetEventsObject = {
|
|
10
|
+
[TutorWidgetEventTypes.OPEN]: {
|
|
10
11
|
name: TutorWidgetEventTypes.OPEN,
|
|
11
|
-
handler: () =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
handler: (callback) => {
|
|
13
|
+
const listener: EventListener = () => {
|
|
14
|
+
void callback()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
window.addEventListener(TutorWidgetEventTypes.OPEN, listener)
|
|
18
|
+
|
|
19
|
+
return () => {
|
|
20
|
+
window.removeEventListener(TutorWidgetEventTypes.OPEN, listener)
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
dispatch: () => {
|
|
24
|
+
window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.OPEN))
|
|
25
|
+
}
|
|
26
|
+
} as ITutorWidgetEvent<void>,
|
|
27
|
+
|
|
28
|
+
[TutorWidgetEventTypes.CLOSE]: {
|
|
15
29
|
name: TutorWidgetEventTypes.CLOSE,
|
|
16
|
-
handler: () =>
|
|
30
|
+
handler: (callback) => {
|
|
31
|
+
const listener: EventListener = () => {
|
|
32
|
+
void callback()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
window.addEventListener(TutorWidgetEventTypes.CLOSE, listener)
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
window.removeEventListener(TutorWidgetEventTypes.CLOSE, listener)
|
|
39
|
+
}
|
|
40
|
+
},
|
|
17
41
|
dispatch: () => window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.CLOSE))
|
|
18
|
-
}
|
|
19
|
-
|
|
42
|
+
} as ITutorWidgetEvent<void>,
|
|
43
|
+
|
|
44
|
+
[TutorWidgetEventTypes.LOADED]: {
|
|
45
|
+
name: TutorWidgetEventTypes.LOADED,
|
|
46
|
+
handler: (callback: (payload: { isSuccess: boolean }) => void) => {
|
|
47
|
+
const listener: EventListener = (e) => {
|
|
48
|
+
const evt = e as CustomEvent<{ isSuccess: boolean }>
|
|
49
|
+
callback(evt.detail)
|
|
50
|
+
}
|
|
51
|
+
window.addEventListener(TutorWidgetEventTypes.LOADED, listener)
|
|
52
|
+
return () => {
|
|
53
|
+
window.removeEventListener(TutorWidgetEventTypes.LOADED, listener)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
dispatch: (payload = { detail: { isSuccess: true } }) => {
|
|
57
|
+
window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.LOADED, payload))
|
|
58
|
+
}
|
|
59
|
+
} as ITutorWidgetEvent<{ isSuccess: boolean }>
|
|
60
|
+
} as const
|
|
20
61
|
|
|
21
|
-
export const TutorWidgetEvents =
|
|
62
|
+
export const TutorWidgetEvents = TutorWidgetEventsObject
|
|
22
63
|
|
|
23
64
|
export const ACTION_EVENTS = {
|
|
24
65
|
SCROLL: 'c3po-app-widget-scroll-to-bottom'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './use-
|
|
1
|
+
export * from './use-listen-to-visibility-events'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as useListenToVisibilityEvents } from './use-listen-to-visibility-events'
|
package/src/modules/widget/hooks/use-listen-to-visibility-events/use-listen-to-visibility-events.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
import { SparkieService } from '@/src/modules/sparkie'
|
|
4
|
+
import { TutorWidgetEvents } from '../../events'
|
|
5
|
+
|
|
6
|
+
function useListenToVisibilityEvents() {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const listener = async () => {
|
|
9
|
+
await SparkieService.destroySparkie()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const clear = TutorWidgetEvents['c3po-app-widget-close'].handler(listener)
|
|
13
|
+
|
|
14
|
+
return () => {
|
|
15
|
+
clear?.()
|
|
16
|
+
}
|
|
17
|
+
}, [])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default useListenToVisibilityEvents
|
|
@@ -8,8 +8,8 @@ export type WidgetTabsProps = {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const INITIAL_PROPS: WidgetTabsProps = {
|
|
11
|
-
currentTab: '
|
|
12
|
-
history: new Set(['
|
|
11
|
+
currentTab: 'starter',
|
|
12
|
+
history: new Set(['starter'])
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const widgetTabsAtom = atom<WidgetTabsProps>(INITIAL_PROPS)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TutorWidgetEventTypes } from './events'
|
|
2
2
|
|
|
3
|
-
export type ITutorWidgetEvent = {
|
|
3
|
+
export type ITutorWidgetEvent<T = unknown> = {
|
|
4
4
|
name: (typeof TutorWidgetEventTypes)[keyof typeof TutorWidgetEventTypes]
|
|
5
|
-
handler: (
|
|
6
|
-
dispatch:
|
|
5
|
+
handler: (callback: (payload: T) => void | Promise<void>) => () => void
|
|
6
|
+
dispatch: (payload?: CustomEventInit<T>) => void
|
|
7
7
|
}
|
package/src/types.ts
CHANGED
|
@@ -36,3 +36,9 @@ export type WidgetSettingProps = {
|
|
|
36
36
|
owner_id?: string
|
|
37
37
|
current_media_codes?: string
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
export interface ICustomEvent<T = object> {
|
|
41
|
+
name: T[keyof T]
|
|
42
|
+
handler: (listener: EventListenerOrEventListenerObject) => () => void | Promise<void>
|
|
43
|
+
dispatch: <D = unknown>(detail?: D) => void
|
|
44
|
+
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { useInitSparkie } from '@/src/modules/widget/hooks/use-init-sparkie'
|
|
2
|
-
|
|
3
|
-
vi.mock('@/src/modules/widget/hooks/use-init-sparkie/use-init-sparkie', () => ({
|
|
4
|
-
useInitSparkie: vi.fn()
|
|
5
|
-
}))
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
vi.mocked(useInitSparkie).mockReturnValue({
|
|
9
|
-
data: true,
|
|
10
|
-
isError: false,
|
|
11
|
-
isLoading: false,
|
|
12
|
-
refetch: vi.fn()
|
|
13
|
-
} as unknown as ReturnType<typeof useInitSparkie>)
|
|
14
|
-
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './use-init-sparkie'
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
|
|
3
|
-
import { SparkieService } from '@/src/modules/sparkie'
|
|
4
|
-
import type { WidgetSettingProps } from '@/src/types'
|
|
5
|
-
|
|
6
|
-
export const getInitSparkieQuery = (settings: WidgetSettingProps) => ({
|
|
7
|
-
queryKey: ['SparkieService:initializeSparkie', settings?.hotmartToken ?? ''],
|
|
8
|
-
queryFn: () =>
|
|
9
|
-
SparkieService.initSparkie({
|
|
10
|
-
token: settings?.hotmartToken,
|
|
11
|
-
skipPresenceSetup: true
|
|
12
|
-
})
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
export function useInitSparkie(settings: WidgetSettingProps | null) {
|
|
16
|
-
return useQuery({
|
|
17
|
-
...getInitSparkieQuery(settings as WidgetSettingProps),
|
|
18
|
-
enabled: Boolean(settings?.hotmartToken?.trim())
|
|
19
|
-
})
|
|
20
|
-
}
|