app-tutor-ai-consumer 1.27.5 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # [1.28.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.27.6...v1.28.0) (2025-08-19)
2
+
3
+ ### Features
4
+
5
+ - add tutor initial message ([afc6bfd](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/afc6bfd9158656ce52afac9e79f4410728e1d7a5))
6
+
7
+ ## [1.27.6](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.27.5...v1.27.6) (2025-08-19)
8
+
1
9
  ## [1.27.5](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.27.4...v1.27.5) (2025-08-18)
2
10
 
3
11
  ## [1.27.4](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.27.3...v1.27.4) (2025-08-14)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.27.5",
3
+ "version": "1.28.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -44,7 +44,8 @@ if (devMode) {
44
44
  sessionId: v4(),
45
45
  user: {
46
46
  id: v4(),
47
- name: chance.name()
47
+ name: chance.name(),
48
+ ucode: 'e6e47e76-af44-46d2-8992-99c7057b53b6'
48
49
  }
49
50
  })
50
51
  })()
@@ -1,5 +1,5 @@
1
- .li{
2
- &>span {
1
+ .li {
2
+ & > span {
3
3
  display: inline !important;
4
4
  }
5
- }
5
+ }
@@ -9,6 +9,8 @@ function MessageItem({ message }: { message: ParsedMessage }) {
9
9
  const messageFromAi = message.metadata.author === 'ai'
10
10
  const isMediaVoice = message.type === 'media/voice'
11
11
 
12
+ if (messageFromAi && isMediaVoice) return null
13
+
12
14
  return (
13
15
  <div
14
16
  className={clsx(
@@ -25,15 +27,13 @@ function MessageItem({ message }: { message: ParsedMessage }) {
25
27
  })}>
26
28
  <MessageContentTypeRenderer message={message} />
27
29
  </div>
28
- {messageFromAi && isMediaVoice ? null : (
29
- <MessageActions
30
- className={clsx('flex items-center justify-between gap-2', {
31
- 'w-full': messageFromAi
32
- })}
33
- message={message}
34
- showActions={messageFromAi}
35
- />
36
- )}
30
+ <MessageActions
31
+ className={clsx('flex items-center justify-between gap-2', {
32
+ 'w-full': messageFromAi
33
+ })}
34
+ message={message}
35
+ showActions={messageFromAi}
36
+ />
37
37
  </div>
38
38
  )
39
39
  }
@@ -3,13 +3,7 @@ const AVATAR_ANIMATION_URL = `${process.env.STATIC_URL}/tutor/web/tutor_sparkle.
3
3
  const AvatarAnimation = () => {
4
4
  return (
5
5
  <div className='flex h-11 w-11 items-center justify-center rounded-lg bg-neutral-300'>
6
- <img
7
- src={AVATAR_ANIMATION_URL}
8
- alt=''
9
- aria-hidden
10
- className='max-w-[70%]'
11
- loading="lazy"
12
- />
6
+ <img src={AVATAR_ANIMATION_URL} alt='' aria-hidden className='max-w-[70%]' loading='lazy' />
13
7
  </div>
14
8
  )
15
9
  }
@@ -1,4 +1,5 @@
1
1
  import { createRef } from 'react'
2
+ import { useDecision } from '@optimizely/react-sdk'
2
3
 
3
4
  import { chance, render, screen, waitFor } from '@/src/config/tests'
4
5
  import { useScroller } from '@/src/modules/messages/hooks/use-scroller'
@@ -8,6 +9,8 @@ import * as Store from '../../store'
8
9
 
9
10
  import ChatPage from './chat-page'
10
11
 
12
+ vi.mock('@optimizely/react-sdk')
13
+
11
14
  vi.mock('@/src/modules/profile', () => ({ useGetProfile: vi.fn() }))
12
15
 
13
16
  vi.mock('@/src/modules/messages/hooks/use-scroller', () => ({
@@ -18,6 +21,7 @@ describe('ChatPage', () => {
18
21
  const defaultSettings = new WidgetSettingPropsBuilder()
19
22
  const scrollerRef = createRef<HTMLDivElement>()
20
23
  const scrollToButtonRef = createRef<HTMLButtonElement>()
24
+ const useDecisionMock = [{ enabled: true }]
21
25
 
22
26
  const useScrollerMock = {
23
27
  scrollerRef,
@@ -32,6 +36,7 @@ describe('ChatPage', () => {
32
36
  vi.spyOn(Store, 'useWidgetSettingsAtomValue').mockReturnValue(defaultSettings)
33
37
  vi.mocked(useGetProfile).mockReturnValue({ data: { id: chance.guid() } } as never)
34
38
  vi.mocked(useScroller).mockReturnValue(useScrollerMock)
39
+ vi.mocked(useDecision).mockReturnValue(useDecisionMock as never)
35
40
  })
36
41
 
37
42
  it('should render each fetched message item from API', async () => {
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useMemo, useRef } from 'react'
2
+ import { useDecision } from '@optimizely/react-sdk'
2
3
  import { useInfiniteQuery } from '@tanstack/react-query'
3
4
 
4
5
  import { useMediaQuery } from '@/src/lib/hooks'
@@ -8,11 +9,13 @@ import { MessagesContainer } from '@/src/modules/messages/components/messages-co
8
9
  import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/hooks'
9
10
  import { useMessagesMaxCount } from '@/src/modules/messages/store'
10
11
  import { useGetProfile } from '@/src/modules/profile'
12
+ import { TutorWidgetEvents } from '../../events'
11
13
  import {
12
14
  useWidgetLoadingAtom,
13
15
  useWidgetSettingsAtomValue,
14
16
  useWidgetTabsValueAtom
15
17
  } from '../../store'
18
+ import { testQuestionRegex } from '../../utils'
16
19
  import { WidgetHeader } from '../header'
17
20
  import { PageLayout } from '../page-layout'
18
21
 
@@ -27,6 +30,8 @@ function ChatPage() {
27
30
  const [value, setValue] = useChatInputValueAtom()
28
31
  const [widgetLoading, setWidgetLoading] = useWidgetLoadingAtom()
29
32
  const isMobile = useMediaQuery({ maxSize: 'md' })
33
+ const hasSentInitialMessage = useRef(false)
34
+ const [lexTutorInitialMessageFF] = useDecision('lex_tutor_new_widget_initial_message')
30
35
 
31
36
  const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
32
37
  const profileId = useMemo(() => profileQuery.data?.id, [profileQuery.data?.id])
@@ -55,6 +60,28 @@ function ChatPage() {
55
60
  })
56
61
  }
57
62
 
63
+ useEffect(() => {
64
+ if (hasSentInitialMessage.current || !lexTutorInitialMessageFF.enabled) return
65
+
66
+ const clear = TutorWidgetEvents['tutor-initial-message'].handler(({ message }) => {
67
+ if (message && !hasSentInitialMessage.current) {
68
+ setValue(testQuestionRegex(message))
69
+
70
+ sendTextMessageMutation.mutate(message, {
71
+ onSuccess() {
72
+ setValue('')
73
+ hasSentInitialMessage.current = true
74
+ }
75
+ })
76
+ }
77
+ })
78
+
79
+ return () => {
80
+ clear?.()
81
+ hasSentInitialMessage.current = false
82
+ }
83
+ }, [lexTutorInitialMessageFF.enabled, sendTextMessageMutation, setValue])
84
+
58
85
  useEffect(() => {
59
86
  if (messagesQuery.isError) {
60
87
  setWidgetLoading(false)
@@ -1,3 +1,5 @@
1
+ import { useDecision } from '@optimizely/react-sdk'
2
+
1
3
  import { render, screen } from '@/src/config/tests'
2
4
  import { useSendTextMessage } from '@/src/modules/messages/hooks'
3
5
 
@@ -12,13 +14,17 @@ vi.mock('@/src/modules/sparkie/hooks/use-init-sparkie', () => ({
12
14
  useInitSparkie: vi.fn(() => true)
13
15
  }))
14
16
 
17
+ vi.mock('@optimizely/react-sdk')
18
+
15
19
  describe('WidgetStarterPage', () => {
16
20
  const useSendTextMessageMock = { mutate: vi.fn() }
21
+ const useDecisionMock = [{ enabled: true, variables: { show_quick_actions: true } }]
17
22
 
18
23
  const renderComponent = () => render(<WidgetStarterPage />)
19
24
 
20
25
  beforeEach(() => {
21
26
  vi.mocked(useSendTextMessage).mockReturnValue(useSendTextMessageMock as never)
27
+ vi.mocked(useDecision).mockReturnValue(useDecisionMock as never)
22
28
  })
23
29
 
24
30
  it('should render without errors', () => {
@@ -1,4 +1,5 @@
1
- import { useEffect, useMemo, useRef } from 'react'
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react'
2
+ import { useDecision } from '@optimizely/react-sdk'
2
3
  import { useQueryClient } from '@tanstack/react-query'
3
4
  import { useTranslation } from 'react-i18next'
4
5
 
@@ -8,7 +9,9 @@ import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/
8
9
  import { useMessagesMaxCount } from '@/src/modules/messages/store'
9
10
  import { useGetProfile } from '@/src/modules/profile'
10
11
  import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
12
+ import { TutorWidgetEvents } from '../../events'
11
13
  import { useWidgetLoadingAtomValue, useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
14
+ import { testQuestionRegex } from '../../utils'
12
15
  import { GreetingsCard } from '../greetings-card'
13
16
  import { WidgetHeader } from '../header'
14
17
  import { PageLayout } from '../page-layout'
@@ -16,7 +19,7 @@ import { QuickActionButtons } from '../quick-action-buttons'
16
19
 
17
20
  function WidgetStarterPage() {
18
21
  const { t } = useTranslation()
19
- const [settings] = useWidgetSettingsAtom()
22
+ const [settings, setWidgetSettings] = useWidgetSettingsAtom()
20
23
  const chatInputRef = useRef<HTMLTextAreaElement>(null)
21
24
  const [chatInputValue, setChatInputValue] = useChatInputValueAtom()
22
25
  const [, setWidgetTabs] = useWidgetTabsAtom()
@@ -29,6 +32,9 @@ function WidgetStarterPage() {
29
32
  const isSparkieReady = useInitSparkie()
30
33
  const isMobile = useMediaQuery({ maxSize: 'md' })
31
34
  const widgetLoading = useWidgetLoadingAtomValue()
35
+ const hasSentInitialMessage = useRef(false)
36
+ const [newTutorWidgetFF] = useDecision('lex_new_tutor_widget')
37
+ const [lexTutorInitialMessageFF] = useDecision('lex_tutor_new_widget_initial_message')
32
38
 
33
39
  useRefEventListener<HTMLTextAreaElement>({
34
40
  config: {
@@ -41,17 +47,20 @@ function WidgetStarterPage() {
41
47
  }
42
48
  })
43
49
 
44
- const sendText = (textContent?: string | null) => {
45
- if (!textContent) return
50
+ const sendText = useCallback(
51
+ (textContent?: string | null) => {
52
+ if (!textContent) return
46
53
 
47
- sendTextMessageMutation.mutate(textContent, {
48
- onSuccess() {
49
- setWidgetTabs('chat')
50
- if (chatInputRef.current) chatInputRef.current.value = ''
51
- setChatInputValue('')
52
- }
53
- })
54
- }
54
+ sendTextMessageMutation.mutate(textContent, {
55
+ onSuccess() {
56
+ setWidgetTabs('chat')
57
+ if (chatInputRef.current) chatInputRef.current.value = ''
58
+ setChatInputValue('')
59
+ }
60
+ })
61
+ },
62
+ [sendTextMessageMutation, setChatInputValue, setWidgetTabs]
63
+ )
55
64
 
56
65
  const handleSend = () => {
57
66
  sendText(chatInputRef.current?.value)
@@ -77,6 +86,48 @@ function WidgetStarterPage() {
77
86
  void queryClient.prefetchInfiniteQuery(messagesQueryConfig)
78
87
  }, [conversationId, messagesQueryConfig, profileId, queryClient])
79
88
 
89
+ useEffect(() => {
90
+ if (!isSparkieReady || hasSentInitialMessage.current || !lexTutorInitialMessageFF.enabled)
91
+ return
92
+
93
+ const clear = TutorWidgetEvents['tutor-initial-message'].handler(({ message }) => {
94
+ if (!message) return
95
+
96
+ setChatInputValue(testQuestionRegex(message))
97
+ sendText(message)
98
+ hasSentInitialMessage.current = true
99
+ })
100
+
101
+ return () => {
102
+ clear?.()
103
+ hasSentInitialMessage.current = false
104
+ }
105
+ }, [isSparkieReady, lexTutorInitialMessageFF.enabled, sendText, setChatInputValue])
106
+
107
+ useEffect(() => {
108
+ if (!isSparkieReady || hasSentInitialMessage.current || !lexTutorInitialMessageFF.enabled)
109
+ return
110
+
111
+ const initialMessage = settings?.initialMessage
112
+
113
+ if (initialMessage) {
114
+ setChatInputValue(testQuestionRegex(initialMessage))
115
+ sendText(initialMessage)
116
+ setWidgetSettings({
117
+ ...settings,
118
+ initialMessage: ''
119
+ })
120
+ hasSentInitialMessage.current = true
121
+ }
122
+ }, [
123
+ settings,
124
+ isSparkieReady,
125
+ sendText,
126
+ setChatInputValue,
127
+ lexTutorInitialMessageFF.enabled,
128
+ setWidgetSettings
129
+ ])
130
+
80
131
  return (
81
132
  <PageLayout
82
133
  asideChild={
@@ -84,7 +135,8 @@ function WidgetStarterPage() {
84
135
  name='new-chat-msg-input'
85
136
  ref={chatInputRef}
86
137
  onSend={handleSend}
87
- buttonDisabled={widgetLoading || !chatInputValue.trim() || !isSparkieReady}
138
+ buttonDisabled={widgetLoading || !chatInputValue.trim()}
139
+ loading={!isSparkieReady}
88
140
  />
89
141
  }>
90
142
  <div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
@@ -103,12 +155,14 @@ function WidgetStarterPage() {
103
155
  />
104
156
  </div>
105
157
  </div>
106
- <QuickActionButtons
107
- 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'
108
- isDarkTheme={isDarkTheme}
109
- send={sendText}
110
- loading={!isSparkieReady}
111
- />
158
+ {newTutorWidgetFF?.enabled && newTutorWidgetFF?.variables?.['show_quick_actions'] ? (
159
+ <QuickActionButtons
160
+ 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'
161
+ isDarkTheme={isDarkTheme}
162
+ send={sendText}
163
+ loading={!isSparkieReady}
164
+ />
165
+ ) : null}
112
166
  </div>
113
167
  </PageLayout>
114
168
  )
@@ -8,7 +8,8 @@ export const TutorWidgetEventTypes = {
8
8
  HIDE: 'c3po-app-widget-hide',
9
9
  LOADED: 'tutor-app-widget-loaded',
10
10
  THEME_CHANGE: 'c3po-app-widget-theme-change',
11
- EXPAND: 'tutor-app-widget-expand'
11
+ EXPAND: 'tutor-app-widget-expand',
12
+ INITIAL_MESSAGE: 'tutor-initial-message'
12
13
  } as const
13
14
 
14
15
  export const TutorWidgetEvents = {
@@ -112,7 +113,26 @@ export const TutorWidgetEvents = {
112
113
  dispatch: (payload) => {
113
114
  window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.EXPAND, payload))
114
115
  }
115
- } as ITutorWidgetEvent<void>
116
+ } as ITutorWidgetEvent<void>,
117
+
118
+ [TutorWidgetEventTypes.INITIAL_MESSAGE]: {
119
+ name: TutorWidgetEventTypes.INITIAL_MESSAGE,
120
+ handler: (callback: (payload: { message: string }) => void) => {
121
+ const listener: EventListener = (e) => {
122
+ const evt = e as CustomEvent<{ message: string }>
123
+ callback(evt.detail)
124
+ }
125
+
126
+ window.addEventListener(TutorWidgetEventTypes.INITIAL_MESSAGE, listener)
127
+
128
+ return () => {
129
+ window.removeEventListener(TutorWidgetEventTypes.INITIAL_MESSAGE, listener)
130
+ }
131
+ },
132
+ dispatch: (payload: { detail: { message: string } }) => {
133
+ window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.INITIAL_MESSAGE, payload))
134
+ }
135
+ } as ITutorWidgetEvent<{ message: string }>
116
136
  } as const
117
137
 
118
138
  export const ACTION_EVENTS = {
@@ -0,0 +1 @@
1
+ export * from './test-question-regex'
@@ -0,0 +1 @@
1
+ export { default as testQuestionRegex } from './test-question-regex'
@@ -0,0 +1,7 @@
1
+ function testQuestionRegex(message: string) {
2
+ const questionRegex = /^question::\s*/i
3
+
4
+ return questionRegex.test(message) ? message.replace(questionRegex, '').trim() : message
5
+ }
6
+
7
+ export default testQuestionRegex
package/src/types.ts CHANGED
@@ -49,6 +49,7 @@ export type WidgetSettingProps = {
49
49
  current_media_codes?: string
50
50
  productType?: string
51
51
  classType?: ClassTypes
52
+ initialMessage?: string
52
53
  config?: {
53
54
  theme?: Theme
54
55
  metadata?: {