app-tutor-ai-consumer 1.22.2 → 1.22.3

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,5 @@
1
+ ## [1.22.3](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.22.2...v1.22.3) (2025-08-08)
2
+
1
3
  ## [1.22.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.22.1...v1.22.2) (2025-08-07)
2
4
 
3
5
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.22.2",
3
+ "version": "1.22.3",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -0,0 +1,5 @@
1
+ <svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path
3
+ d="M11.3016 1.60522H8.62451V0.112061H13.8506V5.33813H12.3574V2.66105L9.15242 5.86604L8.0966 4.81022L11.3016 1.60522ZM0.412109 8.32446H1.90527V11.0016L5.11027 7.79655L6.16609 8.85237L2.9611 12.0574H5.63818V13.5505H0.412109V8.32446Z"
4
+ fill="currentColor" />
5
+ </svg>
@@ -12,6 +12,7 @@ export type ValidIconNames =
12
12
  | 'copy-solid'
13
13
  | 'copy'
14
14
  | 'double-check'
15
+ | 'expand'
15
16
  | 'gallery'
16
17
  | 'info'
17
18
  | 'interrogation'
@@ -22,7 +22,7 @@
22
22
 
23
23
  .right:before {
24
24
  top: 50%;
25
- right: 97%;
25
+ right: 96%;
26
26
  transform: translateY(-50%) rotateZ(-90deg);
27
27
  }
28
28
 
@@ -34,6 +34,6 @@
34
34
 
35
35
  .left:before {
36
36
  top: 50%;
37
- left: 97%;
37
+ left: 96%;
38
38
  transform: translateY(-50%) rotateZ(90deg);
39
39
  }
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useMemo, useRef } from 'react'
2
2
  import { useInfiniteQuery } from '@tanstack/react-query'
3
3
 
4
+ import { useMediaQuery } from '@/src/lib/hooks'
4
5
  import { isTextEmpty } from '@/src/lib/utils/is-text-empty'
5
6
  import { ChatInput, MessagesList, useChatInputValueAtom } from '@/src/modules/messages/components'
6
7
  import { MessagesContainer } from '@/src/modules/messages/components/messages-container'
@@ -25,6 +26,7 @@ function ChatPage() {
25
26
  const limit = useMessagesMaxCount()
26
27
  const [value, setValue] = useChatInputValueAtom()
27
28
  const [, setWidgetLoading] = useWidgetLoadingAtom()
29
+ const isMobile = useMediaQuery({ maxSize: 'md' })
28
30
 
29
31
  const conversationId = useMemo(() => settings?.conversationId, [settings?.conversationId])
30
32
  const profileId = useMemo(() => profileQuery.data?.id, [profileQuery.data?.id])
@@ -72,7 +74,11 @@ function ChatPage() {
72
74
  />
73
75
  }>
74
76
  <div className='max-md:px-[1.125rem] max-md:pt-[1.125rem] md:px-5 md:pt-5'>
75
- <WidgetHeader enabledButtons={['info', 'close']} tutorName={settings?.tutorName} />
77
+ <WidgetHeader
78
+ enabledButtons={['info', 'close', 'arrow-left']}
79
+ tutorName={settings?.tutorName}
80
+ showContentWithoutMeta={!isMobile}
81
+ />
76
82
  </div>
77
83
  <MessagesContainer
78
84
  ref={scrollerRef}
@@ -1,2 +1 @@
1
- export * from './header'
2
- export { default as WidgetHeader } from './header'
1
+ export { default as WidgetHeader } from './widget-header'
@@ -2,7 +2,7 @@ import { render, screen } from '@/src/config/tests'
2
2
  import * as Hooks from '@/src/lib/hooks'
3
3
 
4
4
  import WidgetHeaderPropsBuilder from './__tests__/widget-header-props.builder'
5
- import WidgetHeader from './header'
5
+ import WidgetHeader from './widget-header'
6
6
 
7
7
  describe('<WidgetHeader />', () => {
8
8
  const defaultProps = new WidgetHeaderPropsBuilder()
@@ -29,10 +29,10 @@ describe('<WidgetHeader />', () => {
29
29
  expect(screen.queryByRole('button', { name: /Arrow Left Icon/i })).not.toBeInTheDocument()
30
30
  })
31
31
 
32
- it('should render WidgetHeaderContentWithoutMeta when prop showContentWithoutMeta is true and showContent is false', () => {
32
+ it('should render WidgetHeaderContentWithoutMeta when prop showContentWithoutMeta is true', () => {
33
33
  const props = new WidgetHeaderPropsBuilder()
34
+ .withEnabledButtons(['close', 'arrow-left'])
34
35
  .withShowContentWithoutMeta(true)
35
- .withShowContent(false)
36
36
 
37
37
  renderComponent(props)
38
38
 
@@ -0,0 +1,148 @@
1
+ import { useMemo } from 'react'
2
+ import clsx from 'clsx'
3
+ import { useTranslation } from 'react-i18next'
4
+
5
+ import { DataHubService } from '@/src/config/datahub'
6
+ import { ClickTutorMinimizeSchema } from '@/src/config/datahub/schemas/tutor'
7
+ import { Button, Icon, Tooltip } from '@/src/lib/components'
8
+ import { useMediaQuery } from '@/src/lib/hooks'
9
+ import { TutorWidgetEvents } from '../../events'
10
+ import { useWidgetGoBackTabAtom, useWidgetTabsAtom } from '../../store'
11
+ import { AIAvatar } from '../ai-avatar'
12
+
13
+ import type { WidgetHeaderProps } from './types'
14
+
15
+ import styles from './styles.module.css'
16
+
17
+ function WidgetHeaderActions({
18
+ actionButtons
19
+ }: {
20
+ actionButtons: ('archive' | 'expand' | 'info')[]
21
+ }) {
22
+ const { t } = useTranslation()
23
+ const [, setTab] = useWidgetTabsAtom()
24
+ const isMobile = useMediaQuery({ maxSize: 'md' })
25
+
26
+ const handleClickArchive = () => {
27
+ setTab('chat')
28
+ }
29
+
30
+ const handleClickInfo = () => {
31
+ setTab('information')
32
+ }
33
+
34
+ const handleClickExpand = () => {
35
+ TutorWidgetEvents['tutor-app-widget-expand'].dispatch()
36
+ }
37
+
38
+ if (!(Number(actionButtons?.length) > 0)) return null
39
+
40
+ return (
41
+ <>
42
+ <div
43
+ className={clsx('flex max-w-max items-center gap-3 text-neutral-700', styles.btnContainer)}>
44
+ {actionButtons.includes('archive') && (
45
+ <Tooltip show={!isMobile} content={t('general.buttons.archive')}>
46
+ <Button
47
+ show={actionButtons.includes('archive')}
48
+ onClick={handleClickArchive}
49
+ aria-label={t('general.buttons.archive') + ' Icon'}>
50
+ <Icon name='archive' className='h-4 w-4' aria-hidden />
51
+ </Button>
52
+ </Tooltip>
53
+ )}
54
+ {actionButtons.includes('info') && (
55
+ <Tooltip show={!isMobile} content={t('general.buttons.info')} position='left'>
56
+ <Button
57
+ show={actionButtons.includes('info')}
58
+ onClick={handleClickInfo}
59
+ aria-label={t('general.buttons.info') + ' Icon'}>
60
+ <Icon name='info' className='h-4 w-4' aria-hidden />
61
+ </Button>
62
+ </Tooltip>
63
+ )}
64
+ {actionButtons.includes('expand') && (
65
+ <Tooltip show={!isMobile} content={t('general.buttons.expand')} position='left'>
66
+ <Button
67
+ show={actionButtons.includes('expand')}
68
+ onClick={handleClickExpand}
69
+ aria-label={t('general.buttons.expand') + ' Icon'}>
70
+ <Icon name='expand' className='h-3.5 w-3.5' aria-hidden />
71
+ </Button>
72
+ </Tooltip>
73
+ )}
74
+ </div>
75
+ </>
76
+ )
77
+ }
78
+
79
+ function WidgetHeader({
80
+ enabledButtons = [],
81
+ tutorName,
82
+ showContentWithoutMeta,
83
+ showContent = true
84
+ }: WidgetHeaderProps) {
85
+ const { t } = useTranslation()
86
+ const [, goBack] = useWidgetGoBackTabAtom()
87
+ const name = tutorName ?? t('general.name')
88
+
89
+ const handleHideWidget = () => {
90
+ TutorWidgetEvents['c3po-app-widget-hide'].dispatch()
91
+ DataHubService.sendEvent({ schema: new ClickTutorMinimizeSchema() })
92
+ }
93
+
94
+ const actionButtons = useMemo(
95
+ () => enabledButtons.filter((btn) => btn === 'archive' || btn === 'info' || btn === 'expand'),
96
+ [enabledButtons]
97
+ )
98
+
99
+ const hasToShowActions = useMemo(() => Number(actionButtons?.length) > 0, [actionButtons?.length])
100
+
101
+ return (
102
+ <div className='mb-4 mt-0.5 flex flex-col gap-2 text-neutral-900'>
103
+ <div className='flex justify-end'>
104
+ <div className={styles.closeContainer}>
105
+ <Button
106
+ className='text-neutral-500'
107
+ show={enabledButtons.includes('close')}
108
+ onClick={handleHideWidget}
109
+ aria-label='Close Icon'>
110
+ <Icon name='close' className='h-3 w-3' aria-hidden />
111
+ </Button>
112
+ </div>
113
+ </div>
114
+ <div
115
+ className={clsx('grid items-center max-md:gap-2 md:gap-4', styles.btnContainer, {
116
+ 'grid-cols-[auto_1fr_auto]': enabledButtons.includes('arrow-left'),
117
+ 'grid-cols-[1fr_auto]': !enabledButtons.includes('arrow-left') && hasToShowActions,
118
+ 'grid-cols-[1fr]': !enabledButtons.includes('arrow-left') && !hasToShowActions
119
+ })}>
120
+ {enabledButtons.includes('arrow-left') && (
121
+ <div>
122
+ <Button aria-label='Arrow Left Icon' onClick={goBack}>
123
+ <Icon name='arrow-left' className='h-3.5 w-3.5' aria-hidden />
124
+ </Button>
125
+ </div>
126
+ )}
127
+ <div
128
+ className={clsx('flex w-full items-center gap-2', {
129
+ 'justify-center': showContentWithoutMeta
130
+ })}>
131
+ {showContent && (
132
+ <>
133
+ {!showContentWithoutMeta && <AIAvatar />}
134
+ {name && <h4 className='text-sm/loose font-bold'>{name}</h4>}
135
+ </>
136
+ )}
137
+ </div>
138
+ {hasToShowActions ? (
139
+ <WidgetHeaderActions actionButtons={actionButtons} />
140
+ ) : (
141
+ <div className='h-6 w-6' />
142
+ )}
143
+ </div>
144
+ </div>
145
+ )
146
+ }
147
+
148
+ export default WidgetHeader
@@ -16,40 +16,39 @@ function WidgetInformationPage() {
16
16
  const tutorName = settings?.tutorName ?? t('general.name')
17
17
 
18
18
  return (
19
- <PageLayout className='flex min-h-0 flex-col text-neutral-900 max-md:p-[1.125rem] md:p-5'>
20
- <div className='mb-4'>
19
+ <PageLayout className='flex min-h-0 flex-col text-neutral-900'>
20
+ <div className='max-md:px-[1.125rem] max-md:py-[1.125rem] md:px-5 md:py-5'>
21
21
  <WidgetHeader
22
- enabledButtons={['close']}
23
- showContent={false}
22
+ enabledButtons={['close', 'arrow-left']}
24
23
  showContentWithoutMeta
25
24
  tutorName={t('info.title')}
26
25
  />
27
- </div>
28
26
 
29
- <div className='my-8 flex justify-center'>
30
- <div className='flex flex-col items-center gap-2'>
31
- <AIAvatar size='lg' />
32
-
33
- <h3
34
- className={clsx('font-bold', {
35
- 'text-white': isDarkMode,
36
- 'text-neutral-700': !isDarkMode
37
- })}>
38
- {tutorName}
39
- </h3>
27
+ <div className='my-8 flex justify-center'>
28
+ <div className='flex flex-col items-center gap-2'>
29
+ <AIAvatar size='lg' />
30
+
31
+ <h3
32
+ className={clsx('font-bold', {
33
+ 'text-white': isDarkMode,
34
+ 'text-neutral-700': !isDarkMode
35
+ })}>
36
+ {tutorName}
37
+ </h3>
38
+ </div>
40
39
  </div>
41
- </div>
42
40
 
43
- <div className='flex flex-col gap-5'>
44
- {infoItems({ tutorName: t('general.name') }).map((item) => (
45
- <InformationCard
46
- key={item.titleKey}
47
- icon={item.icon}
48
- title={t(item.titleKey)}
49
- description={t(item.descKey)}
50
- isDarkMode={isDarkMode}
51
- />
52
- ))}
41
+ <div className='flex flex-col gap-5'>
42
+ {infoItems({ tutorName: t('general.name') }).map((item) => (
43
+ <InformationCard
44
+ key={item.titleKey}
45
+ icon={item.icon}
46
+ title={t(item.titleKey)}
47
+ description={t(item.descKey)}
48
+ isDarkMode={isDarkMode}
49
+ />
50
+ ))}
51
+ </div>
53
52
  </div>
54
53
  </PageLayout>
55
54
  )
@@ -4,7 +4,7 @@ import type { CSSProperties, MouseEventHandler } from 'react'
4
4
  import { useTranslation } from 'react-i18next'
5
5
 
6
6
  import { Button, HorizontalDraggableScroll } from '@/src/lib/components'
7
- import { useRefEventListener } from '@/src/lib/hooks'
7
+ import { useMediaQuery, useRefEventListener } from '@/src/lib/hooks'
8
8
  import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/components'
9
9
  import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/hooks'
10
10
  import { useMessagesMaxCount } from '@/src/modules/messages/store'
@@ -28,6 +28,7 @@ function WidgetStarterPage() {
28
28
  const name = settings?.tutorName ?? t('general.name')
29
29
  const isDarkTheme = settings?.config?.theme === 'dark'
30
30
  const isSparkieReady = useInitSparkie()
31
+ const isMobile = useMediaQuery({ maxSize: 'md' })
31
32
 
32
33
  useRefEventListener<HTMLTextAreaElement>({
33
34
  config: {
@@ -104,7 +105,7 @@ function WidgetStarterPage() {
104
105
  <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'>
105
106
  <WidgetHeader
106
107
  enabledButtons={isSparkieReady ? ['close', 'archive', 'info'] : ['close', 'info']}
107
- showContent={false}
108
+ showContent={isMobile}
108
109
  tutorName={name}
109
110
  />
110
111
 
@@ -7,7 +7,8 @@ export const TutorWidgetEventTypes = {
7
7
  CLOSE: 'c3po-app-widget-close',
8
8
  HIDE: 'c3po-app-widget-hide',
9
9
  LOADED: 'tutor-app-widget-loaded',
10
- THEME_CHANGE: 'c3po-app-widget-theme-change'
10
+ THEME_CHANGE: 'c3po-app-widget-theme-change',
11
+ EXPAND: 'tutor-app-widget-expand'
11
12
  } as const
12
13
 
13
14
  export const TutorWidgetEvents = {
@@ -94,7 +95,24 @@ export const TutorWidgetEvents = {
94
95
  dispatch: (payload) => {
95
96
  window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.THEME_CHANGE, payload))
96
97
  }
97
- } as ITutorWidgetEvent<{ theme: Theme }>
98
+ } as ITutorWidgetEvent<{ theme: Theme }>,
99
+
100
+ [TutorWidgetEventTypes.EXPAND]: {
101
+ name: TutorWidgetEventTypes.EXPAND,
102
+ handler: (callback: () => void) => {
103
+ const listener: EventListener = () => {
104
+ callback()
105
+ }
106
+ window.addEventListener(TutorWidgetEventTypes.EXPAND, listener)
107
+
108
+ return () => {
109
+ window.removeEventListener(TutorWidgetEventTypes.EXPAND, listener)
110
+ }
111
+ },
112
+ dispatch: (payload) => {
113
+ window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.EXPAND, payload))
114
+ }
115
+ } as ITutorWidgetEvent<void>
98
116
  } as const
99
117
 
100
118
  export const ACTION_EVENTS = {
@@ -1,118 +0,0 @@
1
- import clsx from 'clsx'
2
- import { useTranslation } from 'react-i18next'
3
-
4
- import { DataHubService } from '@/src/config/datahub'
5
- import { ClickTutorMinimizeSchema } from '@/src/config/datahub/schemas/tutor'
6
- import { Button, Icon, Tooltip } from '@/src/lib/components'
7
- import { useMediaQuery } from '@/src/lib/hooks'
8
- import { TutorWidgetEvents } from '../../events'
9
- import { useWidgetGoBackTabAtom, useWidgetTabsAtom } from '../../store'
10
- import { AIAvatar } from '../ai-avatar'
11
-
12
- import type { WidgetHeaderContentProps, WidgetHeaderProps } from './types'
13
-
14
- import styles from './styles.module.css'
15
-
16
- export function WidgetHeaderContent({ tutorName }: WidgetHeaderContentProps) {
17
- return (
18
- <div className='flex w-full gap-2'>
19
- <AIAvatar />
20
- <div className='flex flex-col justify-center'>
21
- {tutorName && <h4 className='text-sm/loose font-bold'>{tutorName}</h4>}
22
- </div>
23
- </div>
24
- )
25
- }
26
-
27
- export function WidgetHeaderContentWithoutMeta({ name }: { name?: string }) {
28
- const { t } = useTranslation()
29
- const [, goBack] = useWidgetGoBackTabAtom()
30
- const tutorName = name ?? t('general.name')
31
-
32
- return (
33
- <div
34
- className={clsx(
35
- 'grid-areas-[a_b] grid grid-cols-[auto_1fr] items-center gap-1',
36
- styles.withoutMetaContainer
37
- )}>
38
- <Button className='grid-area-[a]' aria-label='Arrow Left Icon' onClick={goBack}>
39
- <Icon name='arrow-left' className='h-3.5 w-3.5' aria-hidden />
40
- </Button>
41
- <div className='grid-area-[b] flex min-h-6 justify-center text-center'>
42
- <span className='absolute bottom-0 left-1/2 -translate-x-1/2 text-base font-bold'>
43
- {tutorName}
44
- </span>
45
- </div>
46
- </div>
47
- )
48
- }
49
-
50
- function WidgetHeader({
51
- enabledButtons = [],
52
- tutorName,
53
- showContentWithoutMeta,
54
- showContent = true
55
- }: WidgetHeaderProps) {
56
- const { t } = useTranslation()
57
- const [, setTab] = useWidgetTabsAtom()
58
- const name = tutorName ?? t('general.name')
59
- const isMobile = useMediaQuery({ maxSize: 'md' })
60
-
61
- const handleClickArchive = () => {
62
- setTab('chat')
63
- }
64
-
65
- const handleClickInfo = () => {
66
- setTab('information')
67
- }
68
-
69
- const handleHideWidget = () => {
70
- TutorWidgetEvents['c3po-app-widget-hide'].dispatch()
71
- DataHubService.sendEvent({ schema: new ClickTutorMinimizeSchema() })
72
- }
73
-
74
- return (
75
- <div className='mt-0.5 flex flex-col gap-2 text-neutral-900'>
76
- <div className='flex justify-end'>
77
- <div className={styles.closeContainer}>
78
- <Button
79
- className='text-neutral-500'
80
- show={enabledButtons.includes('close')}
81
- onClick={handleHideWidget}
82
- aria-label='Close Icon'>
83
- <Icon name='close' className='h-3 w-3' aria-hidden />
84
- </Button>
85
- </div>
86
- </div>
87
- <div className='grid-areas-[a_b] grid grid-cols-[1fr_auto] items-center pb-4'>
88
- <div className='grid-area-[a] relative'>
89
- {showContent && !showContentWithoutMeta && <WidgetHeaderContent tutorName={name} />}
90
- {showContentWithoutMeta && !showContent && <WidgetHeaderContentWithoutMeta name={name} />}
91
- </div>
92
-
93
- <div className='grid-area-[b] ml-auto shrink-0'>
94
- <div className={clsx('flex max-w-max gap-3 text-neutral-700', styles.btnContainer)}>
95
- <Tooltip show={!isMobile} content={t('general.buttons.archive')}>
96
- <Button
97
- show={enabledButtons.includes('archive')}
98
- onClick={handleClickArchive}
99
- aria-label={t('general.buttons.archive') + ' Icon'}>
100
- <Icon name='archive' className='h-4 w-4' aria-hidden />
101
- </Button>
102
- </Tooltip>
103
- <Tooltip show={!isMobile} content={t('general.buttons.info')} position='left'>
104
- <Button
105
- show={enabledButtons.includes('info')}
106
- onClick={handleClickInfo}
107
- aria-label={t('general.buttons.info') + ' Icon'}>
108
- <Icon name='info' className='h-4 w-4' aria-hidden />
109
- </Button>
110
- </Tooltip>
111
- </div>
112
- </div>
113
- </div>
114
- </div>
115
- )
116
- }
117
-
118
- export default WidgetHeader