app-tutor-ai-consumer 1.33.0 → 1.34.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 (122) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/config/vitest/__mocks__/sparkie.tsx +1 -1
  3. package/package.json +1 -1
  4. package/src/bootstrap.ts +40 -0
  5. package/src/config/tests/handlers.ts +5 -4
  6. package/src/config/theme/init-theme.ts +11 -5
  7. package/src/index.tsx +22 -12
  8. package/src/lib/components/dropdown-actions/dropdown-actions.tsx +87 -0
  9. package/src/lib/components/dropdown-actions/dropdownActions.builder.ts +58 -0
  10. package/src/lib/components/dropdown-actions/dropdownActions.spec.tsx +76 -0
  11. package/src/lib/components/dropdown-actions/index.ts +1 -0
  12. package/src/lib/components/dropdown-actions/types.ts +16 -0
  13. package/src/lib/components/errors/generic/generic-error.tsx +11 -8
  14. package/src/lib/components/icons/document.svg +3 -0
  15. package/src/lib/components/icons/file.svg +3 -0
  16. package/src/lib/components/icons/icon-names.d.ts +8 -0
  17. package/src/lib/components/icons/image.svg +3 -0
  18. package/src/lib/components/icons/pdf.svg +3 -0
  19. package/src/lib/components/icons/plus.svg +3 -0
  20. package/src/lib/components/icons/retry.svg +3 -0
  21. package/src/lib/components/icons/spreadsheet.svg +3 -0
  22. package/src/lib/components/icons/tutor-logo.svg +9 -0
  23. package/src/lib/components/index.ts +1 -0
  24. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -3
  25. package/src/lib/hooks/index.ts +1 -0
  26. package/src/lib/hooks/use-chat-file-upload/constants.ts +11 -0
  27. package/src/lib/hooks/use-chat-file-upload/index.ts +1 -0
  28. package/src/lib/hooks/use-chat-file-upload/types.ts +14 -0
  29. package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.spec.ts +59 -0
  30. package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.ts +28 -0
  31. package/src/lib/hooks/use-click-outside/index.ts +1 -0
  32. package/src/lib/hooks/use-click-outside/use-click-outside.tsx +23 -0
  33. package/src/lib/hooks/use-click-outside/useClickOutside.spec.ts +102 -0
  34. package/src/lib/utils/index.ts +1 -0
  35. package/src/lib/utils/is-theme-dark.ts +21 -0
  36. package/src/main/hooks/use-initial-store/index.ts +1 -0
  37. package/src/main/hooks/use-initial-store/use-initial-store.tsx +64 -0
  38. package/src/main/hooks/use-initial-tab/index.ts +1 -0
  39. package/src/main/hooks/use-initial-tab/use-initial-tab.tsx +84 -0
  40. package/src/main/index.ts +1 -0
  41. package/src/main/main-content.tsx +14 -0
  42. package/src/main/main-wrapper.tsx +16 -0
  43. package/src/main/main.spec.tsx +5 -3
  44. package/src/main/main.tsx +7 -16
  45. package/src/main/types.ts +5 -0
  46. package/src/modules/global-providers/global-providers.tsx +1 -15
  47. package/src/modules/messages/__tests__/signed-urls.builder.ts +42 -0
  48. package/src/modules/messages/components/chat-file-preview/chat-file-preview.builder.ts +121 -0
  49. package/src/modules/messages/components/chat-file-preview/chat-file-preview.spec.tsx +107 -0
  50. package/src/modules/messages/components/chat-file-preview/chat-file-preview.tsx +45 -0
  51. package/src/modules/messages/components/chat-file-preview/components/close-button/close-button.tsx +31 -0
  52. package/src/modules/messages/components/chat-file-preview/components/close-button/index.ts +1 -0
  53. package/src/modules/messages/components/chat-file-preview/components/close-button/types.ts +4 -0
  54. package/src/modules/messages/components/chat-file-preview/components/document-preview/document-preview.tsx +63 -0
  55. package/src/modules/messages/components/chat-file-preview/components/document-preview/index.ts +1 -0
  56. package/src/modules/messages/components/chat-file-preview/components/document-preview/types.ts +10 -0
  57. package/src/modules/messages/components/chat-file-preview/components/error-preview/error-preview.tsx +37 -0
  58. package/src/modules/messages/components/chat-file-preview/components/error-preview/index.ts +1 -0
  59. package/src/modules/messages/components/chat-file-preview/components/error-preview/types.ts +4 -0
  60. package/src/modules/messages/components/chat-file-preview/components/image-preview/image-preview.tsx +32 -0
  61. package/src/modules/messages/components/chat-file-preview/components/image-preview/index.ts +1 -0
  62. package/src/modules/messages/components/chat-file-preview/components/image-preview/types.ts +6 -0
  63. package/src/modules/messages/components/chat-file-preview/constants.ts +22 -0
  64. package/src/modules/messages/components/chat-file-preview/index.ts +1 -0
  65. package/src/modules/messages/components/chat-file-preview/types.ts +13 -0
  66. package/src/modules/messages/components/chat-file-preview/utils.spec.ts +38 -0
  67. package/src/modules/messages/components/chat-file-preview/utils.ts +13 -0
  68. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.builder.ts +19 -0
  69. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.spec.tsx +58 -0
  70. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.tsx +80 -0
  71. package/src/modules/messages/components/chat-file-uploader/index.ts +1 -0
  72. package/src/modules/messages/components/chat-file-uploader/types.ts +4 -0
  73. package/src/modules/messages/components/chat-input/chat-input.tsx +1 -1
  74. package/src/modules/messages/components/index.ts +1 -0
  75. package/src/modules/messages/components/message-item/message-item.tsx +1 -2
  76. package/src/modules/messages/constants.ts +2 -1
  77. package/src/modules/messages/hooks/use-get-signed-urls/index.ts +1 -0
  78. package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.spec.tsx +27 -0
  79. package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.tsx +38 -0
  80. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +1 -2
  81. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +3 -8
  82. package/src/modules/messages/hooks/use-prefetch-messages/index.ts +1 -0
  83. package/src/modules/messages/hooks/use-prefetch-messages/use-prefetch-messages.tsx +28 -0
  84. package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +54 -119
  85. package/src/modules/messages/hooks/use-suspense-messages/index.ts +2 -0
  86. package/src/modules/messages/hooks/use-suspense-messages/types.ts +4 -0
  87. package/src/modules/messages/hooks/use-suspense-messages/use-suspense-messages.tsx +21 -0
  88. package/src/modules/messages/service.direct.ts +18 -0
  89. package/src/modules/messages/service.ts +1 -2
  90. package/src/modules/messages/store/messages-max-count.atom.ts +2 -2
  91. package/src/modules/messages/types.ts +14 -0
  92. package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +7 -6
  93. package/src/modules/sparkie/__tests__/sparkie.mock.ts +1 -4
  94. package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +30 -38
  95. package/src/modules/sparkie/service.ts +2 -1
  96. package/src/modules/widget/__tests__/widget-settings-props.builder.ts +20 -1
  97. package/src/modules/widget/components/ai-disclaimer/ai-disclaimer.tsx +19 -0
  98. package/src/modules/widget/components/ai-disclaimer/index.ts +1 -0
  99. package/src/modules/widget/components/chat-page/chat-page.tsx +30 -71
  100. package/src/modules/widget/components/container/container.tsx +14 -0
  101. package/src/modules/widget/components/greetings-card/greetings-card.tsx +9 -2
  102. package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
  103. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +4 -4
  104. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +3 -2
  105. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +0 -46
  106. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +1 -30
  107. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +8 -4
  108. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +15 -13
  109. package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
  110. package/src/modules/widget/components/starter-page/starter-page.tsx +22 -87
  111. package/src/modules/widget/hooks/index.ts +0 -1
  112. package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
  113. package/src/modules/widget/store/create-store.ts +7 -0
  114. package/src/modules/widget/store/index.ts +1 -0
  115. package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
  116. package/src/modules/widget/store/widget-tabs.atom.ts +18 -37
  117. package/src/types.ts +1 -0
  118. package/src/wrapper.tsx +39 -19
  119. package/src/lib/hooks/use-response-timeout/index.ts +0 -1
  120. package/src/lib/hooks/use-response-timeout/use-response-timeout.tsx +0 -42
  121. package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
  122. package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +0 -56
@@ -1,53 +1,45 @@
1
- import { useCallback, useEffect, useState } from 'react'
1
+ import { useCallback, useEffect } from 'react'
2
+ import type { useStore } from 'jotai'
2
3
 
3
- import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '@/src/modules/widget'
4
4
  import { SparkieService } from '../..'
5
- import { useSparkieStateAtom } from '../../store'
5
+ import { sparkieStateAtom } from '../../store'
6
6
 
7
- function useInitSparkie() {
8
- const [isSuccess, setIsSuccess] = useState(false)
9
- const settings = useWidgetSettingsAtomValue()
10
- const [sparkieState, setSparkieState] = useSparkieStateAtom()
11
- const isAgentMode = useIsAgentParentAtomValue()
7
+ export type UseInitSparkieProps = {
8
+ hotmartToken: string
9
+ store?: ReturnType<typeof useStore>
10
+ }
12
11
 
13
- const init = useCallback(async () => {
14
- if (!settings?.hotmartToken) return
12
+ function useInitSparkie({ hotmartToken, store }: UseInitSparkieProps) {
13
+ const sparkieState = store?.get(sparkieStateAtom)
15
14
 
16
- try {
17
- setSparkieState('initializing')
18
- await SparkieService.initSparkie({
19
- token: settings?.hotmartToken,
20
- skipPresenceSetup: true,
21
- retryOptions: {
22
- maxRetries: 5,
23
- retryDelay: 2000,
24
- backoffMultiplier: 1.5
25
- }
26
- })
27
- await SparkieService.ensureInitialized()
28
- setIsSuccess(true)
29
- setSparkieState('initialized')
30
- } catch {
31
- setIsSuccess(false)
32
- setSparkieState('failed')
15
+ const init = useCallback(async () => {
16
+ if (hotmartToken) {
17
+ try {
18
+ store?.set(sparkieStateAtom, 'initializing')
19
+ await SparkieService.initSparkie({
20
+ token: hotmartToken,
21
+ skipPresenceSetup: true,
22
+ retryOptions: {
23
+ maxRetries: 5,
24
+ retryDelay: 1000,
25
+ backoffMultiplier: 1.5
26
+ }
27
+ })
28
+ await SparkieService.ensureInitialized()
29
+ store?.set(sparkieStateAtom, 'initialized')
30
+ } catch {
31
+ store?.set(sparkieStateAtom, 'failed')
32
+ }
33
33
  }
34
- }, [setSparkieState, settings?.hotmartToken])
34
+ }, [hotmartToken, store])
35
35
 
36
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])
37
+ if (sparkieState === 'idle') return init()
38
+ }, [init, sparkieState])
45
39
 
46
40
  useEffect(() => {
47
41
  void checkState()
48
42
  }, [checkState])
49
-
50
- return isSuccess
51
43
  }
52
44
 
53
45
  export default useInitSparkie
@@ -110,7 +110,8 @@ class SparkieService {
110
110
 
111
111
  while (attempt <= maxRetries) {
112
112
  try {
113
- if (!token?.trim()) throw new Error('Invalid or missing token for Sparkie initialization')
113
+ if (!token || !token.trim())
114
+ throw new Error('Invalid or missing token for Sparkie initialization')
114
115
 
115
116
  const sparkie = this.sparkieInstance
116
117
  const service = await sparkie.init(token, { skipPresenceSetup })
@@ -1,6 +1,6 @@
1
1
  import type { ILanguages } from '@/src/config/i18n'
2
2
  import { chance } from '@/src/config/tests'
3
- import type { User, WidgetSettingProps } from '@/src/types'
3
+ import type { Theme, User, WidgetSettingProps } from '@/src/types'
4
4
 
5
5
  class WidgetSettingPropsBuilder implements WidgetSettingProps {
6
6
  hotmartToken: string
@@ -21,6 +21,19 @@ class WidgetSettingPropsBuilder implements WidgetSettingProps {
21
21
  classHashId?: string
22
22
  owner_id?: string
23
23
  current_media_codes?: string
24
+ config?:
25
+ | {
26
+ theme?: Theme
27
+ metadata?: {
28
+ parent?: 'AGENT' | 'TUTOR'
29
+ agentProductId?: number
30
+ agentName?: string
31
+ courseName?: string
32
+ source?: string
33
+ promptId?: string
34
+ }
35
+ }
36
+ | undefined
24
37
 
25
38
  constructor() {
26
39
  this.hotmartToken = chance.guid()
@@ -144,6 +157,12 @@ class WidgetSettingPropsBuilder implements WidgetSettingProps {
144
157
 
145
158
  return this
146
159
  }
160
+
161
+ withTheme(theme: Theme) {
162
+ this.config = { ...(this.config ?? {}), theme }
163
+
164
+ return this
165
+ }
147
166
  }
148
167
 
149
168
  export default WidgetSettingPropsBuilder
@@ -0,0 +1,19 @@
1
+ import { useTranslation } from 'react-i18next'
2
+
3
+ import { Icon } from '@/src/lib/components/icons'
4
+
5
+ const AIDisclaimer = () => {
6
+ const { t } = useTranslation()
7
+
8
+ return (
9
+ <div className='mt-4 flex w-full items-center gap-1 text-xs text-neutral-500'>
10
+ <p className='mb-0'>{t('ai_disclaimer.technology')}</p>
11
+
12
+ <Icon name='tutor-logo' className='inline-flex h-3 w-3 align-middle' />
13
+
14
+ <p className='mb-0'>{t('ai_disclaimer.hotmart_ai')}</p>
15
+ </div>
16
+ )
17
+ }
18
+
19
+ export default AIDisclaimer
@@ -0,0 +1 @@
1
+ export { default as AIDisclaimer } from './ai-disclaimer'
@@ -1,7 +1,6 @@
1
- import { useCallback, useEffect, useMemo, useRef } from 'react'
1
+ import { 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'
5
4
 
6
5
  import { useMediaQuery } from '@/src/lib/hooks'
7
6
  import { isTextEmpty } from '@/src/lib/utils/is-text-empty'
@@ -13,18 +12,16 @@ import { useGetProfile } from '@/src/modules/profile'
13
12
  import { TutorWidgetEvents } from '../../events'
14
13
  import { useSendViewTutorEvent } from '../../hooks/use-send-view-tutor-event'
15
14
  import {
16
- useIsAgentParentAtomValue,
17
15
  useWidgetLoadingAtom,
18
16
  useWidgetSettingsAtomValue,
19
17
  useWidgetTabsValueAtom
20
18
  } from '../../store'
21
19
  import { testQuestionRegex } from '../../utils'
22
- import { GreetingsCard } from '../greetings-card'
20
+ import { AIDisclaimer } from '../ai-disclaimer'
23
21
  import { WidgetHeader } from '../header'
24
22
  import { PageLayout } from '../page-layout'
25
23
 
26
24
  function ChatPage() {
27
- const { t } = useTranslation()
28
25
  const chatInputRef = useRef<HTMLTextAreaElement>(null)
29
26
  const scrollerRef = useRef<HTMLDivElement>(null)
30
27
  const settings = useWidgetSettingsAtomValue()
@@ -37,7 +34,6 @@ function ChatPage() {
37
34
  const isMobile = useMediaQuery({ maxSize: 'md' })
38
35
  const hasSentInitialMessage = useRef(false)
39
36
  const [lexTutorInitialMessageFF] = useDecision('lex_tutor_new_widget_initial_message')
40
- const isAgentMode = useIsAgentParentAtomValue()
41
37
 
42
38
  const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
43
39
  const profileId = useMemo(() => profileQuery.data?.id, [profileQuery.data?.id])
@@ -68,62 +64,6 @@ function ChatPage() {
68
64
  })
69
65
  }
70
66
 
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
-
127
67
  useEffect(() => {
128
68
  if (hasSentInitialMessage.current || !lexTutorInitialMessageFF.enabled) return
129
69
 
@@ -155,14 +95,20 @@ function ChatPage() {
155
95
  return (
156
96
  <PageLayout
157
97
  asideChild={
158
- <ChatInput
159
- name='new-chat-msg-input'
160
- ref={chatInputRef}
161
- onSend={widgetTabs.currentTab === 'chat' ? handleSendMessage : undefined}
162
- loading={sendTextMessageMutation.isPending}
163
- inputDisabled={messagesQuery?.isLoading}
164
- buttonDisabled={widgetLoading || messagesQuery?.isLoading || !value.trim()}
165
- />
98
+ <>
99
+ <ChatInput
100
+ name='new-chat-msg-input'
101
+ ref={chatInputRef}
102
+ onSend={widgetTabs.currentTab === 'chat' ? handleSendMessage : undefined}
103
+ loading={sendTextMessageMutation.isPending}
104
+ inputDisabled={messagesQuery?.isLoading}
105
+ buttonDisabled={widgetLoading || messagesQuery?.isLoading || !value.trim()}
106
+ />
107
+
108
+ <div className='mx-auto w-fit'>
109
+ <AIDisclaimer />
110
+ </div>
111
+ </>
166
112
  }>
167
113
  <div className='max-md:px-[1.125rem] max-md:pt-[1.125rem] md:px-5 md:pt-5'>
168
114
  <WidgetHeader
@@ -171,7 +117,20 @@ function ChatPage() {
171
117
  showContentWithoutMeta={!isMobile}
172
118
  />
173
119
  </div>
174
- {content}
120
+ <MessagesContainer
121
+ ref={scrollerRef}
122
+ handleShowMore={async () => {
123
+ await messagesQuery.fetchNextPage()
124
+ }}
125
+ showButton={messagesQuery.hasNextPage}
126
+ loading={messagesQuery.isFetchingNextPage}
127
+ error={{
128
+ show: messagesQuery.isError,
129
+ message: messagesQuery.error?.message ?? '',
130
+ retry: () => void messagesQuery.refetch()
131
+ }}>
132
+ {messagesQuery.data && <MessagesList messagesMap={messagesQuery.data} />}
133
+ </MessagesContainer>
175
134
  </PageLayout>
176
135
  )
177
136
  }
@@ -1,9 +1,21 @@
1
+ import { useEffect, useState } from 'react'
2
+
1
3
  import { useSubscribeMessageReceivedEvent } from '@/src/modules/messages/hooks'
2
4
  import { useSubscribeThreadClosedEvent } from '@/src/modules/thread/hooks'
3
5
  import { useListenToVisibilityEvents } from '../../hooks'
4
6
  import { useWidgetTabsAtom } from '../../store'
5
7
  import { WIDGET_TABS } from '../constants'
6
8
 
9
+ // TODO: REMOVE
10
+ const hotmartRumKey = 'app-tutor-ai-consumer::hotmart-rum::activate'
11
+ const useSentryDebugger = () => {
12
+ const [logError] = useState(() => Boolean(window?.localStorage?.getItem?.(hotmartRumKey)))
13
+
14
+ useEffect(() => {
15
+ if (logError) throw new Error(hotmartRumKey)
16
+ }, [logError])
17
+ }
18
+
7
19
  function WidgetContainer() {
8
20
  const [widgetTabs] = useWidgetTabsAtom()
9
21
 
@@ -11,6 +23,8 @@ function WidgetContainer() {
11
23
  useSubscribeThreadClosedEvent()
12
24
  useListenToVisibilityEvents()
13
25
 
26
+ useSentryDebugger()
27
+
14
28
  return (
15
29
  <div className='flex h-full flex-col items-center justify-stretch overflow-hidden'>
16
30
  {WIDGET_TABS[widgetTabs.currentTab]}
@@ -1,6 +1,7 @@
1
1
  import clsx from 'clsx'
2
2
  import { useTranslation } from 'react-i18next'
3
3
 
4
+ import { useIsAgentParentAtomValue } from '../../store'
4
5
  import { AIAvatar } from '../ai-avatar'
5
6
 
6
7
  export type GreetingsCardProps = {
@@ -11,6 +12,7 @@ export type GreetingsCardProps = {
11
12
 
12
13
  function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCardProps) {
13
14
  const { t } = useTranslation()
15
+ const isAgentMode = useIsAgentParentAtomValue()
14
16
 
15
17
  return (
16
18
  <div className='flex flex-col items-center justify-center'>
@@ -31,7 +33,12 @@ function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCard
31
33
  'text-white': isDarkTheme,
32
34
  'text-gray-900': !isDarkTheme
33
35
  })}>
34
- {t('general.greetings.firstMessage', { tutorName })}
36
+ {t(
37
+ isAgentMode
38
+ ? 'general.greetings.agentFirstMessage'
39
+ : 'general.greetings.firstMessage',
40
+ { tutorName }
41
+ )}
35
42
  </h3>
36
43
  </div>
37
44
  <p
@@ -39,7 +46,7 @@ function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCard
39
46
  'text-gray-400': isDarkTheme,
40
47
  'text-neutral-600': !isDarkTheme
41
48
  })}>
42
- {t('general.greetings.description')}
49
+ {t(isAgentMode ? 'general.greetings.agentDescription' : 'general.greetings.description')}
43
50
  </p>
44
51
  </div>
45
52
  </div>
@@ -1,25 +1,19 @@
1
1
  import { useCallback, useRef } from 'react'
2
2
 
3
3
  import { useRefEventListener } from '@/src/lib/hooks'
4
- import {
5
- ChatInput,
6
- MessageSkeleton,
7
- useChatInputValueAtom
8
- } from '@/src/modules/messages/components'
4
+ import { ChatInput, MessageSkeleton } from '@/src/modules/messages/components'
9
5
  import { WidgetHeader } from '../header'
10
6
  import { PageLayout } from '../page-layout'
11
7
 
12
- function WidgetLoadingPage() {
8
+ function WidgetLoadingPage({ showHeader = true }: { showHeader?: boolean }) {
13
9
  const chatInputRef = useRef<HTMLTextAreaElement>(null)
14
- const [, setChatInputValue] = useChatInputValueAtom()
15
10
 
16
- const handler = useCallback(
17
- (e: Event) => {
18
- const target = e.target as HTMLTextAreaElement
19
- setChatInputValue(target.value)
20
- },
21
- [setChatInputValue]
22
- )
11
+ const handler = useCallback((e: Event) => {
12
+ const target = e.target as HTMLTextAreaElement
13
+ if (chatInputRef.current) {
14
+ chatInputRef.current.value = target.value
15
+ }
16
+ }, [])
23
17
 
24
18
  useRefEventListener<HTMLTextAreaElement>({
25
19
  config: {
@@ -33,7 +27,7 @@ function WidgetLoadingPage() {
33
27
  <PageLayout
34
28
  asideChild={<ChatInput name='new-chat-msg-input' ref={chatInputRef} loading={true} />}>
35
29
  <div className='flex h-full flex-col justify-start max-md:p-[1.125rem] md:p-5'>
36
- <WidgetHeader enabledButtons={['close']} showContent={false} />
30
+ {showHeader && <WidgetHeader enabledButtons={['close']} showContent={false} />}
37
31
  <div className='mt-auto'>
38
32
  <MessageSkeleton />
39
33
  </div>
@@ -1,7 +1,7 @@
1
1
  import { useDecision } from '@optimizely/react-sdk'
2
2
 
3
3
  import { render, screen } from '@/src/config/tests'
4
- import { useInitSparkie } from '@/src/modules/sparkie/hooks'
4
+ import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
5
5
  import { useIsAgentParentAtomValue } from '../../../store'
6
6
 
7
7
  import WidgetStarterPageActions from './starter-page-actions'
@@ -11,9 +11,9 @@ vi.mock('../../../store', async (importActual) => ({
11
11
  useIsAgentParentAtomValue: vi.fn()
12
12
  }))
13
13
 
14
- vi.mock('@/src/modules/sparkie/hooks', async (importActual) => ({
14
+ vi.mock('@/src/modules/sparkie/store', async (importActual) => ({
15
15
  ...(await importActual()),
16
- useInitSparkie: vi.fn()
16
+ useSparkieStateAtomValue: vi.fn()
17
17
  }))
18
18
 
19
19
  vi.mock('@optimizely/react-sdk', async (importActual) => ({
@@ -28,7 +28,7 @@ describe('<WidgetStarterPageActions />', () => {
28
28
  beforeEach(() => {
29
29
  vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
30
30
  vi.mocked(useDecision).mockReturnValue([{ enabled: false }] as never)
31
- vi.mocked(useInitSparkie).mockReturnValue(true)
31
+ vi.mocked(useSparkieStateAtomValue).mockReturnValue('initialized')
32
32
  })
33
33
 
34
34
  test.each`
@@ -1,7 +1,7 @@
1
1
  import { useMemo } from 'react'
2
2
  import { useDecision } from '@optimizely/react-sdk'
3
3
 
4
- import { useInitSparkie } from '@/src/modules/sparkie/hooks'
4
+ import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
5
5
  import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '../../../store'
6
6
  import { QuickActionButtons } from '../../quick-action-buttons'
7
7
 
@@ -9,9 +9,10 @@ function WidgetStarterPageActions({ send }: { send: (textContent?: string | null
9
9
  const [tutorQuickActionsFF] = useDecision('lex_tutor_quick_actions')
10
10
  const settings = useWidgetSettingsAtomValue()
11
11
  const isAgentMode = useIsAgentParentAtomValue()
12
- const isSparkieReady = useInitSparkie()
12
+ const sparkieState = useSparkieStateAtomValue()
13
13
 
14
14
  const isDarkTheme = useMemo(() => settings?.config?.theme === 'dark', [settings?.config?.theme])
15
+ const isSparkieReady = useMemo(() => sparkieState === 'initialized', [sparkieState])
15
16
 
16
17
  const shouldNotRender = useMemo(
17
18
  () => [isAgentMode, !tutorQuickActionsFF?.enabled].some(Boolean),
@@ -1,36 +1,10 @@
1
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
2
 
9
3
  import WidgetStarterPageContent from './starter-page-content'
10
4
 
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
5
  describe('<WidgetStarterPageContent />', () => {
23
- const widgetTabsMock = ['starter', vi.fn()]
24
-
25
6
  const renderComponent = () => render(<WidgetStarterPageContent />)
26
7
 
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
8
  it('should render greetings card when not in agent mode', () => {
35
9
  renderComponent()
36
10
 
@@ -39,24 +13,4 @@ describe('<WidgetStarterPageContent />', () => {
39
13
  expect(screen.getByText(/general.greetings.firstMessage/i)).toBeInTheDocument()
40
14
  expect(screen.getByText(/general.greetings.description/i)).toBeInTheDocument()
41
15
  })
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
16
  })
@@ -1,23 +1,12 @@
1
1
  import { useMemo } from 'react'
2
2
  import { useTranslation } from 'react-i18next'
3
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'
4
+ import { useWidgetSettingsAtomValue } from '../../../store'
12
5
  import { GreetingsCard } from '../../greetings-card'
13
6
 
14
7
  function WidgetStarterPageContent() {
15
8
  const { t } = useTranslation()
16
9
  const settings = useWidgetSettingsAtomValue()
17
- const isAgentMode = useIsAgentParentAtomValue()
18
- const widgetLoading = useWidgetLoadingAtomValue()
19
- const [, setWidgetTabs] = useWidgetTabsAtom()
20
- const sparkieState = useSparkieStateAtomValue()
21
10
 
22
11
  const authorName = useMemo(() => {
23
12
  const username = typeof settings?.user?.name === 'string' ? settings?.user?.name : ''
@@ -29,24 +18,6 @@ function WidgetStarterPageContent() {
29
18
 
30
19
  const name = useMemo(() => settings?.tutorName ?? t('general.name'), [settings?.tutorName, t])
31
20
 
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
21
  return (
51
22
  <div className='my-auto'>
52
23
  <GreetingsCard author={authorName} tutorName={name} isDarkTheme={isDarkTheme} />
@@ -1,18 +1,22 @@
1
1
  import { render, screen } from '@/src/config/tests'
2
- import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
2
+ import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
3
3
  import { useIsAgentParentAtomValue } from '../../../store/widget-settings-config.atom'
4
4
 
5
5
  import WidgetStarterPageHeader from './starter-page-header'
6
6
 
7
7
  vi.mock('../../../store/widget-settings-config.atom')
8
- vi.mock('@/src/modules/sparkie/hooks/use-init-sparkie', () => ({ useInitSparkie: vi.fn() }))
8
+
9
+ vi.mock('@/src/modules/sparkie/store', async (importActual) => ({
10
+ ...(await importActual()),
11
+ useSparkieStateAtomValue: vi.fn()
12
+ }))
9
13
 
10
14
  describe('<WidgetStarterPageHeader />', () => {
11
15
  const renderComponent = () => render(<WidgetStarterPageHeader />)
12
16
 
13
17
  beforeEach(() => {
14
18
  vi.mocked(useIsAgentParentAtomValue).mockReturnValue(true)
15
- vi.mocked(useInitSparkie).mockReturnValue(false)
19
+ vi.mocked(useSparkieStateAtomValue).mockReturnValue('initializing')
16
20
  })
17
21
 
18
22
  it('should return null when rendered as agent mode', () => {
@@ -31,7 +35,7 @@ describe('<WidgetStarterPageHeader />', () => {
31
35
 
32
36
  it('should render the archive button when isSparkieReady is true', () => {
33
37
  vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
34
- vi.mocked(useInitSparkie).mockReturnValue(true)
38
+ vi.mocked(useSparkieStateAtomValue).mockReturnValue('initialized')
35
39
  renderComponent()
36
40
 
37
41
  expect(
@@ -3,32 +3,34 @@ import { useTranslation } from 'react-i18next'
3
3
 
4
4
  import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
5
5
  import { useMediaQuery } from '@/src/lib/hooks'
6
- import { useInitSparkie } from '@/src/modules/sparkie/hooks'
6
+ import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
7
7
  import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '../../../store'
8
8
  import { WidgetHeader } from '../../header'
9
9
 
10
+ const getEnabledButtons = (isSparkieReady: boolean): ValidIconNames[] => {
11
+ const btns = ['close', 'info'] as ValidIconNames[]
12
+
13
+ return isSparkieReady ? [...btns, 'archive'] : btns
14
+ }
15
+
10
16
  function WidgetStarterPageHeader() {
11
17
  const { t } = useTranslation()
12
18
  const settings = useWidgetSettingsAtomValue()
13
19
  const isAgentMode = useIsAgentParentAtomValue()
14
- const isSparkieReady = useInitSparkie()
20
+ const sparkieState = useSparkieStateAtomValue()
15
21
  const isMobile = useMediaQuery({ maxSize: 'md' })
16
22
 
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
23
  const name = useMemo(() => settings?.tutorName ?? t('general.name'), [settings?.tutorName, t])
28
24
 
29
25
  if (isAgentMode) return null
30
26
 
31
- return <WidgetHeader enabledButtons={enabledButtons} tutorName={name} showContent={isMobile} />
27
+ return (
28
+ <WidgetHeader
29
+ enabledButtons={getEnabledButtons(sparkieState === 'initialized')}
30
+ tutorName={name}
31
+ showContent={isMobile}
32
+ />
33
+ )
32
34
  }
33
35
 
34
36
  export default WidgetStarterPageHeader