app-tutor-ai-consumer 1.21.1 → 1.22.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 +9 -0
- package/config/rspack/utils/envs.js +2 -1
- package/config/vitest/__mocks__/animation-avatar.tsx +3 -0
- package/config/vitest/vitest.config.mts +1 -0
- package/environments/.env.development +2 -0
- package/environments/.env.production +2 -0
- package/environments/.env.staging +3 -0
- package/environments/.env.test +4 -1
- package/package.json +2 -1
- package/src/config/styles/index.css +26 -0
- package/src/lib/components/button/button-default.tsx +35 -0
- package/src/lib/components/button/button.tsx +24 -33
- package/src/lib/components/horizontal-draggable-scroll/horizontal-draggable-scroll.tsx +62 -0
- package/src/lib/components/horizontal-draggable-scroll/index.ts +2 -0
- package/src/lib/components/icons/clone.svg +5 -0
- package/src/lib/components/icons/double-check.svg +5 -0
- package/src/lib/components/icons/icon-names.d.ts +7 -3
- package/src/lib/components/icons/paste.svg +5 -0
- package/src/lib/components/index.ts +1 -0
- package/src/main/main.tsx +2 -2
- package/src/modules/messages/components/message-actions/message-actions.tsx +5 -2
- package/src/modules/messages/components/message-item/message-item.tsx +7 -3
- package/src/modules/messages/components/message-item-error/message-item-error.tsx +1 -1
- package/src/modules/messages/components/message-skeleton/message-skeleton.tsx +8 -5
- package/src/modules/messages/components/message-skeleton/styles.module.css +19 -0
- package/src/modules/messages/components/messages-container/messages-container.tsx +90 -67
- package/src/modules/messages/constants.ts +2 -0
- package/src/modules/messages/hooks/use-send-text-message/use-send-text-message.spec.tsx +30 -28
- package/src/modules/messages/hooks/use-send-text-message/use-send-text-message.tsx +30 -28
- package/src/modules/messages/index.ts +1 -0
- package/src/modules/messages/service.direct.ts +41 -0
- package/src/modules/messages/types.ts +22 -1
- package/src/modules/sparkie/hooks/index.ts +1 -0
- package/src/modules/sparkie/hooks/use-init-sparkie/index.ts +2 -0
- package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +39 -0
- package/src/modules/widget/components/avatar-animation/avatar-animation.tsx +13 -0
- package/src/modules/widget/components/avatar-animation/index.ts +2 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +7 -11
- package/src/modules/widget/components/container/container.tsx +2 -10
- package/src/modules/widget/components/index.ts +1 -0
- package/src/modules/widget/components/starter-page/starter-page.spec.tsx +4 -0
- package/src/modules/widget/components/starter-page/starter-page.tsx +23 -8
- package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +0 -11
- package/src/modules/widget/store/widget-tabs.atom.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
# [1.22.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.21.2...v1.22.0) (2025-08-04)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- add button default ([e10c478](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/e10c47844c68815df58a942250fef1fcd1d0f676))
|
|
6
|
+
- add useInitSparkie ([f3dd5f9](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/f3dd5f9d9542f755126e4fcc2af8e8a500703753))
|
|
7
|
+
|
|
8
|
+
## [1.21.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.21.1...v1.21.2) (2025-08-01)
|
|
9
|
+
|
|
1
10
|
## [1.21.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.21.0...v1.21.1) (2025-07-31)
|
|
2
11
|
|
|
3
12
|
# [1.21.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.20.0...v1.21.0) (2025-07-29)
|
|
@@ -13,6 +13,7 @@ export default defineConfig({
|
|
|
13
13
|
'./config/vitest/__mocks__/i18n.tsx',
|
|
14
14
|
'./config/vitest/__mocks__/sparkie.tsx',
|
|
15
15
|
'./config/vitest/__mocks__/icons.tsx',
|
|
16
|
+
'./config/vitest/__mocks__/animation-avatar.tsx',
|
|
16
17
|
'./config/vitest/__mocks__/intersection-observer.ts',
|
|
17
18
|
'./config/vitest/polyfills/global.js'
|
|
18
19
|
],
|
package/environments/.env.test
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-tutor-ai-consumer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
"@hot-observability-js/react": "~1.1.0",
|
|
107
107
|
"@hotmart/event-agent-js": "~1.1.2",
|
|
108
108
|
"@hotmart/sparkie": "~5.1.0",
|
|
109
|
+
"@lottiefiles/dotlottie-react": "~0.14.4",
|
|
109
110
|
"@optimizely/react-sdk": "~3.2.4",
|
|
110
111
|
"@tanstack/query-sync-storage-persister": "~5.80.7",
|
|
111
112
|
"@tanstack/react-query": "~5.80.6",
|
|
@@ -3,3 +3,29 @@
|
|
|
3
3
|
@tailwind base;
|
|
4
4
|
@tailwind components;
|
|
5
5
|
@tailwind utilities;
|
|
6
|
+
|
|
7
|
+
@layer components {
|
|
8
|
+
.shine-box {
|
|
9
|
+
background-color: var(--shb-bg-color, var(--hc-color-neutral-200));
|
|
10
|
+
background-size: 200% 100%;
|
|
11
|
+
background-image: linear-gradient(
|
|
12
|
+
90deg,
|
|
13
|
+
transparent 0%,
|
|
14
|
+
rgb(from var(--shb-shine-color, var(--hc-color-neutral-900)) r g b / 0.3) 50%,
|
|
15
|
+
transparent 100%
|
|
16
|
+
);
|
|
17
|
+
animation-name: slideShine;
|
|
18
|
+
animation-duration: 1s;
|
|
19
|
+
animation-timing-function: cubic-bezier(0.5, -0.5, 1, 1.5);
|
|
20
|
+
animation-iteration-count: infinite;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@keyframes slideShine {
|
|
24
|
+
0% {
|
|
25
|
+
background-position: 100% 0;
|
|
26
|
+
}
|
|
27
|
+
100% {
|
|
28
|
+
background-position: -100% 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import type { ButtonHTMLAttributes, PropsWithChildren } from 'react'
|
|
3
|
+
|
|
4
|
+
export type ButtonDefaultProps = PropsWithChildren<
|
|
5
|
+
ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
6
|
+
show?: boolean
|
|
7
|
+
loading?: boolean
|
|
8
|
+
}
|
|
9
|
+
>
|
|
10
|
+
|
|
11
|
+
function ButtonDefault({
|
|
12
|
+
loading,
|
|
13
|
+
className,
|
|
14
|
+
children,
|
|
15
|
+
show = true,
|
|
16
|
+
...props
|
|
17
|
+
}: ButtonDefaultProps) {
|
|
18
|
+
if (!show) return null
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<button
|
|
22
|
+
{...props}
|
|
23
|
+
className={clsx(
|
|
24
|
+
'rounded text-base font-medium outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
type={props.type ?? 'button'}
|
|
28
|
+
disabled={props.disabled || loading}
|
|
29
|
+
aria-busy={loading}>
|
|
30
|
+
{children}
|
|
31
|
+
</button>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default ButtonDefault
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
|
-
import type {
|
|
2
|
+
import type { PropsWithChildren } from 'react'
|
|
3
3
|
|
|
4
4
|
import { Spinner } from '../spinner'
|
|
5
5
|
|
|
6
|
+
import type { ButtonDefaultProps } from './button-default'
|
|
7
|
+
import ButtonDefault from './button-default'
|
|
8
|
+
|
|
6
9
|
import styles from './styles.module.css'
|
|
7
10
|
|
|
8
11
|
function ButtonContent({
|
|
@@ -25,13 +28,9 @@ function ButtonContent({
|
|
|
25
28
|
)
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
export type ButtonProps =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
show?: boolean
|
|
32
|
-
loading?: boolean
|
|
33
|
-
}
|
|
34
|
-
>
|
|
31
|
+
export type ButtonProps = ButtonDefaultProps & {
|
|
32
|
+
variant?: 'brand' | 'secondary' | 'primary' | 'tertiary' | 'gradient-outline'
|
|
33
|
+
}
|
|
35
34
|
|
|
36
35
|
function Button({
|
|
37
36
|
children,
|
|
@@ -42,7 +41,7 @@ function Button({
|
|
|
42
41
|
...props
|
|
43
42
|
}: ButtonProps) {
|
|
44
43
|
const defaultClasses =
|
|
45
|
-
'rounded
|
|
44
|
+
'rounded outline-none focus-visible:ring-2 focus-visible:ring-blue-500 text-base font-medium'
|
|
46
45
|
const defaultBorder = 'border border-transparent'
|
|
47
46
|
const defaultPadding = 'px-4 py-2'
|
|
48
47
|
const disabledClasses = 'cursor-not-allowed'
|
|
@@ -53,35 +52,28 @@ function Button({
|
|
|
53
52
|
switch (variant) {
|
|
54
53
|
case 'gradient-outline':
|
|
55
54
|
return (
|
|
56
|
-
<
|
|
57
|
-
className={clsx(
|
|
58
|
-
gridClasses,
|
|
59
|
-
defaultClasses,
|
|
60
|
-
'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)]',
|
|
61
|
-
className
|
|
62
|
-
)}
|
|
55
|
+
<ButtonDefault
|
|
63
56
|
{...props}
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
loading={loading}
|
|
58
|
+
show={show}
|
|
59
|
+
className={clsx(
|
|
60
|
+
className,
|
|
61
|
+
'group relative inline-flex items-center justify-center overflow-hidden rounded-lg p-[1px]',
|
|
62
|
+
{
|
|
63
|
+
'bg-gradient-to-br from-[var(--ai-color-gradient-primary)] to-[var(--ai-color-gradient-accent)] hover:text-neutral-1000 group-hover:from-[var(--ai-color-gradient-primary)] group-hover:to-[var(--ai-color-gradient-accent)]':
|
|
64
|
+
!loading,
|
|
65
|
+
'shine-box cursor-not-allowed': loading
|
|
66
|
+
}
|
|
67
|
+
)}>
|
|
66
68
|
<span
|
|
67
69
|
data-label='text-content'
|
|
68
70
|
className={clsx(
|
|
69
|
-
'
|
|
70
|
-
'flex flex-nowrap
|
|
71
|
-
{
|
|
72
|
-
visible: !loading,
|
|
73
|
-
invisible: loading
|
|
74
|
-
}
|
|
71
|
+
'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',
|
|
72
|
+
'flex flex-nowrap'
|
|
75
73
|
)}>
|
|
76
|
-
{children}
|
|
74
|
+
<span className={clsx({ 'opacity-20': loading })}>{children}</span>
|
|
77
75
|
</span>
|
|
78
|
-
|
|
79
|
-
className={clsx('mx-auto my-auto h-[1em] w-[1em] text-current', '[grid-area:stack]', {
|
|
80
|
-
visible: loading,
|
|
81
|
-
invisible: !loading
|
|
82
|
-
})}
|
|
83
|
-
/>
|
|
84
|
-
</button>
|
|
76
|
+
</ButtonDefault>
|
|
85
77
|
)
|
|
86
78
|
case 'primary':
|
|
87
79
|
return (
|
|
@@ -152,7 +144,6 @@ function Button({
|
|
|
152
144
|
return (
|
|
153
145
|
<button
|
|
154
146
|
className={clsx(
|
|
155
|
-
gridClasses,
|
|
156
147
|
'rounded-full outline-none transition-colors duration-300 ease-in',
|
|
157
148
|
{
|
|
158
149
|
'cursor-pointer ring-primary-500 hover:bg-neutral-300 focus:bg-neutral-300 focus-visible:ring-2':
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useRef, useState } from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import type { MouseEventHandler, PropsWithChildren, WheelEvent } from 'react'
|
|
4
|
+
|
|
5
|
+
export type HorizontalDraggableScrollProps = PropsWithChildren<{ className?: string }>
|
|
6
|
+
|
|
7
|
+
function HorizontalDraggableScroll({ children, className }: HorizontalDraggableScrollProps) {
|
|
8
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
9
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
10
|
+
const [startX, setStartX] = useState(0)
|
|
11
|
+
const [scrollLeft, setScrollLeft] = useState(0)
|
|
12
|
+
|
|
13
|
+
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
|
14
|
+
const container = containerRef.current
|
|
15
|
+
|
|
16
|
+
if (!container) return
|
|
17
|
+
|
|
18
|
+
setIsDragging(true)
|
|
19
|
+
setStartX(e.clientX)
|
|
20
|
+
setScrollLeft(container.scrollLeft)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
|
|
24
|
+
const container = containerRef.current
|
|
25
|
+
|
|
26
|
+
if (!container || !isDragging) return
|
|
27
|
+
|
|
28
|
+
e.preventDefault()
|
|
29
|
+
|
|
30
|
+
const walk = e.clientX - startX
|
|
31
|
+
|
|
32
|
+
container.scrollLeft = scrollLeft - walk
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleMouseUp = () => {
|
|
36
|
+
setIsDragging(false)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
|
|
40
|
+
e.currentTarget.scrollLeft += e.deltaY
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
ref={containerRef}
|
|
46
|
+
onMouseDown={handleMouseDown}
|
|
47
|
+
onMouseMove={handleMouseMove}
|
|
48
|
+
onMouseUp={handleMouseUp}
|
|
49
|
+
onMouseLeave={handleMouseUp}
|
|
50
|
+
onWheel={handleWheel}
|
|
51
|
+
className={clsx(
|
|
52
|
+
{
|
|
53
|
+
'cursor-grabbing select-none': isDragging
|
|
54
|
+
},
|
|
55
|
+
className
|
|
56
|
+
)}>
|
|
57
|
+
{children}
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default HorizontalDraggableScroll
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg"
|
|
2
|
+
viewBox="0 0 512 512">
|
|
3
|
+
<path fill="currentColor"
|
|
4
|
+
d="M64 480H288c17.7 0 32-14.3 32-32V384h32v64c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h64v32H64c-17.7 0-32 14.3-32 32V448c0 17.7 14.3 32 32 32zM224 320H448c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32H224c-17.7 0-32 14.3-32 32V288c0 17.7 14.3 32 32 32zm-64-32V64c0-35.3 28.7-64 64-64H448c35.3 0 64 28.7 64 64V288c0 35.3-28.7 64-64 64H224c-35.3 0-64-28.7-64-64z"></path>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg aria-hidden="true" focusable="false" role="img"
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
3
|
+
<path fill="currentColor"
|
|
4
|
+
d="M331.3 75.3c6.2-6.2 6.2-16.4 0-22.6s-16.4-6.2-22.6 0L160 201.4 91.3 132.7c-6.2-6.2-16.4-6.2-22.6 0s-6.2 16.4 0 22.6l80 80c6.2 6.2 16.4 6.2 22.6 0l160-160zm112 112c6.2-6.2 6.2-16.4 0-22.6s-16.4-6.2-22.6 0L160 425.4 27.3 292.7c-6.2-6.2-16.4-6.2-22.6 0s-6.2 16.4 0 22.6l144 144c6.2 6.2 16.4 6.2 22.6 0l272-272z"></path>
|
|
5
|
+
</svg>
|
|
@@ -5,13 +5,17 @@ export type ValidIconNames =
|
|
|
5
5
|
| 'arrow-down'
|
|
6
6
|
| 'arrow-left'
|
|
7
7
|
| 'arrow-up'
|
|
8
|
+
| 'book'
|
|
8
9
|
| 'chevron-down'
|
|
9
|
-
| '
|
|
10
|
+
| 'clone'
|
|
10
11
|
| 'close'
|
|
12
|
+
| 'copy'
|
|
13
|
+
| 'double-check'
|
|
14
|
+
| 'gallery'
|
|
11
15
|
| 'info'
|
|
16
|
+
| 'interrogation'
|
|
12
17
|
| 'like'
|
|
18
|
+
| 'paste'
|
|
13
19
|
| 'send'
|
|
14
20
|
| 'stop'
|
|
15
21
|
| 'warning'
|
|
16
|
-
| 'interrogation'
|
|
17
|
-
| 'gallery'
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg"
|
|
2
|
+
viewBox="0 0 512 512">
|
|
3
|
+
<path fill="currentColor"
|
|
4
|
+
d="M160 32c11.6 0 21.3 8.2 23.5 19.2C185 58.6 191.6 64 199.2 64H208c8.8 0 16 7.2 16 16V96H96V80c0-8.8 7.2-16 16-16h8.8c7.6 0 14.2-5.4 15.7-12.8C138.7 40.2 148.4 32 160 32zM64 64h2.7C65 69 64 74.4 64 80V96c0 17.7 14.3 32 32 32H224c17.7 0 32-14.3 32-32V80c0-5.6-1-11-2.7-16H256c17.7 0 32 14.3 32 32h32c0-35.3-28.7-64-64-64H210.6c-9-18.9-28.3-32-50.6-32s-41.6 13.1-50.6 32H64C28.7 32 0 60.7 0 96V384c0 35.3 28.7 64 64 64H192V416H64c-17.7 0-32-14.3-32-32V96c0-17.7 14.3-32 32-32zM288 480c-17.7 0-32-14.3-32-32V192c0-17.7 14.3-32 32-32h96v56c0 22.1 17.9 40 40 40h56V448c0 17.7-14.3 32-32 32H288zM416 165.3L474.7 224H424c-4.4 0-8-3.6-8-8V165.3zM448 512c35.3 0 64-28.7 64-64V235.9c0-12.7-5.1-24.9-14.1-33.9l-59.9-59.9c-9-9-21.2-14.1-33.9-14.1H288c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H448z"></path>
|
|
5
|
+
</svg>
|
package/src/main/main.tsx
CHANGED
|
@@ -12,14 +12,14 @@ export type MainProps = {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function Main({ settings }: MainProps) {
|
|
15
|
-
|
|
15
|
+
useInitWidget(settings)
|
|
16
16
|
useAppLang(settings.locale)
|
|
17
17
|
useListenToThemeChangeEvent()
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
20
|
<ErrorBoundary fallback={<GenericError />}>
|
|
21
21
|
<GlobalProviders settings={settings}>
|
|
22
|
-
<WidgetContainer
|
|
22
|
+
<WidgetContainer />
|
|
23
23
|
</GlobalProviders>
|
|
24
24
|
</ErrorBoundary>
|
|
25
25
|
)
|
|
@@ -18,6 +18,7 @@ export type MessageActionsProps = {
|
|
|
18
18
|
function MessageActions({ message, className, showActions = false }: MessageActionsProps) {
|
|
19
19
|
const { t } = useTranslation()
|
|
20
20
|
const [copying, setCopying] = useState(false)
|
|
21
|
+
const [copied, setCopied] = useState(false)
|
|
21
22
|
const [reaction, setReaction] = useState<ButtonReactionsType | null>(null)
|
|
22
23
|
|
|
23
24
|
const copyToClipboard = (): void => {
|
|
@@ -27,11 +28,13 @@ function MessageActions({ message, className, showActions = false }: MessageActi
|
|
|
27
28
|
|
|
28
29
|
navigator.clipboard
|
|
29
30
|
.writeText(message.text)
|
|
31
|
+
.then(() => setCopied(true))
|
|
30
32
|
.catch((err) => {
|
|
31
33
|
console.error('Failed to copy text: ', err)
|
|
32
34
|
})
|
|
33
35
|
.finally(() => {
|
|
34
36
|
setTimeout(() => setCopying(false), 1000)
|
|
37
|
+
setTimeout(() => setCopied(false), 3000)
|
|
35
38
|
})
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -72,9 +75,9 @@ function MessageActions({ message, className, showActions = false }: MessageActi
|
|
|
72
75
|
</Button>
|
|
73
76
|
<Button onClick={copyToClipboard} aria-label={t('general.buttons.copy')} disabled={copying}>
|
|
74
77
|
<Icon
|
|
75
|
-
name='copy'
|
|
78
|
+
name={copied ? 'paste' : 'copy'}
|
|
76
79
|
className={clsx('h-3 w-3.5', {
|
|
77
|
-
'text-info-500':
|
|
80
|
+
'text-info-500': copied
|
|
78
81
|
})}
|
|
79
82
|
/>
|
|
80
83
|
</Button>
|
|
@@ -25,10 +25,14 @@ function MessageItem({ message }: { message: ParsedMessage }) {
|
|
|
25
25
|
<div
|
|
26
26
|
data-test='messages-item'
|
|
27
27
|
className={clsx('w-full overflow-x-hidden rounded-lg px-3', {
|
|
28
|
-
'bg-neutral-
|
|
29
|
-
'
|
|
28
|
+
'max-w-max bg-[rgb(from_var(--hc-color-neutral-300)_r_g_b_/_0.8)]': messageFromUser,
|
|
29
|
+
'bg-neutral-200': messageFromAi
|
|
30
30
|
})}>
|
|
31
|
-
<MarkdownRenderer
|
|
31
|
+
<MarkdownRenderer
|
|
32
|
+
content={message?.text ?? message?.name}
|
|
33
|
+
imgComponent={imgComponent}
|
|
34
|
+
className='w-full'
|
|
35
|
+
/>
|
|
32
36
|
</div>
|
|
33
37
|
<MessageActions
|
|
34
38
|
className={clsx('flex items-center justify-between gap-2', {
|
|
@@ -20,7 +20,7 @@ function MessageItemError({ message, retry, show = true }: MessageItemErrorProps
|
|
|
20
20
|
if (!show) return null
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
|
-
<div className='
|
|
23
|
+
<div className='absolute bottom-4 flex justify-start gap-2 rounded border border-danger-500 bg-neutral-200 p-4 text-neutral-800 max-md:inset-x-[1.125rem] md:inset-x-5'>
|
|
24
24
|
<span>{t('chat_page.error.loading.content')}</span>
|
|
25
25
|
<Button variant='tertiary' className='px-4 text-sm/relaxed text-danger-400' onClick={retry}>
|
|
26
26
|
{t('chat_page.error.loading.action')}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { forwardRef } from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import { AvatarAnimation } from '@/src/modules/widget'
|
|
5
|
+
|
|
6
|
+
import styles from './styles.module.css'
|
|
4
7
|
|
|
5
8
|
const MessageSkeleton = forwardRef<HTMLDivElement>((_, ref) => {
|
|
6
9
|
return (
|
|
7
10
|
<div ref={ref} className='flex flex-col items-start gap-2' aria-label='Loading Component'>
|
|
8
|
-
<
|
|
11
|
+
<AvatarAnimation />
|
|
9
12
|
<div className='flex w-full flex-col items-start gap-2'>
|
|
10
|
-
<div className='h-3 w-full
|
|
11
|
-
<div className='h-3 w-[83%]
|
|
12
|
-
<div className='h-3 w-[56%]
|
|
13
|
+
<div className={clsx('h-3 w-full rounded-full', styles.shine)} />
|
|
14
|
+
<div className={clsx('h-3 w-[83%] rounded-full', styles.shine)} />
|
|
15
|
+
<div className={clsx('h-3 w-[56%] rounded-full', styles.shine)} />
|
|
13
16
|
</div>
|
|
14
17
|
</div>
|
|
15
18
|
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.shine {
|
|
2
|
+
background: linear-gradient(
|
|
3
|
+
90deg,
|
|
4
|
+
var(--hc-color-neutral-200) 33%,
|
|
5
|
+
rgb(from var(--hc-color-neutral-900) r g b / 0.3) 53%,
|
|
6
|
+
var(--hc-color-neutral-200) 76%
|
|
7
|
+
);
|
|
8
|
+
background-size: 295% 100%;
|
|
9
|
+
animation: shine 1500ms infinite alternate;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@keyframes shine {
|
|
13
|
+
0% {
|
|
14
|
+
background-position: right;
|
|
15
|
+
}
|
|
16
|
+
100% {
|
|
17
|
+
background-position: left;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, useEffect } from 'react'
|
|
1
|
+
import { forwardRef, lazy, Suspense, useEffect } from 'react'
|
|
2
2
|
import clsx from 'clsx'
|
|
3
3
|
import type { MouseEventHandler, PropsWithChildren } from 'react'
|
|
4
4
|
import { createPortal } from 'react-dom'
|
|
@@ -14,79 +14,102 @@ import {
|
|
|
14
14
|
} from '@/src/modules/widget'
|
|
15
15
|
import { useScroller } from '../../hooks'
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
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)
|
|
17
|
+
const MessageItemError = lazy(
|
|
18
|
+
() => import('@/src/modules/messages/components/message-item-error/message-item-error')
|
|
19
|
+
)
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
21
|
+
export type MessagesContainerProps = PropsWithChildren<{
|
|
22
|
+
showButton?: boolean
|
|
23
|
+
loading?: boolean
|
|
24
|
+
handleShowMore?: () => Promise<void>
|
|
25
|
+
error?: {
|
|
26
|
+
show: boolean
|
|
27
|
+
message: string
|
|
28
|
+
retry: () => void
|
|
29
|
+
}
|
|
30
|
+
}>
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
32
|
+
const MessagesContainer = forwardRef<HTMLDivElement, MessagesContainerProps>(
|
|
33
|
+
({ children, handleShowMore, error, showButton = false, loading = false }, forwardedRef) => {
|
|
34
|
+
const { t } = useTranslation()
|
|
35
|
+
const skeletonRef = useSkeletonRef()
|
|
36
|
+
const [isLoadingNewMsg] = useWidgetLoadingAtom()
|
|
37
|
+
const mainLayoutRef = usePageLayoutMainRefContext()
|
|
38
|
+
const { scrollerRef, scrollToButtonRef, scrollToBottom, showScrollButton } =
|
|
39
|
+
useScroller(forwardedRef)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
() =>
|
|
44
|
-
scroller.scrollTo({
|
|
45
|
-
top: heightBeforeRender + 10,
|
|
46
|
-
behavior: 'smooth'
|
|
47
|
-
}),
|
|
48
|
-
180
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
}
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
scrollToBottom()
|
|
43
|
+
}, [scrollToBottom])
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
45
|
+
const handleClickShowMore: MouseEventHandler<HTMLButtonElement> = (e) => {
|
|
46
|
+
const scroller = scrollerRef?.current
|
|
47
|
+
const heightBeforeRender = Number(e?.currentTarget?.scrollHeight)
|
|
48
|
+
|
|
49
|
+
void handleShowMore?.().then(() => {
|
|
50
|
+
if (scroller && !isNaN(heightBeforeRender)) {
|
|
51
|
+
setTimeout(
|
|
52
|
+
() =>
|
|
53
|
+
scroller.scrollTo({
|
|
54
|
+
top: heightBeforeRender + 10,
|
|
55
|
+
behavior: 'smooth'
|
|
56
|
+
}),
|
|
57
|
+
180
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
createPortal(
|
|
72
|
-
<ScrollToBottomButton
|
|
73
|
-
ref={scrollToButtonRef}
|
|
74
|
-
show={showScrollButton}
|
|
75
|
-
onClick={scrollToBottom}
|
|
76
|
-
/>,
|
|
77
|
-
mainLayoutRef.current
|
|
78
|
-
)}
|
|
63
|
+
return (
|
|
79
64
|
<div
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
65
|
+
ref={scrollerRef}
|
|
66
|
+
className='flex h-full flex-col gap-2 overflow-auto max-md:p-[1.125rem] md:p-5'>
|
|
67
|
+
<div className='mb-auto flex-1 self-center'>
|
|
68
|
+
<Button
|
|
69
|
+
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'
|
|
70
|
+
onClick={handleClickShowMore}
|
|
71
|
+
loading={loading}
|
|
72
|
+
show={showButton}>
|
|
73
|
+
<Icon name='arrow-up' className='h-4 w-3' aria-hidden />
|
|
74
|
+
<span className='text-nowrap'>{t('general.buttons.show_more')}</span>
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
{children}
|
|
78
|
+
|
|
79
|
+
{error?.show &&
|
|
80
|
+
mainLayoutRef.current &&
|
|
81
|
+
createPortal(
|
|
82
|
+
<Suspense fallback={<div aria-live='polite' />}>
|
|
83
|
+
<MessageItemError
|
|
84
|
+
message={`❌ Error loading messages: ${error?.message}`}
|
|
85
|
+
show={error?.show}
|
|
86
|
+
retry={error?.retry}
|
|
87
|
+
/>
|
|
88
|
+
</Suspense>,
|
|
89
|
+
mainLayoutRef.current
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{mainLayoutRef.current &&
|
|
93
|
+
createPortal(
|
|
94
|
+
<ScrollToBottomButton
|
|
95
|
+
ref={scrollToButtonRef}
|
|
96
|
+
show={showScrollButton}
|
|
97
|
+
onClick={scrollToBottom}
|
|
98
|
+
/>,
|
|
99
|
+
mainLayoutRef.current
|
|
100
|
+
)}
|
|
101
|
+
<div
|
|
102
|
+
className={clsx({
|
|
103
|
+
'pointer-events-none h-0 overflow-hidden opacity-0': !isLoadingNewMsg,
|
|
104
|
+
'mt-2 pb-4': isLoadingNewMsg
|
|
105
|
+
})}
|
|
106
|
+
ref={skeletonRef}>
|
|
107
|
+
<MessageSkeleton />
|
|
108
|
+
</div>
|
|
86
109
|
</div>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
)
|
|
90
113
|
|
|
91
114
|
MessagesContainer.displayName = 'MessagesContainer'
|
|
92
115
|
|
|
@@ -3,5 +3,7 @@ export const MSG_MAX_PAGES = 20
|
|
|
3
3
|
|
|
4
4
|
export const MessagesEndpoints = {
|
|
5
5
|
getAll: (conversationId: string) =>
|
|
6
|
+
`${process.env.API_CONVERSATION_URL}/v1/conversations/${conversationId}/messages`,
|
|
7
|
+
create: (conversationId: string) =>
|
|
6
8
|
`${process.env.API_CONVERSATION_URL}/v1/conversations/${conversationId}/messages`
|
|
7
9
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { UAParser } from 'ua-parser-js'
|
|
2
2
|
|
|
3
3
|
import { act, chance, renderHook, waitFor } from '@/src/config/tests'
|
|
4
|
-
import { MessagesService } from '@/src/modules/messages'
|
|
4
|
+
import { DirectMessagesService as MessagesService } from '@/src/modules/messages'
|
|
5
5
|
import { useGetProfile } from '@/src/modules/profile'
|
|
6
6
|
import * as Store from '@/src/modules/widget'
|
|
7
7
|
import WidgetSettingPropsBuilder from '@/src/modules/widget/__tests__/widget-settings-props.builder'
|
|
@@ -57,40 +57,42 @@ describe('useSendTextMessage', () => {
|
|
|
57
57
|
const txt = 'question::summary'
|
|
58
58
|
const text = txt.replace('question::', '')
|
|
59
59
|
|
|
60
|
-
vi.spyOn(MessagesService, '
|
|
60
|
+
vi.spyOn(MessagesService, 'create')
|
|
61
61
|
|
|
62
62
|
const { result } = render()
|
|
63
63
|
|
|
64
64
|
await waitFor(() => result.current.mutateAsync(txt))
|
|
65
65
|
|
|
66
|
-
expect(MessagesService.
|
|
67
|
-
expect(MessagesService.
|
|
68
|
-
content: {
|
|
69
|
-
type: 'text/plain',
|
|
70
|
-
text
|
|
71
|
-
},
|
|
66
|
+
expect(MessagesService.create).toHaveBeenCalledTimes(1)
|
|
67
|
+
expect(MessagesService.create).toHaveBeenNthCalledWith(1, {
|
|
72
68
|
conversationId: defaultSettings.conversationId,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
language: defaultSettings.locale,
|
|
78
|
-
clubName: defaultSettings.clubName,
|
|
79
|
-
productName: defaultSettings.productName,
|
|
80
|
-
productId: defaultSettings.productId,
|
|
81
|
-
classHashId: defaultSettings.classHashId,
|
|
82
|
-
owner_id: defaultSettings.owner_id,
|
|
83
|
-
current_media_codes: defaultSettings.current_media_codes,
|
|
84
|
-
question: text,
|
|
85
|
-
router: text,
|
|
86
|
-
osVersion: UAParserMock.browser.version,
|
|
87
|
-
platformDetail: UAParserMock.browser.name,
|
|
88
|
-
ucode: defaultSettings.user?.ucode
|
|
69
|
+
message: {
|
|
70
|
+
content: {
|
|
71
|
+
type: 'text/plain',
|
|
72
|
+
text
|
|
89
73
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
74
|
+
metadata: {
|
|
75
|
+
author: 'user',
|
|
76
|
+
contactId: defaultSettings.contactId,
|
|
77
|
+
context: {
|
|
78
|
+
language: defaultSettings.locale,
|
|
79
|
+
clubName: defaultSettings.clubName,
|
|
80
|
+
productName: defaultSettings.productName,
|
|
81
|
+
productId: defaultSettings.productId,
|
|
82
|
+
classHashId: defaultSettings.classHashId,
|
|
83
|
+
owner_id: defaultSettings.owner_id,
|
|
84
|
+
current_media_codes: defaultSettings.current_media_codes,
|
|
85
|
+
question: text,
|
|
86
|
+
router: text,
|
|
87
|
+
osVersion: UAParserMock.browser.version,
|
|
88
|
+
platformDetail: UAParserMock.browser.name,
|
|
89
|
+
ucode: defaultSettings.user?.ucode
|
|
90
|
+
},
|
|
91
|
+
externalId: expect.any(String),
|
|
92
|
+
namespace: defaultSettings.namespace,
|
|
93
|
+
sessionId: defaultSettings.sessionId,
|
|
94
|
+
userId: String(getProfileMock.data.userId)
|
|
95
|
+
}
|
|
94
96
|
}
|
|
95
97
|
})
|
|
96
98
|
})
|
|
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
3
3
|
import { UAParser } from 'ua-parser-js'
|
|
4
4
|
import { v4 } from 'uuid'
|
|
5
5
|
|
|
6
|
-
import { MessagesService } from '@/src/modules/messages'
|
|
6
|
+
import { DirectMessagesService as MessagesService } from '@/src/modules/messages'
|
|
7
7
|
import { useGetProfile } from '@/src/modules/profile'
|
|
8
8
|
import { useWidgetLoadingAtom, useWidgetSettingsAtomValue } from '@/src/modules/widget'
|
|
9
9
|
import { MessagesEvents } from '../../events'
|
|
@@ -47,35 +47,37 @@ function useSendTextMessage() {
|
|
|
47
47
|
questionParam = processedMessage
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
return MessagesService.
|
|
51
|
-
content: {
|
|
52
|
-
type: 'text/plain',
|
|
53
|
-
text: processedMessage
|
|
54
|
-
},
|
|
50
|
+
return MessagesService.create({
|
|
55
51
|
conversationId: settings?.conversationId,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
language: settings.locale,
|
|
61
|
-
clubName: settings.clubName,
|
|
62
|
-
productName: settings.productName,
|
|
63
|
-
productId: settings.productId,
|
|
64
|
-
productType: settings.productType,
|
|
65
|
-
classType: settings.classType,
|
|
66
|
-
classHashId: settings.classHashId,
|
|
67
|
-
owner_id: settings?.owner_id,
|
|
68
|
-
current_media_codes: settings?.current_media_codes,
|
|
69
|
-
question: questionParam,
|
|
70
|
-
router: questionParam ? 'summary' : undefined,
|
|
71
|
-
osVersion: browserInfo.version,
|
|
72
|
-
platformDetail: browserInfo.name,
|
|
73
|
-
ucode: settings.user?.ucode
|
|
52
|
+
message: {
|
|
53
|
+
content: {
|
|
54
|
+
type: 'text/plain',
|
|
55
|
+
text: processedMessage
|
|
74
56
|
},
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
57
|
+
metadata: {
|
|
58
|
+
author: 'user',
|
|
59
|
+
contactId: settings.contactId,
|
|
60
|
+
context: {
|
|
61
|
+
language: settings.locale,
|
|
62
|
+
clubName: settings.clubName,
|
|
63
|
+
productName: settings.productName,
|
|
64
|
+
productId: settings.productId,
|
|
65
|
+
productType: settings.productType,
|
|
66
|
+
classType: settings.classType,
|
|
67
|
+
classHashId: settings.classHashId,
|
|
68
|
+
owner_id: settings?.owner_id,
|
|
69
|
+
current_media_codes: settings?.current_media_codes,
|
|
70
|
+
question: questionParam,
|
|
71
|
+
router: questionParam ? 'summary' : undefined,
|
|
72
|
+
osVersion: browserInfo.version,
|
|
73
|
+
platformDetail: browserInfo.name,
|
|
74
|
+
ucode: settings.user?.ucode
|
|
75
|
+
},
|
|
76
|
+
externalId: v4(),
|
|
77
|
+
namespace: settings.namespace,
|
|
78
|
+
sessionId: settings.sessionId,
|
|
79
|
+
userId
|
|
80
|
+
}
|
|
79
81
|
}
|
|
80
82
|
})
|
|
81
83
|
},
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Message } from '@hotmart/sparkie/dist/MessageService'
|
|
2
|
+
|
|
3
|
+
import { api } from '@/src/config/request'
|
|
4
|
+
|
|
5
|
+
import { MessagesEndpoints } from './constants'
|
|
6
|
+
import type {
|
|
7
|
+
DirectMessagesServiceProps,
|
|
8
|
+
FetchMessagesResponse,
|
|
9
|
+
IMessageWithSenderData
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
class DirectMessagesService {
|
|
13
|
+
async getAll({ conversationId, before, limit }: DirectMessagesServiceProps['GetAll']) {
|
|
14
|
+
const { data: messages } = await api.get<IMessageWithSenderData[]>(
|
|
15
|
+
MessagesEndpoints.getAll(conversationId),
|
|
16
|
+
{
|
|
17
|
+
params: {
|
|
18
|
+
before,
|
|
19
|
+
limit
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
messages,
|
|
26
|
+
hasMore: messages.length === limit
|
|
27
|
+
} as FetchMessagesResponse
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async create({ conversationId, message }: DirectMessagesServiceProps['Create']) {
|
|
31
|
+
const { data } = await api.post<Message>(MessagesEndpoints.create(conversationId), {
|
|
32
|
+
...message,
|
|
33
|
+
type: 'message',
|
|
34
|
+
channel: 'chat'
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return data
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default new DirectMessagesService()
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Message as SparkieMsg,
|
|
3
|
+
MessageContent,
|
|
4
|
+
SenderData
|
|
5
|
+
} from '@hotmart/sparkie/dist/MessageService'
|
|
2
6
|
|
|
3
7
|
export type IMessage = SparkieMsg & {
|
|
4
8
|
metadata: {
|
|
@@ -80,3 +84,20 @@ export type FetchMessagesResponse = {
|
|
|
80
84
|
export type SubmitQuestionEventDetail = {
|
|
81
85
|
timestamp: number
|
|
82
86
|
}
|
|
87
|
+
|
|
88
|
+
export type DirectMessagesServiceProps = {
|
|
89
|
+
GetAll: {
|
|
90
|
+
conversationId: string
|
|
91
|
+
before: number
|
|
92
|
+
limit: number
|
|
93
|
+
}
|
|
94
|
+
Create: {
|
|
95
|
+
conversationId: string
|
|
96
|
+
message: {
|
|
97
|
+
content: MessageContent
|
|
98
|
+
threadId?: string
|
|
99
|
+
parentId?: string
|
|
100
|
+
metadata?: Record<string, unknown>
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './use-init-sparkie'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useWidgetSettingsAtomValue } from '@/src/modules/widget'
|
|
4
|
+
import { SparkieService } from '../..'
|
|
5
|
+
|
|
6
|
+
function useInitSparkie() {
|
|
7
|
+
const [isSuccess, setIsSuccess] = useState(false)
|
|
8
|
+
const settings = useWidgetSettingsAtomValue()
|
|
9
|
+
|
|
10
|
+
const init = useCallback(async () => {
|
|
11
|
+
if (!settings?.hotmartToken) return
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await SparkieService.initSparkie({
|
|
15
|
+
token: settings?.hotmartToken,
|
|
16
|
+
skipPresenceSetup: true,
|
|
17
|
+
retryOptions: {
|
|
18
|
+
maxRetries: 5,
|
|
19
|
+
retryDelay: 2000,
|
|
20
|
+
backoffMultiplier: 1.5
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
await SparkieService.ensureInitialized()
|
|
24
|
+
setIsSuccess(true)
|
|
25
|
+
} catch {
|
|
26
|
+
setIsSuccess(false)
|
|
27
|
+
// TODO: Create Error PAGE and setTab
|
|
28
|
+
// setTab('information')
|
|
29
|
+
}
|
|
30
|
+
}, [settings?.hotmartToken])
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
void init()
|
|
34
|
+
}, [init])
|
|
35
|
+
|
|
36
|
+
return isSuccess
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default useInitSparkie
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DotLottieReact } from '@lottiefiles/dotlottie-react'
|
|
2
|
+
|
|
3
|
+
const AVATAR_ANIMATION_URL = `${process.env.STATIC_URL}/tutor/tutor_sparkle.lottie`
|
|
4
|
+
|
|
5
|
+
const AvatarAnimation = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div className='flex h-11 w-11 items-center justify-center rounded-lg bg-ai-chat-response'>
|
|
8
|
+
<DotLottieReact src={AVATAR_ANIMATION_URL} loop autoplay className='h-auto w-full' />
|
|
9
|
+
</div>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default AvatarAnimation
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
2
2
|
import { useInfiniteQuery } from '@tanstack/react-query'
|
|
3
3
|
|
|
4
4
|
import { isTextEmpty } from '@/src/lib/utils/is-text-empty'
|
|
@@ -15,10 +15,6 @@ import {
|
|
|
15
15
|
import { WidgetHeader } from '../header'
|
|
16
16
|
import { PageLayout } from '../page-layout'
|
|
17
17
|
|
|
18
|
-
const MessageItemError = lazy(
|
|
19
|
-
() => import('@/src/modules/messages/components/message-item-error/message-item-error')
|
|
20
|
-
)
|
|
21
|
-
|
|
22
18
|
function ChatPage() {
|
|
23
19
|
const chatInputRef = useRef<HTMLTextAreaElement>(null)
|
|
24
20
|
const scrollerRef = useRef<HTMLDivElement>(null)
|
|
@@ -84,13 +80,13 @@ function ChatPage() {
|
|
|
84
80
|
await messagesQuery.fetchNextPage()
|
|
85
81
|
}}
|
|
86
82
|
showButton={messagesQuery.hasNextPage}
|
|
87
|
-
loading={messagesQuery.isFetchingNextPage}
|
|
83
|
+
loading={messagesQuery.isFetchingNextPage}
|
|
84
|
+
error={{
|
|
85
|
+
show: messagesQuery.isError,
|
|
86
|
+
message: messagesQuery.error?.message ?? '',
|
|
87
|
+
retry: () => void messagesQuery.refetch()
|
|
88
|
+
}}>
|
|
88
89
|
{messagesQuery.data && <MessagesList messagesMap={messagesQuery.data} />}
|
|
89
|
-
<MessageItemError
|
|
90
|
-
show={messagesQuery.isError}
|
|
91
|
-
message={`❌ Error loading messages: ${messagesQuery.error?.message ?? ''}`}
|
|
92
|
-
retry={() => void messagesQuery.refetch()}
|
|
93
|
-
/>
|
|
94
90
|
</MessagesContainer>
|
|
95
91
|
</PageLayout>
|
|
96
92
|
)
|
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
|
|
3
1
|
import { useSubscribeMessageReceivedEvent } from '@/src/modules/messages/hooks'
|
|
4
2
|
import { useSubscribeThreadClosedEvent } from '@/src/modules/thread/hooks'
|
|
5
3
|
import { useListenToVisibilityEvents } from '../../hooks'
|
|
6
4
|
import { useWidgetTabsAtom } from '../../store'
|
|
7
5
|
import { WIDGET_TABS } from '../constants'
|
|
8
6
|
|
|
9
|
-
function WidgetContainer(
|
|
10
|
-
const [widgetTabs
|
|
7
|
+
function WidgetContainer() {
|
|
8
|
+
const [widgetTabs] = useWidgetTabsAtom()
|
|
11
9
|
|
|
12
10
|
useSubscribeMessageReceivedEvent()
|
|
13
11
|
useSubscribeThreadClosedEvent()
|
|
14
12
|
useListenToVisibilityEvents()
|
|
15
13
|
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (completeSetup) {
|
|
18
|
-
setTab('starter')
|
|
19
|
-
}
|
|
20
|
-
}, [completeSetup, setTab])
|
|
21
|
-
|
|
22
14
|
return (
|
|
23
15
|
<div className='flex h-full flex-col items-center justify-stretch overflow-hidden'>
|
|
24
16
|
{WIDGET_TABS[widgetTabs.currentTab]}
|
|
@@ -8,6 +8,10 @@ vi.mock('@/src/modules/messages/hooks', () => ({
|
|
|
8
8
|
useSendTextMessage: vi.fn()
|
|
9
9
|
}))
|
|
10
10
|
|
|
11
|
+
vi.mock('@/src/modules/sparkie/hooks/use-init-sparkie', () => ({
|
|
12
|
+
useInitSparkie: vi.fn(() => true)
|
|
13
|
+
}))
|
|
14
|
+
|
|
11
15
|
describe('WidgetStarterPage', () => {
|
|
12
16
|
const useSendTextMessageMock = { mutate: vi.fn() }
|
|
13
17
|
|
|
@@ -4,12 +4,14 @@ import clsx from 'clsx'
|
|
|
4
4
|
import type { MouseEventHandler } from 'react'
|
|
5
5
|
import { useTranslation } from 'react-i18next'
|
|
6
6
|
|
|
7
|
-
import { Button } from '@/src/lib/components'
|
|
7
|
+
import { Button, HorizontalDraggableScroll } from '@/src/lib/components'
|
|
8
|
+
import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
|
|
8
9
|
import { useRefEventListener } from '@/src/lib/hooks'
|
|
9
10
|
import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/components'
|
|
10
11
|
import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/hooks'
|
|
11
12
|
import { useMessagesMaxCount } from '@/src/modules/messages/store'
|
|
12
13
|
import { useGetProfile } from '@/src/modules/profile'
|
|
14
|
+
import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
|
|
13
15
|
import { useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
|
|
14
16
|
import { GreetingsCard } from '../greetings-card'
|
|
15
17
|
import { WidgetHeader } from '../header'
|
|
@@ -28,6 +30,7 @@ function WidgetStarterPage() {
|
|
|
28
30
|
const limit = useMessagesMaxCount()
|
|
29
31
|
const queryClient = useQueryClient()
|
|
30
32
|
const name = settings?.tutorName ?? t('general.name')
|
|
33
|
+
const isSparkieReady = useInitSparkie()
|
|
31
34
|
|
|
32
35
|
useRefEventListener<HTMLTextAreaElement>({
|
|
33
36
|
config: {
|
|
@@ -74,9 +77,19 @@ function WidgetStarterPage() {
|
|
|
74
77
|
[conversationId, limit, profileId]
|
|
75
78
|
)
|
|
76
79
|
|
|
80
|
+
const headerBtns = useMemo(() => {
|
|
81
|
+
const btnList = ['info', 'close']
|
|
82
|
+
|
|
83
|
+
if (!isSparkieReady) return btnList
|
|
84
|
+
|
|
85
|
+
return btnList.concat(['archive'])
|
|
86
|
+
}, [isSparkieReady]) as ValidIconNames[]
|
|
87
|
+
|
|
77
88
|
useEffect(() => {
|
|
89
|
+
if (!conversationId || !profileId) return
|
|
90
|
+
|
|
78
91
|
void queryClient.prefetchInfiniteQuery(messagesQueryConfig)
|
|
79
|
-
}, [messagesQueryConfig, queryClient])
|
|
92
|
+
}, [conversationId, messagesQueryConfig, profileId, queryClient])
|
|
80
93
|
|
|
81
94
|
return (
|
|
82
95
|
<PageLayout
|
|
@@ -85,7 +98,7 @@ function WidgetStarterPage() {
|
|
|
85
98
|
name='new-chat-msg-input'
|
|
86
99
|
ref={chatInputRef}
|
|
87
100
|
onSend={handleSend}
|
|
88
|
-
buttonDisabled={!chatInputValue.trim()}
|
|
101
|
+
buttonDisabled={!chatInputValue.trim() || !isSparkieReady}
|
|
89
102
|
/>
|
|
90
103
|
}>
|
|
91
104
|
<div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
|
|
@@ -93,7 +106,7 @@ function WidgetStarterPage() {
|
|
|
93
106
|
className={clsx('grid-area-[a] flex min-h-0 flex-col max-md:p-[1.125rem] md:p-5', {
|
|
94
107
|
[styles.bg]: settings?.config?.theme === 'dark'
|
|
95
108
|
})}>
|
|
96
|
-
<WidgetHeader enabledButtons={
|
|
109
|
+
<WidgetHeader enabledButtons={headerBtns} tutorName={name} />
|
|
97
110
|
|
|
98
111
|
<div className='my-auto'>
|
|
99
112
|
<GreetingsCard
|
|
@@ -103,22 +116,24 @@ function WidgetStarterPage() {
|
|
|
103
116
|
/>
|
|
104
117
|
</div>
|
|
105
118
|
</div>
|
|
106
|
-
<
|
|
119
|
+
<HorizontalDraggableScroll className='grid-area-[b] mx-5 my-6 flex flex-shrink-0 snap-x snap-mandatory gap-2 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
|
|
107
120
|
<Button
|
|
108
121
|
variant='gradient-outline'
|
|
109
122
|
className='shrink-0 snap-end text-sm'
|
|
110
|
-
onClick={handleAskQuestion}
|
|
123
|
+
onClick={handleAskQuestion}
|
|
124
|
+
loading={!isSparkieReady}>
|
|
111
125
|
<span>🤖 </span>
|
|
112
126
|
{t('starter_page.what_does_tutor_do')}
|
|
113
127
|
</Button>
|
|
114
128
|
<Button
|
|
115
129
|
variant='gradient-outline'
|
|
116
130
|
className='shrink-0 snap-end text-sm'
|
|
117
|
-
onClick={handleAskQuestion}
|
|
131
|
+
onClick={handleAskQuestion}
|
|
132
|
+
loading={!isSparkieReady}>
|
|
118
133
|
<span>📝 </span>
|
|
119
134
|
{t('starter_page.wanna_summary')}
|
|
120
135
|
</Button>
|
|
121
|
-
</
|
|
136
|
+
</HorizontalDraggableScroll>
|
|
122
137
|
</div>
|
|
123
138
|
</PageLayout>
|
|
124
139
|
)
|
|
@@ -3,7 +3,6 @@ import { useEffect, useState } from 'react'
|
|
|
3
3
|
import { DataHubStore } from '@/src/config/datahub'
|
|
4
4
|
import { initDayjs } from '@/src/config/dayjs'
|
|
5
5
|
import { initAxios } from '@/src/config/request/api'
|
|
6
|
-
import { SparkieService } from '@/src/modules/sparkie'
|
|
7
6
|
import type { WidgetSettingProps } from '@/src/types'
|
|
8
7
|
import { TutorWidgetEvents } from '../../events'
|
|
9
8
|
|
|
@@ -18,16 +17,6 @@ const init = async (settings: WidgetSettingProps) => {
|
|
|
18
17
|
sessionId: settings.sessionId,
|
|
19
18
|
userId: Number(settings.userId)
|
|
20
19
|
})
|
|
21
|
-
await SparkieService.initSparkie({
|
|
22
|
-
token: settings?.hotmartToken,
|
|
23
|
-
skipPresenceSetup: true,
|
|
24
|
-
retryOptions: {
|
|
25
|
-
maxRetries: 5,
|
|
26
|
-
retryDelay: 2000,
|
|
27
|
-
backoffMultiplier: 1.5
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
await SparkieService.ensureInitialized()
|
|
31
20
|
TutorWidgetEvents['tutor-app-widget-loaded'].dispatch()
|
|
32
21
|
} catch (error) {
|
|
33
22
|
console.error(error)
|
|
@@ -8,8 +8,8 @@ export type WidgetTabsProps = {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const INITIAL_PROPS: WidgetTabsProps = {
|
|
11
|
-
currentTab: '
|
|
12
|
-
history: new Set(['
|
|
11
|
+
currentTab: 'starter',
|
|
12
|
+
history: new Set(['starter'])
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const widgetTabsAtom = atom<WidgetTabsProps>(INITIAL_PROPS)
|