app-tutor-ai-consumer 1.33.0 → 1.33.1

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 (46) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/config/vitest/__mocks__/sparkie.tsx +1 -1
  3. package/package.json +2 -2
  4. package/src/@types/index.d.ts +2 -3
  5. package/src/config/tanstack/query-provider.tsx +3 -7
  6. package/src/index.tsx +11 -64
  7. package/src/lib/components/icons/icon-names.d.ts +1 -0
  8. package/src/lib/components/icons/tutor-logo.svg +9 -0
  9. package/src/modules/global-providers/global-providers.tsx +6 -1
  10. package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +1 -1
  11. package/src/modules/messages/components/chat-input/chat-input.tsx +1 -1
  12. package/src/modules/messages/components/message-item/message-item.tsx +1 -2
  13. package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +54 -119
  14. package/src/modules/messages/service.direct.ts +1 -1
  15. package/src/modules/messages/service.ts +2 -3
  16. package/src/modules/messages/types.ts +1 -1
  17. package/src/modules/messages/utils/set-messages-cache/utils.ts +1 -1
  18. package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +6 -20
  19. package/src/modules/sparkie/service.ts +3 -2
  20. package/src/modules/widget/components/ai-disclaimer/ai-disclaimer.tsx +19 -0
  21. package/src/modules/widget/components/ai-disclaimer/index.ts +1 -0
  22. package/src/modules/widget/components/chat-page/chat-page.tsx +30 -71
  23. package/src/modules/widget/components/constants.tsx +1 -3
  24. package/src/modules/widget/components/container/container.tsx +14 -0
  25. package/src/modules/widget/components/greetings-card/greetings-card.tsx +9 -2
  26. package/src/modules/widget/components/starter-page/starter-page.tsx +63 -34
  27. package/src/modules/widget/store/widget-tabs.atom.ts +1 -31
  28. package/src/types.ts +0 -9
  29. package/src/index.backup.tsx +0 -61
  30. package/src/lib/hooks/use-response-timeout/index.ts +0 -1
  31. package/src/lib/hooks/use-response-timeout/use-response-timeout.tsx +0 -42
  32. package/src/modules/sparkie/store/index.ts +0 -1
  33. package/src/modules/sparkie/store/sparkie-state.atom.ts +0 -13
  34. package/src/modules/widget/components/error-page/error-page.spec.tsx +0 -17
  35. package/src/modules/widget/components/error-page/error-page.tsx +0 -10
  36. package/src/modules/widget/components/error-page/index.ts +0 -1
  37. package/src/modules/widget/components/starter-page/starter-page-actions/index.ts +0 -1
  38. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +0 -68
  39. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +0 -33
  40. package/src/modules/widget/components/starter-page/starter-page-content/index.ts +0 -1
  41. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +0 -62
  42. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +0 -57
  43. package/src/modules/widget/components/starter-page/starter-page-header/index.ts +0 -1
  44. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +0 -41
  45. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +0 -34
  46. package/src/wrapper.tsx +0 -32
@@ -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,5 +1,4 @@
1
1
  import { ChatPage } from './chat-page'
2
- import { WidgetErrorPage } from './error-page'
3
2
  import { WidgetInformationPage } from './information-page'
4
3
  import { WidgetLoadingPage } from './loading-page'
5
4
  import { WidgetStarterPage } from './starter-page'
@@ -8,6 +7,5 @@ export const WIDGET_TABS = {
8
7
  starter: <WidgetStarterPage />,
9
8
  chat: <ChatPage />,
10
9
  loading: <WidgetLoadingPage />,
11
- information: <WidgetInformationPage />,
12
- error: <WidgetErrorPage />
10
+ information: <WidgetInformationPage />
13
11
  }
@@ -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,8 +1,9 @@
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'
4
5
 
5
- import { useRefEventListener } from '@/src/lib/hooks'
6
+ import { useMediaQuery, useRefEventListener } from '@/src/lib/hooks'
6
7
  import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/components'
7
8
  import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/hooks'
8
9
  import { useMessagesMaxCount } from '@/src/modules/messages/store'
@@ -11,42 +12,33 @@ import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
11
12
  import { TutorWidgetEvents } from '../../events'
12
13
  import { useWidgetLoadingAtomValue, useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
13
14
  import { testQuestionRegex } from '../../utils'
15
+ import { AIDisclaimer } from '../ai-disclaimer'
16
+ import { GreetingsCard } from '../greetings-card'
17
+ import { WidgetHeader } from '../header'
14
18
  import { PageLayout } from '../page-layout'
15
-
16
- import { WidgetStarterPageActions } from './starter-page-actions'
17
- import { WidgetStarterPageContent } from './starter-page-content'
18
- import { WidgetStarterPageHeader } from './starter-page-header'
19
+ import { QuickActionButtons } from '../quick-action-buttons'
19
20
 
20
21
  function WidgetStarterPage() {
21
- const chatInputRef = useRef<HTMLTextAreaElement>(null)
22
- const hasSentInitialMessage = useRef(false)
23
-
22
+ const { t } = useTranslation()
24
23
  const [settings, setWidgetSettings] = useWidgetSettingsAtom()
24
+ const chatInputRef = useRef<HTMLTextAreaElement>(null)
25
25
  const [chatInputValue, setChatInputValue] = useChatInputValueAtom()
26
26
  const [, setWidgetTabs] = useWidgetTabsAtom()
27
27
  const sendTextMessageMutation = useSendTextMessage()
28
28
  const profileQuery = useGetProfile()
29
29
  const limit = useMessagesMaxCount()
30
30
  const queryClient = useQueryClient()
31
+ const name = settings?.tutorName ?? t('general.name')
32
+ const authorName =
33
+ typeof settings?.user?.name === 'string' ? settings?.user?.name?.split(' ')?.[0] || '' : ''
34
+ const isDarkTheme = settings?.config?.theme === 'dark'
31
35
  const isSparkieReady = useInitSparkie()
32
-
36
+ const isMobile = useMediaQuery({ maxSize: 'md' })
33
37
  const widgetLoading = useWidgetLoadingAtomValue()
38
+ const hasSentInitialMessage = useRef(false)
39
+ const [tutorQuickActionsFF] = useDecision('lex_tutor_quick_actions')
34
40
  const [lexTutorInitialMessageFF] = useDecision('lex_tutor_new_widget_initial_message')
35
41
 
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
-
50
42
  useRefEventListener<HTMLTextAreaElement>({
51
43
  config: {
52
44
  ref: chatInputRef,
@@ -77,6 +69,20 @@ function WidgetStarterPage() {
77
69
  sendText(chatInputRef.current?.value)
78
70
  }
79
71
 
72
+ const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
73
+
74
+ const profileId = useMemo(() => profileQuery.data?.id, [profileQuery.data?.id])
75
+
76
+ const messagesQueryConfig = useMemo(
77
+ () =>
78
+ getAllMessagesQuery({
79
+ conversationId,
80
+ profileId,
81
+ limit
82
+ }),
83
+ [conversationId, limit, profileId]
84
+ )
85
+
80
86
  useEffect(() => {
81
87
  if (!conversationId || !profileId) return
82
88
 
@@ -110,7 +116,10 @@ function WidgetStarterPage() {
110
116
  if (initialMessage) {
111
117
  setChatInputValue(testQuestionRegex(initialMessage))
112
118
  sendText(initialMessage)
113
- setWidgetSettings({ ...settings, initialMessage: '' })
119
+ setWidgetSettings({
120
+ ...settings,
121
+ initialMessage: ''
122
+ })
114
123
  hasSentInitialMessage.current = true
115
124
  }
116
125
  }, [
@@ -125,20 +134,40 @@ function WidgetStarterPage() {
125
134
  return (
126
135
  <PageLayout
127
136
  asideChild={
128
- <ChatInput
129
- name='new-chat-msg-input'
130
- ref={chatInputRef}
131
- onSend={handleSend}
132
- buttonDisabled={widgetLoading || !chatInputValue.trim()}
133
- loading={!isSparkieReady}
134
- />
137
+ <>
138
+ <ChatInput
139
+ name='new-chat-msg-input'
140
+ ref={chatInputRef}
141
+ onSend={handleSend}
142
+ buttonDisabled={widgetLoading || !chatInputValue.trim()}
143
+ loading={!isSparkieReady}
144
+ />
145
+
146
+ <div className='mx-auto w-fit'>
147
+ <AIDisclaimer />
148
+ </div>
149
+ </>
135
150
  }>
136
151
  <div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
137
152
  <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'>
138
- <WidgetStarterPageHeader />
139
- <WidgetStarterPageContent />
153
+ <WidgetHeader
154
+ enabledButtons={isSparkieReady ? ['close', 'archive', 'info'] : ['close', 'info']}
155
+ showContent={isMobile}
156
+ tutorName={name}
157
+ />
158
+
159
+ <div className='my-auto'>
160
+ <GreetingsCard author={authorName} tutorName={name} isDarkTheme={isDarkTheme} />
161
+ </div>
140
162
  </div>
141
- <WidgetStarterPageActions send={sendText} />
163
+ {tutorQuickActionsFF?.enabled ? (
164
+ <QuickActionButtons
165
+ 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'
166
+ isDarkTheme={isDarkTheme}
167
+ send={sendText}
168
+ loading={!isSparkieReady}
169
+ />
170
+ ) : null}
142
171
  </div>
143
172
  </PageLayout>
144
173
  )
@@ -1,11 +1,7 @@
1
1
  import { atom, useAtom, useAtomValue } from 'jotai'
2
- import { atomWithDefault } from 'jotai/utils'
3
2
 
4
- import { sparkieStateAtom } from '../../sparkie/store'
5
3
  import type { CurrentTabKey } from '../components'
6
4
 
7
- import { widgetSettingsConfigAgentParentAtom } from './widget-settings-config.atom'
8
-
9
5
  export type WidgetTabsProps = {
10
6
  currentTab: CurrentTabKey
11
7
  history: Set<CurrentTabKey>
@@ -16,33 +12,7 @@ const INITIAL_PROPS: WidgetTabsProps = {
16
12
  history: new Set(['starter'])
17
13
  }
18
14
 
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
- })
15
+ export const widgetTabsAtom = atom<WidgetTabsProps>(INITIAL_PROPS)
46
16
 
47
17
  export const setWidgetTabsAtom = atom(
48
18
  (get) => get(widgetTabsAtom),
package/src/types.ts CHANGED
@@ -1,6 +1,3 @@
1
- import type { QueryClient } from '@tanstack/react-query'
2
- import type { Root } from 'react-dom/client'
3
-
4
1
  import type { ILanguages } from './config/i18n'
5
2
 
6
3
  export type StartTutorWidgetProps = {
@@ -71,9 +68,3 @@ export interface ICustomEvent<T = object> {
71
68
  handler: (listener: EventListenerOrEventListenerObject) => () => void | Promise<void>
72
69
  dispatch: <D = unknown>(detail?: D) => void
73
70
  }
74
-
75
- export type WidgetInstance = {
76
- root: Root
77
- container: HTMLElement
78
- queryClient: QueryClient
79
- }
@@ -1,61 +0,0 @@
1
- import './config/styles/global.css'
2
- import './config/styles/index.css'
3
-
4
- import { StrictMode } from 'react'
5
- import { createRoot } from 'react-dom/client'
6
-
7
- import { initTheme } from '@/src/config/theme'
8
- import { version } from '../package.json'
9
-
10
- import { initLanguage } from './config/i18n'
11
- import { devMode, productionMode } from './lib/utils'
12
- import { Main } from './main'
13
- import { TutorWidgetEvents } from './modules/widget'
14
- import type { Theme, WidgetSettingProps } from './types'
15
-
16
- const loadMainStyles = () => {
17
- const isProduction = productionMode
18
- const bundlePath = !isProduction
19
- ? `${process.env.BUNDLE_PATH}/`
20
- : `${process.env.BUNDLE_PATH}/${process.env.APP_NAME}/_current/`
21
-
22
- const cssPath = `${bundlePath}app-tutor-ai-consumer.css?v=${version}`
23
-
24
- if (!document.querySelector(`link[href="${cssPath}"]`)) {
25
- const linkElement = document.createElement('link')
26
- linkElement.rel = 'stylesheet'
27
- linkElement.href = cssPath
28
- document.head.appendChild(linkElement)
29
- }
30
- }
31
-
32
- window.startChatWidget = async (
33
- elementId = 'tutor-chat-app-widget',
34
- settings: WidgetSettingProps
35
- ) => {
36
- if (!devMode) {
37
- loadMainStyles()
38
- }
39
-
40
- const rootElement = document.getElementById(elementId) as HTMLElement
41
-
42
- if (!rootElement) return
43
-
44
- rootElement.setAttribute('id', 'hotmart-app-tutor-ai-consumer-root')
45
- const theme = (rootElement.getAttribute('data-theme') ?? 'dark') as Theme
46
- const root = createRoot(rootElement)
47
-
48
- await initLanguage(settings.locale)
49
- initTheme(theme)
50
-
51
- if (root) {
52
- root.render(
53
- <StrictMode>
54
- <Main settings={{ ...settings, config: { ...settings.config, theme } }} />
55
- </StrictMode>
56
- )
57
- }
58
- }
59
-
60
- window.closeChatWidget = () =>
61
- Promise.resolve(TutorWidgetEvents['c3po-app-widget-close'].dispatch())
@@ -1 +0,0 @@
1
- export { default as useResponseTimeout } from './use-response-timeout'
@@ -1,42 +0,0 @@
1
- import { useCallback, useRef } from 'react'
2
-
3
- const FIVE_MINUTES = 5 * 60 * 1000
4
-
5
- function useResponseTimeout() {
6
- const timeoutRef = useRef<NodeJS.Timeout>(null)
7
- const isWaitingRef = useRef(false)
8
-
9
- const startTimeout = useCallback((callback: () => void) => {
10
- if (timeoutRef.current) clearTimeout(timeoutRef.current)
11
-
12
- isWaitingRef.current = true
13
-
14
- timeoutRef.current = setTimeout(() => {
15
- callback()
16
- isWaitingRef.current = false
17
- }, FIVE_MINUTES)
18
- }, [])
19
-
20
- const reset = useCallback(() => {
21
- if (timeoutRef.current) {
22
- clearTimeout(timeoutRef.current)
23
- timeoutRef.current = null
24
- }
25
- isWaitingRef.current = false
26
- }, [])
27
-
28
- const cleanup = useCallback(() => {
29
- if (timeoutRef.current) {
30
- clearTimeout(timeoutRef.current)
31
- }
32
- }, [])
33
-
34
- return {
35
- startTimeout,
36
- cleanup,
37
- reset,
38
- isWaiting: isWaitingRef.current
39
- }
40
- }
41
-
42
- export default useResponseTimeout
@@ -1 +0,0 @@
1
- export * from './sparkie-state.atom'
@@ -1,13 +0,0 @@
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,17 +0,0 @@
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
- })
@@ -1,10 +0,0 @@
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
@@ -1 +0,0 @@
1
- export { default as WidgetErrorPage } from './error-page'
@@ -1 +0,0 @@
1
- export { default as WidgetStarterPageActions } from './starter-page-actions'
@@ -1,68 +0,0 @@
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
- })