app-tutor-ai-consumer 1.18.2 → 1.19.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.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/config/tests/handlers.ts +12 -0
- package/src/development-bootstrap.tsx +5 -2
- package/src/index.tsx +3 -0
- package/src/lib/components/button/button.tsx +105 -14
- package/src/lib/components/button/styles.module.css +9 -0
- package/src/lib/components/icons/arrow-up.svg +5 -0
- package/src/lib/components/icons/copy.svg +5 -0
- package/src/lib/components/icons/icon-names.d.ts +3 -0
- package/src/lib/components/icons/like.svg +5 -0
- package/src/modules/messages/components/message-actions/index.ts +2 -0
- package/src/modules/messages/components/message-actions/message-actions.tsx +49 -0
- package/src/modules/messages/components/message-item/message-item.tsx +21 -5
- package/src/modules/messages/components/message-item-error/message-item-error.tsx +16 -9
- package/src/modules/messages/components/message-skeleton/message-skeleton.tsx +1 -4
- package/src/modules/messages/components/messages-container/index.ts +2 -0
- package/src/modules/messages/components/messages-container/messages-container.tsx +91 -0
- package/src/modules/messages/components/messages-list/messages-list.tsx +9 -82
- package/src/modules/messages/constants.ts +5 -0
- package/src/modules/messages/events.ts +12 -4
- package/src/modules/messages/hooks/index.ts +1 -0
- package/src/modules/messages/hooks/use-all-messages/use-all-messages.tsx +1 -2
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +18 -19
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +41 -35
- package/src/modules/messages/hooks/use-scroller/index.ts +2 -0
- package/src/modules/messages/hooks/use-scroller/use-scroller.tsx +50 -0
- package/src/modules/messages/hooks/use-send-text-message/use-send-text-message.tsx +31 -2
- package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +47 -64
- package/src/modules/messages/store/index.ts +1 -0
- package/src/modules/messages/store/messages-max-count.atom.ts +13 -0
- package/src/modules/messages/utils/index.ts +2 -0
- package/src/modules/messages/utils/set-messages-cache/index.ts +1 -0
- package/src/modules/messages/utils/set-messages-cache/utils.ts +53 -0
- package/src/modules/widget/components/chat-page/chat-page.spec.tsx +23 -7
- package/src/modules/widget/components/chat-page/chat-page.tsx +70 -14
- package/src/modules/widget/components/greetings-card/greetings-card.tsx +1 -1
- package/src/modules/widget/components/header/header.tsx +6 -4
- package/src/modules/widget/components/starter-page/starter-page.spec.tsx +4 -1
- package/src/modules/widget/components/starter-page/starter-page.tsx +31 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# [1.19.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.18.2...v1.19.0) (2025-07-29)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- add message actions ([3a8d5dc](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/3a8d5dcd1ae75ce40b7cbf6edf8629da921c9308))
|
|
6
|
+
|
|
1
7
|
## [1.18.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.18.1...v1.18.2) (2025-07-28)
|
|
2
8
|
|
|
3
9
|
## [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,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
|
-
|
|
40
|
-
|
|
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=
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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>
|
|
@@ -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,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(
|
|
20
|
+
'flex max-w-[min(90%,52rem)] flex-col items-end gap-2 text-sm/normal text-neutral-900',
|
|
18
21
|
{
|
|
19
|
-
'self-end
|
|
20
|
-
'border border-neutral-300 bg-neutral-100': message.metadata.author === 'ai'
|
|
22
|
+
'self-end': messageFromUser
|
|
21
23
|
}
|
|
22
24
|
)}>
|
|
23
|
-
<
|
|
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
|
|
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-
|
|
15
|
-
{
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
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,91 @@
|
|
|
1
|
+
import { forwardRef, useEffect } from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import type { MouseEventHandler, PropsWithChildren } from 'react'
|
|
4
|
+
import { createPortal } from 'react-dom'
|
|
5
|
+
import { useTranslation } from 'react-i18next'
|
|
6
|
+
|
|
7
|
+
import { Button, Icon } from '@/src/lib/components'
|
|
8
|
+
import { MessageSkeleton } from '@/src/modules/messages/components'
|
|
9
|
+
import { useSkeletonRef } from '@/src/modules/messages/hooks/use-skeleton-ref'
|
|
10
|
+
import {
|
|
11
|
+
ScrollToBottomButton,
|
|
12
|
+
usePageLayoutMainRefContext,
|
|
13
|
+
useWidgetLoadingAtom
|
|
14
|
+
} from '@/src/modules/widget'
|
|
15
|
+
import { useScroller } from '../../hooks'
|
|
16
|
+
|
|
17
|
+
const MessagesContainer = forwardRef<
|
|
18
|
+
HTMLDivElement,
|
|
19
|
+
PropsWithChildren<{
|
|
20
|
+
showButton?: boolean
|
|
21
|
+
loading?: boolean
|
|
22
|
+
handleShowMore?: () => Promise<void>
|
|
23
|
+
}>
|
|
24
|
+
>(({ children, handleShowMore, showButton = false, loading = false }, forwardedRef) => {
|
|
25
|
+
const { t } = useTranslation()
|
|
26
|
+
const skeletonRef = useSkeletonRef()
|
|
27
|
+
const [isLoadingNewMsg] = useWidgetLoadingAtom()
|
|
28
|
+
const mainLayoutRef = usePageLayoutMainRefContext()
|
|
29
|
+
const { scrollerRef, scrollToButtonRef, scrollToBottom, showScrollButton } =
|
|
30
|
+
useScroller(forwardedRef)
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
scrollToBottom()
|
|
34
|
+
}, [scrollToBottom])
|
|
35
|
+
|
|
36
|
+
const handleClickShowMore: MouseEventHandler<HTMLButtonElement> = (e) => {
|
|
37
|
+
const scroller = scrollerRef?.current
|
|
38
|
+
const heightBeforeRender = Number(e?.currentTarget?.scrollHeight)
|
|
39
|
+
|
|
40
|
+
void handleShowMore?.().then(() => {
|
|
41
|
+
if (scroller && !isNaN(heightBeforeRender)) {
|
|
42
|
+
setTimeout(
|
|
43
|
+
() =>
|
|
44
|
+
scroller.scrollTo({
|
|
45
|
+
top: heightBeforeRender + 10,
|
|
46
|
+
behavior: 'smooth'
|
|
47
|
+
}),
|
|
48
|
+
180
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div ref={scrollerRef} className='mx-2 my-4 flex h-full flex-col gap-2 overflow-auto px-4'>
|
|
56
|
+
<div className='mb-auto flex-1 self-center'>
|
|
57
|
+
<Button
|
|
58
|
+
className='max-w-max rounded-full border border-neutral-300 bg-neutral-200 px-2 py-1 text-xs/normal tracking-wide text-neutral-900'
|
|
59
|
+
onClick={handleClickShowMore}
|
|
60
|
+
loading={loading}
|
|
61
|
+
show={showButton}>
|
|
62
|
+
<Icon name='arrow-up' className='h-4 w-3' aria-hidden />
|
|
63
|
+
<span className='text-nowrap'>{t('general.buttons.show_more')}</span>
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
{children}
|
|
67
|
+
|
|
68
|
+
{mainLayoutRef.current &&
|
|
69
|
+
createPortal(
|
|
70
|
+
<ScrollToBottomButton
|
|
71
|
+
ref={scrollToButtonRef}
|
|
72
|
+
show={showScrollButton}
|
|
73
|
+
onClick={scrollToBottom}
|
|
74
|
+
/>,
|
|
75
|
+
mainLayoutRef.current
|
|
76
|
+
)}
|
|
77
|
+
<div
|
|
78
|
+
className={clsx({
|
|
79
|
+
'pointer-events-none h-0 overflow-hidden opacity-0': !isLoadingNewMsg,
|
|
80
|
+
'mt-2 pb-4': isLoadingNewMsg
|
|
81
|
+
})}
|
|
82
|
+
ref={skeletonRef}>
|
|
83
|
+
<MessageSkeleton />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
MessagesContainer.displayName = 'MessagesContainer'
|
|
90
|
+
|
|
91
|
+
export default MessagesContainer
|