app-tutor-ai-consumer 1.34.0 → 1.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [1.35.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.35.0...v1.35.1) (2025-11-05)
2
+
3
+ # [1.35.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.34.0...v1.35.0) (2025-11-05)
4
+
5
+ ### Features
6
+
7
+ - add retry question ([7b75aba](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/7b75aba715ade6d5a7fec04237f8f13e643d47f5))
8
+
1
9
  # [1.34.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.33.1...v1.34.0) (2025-11-04)
2
10
 
3
11
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.34.0",
3
+ "version": "1.35.1",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -111,9 +111,7 @@
111
111
  "@hotmart-org-ca/sparkie": "~5.1.4",
112
112
  "@hotmart/event-agent-js": "~1.1.2",
113
113
  "@optimizely/react-sdk": "~3.2.4",
114
- "@tanstack/query-sync-storage-persister": "~5.80.7",
115
114
  "@tanstack/react-query": "~5.80.6",
116
- "@tanstack/react-query-persist-client": "~5.80.7",
117
115
  "clsx": "~2.1.1",
118
116
  "dayjs": "~1.11.13",
119
117
  "i18next": "~25.2.1",
@@ -1,4 +1,3 @@
1
- import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
2
1
  import { QueryClient } from '@tanstack/react-query'
3
2
 
4
3
  import { DEFAULT_STALE_TIME, HttpCodes } from '@/src/lib/utils'
@@ -26,7 +25,3 @@ export const queryClient = new QueryClient({
26
25
  }
27
26
  }
28
27
  })
29
-
30
- export const persister = createSyncStoragePersister({
31
- storage: window.localStorage
32
- })
@@ -1,10 +1,8 @@
1
1
  import type { QueryClient } from '@tanstack/react-query'
2
+ import { QueryClientProvider } from '@tanstack/react-query'
2
3
  import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
3
- import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
4
4
  import type { PropsWithChildren } from 'react'
5
5
 
6
- import { persister } from './query-client'
7
-
8
6
  export type QueryProviderProps = PropsWithChildren<{
9
7
  showDevTools?: boolean
10
8
  queryClient: QueryClient
@@ -12,10 +10,10 @@ export type QueryProviderProps = PropsWithChildren<{
12
10
 
13
11
  function QueryProvider({ children, queryClient, showDevTools = true }: QueryProviderProps) {
14
12
  return (
15
- <PersistQueryClientProvider client={queryClient} persistOptions={{ persister }}>
13
+ <QueryClientProvider client={queryClient}>
16
14
  {children}
17
15
  {showDevTools && <ReactQueryDevtools buttonPosition='top-right' />}
18
- </PersistQueryClientProvider>
16
+ </QueryClientProvider>
19
17
  )
20
18
  }
21
19
 
@@ -6,7 +6,13 @@ import { Button } from '@/src/lib/components'
6
6
  import { PageLayout, TutorWidgetEvents, useIsAgentParentAtomValue } from '@/src/modules/widget'
7
7
  import { WidgetHeader } from '@/src/modules/widget/components/header'
8
8
 
9
- function GenericError({ isDarkMode = false }: { isDarkMode?: boolean }) {
9
+ function GenericError({
10
+ isDarkMode = false,
11
+ onRetry
12
+ }: {
13
+ isDarkMode?: boolean
14
+ onRetry?: () => void
15
+ }) {
10
16
  const { t } = useTranslation()
11
17
  const isAgentMode = useIsAgentParentAtomValue()
12
18
 
@@ -32,7 +38,7 @@ function GenericError({ isDarkMode = false }: { isDarkMode?: boolean }) {
32
38
  <Button
33
39
  variant='brand'
34
40
  className='mx-auto w-full max-w-max rounded-lg !px-9 py-2 !font-light'
35
- onClick={() => window.location.reload()}
41
+ onClick={onRetry ? () => onRetry() : () => window.location.reload()}
36
42
  aria-label='Retry Button'>
37
43
  {t('general.buttons.try_again')}
38
44
  </Button>
@@ -1,13 +1,12 @@
1
- import { useEffect, useMemo, useRef } from 'react'
1
+ import { useEffect, useMemo } from 'react'
2
2
 
3
3
  import { useSuspenseMessages } from '@/src/modules/messages/hooks/use-suspense-messages'
4
4
  import { useSparkieStateAtomValue } from '@/src/modules/sparkie/store'
5
- import { useWidgetLoadingAtom, useWidgetTabsAtom } from '@/src/modules/widget'
5
+ import { useWidgetTabsAtom } from '@/src/modules/widget'
6
6
  import type { MainProps } from '../../types'
7
7
 
8
8
  function useInitialTab({ settings }: MainProps) {
9
9
  const [, setTab] = useWidgetTabsAtom()
10
- const [, setWidgetLoading] = useWidgetLoadingAtom()
11
10
  const sparkieState = useSparkieStateAtomValue()
12
11
 
13
12
  const suspenseMessagesQuery = useSuspenseMessages({ conversationId: settings.conversationId })
@@ -22,16 +21,6 @@ function useInitialTab({ settings }: MainProps) {
22
21
  [suspenseMessagesQuery.data.size]
23
22
  )
24
23
 
25
- const hasUserMessageWithoutResponse = useMemo(() => {
26
- if (!isAgentMode || !suspenseMessagesQuery.data || msgCount === 0) return false
27
-
28
- const allMessages = Array.from(suspenseMessagesQuery.data.values()).flat()
29
- const userMessages = allMessages.filter((msg) => msg.metadata.author === 'user')
30
- const aiMessages = allMessages.filter((msg) => msg.metadata.author !== 'user')
31
-
32
- return userMessages.length > aiMessages.length && aiMessages.length === 0
33
- }, [isAgentMode, suspenseMessagesQuery.data, msgCount])
34
-
35
24
  const showStarterPage = useMemo(
36
25
  () => !isAgentMode && sparkieState === 'initialized' && msgCount === 0,
37
26
  [isAgentMode, sparkieState, msgCount]
@@ -54,31 +43,6 @@ function useInitialTab({ settings }: MainProps) {
54
43
 
55
44
  if (showChatPage) return setTab('chat')
56
45
  }, [setTab, showChatPage, showErrorPage, showStarterPage])
57
-
58
- const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
59
-
60
- useEffect(() => {
61
- if (hasUserMessageWithoutResponse && showChatPage) {
62
- setWidgetLoading(true)
63
-
64
- loadingTimeoutRef.current = setTimeout(() => {
65
- setWidgetLoading(false)
66
- setTab('error')
67
- }, 60000)
68
- } else {
69
- if (loadingTimeoutRef.current) {
70
- clearTimeout(loadingTimeoutRef.current)
71
- loadingTimeoutRef.current = null
72
- }
73
- }
74
-
75
- return () => {
76
- if (loadingTimeoutRef.current) {
77
- clearTimeout(loadingTimeoutRef.current)
78
- loadingTimeoutRef.current = null
79
- }
80
- }
81
- }, [hasUserMessageWithoutResponse, showChatPage, setWidgetLoading, setTab])
82
46
  }
83
47
 
84
48
  export default useInitialTab
@@ -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,10 +1,12 @@
1
1
  import { GenericError } from '@/src/lib/components'
2
+ import { useRetryLastMessage } from '../../hooks'
2
3
  import { useWidgetSettingsAtomValue } from '../../store'
3
4
 
4
5
  function WidgetErrorPage() {
5
6
  const settings = useWidgetSettingsAtomValue()
7
+ const handleRetry = useRetryLastMessage()
6
8
 
7
- return <GenericError isDarkMode={settings?.config?.theme === 'dark'} />
9
+ return <GenericError isDarkMode={settings?.config?.theme === 'dark'} onRetry={handleRetry} />
8
10
  }
9
11
 
10
12
  export default WidgetErrorPage
@@ -1,3 +1,4 @@
1
1
  export * from './use-listen-to-theme-change-event'
2
2
  export * from './use-listen-to-visibility-events'
3
+ export * from './use-retry-last-message'
3
4
  export * from './use-send-view-tutor-event'
@@ -0,0 +1 @@
1
+ export { default as useRetryLastMessage } from './use-retry-last-message'
@@ -0,0 +1,37 @@
1
+ import { useMemo } from 'react'
2
+
3
+ import { useSendTextMessage } from '@/src/modules/messages/hooks'
4
+ import {
5
+ useWidgetLastUserMessageAtom,
6
+ useWidgetLastUserMessageAtomValue,
7
+ useWidgetSettingsAtomValue,
8
+ useWidgetTabsAtom
9
+ } from '../../store'
10
+
11
+ function useRetryLastMessage() {
12
+ const settings = useWidgetSettingsAtomValue()
13
+ const lastUserMessage = useWidgetLastUserMessageAtomValue()
14
+ const [, setLastUserMessage] = useWidgetLastUserMessageAtom()
15
+ const [, setTab] = useWidgetTabsAtom()
16
+ const sendTextMessageMutation = useSendTextMessage()
17
+
18
+ const isAgentMode = useMemo(
19
+ () => settings?.config?.metadata?.parent === 'AGENT',
20
+ [settings?.config?.metadata?.parent]
21
+ )
22
+
23
+ const retryLastMessage = () => {
24
+ if (isAgentMode && lastUserMessage) {
25
+ sendTextMessageMutation.mutate(lastUserMessage, {
26
+ onSuccess() {
27
+ setLastUserMessage('')
28
+ setTab('chat')
29
+ }
30
+ })
31
+ }
32
+ }
33
+
34
+ return retryLastMessage
35
+ }
36
+
37
+ export default useRetryLastMessage
@@ -1,5 +1,6 @@
1
1
  export * from './create-store'
2
2
  export * from './widget-container-intrinsic-height.atom'
3
+ export * from './widget-last-user-message.atom'
3
4
  export * from './widget-loading.atom'
4
5
  export * from './widget-scrolling.atom'
5
6
  export * from './widget-settings.atom'
@@ -0,0 +1,12 @@
1
+ import { atom, useAtom, useAtomValue } from 'jotai'
2
+
3
+ export const widgetLastUserMessageAtom = atom<string | null>(null)
4
+
5
+ export const setWidgetLastUserMessageAtom = atom(
6
+ (get) => get(widgetLastUserMessageAtom),
7
+ (_, set, lastMessage: string) => set(widgetLastUserMessageAtom, lastMessage)
8
+ )
9
+
10
+ export const useWidgetLastUserMessageAtom = () => useAtom(setWidgetLastUserMessageAtom)
11
+
12
+ export const useWidgetLastUserMessageAtomValue = () => useAtomValue(setWidgetLastUserMessageAtom)