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 +8 -0
- package/package.json +1 -1
- package/src/development-bootstrap.tsx +2 -1
- package/src/lib/components/markdownrenderer/styles.module.css +3 -3
- package/src/modules/messages/components/message-item/message-item.tsx +9 -9
- package/src/modules/widget/components/avatar-animation/avatar-animation.tsx +1 -7
- package/src/modules/widget/components/chat-page/chat-page.spec.tsx +5 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +27 -0
- package/src/modules/widget/components/starter-page/starter-page.spec.tsx +6 -0
- package/src/modules/widget/components/starter-page/starter-page.tsx +73 -19
- package/src/modules/widget/events.ts +22 -2
- package/src/modules/widget/utils/index.ts +1 -0
- package/src/modules/widget/utils/test-question-regex/index.ts +1 -0
- package/src/modules/widget/utils/test-question-regex/test-question-regex.tsx +7 -0
- package/src/types.ts +1 -0
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
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 = (
|
|
45
|
-
|
|
50
|
+
const sendText = useCallback(
|
|
51
|
+
(textContent?: string | null) => {
|
|
52
|
+
if (!textContent) return
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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()
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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'
|