app-tutor-ai-consumer 1.32.3 → 1.33.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/config/vitest/__mocks__/sparkie.tsx +1 -1
  3. package/package.json +2 -2
  4. package/src/@types/index.d.ts +2 -3
  5. package/src/config/tanstack/query-provider.tsx +3 -7
  6. package/src/index.tsx +10 -63
  7. package/src/lib/components/icons/icon-names.d.ts +1 -0
  8. package/src/lib/components/icons/tutor-logo.svg +9 -0
  9. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -1
  10. package/src/modules/global-providers/global-providers.tsx +6 -1
  11. package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +1 -1
  12. package/src/modules/messages/components/chat-input/chat-input.tsx +1 -1
  13. package/src/modules/messages/components/message-item/message-item.tsx +1 -2
  14. package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +54 -119
  15. package/src/modules/messages/service.direct.ts +1 -1
  16. package/src/modules/messages/service.ts +2 -3
  17. package/src/modules/messages/types.ts +1 -1
  18. package/src/modules/messages/utils/set-messages-cache/utils.ts +1 -1
  19. package/src/modules/sparkie/service.ts +3 -2
  20. package/src/modules/widget/components/ai-disclaimer/ai-disclaimer.tsx +19 -0
  21. package/src/modules/widget/components/ai-disclaimer/index.ts +1 -0
  22. package/src/modules/widget/components/chat-page/chat-page.tsx +15 -8
  23. package/src/modules/widget/components/container/container.tsx +14 -0
  24. package/src/modules/widget/components/greetings-card/greetings-card.tsx +9 -2
  25. package/src/modules/widget/components/starter-page/starter-page.tsx +14 -7
  26. package/src/types.ts +0 -9
  27. package/src/index.backup.tsx +0 -61
  28. package/src/lib/hooks/use-response-timeout/index.ts +0 -1
  29. package/src/lib/hooks/use-response-timeout/use-response-timeout.tsx +0 -42
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [1.33.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.33.0...v1.33.1) (2025-10-22)
2
+
3
+ ### Bug Fixes
4
+
5
+ - markdownrenderer ([ae8c08a](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/ae8c08a0620cd95a54a787ac00ddab78640d3f45))
6
+
7
+ # [1.33.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.32.3...v1.33.0) (2025-10-16)
8
+
9
+ ### Features
10
+
11
+ - change sparkie call to messages history page ([ec5aa19](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/ec5aa19ee913204881491d729d7fd979e05bf337))
12
+
1
13
  ## [1.32.3](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.32.2...v1.32.3) (2025-10-13)
2
14
 
3
15
  ## [1.32.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.32.1...v1.32.2) (2025-10-08)
@@ -4,7 +4,7 @@ import {
4
4
  SparkieMessageServiceMock,
5
5
  SparkieCursorServiceMock
6
6
  } from '@/src/modules/sparkie/__tests__/sparkie.mock'
7
- import MessageService from '@hotmart-org-ca/sparkie/dist/MessageService'
7
+ import MessageService from '@hotmart/sparkie/dist/MessageService'
8
8
 
9
9
  vi.mock('@hotmart/sparkie', () => ({ default: SparkieMock }))
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.32.3",
3
+ "version": "1.33.1",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -108,8 +108,8 @@
108
108
  "dependencies": {
109
109
  "@hot-observability-js/react": "~1.1.0",
110
110
  "@hotmart-org-ca/hot-observability-js": "~1.1.0",
111
- "@hotmart-org-ca/sparkie": "~5.1.4",
112
111
  "@hotmart/event-agent-js": "~1.1.2",
112
+ "@hotmart/sparkie": "~5.1.0",
113
113
  "@optimizely/react-sdk": "~3.2.4",
114
114
  "@tanstack/query-sync-storage-persister": "~5.80.7",
115
115
  "@tanstack/react-query": "~5.80.6",
@@ -1,4 +1,4 @@
1
- import type { StartTutorWidgetProps, WidgetInstance } from '@/src/types'
1
+ import type { StartTutorWidgetProps } from '@/src/types'
2
2
 
3
3
  export {}
4
4
 
@@ -13,8 +13,7 @@ declare global {
13
13
  elementId: StartTutorWidgetProps['elementId'],
14
14
  settings: StartTutorWidgetProps['settings']
15
15
  ) => Promise<void>
16
- closeChatWidget: () => Promise<void>
17
- __CHAT_WIDGET_INSTANCE__?: WidgetInstance
16
+ closeChatWidget: () => void
18
17
  TOKEN: string
19
18
  }
20
19
  }
@@ -1,16 +1,12 @@
1
- import type { QueryClient } from '@tanstack/react-query'
2
1
  import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
3
2
  import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
4
3
  import type { PropsWithChildren } from 'react'
5
4
 
6
- import { persister } from './query-client'
5
+ import { persister, queryClient } from './query-client'
7
6
 
8
- export type QueryProviderProps = PropsWithChildren<{
9
- showDevTools?: boolean
10
- queryClient: QueryClient
11
- }>
7
+ export type QueryProviderProps = PropsWithChildren<{ showDevTools?: boolean }>
12
8
 
13
- function QueryProvider({ children, queryClient, showDevTools = true }: QueryProviderProps) {
9
+ function QueryProvider({ children, showDevTools = true }: QueryProviderProps) {
14
10
  return (
15
11
  <PersistQueryClientProvider client={queryClient} persistOptions={{ persister }}>
16
12
  {children}
package/src/index.tsx CHANGED
@@ -4,14 +4,13 @@ import './config/styles/index.css'
4
4
  import { StrictMode } from 'react'
5
5
  import { createRoot } from 'react-dom/client'
6
6
 
7
- import { queryClient, QueryProvider } from '@/src/config/tanstack'
8
7
  import { initTheme } from '@/src/config/theme'
9
8
  import { version } from '../package.json'
10
9
 
11
10
  import { initLanguage } from './config/i18n'
12
11
  import { devMode, productionMode } from './lib/utils'
13
12
  import { Main } from './main'
14
- import { SparkieService } from './modules/sparkie'
13
+ import { TutorWidgetEvents } from './modules/widget'
15
14
  import type { Theme, WidgetSettingProps } from './types'
16
15
 
17
16
  const loadMainStyles = () => {
@@ -34,80 +33,28 @@ window.startChatWidget = async (
34
33
  elementId = 'tutor-chat-app-widget',
35
34
  settings: WidgetSettingProps
36
35
  ) => {
37
- if (window.__CHAT_WIDGET_INSTANCE__) {
38
- await window.closeChatWidget()
39
- }
40
-
41
36
  if (!devMode) {
42
37
  loadMainStyles()
43
38
  }
44
39
 
45
- const container = document.getElementById(elementId) as HTMLElement
40
+ const rootElement = document.getElementById(elementId) as HTMLElement
46
41
 
47
- if (!container) return
42
+ if (!rootElement) return
48
43
 
49
- container.setAttribute('id', 'hotmart-app-tutor-ai-consumer-root')
50
- const theme = (container.getAttribute('data-theme') ?? 'dark') as Theme
44
+ rootElement.setAttribute('id', 'hotmart-app-tutor-ai-consumer-root')
45
+ const theme = (rootElement.getAttribute('data-theme') ?? 'dark') as Theme
46
+ const root = createRoot(rootElement)
51
47
 
52
- const root = createRoot(container)
48
+ await initLanguage(settings.locale)
49
+ initTheme(theme)
53
50
 
54
51
  if (root) {
55
- initTheme(theme)
56
- await initLanguage(settings.locale)
57
-
58
52
  root.render(
59
53
  <StrictMode>
60
- <QueryProvider queryClient={queryClient} showDevTools={false}>
61
- <Main settings={{ ...settings, config: { ...settings.config, theme } }} />
62
- </QueryProvider>
54
+ <Main settings={{ ...settings, config: { ...settings.config, theme } }} />
63
55
  </StrictMode>
64
56
  )
65
-
66
- window.__CHAT_WIDGET_INSTANCE__ = { root, container, queryClient }
67
57
  }
68
58
  }
69
59
 
70
- window.closeChatWidget = async () => {
71
- const chatWidgetInstance = window.__CHAT_WIDGET_INSTANCE__
72
-
73
- if (!chatWidgetInstance) return
74
-
75
- const { root, container, queryClient } = chatWidgetInstance
76
-
77
- try {
78
- await queryClient.cancelQueries()
79
- } catch (err) {
80
- console.error('Error cancelling queries on widget close', err)
81
- }
82
-
83
- try {
84
- root.unmount()
85
- } catch (err) {
86
- console.warn('Error unmounting widget root', err)
87
- }
88
-
89
- if (typeof queryClient.clear === 'function') {
90
- try {
91
- queryClient.clear()
92
- } catch {
93
- queryClient.getQueryCache().clear()
94
- queryClient.getMutationCache().clear()
95
- }
96
- } else {
97
- queryClient.getQueryCache().clear()
98
- queryClient.getMutationCache().clear()
99
- }
100
-
101
- try {
102
- container.remove()
103
- } catch {
104
- // ignore
105
- }
106
-
107
- try {
108
- await SparkieService.destroySparkie()
109
- window.__CHAT_WIDGET_INSTANCE__ = undefined
110
- } catch (err) {
111
- console.error('Error destroying Sparkie instance', err)
112
- }
113
- }
60
+ window.closeChatWidget = () => TutorWidgetEvents['c3po-app-widget-close'].dispatch()
@@ -23,5 +23,6 @@ export type ValidIconNames =
23
23
  | 'sparkle-tutor-light'
24
24
  | 'sparkle-tutor'
25
25
  | 'stop'
26
+ | 'tutor-logo'
26
27
  | 'warning'
27
28
  | 'waveforms'
@@ -0,0 +1,9 @@
1
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0.896914 6.12971C0.779441 6.10837 0.779442 5.89161 0.896914 5.87028C5.07103 5.11209 5.67461 2.64887 5.90592 0.812118C5.92066 0.695019 6.07934 0.695018 6.09408 0.812117C6.32539 2.64887 6.92897 5.11209 11.1031 5.87028C11.2206 5.89161 11.2206 6.10837 11.1031 6.12971C6.92897 6.8879 6.32539 9.35112 6.09408 11.1879C6.07934 11.305 5.92066 11.305 5.90592 11.1879C5.67461 9.35112 5.07103 6.8879 0.896914 6.12971Z" fill="url(#paint0_linear_7672_1716)"/>
3
+ <defs>
4
+ <linearGradient id="paint0_linear_7672_1716" x1="1.125" y1="-0.0649467" x2="10.634" y2="12.0859" gradientUnits="userSpaceOnUse">
5
+ <stop stop-color="#44D0FF"/>
6
+ <stop offset="1" stop-color="#B48EFF"/>
7
+ </linearGradient>
8
+ </defs>
9
+ </svg>
@@ -79,7 +79,7 @@ const mdComponents: Partial<Components> = {
79
79
  )
80
80
  },
81
81
  p({ children }) {
82
- return <span className='inline-block [&:not(:only-child)]:my-3'>{children}</span>
82
+ return <span className='block [&:not(:only-child)]:my-3'>{children}</span>
83
83
  },
84
84
  ul({ children }) {
85
85
  return <ul className='my-3 list-inside list-disc'>{children}</ul>
@@ -2,6 +2,7 @@ import { type PropsWithChildren, useEffect } from 'react'
2
2
  import { v4 } from 'uuid'
3
3
 
4
4
  import { OptimizelyProvider } from '@/src/config/optimizely'
5
+ import { QueryProvider } from '@/src/config/tanstack'
5
6
  import { useWidgetSettingsAtom } from '@/src/modules/widget'
6
7
  import type { WidgetSettingProps } from '@/src/types'
7
8
 
@@ -20,7 +21,11 @@ function GlobalProviders({ children, settings }: GlobalProvidersProps) {
20
21
  setWidgetSettings(settings)
21
22
  }, [setWidgetSettings, settings])
22
23
 
23
- return <OptimizelyProvider settings={settings}>{children}</OptimizelyProvider>
24
+ return (
25
+ <OptimizelyProvider settings={settings}>
26
+ <QueryProvider>{children}</QueryProvider>
27
+ </OptimizelyProvider>
28
+ )
24
29
  }
25
30
 
26
31
  export default GlobalProviders
@@ -1,4 +1,4 @@
1
- import type { MessageContent } from '@hotmart-org-ca/sparkie/dist/MessageService'
1
+ import type { MessageContent } from '@hotmart/sparkie/dist/MessageService'
2
2
 
3
3
  import { chance } from '@/src/config/tests'
4
4
  import type { IMessageWithSenderData } from '../types'
@@ -57,7 +57,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
57
57
  ),
58
58
  { 'cursor-not-allowed opacity-40': inputDisabled }
59
59
  )}
60
- placeholder={t('send_message.field.placeholder')}
60
+ placeholder={t('send_message.field.placeholder_v2')}
61
61
  value={value}
62
62
  onChange={handleChange}
63
63
  onKeyDown={handleKeyDown}
@@ -22,8 +22,7 @@ function MessageItem({ message }: { message: ParsedMessage }) {
22
22
  <div
23
23
  data-test='messages-item'
24
24
  className={clsx('w-full overflow-x-hidden rounded-lg p-3', {
25
- 'max-w-max bg-[rgb(from_var(--hc-color-neutral-300)_r_g_b_/_0.8)]': messageFromUser,
26
- 'bg-neutral-200': messageFromAi
25
+ 'max-w-max bg-[rgb(from_var(--hc-color-neutral-300)_r_g_b_/_0.8)]': messageFromUser
27
26
  })}>
28
27
  <MessageContentTypeRenderer message={message} />
29
28
  </div>
@@ -1,11 +1,10 @@
1
1
  import { useCallback, useEffect, useMemo } from 'react'
2
- import type { InfiniteData, QueryClient } from '@tanstack/react-query'
2
+ import type { InfiniteData } from '@tanstack/react-query'
3
3
  import { useQueryClient } from '@tanstack/react-query'
4
4
  import { produce } from 'immer'
5
5
 
6
6
  import { ComponentSource, DataHubService } from '@/src/config/datahub'
7
7
  import { ViewTutorAnswerMessageSchema } from '@/src/config/datahub/schemas/tutor'
8
- import { useResponseTimeout } from '@/src/lib/hooks/use-response-timeout'
9
8
  import { useUpdateCursor } from '@/src/modules/cursor/hooks'
10
9
  import { useGetProfile } from '@/src/modules/profile'
11
10
  import { SparkieService } from '@/src/modules/sparkie'
@@ -18,59 +17,6 @@ import { useMessagesMaxCount, useUnreadMessagesSetAtom } from '../../store'
18
17
  import type { FetchMessagesResponse, IMessageWithSenderData } from '../../types'
19
18
  import { getAllMessagesQuery } from '../use-infinite-get-messages'
20
19
 
21
- export type MessageReceivedProps = {
22
- data: IMessageWithSenderData
23
- queryClient: QueryClient
24
- conversationId: string
25
- profileId: string
26
- limit: number
27
- messageIsMineCallback?: (data: IMessageWithSenderData) => void
28
- messageIsNotMineCallback?: (data: IMessageWithSenderData) => void
29
- errorCallback?: (error: unknown) => void
30
- }
31
-
32
- const messageReceivedUtil = ({
33
- queryClient,
34
- data,
35
- conversationId,
36
- profileId,
37
- limit,
38
- messageIsMineCallback,
39
- messageIsNotMineCallback,
40
- errorCallback
41
- }: MessageReceivedProps) => {
42
- try {
43
- const messagesQueryConfig = getAllMessagesQuery({ conversationId, profileId, limit })
44
- const queryData = queryClient.getQueryData<InfiniteData<FetchMessagesResponse>>(
45
- messagesQueryConfig.queryKey
46
- )
47
-
48
- const idsList = new Set(
49
- queryData?.pages.flatMap((items) => items.messages.map((msg) => msg.id))
50
- )
51
-
52
- if (idsList.has(data.id)) return
53
-
54
- queryClient.setQueryData<InfiniteData<FetchMessagesResponse>>(
55
- messagesQueryConfig.queryKey,
56
- (oldData) =>
57
- produce(oldData, (draft) => {
58
- draft?.pages.at(-1)?.messages?.push(data)
59
- return draft
60
- })
61
- )
62
-
63
- const isMine = data.contactId === profileId
64
-
65
- if (isMine) {
66
- return messageIsMineCallback?.(data)
67
- }
68
- messageIsNotMineCallback?.(data)
69
- } catch (error) {
70
- errorCallback?.(error)
71
- }
72
- }
73
-
74
20
  const useSubscribeMessageReceivedEvent = () => {
75
21
  const [settings] = useWidgetSettingsAtom()
76
22
  const [, setWidgetLoading] = useWidgetLoadingAtom()
@@ -80,78 +26,75 @@ const useSubscribeMessageReceivedEvent = () => {
80
26
  const useUpdateCursorMutation = useUpdateCursor()
81
27
  const limit = useMessagesMaxCount()
82
28
  const isAgentMode = useIsAgentParentAtomValue()
83
- const { reset: resetLoadingTimeout, startTimeout } = useResponseTimeout()
84
29
 
85
30
  const conversationId = useMemo(() => String(settings?.conversationId), [settings?.conversationId])
86
31
  const profileId = useMemo(() => String(profileQuery?.data?.id), [profileQuery?.data?.id])
87
-
88
- const messageIsMineCallback = useCallback(
89
- (data: IMessageWithSenderData) => {
90
- // The cursor should update only with my messages
91
- useUpdateCursorMutation.mutate(data.conversationId)
92
- },
93
- [useUpdateCursorMutation]
32
+ const messagesQueryConfig = useMemo(
33
+ () =>
34
+ getAllMessagesQuery({
35
+ conversationId,
36
+ profileId,
37
+ limit
38
+ }),
39
+ [conversationId, limit, profileId]
94
40
  )
95
41
 
96
- const messageIsNotMineCallback = useCallback(
42
+ const messageReceived = useCallback(
97
43
  (data: IMessageWithSenderData) => {
98
- setWidgetLoading(false)
99
-
100
- DataHubService.sendEvent({
101
- schema: new ViewTutorAnswerMessageSchema({
102
- correlationId: data.metadata.correlationId,
103
- messageId: data.id,
104
- componentSource: isAgentMode
105
- ? ComponentSource.PRODUCT_AGENT
106
- : ComponentSource.HOTMART_TUTOR
44
+ if (data.conversationId !== conversationId) return
45
+
46
+ const queryData = queryClient.getQueryData<InfiniteData<FetchMessagesResponse>>(
47
+ messagesQueryConfig.queryKey
48
+ )
49
+
50
+ const idsList = new Set(
51
+ queryData?.pages.flatMap((items) => items.messages.map((msg) => msg.id))
52
+ )
53
+
54
+ if (idsList.has(data.id)) return
55
+
56
+ queryClient.setQueryData<InfiniteData<FetchMessagesResponse>>(
57
+ messagesQueryConfig.queryKey,
58
+ (oldData) => {
59
+ return produce(oldData, (draft) => {
60
+ draft?.pages.at(-1)?.messages?.push(data)
61
+ return draft
62
+ })
63
+ }
64
+ )
65
+
66
+ const isMine = data.contactId === profileId
67
+
68
+ if (!isMine) {
69
+ DataHubService.sendEvent({
70
+ schema: new ViewTutorAnswerMessageSchema({
71
+ correlationId: data.metadata.correlationId,
72
+ messageId: data.id,
73
+ componentSource: isAgentMode
74
+ ? ComponentSource.PRODUCT_AGENT
75
+ : ComponentSource.HOTMART_TUTOR
76
+ })
107
77
  })
108
- })
109
78
 
110
- addUnreadMessagesToSet({ itemId: data.id })
111
- },
112
- [addUnreadMessagesToSet, isAgentMode, setWidgetLoading]
113
- )
114
-
115
- const errorCallback = useCallback(
116
- (error: unknown) => {
117
- if (error) {
118
- setWidgetLoading(false)
119
- console.error(error)
120
- throw new Error('Error getting realtime messages')
79
+ addUnreadMessagesToSet({ itemId: data.id })
80
+ setTimeout(() => setWidgetLoading(false), 100)
81
+ } else {
82
+ // The cursor should update only with my messages
83
+ useUpdateCursorMutation.mutate(data.conversationId)
121
84
  }
122
85
  },
123
- [setWidgetLoading]
124
- )
125
-
126
- const messageReceived = useCallback(
127
- (data: IMessageWithSenderData) => {
128
- if (data.conversationId === conversationId)
129
- messageReceivedUtil({
130
- queryClient,
131
- data,
132
- conversationId,
133
- profileId,
134
- limit,
135
- messageIsMineCallback,
136
- messageIsNotMineCallback,
137
- errorCallback
138
- })
139
- },
140
86
  [
87
+ addUnreadMessagesToSet,
141
88
  conversationId,
142
- errorCallback,
143
- limit,
144
- messageIsMineCallback,
145
- messageIsNotMineCallback,
89
+ isAgentMode,
90
+ messagesQueryConfig.queryKey,
146
91
  profileId,
147
- queryClient
92
+ queryClient,
93
+ setWidgetLoading,
94
+ useUpdateCursorMutation
148
95
  ]
149
96
  )
150
97
 
151
- const startLoadingTimeout = useCallback(() => {
152
- startTimeout(() => setWidgetLoading(false))
153
- }, [setWidgetLoading, startTimeout])
154
-
155
98
  useEffect(() => {
156
99
  SparkieService.subscribeEvents({ messageReceived })
157
100
 
@@ -159,14 +102,6 @@ const useSubscribeMessageReceivedEvent = () => {
159
102
  SparkieService.removeEventSubscription({ messageReceived })
160
103
  }
161
104
  }, [messageReceived])
162
-
163
- useEffect(() => {
164
- startLoadingTimeout()
165
-
166
- return () => {
167
- resetLoadingTimeout()
168
- }
169
- }, [resetLoadingTimeout, startLoadingTimeout])
170
105
  }
171
106
 
172
107
  export default useSubscribeMessageReceivedEvent
@@ -1,4 +1,4 @@
1
- import type { Message } from '@hotmart-org-ca/sparkie/dist/MessageService'
1
+ import type { Message } from '@hotmart/sparkie/dist/MessageService'
2
2
 
3
3
  import { api } from '@/src/config/request'
4
4
 
@@ -1,5 +1,4 @@
1
- import type { Message } from '@hotmart-org-ca/sparkie/dist/MessageService'
2
- import type MessageService from '@hotmart-org-ca/sparkie/dist/MessageService'
1
+ import type { Message } from '@hotmart/sparkie/dist/MessageService'
3
2
 
4
3
  import { ApiError } from '@/src/config/request'
5
4
  import { HttpCodes } from '@/src/lib/utils'
@@ -16,7 +15,7 @@ import type {
16
15
  } from './types'
17
16
 
18
17
  class MessagesService {
19
- async getSparkieMessageService(): Promise<MessageService> {
18
+ async getSparkieMessageService() {
20
19
  try {
21
20
  const messageService = await SparkieService.getMessageService()
22
21
 
@@ -2,7 +2,7 @@ import type {
2
2
  Message as SparkieMsg,
3
3
  MessageContent,
4
4
  SenderData
5
- } from '@hotmart-org-ca/sparkie/dist/MessageService'
5
+ } from '@hotmart/sparkie/dist/MessageService'
6
6
 
7
7
  export type IMessage = SparkieMsg & {
8
8
  metadata: {
@@ -1,4 +1,4 @@
1
- import type { Message } from '@hotmart-org-ca/sparkie/dist/MessageService'
1
+ import type { Message } from '@hotmart/sparkie/dist/MessageService'
2
2
  import type { InfiniteData, QueryClient } from '@tanstack/react-query'
3
3
  import { produce } from 'immer'
4
4
 
@@ -1,4 +1,4 @@
1
- import Sparkie from '@hotmart-org-ca/sparkie'
1
+ import Sparkie from '@hotmart/sparkie'
2
2
 
3
3
  import { MSG_MAX_COUNT } from '../messages'
4
4
 
@@ -110,7 +110,8 @@ class SparkieService {
110
110
 
111
111
  while (attempt <= maxRetries) {
112
112
  try {
113
- if (!token?.trim()) throw new Error('Invalid or missing token for Sparkie initialization')
113
+ if (!token || !token.trim())
114
+ throw new Error('Invalid or missing token for Sparkie initialization')
114
115
 
115
116
  const sparkie = this.sparkieInstance
116
117
  const service = await sparkie.init(token, { skipPresenceSetup })
@@ -0,0 +1,19 @@
1
+ import { useTranslation } from 'react-i18next'
2
+
3
+ import { Icon } from '@/src/lib/components/icons'
4
+
5
+ const AIDisclaimer = () => {
6
+ const { t } = useTranslation()
7
+
8
+ return (
9
+ <div className='mt-4 flex w-full items-center gap-1 text-xs text-neutral-500'>
10
+ <p className='mb-0'>{t('ai_disclaimer.technology')}</p>
11
+
12
+ <Icon name='tutor-logo' className='inline-flex h-3 w-3 align-middle' />
13
+
14
+ <p className='mb-0'>{t('ai_disclaimer.hotmart_ai')}</p>
15
+ </div>
16
+ )
17
+ }
18
+
19
+ export default AIDisclaimer
@@ -0,0 +1 @@
1
+ export { default as AIDisclaimer } from './ai-disclaimer'
@@ -17,6 +17,7 @@ import {
17
17
  useWidgetTabsValueAtom
18
18
  } from '../../store'
19
19
  import { testQuestionRegex } from '../../utils'
20
+ import { AIDisclaimer } from '../ai-disclaimer'
20
21
  import { WidgetHeader } from '../header'
21
22
  import { PageLayout } from '../page-layout'
22
23
 
@@ -94,14 +95,20 @@ function ChatPage() {
94
95
  return (
95
96
  <PageLayout
96
97
  asideChild={
97
- <ChatInput
98
- name='new-chat-msg-input'
99
- ref={chatInputRef}
100
- onSend={widgetTabs.currentTab === 'chat' ? handleSendMessage : undefined}
101
- loading={sendTextMessageMutation.isPending}
102
- inputDisabled={messagesQuery?.isLoading}
103
- buttonDisabled={widgetLoading || messagesQuery?.isLoading || !value.trim()}
104
- />
98
+ <>
99
+ <ChatInput
100
+ name='new-chat-msg-input'
101
+ ref={chatInputRef}
102
+ onSend={widgetTabs.currentTab === 'chat' ? handleSendMessage : undefined}
103
+ loading={sendTextMessageMutation.isPending}
104
+ inputDisabled={messagesQuery?.isLoading}
105
+ buttonDisabled={widgetLoading || messagesQuery?.isLoading || !value.trim()}
106
+ />
107
+
108
+ <div className='mx-auto w-fit'>
109
+ <AIDisclaimer />
110
+ </div>
111
+ </>
105
112
  }>
106
113
  <div className='max-md:px-[1.125rem] max-md:pt-[1.125rem] md:px-5 md:pt-5'>
107
114
  <WidgetHeader
@@ -1,9 +1,21 @@
1
+ import { useEffect, useState } from 'react'
2
+
1
3
  import { useSubscribeMessageReceivedEvent } from '@/src/modules/messages/hooks'
2
4
  import { useSubscribeThreadClosedEvent } from '@/src/modules/thread/hooks'
3
5
  import { useListenToVisibilityEvents } from '../../hooks'
4
6
  import { useWidgetTabsAtom } from '../../store'
5
7
  import { WIDGET_TABS } from '../constants'
6
8
 
9
+ // TODO: REMOVE
10
+ const hotmartRumKey = 'app-tutor-ai-consumer::hotmart-rum::activate'
11
+ const useSentryDebugger = () => {
12
+ const [logError] = useState(() => Boolean(window?.localStorage?.getItem?.(hotmartRumKey)))
13
+
14
+ useEffect(() => {
15
+ if (logError) throw new Error(hotmartRumKey)
16
+ }, [logError])
17
+ }
18
+
7
19
  function WidgetContainer() {
8
20
  const [widgetTabs] = useWidgetTabsAtom()
9
21
 
@@ -11,6 +23,8 @@ function WidgetContainer() {
11
23
  useSubscribeThreadClosedEvent()
12
24
  useListenToVisibilityEvents()
13
25
 
26
+ useSentryDebugger()
27
+
14
28
  return (
15
29
  <div className='flex h-full flex-col items-center justify-stretch overflow-hidden'>
16
30
  {WIDGET_TABS[widgetTabs.currentTab]}
@@ -1,6 +1,7 @@
1
1
  import clsx from 'clsx'
2
2
  import { useTranslation } from 'react-i18next'
3
3
 
4
+ import { useIsAgentParentAtomValue } from '../../store'
4
5
  import { AIAvatar } from '../ai-avatar'
5
6
 
6
7
  export type GreetingsCardProps = {
@@ -11,6 +12,7 @@ export type GreetingsCardProps = {
11
12
 
12
13
  function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCardProps) {
13
14
  const { t } = useTranslation()
15
+ const isAgentMode = useIsAgentParentAtomValue()
14
16
 
15
17
  return (
16
18
  <div className='flex flex-col items-center justify-center'>
@@ -31,7 +33,12 @@ function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCard
31
33
  'text-white': isDarkTheme,
32
34
  'text-gray-900': !isDarkTheme
33
35
  })}>
34
- {t('general.greetings.firstMessage', { tutorName })}
36
+ {t(
37
+ isAgentMode
38
+ ? 'general.greetings.agentFirstMessage'
39
+ : 'general.greetings.firstMessage',
40
+ { tutorName }
41
+ )}
35
42
  </h3>
36
43
  </div>
37
44
  <p
@@ -39,7 +46,7 @@ function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCard
39
46
  'text-gray-400': isDarkTheme,
40
47
  'text-neutral-600': !isDarkTheme
41
48
  })}>
42
- {t('general.greetings.description')}
49
+ {t(isAgentMode ? 'general.greetings.agentDescription' : 'general.greetings.description')}
43
50
  </p>
44
51
  </div>
45
52
  </div>
@@ -12,6 +12,7 @@ import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
12
12
  import { TutorWidgetEvents } from '../../events'
13
13
  import { useWidgetLoadingAtomValue, useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
14
14
  import { testQuestionRegex } from '../../utils'
15
+ import { AIDisclaimer } from '../ai-disclaimer'
15
16
  import { GreetingsCard } from '../greetings-card'
16
17
  import { WidgetHeader } from '../header'
17
18
  import { PageLayout } from '../page-layout'
@@ -133,13 +134,19 @@ function WidgetStarterPage() {
133
134
  return (
134
135
  <PageLayout
135
136
  asideChild={
136
- <ChatInput
137
- name='new-chat-msg-input'
138
- ref={chatInputRef}
139
- onSend={handleSend}
140
- buttonDisabled={widgetLoading || !chatInputValue.trim()}
141
- loading={!isSparkieReady}
142
- />
137
+ <>
138
+ <ChatInput
139
+ name='new-chat-msg-input'
140
+ ref={chatInputRef}
141
+ onSend={handleSend}
142
+ buttonDisabled={widgetLoading || !chatInputValue.trim()}
143
+ loading={!isSparkieReady}
144
+ />
145
+
146
+ <div className='mx-auto w-fit'>
147
+ <AIDisclaimer />
148
+ </div>
149
+ </>
143
150
  }>
144
151
  <div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
145
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'>
package/src/types.ts CHANGED
@@ -1,6 +1,3 @@
1
- import type { QueryClient } from '@tanstack/react-query'
2
- import type { Root } from 'react-dom/client'
3
-
4
1
  import type { ILanguages } from './config/i18n'
5
2
 
6
3
  export type StartTutorWidgetProps = {
@@ -71,9 +68,3 @@ export interface ICustomEvent<T = object> {
71
68
  handler: (listener: EventListenerOrEventListenerObject) => () => void | Promise<void>
72
69
  dispatch: <D = unknown>(detail?: D) => void
73
70
  }
74
-
75
- export type WidgetInstance = {
76
- root: Root
77
- container: HTMLElement
78
- queryClient: QueryClient
79
- }
@@ -1,61 +0,0 @@
1
- import './config/styles/global.css'
2
- import './config/styles/index.css'
3
-
4
- import { StrictMode } from 'react'
5
- import { createRoot } from 'react-dom/client'
6
-
7
- import { initTheme } from '@/src/config/theme'
8
- import { version } from '../package.json'
9
-
10
- import { initLanguage } from './config/i18n'
11
- import { devMode, productionMode } from './lib/utils'
12
- import { Main } from './main'
13
- import { TutorWidgetEvents } from './modules/widget'
14
- import type { Theme, WidgetSettingProps } from './types'
15
-
16
- const loadMainStyles = () => {
17
- const isProduction = productionMode
18
- const bundlePath = !isProduction
19
- ? `${process.env.BUNDLE_PATH}/`
20
- : `${process.env.BUNDLE_PATH}/${process.env.APP_NAME}/_current/`
21
-
22
- const cssPath = `${bundlePath}app-tutor-ai-consumer.css?v=${version}`
23
-
24
- if (!document.querySelector(`link[href="${cssPath}"]`)) {
25
- const linkElement = document.createElement('link')
26
- linkElement.rel = 'stylesheet'
27
- linkElement.href = cssPath
28
- document.head.appendChild(linkElement)
29
- }
30
- }
31
-
32
- window.startChatWidget = async (
33
- elementId = 'tutor-chat-app-widget',
34
- settings: WidgetSettingProps
35
- ) => {
36
- if (!devMode) {
37
- loadMainStyles()
38
- }
39
-
40
- const rootElement = document.getElementById(elementId) as HTMLElement
41
-
42
- if (!rootElement) return
43
-
44
- rootElement.setAttribute('id', 'hotmart-app-tutor-ai-consumer-root')
45
- const theme = (rootElement.getAttribute('data-theme') ?? 'dark') as Theme
46
- const root = createRoot(rootElement)
47
-
48
- await initLanguage(settings.locale)
49
- initTheme(theme)
50
-
51
- if (root) {
52
- root.render(
53
- <StrictMode>
54
- <Main settings={{ ...settings, config: { ...settings.config, theme } }} />
55
- </StrictMode>
56
- )
57
- }
58
- }
59
-
60
- window.closeChatWidget = () =>
61
- Promise.resolve(TutorWidgetEvents['c3po-app-widget-close'].dispatch())
@@ -1 +0,0 @@
1
- export { default as useResponseTimeout } from './use-response-timeout'
@@ -1,42 +0,0 @@
1
- import { useCallback, useRef } from 'react'
2
-
3
- const FIVE_MINUTES = 5 * 60 * 1000
4
-
5
- function useResponseTimeout() {
6
- const timeoutRef = useRef<NodeJS.Timeout>(null)
7
- const isWaitingRef = useRef(false)
8
-
9
- const startTimeout = useCallback((callback: () => void) => {
10
- if (timeoutRef.current) clearTimeout(timeoutRef.current)
11
-
12
- isWaitingRef.current = true
13
-
14
- timeoutRef.current = setTimeout(() => {
15
- callback()
16
- isWaitingRef.current = false
17
- }, FIVE_MINUTES)
18
- }, [])
19
-
20
- const reset = useCallback(() => {
21
- if (timeoutRef.current) {
22
- clearTimeout(timeoutRef.current)
23
- timeoutRef.current = null
24
- }
25
- isWaitingRef.current = false
26
- }, [])
27
-
28
- const cleanup = useCallback(() => {
29
- if (timeoutRef.current) {
30
- clearTimeout(timeoutRef.current)
31
- }
32
- }, [])
33
-
34
- return {
35
- startTimeout,
36
- cleanup,
37
- reset,
38
- isWaiting: isWaitingRef.current
39
- }
40
- }
41
-
42
- export default useResponseTimeout