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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
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} metadata={{ initSparkieError, isLoadingSparkie }} />
45
+ <Main settings={settings} />
76
46
  </StrictMode>
77
47
  )
78
48
  }
@@ -23,7 +23,7 @@ describe('Main', () => {
23
23
  renderComponent({ settings: props })
24
24
 
25
25
  await waitFor(() => {
26
- expect(screen.getByText(/send/i)).toBeInTheDocument()
26
+ expect(screen.getByLabelText(/Submit Button/i)).toBeInTheDocument()
27
27
  })
28
28
  })
29
29
  })
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 { useListenToVisibilityEvents } from '../modules/widget/hooks'
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
- function Main({
17
- settings,
18
- metadata = { initSparkieError: null, isLoadingSparkie: false }
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
- useEffect(() => {
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': inputDisabled, 'opacity-40': inputDisabled || loading }
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: profileQuery.data?.userId?.toString()
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 { useWidgetTabsValueAtom } from '../../store'
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 = useWidgetTabsValueAtom()
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'>
@@ -2,5 +2,6 @@ export * from './ai-avatar'
2
2
  export * from './chat-page'
3
3
  export * from './container'
4
4
  export * from './greetings-card'
5
+ export * from './loading-page'
5
6
  export * from './page-layout'
6
7
  export * from './scroll-to-bottom-button'
@@ -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
@@ -1 +1,2 @@
1
+ export * from './use-init-widget'
1
2
  export * from './use-listen-to-visibility-events'
@@ -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: 'starter',
12
- history: new Set(['starter'])
11
+ currentTab: 'loading',
12
+ history: new Set(['loading'])
13
13
  }
14
14
 
15
15
  export const widgetTabsAtom = atom<WidgetTabsProps>(INITIAL_PROPS)