app-tutor-ai-consumer 1.32.2 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/config/vitest/__mocks__/sparkie.tsx +1 -1
- package/package.json +2 -2
- package/src/@types/index.d.ts +3 -2
- package/src/config/tanstack/query-provider.tsx +7 -3
- package/src/index.backup.tsx +61 -0
- package/src/index.tsx +64 -11
- package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -1
- package/src/lib/hooks/use-response-timeout/index.ts +1 -0
- package/src/lib/hooks/use-response-timeout/use-response-timeout.tsx +42 -0
- package/src/modules/global-providers/global-providers.tsx +1 -6
- package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +1 -1
- package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +119 -54
- package/src/modules/messages/service.direct.ts +1 -1
- package/src/modules/messages/service.ts +3 -2
- 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 +20 -6
- package/src/modules/sparkie/service.ts +2 -3
- package/src/modules/sparkie/store/index.ts +1 -0
- package/src/modules/sparkie/store/sparkie-state.atom.ts +13 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +63 -15
- package/src/modules/widget/components/constants.tsx +3 -1
- package/src/modules/widget/components/container/container.tsx +0 -14
- package/src/modules/widget/components/error-page/error-page.spec.tsx +17 -0
- package/src/modules/widget/components/error-page/error-page.tsx +10 -0
- package/src/modules/widget/components/error-page/index.ts +1 -0
- package/src/modules/widget/components/starter-page/starter-page-actions/index.ts +1 -0
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +68 -0
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +33 -0
- package/src/modules/widget/components/starter-page/starter-page-content/index.ts +1 -0
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +62 -0
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +57 -0
- package/src/modules/widget/components/starter-page/starter-page-header/index.ts +1 -0
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +41 -0
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +34 -0
- package/src/modules/widget/components/starter-page/starter-page.tsx +27 -49
- package/src/modules/widget/store/widget-tabs.atom.ts +31 -1
- package/src/types.ts +9 -0
- package/src/wrapper.tsx +32 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { atom, useAtom, useAtomValue } from 'jotai'
|
|
2
|
+
|
|
3
|
+
import type { InitializationState } from '../types'
|
|
4
|
+
|
|
5
|
+
export const sparkieStateAtom = atom<InitializationState>('idle')
|
|
6
|
+
|
|
7
|
+
const setSparkieStateAtom = atom(
|
|
8
|
+
(get) => get(sparkieStateAtom),
|
|
9
|
+
(_, set, sparkieState: InitializationState) => set(sparkieStateAtom, sparkieState)
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
export const useSparkieStateAtom = () => useAtom(setSparkieStateAtom)
|
|
13
|
+
export const useSparkieStateAtomValue = () => useAtomValue(sparkieStateAtom)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
2
2
|
import { useDecision } from '@optimizely/react-sdk'
|
|
3
3
|
import { useInfiniteQuery } from '@tanstack/react-query'
|
|
4
|
+
import { useTranslation } from 'react-i18next'
|
|
4
5
|
|
|
5
6
|
import { useMediaQuery } from '@/src/lib/hooks'
|
|
6
7
|
import { isTextEmpty } from '@/src/lib/utils/is-text-empty'
|
|
@@ -12,15 +13,18 @@ import { useGetProfile } from '@/src/modules/profile'
|
|
|
12
13
|
import { TutorWidgetEvents } from '../../events'
|
|
13
14
|
import { useSendViewTutorEvent } from '../../hooks/use-send-view-tutor-event'
|
|
14
15
|
import {
|
|
16
|
+
useIsAgentParentAtomValue,
|
|
15
17
|
useWidgetLoadingAtom,
|
|
16
18
|
useWidgetSettingsAtomValue,
|
|
17
19
|
useWidgetTabsValueAtom
|
|
18
20
|
} from '../../store'
|
|
19
21
|
import { testQuestionRegex } from '../../utils'
|
|
22
|
+
import { GreetingsCard } from '../greetings-card'
|
|
20
23
|
import { WidgetHeader } from '../header'
|
|
21
24
|
import { PageLayout } from '../page-layout'
|
|
22
25
|
|
|
23
26
|
function ChatPage() {
|
|
27
|
+
const { t } = useTranslation()
|
|
24
28
|
const chatInputRef = useRef<HTMLTextAreaElement>(null)
|
|
25
29
|
const scrollerRef = useRef<HTMLDivElement>(null)
|
|
26
30
|
const settings = useWidgetSettingsAtomValue()
|
|
@@ -33,6 +37,7 @@ function ChatPage() {
|
|
|
33
37
|
const isMobile = useMediaQuery({ maxSize: 'md' })
|
|
34
38
|
const hasSentInitialMessage = useRef(false)
|
|
35
39
|
const [lexTutorInitialMessageFF] = useDecision('lex_tutor_new_widget_initial_message')
|
|
40
|
+
const isAgentMode = useIsAgentParentAtomValue()
|
|
36
41
|
|
|
37
42
|
const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
|
|
38
43
|
const profileId = useMemo(() => profileQuery.data?.id, [profileQuery.data?.id])
|
|
@@ -63,6 +68,62 @@ function ChatPage() {
|
|
|
63
68
|
})
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
const fetchNextPage = useMemo(() => messagesQuery.fetchNextPage, [messagesQuery.fetchNextPage])
|
|
72
|
+
|
|
73
|
+
const retry = useMemo(() => messagesQuery.refetch, [messagesQuery.refetch])
|
|
74
|
+
|
|
75
|
+
const handleShowMore = useCallback(async () => {
|
|
76
|
+
await fetchNextPage()
|
|
77
|
+
}, [fetchNextPage])
|
|
78
|
+
|
|
79
|
+
const errorConfig = useMemo(
|
|
80
|
+
() => ({
|
|
81
|
+
show: messagesQuery.isError,
|
|
82
|
+
message: messagesQuery.error?.message ?? '',
|
|
83
|
+
retry
|
|
84
|
+
}),
|
|
85
|
+
[messagesQuery.error?.message, messagesQuery.isError, retry]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const isDarkTheme = useMemo(() => settings?.config?.theme === 'dark', [settings?.config?.theme])
|
|
89
|
+
|
|
90
|
+
const authorName = useMemo(() => {
|
|
91
|
+
const username = typeof settings?.user?.name === 'string' ? settings?.user?.name : ''
|
|
92
|
+
|
|
93
|
+
return username?.split?.(' ')?.[0] || ''
|
|
94
|
+
}, [settings?.user?.name])
|
|
95
|
+
|
|
96
|
+
const name = useMemo(() => settings?.tutorName ?? t('general.name'), [settings?.tutorName, t])
|
|
97
|
+
|
|
98
|
+
const content = useMemo(() => {
|
|
99
|
+
if (!isAgentMode || (messagesQuery.data && Number(messagesQuery.data?.size) > 0))
|
|
100
|
+
return (
|
|
101
|
+
<MessagesContainer
|
|
102
|
+
ref={scrollerRef}
|
|
103
|
+
handleShowMore={handleShowMore}
|
|
104
|
+
showButton={messagesQuery.hasNextPage}
|
|
105
|
+
loading={messagesQuery.isFetchingNextPage}
|
|
106
|
+
error={errorConfig}>
|
|
107
|
+
{messagesQuery.data && <MessagesList messagesMap={messagesQuery.data} />}
|
|
108
|
+
</MessagesContainer>
|
|
109
|
+
)
|
|
110
|
+
return (
|
|
111
|
+
<div className='my-auto'>
|
|
112
|
+
<GreetingsCard author={authorName} tutorName={name} isDarkTheme={isDarkTheme} />
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}, [
|
|
116
|
+
authorName,
|
|
117
|
+
errorConfig,
|
|
118
|
+
handleShowMore,
|
|
119
|
+
isAgentMode,
|
|
120
|
+
isDarkTheme,
|
|
121
|
+
messagesQuery.data,
|
|
122
|
+
messagesQuery.hasNextPage,
|
|
123
|
+
messagesQuery.isFetchingNextPage,
|
|
124
|
+
name
|
|
125
|
+
])
|
|
126
|
+
|
|
66
127
|
useEffect(() => {
|
|
67
128
|
if (hasSentInitialMessage.current || !lexTutorInitialMessageFF.enabled) return
|
|
68
129
|
|
|
@@ -110,20 +171,7 @@ function ChatPage() {
|
|
|
110
171
|
showContentWithoutMeta={!isMobile}
|
|
111
172
|
/>
|
|
112
173
|
</div>
|
|
113
|
-
|
|
114
|
-
ref={scrollerRef}
|
|
115
|
-
handleShowMore={async () => {
|
|
116
|
-
await messagesQuery.fetchNextPage()
|
|
117
|
-
}}
|
|
118
|
-
showButton={messagesQuery.hasNextPage}
|
|
119
|
-
loading={messagesQuery.isFetchingNextPage}
|
|
120
|
-
error={{
|
|
121
|
-
show: messagesQuery.isError,
|
|
122
|
-
message: messagesQuery.error?.message ?? '',
|
|
123
|
-
retry: () => void messagesQuery.refetch()
|
|
124
|
-
}}>
|
|
125
|
-
{messagesQuery.data && <MessagesList messagesMap={messagesQuery.data} />}
|
|
126
|
-
</MessagesContainer>
|
|
174
|
+
{content}
|
|
127
175
|
</PageLayout>
|
|
128
176
|
)
|
|
129
177
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChatPage } from './chat-page'
|
|
2
|
+
import { WidgetErrorPage } from './error-page'
|
|
2
3
|
import { WidgetInformationPage } from './information-page'
|
|
3
4
|
import { WidgetLoadingPage } from './loading-page'
|
|
4
5
|
import { WidgetStarterPage } from './starter-page'
|
|
@@ -7,5 +8,6 @@ export const WIDGET_TABS = {
|
|
|
7
8
|
starter: <WidgetStarterPage />,
|
|
8
9
|
chat: <ChatPage />,
|
|
9
10
|
loading: <WidgetLoadingPage />,
|
|
10
|
-
information: <WidgetInformationPage
|
|
11
|
+
information: <WidgetInformationPage />,
|
|
12
|
+
error: <WidgetErrorPage />
|
|
11
13
|
}
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
|
|
3
1
|
import { useSubscribeMessageReceivedEvent } from '@/src/modules/messages/hooks'
|
|
4
2
|
import { useSubscribeThreadClosedEvent } from '@/src/modules/thread/hooks'
|
|
5
3
|
import { useListenToVisibilityEvents } from '../../hooks'
|
|
6
4
|
import { useWidgetTabsAtom } from '../../store'
|
|
7
5
|
import { WIDGET_TABS } from '../constants'
|
|
8
6
|
|
|
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
|
-
|
|
19
7
|
function WidgetContainer() {
|
|
20
8
|
const [widgetTabs] = useWidgetTabsAtom()
|
|
21
9
|
|
|
@@ -23,8 +11,6 @@ function WidgetContainer() {
|
|
|
23
11
|
useSubscribeThreadClosedEvent()
|
|
24
12
|
useListenToVisibilityEvents()
|
|
25
13
|
|
|
26
|
-
useSentryDebugger()
|
|
27
|
-
|
|
28
14
|
return (
|
|
29
15
|
<div className='flex h-full flex-col items-center justify-stretch overflow-hidden'>
|
|
30
16
|
{WIDGET_TABS[widgetTabs.currentTab]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render, screen } from '@/src/config/tests'
|
|
2
|
+
|
|
3
|
+
import WidgetErrorPage from './error-page'
|
|
4
|
+
|
|
5
|
+
describe('<WidgetErrorPage/>', () => {
|
|
6
|
+
const renderComponent = () => render(<WidgetErrorPage />)
|
|
7
|
+
|
|
8
|
+
it('should render without errors', () => {
|
|
9
|
+
renderComponent()
|
|
10
|
+
|
|
11
|
+
expect(screen.getByRole('img', { name: /generic_error.image_alt/i })).toBeInTheDocument()
|
|
12
|
+
expect(screen.getByText(/generic_error.title/i)).toBeInTheDocument()
|
|
13
|
+
expect(screen.getByText(/generic_error.description/i)).toBeInTheDocument()
|
|
14
|
+
expect(screen.getByRole('button', { name: /Retry Button/i })).toBeInTheDocument()
|
|
15
|
+
expect(screen.getByRole('button', { name: /Close Icon/i })).toBeInTheDocument()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GenericError } from '@/src/lib/components'
|
|
2
|
+
import { useWidgetSettingsAtomValue } from '../../store'
|
|
3
|
+
|
|
4
|
+
function WidgetErrorPage() {
|
|
5
|
+
const settings = useWidgetSettingsAtomValue()
|
|
6
|
+
|
|
7
|
+
return <GenericError isDarkMode={settings?.config?.theme === 'dark'} />
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default WidgetErrorPage
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as WidgetErrorPage } from './error-page'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as WidgetStarterPageActions } from './starter-page-actions'
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useDecision } from '@optimizely/react-sdk'
|
|
2
|
+
|
|
3
|
+
import { render, screen } from '@/src/config/tests'
|
|
4
|
+
import { useInitSparkie } from '@/src/modules/sparkie/hooks'
|
|
5
|
+
import { useIsAgentParentAtomValue } from '../../../store'
|
|
6
|
+
|
|
7
|
+
import WidgetStarterPageActions from './starter-page-actions'
|
|
8
|
+
|
|
9
|
+
vi.mock('../../../store', async (importActual) => ({
|
|
10
|
+
...(await importActual()),
|
|
11
|
+
useIsAgentParentAtomValue: vi.fn()
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
vi.mock('@/src/modules/sparkie/hooks', async (importActual) => ({
|
|
15
|
+
...(await importActual()),
|
|
16
|
+
useInitSparkie: vi.fn()
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
vi.mock('@optimizely/react-sdk', async (importActual) => ({
|
|
20
|
+
...(await importActual()),
|
|
21
|
+
useDecision: vi.fn()
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
describe('<WidgetStarterPageActions />', () => {
|
|
25
|
+
const defaultProps = { send: vi.fn() }
|
|
26
|
+
const renderComponent = (props = defaultProps) => render(<WidgetStarterPageActions {...props} />)
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
|
|
30
|
+
vi.mocked(useDecision).mockReturnValue([{ enabled: false }] as never)
|
|
31
|
+
vi.mocked(useInitSparkie).mockReturnValue(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test.each`
|
|
35
|
+
isAgentMode | enabled
|
|
36
|
+
${true} | ${false}
|
|
37
|
+
${true} | ${true}
|
|
38
|
+
${false} | ${false}
|
|
39
|
+
`(
|
|
40
|
+
'should render null when isAgentMode is: $isAgentMode and enabled is: $enabled',
|
|
41
|
+
({ isAgentMode, enabled }: { isAgentMode: boolean; enabled: boolean }) => {
|
|
42
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(isAgentMode)
|
|
43
|
+
vi.mocked(useDecision).mockReturnValue([{ enabled }] as never)
|
|
44
|
+
|
|
45
|
+
const { container } = renderComponent()
|
|
46
|
+
|
|
47
|
+
expect(container).toBeEmptyDOMElement()
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
test.each`
|
|
52
|
+
isAgentMode | enabled
|
|
53
|
+
${false} | ${true}
|
|
54
|
+
`(
|
|
55
|
+
'should render quick actions when isAgentMode is: $isAgentMode and enabled is: $enabled',
|
|
56
|
+
({ isAgentMode, enabled }: { isAgentMode: boolean; enabled: boolean }) => {
|
|
57
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(isAgentMode)
|
|
58
|
+
vi.mocked(useDecision).mockReturnValue([{ enabled }] as never)
|
|
59
|
+
|
|
60
|
+
renderComponent()
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
screen.getByRole('button', { name: /starter_page.what_does_tutor_do/i })
|
|
64
|
+
).toBeInTheDocument()
|
|
65
|
+
expect(screen.getByRole('button', { name: /starter_page.test_me/i })).toBeInTheDocument()
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
})
|
package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useDecision } from '@optimizely/react-sdk'
|
|
3
|
+
|
|
4
|
+
import { useInitSparkie } from '@/src/modules/sparkie/hooks'
|
|
5
|
+
import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '../../../store'
|
|
6
|
+
import { QuickActionButtons } from '../../quick-action-buttons'
|
|
7
|
+
|
|
8
|
+
function WidgetStarterPageActions({ send }: { send: (textContent?: string | null) => void }) {
|
|
9
|
+
const [tutorQuickActionsFF] = useDecision('lex_tutor_quick_actions')
|
|
10
|
+
const settings = useWidgetSettingsAtomValue()
|
|
11
|
+
const isAgentMode = useIsAgentParentAtomValue()
|
|
12
|
+
const isSparkieReady = useInitSparkie()
|
|
13
|
+
|
|
14
|
+
const isDarkTheme = useMemo(() => settings?.config?.theme === 'dark', [settings?.config?.theme])
|
|
15
|
+
|
|
16
|
+
const shouldNotRender = useMemo(
|
|
17
|
+
() => [isAgentMode, !tutorQuickActionsFF?.enabled].some(Boolean),
|
|
18
|
+
[isAgentMode, tutorQuickActionsFF?.enabled]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if (shouldNotRender) return null
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<QuickActionButtons
|
|
25
|
+
className='grid-area-[b] my-4 flex flex-shrink-0 snap-x snap-mandatory gap-2 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
|
|
26
|
+
isDarkTheme={isDarkTheme}
|
|
27
|
+
send={send}
|
|
28
|
+
loading={!isSparkieReady}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default WidgetStarterPageActions
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as WidgetStarterPageContent } from './starter-page-content'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { render, screen } from '@/src/config/tests'
|
|
2
|
+
import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
|
|
3
|
+
import {
|
|
4
|
+
useIsAgentParentAtomValue,
|
|
5
|
+
useWidgetLoadingAtomValue,
|
|
6
|
+
useWidgetTabsAtom
|
|
7
|
+
} from '../../../store'
|
|
8
|
+
|
|
9
|
+
import WidgetStarterPageContent from './starter-page-content'
|
|
10
|
+
|
|
11
|
+
vi.mock('../../../store', async (importActual) => ({
|
|
12
|
+
...(await importActual()),
|
|
13
|
+
useIsAgentParentAtomValue: vi.fn(),
|
|
14
|
+
useWidgetLoadingAtomValue: vi.fn(),
|
|
15
|
+
useWidgetTabsAtom: vi.fn()
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
vi.mock('@/src/modules/sparkie/store', () => ({
|
|
19
|
+
useSparkieStateAtomValue: vi.fn()
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
describe('<WidgetStarterPageContent />', () => {
|
|
23
|
+
const widgetTabsMock = ['starter', vi.fn()]
|
|
24
|
+
|
|
25
|
+
const renderComponent = () => render(<WidgetStarterPageContent />)
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
|
|
29
|
+
vi.mocked(useWidgetLoadingAtomValue).mockReturnValue(false)
|
|
30
|
+
vi.mocked(useSparkieStateAtomValue).mockReturnValue('idle')
|
|
31
|
+
vi.mocked(useWidgetTabsAtom).mockReturnValue(widgetTabsMock as never)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should render greetings card when not in agent mode', () => {
|
|
35
|
+
renderComponent()
|
|
36
|
+
|
|
37
|
+
expect(screen.getByText(/sparkle-tutor-light/i)).toBeInTheDocument()
|
|
38
|
+
expect(screen.getByText(/general.greetings.hello/i)).toBeInTheDocument()
|
|
39
|
+
expect(screen.getByText(/general.greetings.firstMessage/i)).toBeInTheDocument()
|
|
40
|
+
expect(screen.getByText(/general.greetings.description/i)).toBeInTheDocument()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should render the skeleton when in agent mode and is loading state', () => {
|
|
44
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValueOnce(true)
|
|
45
|
+
vi.mocked(useWidgetLoadingAtomValue).mockReturnValueOnce(true)
|
|
46
|
+
|
|
47
|
+
renderComponent()
|
|
48
|
+
|
|
49
|
+
expect(screen.getByTestId('avatar-animation-icon')).toBeInTheDocument()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should redirect to chat page when in agent mode, widget is not loading and sparkie is initialized', () => {
|
|
53
|
+
vi.mocked(useSparkieStateAtomValue).mockReturnValue('initialized')
|
|
54
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValueOnce(true)
|
|
55
|
+
|
|
56
|
+
const { container } = renderComponent()
|
|
57
|
+
|
|
58
|
+
expect(container).toBeEmptyDOMElement()
|
|
59
|
+
expect(widgetTabsMock[1]).toHaveBeenCalledOnce()
|
|
60
|
+
expect(widgetTabsMock[1]).toHaveBeenNthCalledWith(1, 'chat')
|
|
61
|
+
})
|
|
62
|
+
})
|
package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
import { MessageSkeleton } from '@/src/modules/messages/components'
|
|
5
|
+
import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
|
|
6
|
+
import {
|
|
7
|
+
useIsAgentParentAtomValue,
|
|
8
|
+
useWidgetLoadingAtomValue,
|
|
9
|
+
useWidgetSettingsAtomValue,
|
|
10
|
+
useWidgetTabsAtom
|
|
11
|
+
} from '../../../store'
|
|
12
|
+
import { GreetingsCard } from '../../greetings-card'
|
|
13
|
+
|
|
14
|
+
function WidgetStarterPageContent() {
|
|
15
|
+
const { t } = useTranslation()
|
|
16
|
+
const settings = useWidgetSettingsAtomValue()
|
|
17
|
+
const isAgentMode = useIsAgentParentAtomValue()
|
|
18
|
+
const widgetLoading = useWidgetLoadingAtomValue()
|
|
19
|
+
const [, setWidgetTabs] = useWidgetTabsAtom()
|
|
20
|
+
const sparkieState = useSparkieStateAtomValue()
|
|
21
|
+
|
|
22
|
+
const authorName = useMemo(() => {
|
|
23
|
+
const username = typeof settings?.user?.name === 'string' ? settings?.user?.name : ''
|
|
24
|
+
|
|
25
|
+
return username?.split?.(' ')?.[0] || ''
|
|
26
|
+
}, [settings?.user?.name])
|
|
27
|
+
|
|
28
|
+
const isDarkTheme = useMemo(() => settings?.config?.theme === 'dark', [settings?.config?.theme])
|
|
29
|
+
|
|
30
|
+
const name = useMemo(() => settings?.tutorName ?? t('general.name'), [settings?.tutorName, t])
|
|
31
|
+
|
|
32
|
+
const shouldGoToChat = useMemo(
|
|
33
|
+
() => [isAgentMode, !widgetLoading, sparkieState === 'initialized'].every(Boolean),
|
|
34
|
+
[isAgentMode, sparkieState, widgetLoading]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if (shouldGoToChat) {
|
|
38
|
+
setWidgetTabs('chat')
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (isAgentMode && (widgetLoading || sparkieState !== 'initialized')) {
|
|
43
|
+
return (
|
|
44
|
+
<div className='mt-auto'>
|
|
45
|
+
<MessageSkeleton />
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className='my-auto'>
|
|
52
|
+
<GreetingsCard author={authorName} tutorName={name} isDarkTheme={isDarkTheme} />
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default WidgetStarterPageContent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as WidgetStarterPageHeader } from './starter-page-header'
|
package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { render, screen } from '@/src/config/tests'
|
|
2
|
+
import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
|
|
3
|
+
import { useIsAgentParentAtomValue } from '../../../store/widget-settings-config.atom'
|
|
4
|
+
|
|
5
|
+
import WidgetStarterPageHeader from './starter-page-header'
|
|
6
|
+
|
|
7
|
+
vi.mock('../../../store/widget-settings-config.atom')
|
|
8
|
+
vi.mock('@/src/modules/sparkie/hooks/use-init-sparkie', () => ({ useInitSparkie: vi.fn() }))
|
|
9
|
+
|
|
10
|
+
describe('<WidgetStarterPageHeader />', () => {
|
|
11
|
+
const renderComponent = () => render(<WidgetStarterPageHeader />)
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(true)
|
|
15
|
+
vi.mocked(useInitSparkie).mockReturnValue(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should return null when rendered as agent mode', () => {
|
|
19
|
+
const { container } = renderComponent()
|
|
20
|
+
|
|
21
|
+
expect(container).toBeEmptyDOMElement()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should render without errors when is not rendered as agent', () => {
|
|
25
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
|
|
26
|
+
renderComponent()
|
|
27
|
+
|
|
28
|
+
expect(screen.getByRole('button', { name: /general.buttons.info Icon/i })).toBeInTheDocument()
|
|
29
|
+
expect(screen.getByRole('button', { name: /Close Icon/i })).toBeInTheDocument()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should render the archive button when isSparkieReady is true', () => {
|
|
33
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
|
|
34
|
+
vi.mocked(useInitSparkie).mockReturnValue(true)
|
|
35
|
+
renderComponent()
|
|
36
|
+
|
|
37
|
+
expect(
|
|
38
|
+
screen.getByRole('button', { name: /general.buttons.archive Icon/i })
|
|
39
|
+
).toBeInTheDocument()
|
|
40
|
+
})
|
|
41
|
+
})
|
package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
|
|
5
|
+
import { useMediaQuery } from '@/src/lib/hooks'
|
|
6
|
+
import { useInitSparkie } from '@/src/modules/sparkie/hooks'
|
|
7
|
+
import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '../../../store'
|
|
8
|
+
import { WidgetHeader } from '../../header'
|
|
9
|
+
|
|
10
|
+
function WidgetStarterPageHeader() {
|
|
11
|
+
const { t } = useTranslation()
|
|
12
|
+
const settings = useWidgetSettingsAtomValue()
|
|
13
|
+
const isAgentMode = useIsAgentParentAtomValue()
|
|
14
|
+
const isSparkieReady = useInitSparkie()
|
|
15
|
+
const isMobile = useMediaQuery({ maxSize: 'md' })
|
|
16
|
+
|
|
17
|
+
const enabledButtons = useMemo(() => {
|
|
18
|
+
const btns = ['close', 'info'] as ValidIconNames[]
|
|
19
|
+
|
|
20
|
+
if (isSparkieReady) {
|
|
21
|
+
btns.push('archive')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return btns
|
|
25
|
+
}, [isSparkieReady])
|
|
26
|
+
|
|
27
|
+
const name = useMemo(() => settings?.tutorName ?? t('general.name'), [settings?.tutorName, t])
|
|
28
|
+
|
|
29
|
+
if (isAgentMode) return null
|
|
30
|
+
|
|
31
|
+
return <WidgetHeader enabledButtons={enabledButtons} tutorName={name} showContent={isMobile} />
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default WidgetStarterPageHeader
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
2
2
|
import { useDecision } from '@optimizely/react-sdk'
|
|
3
3
|
import { useQueryClient } from '@tanstack/react-query'
|
|
4
|
-
import { useTranslation } from 'react-i18next'
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import { useRefEventListener } from '@/src/lib/hooks'
|
|
7
6
|
import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/components'
|
|
8
7
|
import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/hooks'
|
|
9
8
|
import { useMessagesMaxCount } from '@/src/modules/messages/store'
|
|
@@ -12,32 +11,42 @@ import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
|
|
|
12
11
|
import { TutorWidgetEvents } from '../../events'
|
|
13
12
|
import { useWidgetLoadingAtomValue, useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
|
|
14
13
|
import { testQuestionRegex } from '../../utils'
|
|
15
|
-
import { GreetingsCard } from '../greetings-card'
|
|
16
|
-
import { WidgetHeader } from '../header'
|
|
17
14
|
import { PageLayout } from '../page-layout'
|
|
18
|
-
|
|
15
|
+
|
|
16
|
+
import { WidgetStarterPageActions } from './starter-page-actions'
|
|
17
|
+
import { WidgetStarterPageContent } from './starter-page-content'
|
|
18
|
+
import { WidgetStarterPageHeader } from './starter-page-header'
|
|
19
19
|
|
|
20
20
|
function WidgetStarterPage() {
|
|
21
|
-
const { t } = useTranslation()
|
|
22
|
-
const [settings, setWidgetSettings] = useWidgetSettingsAtom()
|
|
23
21
|
const chatInputRef = useRef<HTMLTextAreaElement>(null)
|
|
22
|
+
const hasSentInitialMessage = useRef(false)
|
|
23
|
+
|
|
24
|
+
const [settings, setWidgetSettings] = useWidgetSettingsAtom()
|
|
24
25
|
const [chatInputValue, setChatInputValue] = useChatInputValueAtom()
|
|
25
26
|
const [, setWidgetTabs] = useWidgetTabsAtom()
|
|
26
27
|
const sendTextMessageMutation = useSendTextMessage()
|
|
27
28
|
const profileQuery = useGetProfile()
|
|
28
29
|
const limit = useMessagesMaxCount()
|
|
29
30
|
const queryClient = useQueryClient()
|
|
30
|
-
const name = settings?.tutorName ?? t('general.name')
|
|
31
|
-
const authorName =
|
|
32
|
-
typeof settings?.user?.name === 'string' ? settings?.user?.name?.split(' ')?.[0] || '' : ''
|
|
33
|
-
const isDarkTheme = settings?.config?.theme === 'dark'
|
|
34
31
|
const isSparkieReady = useInitSparkie()
|
|
35
|
-
|
|
32
|
+
|
|
36
33
|
const widgetLoading = useWidgetLoadingAtomValue()
|
|
37
|
-
const hasSentInitialMessage = useRef(false)
|
|
38
|
-
const [tutorQuickActionsFF] = useDecision('lex_tutor_quick_actions')
|
|
39
34
|
const [lexTutorInitialMessageFF] = useDecision('lex_tutor_new_widget_initial_message')
|
|
40
35
|
|
|
36
|
+
const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
|
|
37
|
+
|
|
38
|
+
const profileId = useMemo(() => profileQuery.data?.id, [profileQuery.data?.id])
|
|
39
|
+
|
|
40
|
+
const messagesQueryConfig = useMemo(
|
|
41
|
+
() =>
|
|
42
|
+
getAllMessagesQuery({
|
|
43
|
+
conversationId,
|
|
44
|
+
profileId,
|
|
45
|
+
limit
|
|
46
|
+
}),
|
|
47
|
+
[conversationId, limit, profileId]
|
|
48
|
+
)
|
|
49
|
+
|
|
41
50
|
useRefEventListener<HTMLTextAreaElement>({
|
|
42
51
|
config: {
|
|
43
52
|
ref: chatInputRef,
|
|
@@ -68,20 +77,6 @@ function WidgetStarterPage() {
|
|
|
68
77
|
sendText(chatInputRef.current?.value)
|
|
69
78
|
}
|
|
70
79
|
|
|
71
|
-
const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
|
|
72
|
-
|
|
73
|
-
const profileId = useMemo(() => profileQuery.data?.id, [profileQuery.data?.id])
|
|
74
|
-
|
|
75
|
-
const messagesQueryConfig = useMemo(
|
|
76
|
-
() =>
|
|
77
|
-
getAllMessagesQuery({
|
|
78
|
-
conversationId,
|
|
79
|
-
profileId,
|
|
80
|
-
limit
|
|
81
|
-
}),
|
|
82
|
-
[conversationId, limit, profileId]
|
|
83
|
-
)
|
|
84
|
-
|
|
85
80
|
useEffect(() => {
|
|
86
81
|
if (!conversationId || !profileId) return
|
|
87
82
|
|
|
@@ -115,10 +110,7 @@ function WidgetStarterPage() {
|
|
|
115
110
|
if (initialMessage) {
|
|
116
111
|
setChatInputValue(testQuestionRegex(initialMessage))
|
|
117
112
|
sendText(initialMessage)
|
|
118
|
-
setWidgetSettings({
|
|
119
|
-
...settings,
|
|
120
|
-
initialMessage: ''
|
|
121
|
-
})
|
|
113
|
+
setWidgetSettings({ ...settings, initialMessage: '' })
|
|
122
114
|
hasSentInitialMessage.current = true
|
|
123
115
|
}
|
|
124
116
|
}, [
|
|
@@ -143,24 +135,10 @@ function WidgetStarterPage() {
|
|
|
143
135
|
}>
|
|
144
136
|
<div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
|
|
145
137
|
<div className='grid-area-[a] flex min-h-0 flex-col max-md:px-[1.125rem] max-md:pt-[1.125rem] md:px-5 md:pt-5'>
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
showContent={isMobile}
|
|
149
|
-
tutorName={name}
|
|
150
|
-
/>
|
|
151
|
-
|
|
152
|
-
<div className='my-auto'>
|
|
153
|
-
<GreetingsCard author={authorName} tutorName={name} isDarkTheme={isDarkTheme} />
|
|
154
|
-
</div>
|
|
138
|
+
<WidgetStarterPageHeader />
|
|
139
|
+
<WidgetStarterPageContent />
|
|
155
140
|
</div>
|
|
156
|
-
{
|
|
157
|
-
<QuickActionButtons
|
|
158
|
-
className='grid-area-[b] my-4 flex flex-shrink-0 snap-x snap-mandatory gap-2 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
|
|
159
|
-
isDarkTheme={isDarkTheme}
|
|
160
|
-
send={sendText}
|
|
161
|
-
loading={!isSparkieReady}
|
|
162
|
-
/>
|
|
163
|
-
) : null}
|
|
141
|
+
<WidgetStarterPageActions send={sendText} />
|
|
164
142
|
</div>
|
|
165
143
|
</PageLayout>
|
|
166
144
|
)
|