app-tutor-ai-consumer 1.9.0 → 1.10.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 +6 -0
- package/package.json +1 -1
- package/src/index.tsx +1 -31
- package/src/main/main.spec.tsx +1 -1
- package/src/main/main.tsx +5 -17
- package/src/modules/messages/components/chat-input/chat-input.tsx +9 -3
- package/src/modules/messages/hooks/use-send-text-message/use-send-text-message.tsx +4 -1
- package/src/modules/widget/components/constants.tsx +3 -1
- package/src/modules/widget/components/container/container.tsx +14 -3
- package/src/modules/widget/components/index.ts +1 -0
- package/src/modules/widget/components/loading-page/index.ts +1 -0
- package/src/modules/widget/components/loading-page/loading-page.tsx +41 -0
- package/src/modules/widget/hooks/index.ts +1 -0
- package/src/modules/widget/hooks/use-init-widget/index.ts +1 -0
- package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +47 -0
- package/src/modules/widget/store/widget-tabs.atom.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# [1.10.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.9.0...v1.10.0) (2025-07-17)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- add new loading logic ([ddfcfb6](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/ddfcfb6b5018440a6cd4b5fb2a03d53ee949add7))
|
|
6
|
+
|
|
1
7
|
# [1.9.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.8.2...v1.9.0) (2025-07-17)
|
|
2
8
|
|
|
3
9
|
### Features
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -4,12 +4,9 @@ import './config/styles/index.css'
|
|
|
4
4
|
import { StrictMode } from 'react'
|
|
5
5
|
import { createRoot } from 'react-dom/client'
|
|
6
6
|
|
|
7
|
-
import { initDayjs } from './config/dayjs'
|
|
8
7
|
import { initLanguage } from './config/i18n'
|
|
9
|
-
import { initAxios } from './config/request/api'
|
|
10
8
|
import { devMode, productionMode } from './lib/utils'
|
|
11
9
|
import { Main } from './main'
|
|
12
|
-
import { SparkieService } from './modules/sparkie'
|
|
13
10
|
import { TutorWidgetEvents } from './modules/widget'
|
|
14
11
|
import type { WidgetSettingProps } from './types'
|
|
15
12
|
|
|
@@ -40,39 +37,12 @@ window.startChatWidget = async (
|
|
|
40
37
|
const rootElement = document.getElementById(elementId) as HTMLElement
|
|
41
38
|
const root = createRoot(rootElement)
|
|
42
39
|
|
|
43
|
-
initAxios(settings.hotmartToken)
|
|
44
40
|
await initLanguage(settings.locale)
|
|
45
|
-
await initDayjs(settings.locale)
|
|
46
|
-
|
|
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
41
|
|
|
71
42
|
if (root) {
|
|
72
|
-
TutorWidgetEvents['c3po-app-widget-open'].dispatch()
|
|
73
43
|
root.render(
|
|
74
44
|
<StrictMode>
|
|
75
|
-
<Main settings={settings}
|
|
45
|
+
<Main settings={settings} />
|
|
76
46
|
</StrictMode>
|
|
77
47
|
)
|
|
78
48
|
}
|
package/src/main/main.spec.tsx
CHANGED
package/src/main/main.tsx
CHANGED
|
@@ -3,36 +3,24 @@ 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'
|
|
7
6
|
import { GlobalProviders } from '../modules/global-providers'
|
|
8
7
|
import { WidgetContainer } from '../modules/widget'
|
|
9
|
-
import {
|
|
8
|
+
import { useInitWidget } from '../modules/widget/hooks'
|
|
10
9
|
import type { WidgetSettingProps } from '../types'
|
|
11
10
|
|
|
12
11
|
export type MainProps = {
|
|
13
12
|
settings: WidgetSettingProps
|
|
14
|
-
metadata?: { isLoadingSparkie: boolean; initSparkieError: unknown }
|
|
15
13
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}: MainProps) {
|
|
14
|
+
|
|
15
|
+
function Main({ settings }: MainProps) {
|
|
16
|
+
const { completeSetup } = useInitWidget(settings)
|
|
20
17
|
useDefaultId()
|
|
21
18
|
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
|
-
}
|
|
31
19
|
|
|
32
20
|
return (
|
|
33
21
|
<ErrorBoundary fallback={<GenericError />}>
|
|
34
22
|
<GlobalProviders settings={settings}>
|
|
35
|
-
<WidgetContainer />
|
|
23
|
+
<WidgetContainer completeSetup={completeSetup} />
|
|
36
24
|
</GlobalProviders>
|
|
37
25
|
</ErrorBoundary>
|
|
38
26
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
|
|
1
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react'
|
|
2
2
|
import clsx from 'clsx'
|
|
3
3
|
import type { ChangeEvent, KeyboardEvent } from 'react'
|
|
4
4
|
import { useTranslation } from 'react-i18next'
|
|
@@ -40,11 +40,13 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
const setInputFocus = useCallback(() => {
|
|
44
44
|
if (inputDisabled) return
|
|
45
45
|
|
|
46
46
|
const input = ref?.current
|
|
47
47
|
|
|
48
|
+
if (input === document.activeElement) return
|
|
49
|
+
|
|
48
50
|
if (input) {
|
|
49
51
|
input.focus()
|
|
50
52
|
|
|
@@ -53,6 +55,10 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
|
|
53
55
|
}
|
|
54
56
|
}, [inputDisabled])
|
|
55
57
|
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setInputFocus()
|
|
60
|
+
}, [setInputFocus])
|
|
61
|
+
|
|
56
62
|
return (
|
|
57
63
|
<div
|
|
58
64
|
className={clsx(
|
|
@@ -68,7 +74,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
|
|
68
74
|
'max-h-12 w-full resize-none border-none bg-transparent text-neutral-100 outline-none outline-0 placeholder:text-neutral-400',
|
|
69
75
|
styles.textArea
|
|
70
76
|
),
|
|
71
|
-
{ 'cursor-not-allowed
|
|
77
|
+
{ 'cursor-not-allowed opacity-40': inputDisabled }
|
|
72
78
|
)}
|
|
73
79
|
placeholder={t('send_message.field.placeholder')}
|
|
74
80
|
value={value}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
1
2
|
import { useMutation } from '@tanstack/react-query'
|
|
2
3
|
import { v4 } from 'uuid'
|
|
3
4
|
|
|
@@ -11,6 +12,8 @@ function useSendTextMessage() {
|
|
|
11
12
|
const profileQuery = useGetProfile()
|
|
12
13
|
const [, setWidgetLoading] = useWidgetLoadingAtom()
|
|
13
14
|
|
|
15
|
+
const userId = useMemo(() => profileQuery.data?.userId?.toString(), [profileQuery.data?.userId])
|
|
16
|
+
|
|
14
17
|
return useMutation({
|
|
15
18
|
mutationFn(message: string) {
|
|
16
19
|
let processedMessage = message
|
|
@@ -48,7 +51,7 @@ function useSendTextMessage() {
|
|
|
48
51
|
externalId: v4(),
|
|
49
52
|
namespace: settings.namespace,
|
|
50
53
|
sessionId: settings.sessionId,
|
|
51
|
-
userId
|
|
54
|
+
userId
|
|
52
55
|
}
|
|
53
56
|
})
|
|
54
57
|
},
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { ChatPage } from './chat-page'
|
|
2
|
+
import { WidgetLoadingPage } from './loading-page'
|
|
2
3
|
import { WidgetOnboardingPage } from './onboarding-page'
|
|
3
4
|
import { WidgetStarterPage } from './starter-page'
|
|
4
5
|
|
|
5
6
|
export const WIDGET_TABS = {
|
|
6
7
|
onboarding: <WidgetOnboardingPage />,
|
|
7
8
|
starter: <WidgetStarterPage />,
|
|
8
|
-
chat: <ChatPage
|
|
9
|
+
chat: <ChatPage />,
|
|
10
|
+
loading: <WidgetLoadingPage />
|
|
9
11
|
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
1
3
|
import { useSubscribeMessageReceivedEvent } from '@/src/modules/messages/hooks'
|
|
2
4
|
import { useSubscribeThreadClosedEvent } from '@/src/modules/thread/hooks'
|
|
3
|
-
import {
|
|
5
|
+
import { useListenToVisibilityEvents } from '../../hooks'
|
|
6
|
+
import { useWidgetTabsAtom } from '../../store'
|
|
4
7
|
import { WIDGET_TABS } from '../constants'
|
|
5
8
|
|
|
6
|
-
function WidgetContainer() {
|
|
7
|
-
const widgetTabs =
|
|
9
|
+
function WidgetContainer({ completeSetup = false }: { completeSetup?: boolean }) {
|
|
10
|
+
const [widgetTabs, setTab] = useWidgetTabsAtom()
|
|
11
|
+
|
|
8
12
|
useSubscribeMessageReceivedEvent()
|
|
9
13
|
useSubscribeThreadClosedEvent()
|
|
14
|
+
useListenToVisibilityEvents()
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (completeSetup) {
|
|
18
|
+
setTab('chat')
|
|
19
|
+
}
|
|
20
|
+
}, [completeSetup, setTab])
|
|
10
21
|
|
|
11
22
|
return (
|
|
12
23
|
<div className='flex h-full flex-col items-center justify-stretch overflow-hidden'>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as WidgetLoadingPage } from './loading-page'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useRefEventListener } from '@/src/lib/hooks'
|
|
4
|
+
import {
|
|
5
|
+
ChatInput,
|
|
6
|
+
MessageSkeleton,
|
|
7
|
+
useChatInputValueAtom
|
|
8
|
+
} from '@/src/modules/messages/components'
|
|
9
|
+
import { PageLayout } from '../page-layout'
|
|
10
|
+
|
|
11
|
+
function WidgetLoadingPage() {
|
|
12
|
+
const chatInputRef = useRef<HTMLTextAreaElement>(null)
|
|
13
|
+
const [, setChatInputValue] = useChatInputValueAtom()
|
|
14
|
+
|
|
15
|
+
const handler = useCallback(
|
|
16
|
+
(e: Event) => {
|
|
17
|
+
const target = e.target as HTMLTextAreaElement
|
|
18
|
+
setChatInputValue(target.value)
|
|
19
|
+
},
|
|
20
|
+
[setChatInputValue]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
useRefEventListener<HTMLTextAreaElement>({
|
|
24
|
+
config: {
|
|
25
|
+
ref: chatInputRef,
|
|
26
|
+
eventTypes: ['input', 'change'],
|
|
27
|
+
handler
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<PageLayout
|
|
33
|
+
asideChild={<ChatInput name='new-chat-msg-input' ref={chatInputRef} loading={true} />}>
|
|
34
|
+
<div className='flex h-full flex-col justify-end px-5 py-4'>
|
|
35
|
+
<MessageSkeleton />
|
|
36
|
+
</div>
|
|
37
|
+
</PageLayout>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default WidgetLoadingPage
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as useInitWidget } from './use-init-widget'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { initDayjs } from '@/src/config/dayjs'
|
|
4
|
+
import { initAxios } from '@/src/config/request/api'
|
|
5
|
+
import { SparkieService } from '@/src/modules/sparkie'
|
|
6
|
+
import type { WidgetSettingProps } from '@/src/types'
|
|
7
|
+
import { TutorWidgetEvents } from '../../events'
|
|
8
|
+
|
|
9
|
+
const init = async (settings: WidgetSettingProps) => {
|
|
10
|
+
try {
|
|
11
|
+
initAxios(settings.hotmartToken)
|
|
12
|
+
await initDayjs(settings.locale)
|
|
13
|
+
await SparkieService.initSparkie({
|
|
14
|
+
token: settings?.hotmartToken,
|
|
15
|
+
skipPresenceSetup: true,
|
|
16
|
+
retryOptions: {
|
|
17
|
+
maxRetries: 5,
|
|
18
|
+
retryDelay: 2000,
|
|
19
|
+
backoffMultiplier: 1.5
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
await SparkieService.ensureInitialized()
|
|
23
|
+
TutorWidgetEvents['tutor-app-widget-loaded'].dispatch()
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(error)
|
|
26
|
+
TutorWidgetEvents['tutor-app-widget-loaded'].dispatch({ detail: { isSuccess: false } })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function useInitWidget(settings: WidgetSettingProps) {
|
|
31
|
+
const [completeSetup, setCompleteSetup] = useState(false)
|
|
32
|
+
const [error, setError] = useState<unknown>(null)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (completeSetup) return
|
|
36
|
+
|
|
37
|
+
init(settings)
|
|
38
|
+
.then(() => {
|
|
39
|
+
setCompleteSetup(true)
|
|
40
|
+
})
|
|
41
|
+
.catch(setError)
|
|
42
|
+
}, [completeSetup, settings])
|
|
43
|
+
|
|
44
|
+
return { completeSetup, error }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default useInitWidget
|
|
@@ -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: 'loading',
|
|
12
|
+
history: new Set(['loading'])
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const widgetTabsAtom = atom<WidgetTabsProps>(INITIAL_PROPS)
|