app-tutor-ai-consumer 1.33.1 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/config/vitest/__mocks__/sparkie.tsx +2 -2
  3. package/package.json +2 -2
  4. package/src/@types/index.d.ts +3 -2
  5. package/src/bootstrap.ts +40 -0
  6. package/src/config/tanstack/query-provider.tsx +7 -3
  7. package/src/config/tests/handlers.ts +5 -4
  8. package/src/config/theme/init-theme.ts +11 -5
  9. package/src/index.backup.tsx +61 -0
  10. package/src/index.tsx +80 -17
  11. package/src/lib/components/dropdown-actions/dropdown-actions.tsx +87 -0
  12. package/src/lib/components/dropdown-actions/dropdownActions.builder.ts +58 -0
  13. package/src/lib/components/dropdown-actions/dropdownActions.spec.tsx +76 -0
  14. package/src/lib/components/dropdown-actions/index.ts +1 -0
  15. package/src/lib/components/dropdown-actions/types.ts +16 -0
  16. package/src/lib/components/errors/generic/generic-error.tsx +11 -8
  17. package/src/lib/components/icons/document.svg +3 -0
  18. package/src/lib/components/icons/file.svg +3 -0
  19. package/src/lib/components/icons/icon-names.d.ts +7 -0
  20. package/src/lib/components/icons/image.svg +3 -0
  21. package/src/lib/components/icons/pdf.svg +3 -0
  22. package/src/lib/components/icons/plus.svg +3 -0
  23. package/src/lib/components/icons/retry.svg +3 -0
  24. package/src/lib/components/icons/spreadsheet.svg +3 -0
  25. package/src/lib/components/index.ts +1 -0
  26. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -3
  27. package/src/lib/hooks/index.ts +1 -0
  28. package/src/lib/hooks/use-chat-file-upload/constants.ts +11 -0
  29. package/src/lib/hooks/use-chat-file-upload/index.ts +1 -0
  30. package/src/lib/hooks/use-chat-file-upload/types.ts +14 -0
  31. package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.spec.ts +59 -0
  32. package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.ts +28 -0
  33. package/src/lib/hooks/use-click-outside/index.ts +1 -0
  34. package/src/lib/hooks/use-click-outside/use-click-outside.tsx +23 -0
  35. package/src/lib/hooks/use-click-outside/useClickOutside.spec.ts +102 -0
  36. package/src/lib/utils/index.ts +1 -0
  37. package/src/lib/utils/is-theme-dark.ts +21 -0
  38. package/src/main/hooks/use-initial-store/index.ts +1 -0
  39. package/src/main/hooks/use-initial-store/use-initial-store.tsx +64 -0
  40. package/src/main/hooks/use-initial-tab/index.ts +1 -0
  41. package/src/main/hooks/use-initial-tab/use-initial-tab.tsx +84 -0
  42. package/src/main/index.ts +1 -0
  43. package/src/main/main-content.tsx +14 -0
  44. package/src/main/main-wrapper.tsx +16 -0
  45. package/src/main/main.spec.tsx +5 -3
  46. package/src/main/main.tsx +7 -16
  47. package/src/main/types.ts +5 -0
  48. package/src/modules/global-providers/global-providers.tsx +2 -21
  49. package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +1 -1
  50. package/src/modules/messages/__tests__/signed-urls.builder.ts +42 -0
  51. package/src/modules/messages/components/chat-file-preview/chat-file-preview.builder.ts +121 -0
  52. package/src/modules/messages/components/chat-file-preview/chat-file-preview.spec.tsx +107 -0
  53. package/src/modules/messages/components/chat-file-preview/chat-file-preview.tsx +45 -0
  54. package/src/modules/messages/components/chat-file-preview/components/close-button/close-button.tsx +31 -0
  55. package/src/modules/messages/components/chat-file-preview/components/close-button/index.ts +1 -0
  56. package/src/modules/messages/components/chat-file-preview/components/close-button/types.ts +4 -0
  57. package/src/modules/messages/components/chat-file-preview/components/document-preview/document-preview.tsx +63 -0
  58. package/src/modules/messages/components/chat-file-preview/components/document-preview/index.ts +1 -0
  59. package/src/modules/messages/components/chat-file-preview/components/document-preview/types.ts +10 -0
  60. package/src/modules/messages/components/chat-file-preview/components/error-preview/error-preview.tsx +37 -0
  61. package/src/modules/messages/components/chat-file-preview/components/error-preview/index.ts +1 -0
  62. package/src/modules/messages/components/chat-file-preview/components/error-preview/types.ts +4 -0
  63. package/src/modules/messages/components/chat-file-preview/components/image-preview/image-preview.tsx +32 -0
  64. package/src/modules/messages/components/chat-file-preview/components/image-preview/index.ts +1 -0
  65. package/src/modules/messages/components/chat-file-preview/components/image-preview/types.ts +6 -0
  66. package/src/modules/messages/components/chat-file-preview/constants.ts +22 -0
  67. package/src/modules/messages/components/chat-file-preview/index.ts +1 -0
  68. package/src/modules/messages/components/chat-file-preview/types.ts +13 -0
  69. package/src/modules/messages/components/chat-file-preview/utils.spec.ts +38 -0
  70. package/src/modules/messages/components/chat-file-preview/utils.ts +13 -0
  71. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.builder.ts +19 -0
  72. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.spec.tsx +58 -0
  73. package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.tsx +80 -0
  74. package/src/modules/messages/components/chat-file-uploader/index.ts +1 -0
  75. package/src/modules/messages/components/chat-file-uploader/types.ts +4 -0
  76. package/src/modules/messages/components/index.ts +1 -0
  77. package/src/modules/messages/constants.ts +2 -1
  78. package/src/modules/messages/hooks/use-get-signed-urls/index.ts +1 -0
  79. package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.spec.tsx +27 -0
  80. package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.tsx +38 -0
  81. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +1 -2
  82. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +3 -8
  83. package/src/modules/messages/hooks/use-prefetch-messages/index.ts +1 -0
  84. package/src/modules/messages/hooks/use-prefetch-messages/use-prefetch-messages.tsx +28 -0
  85. package/src/modules/messages/hooks/use-suspense-messages/index.ts +2 -0
  86. package/src/modules/messages/hooks/use-suspense-messages/types.ts +4 -0
  87. package/src/modules/messages/hooks/use-suspense-messages/use-suspense-messages.tsx +21 -0
  88. package/src/modules/messages/service.direct.ts +19 -1
  89. package/src/modules/messages/service.ts +1 -1
  90. package/src/modules/messages/store/messages-max-count.atom.ts +2 -2
  91. package/src/modules/messages/types.ts +15 -1
  92. package/src/modules/messages/utils/set-messages-cache/utils.ts +1 -1
  93. package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +7 -6
  94. package/src/modules/sparkie/__tests__/sparkie.mock.ts +1 -4
  95. package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +34 -28
  96. package/src/modules/sparkie/service.ts +1 -1
  97. package/src/modules/sparkie/store/index.ts +1 -0
  98. package/src/modules/sparkie/store/sparkie-state.atom.ts +13 -0
  99. package/src/modules/widget/__tests__/widget-settings-props.builder.ts +20 -1
  100. package/src/modules/widget/components/constants.tsx +3 -1
  101. package/src/modules/widget/components/error-page/error-page.spec.tsx +17 -0
  102. package/src/modules/widget/components/error-page/error-page.tsx +10 -0
  103. package/src/modules/widget/components/error-page/index.ts +1 -0
  104. package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
  105. package/src/modules/widget/components/starter-page/starter-page-actions/index.ts +1 -0
  106. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +68 -0
  107. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +34 -0
  108. package/src/modules/widget/components/starter-page/starter-page-content/index.ts +1 -0
  109. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +16 -0
  110. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +28 -0
  111. package/src/modules/widget/components/starter-page/starter-page-header/index.ts +1 -0
  112. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +45 -0
  113. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +36 -0
  114. package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
  115. package/src/modules/widget/components/starter-page/starter-page.tsx +15 -109
  116. package/src/modules/widget/hooks/index.ts +0 -1
  117. package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
  118. package/src/modules/widget/store/create-store.ts +7 -0
  119. package/src/modules/widget/store/index.ts +1 -0
  120. package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
  121. package/src/modules/widget/store/widget-tabs.atom.ts +17 -6
  122. package/src/types.ts +10 -0
  123. package/src/wrapper.tsx +52 -0
  124. package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
  125. package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +0 -56
@@ -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'
@@ -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
- (e: Event) => {
18
- const target = e.target as HTMLTextAreaElement
19
- setChatInputValue(target.value)
20
- },
21
- [setChatInputValue]
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
+ })
@@ -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
+ })
@@ -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'
@@ -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
+ })
@@ -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('@/src/modules/sparkie/hooks/use-init-sparkie', () => ({
14
- useInitSparkie: vi.fn(() => true)
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('@optimizely/react-sdk')
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, useEffect, useMemo, useRef } from 'react'
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 { useMediaQuery, useRefEventListener } from '@/src/lib/hooks'
3
+ import { useRefEventListener } from '@/src/lib/hooks'
7
4
  import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/components'
8
- import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/hooks'
9
- import { useMessagesMaxCount } from '@/src/modules/messages/store'
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
- import { QuickActionButtons } from '../quick-action-buttons'
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
- const profileQuery = useGetProfile()
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-area-[a] flex min-h-0 flex-col max-md:px-[1.125rem] max-md:pt-[1.125rem] md:px-5 md:pt-5'>
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} />
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
  )
@@ -1,4 +1,3 @@
1
- export * from './use-init-widget'
2
1
  export * from './use-listen-to-theme-change-event'
3
2
  export * from './use-listen-to-visibility-events'
4
3
  export * from './use-send-view-tutor-event'
@@ -1,20 +1,22 @@
1
1
  import { useLayoutEffect } from 'react'
2
2
  import { produce } from 'immer'
3
+ import type { useStore } from 'jotai'
3
4
 
4
5
  import { initTheme } from '@/src/config/theme'
5
6
  import { TutorWidgetEvents } from '../../events'
6
- import { useWidgetSettingsAtom } from '../../store'
7
-
8
- function useListenToThemeChangeEvent() {
9
- const [widgetSettings, setWidgetSettings] = useWidgetSettingsAtom()
7
+ import { widgetSettingsAtom } from '../../store'
10
8
 
9
+ function useListenToThemeChangeEvent(store?: ReturnType<typeof useStore>) {
11
10
  useLayoutEffect(() => {
12
11
  const clear = TutorWidgetEvents['c3po-app-widget-theme-change'].handler(({ theme }) => {
12
+ const widgetSettings = store?.get(widgetSettingsAtom)
13
+
13
14
  initTheme(theme)
14
15
 
15
16
  if (!widgetSettings || theme === widgetSettings?.config?.theme) return
16
17
 
17
- setWidgetSettings(
18
+ store?.set(
19
+ widgetSettingsAtom,
18
20
  produce(widgetSettings, (draft) => {
19
21
  draft.config = { ...draft.config, theme }
20
22
 
@@ -24,7 +26,7 @@ function useListenToThemeChangeEvent() {
24
26
  })
25
27
 
26
28
  return () => clear?.()
27
- }, [setWidgetSettings, widgetSettings])
29
+ }, [store])
28
30
  }
29
31
 
30
32
  export default useListenToThemeChangeEvent
@@ -0,0 +1,7 @@
1
+ import { createStore as jotaiCreateStore } from 'jotai'
2
+
3
+ export const createStore = () => {
4
+ const store = jotaiCreateStore()
5
+
6
+ return store
7
+ }
@@ -1,3 +1,4 @@
1
+ export * from './create-store'
1
2
  export * from './widget-container-intrinsic-height.atom'
2
3
  export * from './widget-loading.atom'
3
4
  export * from './widget-scrolling.atom'
@@ -1,10 +1,5 @@
1
1
  import { atom, useAtomValue } from 'jotai'
2
2
 
3
- import { widgetSettingsAtom } from './widget-settings.atom'
4
-
5
- export const widgetSettingsConfigAgentParentAtom = atom((get) => {
6
- const settings = get(widgetSettingsAtom)
7
- return settings?.config?.metadata?.parent === 'AGENT'
8
- })
3
+ export const widgetSettingsConfigAgentParentAtom = atom(false)
9
4
 
10
5
  export const useIsAgentParentAtomValue = () => useAtomValue(widgetSettingsConfigAgentParentAtom)
@@ -7,9 +7,12 @@ export type WidgetTabsProps = {
7
7
  history: Set<CurrentTabKey>
8
8
  }
9
9
 
10
+ // Prevent memory issues
11
+ const MAX_HISTORY_SIZE = 10
12
+
10
13
  const INITIAL_PROPS: WidgetTabsProps = {
11
- currentTab: 'starter',
12
- history: new Set(['starter'])
14
+ currentTab: 'loading',
15
+ history: new Set(['loading'])
13
16
  }
14
17
 
15
18
  export const widgetTabsAtom = atom<WidgetTabsProps>(INITIAL_PROPS)
@@ -21,11 +24,15 @@ export const setWidgetTabsAtom = atom(
21
24
 
22
25
  if (currentValue.currentTab === currentTab) return
23
26
 
24
- const history = new Set([...currentValue.history, currentTab])
27
+ const historyArray = [...currentValue.history, currentTab]
28
+
29
+ if (historyArray.length > MAX_HISTORY_SIZE) {
30
+ historyArray.shift()
31
+ }
25
32
 
26
33
  const config: WidgetTabsProps = {
27
34
  currentTab,
28
- history
35
+ history: new Set(historyArray)
29
36
  }
30
37
 
31
38
  set(widgetTabsAtom, config)
@@ -40,8 +47,12 @@ export const goBackTabAtom = atom(null, (get, set) => {
40
47
 
41
48
  historyArray.pop()
42
49
 
50
+ const previousTab = historyArray[historyArray.length - 1]
51
+
52
+ if (!previousTab) return
53
+
43
54
  const config: WidgetTabsProps = {
44
- currentTab: historyArray[historyArray.length - 1],
55
+ currentTab: previousTab,
45
56
  history: new Set(historyArray)
46
57
  }
47
58
 
@@ -50,5 +61,5 @@ export const goBackTabAtom = atom(null, (get, set) => {
50
61
 
51
62
  export const useGetWidgetTabsAtom = () => useAtom(widgetTabsAtom)
52
63
  export const useWidgetTabsAtom = () => useAtom(setWidgetTabsAtom)
53
- export const useWidgetTabsValueAtom = () => useAtomValue(setWidgetTabsAtom)
64
+ export const useWidgetTabsValueAtom = () => useAtomValue(widgetTabsAtom)
54
65
  export const useWidgetGoBackTabAtom = () => useAtom(goBackTabAtom)