app-tutor-ai-consumer 1.33.1 → 1.35.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 +2 -2
- package/package.json +2 -2
- package/src/@types/index.d.ts +3 -2
- package/src/bootstrap.ts +40 -0
- package/src/config/tanstack/query-provider.tsx +15 -4
- package/src/config/tests/handlers.ts +5 -4
- package/src/config/theme/init-theme.ts +11 -5
- package/src/index.backup.tsx +61 -0
- package/src/index.tsx +80 -17
- 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 +19 -10
- 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 +7 -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/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 +48 -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 +2 -21
- package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +1 -1
- 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/index.ts +1 -0
- 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 +6 -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-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 +19 -1
- package/src/modules/messages/service.ts +1 -1
- package/src/modules/messages/store/messages-max-count.atom.ts +2 -2
- package/src/modules/messages/types.ts +15 -1
- package/src/modules/messages/utils/set-messages-cache/utils.ts +1 -1
- 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 +34 -28
- package/src/modules/sparkie/service.ts +1 -1
- package/src/modules/sparkie/store/index.ts +1 -0
- package/src/modules/sparkie/store/sparkie-state.atom.ts +13 -0
- package/src/modules/widget/__tests__/widget-settings-props.builder.ts +20 -1
- package/src/modules/widget/components/chat-page/chat-page.tsx +58 -0
- package/src/modules/widget/components/constants.tsx +3 -1
- package/src/modules/widget/components/error-page/error-page.spec.tsx +17 -0
- package/src/modules/widget/components/error-page/error-page.tsx +12 -0
- package/src/modules/widget/components/error-page/index.ts +1 -0
- package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
- 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 +34 -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 +16 -0
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +28 -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 +45 -0
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +36 -0
- package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
- package/src/modules/widget/components/starter-page/starter-page.tsx +15 -109
- package/src/modules/widget/hooks/index.ts +1 -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/hooks/use-retry-last-message/index.ts +1 -0
- package/src/modules/widget/hooks/use-retry-last-message/use-retry-last-message.tsx +37 -0
- package/src/modules/widget/store/create-store.ts +7 -0
- package/src/modules/widget/store/index.ts +2 -0
- package/src/modules/widget/store/widget-last-user-message.atom.ts +12 -0
- package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
- package/src/modules/widget/store/widget-tabs.atom.ts +17 -6
- package/src/types.ts +10 -0
- package/src/wrapper.tsx +52 -0
- 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
|
@@ -12,8 +12,10 @@ import { useGetProfile } from '@/src/modules/profile'
|
|
|
12
12
|
import { TutorWidgetEvents } from '../../events'
|
|
13
13
|
import { useSendViewTutorEvent } from '../../hooks/use-send-view-tutor-event'
|
|
14
14
|
import {
|
|
15
|
+
useWidgetLastUserMessageAtom,
|
|
15
16
|
useWidgetLoadingAtom,
|
|
16
17
|
useWidgetSettingsAtomValue,
|
|
18
|
+
useWidgetTabsAtom,
|
|
17
19
|
useWidgetTabsValueAtom
|
|
18
20
|
} from '../../store'
|
|
19
21
|
import { testQuestionRegex } from '../../utils'
|
|
@@ -24,13 +26,16 @@ import { PageLayout } from '../page-layout'
|
|
|
24
26
|
function ChatPage() {
|
|
25
27
|
const chatInputRef = useRef<HTMLTextAreaElement>(null)
|
|
26
28
|
const scrollerRef = useRef<HTMLDivElement>(null)
|
|
29
|
+
const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
27
30
|
const settings = useWidgetSettingsAtomValue()
|
|
28
31
|
const profileQuery = useGetProfile()
|
|
29
32
|
const widgetTabs = useWidgetTabsValueAtom()
|
|
33
|
+
const [, setTab] = useWidgetTabsAtom()
|
|
30
34
|
const sendTextMessageMutation = useSendTextMessage()
|
|
31
35
|
const limit = useMessagesMaxCount()
|
|
32
36
|
const [value, setValue] = useChatInputValueAtom()
|
|
33
37
|
const [widgetLoading, setWidgetLoading] = useWidgetLoadingAtom()
|
|
38
|
+
const [, setLastUserMessage] = useWidgetLastUserMessageAtom()
|
|
34
39
|
const isMobile = useMediaQuery({ maxSize: 'md' })
|
|
35
40
|
const hasSentInitialMessage = useRef(false)
|
|
36
41
|
const [lexTutorInitialMessageFF] = useDecision('lex_tutor_new_widget_initial_message')
|
|
@@ -49,6 +54,36 @@ function ChatPage() {
|
|
|
49
54
|
|
|
50
55
|
const messagesQuery = useInfiniteQuery(messagesQueryConfig)
|
|
51
56
|
|
|
57
|
+
const isAgentMode = useMemo(
|
|
58
|
+
() => settings?.config?.metadata?.parent === 'AGENT',
|
|
59
|
+
[settings?.config?.metadata?.parent]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const msgCount = useMemo(() => {
|
|
63
|
+
if (!messagesQuery.data) return 0
|
|
64
|
+
return Array.from(messagesQuery.data.values()).reduce(
|
|
65
|
+
(total: number, messages) => total + messages.length,
|
|
66
|
+
0
|
|
67
|
+
)
|
|
68
|
+
}, [messagesQuery.data])
|
|
69
|
+
|
|
70
|
+
const hasUserMessageWithoutResponse = useMemo(() => {
|
|
71
|
+
if (!isAgentMode || !messagesQuery.data || msgCount === 0) return false
|
|
72
|
+
|
|
73
|
+
const allMessages = Array.from(messagesQuery.data.values()).flat()
|
|
74
|
+
const userMessages = allMessages.filter((msg) => msg?.metadata?.author === 'user')
|
|
75
|
+
const aiMessages = allMessages.filter((msg) => msg?.metadata?.author !== 'user')
|
|
76
|
+
|
|
77
|
+
if (userMessages.length > aiMessages.length && aiMessages.length === 0) {
|
|
78
|
+
const lastUserMsg = userMessages[userMessages.length - 1]
|
|
79
|
+
if (lastUserMsg?.text) {
|
|
80
|
+
setLastUserMessage(lastUserMsg.text)
|
|
81
|
+
}
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}, [isAgentMode, messagesQuery.data, msgCount, setLastUserMessage])
|
|
86
|
+
|
|
52
87
|
useSendViewTutorEvent()
|
|
53
88
|
|
|
54
89
|
const handleSendMessage = () => {
|
|
@@ -92,6 +127,29 @@ function ChatPage() {
|
|
|
92
127
|
}
|
|
93
128
|
}, [messagesQuery.isError, setWidgetLoading])
|
|
94
129
|
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (hasUserMessageWithoutResponse) {
|
|
132
|
+
setWidgetLoading(true)
|
|
133
|
+
|
|
134
|
+
loadingTimeoutRef.current = setTimeout(() => {
|
|
135
|
+
setWidgetLoading(false)
|
|
136
|
+
setTab('error')
|
|
137
|
+
}, 60000)
|
|
138
|
+
} else {
|
|
139
|
+
if (loadingTimeoutRef.current) {
|
|
140
|
+
clearTimeout(loadingTimeoutRef.current)
|
|
141
|
+
loadingTimeoutRef.current = null
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return () => {
|
|
146
|
+
if (loadingTimeoutRef.current) {
|
|
147
|
+
clearTimeout(loadingTimeoutRef.current)
|
|
148
|
+
loadingTimeoutRef.current = null
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}, [hasUserMessageWithoutResponse, setWidgetLoading, setTab])
|
|
152
|
+
|
|
95
153
|
return (
|
|
96
154
|
<PageLayout
|
|
97
155
|
asideChild={
|
|
@@ -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
|
}
|
|
@@ -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,12 @@
|
|
|
1
|
+
import { GenericError } from '@/src/lib/components'
|
|
2
|
+
import { useRetryLastMessage } from '../../hooks'
|
|
3
|
+
import { useWidgetSettingsAtomValue } from '../../store'
|
|
4
|
+
|
|
5
|
+
function WidgetErrorPage() {
|
|
6
|
+
const settings = useWidgetSettingsAtomValue()
|
|
7
|
+
const handleRetry = useRetryLastMessage()
|
|
8
|
+
|
|
9
|
+
return <GenericError isDarkMode={settings?.config?.theme === 'dark'} onRetry={handleRetry} />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default WidgetErrorPage
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as WidgetErrorPage } from './error-page'
|
|
@@ -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>
|
|
@@ -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 { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
|
|
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/store', async (importActual) => ({
|
|
15
|
+
...(await importActual()),
|
|
16
|
+
useSparkieStateAtomValue: 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(useSparkieStateAtomValue).mockReturnValue('initialized')
|
|
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,34 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useDecision } from '@optimizely/react-sdk'
|
|
3
|
+
|
|
4
|
+
import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
|
|
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 sparkieState = useSparkieStateAtomValue()
|
|
13
|
+
|
|
14
|
+
const isDarkTheme = useMemo(() => settings?.config?.theme === 'dark', [settings?.config?.theme])
|
|
15
|
+
const isSparkieReady = useMemo(() => sparkieState === 'initialized', [sparkieState])
|
|
16
|
+
|
|
17
|
+
const shouldNotRender = useMemo(
|
|
18
|
+
() => [isAgentMode, !tutorQuickActionsFF?.enabled].some(Boolean),
|
|
19
|
+
[isAgentMode, tutorQuickActionsFF?.enabled]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if (shouldNotRender) return null
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<QuickActionButtons
|
|
26
|
+
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'
|
|
27
|
+
isDarkTheme={isDarkTheme}
|
|
28
|
+
send={send}
|
|
29
|
+
loading={!isSparkieReady}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default WidgetStarterPageActions
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as WidgetStarterPageContent } from './starter-page-content'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { render, screen } from '@/src/config/tests'
|
|
2
|
+
|
|
3
|
+
import WidgetStarterPageContent from './starter-page-content'
|
|
4
|
+
|
|
5
|
+
describe('<WidgetStarterPageContent />', () => {
|
|
6
|
+
const renderComponent = () => render(<WidgetStarterPageContent />)
|
|
7
|
+
|
|
8
|
+
it('should render greetings card when not in agent mode', () => {
|
|
9
|
+
renderComponent()
|
|
10
|
+
|
|
11
|
+
expect(screen.getByText(/sparkle-tutor-light/i)).toBeInTheDocument()
|
|
12
|
+
expect(screen.getByText(/general.greetings.hello/i)).toBeInTheDocument()
|
|
13
|
+
expect(screen.getByText(/general.greetings.firstMessage/i)).toBeInTheDocument()
|
|
14
|
+
expect(screen.getByText(/general.greetings.description/i)).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
})
|
package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
import { useWidgetSettingsAtomValue } from '../../../store'
|
|
5
|
+
import { GreetingsCard } from '../../greetings-card'
|
|
6
|
+
|
|
7
|
+
function WidgetStarterPageContent() {
|
|
8
|
+
const { t } = useTranslation()
|
|
9
|
+
const settings = useWidgetSettingsAtomValue()
|
|
10
|
+
|
|
11
|
+
const authorName = useMemo(() => {
|
|
12
|
+
const username = typeof settings?.user?.name === 'string' ? settings?.user?.name : ''
|
|
13
|
+
|
|
14
|
+
return username?.split?.(' ')?.[0] || ''
|
|
15
|
+
}, [settings?.user?.name])
|
|
16
|
+
|
|
17
|
+
const isDarkTheme = useMemo(() => settings?.config?.theme === 'dark', [settings?.config?.theme])
|
|
18
|
+
|
|
19
|
+
const name = useMemo(() => settings?.tutorName ?? t('general.name'), [settings?.tutorName, t])
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className='my-auto'>
|
|
23
|
+
<GreetingsCard author={authorName} tutorName={name} isDarkTheme={isDarkTheme} />
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
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,45 @@
|
|
|
1
|
+
import { render, screen } from '@/src/config/tests'
|
|
2
|
+
import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
|
|
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
|
+
|
|
9
|
+
vi.mock('@/src/modules/sparkie/store', async (importActual) => ({
|
|
10
|
+
...(await importActual()),
|
|
11
|
+
useSparkieStateAtomValue: vi.fn()
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
describe('<WidgetStarterPageHeader />', () => {
|
|
15
|
+
const renderComponent = () => render(<WidgetStarterPageHeader />)
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(true)
|
|
19
|
+
vi.mocked(useSparkieStateAtomValue).mockReturnValue('initializing')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should return null when rendered as agent mode', () => {
|
|
23
|
+
const { container } = renderComponent()
|
|
24
|
+
|
|
25
|
+
expect(container).toBeEmptyDOMElement()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should render without errors when is not rendered as agent', () => {
|
|
29
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
|
|
30
|
+
renderComponent()
|
|
31
|
+
|
|
32
|
+
expect(screen.getByRole('button', { name: /general.buttons.info Icon/i })).toBeInTheDocument()
|
|
33
|
+
expect(screen.getByRole('button', { name: /Close Icon/i })).toBeInTheDocument()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should render the archive button when isSparkieReady is true', () => {
|
|
37
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
|
|
38
|
+
vi.mocked(useSparkieStateAtomValue).mockReturnValue('initialized')
|
|
39
|
+
renderComponent()
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
screen.getByRole('button', { name: /general.buttons.archive Icon/i })
|
|
43
|
+
).toBeInTheDocument()
|
|
44
|
+
})
|
|
45
|
+
})
|
package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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 { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
|
|
7
|
+
import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '../../../store'
|
|
8
|
+
import { WidgetHeader } from '../../header'
|
|
9
|
+
|
|
10
|
+
const getEnabledButtons = (isSparkieReady: boolean): ValidIconNames[] => {
|
|
11
|
+
const btns = ['close', 'info'] as ValidIconNames[]
|
|
12
|
+
|
|
13
|
+
return isSparkieReady ? [...btns, 'archive'] : btns
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function WidgetStarterPageHeader() {
|
|
17
|
+
const { t } = useTranslation()
|
|
18
|
+
const settings = useWidgetSettingsAtomValue()
|
|
19
|
+
const isAgentMode = useIsAgentParentAtomValue()
|
|
20
|
+
const sparkieState = useSparkieStateAtomValue()
|
|
21
|
+
const isMobile = useMediaQuery({ maxSize: 'md' })
|
|
22
|
+
|
|
23
|
+
const name = useMemo(() => settings?.tutorName ?? t('general.name'), [settings?.tutorName, t])
|
|
24
|
+
|
|
25
|
+
if (isAgentMode) return null
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<WidgetHeader
|
|
29
|
+
enabledButtons={getEnabledButtons(sparkieState === 'initialized')}
|
|
30
|
+
tutorName={name}
|
|
31
|
+
showContent={isMobile}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default WidgetStarterPageHeader
|
|
@@ -2,6 +2,8 @@ import { useDecision } from '@optimizely/react-sdk'
|
|
|
2
2
|
|
|
3
3
|
import { render, screen } from '@/src/config/tests'
|
|
4
4
|
import { useSendTextMessage } from '@/src/modules/messages/hooks'
|
|
5
|
+
import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
|
|
6
|
+
import { useIsAgentParentAtomValue } from '../../store'
|
|
5
7
|
|
|
6
8
|
import WidgetStarterPage from './starter-page'
|
|
7
9
|
|
|
@@ -10,11 +12,17 @@ vi.mock('@/src/modules/messages/hooks', () => ({
|
|
|
10
12
|
useSendTextMessage: vi.fn()
|
|
11
13
|
}))
|
|
12
14
|
|
|
13
|
-
vi.mock('
|
|
14
|
-
|
|
15
|
+
vi.mock('@optimizely/react-sdk')
|
|
16
|
+
|
|
17
|
+
vi.mock('@/src/modules/sparkie/store', async (actual) => ({
|
|
18
|
+
...(await actual()),
|
|
19
|
+
useSparkieStateAtomValue: vi.fn()
|
|
15
20
|
}))
|
|
16
21
|
|
|
17
|
-
vi.mock('
|
|
22
|
+
vi.mock('../../store', async (actual) => ({
|
|
23
|
+
...(await actual()),
|
|
24
|
+
useIsAgentParentAtomValue: vi.fn()
|
|
25
|
+
}))
|
|
18
26
|
|
|
19
27
|
describe('WidgetStarterPage', () => {
|
|
20
28
|
const useSendTextMessageMock = { mutate: vi.fn() }
|
|
@@ -25,6 +33,8 @@ describe('WidgetStarterPage', () => {
|
|
|
25
33
|
beforeEach(() => {
|
|
26
34
|
vi.mocked(useSendTextMessage).mockReturnValue(useSendTextMessageMock as never)
|
|
27
35
|
vi.mocked(useDecision).mockReturnValue(useDecisionMock as never)
|
|
36
|
+
vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
|
|
37
|
+
vi.mocked(useSparkieStateAtomValue).mockReturnValue('initialized')
|
|
28
38
|
})
|
|
29
39
|
|
|
30
40
|
it('should render without errors', () => {
|
|
@@ -1,43 +1,24 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import { useDecision } from '@optimizely/react-sdk'
|
|
3
|
-
import { useQueryClient } from '@tanstack/react-query'
|
|
4
|
-
import { useTranslation } from 'react-i18next'
|
|
1
|
+
import { useCallback, useRef } from 'react'
|
|
5
2
|
|
|
6
|
-
import {
|
|
3
|
+
import { useRefEventListener } from '@/src/lib/hooks'
|
|
7
4
|
import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/components'
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { useGetProfile } from '@/src/modules/profile'
|
|
11
|
-
import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
|
|
12
|
-
import { TutorWidgetEvents } from '../../events'
|
|
13
|
-
import { useWidgetLoadingAtomValue, useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
|
|
14
|
-
import { testQuestionRegex } from '../../utils'
|
|
5
|
+
import { useSendTextMessage } from '@/src/modules/messages/hooks'
|
|
6
|
+
import { useWidgetLoadingAtomValue, useWidgetTabsAtom } from '../../store'
|
|
15
7
|
import { AIDisclaimer } from '../ai-disclaimer'
|
|
16
|
-
import { GreetingsCard } from '../greetings-card'
|
|
17
|
-
import { WidgetHeader } from '../header'
|
|
18
8
|
import { PageLayout } from '../page-layout'
|
|
19
|
-
|
|
9
|
+
|
|
10
|
+
import { WidgetStarterPageActions } from './starter-page-actions'
|
|
11
|
+
import { WidgetStarterPageContent } from './starter-page-content'
|
|
12
|
+
import { WidgetStarterPageHeader } from './starter-page-header'
|
|
20
13
|
|
|
21
14
|
function WidgetStarterPage() {
|
|
22
|
-
const { t } = useTranslation()
|
|
23
|
-
const [settings, setWidgetSettings] = useWidgetSettingsAtom()
|
|
24
15
|
const chatInputRef = useRef<HTMLTextAreaElement>(null)
|
|
16
|
+
|
|
25
17
|
const [chatInputValue, setChatInputValue] = useChatInputValueAtom()
|
|
26
18
|
const [, setWidgetTabs] = useWidgetTabsAtom()
|
|
27
19
|
const sendTextMessageMutation = useSendTextMessage()
|
|
28
|
-
|
|
29
|
-
const limit = useMessagesMaxCount()
|
|
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'
|
|
35
|
-
const isSparkieReady = useInitSparkie()
|
|
36
|
-
const isMobile = useMediaQuery({ maxSize: 'md' })
|
|
20
|
+
|
|
37
21
|
const widgetLoading = useWidgetLoadingAtomValue()
|
|
38
|
-
const hasSentInitialMessage = useRef(false)
|
|
39
|
-
const [tutorQuickActionsFF] = useDecision('lex_tutor_quick_actions')
|
|
40
|
-
const [lexTutorInitialMessageFF] = useDecision('lex_tutor_new_widget_initial_message')
|
|
41
22
|
|
|
42
23
|
useRefEventListener<HTMLTextAreaElement>({
|
|
43
24
|
config: {
|
|
@@ -69,68 +50,6 @@ function WidgetStarterPage() {
|
|
|
69
50
|
sendText(chatInputRef.current?.value)
|
|
70
51
|
}
|
|
71
52
|
|
|
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
|
-
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
if (!conversationId || !profileId) return
|
|
88
|
-
|
|
89
|
-
void queryClient.prefetchInfiniteQuery(messagesQueryConfig)
|
|
90
|
-
}, [conversationId, messagesQueryConfig, profileId, queryClient])
|
|
91
|
-
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
if (!isSparkieReady || hasSentInitialMessage.current || !lexTutorInitialMessageFF.enabled)
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
const clear = TutorWidgetEvents['tutor-initial-message'].handler(({ message }) => {
|
|
97
|
-
if (!message) return
|
|
98
|
-
|
|
99
|
-
setChatInputValue(testQuestionRegex(message))
|
|
100
|
-
sendText(message)
|
|
101
|
-
hasSentInitialMessage.current = true
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
return () => {
|
|
105
|
-
clear?.()
|
|
106
|
-
hasSentInitialMessage.current = false
|
|
107
|
-
}
|
|
108
|
-
}, [isSparkieReady, lexTutorInitialMessageFF.enabled, sendText, setChatInputValue])
|
|
109
|
-
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
if (!isSparkieReady || hasSentInitialMessage.current || !lexTutorInitialMessageFF.enabled)
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
const initialMessage = settings?.initialMessage
|
|
115
|
-
|
|
116
|
-
if (initialMessage) {
|
|
117
|
-
setChatInputValue(testQuestionRegex(initialMessage))
|
|
118
|
-
sendText(initialMessage)
|
|
119
|
-
setWidgetSettings({
|
|
120
|
-
...settings,
|
|
121
|
-
initialMessage: ''
|
|
122
|
-
})
|
|
123
|
-
hasSentInitialMessage.current = true
|
|
124
|
-
}
|
|
125
|
-
}, [
|
|
126
|
-
settings,
|
|
127
|
-
isSparkieReady,
|
|
128
|
-
sendText,
|
|
129
|
-
setChatInputValue,
|
|
130
|
-
lexTutorInitialMessageFF.enabled,
|
|
131
|
-
setWidgetSettings
|
|
132
|
-
])
|
|
133
|
-
|
|
134
53
|
return (
|
|
135
54
|
<PageLayout
|
|
136
55
|
asideChild={
|
|
@@ -140,7 +59,6 @@ function WidgetStarterPage() {
|
|
|
140
59
|
ref={chatInputRef}
|
|
141
60
|
onSend={handleSend}
|
|
142
61
|
buttonDisabled={widgetLoading || !chatInputValue.trim()}
|
|
143
|
-
loading={!isSparkieReady}
|
|
144
62
|
/>
|
|
145
63
|
|
|
146
64
|
<div className='mx-auto w-fit'>
|
|
@@ -149,25 +67,13 @@ function WidgetStarterPage() {
|
|
|
149
67
|
</>
|
|
150
68
|
}>
|
|
151
69
|
<div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
|
|
152
|
-
<div className='grid-
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
tutorName={name}
|
|
157
|
-
/>
|
|
158
|
-
|
|
159
|
-
<div className='my-auto'>
|
|
160
|
-
<GreetingsCard author={authorName} tutorName={name} isDarkTheme={isDarkTheme} />
|
|
70
|
+
<div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
|
|
71
|
+
<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'>
|
|
72
|
+
<WidgetStarterPageHeader />
|
|
73
|
+
<WidgetStarterPageContent />
|
|
161
74
|
</div>
|
|
75
|
+
<WidgetStarterPageActions send={sendText} />
|
|
162
76
|
</div>
|
|
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}
|
|
171
77
|
</div>
|
|
172
78
|
</PageLayout>
|
|
173
79
|
)
|