app-tutor-ai-consumer 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/development-bootstrap.tsx +1 -1
  5. package/src/lib/components/button/button.tsx +57 -14
  6. package/src/lib/components/icons/archive.svg +5 -0
  7. package/src/lib/components/icons/arrow-left.svg +5 -0
  8. package/src/lib/components/icons/close.svg +5 -0
  9. package/src/lib/components/icons/icon-names.d.ts +10 -1
  10. package/src/lib/components/icons/info.svg +5 -0
  11. package/src/modules/messages/components/chat-input/chat-input.tsx +12 -22
  12. package/src/modules/widget/components/ai-avatar/ai-avatar-icon.tsx +17 -0
  13. package/src/modules/widget/components/ai-avatar/ai-avatar.tsx +13 -8
  14. package/src/modules/widget/components/ai-avatar/index.ts +3 -1
  15. package/src/modules/widget/components/ai-avatar/styles.module.css +3 -0
  16. package/src/modules/widget/components/chat-page/chat-page.tsx +17 -2
  17. package/src/modules/widget/components/greetings-card/greetings-card.tsx +2 -8
  18. package/src/modules/widget/components/greetings-card/styles.module.css +0 -4
  19. package/src/modules/widget/components/header/__tests__/widget-header-props.builder.ts +46 -0
  20. package/src/modules/widget/components/header/header.spec.tsx +47 -0
  21. package/src/modules/widget/components/header/header.tsx +69 -0
  22. package/src/modules/widget/components/header/index.ts +2 -0
  23. package/src/modules/widget/components/header/types.ts +9 -0
  24. package/src/modules/widget/components/index.ts +1 -0
  25. package/src/modules/widget/components/starter-page/starter-page.tsx +12 -6
  26. package/src/modules/widget/events.ts +17 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # [1.17.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.16.0...v1.17.0) (2025-07-22)
2
+
3
+ ### Features
4
+
5
+ - add header navigation ([1b1f8fb](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/1b1f8fb8ed1cb832bf7bbe4c0ddc1418b9946ce3))
6
+
1
7
  # [1.16.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.15.0...v1.16.0) (2025-07-22)
2
8
 
3
9
  ### Features
package/README.md CHANGED
@@ -78,7 +78,7 @@ nvm install
78
78
  2. To generate SSL certificates locally, run:
79
79
 
80
80
  ```sh
81
- ./config/certs/ssl-generate.sh
81
+ cd ./config/certs/ && ./ssl-generate.sh && cd ../../
82
82
  ```
83
83
 
84
84
  3. To install package dependencies, run:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -18,7 +18,7 @@ if (devMode) {
18
18
  container.setAttribute('id', rootId)
19
19
  container.setAttribute(
20
20
  'class',
21
- 'bg-ai-dark fixed bottom-5 right-5 w-[27rem] h-[min(43rem,calc(100vh-2.5rem))] max-h-[calc(100vh-2.5rem)] z-10 rounded-[0.625rem] border border-neutral-800 shadow-lg overflow-hidden flex flex-col'
21
+ 'bg-ai-dark fixed bottom-5 right-5 w-[27rem] h-[min(43rem,calc(100vh-2.5rem))] max-h-[calc(100vh-2.5rem)] z-10 rounded-[0.625rem] border border-neutral-200 shadow-lg overflow-hidden flex flex-col'
22
22
  )
23
23
 
24
24
  root?.appendChild(container)
@@ -1,17 +1,33 @@
1
1
  import clsx from 'clsx'
2
- import type { HTMLAttributes, PropsWithChildren } from 'react'
2
+ import type { ButtonHTMLAttributes, PropsWithChildren } from 'react'
3
+
4
+ import { Spinner } from '../spinner'
3
5
 
4
6
  export type ButtonProps = PropsWithChildren<
5
- HTMLAttributes<HTMLButtonElement> & {
7
+ ButtonHTMLAttributes<HTMLButtonElement> & {
6
8
  variant?: 'brand' | 'secondary' | 'primary' | 'tertiary' | 'gradient-outline'
9
+ show?: boolean
10
+ loading?: boolean
7
11
  }
8
12
  >
9
13
 
10
- function Button({ children, className, variant = 'brand', ...props }: ButtonProps) {
14
+ function Button({
15
+ children,
16
+ className,
17
+ variant,
18
+ show = true,
19
+ loading = false,
20
+ ...props
21
+ }: ButtonProps) {
11
22
  const defaultClasses =
12
23
  'rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 text-base font-medium'
13
24
  const defaultBorder = 'border border-transparent'
14
25
  const defaultPadding = 'px-4 py-2'
26
+ const disabledClasses = 'cursor-not-allowed'
27
+
28
+ const content = loading ? <Spinner className='h-full w-full text-current' /> : children
29
+
30
+ if (!show) return null
15
31
 
16
32
  switch (variant) {
17
33
  case 'gradient-outline':
@@ -22,11 +38,13 @@ function Button({ children, className, variant = 'brand', ...props }: ButtonProp
22
38
  'group relative inline-flex items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-[var(--ai-color-gradient-primary)] to-[var(--ai-color-gradient-accent)] p-[1px] hover:text-neutral-1000 group-hover:from-[var(--ai-color-gradient-primary)] group-hover:to-[var(--ai-color-gradient-accent)]',
23
39
  className
24
40
  )}
25
- {...props}>
41
+ {...props}
42
+ disabled={props.disabled || loading}
43
+ aria-busy={loading}>
26
44
  <span
27
45
  data-label='text-content'
28
46
  className='relative flex-1 rounded-lg bg-neutral-100 px-5 py-2.5 text-neutral-1000 transition-all duration-75 ease-in group-hover:bg-transparent'>
29
- {children}
47
+ {content}
30
48
  </span>
31
49
  </button>
32
50
  )
@@ -40,8 +58,10 @@ function Button({ children, className, variant = 'brand', ...props }: ButtonProp
40
58
  'bg-primary-500 text-neutral-1000 hover:bg-primary-400 focus:outline-none',
41
59
  className
42
60
  )}
43
- {...props}>
44
- {children}
61
+ {...props}
62
+ disabled={props.disabled || loading}
63
+ aria-busy={loading}>
64
+ {content}
45
65
  </button>
46
66
  )
47
67
  case 'secondary':
@@ -54,8 +74,10 @@ function Button({ children, className, variant = 'brand', ...props }: ButtonProp
54
74
  'border-neutral-900 bg-transparent text-neutral-900 hover:bg-neutral-900 hover:text-neutral-100',
55
75
  className
56
76
  )}
57
- {...props}>
58
- {children}
77
+ {...props}
78
+ disabled={props.disabled || loading}
79
+ aria-busy={loading}>
80
+ {content}
59
81
  </button>
60
82
  )
61
83
  case 'tertiary':
@@ -68,12 +90,13 @@ function Button({ children, className, variant = 'brand', ...props }: ButtonProp
68
90
  'text-1000 bg-transparent hover:bg-neutral-900',
69
91
  className
70
92
  )}
71
- {...props}>
72
- {children}
93
+ {...props}
94
+ disabled={props.disabled || loading}
95
+ aria-busy={loading}>
96
+ {content}
73
97
  </button>
74
98
  )
75
99
  case 'brand':
76
- default:
77
100
  return (
78
101
  <button
79
102
  className={clsx(
@@ -83,8 +106,28 @@ function Button({ children, className, variant = 'brand', ...props }: ButtonProp
83
106
  'rounded bg-neutral-900 text-neutral-100 outline-none hover:bg-neutral-800',
84
107
  className
85
108
  )}
86
- {...props}>
87
- {children}
109
+ {...props}
110
+ disabled={props.disabled || loading}
111
+ aria-busy={loading}>
112
+ {content}
113
+ </button>
114
+ )
115
+ default:
116
+ return (
117
+ <button
118
+ className={clsx(
119
+ 'rounded-full p-2 outline-none transition-colors duration-300 ease-in',
120
+ {
121
+ 'cursor-pointer ring-primary-500 hover:bg-neutral-300 focus:bg-neutral-300 focus-visible:ring-2':
122
+ !props.disabled,
123
+ [disabledClasses]: props.disabled
124
+ },
125
+ className
126
+ )}
127
+ {...props}
128
+ disabled={props.disabled || loading}
129
+ aria-busy={loading}>
130
+ {content}
88
131
  </button>
89
132
  )
90
133
  }
@@ -0,0 +1,5 @@
1
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path
3
+ d="M14.5 1H1.5C0.65625 1 0 1.6875 0 2.5V4.5C0 4.78125 0.21875 5 0.5 5H1V13C1 14.125 1.875 15 3 15H13C14.0938 15 15 14.125 15 13V5H15.5C15.75 5 16 4.78125 16 4.5V2.5C16 1.6875 15.3125 1 14.5 1ZM14 13C14 13.5625 13.5312 14 13 14H3C2.4375 14 2 13.5625 2 13V5H14V13ZM15 4H1V2.5C1 2.25 1.21875 2 1.5 2H14.5C14.75 2 15 2.25 15 2.5V4ZM5.5 8H10.5C10.75 8 11 7.78125 11 7.5C11 7.25 10.75 7 10.5 7H5.5C5.21875 7 5 7.25 5 7.5C5 7.78125 5.21875 8 5.5 8Z"
4
+ fill="currentColor" />
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path
3
+ d="M3.99297 6.96303H15.0076V8.77293H3.99297L8.84708 13.627L7.5673 14.9068L0.528442 7.86798L7.5673 0.829102L8.84708 2.10889L3.99297 6.96303Z"
4
+ fill="currentColor" />
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path
3
+ d="M15.2656 15.3125C14.9844 15.5938 14.4688 15.5938 14.1875 15.3125L8 9.07812L1.76562 15.3125C1.48438 15.5938 0.96875 15.5938 0.6875 15.3125C0.40625 15.0312 0.40625 14.5156 0.6875 14.2344L6.92188 8L0.6875 1.8125C0.40625 1.53125 0.40625 1.01562 0.6875 0.734375C0.96875 0.453125 1.48438 0.453125 1.76562 0.734375L8 6.96875L14.1875 0.734375C14.4688 0.453125 14.9844 0.453125 15.2656 0.734375C15.5469 1.01562 15.5469 1.53125 15.2656 1.8125L9.03125 8L15.2656 14.2344C15.5469 14.5156 15.5469 15.0312 15.2656 15.3125Z"
4
+ fill="currentColor" />
5
+ </svg>
@@ -1,2 +1,11 @@
1
1
  // Auto-generated file - DO NOT EDIT
2
- export type ValidIconNames = 'ai-color' | 'arrow-down' | 'chevron-down' | 'send' | 'stop'
2
+ export type ValidIconNames =
3
+ | 'ai-color'
4
+ | 'archive'
5
+ | 'arrow-down'
6
+ | 'arrow-left'
7
+ | 'chevron-down'
8
+ | 'close'
9
+ | 'info'
10
+ | 'send'
11
+ | 'stop'
@@ -0,0 +1,5 @@
1
+ <svg viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd"
3
+ d="M0 8.5C0 4.08172 3.58172 0.5 8 0.5C12.4183 0.5 16 4.08172 16 8.5C16 12.9183 12.4183 16.5 8 16.5C3.58172 16.5 0 12.9183 0 8.5ZM8 1.5C4.13401 1.5 1 4.63401 1 8.5C1 12.366 4.13401 15.5 8 15.5C11.866 15.5 15 12.366 15 8.5C15 4.63401 11.866 1.5 8 1.5ZM7 5.5C7 4.94772 7.44772 4.5 8 4.5C8.55229 4.5 9 4.94772 9 5.5C9 6.05228 8.55229 6.5 8 6.5C7.44772 6.5 7 6.05228 7 5.5ZM7 7.5H7.5C8.32843 7.5 9 8.17157 9 9V12.5H8V9C8 8.72386 7.77614 8.5 7.5 8.5H7V7.5Z"
4
+ fill="currentColor" />
5
+ </svg>
@@ -4,7 +4,7 @@ import type { ChangeEvent, KeyboardEvent } from 'react'
4
4
  import { useTranslation } from 'react-i18next'
5
5
  import TextareaAutosize from 'react-textarea-autosize'
6
6
 
7
- import { Icon, Spinner } from '@/src/lib/components'
7
+ import { Button, Icon } from '@/src/lib/components'
8
8
 
9
9
  import { useChatInputValueAtom } from './chat-input.atom'
10
10
  import type { ChatInputProps } from './types'
@@ -82,30 +82,20 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
82
82
  onKeyDown={handleKeyDown}
83
83
  disabled={inputDisabled}
84
84
  />
85
- <button
85
+ <Button
86
86
  onClick={onSend}
87
87
  disabled={buttonDisabled || loading}
88
- className={clsx(
89
- 'flex size-8 flex-col items-center justify-center rounded-full outline-none transition-colors duration-300 ease-in',
90
- {
91
- 'cursor-pointer hover:scale-110 hover:bg-neutral-400 focus:bg-neutral-500 focus:outline-none focus:ring-1 focus:ring-neutral-0 focus:ring-offset-2':
92
- !buttonDisabled,
93
- 'cursor-not-allowed': buttonDisabled
94
- }
95
- )}
88
+ className={clsx('flex size-8 flex-col items-center justify-center', {
89
+ 'text-neutral-700': !buttonDisabled,
90
+ 'text-neutral-400': buttonDisabled
91
+ })}
92
+ loading={loading}
96
93
  aria-label='Submit Button'>
97
- {loading ? (
98
- <Spinner className='h-5 w-5 text-neutral-600' />
99
- ) : (
100
- <Icon
101
- name='send'
102
- className={clsx('h-4 w-4 pr-0.5 pt-0.5 transition-colors duration-150', {
103
- 'text-neutral-700': !buttonDisabled,
104
- 'text-neutral-400': buttonDisabled
105
- })}
106
- />
107
- )}
108
- </button>
94
+ <Icon
95
+ name='send'
96
+ className='h-4 w-4 pr-0.5 pt-0.5 text-current transition-colors duration-150'
97
+ />
98
+ </Button>
109
99
  </div>
110
100
  )
111
101
  }
@@ -0,0 +1,17 @@
1
+ import clsx from 'clsx'
2
+
3
+ import { Icon } from '@/src/lib/components'
4
+
5
+ function AIAvatarIcon({
6
+ className = 'rounded-full border-4 border-neutral-100 bg-neutral-200'
7
+ }: {
8
+ className?: string
9
+ }) {
10
+ return (
11
+ <div className={clsx('flex h-11 w-11 items-center justify-center', className)}>
12
+ <Icon name='ai-color' className='h-6 w-6' aria-label='AI avatar Icon' />
13
+ </div>
14
+ )
15
+ }
16
+
17
+ export default AIAvatarIcon
@@ -2,16 +2,21 @@ import clsx from 'clsx'
2
2
 
3
3
  import { Icon } from '@/src/lib/components'
4
4
 
5
- function AIAvatarIcon({
5
+ import styles from './styles.module.css'
6
+
7
+ export type AIAvatarProps = { className?: string }
8
+
9
+ function AIAvatar({
6
10
  className = 'rounded-full border-4 border-neutral-100 bg-neutral-200'
7
- }: {
8
- className?: string
9
- }) {
11
+ }: AIAvatarProps) {
10
12
  return (
11
- <div className={clsx('flex h-11 w-11 items-center justify-center', className)}>
12
- <Icon name='ai-color' className='h-6 w-6' aria-label='AI avatar Icon' />
13
- </div>
13
+ <figure
14
+ className={clsx('flex h-12 w-12 items-center justify-center rounded-full', styles.avatar)}>
15
+ <div className={clsx('flex h-11 w-11 items-center justify-center', className)}>
16
+ <Icon name='ai-color' className='h-6 w-6' aria-label='AI avatar Icon' />
17
+ </div>
18
+ </figure>
14
19
  )
15
20
  }
16
21
 
17
- export default AIAvatarIcon
22
+ export default AIAvatar
@@ -1 +1,3 @@
1
- export { default as AIAvatarIcon } from './ai-avatar'
1
+ export * from './ai-avatar'
2
+ export { default as AIAvatar } from './ai-avatar'
3
+ export { default as AIAvatarIcon } from './ai-avatar-icon'
@@ -0,0 +1,3 @@
1
+ .avatar {
2
+ background: linear-gradient(90deg, #44d0ff 0%, #b48eff 100%);
3
+ }
@@ -3,7 +3,12 @@ import { useRef } from 'react'
3
3
  import { isTextEmpty } from '@/src/lib/utils/is-text-empty'
4
4
  import { ChatInput, MessagesList, useChatInputValueAtom } from '@/src/modules/messages/components'
5
5
  import { useAllMessages, useSendTextMessage } from '@/src/modules/messages/hooks'
6
- import { useWidgetLoadingAtomValue, useWidgetTabsValueAtom } from '../../store'
6
+ import {
7
+ useWidgetLoadingAtomValue,
8
+ useWidgetSettingsAtomValue,
9
+ useWidgetTabsValueAtom
10
+ } from '../../store'
11
+ import { WidgetHeader } from '../header'
7
12
  import { PageLayout } from '../page-layout'
8
13
 
9
14
  function ChatPage() {
@@ -13,6 +18,7 @@ function ChatPage() {
13
18
  const { messagesQuery } = useAllMessages()
14
19
  const widgetLoading = useWidgetLoadingAtomValue()
15
20
  const [value, setValue] = useChatInputValueAtom()
21
+ const settings = useWidgetSettingsAtomValue()
16
22
 
17
23
  const handleSendMessage = () => {
18
24
  const text = chatInputRef.current?.value ?? ''
@@ -39,7 +45,16 @@ function ChatPage() {
39
45
  buttonDisabled={messagesQuery?.isLoading || !value.trim()}
40
46
  />
41
47
  }>
42
- <MessagesList />
48
+ <>
49
+ <div className='mt-4 px-6 py-4'>
50
+ <WidgetHeader
51
+ enabledButtons={['info', 'close']}
52
+ clubName={settings?.clubName}
53
+ tutorName={settings?.tutorName}
54
+ />
55
+ </div>
56
+ <MessagesList />
57
+ </>
43
58
  </PageLayout>
44
59
  )
45
60
  }
@@ -1,7 +1,7 @@
1
1
  import clsx from 'clsx'
2
2
  import { useTranslation } from 'react-i18next'
3
3
 
4
- import { AIAvatarIcon } from '../ai-avatar'
4
+ import { AIAvatar } from '../ai-avatar'
5
5
 
6
6
  import styles from './styles.module.css'
7
7
 
@@ -16,13 +16,7 @@ function GreetingsCard({ author, tutorName }: GreetingsCardProps) {
16
16
  return (
17
17
  <div className='flex flex-col items-center justify-center text-neutral-900'>
18
18
  <div className='flex flex-col items-center justify-center gap-4 text-center'>
19
- <figure
20
- className={clsx(
21
- 'flex h-12 w-12 items-center justify-center rounded-full',
22
- styles.avatar
23
- )}>
24
- <AIAvatarIcon />
25
- </figure>
19
+ <AIAvatar />
26
20
  <div className='flex flex-col gap-2'>
27
21
  <span className='text-base font-light'>
28
22
  {t('general.greetings.hello', { name: author })}
@@ -1,7 +1,3 @@
1
- .avatar {
2
- background: linear-gradient(90deg, #44d0ff 0%, #b48eff 100%);
3
- }
4
-
5
1
  .faceTxt {
6
2
  composes: gradientText from '../../../../config/styles/utilities/text-utilities.module.css';
7
3
  }
@@ -0,0 +1,46 @@
1
+ import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
2
+ import type { WidgetHeaderProps } from '../types'
3
+
4
+ class WidgetHeaderPropsBuilder implements WidgetHeaderProps {
5
+ enabledButtons: ValidIconNames[]
6
+ showContent?: boolean
7
+ showContentWithoutMeta?: boolean
8
+ clubName?: string
9
+ tutorName?: string
10
+
11
+ constructor() {
12
+ this.enabledButtons = ['close']
13
+ }
14
+
15
+ withEnabledButtons(enabledButtons: typeof this.enabledButtons) {
16
+ this.enabledButtons = enabledButtons
17
+
18
+ return this
19
+ }
20
+
21
+ withShowContent(showContent: typeof this.showContent) {
22
+ this.showContent = showContent
23
+
24
+ return this
25
+ }
26
+
27
+ withShowContentWithoutMeta(showContentWithoutMeta: typeof this.showContentWithoutMeta) {
28
+ this.showContentWithoutMeta = showContentWithoutMeta
29
+
30
+ return this
31
+ }
32
+
33
+ withClubName(clubName: typeof this.clubName) {
34
+ this.clubName = clubName
35
+
36
+ return this
37
+ }
38
+
39
+ withTutorName(tutorName: typeof this.tutorName) {
40
+ this.tutorName = tutorName
41
+
42
+ return this
43
+ }
44
+ }
45
+
46
+ export default WidgetHeaderPropsBuilder
@@ -0,0 +1,47 @@
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(/ai-color/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(/ai-color/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
+ })
@@ -0,0 +1,69 @@
1
+ import { Button, Icon } from '@/src/lib/components'
2
+ import { TutorWidgetEvents } from '../../events'
3
+ import { AIAvatar } from '../ai-avatar'
4
+
5
+ import type { WidgetHeaderContentProps, WidgetHeaderProps } from './types'
6
+
7
+ export function WidgetHeaderContent({ clubName, tutorName }: WidgetHeaderContentProps) {
8
+ return (
9
+ <div className='flex w-full gap-2'>
10
+ <AIAvatar />
11
+ <div className='flex flex-col'>
12
+ {tutorName && <h4 className='text-base'>{tutorName}</h4>}
13
+ {clubName && <p className='text-sm/normal text-neutral-600'>{clubName}</p>}
14
+ </div>
15
+ </div>
16
+ )
17
+ }
18
+
19
+ export function WidgetHeaderContentWithoutMeta({ name }: { name?: string }) {
20
+ return (
21
+ <div className='grid-areas-[a_b] grid grid-cols-[auto_1fr] items-center gap-1'>
22
+ <Button className='grid-area-[a]' aria-label='Arrow Left Icon'>
23
+ <Icon name='arrow-left' width={15} height={15} aria-hidden />
24
+ </Button>
25
+ <div className='grid-area-[b] flex justify-center'>
26
+ <span>{name}</span>
27
+ </div>
28
+ </div>
29
+ )
30
+ }
31
+
32
+ function WidgetHeader({
33
+ enabledButtons,
34
+ clubName,
35
+ tutorName,
36
+ showContentWithoutMeta,
37
+ showContent = true
38
+ }: WidgetHeaderProps) {
39
+ return (
40
+ <div className='grid-areas-[a_b] mt-0.5 grid grid-cols-[1fr_auto] items-center text-neutral-1000'>
41
+ <div className='grid-area-[a]'>
42
+ {showContent && !showContentWithoutMeta && (
43
+ <WidgetHeaderContent clubName={clubName} tutorName={tutorName} />
44
+ )}
45
+ {showContentWithoutMeta && !showContent && (
46
+ <WidgetHeaderContentWithoutMeta name={tutorName} />
47
+ )}
48
+ </div>
49
+ <div className='shrink-0'>
50
+ <div className='grid-area-[b] ml-auto flex max-w-max gap-3 text-neutral-700'>
51
+ <Button show={enabledButtons.includes('archive')} aria-label='Archive Icon'>
52
+ <Icon name='archive' className='h-4 w-4' aria-hidden />
53
+ </Button>
54
+ <Button show={enabledButtons.includes('info')} aria-label='Info Icon'>
55
+ <Icon name='info' className='h-4 w-4' aria-hidden />
56
+ </Button>
57
+ <Button
58
+ show={enabledButtons.includes('close')}
59
+ onClick={() => TutorWidgetEvents['c3po-app-widget-hide'].dispatch()}
60
+ aria-label='Close Icon'>
61
+ <Icon name='close' className='h-4 w-4' aria-hidden />
62
+ </Button>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ )
67
+ }
68
+
69
+ export default WidgetHeader
@@ -0,0 +1,2 @@
1
+ export * from './header'
2
+ export { default as WidgetHeader } from './header'
@@ -0,0 +1,9 @@
1
+ import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
2
+
3
+ export type WidgetHeaderContentProps = { clubName?: string; tutorName?: string }
4
+
5
+ export type WidgetHeaderProps = {
6
+ enabledButtons: ValidIconNames[]
7
+ showContent?: boolean
8
+ showContentWithoutMeta?: boolean
9
+ } & WidgetHeaderContentProps
@@ -2,6 +2,7 @@ export * from './ai-avatar'
2
2
  export * from './chat-page'
3
3
  export * from './container'
4
4
  export * from './greetings-card'
5
+ export * from './header'
5
6
  export * from './loading-page'
6
7
  export * from './page-layout'
7
8
  export * from './scroll-to-bottom-button'
@@ -9,6 +9,7 @@ import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/compone
9
9
  import { useSendTextMessage } from '@/src/modules/messages/hooks'
10
10
  import { useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
11
11
  import { GreetingsCard } from '../greetings-card'
12
+ import { WidgetHeader } from '../header'
12
13
  import { PageLayout } from '../page-layout'
13
14
 
14
15
  import styles from './styles.module.css'
@@ -57,12 +58,17 @@ function WidgetStarterPage() {
57
58
  />
58
59
  }>
59
60
  <div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
60
- <div
61
- className={clsx(
62
- 'grid-area-[a] flex min-h-0 flex-col justify-center px-5 py-4',
63
- styles.bg
64
- )}>
65
- <GreetingsCard author={settings?.author ?? ''} tutorName={settings?.tutorName ?? ''} />
61
+ <div className={clsx('grid-area-[a] flex min-h-0 flex-col px-5 py-4', styles.bg)}>
62
+ <WidgetHeader
63
+ enabledButtons={['archive', 'info', 'close']}
64
+ clubName={settings?.clubName}
65
+ tutorName={settings?.tutorName}
66
+ showContent={false}
67
+ />
68
+
69
+ <div className='my-auto'>
70
+ <GreetingsCard author={settings?.author ?? ''} tutorName={settings?.tutorName ?? ''} />
71
+ </div>
66
72
  </div>
67
73
  <div className='grid-area-[b] mx-5 my-6 flex flex-shrink-0 snap-x snap-mandatory gap-2 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
68
74
  <Button
@@ -3,6 +3,7 @@ import type { ITutorWidgetEvent } from './types'
3
3
  export const TutorWidgetEventTypes = {
4
4
  OPEN: 'c3po-app-widget-open',
5
5
  CLOSE: 'c3po-app-widget-close',
6
+ HIDE: 'c3po-app-widget-hide',
6
7
  LOADED: 'tutor-app-widget-loaded'
7
8
  } as const
8
9
 
@@ -41,6 +42,22 @@ const TutorWidgetEventsObject = {
41
42
  dispatch: () => window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.CLOSE))
42
43
  } as ITutorWidgetEvent<void>,
43
44
 
45
+ [TutorWidgetEventTypes.HIDE]: {
46
+ name: TutorWidgetEventTypes.HIDE,
47
+ handler: (callback) => {
48
+ const listener: EventListener = () => {
49
+ void callback()
50
+ }
51
+
52
+ window.addEventListener(TutorWidgetEventTypes.HIDE, listener)
53
+
54
+ return () => {
55
+ window.removeEventListener(TutorWidgetEventTypes.HIDE, listener)
56
+ }
57
+ },
58
+ dispatch: () => window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.HIDE))
59
+ } as ITutorWidgetEvent<void>,
60
+
44
61
  [TutorWidgetEventTypes.LOADED]: {
45
62
  name: TutorWidgetEventTypes.LOADED,
46
63
  handler: (callback: (payload: { isSuccess: boolean }) => void) => {