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.
- package/CHANGELOG.md +37 -0
- package/config/vitest/__mocks__/sparkie.tsx +1 -1
- package/package.json +1 -1
- package/src/bootstrap.ts +40 -0
- package/src/config/tests/handlers.ts +5 -4
- package/src/config/theme/init-theme.ts +11 -5
- package/src/index.tsx +22 -12
- package/src/lib/components/dropdown-actions/dropdown-actions.tsx +87 -0
- package/src/lib/components/dropdown-actions/dropdownActions.builder.ts +58 -0
- package/src/lib/components/dropdown-actions/dropdownActions.spec.tsx +76 -0
- package/src/lib/components/dropdown-actions/index.ts +1 -0
- package/src/lib/components/dropdown-actions/types.ts +16 -0
- package/src/lib/components/errors/generic/generic-error.tsx +11 -8
- package/src/lib/components/icons/document.svg +3 -0
- package/src/lib/components/icons/file.svg +3 -0
- package/src/lib/components/icons/icon-names.d.ts +8 -0
- package/src/lib/components/icons/image.svg +3 -0
- package/src/lib/components/icons/pdf.svg +3 -0
- package/src/lib/components/icons/plus.svg +3 -0
- package/src/lib/components/icons/retry.svg +3 -0
- package/src/lib/components/icons/spreadsheet.svg +3 -0
- package/src/lib/components/icons/tutor-logo.svg +9 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -3
- package/src/lib/hooks/index.ts +1 -0
- package/src/lib/hooks/use-chat-file-upload/constants.ts +11 -0
- package/src/lib/hooks/use-chat-file-upload/index.ts +1 -0
- package/src/lib/hooks/use-chat-file-upload/types.ts +14 -0
- package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.spec.ts +59 -0
- package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.ts +28 -0
- package/src/lib/hooks/use-click-outside/index.ts +1 -0
- package/src/lib/hooks/use-click-outside/use-click-outside.tsx +23 -0
- package/src/lib/hooks/use-click-outside/useClickOutside.spec.ts +102 -0
- package/src/lib/utils/index.ts +1 -0
- package/src/lib/utils/is-theme-dark.ts +21 -0
- package/src/main/hooks/use-initial-store/index.ts +1 -0
- package/src/main/hooks/use-initial-store/use-initial-store.tsx +64 -0
- package/src/main/hooks/use-initial-tab/index.ts +1 -0
- package/src/main/hooks/use-initial-tab/use-initial-tab.tsx +84 -0
- package/src/main/index.ts +1 -0
- package/src/main/main-content.tsx +14 -0
- package/src/main/main-wrapper.tsx +16 -0
- package/src/main/main.spec.tsx +5 -3
- package/src/main/main.tsx +7 -16
- package/src/main/types.ts +5 -0
- package/src/modules/global-providers/global-providers.tsx +1 -15
- package/src/modules/messages/__tests__/signed-urls.builder.ts +42 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.builder.ts +121 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.spec.tsx +107 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.tsx +45 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/close-button.tsx +31 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/types.ts +4 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/document-preview.tsx +63 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/types.ts +10 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/error-preview.tsx +37 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/types.ts +4 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/image-preview.tsx +32 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/types.ts +6 -0
- package/src/modules/messages/components/chat-file-preview/constants.ts +22 -0
- package/src/modules/messages/components/chat-file-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/types.ts +13 -0
- package/src/modules/messages/components/chat-file-preview/utils.spec.ts +38 -0
- package/src/modules/messages/components/chat-file-preview/utils.ts +13 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.builder.ts +19 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.spec.tsx +58 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.tsx +80 -0
- package/src/modules/messages/components/chat-file-uploader/index.ts +1 -0
- package/src/modules/messages/components/chat-file-uploader/types.ts +4 -0
- package/src/modules/messages/components/chat-input/chat-input.tsx +1 -1
- package/src/modules/messages/components/index.ts +1 -0
- package/src/modules/messages/components/message-item/message-item.tsx +1 -2
- package/src/modules/messages/constants.ts +2 -1
- package/src/modules/messages/hooks/use-get-signed-urls/index.ts +1 -0
- package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.spec.tsx +27 -0
- package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.tsx +38 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +1 -2
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +3 -8
- package/src/modules/messages/hooks/use-prefetch-messages/index.ts +1 -0
- package/src/modules/messages/hooks/use-prefetch-messages/use-prefetch-messages.tsx +28 -0
- package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +54 -119
- package/src/modules/messages/hooks/use-suspense-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-suspense-messages/types.ts +4 -0
- package/src/modules/messages/hooks/use-suspense-messages/use-suspense-messages.tsx +21 -0
- package/src/modules/messages/service.direct.ts +18 -0
- package/src/modules/messages/service.ts +1 -2
- package/src/modules/messages/store/messages-max-count.atom.ts +2 -2
- package/src/modules/messages/types.ts +14 -0
- package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +7 -6
- package/src/modules/sparkie/__tests__/sparkie.mock.ts +1 -4
- package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +30 -38
- package/src/modules/sparkie/service.ts +2 -1
- package/src/modules/widget/__tests__/widget-settings-props.builder.ts +20 -1
- package/src/modules/widget/components/ai-disclaimer/ai-disclaimer.tsx +19 -0
- package/src/modules/widget/components/ai-disclaimer/index.ts +1 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +30 -71
- package/src/modules/widget/components/container/container.tsx +14 -0
- package/src/modules/widget/components/greetings-card/greetings-card.tsx +9 -2
- package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +4 -4
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +3 -2
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +0 -46
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +1 -30
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +8 -4
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +15 -13
- package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
- package/src/modules/widget/components/starter-page/starter-page.tsx +22 -87
- package/src/modules/widget/hooks/index.ts +0 -1
- package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
- package/src/modules/widget/store/create-store.ts +7 -0
- package/src/modules/widget/store/index.ts +1 -0
- package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
- package/src/modules/widget/store/widget-tabs.atom.ts +18 -37
- package/src/types.ts +1 -0
- package/src/wrapper.tsx +39 -19
- package/src/lib/hooks/use-response-timeout/index.ts +0 -1
- package/src/lib/hooks/use-response-timeout/use-response-timeout.tsx +0 -42
- package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
- package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +0 -56
|
@@ -1,53 +1,45 @@
|
|
|
1
|
-
import { useCallback, useEffect
|
|
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 {
|
|
5
|
+
import { sparkieStateAtom } from '../../store'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const isAgentMode = useIsAgentParentAtomValue()
|
|
7
|
+
export type UseInitSparkieProps = {
|
|
8
|
+
hotmartToken: string
|
|
9
|
+
store?: ReturnType<typeof useStore>
|
|
10
|
+
}
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
function useInitSparkie({ hotmartToken, store }: UseInitSparkieProps) {
|
|
13
|
+
const sparkieState = store?.get(sparkieStateAtom)
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
}, [
|
|
34
|
+
}, [hotmartToken, store])
|
|
35
35
|
|
|
36
36
|
const checkState = useCallback(() => {
|
|
37
|
-
if (
|
|
38
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
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 {
|
|
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/
|
|
14
|
+
vi.mock('@/src/modules/sparkie/store', async (importActual) => ({
|
|
15
15
|
...(await importActual()),
|
|
16
|
-
|
|
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(
|
|
31
|
+
vi.mocked(useSparkieStateAtomValue).mockReturnValue('initialized')
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
test.each`
|
package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
2
|
import { useDecision } from '@optimizely/react-sdk'
|
|
3
3
|
|
|
4
|
-
import {
|
|
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
|
|
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
|
})
|
package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
2
|
import { useTranslation } from 'react-i18next'
|
|
3
3
|
|
|
4
|
-
import {
|
|
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} />
|
package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { render, screen } from '@/src/config/tests'
|
|
2
|
-
import {
|
|
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
|
-
|
|
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(
|
|
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(
|
|
38
|
+
vi.mocked(useSparkieStateAtomValue).mockReturnValue('initialized')
|
|
35
39
|
renderComponent()
|
|
36
40
|
|
|
37
41
|
expect(
|
package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|