app-tutor-ai-consumer 1.9.0 → 1.11.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,15 @@
1
+ # [1.11.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.10.0...v1.11.0) (2025-07-17)
2
+
3
+ ### Features
4
+
5
+ - adding new tutor icon ([a5f883f](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/a5f883fcb97ef4a3774cc9c9c900aa190f743e79))
6
+
7
+ # [1.10.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.9.0...v1.10.0) (2025-07-17)
8
+
9
+ ### Features
10
+
11
+ - add new loading logic ([ddfcfb6](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/ddfcfb6b5018440a6cd4b5fb2a03d53ee949add7))
12
+
1
13
  # [1.9.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.8.2...v1.9.0) (2025-07-17)
2
14
 
3
15
  ### 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.11.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
  }
@@ -1,17 +1,9 @@
1
- <svg viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <path
3
- d="M21.5813 16.7843L24.9086 18.5499L21.5813 20.3154L19.8068 23.6263L18.0313 20.3154L14.7041 18.5499L18.0313 16.7843L19.8068 13.4734L21.5813 16.7843ZM11.9119 9.10141L17.2354 11.9261L11.9119 14.7508L9.07199 20.0485L6.23301 14.7508L0.908569 11.9261L6.23301 9.10141L9.07199 3.80376L11.9119 9.10141ZM20.1457 3.24642L22.7786 4.64367L20.1457 6.03994L18.7413 8.66002L17.337 6.03994L14.7041 4.64367L17.337 3.24642L18.7413 0.626343L20.1457 3.24642Z"
4
- fill="url(#paint0_linear_18592_52336)" />
5
- <defs>
6
- <linearGradient
7
- id="paint0_linear_18592_52336"
8
- x1="0.908569"
9
- y1="12.1263"
10
- x2="24.9086"
11
- y2="12.1263"
12
- gradientUnits="userSpaceOnUse">
13
- <stop stop-color="#44D0FF" />
14
- <stop offset="1" stop-color="#B48EFF" />
15
- </linearGradient>
16
- </defs>
1
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0.897063 10.1169C0.779007 10.0991 0.779007 9.90091 0.897063 9.88308C8.8355 8.68408 9.54825 3.99021 9.90743 0.811152C9.92068 0.693875 10.0793 0.693875 10.0926 0.811152C10.4517 3.99021 11.1645 8.68408 19.1029 9.88308C19.221 9.90091 19.221 10.0991 19.1029 10.1169C11.1645 11.3159 10.4517 16.0098 10.0926 19.1888C10.0793 19.3061 9.92068 19.3061 9.90743 19.1888C9.54825 16.0098 8.8355 11.3159 0.897063 10.1169Z" fill="url(#paint0_linear_7856_83233)"/>
3
+ <defs>
4
+ <linearGradient id="paint0_linear_7856_83233" x1="1.875" y1="-0.108232" x2="17.7233" y2="20.1432" gradientUnits="userSpaceOnUse">
5
+ <stop stop-color="#44D0FF"/>
6
+ <stop offset="1" stop-color="#B48EFF"/>
7
+ </linearGradient>
8
+ </defs>
17
9
  </svg>
@@ -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)