app-tutor-ai-consumer 1.24.2 → 1.25.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,19 @@
1
+ # [1.25.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.24.3...v1.25.0) (2025-08-12)
2
+
3
+ ### Bug Fixes
4
+
5
+ - cr issues ([ec25121](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/ec25121f4271f305c4b288586fcba125fa4f77e6))
6
+
7
+ ### Features
8
+
9
+ - add Tutor voice messages support ([3024d7c](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/3024d7c74bb8e07593f28d4b5b554f0d5d6495d1))
10
+
11
+ ## [1.24.3](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.24.2...v1.24.3) (2025-08-12)
12
+
13
+ ### Bug Fixes
14
+
15
+ - qa issues ([32bf6f9](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/32bf6f951b4b769a2ee2a90a566ebbcc0aded6c6))
16
+
1
17
  ## [1.24.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.24.1...v1.24.2) (2025-08-11)
2
18
 
3
19
  ## [1.24.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.24.0...v1.24.1) (2025-08-11)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.24.2",
3
+ "version": "1.25.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -24,3 +24,4 @@ export type ValidIconNames =
24
24
  | 'sparkle-tutor'
25
25
  | 'stop'
26
26
  | 'warning'
27
+ | 'waveforms'
@@ -0,0 +1,6 @@
1
+ <svg aria-hidden="true" focusable="false"
2
+ role="img" xmlns="http://www.w3.org/2000/svg"
3
+ viewBox="0 0 640 512">
4
+ <path fill="currentColor"
5
+ d="M328 0h-16a16 16 0 0 0-16 16v480a16 16 0 0 0 16 16h16a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16zm-96 96h-16a16 16 0 0 0-16 16v288a16 16 0 0 0 16 16h16a16 16 0 0 0 16-16V112a16 16 0 0 0-16-16zm192 32h-16a16 16 0 0 0-16 16v224a16 16 0 0 0 16 16h16a16 16 0 0 0 16-16V144a16 16 0 0 0-16-16zm96-64h-16a16 16 0 0 0-16 16v352a16 16 0 0 0 16 16h16a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zM136 192h-16a16 16 0 0 0-16 16v96a16 16 0 0 0 16 16h16a16 16 0 0 0 16-16v-96a16 16 0 0 0-16-16zm-96 32H24a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h16a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm576 0h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h16a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"></path>
6
+ </svg>
@@ -9,6 +9,7 @@ function useMediaQuery({ maxSize }: { maxSize: keyof typeof SCREEN_SIZES }) {
9
9
  const mediaquery = window.matchMedia(`(max-width: ${SCREEN_SIZES[maxSize]}px)`)
10
10
  const listener = () => setMatches(mediaquery.matches)
11
11
 
12
+ listener()
12
13
  mediaquery.addEventListener('change', listener)
13
14
 
14
15
  return () => mediaquery.removeEventListener('change', listener)
@@ -4,4 +4,6 @@ export * from './extract-text-from-react-nodes'
4
4
  export { default as HttpCodes } from './http-codes'
5
5
  export * from './languages'
6
6
  export * from './message-types'
7
+ export * from './toast'
8
+ export { default as ToastUtils } from './toast'
7
9
  export { default as URLutils } from './urls'
@@ -0,0 +1,37 @@
1
+ export const ToastPosition = {
2
+ TOP_RIGHT: 'top right',
3
+ TOP_LEFT: 'top left',
4
+ BOTTOM_LEFT: 'bottom left',
5
+ BOTTOM_RIGHT: 'bottom right'
6
+ } as const
7
+
8
+ export const Toast = {
9
+ SUCCESS: 'success',
10
+ DANGER: 'danger',
11
+ WARNING: 'warning',
12
+ INFO: 'info'
13
+ } as const
14
+
15
+ export type DispatchToastParams = {
16
+ message: string
17
+ type?: (typeof Toast)[keyof typeof Toast]
18
+ position?: (typeof ToastPosition)[keyof typeof ToastPosition]
19
+ }
20
+
21
+ export const TOAST_EVENT_NAME = 'toasted'
22
+
23
+ class ToastUtils {
24
+ dispatch({
25
+ message,
26
+ type = Toast.SUCCESS,
27
+ position = ToastPosition.TOP_RIGHT
28
+ }: DispatchToastParams) {
29
+ window.dispatchEvent(
30
+ new CustomEvent(TOAST_EVENT_NAME, {
31
+ detail: { message, type, position }
32
+ })
33
+ )
34
+ }
35
+ }
36
+
37
+ export default new ToastUtils()
@@ -7,6 +7,7 @@ import { DataHubService } from '@/src/config/datahub'
7
7
  import type { ButtonReactionsType } from '@/src/config/datahub/schemas/tutor'
8
8
  import { ButtonReactions, ClickHotmartTutor } from '@/src/config/datahub/schemas/tutor'
9
9
  import { Button, Icon } from '@/src/lib/components'
10
+ import { ToastUtils } from '@/src/lib/utils'
10
11
  import type { ParsedMessage } from '../../types'
11
12
 
12
13
  export type MessageActionsProps = {
@@ -28,9 +29,14 @@ function MessageActions({ message, className, showActions = false }: MessageActi
28
29
 
29
30
  navigator.clipboard
30
31
  .writeText(message.text)
31
- .then(() => setCopied(true))
32
+ .then(() => {
33
+ setCopied(true)
34
+ ToastUtils.dispatch({ message: t('general.buttons.copied') })
35
+ })
32
36
  .catch((err) => {
33
- console.error('Failed to copy text: ', err)
37
+ const errorMessage = t('generic_error.title')
38
+ console.error(`${errorMessage}: `, err)
39
+ ToastUtils.dispatch({ message: errorMessage })
34
40
  })
35
41
  .finally(() => {
36
42
  setTimeout(() => setCopying(false), 1000)
@@ -0,0 +1,2 @@
1
+ export * from './message-content-type-renderer'
2
+ export { default as MessageContentTypeRenderer } from './message-content-type-renderer'
@@ -0,0 +1,47 @@
1
+ import { useTranslation } from 'react-i18next'
2
+ import type { Components } from 'react-markdown'
3
+
4
+ import { Icon, MarkdownRenderer } from '@/src/lib/components'
5
+ import type { ParsedMessage } from '../../types'
6
+ import { MessageImg } from '../message-img'
7
+
8
+ export const imgComponent: Components['img'] = ({ src }) => {
9
+ return <MessageImg message={{ thumbnails: {}, url: src, dimensions: {} } as ParsedMessage} />
10
+ }
11
+
12
+ export function MediaVoiceMessage() {
13
+ const { t } = useTranslation()
14
+ return (
15
+ <div className='flex items-center gap-2 py-3 text-xs/normal'>
16
+ <Icon name='waveforms' className='inline-flex h-4 w-4 shrink-0' />
17
+ <span>{t('chat_page.messages.audio')}</span>
18
+ </div>
19
+ )
20
+ }
21
+
22
+ export function TextMessage({ message }: { message: ParsedMessage }) {
23
+ return (
24
+ <MarkdownRenderer
25
+ content={message?.text ?? message?.name}
26
+ imgComponent={imgComponent}
27
+ className='w-full'
28
+ />
29
+ )
30
+ }
31
+
32
+ function MessageContentTypeRenderer({ message }: { message: ParsedMessage }) {
33
+ const messageFromAi = message.metadata.author === 'ai'
34
+ const messageType = message.type
35
+ const isMediaVoice = messageType === 'media/voice'
36
+
37
+ if (messageFromAi && isMediaVoice) return null
38
+
39
+ switch (message.type) {
40
+ case 'media/voice':
41
+ return <MediaVoiceMessage />
42
+ default:
43
+ return <TextMessage message={message} />
44
+ }
45
+ }
46
+
47
+ export default MessageContentTypeRenderer
@@ -1,18 +1,13 @@
1
1
  import clsx from 'clsx'
2
- import type { Components } from 'react-markdown'
3
2
 
4
- import { MarkdownRenderer } from '@/src/lib/components'
5
3
  import type { ParsedMessage } from '../../types'
6
4
  import { MessageActions } from '../message-actions'
7
- import { MessageImg } from '../message-img'
8
-
9
- const imgComponent: Components['img'] = ({ src }) => {
10
- return <MessageImg message={{ thumbnails: {}, url: src, dimensions: {} } as ParsedMessage} />
11
- }
5
+ import { MessageContentTypeRenderer } from '../message-content-type-renderer'
12
6
 
13
7
  function MessageItem({ message }: { message: ParsedMessage }) {
14
8
  const messageFromUser = message.metadata.author === 'user'
15
9
  const messageFromAi = message.metadata.author === 'ai'
10
+ const isMediaVoice = message.type === 'media/voice'
16
11
 
17
12
  return (
18
13
  <div
@@ -28,19 +23,17 @@ function MessageItem({ message }: { message: ParsedMessage }) {
28
23
  'max-w-max bg-[rgb(from_var(--hc-color-neutral-300)_r_g_b_/_0.8)]': messageFromUser,
29
24
  'bg-neutral-200': messageFromAi
30
25
  })}>
31
- <MarkdownRenderer
32
- content={message?.text ?? message?.name}
33
- imgComponent={imgComponent}
34
- className='w-full'
35
- />
26
+ <MessageContentTypeRenderer message={message} />
36
27
  </div>
37
- <MessageActions
38
- className={clsx('flex items-center justify-between gap-2', {
39
- 'w-full': messageFromAi
40
- })}
41
- message={message}
42
- showActions={messageFromAi}
43
- />
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
+ )}
44
37
  </div>
45
38
  )
46
39
  }
@@ -66,7 +66,7 @@ const MessagesContainer = forwardRef<HTMLDivElement, MessagesContainerProps>(
66
66
  className='flex h-full flex-col gap-2 overflow-auto max-md:p-[1.125rem] md:p-5'>
67
67
  <div className='mb-auto flex-1 self-center'>
68
68
  <Button
69
- className='max-w-max rounded-full border border-neutral-300 bg-neutral-200 px-2 py-1 text-xs/normal tracking-wide text-neutral-900'
69
+ className='max-w-max rounded-full border border-neutral-300 bg-neutral-200 px-2 py-1 text-xs/normal tracking-wide text-neutral-900 hover:text-neutral-900 focus:text-neutral-900 active:text-neutral-900'
70
70
  onClick={handleClickShowMore}
71
71
  loading={loading}
72
72
  show={showButton}>
@@ -25,7 +25,7 @@ function ChatPage() {
25
25
  const sendTextMessageMutation = useSendTextMessage()
26
26
  const limit = useMessagesMaxCount()
27
27
  const [value, setValue] = useChatInputValueAtom()
28
- const [, setWidgetLoading] = useWidgetLoadingAtom()
28
+ const [widgetLoading, setWidgetLoading] = useWidgetLoadingAtom()
29
29
  const isMobile = useMediaQuery({ maxSize: 'md' })
30
30
 
31
31
  const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
@@ -70,7 +70,7 @@ function ChatPage() {
70
70
  onSend={widgetTabs.currentTab === 'chat' ? handleSendMessage : undefined}
71
71
  loading={sendTextMessageMutation.isPending}
72
72
  inputDisabled={messagesQuery?.isLoading}
73
- buttonDisabled={messagesQuery?.isLoading || !value.trim()}
73
+ buttonDisabled={widgetLoading || messagesQuery?.isLoading || !value.trim()}
74
74
  />
75
75
  }>
76
76
  <div className='max-md:px-[1.125rem] max-md:pt-[1.125rem] md:px-5 md:pt-5'>
@@ -8,7 +8,7 @@ import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/
8
8
  import { useMessagesMaxCount } from '@/src/modules/messages/store'
9
9
  import { useGetProfile } from '@/src/modules/profile'
10
10
  import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
11
- import { useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
11
+ import { useWidgetLoadingAtomValue, useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
12
12
  import { GreetingsCard } from '../greetings-card'
13
13
  import { WidgetHeader } from '../header'
14
14
  import { PageLayout } from '../page-layout'
@@ -28,6 +28,7 @@ function WidgetStarterPage() {
28
28
  const isDarkTheme = settings?.config?.theme === 'dark'
29
29
  const isSparkieReady = useInitSparkie()
30
30
  const isMobile = useMediaQuery({ maxSize: 'md' })
31
+ const widgetLoading = useWidgetLoadingAtomValue()
31
32
 
32
33
  useRefEventListener<HTMLTextAreaElement>({
33
34
  config: {
@@ -83,7 +84,7 @@ function WidgetStarterPage() {
83
84
  name='new-chat-msg-input'
84
85
  ref={chatInputRef}
85
86
  onSend={handleSend}
86
- buttonDisabled={!chatInputValue.trim() || !isSparkieReady}
87
+ buttonDisabled={widgetLoading || !chatInputValue.trim() || !isSparkieReady}
87
88
  />
88
89
  }>
89
90
  <div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>