app-tutor-ai-consumer 1.22.1 → 1.22.2

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 (34) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/config/vitest/__mocks__/window.ts +11 -0
  3. package/config/vitest/vitest.config.mts +1 -0
  4. package/package.json +1 -1
  5. package/src/config/datahub/actions.ts +6 -0
  6. package/src/config/datahub/constants.ts +3 -6
  7. package/src/config/datahub/entities.ts +2 -1
  8. package/src/config/datahub/schemas/base-click-schema.ts +48 -0
  9. package/src/config/datahub/schemas/base-try-schema.ts +64 -0
  10. package/src/config/datahub/schemas/tutor/__tests__/click-hotmart-tutor.spec.ts +2 -1
  11. package/src/config/datahub/schemas/tutor/click-hotmart-tutor.ts +2 -1
  12. package/src/config/datahub/schemas/tutor/click-tutor-minimize.ts +24 -0
  13. package/src/config/datahub/schemas/tutor/index.ts +1 -0
  14. package/src/config/datahub/schemas/tutor/try-product-tutor.ts +24 -0
  15. package/src/config/datahub/types.ts +3 -1
  16. package/src/config/theme/constants.ts +7 -0
  17. package/src/lib/components/button/button.tsx +7 -8
  18. package/src/lib/components/errors/generic/generic-error.tsx +9 -25
  19. package/src/lib/components/icons/copy-solid.svg +5 -0
  20. package/src/lib/components/icons/icon-names.d.ts +2 -0
  21. package/src/lib/components/icons/like-solid.svg +5 -0
  22. package/src/lib/components/index.ts +1 -0
  23. package/src/lib/components/tooltip/index.ts +2 -0
  24. package/src/lib/components/tooltip/styles.module.css +39 -0
  25. package/src/lib/components/tooltip/tooltip.tsx +41 -0
  26. package/src/lib/hooks/index.ts +1 -0
  27. package/src/lib/hooks/use-media-query/index.ts +2 -0
  28. package/src/lib/hooks/use-media-query/use-media-query.tsx +20 -0
  29. package/src/main/main.tsx +1 -1
  30. package/src/modules/messages/components/chat-input/chat-input.tsx +2 -1
  31. package/src/modules/messages/components/message-actions/message-actions.tsx +12 -12
  32. package/src/modules/widget/components/header/header.spec.tsx +32 -4
  33. package/src/modules/widget/components/header/header.tsx +33 -21
  34. package/src/modules/widget/components/information-page/information-page.tsx +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.22.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.22.1...v1.22.2) (2025-08-07)
2
+
3
+ ### Bug Fixes
4
+
5
+ - header qa issues part 1 ([4910041](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/4910041905995e3b8161d015b261c82331131d20))
6
+ - qa issues part 2 ([65d5eb6](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/65d5eb64768f7496a1f767a6d2bd9e614abef547))
7
+
1
8
  ## [1.22.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.22.0...v1.22.1) (2025-08-06)
2
9
 
3
10
  ### Bug Fixes
@@ -0,0 +1,11 @@
1
+ Object.defineProperty(window, 'matchMedia', {
2
+ writable: true,
3
+ value: vi.fn().mockImplementation((query) => ({
4
+ matches: false,
5
+ media: query,
6
+ onchange: null,
7
+ addEventListener: vi.fn(),
8
+ removeEventListener: vi.fn(),
9
+ dispatchEvent: vi.fn()
10
+ }))
11
+ })
@@ -15,6 +15,7 @@ export default defineConfig({
15
15
  './config/vitest/__mocks__/icons.tsx',
16
16
  './config/vitest/__mocks__/animation-avatar.tsx',
17
17
  './config/vitest/__mocks__/intersection-observer.ts',
18
+ './config/vitest/__mocks__/window.ts',
18
19
  './config/vitest/polyfills/global.js'
19
20
  ],
20
21
  coverage: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.22.1",
3
+ "version": "1.22.2",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -8,3 +8,9 @@ export const DataHubActions = {
8
8
  } as const
9
9
 
10
10
  export type DataHubActionTypes = (typeof DataHubActions)[keyof typeof DataHubActions]
11
+
12
+ export const ActionNames = {
13
+ CLICK_HOTMART_TUTOR: `${DataHubActions.CLICK}_hotmart_tutor`,
14
+ CLICK_TUTOR_MINIMIZE: `${DataHubActions.CLICK}_tutor_minimize`,
15
+ TRY_PRODUCT_TUTOR: `${DataHubActions.TRY}_product_tutor`
16
+ } as const
@@ -6,10 +6,6 @@ export const System = {
6
6
  HOTMART_CLUB: 'hotmart_club'
7
7
  } as const
8
8
 
9
- export const ActionNames = {
10
- CLICK_HOTMART_TUTOR: 'click_hotmart_tutor'
11
- } as const
12
-
13
9
  export const ScreenNames = {
14
10
  NOT_APP_EVENT: 'NOT_APP_EVENT',
15
11
  HOME_CONSUMER: 'HOME_CONSUMER'
@@ -19,7 +15,7 @@ export const Platform = {
19
15
  WEB: 'WEB'
20
16
  } as const
21
17
 
22
- export const ResultType = {
18
+ export const Result = {
23
19
  SUCCESS: 'SUCCESSFUL',
24
20
  FAILURE: 'FAILURE',
25
21
  FAILURE_DESCRIPTION: 'NOT_FAILURE_RESULT_EVENT'
@@ -27,7 +23,8 @@ export const ResultType = {
27
23
 
28
24
  export const ComponentNames = {
29
25
  BUTTON_LIKE_ANSWER: 'BUTTON_LIKE_ANSWER',
30
- BUTTON_DISLIKE_ANSWER: 'BUTTON_DISLIKE_ANSWER'
26
+ BUTTON_DISLIKE_ANSWER: 'BUTTON_DISLIKE_ANSWER',
27
+ BUTTON_CLOSE_TUTOR_CHAT: 'BUTTON_CLOSE_TUTOR_CHAT'
31
28
  } as const
32
29
 
33
30
  export const ComponentSource = {
@@ -1,4 +1,5 @@
1
1
  export const DataHubEntities = {
2
2
  ADMIN: 'admin',
3
- HOME: 'home'
3
+ HOME: 'home',
4
+ PRODUCT_CONSUME: 'product_consume'
4
5
  } as const
@@ -0,0 +1,48 @@
1
+ import { ScreenNames } from '../constants'
2
+ import { DataHubEntities } from '../entities'
3
+ import type { ComponentNamesType, DataHubEntityTypes, ScreenNamesType } from '../types'
4
+
5
+ import BaseSchema from './base-schema'
6
+
7
+ export type BaseClickSchemaConstructorProps = {
8
+ componentName: ComponentNamesType
9
+ entity?: DataHubEntityTypes
10
+ isLogged?: boolean
11
+ screenName?: ScreenNamesType
12
+ }
13
+
14
+ abstract class BaseClickSchema extends BaseSchema {
15
+ protected componentName: ComponentNamesType
16
+ protected screenName: ScreenNamesType
17
+
18
+ entity: DataHubEntityTypes
19
+ isLogged: boolean
20
+
21
+ constructor(args: BaseClickSchemaConstructorProps) {
22
+ const {
23
+ componentName,
24
+ entity = DataHubEntities.HOME,
25
+ isLogged = true,
26
+ screenName = ScreenNames.HOME_CONSUMER
27
+ } = args
28
+
29
+ super()
30
+
31
+ this.componentName = componentName
32
+ this.entity = entity
33
+ this.isLogged = isLogged
34
+ this.screenName = screenName
35
+ }
36
+
37
+ prepare() {
38
+ return {
39
+ ...super.prepare(),
40
+ componentName: this.componentName,
41
+ entity: this.entity,
42
+ isLogged: this.isLogged,
43
+ screenName: this.screenName
44
+ }
45
+ }
46
+ }
47
+
48
+ export default BaseClickSchema
@@ -0,0 +1,64 @@
1
+ import { HttpCodes } from '@/src/lib/utils'
2
+ import { Result } from '../constants'
3
+ import { DataHubEntities } from '../entities'
4
+ import type { DataHubEntityTypes, ResultType } from '../types'
5
+
6
+ import BaseSchema from './base-schema'
7
+
8
+ export type BaseTrySchemaConstructorArgs = {
9
+ componentName: string
10
+ componentSource: string
11
+ entity?: DataHubEntityTypes
12
+ isLogged?: boolean
13
+ result?: ResultType
14
+ statusCode?: number
15
+ failureDescription?: string
16
+ }
17
+
18
+ abstract class BaseTrySchema extends BaseSchema {
19
+ private componentName: string
20
+ private componentSource: string
21
+ private failureDescription: string
22
+ private result: ResultType
23
+ private statusCode: number
24
+
25
+ entity: DataHubEntityTypes
26
+ isLogged: boolean
27
+
28
+ constructor(args: BaseTrySchemaConstructorArgs) {
29
+ const {
30
+ componentName,
31
+ componentSource,
32
+ entity = DataHubEntities.HOME,
33
+ failureDescription = Result.FAILURE_DESCRIPTION,
34
+ isLogged = true,
35
+ result = Result.SUCCESS,
36
+ statusCode = HttpCodes.OK
37
+ } = args
38
+
39
+ super()
40
+
41
+ this.componentName = componentName
42
+ this.componentSource = componentSource
43
+ this.entity = entity
44
+ this.failureDescription = failureDescription
45
+ this.isLogged = isLogged
46
+ this.result = result
47
+ this.statusCode = statusCode
48
+ }
49
+
50
+ prepare(): Record<string, unknown> {
51
+ return {
52
+ ...super.prepare(),
53
+ componentName: this.componentName,
54
+ componentSource: this.componentSource,
55
+ entity: this.entity,
56
+ failureDescription: this.failureDescription,
57
+ isLogged: this.isLogged,
58
+ result: this.result,
59
+ statusCode: this.statusCode
60
+ }
61
+ }
62
+ }
63
+
64
+ export default BaseTrySchema
@@ -1,6 +1,7 @@
1
1
  import { chance } from '@/src/config/tests'
2
2
  import { DataHubStore } from '../../..'
3
- import { ActionNames, ComponentNames, ScreenNames } from '../../../constants'
3
+ import { ActionNames } from '../../../actions'
4
+ import { ComponentNames, ScreenNames } from '../../../constants'
4
5
  import { DataHubEntities } from '../../../entities'
5
6
  import { UserRole } from '../../constants'
6
7
  import ClickHotmartTutor from '../click-hotmart-tutor'
@@ -1,4 +1,5 @@
1
- import { ActionNames, ComponentNames, ScreenNames } from '../../constants'
1
+ import { ActionNames } from '../../actions'
2
+ import { ComponentNames, ScreenNames } from '../../constants'
2
3
  import { DataHubEntities } from '../../entities'
3
4
  import type {
4
5
  ActionNamesType,
@@ -0,0 +1,24 @@
1
+ import { ActionNames } from '../../actions'
2
+ import { ComponentNames } from '../../constants'
3
+ import { DataHubEntities } from '../../entities'
4
+ import type { ActionNamesType } from '../../types'
5
+ import BaseClickSchema from '../base-click-schema'
6
+
7
+ class ClickTutorMinimizeSchema extends BaseClickSchema {
8
+ action: ActionNamesType
9
+
10
+ constructor() {
11
+ super({ componentName: ComponentNames.BUTTON_CLOSE_TUTOR_CHAT })
12
+
13
+ this.action = ActionNames.CLICK_TUTOR_MINIMIZE
14
+ this.entity = DataHubEntities.PRODUCT_CONSUME
15
+ }
16
+
17
+ getDataHubEventData(): Record<string, unknown> {
18
+ return {
19
+ ...super.prepare()
20
+ }
21
+ }
22
+ }
23
+
24
+ export default ClickTutorMinimizeSchema
@@ -1,3 +1,4 @@
1
1
  export { default as ClickHotmartTutor } from './click-hotmart-tutor'
2
+ export { default as ClickTutorMinimizeSchema } from './click-tutor-minimize'
2
3
  export * from './constants'
3
4
  export * from './types'
@@ -0,0 +1,24 @@
1
+ import { ActionNames } from '../../actions'
2
+ import type { ActionNamesType } from '../../types'
3
+ import type { BaseTrySchemaConstructorArgs } from '../base-try-schema'
4
+ import BaseTrySchema from '../base-try-schema'
5
+
6
+ export type TryProductTutorConstructorArgs = BaseTrySchemaConstructorArgs
7
+
8
+ class TryProductTutor extends BaseTrySchema {
9
+ action: ActionNamesType
10
+
11
+ constructor(args: TryProductTutorConstructorArgs) {
12
+ super(args)
13
+
14
+ this.action = ActionNames.TRY_PRODUCT_TUTOR
15
+ }
16
+
17
+ getDataHubEventData(): Record<string, unknown> {
18
+ return {
19
+ ...this.prepare()
20
+ }
21
+ }
22
+ }
23
+
24
+ export default TryProductTutor
@@ -1,4 +1,5 @@
1
- import type { ActionNames, ComponentNames, Platform, ScreenNames, System } from './constants'
1
+ import type { ActionNames } from './actions'
2
+ import type { ComponentNames, Platform, Result, ScreenNames, System } from './constants'
2
3
  import type { DataHubEntities } from './entities'
3
4
 
4
5
  export type ActionNamesType = (typeof ActionNames)[keyof typeof ActionNames]
@@ -7,6 +8,7 @@ export type PlatformType = (typeof Platform)[keyof typeof Platform]
7
8
  export type SystemType = (typeof System)[keyof typeof System]
8
9
  export type ComponentNamesType = (typeof ComponentNames)[keyof typeof ComponentNames]
9
10
  export type ScreenNamesType = (typeof ScreenNames)[keyof typeof ScreenNames]
11
+ export type ResultType = (typeof Result)[keyof typeof Result]
10
12
 
11
13
  export type SchemaType = {
12
14
  action: ActionNamesType
@@ -0,0 +1,7 @@
1
+ export const SCREEN_SIZES = {
2
+ sm: 576,
3
+ md: 768,
4
+ lg: 992,
5
+ xl: 1200,
6
+ xxl: 1700
7
+ } as const
@@ -98,20 +98,19 @@ function Button({
98
98
  )
99
99
  case 'secondary':
100
100
  return (
101
- <button
101
+ <ButtonDefault
102
102
  className={clsx(
103
- gridClasses,
104
103
  defaultPadding,
105
104
  defaultClasses,
106
105
  defaultBorder,
107
- 'border-neutral-900 bg-transparent text-neutral-900 hover:bg-neutral-900 hover:text-neutral-100',
106
+ 'border-neutral-900 bg-transparent text-neutral-900 hover:bg-[rgb(from_var(--hc-color-neutral-900)_r_g_b_/_0.05)]',
108
107
  className
109
108
  )}
110
109
  {...props}
111
110
  disabled={props.disabled || loading}
112
111
  aria-busy={loading}>
113
112
  <ButtonContent loading={loading}>{children}</ButtonContent>
114
- </button>
113
+ </ButtonDefault>
115
114
  )
116
115
  case 'tertiary':
117
116
  return (
@@ -129,9 +128,8 @@ function Button({
129
128
  )
130
129
  case 'brand':
131
130
  return (
132
- <button
131
+ <ButtonDefault
133
132
  className={clsx(
134
- gridClasses,
135
133
  defaultPadding,
136
134
  defaultClasses,
137
135
  defaultBorder,
@@ -142,7 +140,7 @@ function Button({
142
140
  disabled={props.disabled || loading}
143
141
  aria-busy={loading}>
144
142
  <ButtonContent loading={loading}>{children}</ButtonContent>
145
- </button>
143
+ </ButtonDefault>
146
144
  )
147
145
  default:
148
146
  return (
@@ -150,7 +148,8 @@ function Button({
150
148
  className={clsx(
151
149
  'rounded-full outline-none transition-colors duration-100',
152
150
  {
153
- 'cursor-pointer hover:bg-neutral-400 focus:bg-neutral-400': !props.disabled,
151
+ 'cursor-pointer hover:bg-neutral-300 hover:text-current focus:bg-neutral-300 focus:text-current':
152
+ !props.disabled,
154
153
  [disabledClasses]: props.disabled
155
154
  },
156
155
  styles.defaultButton,
@@ -1,20 +1,17 @@
1
- import clsx from 'clsx'
2
1
  import { useTranslation } from 'react-i18next'
3
2
 
4
3
  import ErrorDarkSVG from '@/public/assets/svg/error-dark.svg?url'
5
4
  import ErrorLightSVG from '@/public/assets/svg/error-light.svg?url'
6
5
  import { Button } from '@/src/lib/components'
7
- import { PageLayout, TutorWidgetEvents, useWidgetSettingsAtom } from '@/src/modules/widget'
6
+ import { PageLayout, TutorWidgetEvents } from '@/src/modules/widget'
8
7
  import { WidgetHeader } from '@/src/modules/widget/components/header'
9
8
 
10
- function GenericError() {
9
+ function GenericError({ isDarkMode = false }: { isDarkMode?: boolean }) {
11
10
  const { t } = useTranslation()
12
- const [settings] = useWidgetSettingsAtom()
13
- const isDarkMode = settings?.config?.theme === 'dark'
14
11
 
15
12
  return (
16
13
  <PageLayout className='p-5'>
17
- <WidgetHeader enabledButtons={['close']} showContent={false} />
14
+ <WidgetHeader enabledButtons={['close']} />
18
15
 
19
16
  <div className='flex h-full flex-col items-center justify-center p-10'>
20
17
  <div className='mb-8 flex flex-col items-center gap-1 text-center'>
@@ -26,38 +23,25 @@ function GenericError() {
26
23
  height={200}
27
24
  />
28
25
 
29
- <h2
30
- className={clsx('text-xl font-bold', {
31
- 'text-white': isDarkMode,
32
- 'text-gray-800': !isDarkMode
33
- })}>
34
- {t('generic_error.title')}
35
- </h2>
36
-
37
- <p
38
- className={clsx('text-sm', {
39
- 'text-gray-400': isDarkMode,
40
- 'text-gray-500': !isDarkMode
41
- })}>
42
- {t('generic_error.description')}
43
- </p>
26
+ <h2 className='text-xl font-bold text-neutral-900'>{t('generic_error.title')}</h2>
27
+ <p className='text-sm text-neutral-600'>{t('generic_error.description')}</p>
44
28
  </div>
45
29
 
46
30
  <div className='flex w-full flex-col gap-4'>
47
31
  <Button
48
32
  variant='brand'
49
- className='w-full rounded-lg py-2'
33
+ className='mx-auto w-full max-w-max rounded-lg !px-9 py-2 !font-light'
50
34
  onClick={() => window.location.reload()}
51
35
  aria-label='Retry Button'>
52
- <span className='font-light'>{t('general.buttons.try_again')}</span>
36
+ {t('general.buttons.try_again')}
53
37
  </Button>
54
38
 
55
39
  <Button
56
40
  variant='secondary'
57
- className='w-full rounded-lg py-2'
41
+ className='mx-auto w-full max-w-max rounded-lg !px-9 py-2 !font-light'
58
42
  onClick={() => TutorWidgetEvents['c3po-app-widget-hide'].dispatch()}
59
43
  aria-label='Close Button'>
60
- <span className='font-light'>{t('general.buttons.close')}</span>
44
+ {t('general.buttons.close')}
61
45
  </Button>
62
46
  </div>
63
47
  </div>
@@ -0,0 +1,5 @@
1
+ <svg viewBox="0 0 13 15" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path
3
+ d="M6.32585 0.734428H9.55762C9.88835 0.734428 10.2061 0.867241 10.4404 1.10162L12.2087 2.86984C12.443 3.10422 12.5758 3.42193 12.5758 3.75266V9.48443C12.5758 10.1745 12.0159 10.7344 11.3258 10.7344H6.32585C5.63574 10.7344 5.07585 10.1745 5.07585 9.48443V1.98443C5.07585 1.29432 5.63574 0.734428 6.32585 0.734428ZM2.15918 4.06776H4.24251V5.73443H2.57585V12.4011H7.57585V11.5678H9.24251V12.8178C9.24251 13.5079 8.68262 14.0678 7.99251 14.0678H2.15918C1.46908 14.0678 0.90918 13.5079 0.90918 12.8178V5.31776C0.90918 4.62766 1.46908 4.06776 2.15918 4.06776Z"
4
+ fill="currentColor" />
5
+ </svg>
@@ -9,11 +9,13 @@ export type ValidIconNames =
9
9
  | 'chevron-down'
10
10
  | 'clone'
11
11
  | 'close'
12
+ | 'copy-solid'
12
13
  | 'copy'
13
14
  | 'double-check'
14
15
  | 'gallery'
15
16
  | 'info'
16
17
  | 'interrogation'
18
+ | 'like-solid'
17
19
  | 'like'
18
20
  | 'paste'
19
21
  | 'send'
@@ -0,0 +1,5 @@
1
+ <svg viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path
3
+ d="M8.40316 0.426269C9.08024 0.565542 9.52034 1.24316 9.38493 1.93953L9.32503 2.24486C9.18701 2.95997 8.9318 3.64027 8.57503 4.25896H12.325C13.0151 4.25896 13.575 4.8348 13.575 5.54456C13.575 6.04005 13.3016 6.47126 12.9006 6.68553C13.1844 6.92122 13.3667 7.2828 13.3667 7.68723C13.3667 8.31396 12.9292 8.83623 12.3537 8.94872C12.4683 9.14424 12.5334 9.3719 12.5334 9.61563C12.5334 10.1861 12.1714 10.6709 11.6714 10.8369C11.6896 10.9253 11.7 11.0191 11.7 11.1155C11.7 11.8253 11.1401 12.4011 10.45 12.4011H7.91097C7.41618 12.4011 6.93441 12.2511 6.52295 11.9699L5.52034 11.2815C4.82503 10.8048 4.40837 10.0013 4.40837 9.14156V8.11576V6.83016V6.16326C4.40837 5.38118 4.75472 4.64464 5.34587 4.15451L5.53857 3.99649C6.22868 3.42868 6.70003 2.63054 6.87191 1.74133L6.9318 1.436C7.06722 0.739634 7.72607 0.286996 8.40316 0.426269ZM1.07503 4.6875H2.7417C3.20264 4.6875 3.57503 5.0705 3.57503 5.54456V11.544C3.57503 12.0181 3.20264 12.4011 2.7417 12.4011H1.07503C0.614095 12.4011 0.241699 12.0181 0.241699 11.544V5.54456C0.241699 5.0705 0.614095 4.6875 1.07503 4.6875Z"
4
+ fill="currentColor" />
5
+ </svg>
@@ -4,3 +4,4 @@ export * from './horizontal-draggable-scroll'
4
4
  export * from './icons'
5
5
  export * from './markdownrenderer'
6
6
  export * from './spinner'
7
+ export * from './tooltip'
@@ -0,0 +1,2 @@
1
+ export * from './tooltip'
2
+ export { default as Tooltip } from './tooltip'
@@ -0,0 +1,39 @@
1
+ .triangle {
2
+ z-index: 5;
3
+
4
+ &:before {
5
+ content: '';
6
+ display: inline-block;
7
+ width: 0;
8
+ height: 0;
9
+ border-style: solid;
10
+ border-width: 0px 0.5rem 0.625rem 0.5rem;
11
+ border-color: transparent transparent var(--hc-color-neutral-300) transparent;
12
+ position: absolute;
13
+ z-index: 4;
14
+ }
15
+ }
16
+
17
+ .top:before {
18
+ top: 100%;
19
+ left: 50%;
20
+ transform: translateX(-50%) rotateZ(180deg);
21
+ }
22
+
23
+ .right:before {
24
+ top: 50%;
25
+ right: 97%;
26
+ transform: translateY(-50%) rotateZ(-90deg);
27
+ }
28
+
29
+ .bottom:before {
30
+ bottom: 100%;
31
+ left: 50%;
32
+ transform: translateX(-50%);
33
+ }
34
+
35
+ .left:before {
36
+ top: 50%;
37
+ left: 97%;
38
+ transform: translateY(-50%) rotateZ(90deg);
39
+ }
@@ -0,0 +1,41 @@
1
+ import clsx from 'clsx'
2
+ import type { PropsWithChildren, ReactNode } from 'react'
3
+
4
+ import styles from './styles.module.css'
5
+
6
+ const POSITION = {
7
+ top: 'left-1/2 transform -translate-x-1/2 bottom-full mb-3',
8
+ right: 'left-full top-1/2 transform -translate-y-1/2 ml-3',
9
+ bottom: 'left-1/2 transform -translate-x-1/2 top-full mt-3',
10
+ left: 'right-full top-1/2 transform -translate-y-1/2 mr-3'
11
+ } as const
12
+
13
+ export type TooltipProps = PropsWithChildren<{
14
+ content?: ReactNode
15
+ position?: keyof typeof POSITION
16
+ show?: boolean
17
+ }>
18
+
19
+ function Tooltip({ children, content, position = 'top', show = true }: TooltipProps) {
20
+ if (!show) return children
21
+
22
+ return (
23
+ <div className='group relative flex items-center justify-center'>
24
+ {children}
25
+ {content && (
26
+ <div
27
+ className={clsx(
28
+ 'absolute hidden rounded-lg group-hover:block',
29
+ 'bg-neutral-300 px-4 py-2 text-xs text-neutral-900',
30
+ POSITION[position],
31
+ styles.triangle,
32
+ styles[position]
33
+ )}>
34
+ {content}
35
+ </div>
36
+ )}
37
+ </div>
38
+ )
39
+ }
40
+
41
+ export default Tooltip
@@ -1,4 +1,5 @@
1
1
  export * from './use-intersection-observer-reverse-scroll'
2
+ export * from './use-media-query'
2
3
  export * from './use-ref-client-height'
3
4
  export * from './use-ref-event-listener'
4
5
  export * from './use-throttle'
@@ -0,0 +1,2 @@
1
+ export * from './use-media-query'
2
+ export { default as useMediaQuery } from './use-media-query'
@@ -0,0 +1,20 @@
1
+ import { useLayoutEffect, useState } from 'react'
2
+
3
+ import { SCREEN_SIZES } from '@/src/config/theme/constants'
4
+
5
+ function useMediaQuery({ maxSize }: { maxSize: keyof typeof SCREEN_SIZES }) {
6
+ const [matches, setMatches] = useState(false)
7
+
8
+ useLayoutEffect(() => {
9
+ const mediaquery = window.matchMedia(`(max-width: ${SCREEN_SIZES[maxSize]}px)`)
10
+ const listener = () => setMatches(mediaquery.matches)
11
+
12
+ mediaquery.addEventListener('change', listener)
13
+
14
+ return () => mediaquery.removeEventListener('change', listener)
15
+ }, [maxSize])
16
+
17
+ return matches
18
+ }
19
+
20
+ export default useMediaQuery
package/src/main/main.tsx CHANGED
@@ -17,7 +17,7 @@ function Main({ settings }: MainProps) {
17
17
  useListenToThemeChangeEvent()
18
18
 
19
19
  return (
20
- <ErrorBoundary fallback={<GenericError />}>
20
+ <ErrorBoundary fallback={<GenericError isDarkMode={settings.config?.theme === 'dark'} />}>
21
21
  <GlobalProviders settings={settings}>
22
22
  <WidgetContainer />
23
23
  </GlobalProviders>
@@ -86,7 +86,8 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
86
86
  onClick={onSend}
87
87
  disabled={buttonDisabled || loading}
88
88
  className={clsx('flex flex-col items-center justify-center', styles.send, {
89
- 'bg-neutral-900 text-neutral-100': !buttonDisabled,
89
+ 'bg-neutral-900 text-neutral-100 hover:text-neutral-900 focus:text-neutral-900':
90
+ !buttonDisabled,
90
91
  'text-neutral-900': buttonDisabled
91
92
  })}
92
93
  loading={loading}
@@ -55,36 +55,36 @@ function MessageActions({ message, className, showActions = false }: MessageActi
55
55
  hidden: !showActions
56
56
  })}>
57
57
  <Button
58
- className='hover:!bg-transparent hover:text-info-500 focus:!bg-transparent'
58
+ className='hover:!bg-transparent hover:text-neutral-500 focus:!bg-transparent'
59
59
  onClick={() => handleReaction()}
60
60
  aria-label={t('general.buttons.like')}>
61
61
  <Icon
62
- name='like'
63
- className={clsx('size-3', {
64
- 'text-info-500': reaction === ButtonReactions.LIKE
62
+ name={reaction === ButtonReactions.LIKE ? 'like-solid' : 'like'}
63
+ className={clsx('size-3 transition-colors duration-100', {
64
+ 'text-neutral-900': reaction === ButtonReactions.LIKE
65
65
  })}
66
66
  />
67
67
  </Button>
68
68
  <Button
69
- className='rotate-180 scale-x-[-1] hover:!bg-transparent hover:text-danger-500 focus:!bg-transparent'
69
+ className='rotate-180 scale-x-[-1] hover:!bg-transparent hover:text-neutral-500 focus:!bg-transparent'
70
70
  onClick={() => handleReaction(ButtonReactions.DISLIKE)}
71
71
  aria-label={t('general.buttons.dislike')}>
72
72
  <Icon
73
- name='like'
74
- className={clsx('size-3', {
75
- 'text-danger-500': reaction === ButtonReactions.DISLIKE
73
+ name={reaction === ButtonReactions.DISLIKE ? 'like-solid' : 'like'}
74
+ className={clsx('size-3 transition-colors duration-100', {
75
+ 'text-neutral-900': reaction === ButtonReactions.DISLIKE
76
76
  })}
77
77
  />
78
78
  </Button>
79
79
  <Button
80
- className='hover:!bg-transparent hover:text-info-500 focus:!bg-transparent'
80
+ className='hover:!bg-transparent hover:text-neutral-500 focus:!bg-transparent'
81
81
  onClick={copyToClipboard}
82
82
  aria-label={t('general.buttons.copy')}
83
83
  disabled={copying}>
84
84
  <Icon
85
- name={copied ? 'paste' : 'copy'}
86
- className={clsx('size-3', {
87
- 'text-info-500': copied
85
+ name={copied ? 'copy-solid' : 'copy'}
86
+ className={clsx('size-3 transition-colors duration-100', {
87
+ 'text-neutral-900': copied
88
88
  })}
89
89
  />
90
90
  </Button>
@@ -1,4 +1,5 @@
1
1
  import { render, screen } from '@/src/config/tests'
2
+ import * as Hooks from '@/src/lib/hooks'
2
3
 
3
4
  import WidgetHeaderPropsBuilder from './__tests__/widget-header-props.builder'
4
5
  import WidgetHeader from './header'
@@ -12,8 +13,12 @@ describe('<WidgetHeader />', () => {
12
13
 
13
14
  expect(screen.getByRole('button', { name: /Close Icon/i })).toBeInTheDocument()
14
15
 
15
- expect(screen.queryByRole('button', { name: /Archive Icon/i })).not.toBeInTheDocument()
16
- expect(screen.queryByRole('button', { name: /Info Icon/i })).not.toBeInTheDocument()
16
+ expect(
17
+ screen.queryByRole('button', { name: /general.buttons.archive Icon/i })
18
+ ).not.toBeInTheDocument()
19
+ expect(
20
+ screen.queryByRole('button', { name: /general.buttons.info Icon/i })
21
+ ).not.toBeInTheDocument()
17
22
  })
18
23
 
19
24
  it('should render WidgetHeaderContent when prop showContent is true', () => {
@@ -41,7 +46,30 @@ describe('<WidgetHeader />', () => {
41
46
 
42
47
  renderComponent(props)
43
48
 
44
- expect(screen.getByRole('button', { name: /Archive Icon/i })).toBeInTheDocument()
45
- expect(screen.getByRole('button', { name: /Info Icon/i })).toBeInTheDocument()
49
+ expect(
50
+ screen.getByRole('button', { name: /general.buttons.archive Icon/i })
51
+ ).toBeInTheDocument()
52
+ expect(screen.getByRole('button', { name: /general.buttons.info Icon/i })).toBeInTheDocument()
53
+ })
54
+
55
+ it('should show header buttons tooltip when window is not in mobile view', () => {
56
+ const props = new WidgetHeaderPropsBuilder().withEnabledButtons(['archive', 'info'])
57
+
58
+ renderComponent(props)
59
+
60
+ expect(screen.getByText(/general.buttons.archive/i)).toBeInTheDocument()
61
+ })
62
+
63
+ it('should not show header buttons tooltip when window is in mobile view', () => {
64
+ vi.spyOn(Hooks, 'useMediaQuery').mockReturnValue(true)
65
+
66
+ const props = new WidgetHeaderPropsBuilder().withEnabledButtons(['archive', 'info'])
67
+
68
+ renderComponent(props)
69
+
70
+ expect(
71
+ screen.getByRole('button', { name: /general.buttons.archive Icon/i })
72
+ ).toBeInTheDocument()
73
+ expect(screen.queryByText(/general.buttons.archive/i)).not.toBeInTheDocument()
46
74
  })
47
75
  })
@@ -1,7 +1,10 @@
1
1
  import clsx from 'clsx'
2
2
  import { useTranslation } from 'react-i18next'
3
3
 
4
- import { Button, Icon } from '@/src/lib/components'
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'
5
8
  import { TutorWidgetEvents } from '../../events'
6
9
  import { useWidgetGoBackTabAtom, useWidgetTabsAtom } from '../../store'
7
10
  import { AIAvatar } from '../ai-avatar'
@@ -22,7 +25,9 @@ export function WidgetHeaderContent({ tutorName }: WidgetHeaderContentProps) {
22
25
  }
23
26
 
24
27
  export function WidgetHeaderContentWithoutMeta({ name }: { name?: string }) {
28
+ const { t } = useTranslation()
25
29
  const [, goBack] = useWidgetGoBackTabAtom()
30
+ const tutorName = name ?? t('general.name')
26
31
 
27
32
  return (
28
33
  <div
@@ -35,7 +40,7 @@ export function WidgetHeaderContentWithoutMeta({ name }: { name?: string }) {
35
40
  </Button>
36
41
  <div className='grid-area-[b] flex min-h-6 justify-center text-center'>
37
42
  <span className='absolute bottom-0 left-1/2 -translate-x-1/2 text-base font-bold'>
38
- {name}
43
+ {tutorName}
39
44
  </span>
40
45
  </div>
41
46
  </div>
@@ -51,6 +56,7 @@ function WidgetHeader({
51
56
  const { t } = useTranslation()
52
57
  const [, setTab] = useWidgetTabsAtom()
53
58
  const name = tutorName ?? t('general.name')
59
+ const isMobile = useMediaQuery({ maxSize: 'md' })
54
60
 
55
61
  const handleClickArchive = () => {
56
62
  setTab('chat')
@@ -60,6 +66,11 @@ function WidgetHeader({
60
66
  setTab('information')
61
67
  }
62
68
 
69
+ const handleHideWidget = () => {
70
+ TutorWidgetEvents['c3po-app-widget-hide'].dispatch()
71
+ DataHubService.sendEvent({ schema: new ClickTutorMinimizeSchema() })
72
+ }
73
+
63
74
  return (
64
75
  <div className='mt-0.5 flex flex-col gap-2 text-neutral-900'>
65
76
  <div className='flex justify-end'>
@@ -67,7 +78,7 @@ function WidgetHeader({
67
78
  <Button
68
79
  className='text-neutral-500'
69
80
  show={enabledButtons.includes('close')}
70
- onClick={() => TutorWidgetEvents['c3po-app-widget-hide'].dispatch()}
81
+ onClick={handleHideWidget}
71
82
  aria-label='Close Icon'>
72
83
  <Icon name='close' className='h-3 w-3' aria-hidden />
73
84
  </Button>
@@ -78,24 +89,25 @@ function WidgetHeader({
78
89
  {showContent && !showContentWithoutMeta && <WidgetHeaderContent tutorName={name} />}
79
90
  {showContentWithoutMeta && !showContent && <WidgetHeaderContentWithoutMeta name={name} />}
80
91
  </div>
81
- <div className='shrink-0'>
82
- <div
83
- className={clsx(
84
- 'grid-area-[b] ml-auto flex max-w-max gap-3 text-neutral-700',
85
- styles.btnContainer
86
- )}>
87
- <Button
88
- show={enabledButtons.includes('archive')}
89
- onClick={handleClickArchive}
90
- aria-label='Archive Icon'>
91
- <Icon name='archive' className='h-4 w-4' aria-hidden />
92
- </Button>
93
- <Button
94
- show={enabledButtons.includes('info')}
95
- aria-label='Info Icon'
96
- onClick={handleClickInfo}>
97
- <Icon name='info' className='h-4 w-4' aria-hidden />
98
- </Button>
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>
99
111
  </div>
100
112
  </div>
101
113
  </div>
@@ -41,7 +41,7 @@ function WidgetInformationPage() {
41
41
  </div>
42
42
 
43
43
  <div className='flex flex-col gap-5'>
44
- {infoItems({ tutorName }).map((item) => (
44
+ {infoItems({ tutorName: t('general.name') }).map((item) => (
45
45
  <InformationCard
46
46
  key={item.titleKey}
47
47
  icon={item.icon}