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.
- package/CHANGELOG.md +6 -0
- package/config/vitest/__mocks__/sparkie.tsx +1 -1
- package/package.json +2 -2
- package/src/@types/index.d.ts +2 -3
- package/src/config/tanstack/query-provider.tsx +3 -7
- package/src/index.tsx +11 -64
- package/src/lib/components/icons/icon-names.d.ts +1 -0
- package/src/lib/components/icons/tutor-logo.svg +9 -0
- package/src/modules/global-providers/global-providers.tsx +6 -1
- package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +1 -1
- package/src/modules/messages/components/chat-input/chat-input.tsx +1 -1
- package/src/modules/messages/components/message-item/message-item.tsx +1 -2
- package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +54 -119
- package/src/modules/messages/service.direct.ts +1 -1
- package/src/modules/messages/service.ts +2 -3
- package/src/modules/messages/types.ts +1 -1
- package/src/modules/messages/utils/set-messages-cache/utils.ts +1 -1
- package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +6 -20
- package/src/modules/sparkie/service.ts +3 -2
- 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/constants.tsx +1 -3
- 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/starter-page/starter-page.tsx +63 -34
- package/src/modules/widget/store/widget-tabs.atom.ts +1 -31
- package/src/types.ts +0 -9
- package/src/index.backup.tsx +0 -61
- 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/sparkie/store/index.ts +0 -1
- package/src/modules/sparkie/store/sparkie-state.atom.ts +0 -13
- package/src/modules/widget/components/error-page/error-page.spec.tsx +0 -17
- package/src/modules/widget/components/error-page/error-page.tsx +0 -10
- package/src/modules/widget/components/error-page/index.ts +0 -1
- package/src/modules/widget/components/starter-page/starter-page-actions/index.ts +0 -1
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +0 -68
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +0 -33
- package/src/modules/widget/components/starter-page/starter-page-content/index.ts +0 -1
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +0 -62
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +0 -57
- package/src/modules/widget/components/starter-page/starter-page-header/index.ts +0 -1
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +0 -41
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +0 -34
- package/src/wrapper.tsx +0 -32
|
@@ -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,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(
|
|
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
|
|
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({
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
<
|
|
139
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
}
|
package/src/index.backup.tsx
DELETED
|
@@ -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
|
-
})
|