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.
Files changed (129) hide show
  1. package/CHANGELOG.md +37 -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 +15 -4
  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 +19 -10
  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 +48 -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 +6 -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/chat-page/chat-page.tsx +58 -0
  101. package/src/modules/widget/components/constants.tsx +3 -1
  102. package/src/modules/widget/components/error-page/error-page.spec.tsx +17 -0
  103. package/src/modules/widget/components/error-page/error-page.tsx +12 -0
  104. package/src/modules/widget/components/error-page/index.ts +1 -0
  105. package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
  106. package/src/modules/widget/components/starter-page/starter-page-actions/index.ts +1 -0
  107. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +68 -0
  108. package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +34 -0
  109. package/src/modules/widget/components/starter-page/starter-page-content/index.ts +1 -0
  110. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +16 -0
  111. package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +28 -0
  112. package/src/modules/widget/components/starter-page/starter-page-header/index.ts +1 -0
  113. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +45 -0
  114. package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +36 -0
  115. package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
  116. package/src/modules/widget/components/starter-page/starter-page.tsx +15 -109
  117. package/src/modules/widget/hooks/index.ts +1 -1
  118. package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
  119. package/src/modules/widget/hooks/use-retry-last-message/index.ts +1 -0
  120. package/src/modules/widget/hooks/use-retry-last-message/use-retry-last-message.tsx +37 -0
  121. package/src/modules/widget/store/create-store.ts +7 -0
  122. package/src/modules/widget/store/index.ts +2 -0
  123. package/src/modules/widget/store/widget-last-user-message.atom.ts +12 -0
  124. package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
  125. package/src/modules/widget/store/widget-tabs.atom.ts +17 -6
  126. package/src/types.ts +10 -0
  127. package/src/wrapper.tsx +52 -0
  128. package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
  129. 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
- (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,4 @@
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'
3
+ export * from './use-retry-last-message'
4
4
  export * from './use-send-view-tutor-event'