app-tutor-ai-consumer 1.18.2 → 1.20.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 (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/public/assets/svg/error-dark.svg +27 -0
  4. package/public/assets/svg/error-light.svg +27 -0
  5. package/src/config/tests/handlers.ts +12 -0
  6. package/src/development-bootstrap.tsx +5 -2
  7. package/src/index.tsx +3 -0
  8. package/src/lib/components/button/button.tsx +105 -14
  9. package/src/lib/components/button/styles.module.css +9 -0
  10. package/src/lib/components/errors/generic/generic-error.tsx +58 -3
  11. package/src/lib/components/icons/arrow-up.svg +5 -0
  12. package/src/lib/components/icons/copy.svg +5 -0
  13. package/src/lib/components/icons/icon-names.d.ts +3 -0
  14. package/src/lib/components/icons/like.svg +5 -0
  15. package/src/modules/messages/components/message-actions/index.ts +2 -0
  16. package/src/modules/messages/components/message-actions/message-actions.tsx +49 -0
  17. package/src/modules/messages/components/message-item/message-item.tsx +21 -5
  18. package/src/modules/messages/components/message-item-error/message-item-error.tsx +16 -9
  19. package/src/modules/messages/components/message-skeleton/message-skeleton.tsx +1 -4
  20. package/src/modules/messages/components/messages-container/index.ts +2 -0
  21. package/src/modules/messages/components/messages-container/messages-container.tsx +91 -0
  22. package/src/modules/messages/components/messages-list/messages-list.tsx +9 -82
  23. package/src/modules/messages/constants.ts +5 -0
  24. package/src/modules/messages/events.ts +12 -4
  25. package/src/modules/messages/hooks/index.ts +1 -0
  26. package/src/modules/messages/hooks/use-all-messages/use-all-messages.tsx +1 -2
  27. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +18 -19
  28. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +41 -35
  29. package/src/modules/messages/hooks/use-scroller/index.ts +2 -0
  30. package/src/modules/messages/hooks/use-scroller/use-scroller.tsx +50 -0
  31. package/src/modules/messages/hooks/use-send-text-message/use-send-text-message.tsx +31 -2
  32. package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +47 -64
  33. package/src/modules/messages/store/index.ts +1 -0
  34. package/src/modules/messages/store/messages-max-count.atom.ts +13 -0
  35. package/src/modules/messages/utils/index.ts +2 -0
  36. package/src/modules/messages/utils/set-messages-cache/index.ts +1 -0
  37. package/src/modules/messages/utils/set-messages-cache/utils.ts +53 -0
  38. package/src/modules/widget/components/chat-page/chat-page.spec.tsx +23 -7
  39. package/src/modules/widget/components/chat-page/chat-page.tsx +70 -14
  40. package/src/modules/widget/components/greetings-card/greetings-card.tsx +1 -1
  41. package/src/modules/widget/components/header/header.tsx +6 -4
  42. package/src/modules/widget/components/starter-page/starter-page.spec.tsx +4 -1
  43. package/src/modules/widget/components/starter-page/starter-page.tsx +31 -5
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [1.20.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.19.0...v1.20.0) (2025-07-29)
2
+
3
+ ### Features
4
+
5
+ - add generic error page ([2ac19d0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/2ac19d0d033731508580778dcf679fb9c71ca24f))
6
+
7
+ # [1.19.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.18.2...v1.19.0) (2025-07-29)
8
+
9
+ ### Features
10
+
11
+ - add message actions ([3a8d5dc](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/3a8d5dcd1ae75ce40b7cbf6edf8629da921c9308))
12
+
1
13
  ## [1.18.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.18.1...v1.18.2) (2025-07-28)
2
14
 
3
15
  ## [1.18.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.18.0...v1.18.1) (2025-07-24)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.18.2",
3
+ "version": "1.20.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -0,0 +1,27 @@
1
+ <svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <circle opacity="0.5" cx="80" cy="80" r="80" fill="#282C2F"/>
3
+ <g filter="url(#filter0_d_22198_69476)">
4
+ <path d="M32.8 47.9999C32.8 47.1162 33.5164 46.3999 34.4 46.3999H125.6C126.484 46.3999 127.2 47.1162 127.2 47.9999V55.9999H32.8V47.9999Z" fill="#464B52"/>
5
+ <path d="M32.8 56H127.2V112C127.2 112.884 126.484 113.6 125.6 113.6H34.4001C33.5164 113.6 32.8 112.884 32.8 112V56Z" fill="white"/>
6
+ <rect width="11.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 74.3999 91.2)" fill="#C9CED4"/>
7
+ <rect width="4" height="4" rx="2" transform="matrix(1 0 0 -1 68 81.6001)" fill="#C9CED4"/>
8
+ <rect width="4" height="4" rx="2" transform="matrix(1 0 0 -1 88 81.6001)" fill="#C9CED4"/>
9
+ </g>
10
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 36 52.8)" fill="#E37570"/>
11
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 41.6001 52.8)" fill="#EFBA0F"/>
12
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 47.2 52.8)" fill="#4ACC82"/>
13
+ <path d="M141.143 46.1714C141.143 53.9981 134.798 60.3429 126.971 60.3429C119.145 60.3429 112.8 53.9981 112.8 46.1714C112.8 38.3447 119.145 32 126.971 32C134.798 32 141.143 38.3447 141.143 46.1714Z" fill="#5981E3"/>
14
+ <path d="M128.297 49.5064H125.566L124.993 38.562H128.87L128.297 49.5064ZM124.947 53.3381C124.947 52.6345 125.138 52.145 125.52 51.8697C125.903 51.579 126.369 51.4337 126.92 51.4337C127.455 51.4337 127.914 51.579 128.297 51.8697C128.679 52.145 128.87 52.6345 128.87 53.3381C128.87 54.0111 128.679 54.5006 128.297 54.8065C127.914 55.0971 127.455 55.2425 126.92 55.2425C126.369 55.2425 125.903 55.0971 125.52 54.8065C125.138 54.5006 124.947 54.0111 124.947 53.3381Z" fill="white"/>
15
+ <defs>
16
+ <filter id="filter0_d_22198_69476" x="24.8" y="44.7999" width="110.4" height="83.2002" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
17
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
18
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
19
+ <feMorphology radius="4.8" operator="erode" in="SourceAlpha" result="effect1_dropShadow_22198_69476"/>
20
+ <feOffset dy="6.4"/>
21
+ <feGaussianBlur stdDeviation="6.4"/>
22
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
23
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_22198_69476"/>
24
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_22198_69476" result="shape"/>
25
+ </filter>
26
+ </defs>
27
+ </svg>
@@ -0,0 +1,27 @@
1
+ <svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <circle opacity="0.5" cx="80" cy="80" r="80" fill="#E6E9ED"/>
3
+ <g filter="url(#filter0_d_22198_92633)">
4
+ <path d="M32.8 47.9999C32.8 47.1162 33.5164 46.3999 34.4 46.3999H125.6C126.484 46.3999 127.2 47.1162 127.2 47.9999V55.9999H32.8V47.9999Z" fill="#464B52"/>
5
+ <path d="M32.8 56H127.2V112C127.2 112.884 126.484 113.6 125.6 113.6H34.4001C33.5164 113.6 32.8 112.884 32.8 112V56Z" fill="white"/>
6
+ <rect width="11.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 74.3999 91.2002)" fill="#C9CED4"/>
7
+ <rect width="4" height="4" rx="2" transform="matrix(1 0 0 -1 68 81.6001)" fill="#C9CED4"/>
8
+ <rect width="4" height="4" rx="2" transform="matrix(1 0 0 -1 88 81.6001)" fill="#C9CED4"/>
9
+ </g>
10
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 36 52.7998)" fill="#E37570"/>
11
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 41.6001 52.7998)" fill="#EFBA0F"/>
12
+ <rect width="3.2" height="3.2" rx="1.6" transform="matrix(1 0 0 -1 47.2 52.7998)" fill="#4ACC82"/>
13
+ <path d="M141.143 46.1714C141.143 53.9981 134.798 60.3429 126.971 60.3429C119.145 60.3429 112.8 53.9981 112.8 46.1714C112.8 38.3447 119.145 32 126.971 32C134.798 32 141.143 38.3447 141.143 46.1714Z" fill="#5981E3"/>
14
+ <path d="M128.297 49.5064H125.566L124.993 38.562H128.87L128.297 49.5064ZM124.947 53.3381C124.947 52.6345 125.138 52.145 125.52 51.8697C125.903 51.579 126.369 51.4337 126.92 51.4337C127.455 51.4337 127.914 51.579 128.297 51.8697C128.679 52.145 128.87 52.6345 128.87 53.3381C128.87 54.0111 128.679 54.5006 128.297 54.8065C127.914 55.0971 127.455 55.2425 126.92 55.2425C126.369 55.2425 125.903 55.0971 125.52 54.8065C125.138 54.5006 124.947 54.0111 124.947 53.3381Z" fill="white"/>
15
+ <defs>
16
+ <filter id="filter0_d_22198_92633" x="24.8" y="44.7999" width="110.4" height="83.2002" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
17
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
18
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
19
+ <feMorphology radius="4.8" operator="erode" in="SourceAlpha" result="effect1_dropShadow_22198_92633"/>
20
+ <feOffset dy="6.4"/>
21
+ <feGaussianBlur stdDeviation="6.4"/>
22
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
23
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_22198_92633"/>
24
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_22198_92633" result="shape"/>
25
+ </filter>
26
+ </defs>
27
+ </svg>
@@ -1,5 +1,8 @@
1
1
  import { http, HttpResponse } from 'msw'
2
2
 
3
+ import type { IMessageWithSenderData } from '@/src/modules/messages'
4
+ import { MessagesEndpoints, MSG_MAX_COUNT } from '@/src/modules/messages'
5
+ import IMessageWithSenderDataMock from '@/src/modules/messages/__tests__/imessage-with-sender-data.mock'
3
6
  import { ProfileEndpoints } from '@/src/modules/profile'
4
7
  import ProfileAPIPropsBuilder from '@/src/modules/profile/__tests__/profile-api-props.builder'
5
8
 
@@ -12,5 +15,14 @@ export const handlers = [
12
15
  }),
13
16
  http.all(ProfileEndpoints.getProfile(), () => {
14
17
  return HttpResponse.json(new ProfileAPIPropsBuilder())
18
+ }),
19
+ http.all(MessagesEndpoints.getAll(':conversationId'), ({ request }) => {
20
+ const limit = Number(new URL(request.url)?.searchParams?.get?.('limit'))
21
+
22
+ return HttpResponse.json(
23
+ new IMessageWithSenderDataMock().getMany(
24
+ isNaN(limit) ? MSG_MAX_COUNT : limit
25
+ ) as IMessageWithSenderData[]
26
+ )
15
27
  })
16
28
  ]
@@ -1,10 +1,12 @@
1
1
  import './index'
2
2
 
3
+ import Chance from 'chance'
3
4
  import { v4 } from 'uuid'
4
5
 
5
6
  import { LANGUAGES } from './config/i18n'
6
7
  import { devMode } from './lib/utils'
7
8
 
9
+ const chance = new Chance()
8
10
  const rootId = 'app-tutor-ai-widget'
9
11
 
10
12
  if (devMode) {
@@ -36,8 +38,9 @@ if (devMode) {
36
38
  productName: 'Curso de Assinatura',
37
39
  productId: 4266504,
38
40
  sessionId: v4(),
39
- config: {
40
- theme: 'dark'
41
+ user: {
42
+ id: v4(),
43
+ name: chance.name()
41
44
  }
42
45
  })
43
46
  })()
package/src/index.tsx CHANGED
@@ -37,6 +37,9 @@ window.startChatWidget = async (
37
37
  }
38
38
 
39
39
  const rootElement = document.getElementById(elementId) as HTMLElement
40
+
41
+ if (!rootElement) return
42
+
40
43
  rootElement.setAttribute('id', 'hotmart-app-tutor-ai-consumer-root')
41
44
  const theme = (rootElement.getAttribute('data-theme') ?? 'dark') as Theme
42
45
  const root = createRoot(rootElement)
@@ -3,6 +3,8 @@ import type { ButtonHTMLAttributes, PropsWithChildren } from 'react'
3
3
 
4
4
  import { Spinner } from '../spinner'
5
5
 
6
+ import styles from './styles.module.css'
7
+
6
8
  export type ButtonProps = PropsWithChildren<
7
9
  ButtonHTMLAttributes<HTMLButtonElement> & {
8
10
  variant?: 'brand' | 'secondary' | 'primary' | 'tertiary' | 'gradient-outline'
@@ -24,8 +26,7 @@ function Button({
24
26
  const defaultBorder = 'border border-transparent'
25
27
  const defaultPadding = 'px-4 py-2'
26
28
  const disabledClasses = 'cursor-not-allowed'
27
-
28
- const content = loading ? <Spinner className='h-full w-full text-current' /> : children
29
+ const gridClasses = 'grid [grid-template-areas:stack]'
29
30
 
30
31
  if (!show) return null
31
32
 
@@ -34,6 +35,7 @@ function Button({
34
35
  return (
35
36
  <button
36
37
  className={clsx(
38
+ gridClasses,
37
39
  defaultClasses,
38
40
  '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)]',
39
41
  className
@@ -43,15 +45,29 @@ function Button({
43
45
  aria-busy={loading}>
44
46
  <span
45
47
  data-label='text-content'
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'>
47
- {content}
48
+ className={clsx(
49
+ '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',
50
+ 'flex flex-nowrap gap-2 [grid-area:stack]',
51
+ {
52
+ visible: !loading,
53
+ invisible: loading
54
+ }
55
+ )}>
56
+ {children}
48
57
  </span>
58
+ <Spinner
59
+ className={clsx('mx-auto my-auto h-[1em] w-[1em] text-current', '[grid-area:stack]', {
60
+ visible: loading,
61
+ invisible: !loading
62
+ })}
63
+ />
49
64
  </button>
50
65
  )
51
66
  case 'primary':
52
67
  return (
53
68
  <button
54
69
  className={clsx(
70
+ gridClasses,
55
71
  defaultPadding,
56
72
  defaultClasses,
57
73
  defaultBorder,
@@ -61,13 +77,27 @@ function Button({
61
77
  {...props}
62
78
  disabled={props.disabled || loading}
63
79
  aria-busy={loading}>
64
- {content}
80
+ <span
81
+ data-label='text-content'
82
+ className={clsx('flex flex-nowrap gap-2 [grid-area:stack]', {
83
+ visible: !loading,
84
+ invisible: loading
85
+ })}>
86
+ {children}
87
+ </span>
88
+ <Spinner
89
+ className={clsx('mx-auto my-auto h-[1em] w-[1em] text-current', '[grid-area:stack]', {
90
+ visible: loading,
91
+ invisible: !loading
92
+ })}
93
+ />
65
94
  </button>
66
95
  )
67
96
  case 'secondary':
68
97
  return (
69
98
  <button
70
99
  className={clsx(
100
+ gridClasses,
71
101
  defaultPadding,
72
102
  defaultClasses,
73
103
  defaultBorder,
@@ -77,29 +107,62 @@ function Button({
77
107
  {...props}
78
108
  disabled={props.disabled || loading}
79
109
  aria-busy={loading}>
80
- {content}
110
+ <span
111
+ data-label='text-content'
112
+ className={clsx('flex flex-nowrap gap-2 [grid-area:stack]', {
113
+ visible: !loading,
114
+ invisible: loading
115
+ })}>
116
+ {children}
117
+ </span>
118
+ <Spinner
119
+ className={clsx('mx-auto my-auto h-[1em] w-[1em] text-current', '[grid-area:stack]', {
120
+ visible: loading,
121
+ invisible: !loading
122
+ })}
123
+ />
81
124
  </button>
82
125
  )
83
126
  case 'tertiary':
84
127
  return (
85
128
  <button
86
129
  className={clsx(
87
- defaultPadding,
88
- defaultClasses,
89
- defaultBorder,
90
- 'text-1000 bg-transparent hover:bg-neutral-900',
130
+ `relative grid place-items-center rounded bg-transparent`,
131
+ styles.tertiary,
91
132
  className
92
133
  )}
93
134
  {...props}
94
135
  disabled={props.disabled || loading}
95
136
  aria-busy={loading}>
96
- {content}
137
+ <span
138
+ data-label='text-content'
139
+ className={clsx(`col-start-1 row-start-1 flex flex-nowrap gap-2`, {
140
+ invisible: loading,
141
+ visible: !loading
142
+ })}>
143
+ {children}
144
+ </span>
145
+
146
+ {loading && (
147
+ <div className='col-start-1 row-start-1'>
148
+ <Spinner
149
+ className={clsx(
150
+ 'col-start-1 row-start-1 mx-auto my-auto h-[1em] w-[1em] text-current',
151
+ {
152
+ visible: loading,
153
+ invisible: !loading
154
+ }
155
+ )}
156
+ />
157
+ </div>
158
+ )}
97
159
  </button>
98
160
  )
99
161
  case 'brand':
100
162
  return (
101
163
  <button
102
164
  className={clsx(
165
+ gridClasses,
103
166
  defaultPadding,
104
167
  defaultClasses,
105
168
  defaultBorder,
@@ -109,25 +172,53 @@ function Button({
109
172
  {...props}
110
173
  disabled={props.disabled || loading}
111
174
  aria-busy={loading}>
112
- {content}
175
+ <span
176
+ data-label='text-content'
177
+ className={clsx('flex flex-nowrap gap-2 [grid-area:stack]', {
178
+ visible: !loading,
179
+ invisible: loading
180
+ })}>
181
+ {children}
182
+ </span>
183
+ <Spinner
184
+ className={clsx('mx-auto my-auto h-[1em] w-[1em] text-current', '[grid-area:stack]', {
185
+ visible: loading,
186
+ invisible: !loading
187
+ })}
188
+ />
113
189
  </button>
114
190
  )
115
191
  default:
116
192
  return (
117
193
  <button
118
194
  className={clsx(
119
- 'rounded-full p-2 outline-none transition-colors duration-300 ease-in',
195
+ gridClasses,
196
+ 'rounded-full outline-none transition-colors duration-300 ease-in',
120
197
  {
121
198
  'cursor-pointer ring-primary-500 hover:bg-neutral-300 focus:bg-neutral-300 focus-visible:ring-2':
122
199
  !props.disabled,
123
200
  [disabledClasses]: props.disabled
124
201
  },
202
+ styles.defaultButton,
125
203
  className
126
204
  )}
127
205
  {...props}
128
206
  disabled={props.disabled || loading}
129
207
  aria-busy={loading}>
130
- {content}
208
+ <span
209
+ data-label='text-content'
210
+ className={clsx('flex flex-nowrap gap-2 [grid-area:stack]', {
211
+ visible: !loading,
212
+ invisible: loading
213
+ })}>
214
+ {children}
215
+ </span>
216
+ <Spinner
217
+ className={clsx('mx-auto my-auto h-[1em] w-[1em] text-current', '[grid-area:stack]', {
218
+ visible: loading,
219
+ invisible: !loading
220
+ })}
221
+ />
131
222
  </button>
132
223
  )
133
224
  }
@@ -0,0 +1,9 @@
1
+ .tertiary {
2
+ &:hover {
3
+ background-color: hsl(from currentColor h s calc(l - 30));
4
+ }
5
+ }
6
+
7
+ .defaultButton {
8
+ padding: var(--custom-btn-padding, var(--hc-size-spacing-2));
9
+ }
@@ -1,12 +1,67 @@
1
+ import clsx from 'clsx'
1
2
  import { useTranslation } from 'react-i18next'
2
3
 
4
+ import ErrorDarkSVG from '@/public/assets/svg/error-dark.svg?url'
5
+ import ErrorLightSVG from '@/public/assets/svg/error-light.svg?url'
6
+ import { Button } from '@/src/lib/components'
7
+ import { PageLayout, TutorWidgetEvents, useWidgetSettingsAtom } from '@/src/modules/widget'
8
+ import { WidgetHeader } from '@/src/modules/widget/components/header'
9
+
3
10
  function GenericError() {
4
11
  const { t } = useTranslation()
12
+ const [settings] = useWidgetSettingsAtom()
13
+ const isDarkMode = settings?.config?.theme === 'dark'
5
14
 
6
15
  return (
7
- <div>
8
- <h4 className='text-xl font-bold'>{t('generic.error')}</h4>
9
- </div>
16
+ <PageLayout className='p-5'>
17
+ <WidgetHeader enabledButtons={['close']} showContent={false} />
18
+
19
+ <div className='flex h-full flex-col items-center justify-center p-10'>
20
+ <div className='mb-8 flex flex-col items-center gap-1 text-center'>
21
+ <img
22
+ alt={t('generic_error.image_alt')}
23
+ className='mb-4'
24
+ src={isDarkMode ? ErrorDarkSVG : ErrorLightSVG}
25
+ width={200}
26
+ height={200}
27
+ />
28
+
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>
44
+ </div>
45
+
46
+ <div className='flex w-full flex-col gap-4'>
47
+ <Button
48
+ variant='brand'
49
+ className='w-full rounded-lg py-2'
50
+ onClick={() => window.location.reload()}
51
+ aria-label='Retry Button'>
52
+ <span className='font-light'>{t('general.buttons.try_again')}</span>
53
+ </Button>
54
+
55
+ <Button
56
+ variant='secondary'
57
+ className='w-full rounded-lg py-2'
58
+ onClick={() => TutorWidgetEvents['c3po-app-widget-hide'].dispatch()}
59
+ aria-label='Close Button'>
60
+ <span className='font-light'>{t('general.buttons.close')}</span>
61
+ </Button>
62
+ </div>
63
+ </div>
64
+ </PageLayout>
10
65
  )
11
66
  }
12
67
 
@@ -0,0 +1,5 @@
1
+ <svg viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path
3
+ d="M11.863 6.04013C11.7692 6.16499 11.6441 6.22743 11.519 6.22743C11.3939 6.22743 11.2689 6.16499 11.175 6.07135L6.51594 1.70123L6.51594 13.4693C6.51594 13.7503 6.26579 14 6.01563 14C5.76548 14 5.51533 13.7503 5.51533 13.4693L5.51533 1.70123L0.82495 6.07135C0.637335 6.25864 0.324643 6.25864 0.137028 6.04013C-0.0505867 5.82163 -0.0505867 5.50948 0.168298 5.32219L5.67167 0.140468C5.85929 -0.0468227 6.14071 -0.0468227 6.32833 0.140468L11.8317 5.32219C12.0506 5.50948 12.0506 5.82163 11.863 6.04013Z"
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="M7.42886 11.1663C7.19494 11.1663 7.01301 11.3747 7.01301 11.583V11.9997C7.01301 12.4684 6.62314 12.833 6.1813 12.833H2.02275C1.55492 12.833 1.19104 12.4684 1.19104 11.9997V5.33301C1.19104 4.8903 1.55492 4.49967 2.02275 4.49967H4.93373C5.14166 4.49967 5.34959 4.31738 5.34959 4.08301C5.34959 3.87467 5.14166 3.66634 4.93373 3.66634H1.99676C1.08708 3.66634 0.333344 4.42155 0.333344 5.33301L0.359334 11.9997C0.359334 12.9372 1.08708 13.6663 2.02275 13.6663H6.1813C7.09098 13.6663 7.84471 12.9372 7.84471 11.9997V11.583C7.84471 11.3747 7.63679 11.1663 7.42886 11.1663ZM13.4068 2.59863L11.4055 0.593424C11.2495 0.437174 11.0416 0.333008 10.8077 0.333008H7.84471C6.90904 0.333008 6.1813 1.08822 6.1813 1.99967V8.66634C6.1813 9.60384 6.90904 10.333 7.84471 10.333H12.0033C12.9129 10.333 13.6667 9.60384 13.6667 8.66634V3.19759C13.6667 2.96322 13.5627 2.75488 13.4068 2.59863ZM11.1715 1.53092L12.4711 2.83301H11.1715V1.53092ZM12.835 8.66634C12.835 9.13509 12.4451 9.49967 12.0033 9.49967H7.84471C7.37688 9.49967 7.01301 9.13509 7.01301 8.66634V1.99967C7.01301 1.55697 7.37688 1.16634 7.84471 1.16634H10.3398V2.83301C10.3398 3.30176 10.7037 3.66634 11.1715 3.66634H12.835V8.66634Z"
4
+ fill="currentColor" />
5
+ </svg>
@@ -4,9 +4,12 @@ export type ValidIconNames =
4
4
  | 'archive'
5
5
  | 'arrow-down'
6
6
  | 'arrow-left'
7
+ | 'arrow-up'
7
8
  | 'chevron-down'
9
+ | 'copy'
8
10
  | 'close'
9
11
  | 'info'
12
+ | 'like'
10
13
  | 'send'
11
14
  | 'stop'
12
15
  | 'warning'
@@ -0,0 +1,5 @@
1
+ <svg viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path
3
+ d="M2.62501 4.28571H1.37501C0.776052 4.28571 0.333344 4.76786 0.333344 5.35714V10.9286C0.333344 11.5446 0.776052 12 1.37501 12H2.62501C3.19793 12 3.66668 11.5446 3.66668 10.9286V5.35714C3.66668 4.76786 3.19793 4.28571 2.62501 4.28571ZM2.83334 10.9286C2.83334 11.0625 2.72918 11.1429 2.62501 11.1429H1.37501C1.2448 11.1429 1.16668 11.0625 1.16668 10.9286V5.35714C1.16668 5.25 1.2448 5.14286 1.37501 5.14286H2.62501C2.72918 5.14286 2.83334 5.25 2.83334 5.35714V10.9286ZM13.6667 5.08929C13.6667 4.17857 12.9375 3.42857 12.0521 3.42857H9.39584C9.6823 2.70536 9.86459 2.00893 9.86459 1.63393C9.86459 0.830357 9.26563 0 8.25001 0C7.1823 0 6.94793 0.803571 6.71355 1.47321C6.01043 3.75 4.50001 4.09821 4.50001 4.71429C4.50001 4.98214 4.6823 5.14286 4.91668 5.14286C5.02084 5.14286 5.12501 5.11607 5.20313 5.03571C6.5573 3.61607 6.94793 3.53571 7.52084 1.74107C7.75522 0.991071 7.83334 0.857143 8.25001 0.857143C8.79688 0.857143 9.03126 1.3125 9.03126 1.63393C9.03126 1.90179 8.79688 2.8125 8.35418 3.66964C8.30209 3.72321 8.30209 3.80357 8.30209 3.85714C8.30209 4.125 8.51043 4.28571 8.71876 4.28571H12.0521C12.4688 4.28571 12.8333 4.66071 12.8333 5.08929C12.8333 5.49107 12.4948 5.83929 12.1042 5.86607C11.8958 5.89286 11.7136 6.08036 11.7136 6.29464C11.7136 6.61607 12.0261 6.64286 12.0261 7.125C12.0261 7.5 11.7656 7.82143 11.4011 7.90179C11.2448 7.92857 11.0625 8.0625 11.0625 8.30357C11.0625 8.54464 11.2448 8.59821 11.2448 8.94643C11.2448 9.77679 10.4115 9.53571 10.4115 10.0982C10.4115 10.2054 10.4636 10.2321 10.4636 10.3661C10.4636 10.7946 10.099 11.1429 9.6823 11.1429H8.22397C6.08855 11.1429 5.41147 9.42857 4.91668 9.42857C4.6823 9.42857 4.50001 9.64286 4.50001 9.85714C4.47397 10.3125 6.16668 12 8.22397 12H9.6823C10.5677 12 11.2969 11.2768 11.2969 10.3661C11.7656 10.0714 12.0781 9.53571 12.0781 8.94643C12.0781 8.8125 12.0521 8.67857 12.0261 8.57143C12.5208 8.27679 12.8594 7.74107 12.8594 7.125C12.8594 6.9375 12.8333 6.72321 12.7552 6.5625C13.3021 6.29464 13.6667 5.73214 13.6667 5.08929Z"
4
+ fill="currentColor" />
5
+ </svg>
@@ -0,0 +1,2 @@
1
+ export * from './message-actions'
2
+ export { default as MessageActions } from './message-actions'
@@ -0,0 +1,49 @@
1
+ import clsx from 'clsx'
2
+ import dayjs from 'dayjs'
3
+ import type { CSSProperties } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+
6
+ import { Button, Icon } from '@/src/lib/components'
7
+ import type { ParsedMessage } from '../../types'
8
+
9
+ export type MessageActionsProps = {
10
+ message: ParsedMessage
11
+ className?: string
12
+ showActions?: boolean
13
+ }
14
+
15
+ function MessageActions({ message, className, showActions = false }: MessageActionsProps) {
16
+ const { t } = useTranslation()
17
+ const copyToClipboard = () => {
18
+ if (!message.text) return
19
+
20
+ navigator.clipboard.writeText(message.text).catch((err) => {
21
+ console.error('Failed to copy text: ', err)
22
+ })
23
+ }
24
+
25
+ return (
26
+ <div className={className}>
27
+ <span className='text-xs tracking-wide'>
28
+ {dayjs(message.timestamp).format('DD/MM [•] LT')}
29
+ </span>
30
+ <div
31
+ style={{ '--custom-btn-padding': '0.5rem' } as CSSProperties}
32
+ className={clsx('flex flex-nowrap gap-2 text-neutral-600', {
33
+ hidden: !showActions
34
+ })}>
35
+ <Button aria-label={t('general.buttons.like')}>
36
+ <Icon name='like' className='h-3 w-3.5' />
37
+ </Button>
38
+ <Button className='rotate-180 scale-x-[-1]' aria-label={t('general.buttons.dislike')}>
39
+ <Icon name='like' className='h-3 w-3.5' />
40
+ </Button>
41
+ <Button onClick={copyToClipboard} aria-label={t('general.buttons.copy')}>
42
+ <Icon name='copy' className='h-3 w-3.5' />
43
+ </Button>
44
+ </div>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export default MessageActions
@@ -3,6 +3,7 @@ import type { Components } from 'react-markdown'
3
3
 
4
4
  import { MarkdownRenderer } from '@/src/lib/components'
5
5
  import type { ParsedMessage } from '../../types'
6
+ import { MessageActions } from '../message-actions'
6
7
  import { MessageImg } from '../message-img'
7
8
 
8
9
  const imgComponent: Components['img'] = ({ src }) => {
@@ -10,17 +11,32 @@ const imgComponent: Components['img'] = ({ src }) => {
10
11
  }
11
12
 
12
13
  function MessageItem({ message }: { message: ParsedMessage }) {
14
+ const messageFromUser = message.metadata.author === 'user'
15
+ const messageFromAi = message.metadata.author === 'ai'
16
+
13
17
  return (
14
18
  <div
15
- data-test='messages-item'
16
19
  className={clsx(
17
- 'max-w-[min(80%,52rem)] overflow-x-hidden rounded-lg px-3 text-sm/normal text-neutral-900',
20
+ 'flex max-w-[min(90%,52rem)] flex-col items-end gap-2 text-sm/normal text-neutral-900',
18
21
  {
19
- 'self-end bg-neutral-200': message.metadata.author === 'user',
20
- 'border border-neutral-300 bg-neutral-100': message.metadata.author === 'ai'
22
+ 'self-end': messageFromUser
21
23
  }
22
24
  )}>
23
- <MarkdownRenderer content={message?.text ?? message?.name} imgComponent={imgComponent} />
25
+ <div
26
+ data-test='messages-item'
27
+ className={clsx('w-full overflow-x-hidden rounded-lg px-3', {
28
+ 'bg-neutral-200': messageFromUser,
29
+ 'border border-neutral-300 bg-neutral-100': messageFromAi
30
+ })}>
31
+ <MarkdownRenderer content={message?.text ?? message?.name} imgComponent={imgComponent} />
32
+ </div>
33
+ <MessageActions
34
+ className={clsx('flex items-center justify-between gap-2', {
35
+ 'w-full': messageFromAi
36
+ })}
37
+ message={message}
38
+ showActions={messageFromAi}
39
+ />
24
40
  </div>
25
41
  )
26
42
  }
@@ -1,4 +1,8 @@
1
- import type { MouseEventHandler, ReactNode } from 'react'
1
+ import { type MouseEventHandler, type ReactNode, useEffect } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ import { Button } from '@/src/lib/components'
5
+ import { devMode } from '@/src/lib/utils'
2
6
 
3
7
  export type MessageItemErrorProps = {
4
8
  message?: ReactNode
@@ -6,18 +10,21 @@ export type MessageItemErrorProps = {
6
10
  show?: boolean
7
11
  }
8
12
 
9
- // TODO: [PLACEHOLDER] Refactor using the PD choice for Error Handling
10
13
  function MessageItemError({ message, retry, show = true }: MessageItemErrorProps) {
14
+ const { t } = useTranslation()
15
+
16
+ useEffect(() => {
17
+ if (show && devMode) console.log(message)
18
+ }, [message, show])
19
+
11
20
  if (!show) return null
12
21
 
13
22
  return (
14
- <div className='rounded bg-red-900/20 p-4 text-center text-red-400'>
15
- {message}
16
- <button
17
- onClick={retry}
18
- className='ml-2 cursor-pointer rounded bg-danger-600 px-3 py-1 text-sm text-danger-100 transition-colors hover:bg-danger-700'>
19
- Retry
20
- </button>
23
+ <div className='mt-2 flex justify-start gap-2 rounded border border-danger-500 bg-neutral-200 p-4 text-neutral-800'>
24
+ <span>{t('chat_page.error.loading.content')}</span>
25
+ <Button variant='tertiary' className='px-4 text-sm/relaxed text-danger-400' onClick={retry}>
26
+ {t('chat_page.error.loading.action')}
27
+ </Button>
21
28
  </div>
22
29
  )
23
30
  }
@@ -4,10 +4,7 @@ import { AIAvatarIcon } from '@/src/modules/widget'
4
4
 
5
5
  const MessageSkeleton = forwardRef<HTMLDivElement>((_, ref) => {
6
6
  return (
7
- <div
8
- ref={ref}
9
- className='flex max-w-[86%] flex-col items-start gap-2'
10
- aria-label='Loading Component'>
7
+ <div ref={ref} className='flex flex-col items-start gap-2' aria-label='Loading Component'>
11
8
  <AIAvatarIcon className='rounded-lg bg-ai-chat-response' />
12
9
  <div className='flex w-full flex-col items-start gap-2'>
13
10
  <div className='h-3 w-full animate-pulse rounded-full bg-neutral-200 transition-colors delay-0' />
@@ -0,0 +1,2 @@
1
+ export * from './messages-container'
2
+ export { default as MessagesContainer } from './messages-container'