app-tutor-ai-consumer 1.22.1 → 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.
Files changed (41) hide show
  1. package/CHANGELOG.md +9 -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/expand.svg +5 -0
  21. package/src/lib/components/icons/icon-names.d.ts +3 -0
  22. package/src/lib/components/icons/like-solid.svg +5 -0
  23. package/src/lib/components/index.ts +1 -0
  24. package/src/lib/components/tooltip/index.ts +2 -0
  25. package/src/lib/components/tooltip/styles.module.css +39 -0
  26. package/src/lib/components/tooltip/tooltip.tsx +41 -0
  27. package/src/lib/hooks/index.ts +1 -0
  28. package/src/lib/hooks/use-media-query/index.ts +2 -0
  29. package/src/lib/hooks/use-media-query/use-media-query.tsx +20 -0
  30. package/src/main/main.tsx +1 -1
  31. package/src/modules/messages/components/chat-input/chat-input.tsx +2 -1
  32. package/src/modules/messages/components/message-actions/message-actions.tsx +12 -12
  33. package/src/modules/widget/components/chat-page/chat-page.tsx +7 -1
  34. package/src/modules/widget/components/header/index.ts +1 -2
  35. package/src/modules/widget/components/header/widget-header.spec.tsx +75 -0
  36. package/src/modules/widget/components/header/widget-header.tsx +148 -0
  37. package/src/modules/widget/components/information-page/information-page.tsx +26 -27
  38. package/src/modules/widget/components/starter-page/starter-page.tsx +3 -2
  39. package/src/modules/widget/events.ts +20 -2
  40. package/src/modules/widget/components/header/header.spec.tsx +0 -47
  41. package/src/modules/widget/components/header/header.tsx +0 -106
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [1.22.3](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.22.2...v1.22.3) (2025-08-08)
2
+
3
+ ## [1.22.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.22.1...v1.22.2) (2025-08-07)
4
+
5
+ ### Bug Fixes
6
+
7
+ - header qa issues part 1 ([4910041](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/4910041905995e3b8161d015b261c82331131d20))
8
+ - qa issues part 2 ([65d5eb6](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/65d5eb64768f7496a1f767a6d2bd9e614abef547))
9
+
1
10
  ## [1.22.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.22.0...v1.22.1) (2025-08-06)
2
11
 
3
12
  ### 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.3",
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>
@@ -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>
@@ -9,11 +9,14 @@ export type ValidIconNames =
9
9
  | 'chevron-down'
10
10
  | 'clone'
11
11
  | 'close'
12
+ | 'copy-solid'
12
13
  | 'copy'
13
14
  | 'double-check'
15
+ | 'expand'
14
16
  | 'gallery'
15
17
  | 'info'
16
18
  | 'interrogation'
19
+ | 'like-solid'
17
20
  | 'like'
18
21
  | 'paste'
19
22
  | '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: 96%;
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: 96%;
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,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'
@@ -0,0 +1,75 @@
1
+ import { render, screen } from '@/src/config/tests'
2
+ import * as Hooks from '@/src/lib/hooks'
3
+
4
+ import WidgetHeaderPropsBuilder from './__tests__/widget-header-props.builder'
5
+ import WidgetHeader from './widget-header'
6
+
7
+ describe('<WidgetHeader />', () => {
8
+ const defaultProps = new WidgetHeaderPropsBuilder()
9
+ const renderComponent = (props = defaultProps) => render(<WidgetHeader {...props} />)
10
+
11
+ it('should render the only the enabled button', () => {
12
+ renderComponent()
13
+
14
+ expect(screen.getByRole('button', { name: /Close Icon/i })).toBeInTheDocument()
15
+
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()
22
+ })
23
+
24
+ it('should render WidgetHeaderContent when prop showContent is true', () => {
25
+ renderComponent()
26
+
27
+ expect(screen.getByText(/sparkle-tutor-light/i)).toBeInTheDocument()
28
+
29
+ expect(screen.queryByRole('button', { name: /Arrow Left Icon/i })).not.toBeInTheDocument()
30
+ })
31
+
32
+ it('should render WidgetHeaderContentWithoutMeta when prop showContentWithoutMeta is true', () => {
33
+ const props = new WidgetHeaderPropsBuilder()
34
+ .withEnabledButtons(['close', 'arrow-left'])
35
+ .withShowContentWithoutMeta(true)
36
+
37
+ renderComponent(props)
38
+
39
+ expect(screen.getByRole('button', { name: /Arrow Left Icon/i })).toBeInTheDocument()
40
+
41
+ expect(screen.queryByText(/sparkle-tutor-light/i)).not.toBeInTheDocument()
42
+ })
43
+
44
+ it('should be able to render the remaining icons', () => {
45
+ const props = new WidgetHeaderPropsBuilder().withEnabledButtons(['archive', 'info'])
46
+
47
+ renderComponent(props)
48
+
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()
74
+ })
75
+ })
@@ -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 }).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,47 +0,0 @@
1
- import { render, screen } from '@/src/config/tests'
2
-
3
- import WidgetHeaderPropsBuilder from './__tests__/widget-header-props.builder'
4
- import WidgetHeader from './header'
5
-
6
- describe('<WidgetHeader />', () => {
7
- const defaultProps = new WidgetHeaderPropsBuilder()
8
- const renderComponent = (props = defaultProps) => render(<WidgetHeader {...props} />)
9
-
10
- it('should render the only the enabled button', () => {
11
- renderComponent()
12
-
13
- expect(screen.getByRole('button', { name: /Close Icon/i })).toBeInTheDocument()
14
-
15
- expect(screen.queryByRole('button', { name: /Archive Icon/i })).not.toBeInTheDocument()
16
- expect(screen.queryByRole('button', { name: /Info Icon/i })).not.toBeInTheDocument()
17
- })
18
-
19
- it('should render WidgetHeaderContent when prop showContent is true', () => {
20
- renderComponent()
21
-
22
- expect(screen.getByText(/sparkle-tutor-light/i)).toBeInTheDocument()
23
-
24
- expect(screen.queryByRole('button', { name: /Arrow Left Icon/i })).not.toBeInTheDocument()
25
- })
26
-
27
- it('should render WidgetHeaderContentWithoutMeta when prop showContentWithoutMeta is true and showContent is false', () => {
28
- const props = new WidgetHeaderPropsBuilder()
29
- .withShowContentWithoutMeta(true)
30
- .withShowContent(false)
31
-
32
- renderComponent(props)
33
-
34
- expect(screen.getByRole('button', { name: /Arrow Left Icon/i })).toBeInTheDocument()
35
-
36
- expect(screen.queryByText(/sparkle-tutor-light/i)).not.toBeInTheDocument()
37
- })
38
-
39
- it('should be able to render the remaining icons', () => {
40
- const props = new WidgetHeaderPropsBuilder().withEnabledButtons(['archive', 'info'])
41
-
42
- renderComponent(props)
43
-
44
- expect(screen.getByRole('button', { name: /Archive Icon/i })).toBeInTheDocument()
45
- expect(screen.getByRole('button', { name: /Info Icon/i })).toBeInTheDocument()
46
- })
47
- })
@@ -1,106 +0,0 @@
1
- import clsx from 'clsx'
2
- import { useTranslation } from 'react-i18next'
3
-
4
- import { Button, Icon } from '@/src/lib/components'
5
- import { TutorWidgetEvents } from '../../events'
6
- import { useWidgetGoBackTabAtom, useWidgetTabsAtom } from '../../store'
7
- import { AIAvatar } from '../ai-avatar'
8
-
9
- import type { WidgetHeaderContentProps, WidgetHeaderProps } from './types'
10
-
11
- import styles from './styles.module.css'
12
-
13
- export function WidgetHeaderContent({ tutorName }: WidgetHeaderContentProps) {
14
- return (
15
- <div className='flex w-full gap-2'>
16
- <AIAvatar />
17
- <div className='flex flex-col justify-center'>
18
- {tutorName && <h4 className='text-sm/loose font-bold'>{tutorName}</h4>}
19
- </div>
20
- </div>
21
- )
22
- }
23
-
24
- export function WidgetHeaderContentWithoutMeta({ name }: { name?: string }) {
25
- const [, goBack] = useWidgetGoBackTabAtom()
26
-
27
- return (
28
- <div
29
- className={clsx(
30
- 'grid-areas-[a_b] grid grid-cols-[auto_1fr] items-center gap-1',
31
- styles.withoutMetaContainer
32
- )}>
33
- <Button className='grid-area-[a]' aria-label='Arrow Left Icon' onClick={goBack}>
34
- <Icon name='arrow-left' className='h-3.5 w-3.5' aria-hidden />
35
- </Button>
36
- <div className='grid-area-[b] flex min-h-6 justify-center text-center'>
37
- <span className='absolute bottom-0 left-1/2 -translate-x-1/2 text-base font-bold'>
38
- {name}
39
- </span>
40
- </div>
41
- </div>
42
- )
43
- }
44
-
45
- function WidgetHeader({
46
- enabledButtons = [],
47
- tutorName,
48
- showContentWithoutMeta,
49
- showContent = true
50
- }: WidgetHeaderProps) {
51
- const { t } = useTranslation()
52
- const [, setTab] = useWidgetTabsAtom()
53
- const name = tutorName ?? t('general.name')
54
-
55
- const handleClickArchive = () => {
56
- setTab('chat')
57
- }
58
-
59
- const handleClickInfo = () => {
60
- setTab('information')
61
- }
62
-
63
- return (
64
- <div className='mt-0.5 flex flex-col gap-2 text-neutral-900'>
65
- <div className='flex justify-end'>
66
- <div className={styles.closeContainer}>
67
- <Button
68
- className='text-neutral-500'
69
- show={enabledButtons.includes('close')}
70
- onClick={() => TutorWidgetEvents['c3po-app-widget-hide'].dispatch()}
71
- aria-label='Close Icon'>
72
- <Icon name='close' className='h-3 w-3' aria-hidden />
73
- </Button>
74
- </div>
75
- </div>
76
- <div className='grid-areas-[a_b] grid grid-cols-[1fr_auto] items-center pb-4'>
77
- <div className='grid-area-[a] relative'>
78
- {showContent && !showContentWithoutMeta && <WidgetHeaderContent tutorName={name} />}
79
- {showContentWithoutMeta && !showContent && <WidgetHeaderContentWithoutMeta name={name} />}
80
- </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>
99
- </div>
100
- </div>
101
- </div>
102
- </div>
103
- )
104
- }
105
-
106
- export default WidgetHeader