app-tutor-ai-consumer 1.32.3 → 1.33.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 (24) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +1 -1
  3. package/src/index.tsx +2 -2
  4. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -1
  5. package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +20 -6
  6. package/src/modules/sparkie/store/index.ts +1 -0
  7. package/src/modules/sparkie/store/sparkie-state.atom.ts +13 -0
  8. package/src/modules/widget/components/chat-page/chat-page.tsx +63 -15
  9. package/src/modules/widget/components/constants.tsx +3 -1
  10. package/src/modules/widget/components/error-page/error-page.spec.tsx +17 -0
  11. package/src/modules/widget/components/error-page/error-page.tsx +10 -0
  12. package/src/modules/widget/components/error-page/index.ts +1 -0
  13. package/src/modules/widget/components/starter-page/starter-page-actions/index.ts +1 -0
  14. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +68 -0
  15. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +33 -0
  16. package/src/modules/widget/components/starter-page/starter-page-content/index.ts +1 -0
  17. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +62 -0
  18. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +57 -0
  19. package/src/modules/widget/components/starter-page/starter-page-header/index.ts +1 -0
  20. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +41 -0
  21. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +34 -0
  22. package/src/modules/widget/components/starter-page/starter-page.tsx +27 -49
  23. package/src/modules/widget/store/widget-tabs.atom.ts +31 -1
  24. package/src/wrapper.tsx +32 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # [1.33.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.32.3...v1.33.0) (2025-10-16)
2
+
3
+ ### Features
4
+
5
+ - change sparkie call to messages history page ([ec5aa19](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/ec5aa19ee913204881491d729d7fd979e05bf337))
6
+
1
7
  ## [1.32.3](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.32.2...v1.32.3) (2025-10-13)
2
8
 
3
9
  ## [1.32.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.32.1...v1.32.2) (2025-10-08)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.32.3",
3
+ "version": "1.33.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
@@ -10,9 +10,9 @@ import { version } from '../package.json'
10
10
 
11
11
  import { initLanguage } from './config/i18n'
12
12
  import { devMode, productionMode } from './lib/utils'
13
- import { Main } from './main'
14
13
  import { SparkieService } from './modules/sparkie'
15
14
  import type { Theme, WidgetSettingProps } from './types'
15
+ import Wrapper from './wrapper'
16
16
 
17
17
  const loadMainStyles = () => {
18
18
  const isProduction = productionMode
@@ -58,7 +58,7 @@ window.startChatWidget = async (
58
58
  root.render(
59
59
  <StrictMode>
60
60
  <QueryProvider queryClient={queryClient} showDevTools={false}>
61
- <Main settings={{ ...settings, config: { ...settings.config, theme } }} />
61
+ <Wrapper settings={{ ...settings, config: { ...settings.config, theme } }} />
62
62
  </QueryProvider>
63
63
  </StrictMode>
64
64
  )
@@ -79,7 +79,7 @@ const mdComponents: Partial<Components> = {
79
79
  )
80
80
  },
81
81
  p({ children }) {
82
- return <span className='inline-block [&:not(:only-child)]:my-3'>{children}</span>
82
+ return <span className='block [&:not(:only-child)]:my-3'>{children}</span>
83
83
  },
84
84
  ul({ children }) {
85
85
  return <ul className='my-3 list-inside list-disc'>{children}</ul>
@@ -1,16 +1,20 @@
1
1
  import { useCallback, useEffect, useState } from 'react'
2
2
 
3
- import { useWidgetSettingsAtomValue } from '@/src/modules/widget'
3
+ import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '@/src/modules/widget'
4
4
  import { SparkieService } from '../..'
5
+ import { useSparkieStateAtom } from '../../store'
5
6
 
6
7
  function useInitSparkie() {
7
8
  const [isSuccess, setIsSuccess] = useState(false)
8
9
  const settings = useWidgetSettingsAtomValue()
10
+ const [sparkieState, setSparkieState] = useSparkieStateAtom()
11
+ const isAgentMode = useIsAgentParentAtomValue()
9
12
 
10
13
  const init = useCallback(async () => {
11
14
  if (!settings?.hotmartToken) return
12
15
 
13
16
  try {
17
+ setSparkieState('initializing')
14
18
  await SparkieService.initSparkie({
15
19
  token: settings?.hotmartToken,
16
20
  skipPresenceSetup: true,
@@ -22,16 +26,26 @@ function useInitSparkie() {
22
26
  })
23
27
  await SparkieService.ensureInitialized()
24
28
  setIsSuccess(true)
29
+ setSparkieState('initialized')
25
30
  } catch {
26
31
  setIsSuccess(false)
27
- // TODO: Create Error PAGE and setTab
28
- // setTab('information')
32
+ setSparkieState('failed')
29
33
  }
30
- }, [settings?.hotmartToken])
34
+ }, [setSparkieState, settings?.hotmartToken])
35
+
36
+ const checkState = useCallback(() => {
37
+ if (!isAgentMode && sparkieState === 'idle') {
38
+ return init()
39
+ }
40
+
41
+ if (sparkieState === 'initialized') {
42
+ setIsSuccess(true)
43
+ }
44
+ }, [init, isAgentMode, sparkieState])
31
45
 
32
46
  useEffect(() => {
33
- void init()
34
- }, [init])
47
+ void checkState()
48
+ }, [checkState])
35
49
 
36
50
  return isSuccess
37
51
  }
@@ -0,0 +1 @@
1
+ export * from './sparkie-state.atom'
@@ -0,0 +1,13 @@
1
+ import { atom, useAtom, useAtomValue } from 'jotai'
2
+
3
+ import type { InitializationState } from '../types'
4
+
5
+ export const sparkieStateAtom = atom<InitializationState>('idle')
6
+
7
+ const setSparkieStateAtom = atom(
8
+ (get) => get(sparkieStateAtom),
9
+ (_, set, sparkieState: InitializationState) => set(sparkieStateAtom, sparkieState)
10
+ )
11
+
12
+ export const useSparkieStateAtom = () => useAtom(setSparkieStateAtom)
13
+ export const useSparkieStateAtomValue = () => useAtomValue(sparkieStateAtom)
@@ -1,6 +1,7 @@
1
- import { useEffect, useMemo, useRef } from 'react'
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react'
2
2
  import { useDecision } from '@optimizely/react-sdk'
3
3
  import { useInfiniteQuery } from '@tanstack/react-query'
4
+ import { useTranslation } from 'react-i18next'
4
5
 
5
6
  import { useMediaQuery } from '@/src/lib/hooks'
6
7
  import { isTextEmpty } from '@/src/lib/utils/is-text-empty'
@@ -12,15 +13,18 @@ import { useGetProfile } from '@/src/modules/profile'
12
13
  import { TutorWidgetEvents } from '../../events'
13
14
  import { useSendViewTutorEvent } from '../../hooks/use-send-view-tutor-event'
14
15
  import {
16
+ useIsAgentParentAtomValue,
15
17
  useWidgetLoadingAtom,
16
18
  useWidgetSettingsAtomValue,
17
19
  useWidgetTabsValueAtom
18
20
  } from '../../store'
19
21
  import { testQuestionRegex } from '../../utils'
22
+ import { GreetingsCard } from '../greetings-card'
20
23
  import { WidgetHeader } from '../header'
21
24
  import { PageLayout } from '../page-layout'
22
25
 
23
26
  function ChatPage() {
27
+ const { t } = useTranslation()
24
28
  const chatInputRef = useRef<HTMLTextAreaElement>(null)
25
29
  const scrollerRef = useRef<HTMLDivElement>(null)
26
30
  const settings = useWidgetSettingsAtomValue()
@@ -33,6 +37,7 @@ function ChatPage() {
33
37
  const isMobile = useMediaQuery({ maxSize: 'md' })
34
38
  const hasSentInitialMessage = useRef(false)
35
39
  const [lexTutorInitialMessageFF] = useDecision('lex_tutor_new_widget_initial_message')
40
+ const isAgentMode = useIsAgentParentAtomValue()
36
41
 
37
42
  const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
38
43
  const profileId = useMemo(() => profileQuery.data?.id, [profileQuery.data?.id])
@@ -63,6 +68,62 @@ function ChatPage() {
63
68
  })
64
69
  }
65
70
 
71
+ const fetchNextPage = useMemo(() => messagesQuery.fetchNextPage, [messagesQuery.fetchNextPage])
72
+
73
+ const retry = useMemo(() => messagesQuery.refetch, [messagesQuery.refetch])
74
+
75
+ const handleShowMore = useCallback(async () => {
76
+ await fetchNextPage()
77
+ }, [fetchNextPage])
78
+
79
+ const errorConfig = useMemo(
80
+ () => ({
81
+ show: messagesQuery.isError,
82
+ message: messagesQuery.error?.message ?? '',
83
+ retry
84
+ }),
85
+ [messagesQuery.error?.message, messagesQuery.isError, retry]
86
+ )
87
+
88
+ const isDarkTheme = useMemo(() => settings?.config?.theme === 'dark', [settings?.config?.theme])
89
+
90
+ const authorName = useMemo(() => {
91
+ const username = typeof settings?.user?.name === 'string' ? settings?.user?.name : ''
92
+
93
+ return username?.split?.(' ')?.[0] || ''
94
+ }, [settings?.user?.name])
95
+
96
+ const name = useMemo(() => settings?.tutorName ?? t('general.name'), [settings?.tutorName, t])
97
+
98
+ const content = useMemo(() => {
99
+ if (!isAgentMode || (messagesQuery.data && Number(messagesQuery.data?.size) > 0))
100
+ return (
101
+ <MessagesContainer
102
+ ref={scrollerRef}
103
+ handleShowMore={handleShowMore}
104
+ showButton={messagesQuery.hasNextPage}
105
+ loading={messagesQuery.isFetchingNextPage}
106
+ error={errorConfig}>
107
+ {messagesQuery.data && <MessagesList messagesMap={messagesQuery.data} />}
108
+ </MessagesContainer>
109
+ )
110
+ return (
111
+ <div className='my-auto'>
112
+ <GreetingsCard author={authorName} tutorName={name} isDarkTheme={isDarkTheme} />
113
+ </div>
114
+ )
115
+ }, [
116
+ authorName,
117
+ errorConfig,
118
+ handleShowMore,
119
+ isAgentMode,
120
+ isDarkTheme,
121
+ messagesQuery.data,
122
+ messagesQuery.hasNextPage,
123
+ messagesQuery.isFetchingNextPage,
124
+ name
125
+ ])
126
+
66
127
  useEffect(() => {
67
128
  if (hasSentInitialMessage.current || !lexTutorInitialMessageFF.enabled) return
68
129
 
@@ -110,20 +171,7 @@ function ChatPage() {
110
171
  showContentWithoutMeta={!isMobile}
111
172
  />
112
173
  </div>
113
- <MessagesContainer
114
- ref={scrollerRef}
115
- handleShowMore={async () => {
116
- await messagesQuery.fetchNextPage()
117
- }}
118
- showButton={messagesQuery.hasNextPage}
119
- loading={messagesQuery.isFetchingNextPage}
120
- error={{
121
- show: messagesQuery.isError,
122
- message: messagesQuery.error?.message ?? '',
123
- retry: () => void messagesQuery.refetch()
124
- }}>
125
- {messagesQuery.data && <MessagesList messagesMap={messagesQuery.data} />}
126
- </MessagesContainer>
174
+ {content}
127
175
  </PageLayout>
128
176
  )
129
177
  }
@@ -1,4 +1,5 @@
1
1
  import { ChatPage } from './chat-page'
2
+ import { WidgetErrorPage } from './error-page'
2
3
  import { WidgetInformationPage } from './information-page'
3
4
  import { WidgetLoadingPage } from './loading-page'
4
5
  import { WidgetStarterPage } from './starter-page'
@@ -7,5 +8,6 @@ export const WIDGET_TABS = {
7
8
  starter: <WidgetStarterPage />,
8
9
  chat: <ChatPage />,
9
10
  loading: <WidgetLoadingPage />,
10
- information: <WidgetInformationPage />
11
+ information: <WidgetInformationPage />,
12
+ error: <WidgetErrorPage />
11
13
  }
@@ -0,0 +1,17 @@
1
+ import { render, screen } from '@/src/config/tests'
2
+
3
+ import WidgetErrorPage from './error-page'
4
+
5
+ describe('<WidgetErrorPage/>', () => {
6
+ const renderComponent = () => render(<WidgetErrorPage />)
7
+
8
+ it('should render without errors', () => {
9
+ renderComponent()
10
+
11
+ expect(screen.getByRole('img', { name: /generic_error.image_alt/i })).toBeInTheDocument()
12
+ expect(screen.getByText(/generic_error.title/i)).toBeInTheDocument()
13
+ expect(screen.getByText(/generic_error.description/i)).toBeInTheDocument()
14
+ expect(screen.getByRole('button', { name: /Retry Button/i })).toBeInTheDocument()
15
+ expect(screen.getByRole('button', { name: /Close Icon/i })).toBeInTheDocument()
16
+ })
17
+ })
@@ -0,0 +1,10 @@
1
+ import { GenericError } from '@/src/lib/components'
2
+ import { useWidgetSettingsAtomValue } from '../../store'
3
+
4
+ function WidgetErrorPage() {
5
+ const settings = useWidgetSettingsAtomValue()
6
+
7
+ return <GenericError isDarkMode={settings?.config?.theme === 'dark'} />
8
+ }
9
+
10
+ export default WidgetErrorPage
@@ -0,0 +1 @@
1
+ export { default as WidgetErrorPage } from './error-page'
@@ -0,0 +1 @@
1
+ export { default as WidgetStarterPageActions } from './starter-page-actions'
@@ -0,0 +1,68 @@
1
+ import { useDecision } from '@optimizely/react-sdk'
2
+
3
+ import { render, screen } from '@/src/config/tests'
4
+ import { useInitSparkie } from '@/src/modules/sparkie/hooks'
5
+ import { useIsAgentParentAtomValue } from '../../../store'
6
+
7
+ import WidgetStarterPageActions from './starter-page-actions'
8
+
9
+ vi.mock('../../../store', async (importActual) => ({
10
+ ...(await importActual()),
11
+ useIsAgentParentAtomValue: vi.fn()
12
+ }))
13
+
14
+ vi.mock('@/src/modules/sparkie/hooks', async (importActual) => ({
15
+ ...(await importActual()),
16
+ useInitSparkie: vi.fn()
17
+ }))
18
+
19
+ vi.mock('@optimizely/react-sdk', async (importActual) => ({
20
+ ...(await importActual()),
21
+ useDecision: vi.fn()
22
+ }))
23
+
24
+ describe('<WidgetStarterPageActions />', () => {
25
+ const defaultProps = { send: vi.fn() }
26
+ const renderComponent = (props = defaultProps) => render(<WidgetStarterPageActions {...props} />)
27
+
28
+ beforeEach(() => {
29
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
30
+ vi.mocked(useDecision).mockReturnValue([{ enabled: false }] as never)
31
+ vi.mocked(useInitSparkie).mockReturnValue(true)
32
+ })
33
+
34
+ test.each`
35
+ isAgentMode | enabled
36
+ ${true} | ${false}
37
+ ${true} | ${true}
38
+ ${false} | ${false}
39
+ `(
40
+ 'should render null when isAgentMode is: $isAgentMode and enabled is: $enabled',
41
+ ({ isAgentMode, enabled }: { isAgentMode: boolean; enabled: boolean }) => {
42
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValue(isAgentMode)
43
+ vi.mocked(useDecision).mockReturnValue([{ enabled }] as never)
44
+
45
+ const { container } = renderComponent()
46
+
47
+ expect(container).toBeEmptyDOMElement()
48
+ }
49
+ )
50
+
51
+ test.each`
52
+ isAgentMode | enabled
53
+ ${false} | ${true}
54
+ `(
55
+ 'should render quick actions when isAgentMode is: $isAgentMode and enabled is: $enabled',
56
+ ({ isAgentMode, enabled }: { isAgentMode: boolean; enabled: boolean }) => {
57
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValue(isAgentMode)
58
+ vi.mocked(useDecision).mockReturnValue([{ enabled }] as never)
59
+
60
+ renderComponent()
61
+
62
+ expect(
63
+ screen.getByRole('button', { name: /starter_page.what_does_tutor_do/i })
64
+ ).toBeInTheDocument()
65
+ expect(screen.getByRole('button', { name: /starter_page.test_me/i })).toBeInTheDocument()
66
+ }
67
+ )
68
+ })
@@ -0,0 +1,33 @@
1
+ import { useMemo } from 'react'
2
+ import { useDecision } from '@optimizely/react-sdk'
3
+
4
+ import { useInitSparkie } from '@/src/modules/sparkie/hooks'
5
+ import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '../../../store'
6
+ import { QuickActionButtons } from '../../quick-action-buttons'
7
+
8
+ function WidgetStarterPageActions({ send }: { send: (textContent?: string | null) => void }) {
9
+ const [tutorQuickActionsFF] = useDecision('lex_tutor_quick_actions')
10
+ const settings = useWidgetSettingsAtomValue()
11
+ const isAgentMode = useIsAgentParentAtomValue()
12
+ const isSparkieReady = useInitSparkie()
13
+
14
+ const isDarkTheme = useMemo(() => settings?.config?.theme === 'dark', [settings?.config?.theme])
15
+
16
+ const shouldNotRender = useMemo(
17
+ () => [isAgentMode, !tutorQuickActionsFF?.enabled].some(Boolean),
18
+ [isAgentMode, tutorQuickActionsFF?.enabled]
19
+ )
20
+
21
+ if (shouldNotRender) return null
22
+
23
+ return (
24
+ <QuickActionButtons
25
+ className='grid-area-[b] my-4 flex flex-shrink-0 snap-x snap-mandatory gap-2 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
26
+ isDarkTheme={isDarkTheme}
27
+ send={send}
28
+ loading={!isSparkieReady}
29
+ />
30
+ )
31
+ }
32
+
33
+ export default WidgetStarterPageActions
@@ -0,0 +1 @@
1
+ export { default as WidgetStarterPageContent } from './starter-page-content'
@@ -0,0 +1,62 @@
1
+ import { render, screen } from '@/src/config/tests'
2
+ import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
3
+ import {
4
+ useIsAgentParentAtomValue,
5
+ useWidgetLoadingAtomValue,
6
+ useWidgetTabsAtom
7
+ } from '../../../store'
8
+
9
+ import WidgetStarterPageContent from './starter-page-content'
10
+
11
+ vi.mock('../../../store', async (importActual) => ({
12
+ ...(await importActual()),
13
+ useIsAgentParentAtomValue: vi.fn(),
14
+ useWidgetLoadingAtomValue: vi.fn(),
15
+ useWidgetTabsAtom: vi.fn()
16
+ }))
17
+
18
+ vi.mock('@/src/modules/sparkie/store', () => ({
19
+ useSparkieStateAtomValue: vi.fn()
20
+ }))
21
+
22
+ describe('<WidgetStarterPageContent />', () => {
23
+ const widgetTabsMock = ['starter', vi.fn()]
24
+
25
+ const renderComponent = () => render(<WidgetStarterPageContent />)
26
+
27
+ beforeEach(() => {
28
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
29
+ vi.mocked(useWidgetLoadingAtomValue).mockReturnValue(false)
30
+ vi.mocked(useSparkieStateAtomValue).mockReturnValue('idle')
31
+ vi.mocked(useWidgetTabsAtom).mockReturnValue(widgetTabsMock as never)
32
+ })
33
+
34
+ it('should render greetings card when not in agent mode', () => {
35
+ renderComponent()
36
+
37
+ expect(screen.getByText(/sparkle-tutor-light/i)).toBeInTheDocument()
38
+ expect(screen.getByText(/general.greetings.hello/i)).toBeInTheDocument()
39
+ expect(screen.getByText(/general.greetings.firstMessage/i)).toBeInTheDocument()
40
+ expect(screen.getByText(/general.greetings.description/i)).toBeInTheDocument()
41
+ })
42
+
43
+ it('should render the skeleton when in agent mode and is loading state', () => {
44
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValueOnce(true)
45
+ vi.mocked(useWidgetLoadingAtomValue).mockReturnValueOnce(true)
46
+
47
+ renderComponent()
48
+
49
+ expect(screen.getByTestId('avatar-animation-icon')).toBeInTheDocument()
50
+ })
51
+
52
+ it('should redirect to chat page when in agent mode, widget is not loading and sparkie is initialized', () => {
53
+ vi.mocked(useSparkieStateAtomValue).mockReturnValue('initialized')
54
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValueOnce(true)
55
+
56
+ const { container } = renderComponent()
57
+
58
+ expect(container).toBeEmptyDOMElement()
59
+ expect(widgetTabsMock[1]).toHaveBeenCalledOnce()
60
+ expect(widgetTabsMock[1]).toHaveBeenNthCalledWith(1, 'chat')
61
+ })
62
+ })
@@ -0,0 +1,57 @@
1
+ import { useMemo } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ import { MessageSkeleton } from '@/src/modules/messages/components'
5
+ import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
6
+ import {
7
+ useIsAgentParentAtomValue,
8
+ useWidgetLoadingAtomValue,
9
+ useWidgetSettingsAtomValue,
10
+ useWidgetTabsAtom
11
+ } from '../../../store'
12
+ import { GreetingsCard } from '../../greetings-card'
13
+
14
+ function WidgetStarterPageContent() {
15
+ const { t } = useTranslation()
16
+ const settings = useWidgetSettingsAtomValue()
17
+ const isAgentMode = useIsAgentParentAtomValue()
18
+ const widgetLoading = useWidgetLoadingAtomValue()
19
+ const [, setWidgetTabs] = useWidgetTabsAtom()
20
+ const sparkieState = useSparkieStateAtomValue()
21
+
22
+ const authorName = useMemo(() => {
23
+ const username = typeof settings?.user?.name === 'string' ? settings?.user?.name : ''
24
+
25
+ return username?.split?.(' ')?.[0] || ''
26
+ }, [settings?.user?.name])
27
+
28
+ const isDarkTheme = useMemo(() => settings?.config?.theme === 'dark', [settings?.config?.theme])
29
+
30
+ const name = useMemo(() => settings?.tutorName ?? t('general.name'), [settings?.tutorName, t])
31
+
32
+ const shouldGoToChat = useMemo(
33
+ () => [isAgentMode, !widgetLoading, sparkieState === 'initialized'].every(Boolean),
34
+ [isAgentMode, sparkieState, widgetLoading]
35
+ )
36
+
37
+ if (shouldGoToChat) {
38
+ setWidgetTabs('chat')
39
+ return null
40
+ }
41
+
42
+ if (isAgentMode && (widgetLoading || sparkieState !== 'initialized')) {
43
+ return (
44
+ <div className='mt-auto'>
45
+ <MessageSkeleton />
46
+ </div>
47
+ )
48
+ }
49
+
50
+ return (
51
+ <div className='my-auto'>
52
+ <GreetingsCard author={authorName} tutorName={name} isDarkTheme={isDarkTheme} />
53
+ </div>
54
+ )
55
+ }
56
+
57
+ export default WidgetStarterPageContent
@@ -0,0 +1 @@
1
+ export { default as WidgetStarterPageHeader } from './starter-page-header'
@@ -0,0 +1,41 @@
1
+ import { render, screen } from '@/src/config/tests'
2
+ import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
3
+ import { useIsAgentParentAtomValue } from '../../../store/widget-settings-config.atom'
4
+
5
+ import WidgetStarterPageHeader from './starter-page-header'
6
+
7
+ vi.mock('../../../store/widget-settings-config.atom')
8
+ vi.mock('@/src/modules/sparkie/hooks/use-init-sparkie', () => ({ useInitSparkie: vi.fn() }))
9
+
10
+ describe('<WidgetStarterPageHeader />', () => {
11
+ const renderComponent = () => render(<WidgetStarterPageHeader />)
12
+
13
+ beforeEach(() => {
14
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValue(true)
15
+ vi.mocked(useInitSparkie).mockReturnValue(false)
16
+ })
17
+
18
+ it('should return null when rendered as agent mode', () => {
19
+ const { container } = renderComponent()
20
+
21
+ expect(container).toBeEmptyDOMElement()
22
+ })
23
+
24
+ it('should render without errors when is not rendered as agent', () => {
25
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
26
+ renderComponent()
27
+
28
+ expect(screen.getByRole('button', { name: /general.buttons.info Icon/i })).toBeInTheDocument()
29
+ expect(screen.getByRole('button', { name: /Close Icon/i })).toBeInTheDocument()
30
+ })
31
+
32
+ it('should render the archive button when isSparkieReady is true', () => {
33
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
34
+ vi.mocked(useInitSparkie).mockReturnValue(true)
35
+ renderComponent()
36
+
37
+ expect(
38
+ screen.getByRole('button', { name: /general.buttons.archive Icon/i })
39
+ ).toBeInTheDocument()
40
+ })
41
+ })
@@ -0,0 +1,34 @@
1
+ import { useMemo } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
5
+ import { useMediaQuery } from '@/src/lib/hooks'
6
+ import { useInitSparkie } from '@/src/modules/sparkie/hooks'
7
+ import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '../../../store'
8
+ import { WidgetHeader } from '../../header'
9
+
10
+ function WidgetStarterPageHeader() {
11
+ const { t } = useTranslation()
12
+ const settings = useWidgetSettingsAtomValue()
13
+ const isAgentMode = useIsAgentParentAtomValue()
14
+ const isSparkieReady = useInitSparkie()
15
+ const isMobile = useMediaQuery({ maxSize: 'md' })
16
+
17
+ const enabledButtons = useMemo(() => {
18
+ const btns = ['close', 'info'] as ValidIconNames[]
19
+
20
+ if (isSparkieReady) {
21
+ btns.push('archive')
22
+ }
23
+
24
+ return btns
25
+ }, [isSparkieReady])
26
+
27
+ const name = useMemo(() => settings?.tutorName ?? t('general.name'), [settings?.tutorName, t])
28
+
29
+ if (isAgentMode) return null
30
+
31
+ return <WidgetHeader enabledButtons={enabledButtons} tutorName={name} showContent={isMobile} />
32
+ }
33
+
34
+ export default WidgetStarterPageHeader
@@ -1,9 +1,8 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from 'react'
2
2
  import { useDecision } from '@optimizely/react-sdk'
3
3
  import { useQueryClient } from '@tanstack/react-query'
4
- import { useTranslation } from 'react-i18next'
5
4
 
6
- import { useMediaQuery, useRefEventListener } from '@/src/lib/hooks'
5
+ import { useRefEventListener } from '@/src/lib/hooks'
7
6
  import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/components'
8
7
  import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/hooks'
9
8
  import { useMessagesMaxCount } from '@/src/modules/messages/store'
@@ -12,32 +11,42 @@ import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
12
11
  import { TutorWidgetEvents } from '../../events'
13
12
  import { useWidgetLoadingAtomValue, useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
14
13
  import { testQuestionRegex } from '../../utils'
15
- import { GreetingsCard } from '../greetings-card'
16
- import { WidgetHeader } from '../header'
17
14
  import { PageLayout } from '../page-layout'
18
- import { QuickActionButtons } from '../quick-action-buttons'
15
+
16
+ import { WidgetStarterPageActions } from './starter-page-actions'
17
+ import { WidgetStarterPageContent } from './starter-page-content'
18
+ import { WidgetStarterPageHeader } from './starter-page-header'
19
19
 
20
20
  function WidgetStarterPage() {
21
- const { t } = useTranslation()
22
- const [settings, setWidgetSettings] = useWidgetSettingsAtom()
23
21
  const chatInputRef = useRef<HTMLTextAreaElement>(null)
22
+ const hasSentInitialMessage = useRef(false)
23
+
24
+ const [settings, setWidgetSettings] = useWidgetSettingsAtom()
24
25
  const [chatInputValue, setChatInputValue] = useChatInputValueAtom()
25
26
  const [, setWidgetTabs] = useWidgetTabsAtom()
26
27
  const sendTextMessageMutation = useSendTextMessage()
27
28
  const profileQuery = useGetProfile()
28
29
  const limit = useMessagesMaxCount()
29
30
  const queryClient = useQueryClient()
30
- const name = settings?.tutorName ?? t('general.name')
31
- const authorName =
32
- typeof settings?.user?.name === 'string' ? settings?.user?.name?.split(' ')?.[0] || '' : ''
33
- const isDarkTheme = settings?.config?.theme === 'dark'
34
31
  const isSparkieReady = useInitSparkie()
35
- const isMobile = useMediaQuery({ maxSize: 'md' })
32
+
36
33
  const widgetLoading = useWidgetLoadingAtomValue()
37
- const hasSentInitialMessage = useRef(false)
38
- const [tutorQuickActionsFF] = useDecision('lex_tutor_quick_actions')
39
34
  const [lexTutorInitialMessageFF] = useDecision('lex_tutor_new_widget_initial_message')
40
35
 
36
+ const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
37
+
38
+ const profileId = useMemo(() => profileQuery.data?.id, [profileQuery.data?.id])
39
+
40
+ const messagesQueryConfig = useMemo(
41
+ () =>
42
+ getAllMessagesQuery({
43
+ conversationId,
44
+ profileId,
45
+ limit
46
+ }),
47
+ [conversationId, limit, profileId]
48
+ )
49
+
41
50
  useRefEventListener<HTMLTextAreaElement>({
42
51
  config: {
43
52
  ref: chatInputRef,
@@ -68,20 +77,6 @@ function WidgetStarterPage() {
68
77
  sendText(chatInputRef.current?.value)
69
78
  }
70
79
 
71
- const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
72
-
73
- const profileId = useMemo(() => profileQuery.data?.id, [profileQuery.data?.id])
74
-
75
- const messagesQueryConfig = useMemo(
76
- () =>
77
- getAllMessagesQuery({
78
- conversationId,
79
- profileId,
80
- limit
81
- }),
82
- [conversationId, limit, profileId]
83
- )
84
-
85
80
  useEffect(() => {
86
81
  if (!conversationId || !profileId) return
87
82
 
@@ -115,10 +110,7 @@ function WidgetStarterPage() {
115
110
  if (initialMessage) {
116
111
  setChatInputValue(testQuestionRegex(initialMessage))
117
112
  sendText(initialMessage)
118
- setWidgetSettings({
119
- ...settings,
120
- initialMessage: ''
121
- })
113
+ setWidgetSettings({ ...settings, initialMessage: '' })
122
114
  hasSentInitialMessage.current = true
123
115
  }
124
116
  }, [
@@ -143,24 +135,10 @@ function WidgetStarterPage() {
143
135
  }>
144
136
  <div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
145
137
  <div className='grid-area-[a] flex min-h-0 flex-col max-md:px-[1.125rem] max-md:pt-[1.125rem] md:px-5 md:pt-5'>
146
- <WidgetHeader
147
- enabledButtons={isSparkieReady ? ['close', 'archive', 'info'] : ['close', 'info']}
148
- showContent={isMobile}
149
- tutorName={name}
150
- />
151
-
152
- <div className='my-auto'>
153
- <GreetingsCard author={authorName} tutorName={name} isDarkTheme={isDarkTheme} />
154
- </div>
138
+ <WidgetStarterPageHeader />
139
+ <WidgetStarterPageContent />
155
140
  </div>
156
- {tutorQuickActionsFF?.enabled ? (
157
- <QuickActionButtons
158
- className='grid-area-[b] my-4 flex flex-shrink-0 snap-x snap-mandatory gap-2 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
159
- isDarkTheme={isDarkTheme}
160
- send={sendText}
161
- loading={!isSparkieReady}
162
- />
163
- ) : null}
141
+ <WidgetStarterPageActions send={sendText} />
164
142
  </div>
165
143
  </PageLayout>
166
144
  )
@@ -1,7 +1,11 @@
1
1
  import { atom, useAtom, useAtomValue } from 'jotai'
2
+ import { atomWithDefault } from 'jotai/utils'
2
3
 
4
+ import { sparkieStateAtom } from '../../sparkie/store'
3
5
  import type { CurrentTabKey } from '../components'
4
6
 
7
+ import { widgetSettingsConfigAgentParentAtom } from './widget-settings-config.atom'
8
+
5
9
  export type WidgetTabsProps = {
6
10
  currentTab: CurrentTabKey
7
11
  history: Set<CurrentTabKey>
@@ -12,7 +16,33 @@ const INITIAL_PROPS: WidgetTabsProps = {
12
16
  history: new Set(['starter'])
13
17
  }
14
18
 
15
- export const widgetTabsAtom = atom<WidgetTabsProps>(INITIAL_PROPS)
19
+ export const widgetTabsAtom = atomWithDefault<WidgetTabsProps>((get) => {
20
+ const sparkieState = get(sparkieStateAtom)
21
+ const isAgentMode = get(widgetSettingsConfigAgentParentAtom)
22
+
23
+ if (!isAgentMode) return INITIAL_PROPS
24
+
25
+ switch (sparkieState) {
26
+ case 'idle':
27
+ case 'initializing':
28
+ return {
29
+ currentTab: 'loading',
30
+ history: new Set(['loading'])
31
+ }
32
+ case 'initialized':
33
+ return {
34
+ currentTab: 'chat',
35
+ history: new Set(['chat'])
36
+ }
37
+ case 'failed':
38
+ return {
39
+ currentTab: 'error',
40
+ history: new Set(['error'])
41
+ }
42
+ default:
43
+ return INITIAL_PROPS
44
+ }
45
+ })
16
46
 
17
47
  export const setWidgetTabsAtom = atom(
18
48
  (get) => get(widgetTabsAtom),
@@ -0,0 +1,32 @@
1
+ import { useEffect } from 'react'
2
+
3
+ import { Main } from './main'
4
+ import { SparkieService } from './modules/sparkie'
5
+ import { useSparkieStateAtom } from './modules/sparkie/store'
6
+ import type { WidgetSettingProps } from './types'
7
+
8
+ export type WrapperProps = {
9
+ settings: WidgetSettingProps
10
+ }
11
+
12
+ function Wrapper({ settings }: WrapperProps) {
13
+ const [, setSparkieState] = useSparkieStateAtom()
14
+
15
+ useEffect(() => {
16
+ SparkieService.initSparkie({
17
+ token: settings?.hotmartToken,
18
+ skipPresenceSetup: true,
19
+ retryOptions: {
20
+ maxRetries: 5,
21
+ retryDelay: 2000,
22
+ backoffMultiplier: 1.5
23
+ }
24
+ })
25
+ .then((result) => setSparkieState(result ? 'initialized' : 'failed'))
26
+ .catch(() => setSparkieState('failed'))
27
+ }, [setSparkieState, settings?.hotmartToken])
28
+
29
+ return <Main settings={settings} />
30
+ }
31
+
32
+ export default Wrapper