app-tutor-ai-consumer 1.21.2 → 1.22.1
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 +13 -0
- package/config/vitest/__mocks__/animation-avatar.tsx +3 -0
- package/config/vitest/vitest.config.mts +1 -0
- package/package.json +1 -1
- package/src/config/styles/index.css +26 -0
- package/src/development-bootstrap.tsx +3 -0
- package/src/lib/components/button/button-default.tsx +35 -0
- package/src/lib/components/button/button.tsx +31 -37
- package/src/lib/components/button/styles.module.css +6 -0
- package/src/lib/components/icons/icon-names.d.ts +2 -0
- package/src/lib/components/icons/send.svg +5 -3
- package/src/lib/components/icons/sparkle-tutor-light.svg +15 -0
- package/src/lib/components/icons/sparkle-tutor.svg +19 -0
- package/src/main/main.tsx +2 -2
- package/src/modules/messages/components/chat-input/chat-input.tsx +4 -4
- package/src/modules/messages/components/chat-input/styles.module.css +4 -0
- package/src/modules/messages/components/message-actions/message-actions.tsx +15 -8
- package/src/modules/messages/components/message-item/message-item.tsx +1 -1
- 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/ai-avatar/ai-avatar.tsx +19 -8
- package/src/modules/widget/components/avatar-animation/avatar-animation.tsx +1 -1
- package/src/modules/widget/components/container/container.tsx +2 -10
- package/src/modules/widget/components/greetings-card/greetings-card.tsx +5 -0
- package/src/modules/widget/components/header/header.spec.tsx +2 -2
- package/src/modules/widget/components/header/header.tsx +2 -2
- package/src/modules/widget/components/header/types.ts +1 -1
- package/src/modules/widget/components/information-page/constants.ts +3 -2
- package/src/modules/widget/components/information-page/information-page.tsx +4 -3
- package/src/modules/widget/components/scroll-to-bottom-button/scroll-to-bottom-button.tsx +2 -2
- package/src/modules/widget/components/starter-page/starter-page.spec.tsx +5 -1
- package/src/modules/widget/components/starter-page/starter-page.tsx +35 -18
- 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,16 @@
|
|
|
1
|
+
## [1.22.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.22.0...v1.22.1) (2025-08-06)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- qa issues part 1 ([6b99c06](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/6b99c06114fee13fb4d4dd68e4888392007988a4))
|
|
6
|
+
|
|
7
|
+
# [1.22.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.21.2...v1.22.0) (2025-08-04)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- add button default ([e10c478](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/e10c47844c68815df58a942250fef1fcd1d0f676))
|
|
12
|
+
- add useInitSparkie ([f3dd5f9](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/f3dd5f9d9542f755126e4fcc2af8e8a500703753))
|
|
13
|
+
|
|
1
14
|
## [1.21.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.21.1...v1.21.2) (2025-08-01)
|
|
2
15
|
|
|
3
16
|
## [1.21.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.21.0...v1.21.1) (2025-07-31)
|
|
@@ -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/package.json
CHANGED
|
@@ -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
|
+
}
|
|
@@ -23,6 +23,9 @@ if (devMode) {
|
|
|
23
23
|
'bg-ai-dark fixed bottom-5 right-5 w-[27rem] h-[min(43rem,calc(100vh-2.5rem))] max-h-[calc(100vh-2.5rem)] z-10 rounded-[0.625rem] border border-neutral-200 shadow-lg overflow-hidden flex flex-col'
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
+
// Theme setter
|
|
27
|
+
container.setAttribute('data-theme', 'dark')
|
|
28
|
+
|
|
26
29
|
root?.appendChild(container)
|
|
27
30
|
|
|
28
31
|
await window.startChatWidget(rootId, {
|
|
@@ -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-neutral-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,
|
|
@@ -53,35 +52,32 @@ 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
|
+
styles.gradientButton,
|
|
62
|
+
'group relative inline-flex items-center justify-center overflow-hidden rounded-lg p-[1px]',
|
|
63
|
+
{
|
|
64
|
+
'bg-gradient-to-br from-[var(--ai-color-gradient-primary)] to-[var(--ai-color-gradient-accent)] hover:text-neutral-1000':
|
|
65
|
+
!loading,
|
|
66
|
+
'shine-box cursor-not-allowed': loading
|
|
67
|
+
}
|
|
68
|
+
)}>
|
|
66
69
|
<span
|
|
67
70
|
data-label='text-content'
|
|
68
71
|
className={clsx(
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
72
|
+
'rounded-lg transition-all duration-75 ease-in',
|
|
73
|
+
'px-5 py-2.5',
|
|
74
|
+
'text-neutral-1000',
|
|
75
|
+
'bg-[var(--gradient-btn-hover-background)] hover:bg-[linear-gradient(to_right,_rgb(from_var(--gradient-btn-hover-foreground)_r_g_b_/_var(--gradient-btn-hover-foreground-opacity)),_rgb(from_var(--gradient-btn-hover-foreground)_r_g_b_/_var(--gradient-btn-hover-foreground-opacity))),_linear-gradient(to_right,_var(--gradient-btn-hover-background),_var(--gradient-btn-hover-background))] group-hover:bg-[linear-gradient(to_right,_rgb(from_var(--gradient-btn-hover-foreground)_r_g_b_/_var(--gradient-btn-hover-foreground-opacity)),_rgb(from_var(--gradient-btn-hover-foreground)_r_g_b_/_var(--gradient-btn-hover-foreground-opacity))),_linear-gradient(to_right,_var(--gradient-btn-hover-background),_var(--gradient-btn-hover-background))]',
|
|
76
|
+
'flex flex-1 flex-nowrap'
|
|
75
77
|
)}>
|
|
76
|
-
{children}
|
|
78
|
+
<span className={clsx({ 'opacity-20': loading })}>{children}</span>
|
|
77
79
|
</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>
|
|
80
|
+
</ButtonDefault>
|
|
85
81
|
)
|
|
86
82
|
case 'primary':
|
|
87
83
|
return (
|
|
@@ -150,13 +146,11 @@ function Button({
|
|
|
150
146
|
)
|
|
151
147
|
default:
|
|
152
148
|
return (
|
|
153
|
-
<
|
|
149
|
+
<ButtonDefault
|
|
154
150
|
className={clsx(
|
|
155
|
-
|
|
156
|
-
'rounded-full outline-none transition-colors duration-300 ease-in',
|
|
151
|
+
'rounded-full outline-none transition-colors duration-100',
|
|
157
152
|
{
|
|
158
|
-
'cursor-pointer
|
|
159
|
-
!props.disabled,
|
|
153
|
+
'cursor-pointer hover:bg-neutral-400 focus:bg-neutral-400': !props.disabled,
|
|
160
154
|
[disabledClasses]: props.disabled
|
|
161
155
|
},
|
|
162
156
|
styles.defaultButton,
|
|
@@ -166,7 +160,7 @@ function Button({
|
|
|
166
160
|
disabled={props.disabled || loading}
|
|
167
161
|
aria-busy={loading}>
|
|
168
162
|
<ButtonContent loading={loading}>{children}</ButtonContent>
|
|
169
|
-
</
|
|
163
|
+
</ButtonDefault>
|
|
170
164
|
)
|
|
171
165
|
}
|
|
172
166
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
.gradientButton {
|
|
2
|
+
--gradient-btn-hover-background: var(--hc-color-neutral-100);
|
|
3
|
+
--gradient-btn-hover-foreground: var(--hc-color-neutral-1000);
|
|
4
|
+
--gradient-btn-hover-foreground-opacity: 0.07;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
.tertiary {
|
|
2
8
|
&:hover {
|
|
3
9
|
background-color: hsl(from currentColor h s calc(l - 30));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
<svg
|
|
2
|
-
<path
|
|
3
|
-
|
|
1
|
+
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
3
|
+
d="M13.1236 6.53816L1.66737 0.903633C1.43153 0.787637 1.2011 0.845082 1.02441 1.04583C0.842459 1.25255 0.772227 1.56418 0.928775 1.87216L3.08081 6.10592C3.41727 6.76784 3.41727 7.56083 3.08081 8.22275L0.928776 12.4565C0.772227 12.7645 0.842459 13.0761 1.02441 13.2828C1.2011 13.4836 1.43153 13.541 1.66737 13.425L13.1236 7.7905C13.3469 7.68066 13.4839 7.44778 13.4839 7.16433C13.4839 6.88089 13.3469 6.648 13.1236 6.53816ZM13.4723 5.82916L2.01609 0.194624C0.81102 -0.398066 -0.407713 0.986573 0.224422 2.23019L2.37646 6.46395C2.59855 6.90088 2.59855 7.42779 2.37646 7.86472L0.224423 12.0985C-0.407713 13.3421 0.811018 14.7267 2.01609 14.134L13.4723 8.49951C14.5412 7.9738 14.5412 6.35487 13.4723 5.82916Z"
|
|
4
|
+
fill="currentColor" />
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="28" cy="28.05" r="27" stroke="url(#paint0_linear_23639_22377)" stroke-width="2"/>
|
|
3
|
+
<circle cx="28" cy="28.05" r="23" fill="#E6E9ED"/>
|
|
4
|
+
<path d="M12.3207 28.0869C12.2793 28.082 12.2793 28.0181 12.3207 28.0132C26.995 26.2646 27.3639 17.5043 27.9665 12.331C27.9713 12.2901 28.0287 12.2901 28.0335 12.331C28.6361 17.5043 29.005 26.2646 43.6793 28.0132C43.7207 28.0181 43.7207 28.082 43.6793 28.0869C29.005 29.8354 28.6361 38.5958 28.0335 43.7691C28.0287 43.81 27.9713 43.81 27.9665 43.7691C27.3639 38.5958 26.995 29.8354 12.3207 28.0869Z" fill="url(#paint1_linear_23639_22377)"/>
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id="paint0_linear_23639_22377" x1="0" y1="28.05" x2="56" y2="28.05" gradientUnits="userSpaceOnUse">
|
|
7
|
+
<stop stop-color="#44D0FF"/>
|
|
8
|
+
<stop offset="1" stop-color="#B48EFF"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
<linearGradient id="paint1_linear_23639_22377" x1="15" y1="11.8769" x2="40.3572" y2="44.2792" gradientUnits="userSpaceOnUse">
|
|
11
|
+
<stop stop-color="#44D0FF"/>
|
|
12
|
+
<stop offset="1" stop-color="#B48EFF"/>
|
|
13
|
+
</linearGradient>
|
|
14
|
+
</defs>
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="28" cy="28" r="27" stroke="url(#paint0_linear_23573_147690)" stroke-width="2" />
|
|
3
|
+
<circle cx="28" cy="28" r="23" fill="#282C2F" />
|
|
4
|
+
<path
|
|
5
|
+
d="M12.3207 28.0368C12.2793 28.0319 12.2793 27.9681 12.3207 27.9632C26.995 26.2146 27.3639 17.4543 27.9665 12.2809C27.9713 12.24 28.0287 12.24 28.0335 12.2809C28.6361 17.4543 29.005 26.2146 43.6793 27.9632C43.7207 27.9681 43.7207 28.0319 43.6793 28.0368C29.005 29.7854 28.6361 38.5457 28.0335 43.7191C28.0287 43.76 27.9713 43.76 27.9665 43.7191C27.3639 38.5457 26.995 29.7854 12.3207 28.0368Z"
|
|
6
|
+
fill="url(#paint1_linear_23573_147690)" />
|
|
7
|
+
<defs>
|
|
8
|
+
<linearGradient id="paint0_linear_23573_147690" x1="0" y1="28" x2="56" y2="28"
|
|
9
|
+
gradientUnits="userSpaceOnUse">
|
|
10
|
+
<stop stop-color="#44D0FF" />
|
|
11
|
+
<stop offset="1" stop-color="#B48EFF" />
|
|
12
|
+
</linearGradient>
|
|
13
|
+
<linearGradient id="paint1_linear_23573_147690" x1="15" y1="11.8268" x2="40.3572" y2="44.2291"
|
|
14
|
+
gradientUnits="userSpaceOnUse">
|
|
15
|
+
<stop stop-color="#44D0FF" />
|
|
16
|
+
<stop offset="1" stop-color="#B48EFF" />
|
|
17
|
+
</linearGradient>
|
|
18
|
+
</defs>
|
|
19
|
+
</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
|
)
|
|
@@ -85,15 +85,15 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
|
|
85
85
|
<Button
|
|
86
86
|
onClick={onSend}
|
|
87
87
|
disabled={buttonDisabled || loading}
|
|
88
|
-
className={clsx('flex
|
|
89
|
-
'text-neutral-
|
|
90
|
-
'text-neutral-
|
|
88
|
+
className={clsx('flex flex-col items-center justify-center', styles.send, {
|
|
89
|
+
'bg-neutral-900 text-neutral-100': !buttonDisabled,
|
|
90
|
+
'text-neutral-900': buttonDisabled
|
|
91
91
|
})}
|
|
92
92
|
loading={loading}
|
|
93
93
|
aria-label='Submit Button'>
|
|
94
94
|
<Icon
|
|
95
95
|
name='send'
|
|
96
|
-
className='h-4 w-4
|
|
96
|
+
className={clsx('ml-0.5 h-4 w-4 p-0.5 text-current transition-colors duration-150')}
|
|
97
97
|
/>
|
|
98
98
|
</Button>
|
|
99
99
|
</div>
|
|
@@ -50,33 +50,40 @@ function MessageActions({ message, className, showActions = false }: MessageActi
|
|
|
50
50
|
{dayjs(message.timestamp).format('DD/MM [•] LT')}
|
|
51
51
|
</span>
|
|
52
52
|
<div
|
|
53
|
-
style={{ '--custom-btn-padding': '0.
|
|
54
|
-
className={clsx('flex flex-nowrap gap-
|
|
53
|
+
style={{ '--custom-btn-padding': '0.125rem' } as CSSProperties}
|
|
54
|
+
className={clsx('flex flex-nowrap gap-3 text-neutral-600', {
|
|
55
55
|
hidden: !showActions
|
|
56
56
|
})}>
|
|
57
|
-
<Button
|
|
57
|
+
<Button
|
|
58
|
+
className='hover:!bg-transparent hover:text-info-500 focus:!bg-transparent'
|
|
59
|
+
onClick={() => handleReaction()}
|
|
60
|
+
aria-label={t('general.buttons.like')}>
|
|
58
61
|
<Icon
|
|
59
62
|
name='like'
|
|
60
|
-
className={clsx('
|
|
63
|
+
className={clsx('size-3', {
|
|
61
64
|
'text-info-500': reaction === ButtonReactions.LIKE
|
|
62
65
|
})}
|
|
63
66
|
/>
|
|
64
67
|
</Button>
|
|
65
68
|
<Button
|
|
66
|
-
className='rotate-180 scale-x-[-1]'
|
|
69
|
+
className='rotate-180 scale-x-[-1] hover:!bg-transparent hover:text-danger-500 focus:!bg-transparent'
|
|
67
70
|
onClick={() => handleReaction(ButtonReactions.DISLIKE)}
|
|
68
71
|
aria-label={t('general.buttons.dislike')}>
|
|
69
72
|
<Icon
|
|
70
73
|
name='like'
|
|
71
|
-
className={clsx('
|
|
74
|
+
className={clsx('size-3', {
|
|
72
75
|
'text-danger-500': reaction === ButtonReactions.DISLIKE
|
|
73
76
|
})}
|
|
74
77
|
/>
|
|
75
78
|
</Button>
|
|
76
|
-
<Button
|
|
79
|
+
<Button
|
|
80
|
+
className='hover:!bg-transparent hover:text-info-500 focus:!bg-transparent'
|
|
81
|
+
onClick={copyToClipboard}
|
|
82
|
+
aria-label={t('general.buttons.copy')}
|
|
83
|
+
disabled={copying}>
|
|
77
84
|
<Icon
|
|
78
85
|
name={copied ? 'paste' : 'copy'}
|
|
79
|
-
className={clsx('
|
|
86
|
+
className={clsx('size-3', {
|
|
80
87
|
'text-info-500': copied
|
|
81
88
|
})}
|
|
82
89
|
/>
|
|
@@ -17,7 +17,7 @@ function MessageItem({ message }: { message: ParsedMessage }) {
|
|
|
17
17
|
return (
|
|
18
18
|
<div
|
|
19
19
|
className={clsx(
|
|
20
|
-
'flex max-w-[min(90%,52rem)] flex-col items-end gap-
|
|
20
|
+
'flex max-w-[min(90%,52rem)] flex-col items-end gap-1 text-sm/normal text-neutral-900',
|
|
21
21
|
{
|
|
22
22
|
'self-end': messageFromUser
|
|
23
23
|
}
|
|
@@ -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
|
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@/src/lib/components'
|
|
4
|
+
import { useWidgetSettingsAtomValue } from '../../store'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export type AIAvatarProps = { className?: string }
|
|
6
|
+
export type AIAvatarProps = { className?: string; size?: 'sm' | 'lg' }
|
|
8
7
|
|
|
9
8
|
function AIAvatar({
|
|
10
|
-
className = 'rounded-full border-4 border-neutral-100 bg-neutral-200'
|
|
9
|
+
className = 'rounded-full border-4 border-neutral-100 bg-neutral-200',
|
|
10
|
+
size = 'sm'
|
|
11
11
|
}: AIAvatarProps) {
|
|
12
|
+
const settings = useWidgetSettingsAtomValue()
|
|
13
|
+
const isDarkTheme = settings?.config?.theme === 'dark'
|
|
12
14
|
return (
|
|
13
15
|
<figure
|
|
14
|
-
className={clsx(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
className={clsx(
|
|
17
|
+
'flex items-center justify-center rounded-full',
|
|
18
|
+
{
|
|
19
|
+
'bg-neutral-100': isDarkTheme,
|
|
20
|
+
'bg-white': !isDarkTheme
|
|
21
|
+
},
|
|
22
|
+
className
|
|
23
|
+
)}>
|
|
24
|
+
<Icon
|
|
25
|
+
name={!isDarkTheme ? 'sparkle-tutor-light' : 'sparkle-tutor'}
|
|
26
|
+
className={clsx({ 'h-9 w-9': size === 'sm', 'h-14 w-14': size === 'lg' })}
|
|
27
|
+
aria-label='AI avatar Icon'
|
|
28
|
+
/>
|
|
18
29
|
</figure>
|
|
19
30
|
)
|
|
20
31
|
}
|
|
@@ -4,7 +4,7 @@ const AVATAR_ANIMATION_URL = `${process.env.STATIC_URL}/tutor/tutor_sparkle.lott
|
|
|
4
4
|
|
|
5
5
|
const AvatarAnimation = () => {
|
|
6
6
|
return (
|
|
7
|
-
<div className='flex h-11 w-11 items-center justify-center rounded-lg bg-
|
|
7
|
+
<div className='flex h-11 w-11 items-center justify-center rounded-lg bg-neutral-300'>
|
|
8
8
|
<DotLottieReact src={AVATAR_ANIMATION_URL} loop autoplay className='h-auto w-full' />
|
|
9
9
|
</div>
|
|
10
10
|
)
|
|
@@ -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]}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
2
|
import { useTranslation } from 'react-i18next'
|
|
3
3
|
|
|
4
|
+
import { AIAvatar } from '../ai-avatar'
|
|
5
|
+
|
|
4
6
|
export type GreetingsCardProps = {
|
|
5
7
|
tutorName: string
|
|
6
8
|
author?: string
|
|
@@ -12,6 +14,9 @@ function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCard
|
|
|
12
14
|
|
|
13
15
|
return (
|
|
14
16
|
<div className='flex flex-col items-center justify-center'>
|
|
17
|
+
<div className='max-md:hidden md:mb-4 md:block'>
|
|
18
|
+
<AIAvatar size='lg' />
|
|
19
|
+
</div>
|
|
15
20
|
<div className='flex flex-col items-center justify-center gap-4 text-center'>
|
|
16
21
|
<div className='flex flex-col gap-2'>
|
|
17
22
|
<span
|
|
@@ -19,7 +19,7 @@ describe('<WidgetHeader />', () => {
|
|
|
19
19
|
it('should render WidgetHeaderContent when prop showContent is true', () => {
|
|
20
20
|
renderComponent()
|
|
21
21
|
|
|
22
|
-
expect(screen.getByText(/
|
|
22
|
+
expect(screen.getByText(/sparkle-tutor-light/i)).toBeInTheDocument()
|
|
23
23
|
|
|
24
24
|
expect(screen.queryByRole('button', { name: /Arrow Left Icon/i })).not.toBeInTheDocument()
|
|
25
25
|
})
|
|
@@ -33,7 +33,7 @@ describe('<WidgetHeader />', () => {
|
|
|
33
33
|
|
|
34
34
|
expect(screen.getByRole('button', { name: /Arrow Left Icon/i })).toBeInTheDocument()
|
|
35
35
|
|
|
36
|
-
expect(screen.queryByText(/
|
|
36
|
+
expect(screen.queryByText(/sparkle-tutor-light/i)).not.toBeInTheDocument()
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
it('should be able to render the remaining icons', () => {
|
|
@@ -43,7 +43,7 @@ export function WidgetHeaderContentWithoutMeta({ name }: { name?: string }) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function WidgetHeader({
|
|
46
|
-
enabledButtons,
|
|
46
|
+
enabledButtons = [],
|
|
47
47
|
tutorName,
|
|
48
48
|
showContentWithoutMeta,
|
|
49
49
|
showContent = true
|
|
@@ -73,7 +73,7 @@ function WidgetHeader({
|
|
|
73
73
|
</Button>
|
|
74
74
|
</div>
|
|
75
75
|
</div>
|
|
76
|
-
<div className='grid-areas-[a_b] grid grid-cols-[1fr_auto] items-center'>
|
|
76
|
+
<div className='grid-areas-[a_b] grid grid-cols-[1fr_auto] items-center pb-4'>
|
|
77
77
|
<div className='grid-area-[a] relative'>
|
|
78
78
|
{showContent && !showContentWithoutMeta && <WidgetHeaderContent tutorName={name} />}
|
|
79
79
|
{showContentWithoutMeta && !showContent && <WidgetHeaderContentWithoutMeta name={name} />}
|
|
@@ -3,7 +3,7 @@ import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
|
|
|
3
3
|
export type WidgetHeaderContentProps = { tutorName?: string }
|
|
4
4
|
|
|
5
5
|
export type WidgetHeaderProps = {
|
|
6
|
-
enabledButtons
|
|
6
|
+
enabledButtons?: ValidIconNames[]
|
|
7
7
|
showContent?: boolean
|
|
8
8
|
showContentWithoutMeta?: boolean
|
|
9
9
|
} & WidgetHeaderContentProps
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t } from '@/src/config/i18n'
|
|
1
2
|
import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
|
|
2
3
|
|
|
3
4
|
type InfoItem = {
|
|
@@ -6,10 +7,10 @@ type InfoItem = {
|
|
|
6
7
|
descKey: string
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export const infoItems: InfoItem[] = [
|
|
10
|
+
export const infoItems: (config: { tutorName: string }) => InfoItem[] = ({ tutorName }) => [
|
|
10
11
|
{
|
|
11
12
|
icon: 'interrogation',
|
|
12
|
-
titleKey: 'info.what_it_does_question',
|
|
13
|
+
titleKey: t('info.what_it_does_question', { tutor_name: tutorName }),
|
|
13
14
|
descKey: 'info.what_it_does_answer'
|
|
14
15
|
},
|
|
15
16
|
{
|
|
@@ -13,6 +13,7 @@ function WidgetInformationPage() {
|
|
|
13
13
|
const { t } = useTranslation()
|
|
14
14
|
const [settings] = useWidgetSettingsAtom()
|
|
15
15
|
const isDarkMode = settings?.config?.theme === 'dark'
|
|
16
|
+
const tutorName = settings?.tutorName ?? t('general.name')
|
|
16
17
|
|
|
17
18
|
return (
|
|
18
19
|
<PageLayout className='flex min-h-0 flex-col text-neutral-900 max-md:p-[1.125rem] md:p-5'>
|
|
@@ -27,20 +28,20 @@ function WidgetInformationPage() {
|
|
|
27
28
|
|
|
28
29
|
<div className='my-8 flex justify-center'>
|
|
29
30
|
<div className='flex flex-col items-center gap-2'>
|
|
30
|
-
<AIAvatar />
|
|
31
|
+
<AIAvatar size='lg' />
|
|
31
32
|
|
|
32
33
|
<h3
|
|
33
34
|
className={clsx('font-bold', {
|
|
34
35
|
'text-white': isDarkMode,
|
|
35
36
|
'text-neutral-700': !isDarkMode
|
|
36
37
|
})}>
|
|
37
|
-
{
|
|
38
|
+
{tutorName}
|
|
38
39
|
</h3>
|
|
39
40
|
</div>
|
|
40
41
|
</div>
|
|
41
42
|
|
|
42
43
|
<div className='flex flex-col gap-5'>
|
|
43
|
-
{infoItems.map((item) => (
|
|
44
|
+
{infoItems({ tutorName }).map((item) => (
|
|
44
45
|
<InformationCard
|
|
45
46
|
key={item.titleKey}
|
|
46
47
|
icon={item.icon}
|
|
@@ -15,8 +15,8 @@ const ScrollToBottomButton = forwardRef<HTMLButtonElement, IScrollToBottomButton
|
|
|
15
15
|
{...props}
|
|
16
16
|
ref={ref}
|
|
17
17
|
className={clsx(
|
|
18
|
-
'absolute bottom-4 left-1/2 flex size-7 cursor-pointer flex-col items-center justify-center rounded-full bg-neutral-
|
|
19
|
-
{ 'opacity-
|
|
18
|
+
'absolute bottom-4 left-1/2 flex size-7 -translate-x-1/2 cursor-pointer flex-col items-center justify-center rounded-full border border-neutral-500 bg-neutral-300 text-sm text-neutral-900 outline-none transition-colors duration-300 ease-in hover:scale-110 hover:bg-neutral-400 focus:outline-none focus:ring-neutral-500 focus:ring-offset-2 focus-visible:ring-2 active:ring-2',
|
|
19
|
+
{ 'opacity-90': show, 'pointer-events-none opacity-0': !show },
|
|
20
20
|
className
|
|
21
21
|
)}
|
|
22
22
|
onClick={onClick}
|
|
@@ -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
|
|
|
@@ -23,7 +27,7 @@ describe('WidgetStarterPage', () => {
|
|
|
23
27
|
expect(
|
|
24
28
|
screen.getByRole('button', { name: /starter_page.what_does_tutor_do/i })
|
|
25
29
|
).toBeInTheDocument()
|
|
26
|
-
expect(screen.getByRole('button', { name: /starter_page.
|
|
30
|
+
expect(screen.getByRole('button', { name: /starter_page.test_me/i })).toBeInTheDocument()
|
|
27
31
|
})
|
|
28
32
|
|
|
29
33
|
it('should post the slider button text content to the backend', async () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef } from 'react'
|
|
2
2
|
import { useQueryClient } from '@tanstack/react-query'
|
|
3
|
-
import
|
|
4
|
-
import type { MouseEventHandler } from 'react'
|
|
3
|
+
import type { CSSProperties, MouseEventHandler } from 'react'
|
|
5
4
|
import { useTranslation } from 'react-i18next'
|
|
6
5
|
|
|
7
6
|
import { Button, HorizontalDraggableScroll } from '@/src/lib/components'
|
|
@@ -10,13 +9,12 @@ import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/compone
|
|
|
10
9
|
import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/hooks'
|
|
11
10
|
import { useMessagesMaxCount } from '@/src/modules/messages/store'
|
|
12
11
|
import { useGetProfile } from '@/src/modules/profile'
|
|
12
|
+
import { useInitSparkie } from '@/src/modules/sparkie/hooks/use-init-sparkie'
|
|
13
13
|
import { useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
|
|
14
14
|
import { GreetingsCard } from '../greetings-card'
|
|
15
15
|
import { WidgetHeader } from '../header'
|
|
16
16
|
import { PageLayout } from '../page-layout'
|
|
17
17
|
|
|
18
|
-
import styles from './styles.module.css'
|
|
19
|
-
|
|
20
18
|
function WidgetStarterPage() {
|
|
21
19
|
const { t } = useTranslation()
|
|
22
20
|
const [settings] = useWidgetSettingsAtom()
|
|
@@ -28,6 +26,8 @@ function WidgetStarterPage() {
|
|
|
28
26
|
const limit = useMessagesMaxCount()
|
|
29
27
|
const queryClient = useQueryClient()
|
|
30
28
|
const name = settings?.tutorName ?? t('general.name')
|
|
29
|
+
const isDarkTheme = settings?.config?.theme === 'dark'
|
|
30
|
+
const isSparkieReady = useInitSparkie()
|
|
31
31
|
|
|
32
32
|
useRefEventListener<HTMLTextAreaElement>({
|
|
33
33
|
config: {
|
|
@@ -74,9 +74,21 @@ function WidgetStarterPage() {
|
|
|
74
74
|
[conversationId, limit, profileId]
|
|
75
75
|
)
|
|
76
76
|
|
|
77
|
+
const actionButtonStyle = useMemo(
|
|
78
|
+
() =>
|
|
79
|
+
isDarkTheme
|
|
80
|
+
? undefined
|
|
81
|
+
: ({
|
|
82
|
+
'--gradient-btn-hover-foreground': '#0d0d0d'
|
|
83
|
+
} as CSSProperties),
|
|
84
|
+
[isDarkTheme]
|
|
85
|
+
)
|
|
86
|
+
|
|
77
87
|
useEffect(() => {
|
|
88
|
+
if (!conversationId || !profileId) return
|
|
89
|
+
|
|
78
90
|
void queryClient.prefetchInfiniteQuery(messagesQueryConfig)
|
|
79
|
-
}, [messagesQueryConfig, queryClient])
|
|
91
|
+
}, [conversationId, messagesQueryConfig, profileId, queryClient])
|
|
80
92
|
|
|
81
93
|
return (
|
|
82
94
|
<PageLayout
|
|
@@ -85,38 +97,43 @@ function WidgetStarterPage() {
|
|
|
85
97
|
name='new-chat-msg-input'
|
|
86
98
|
ref={chatInputRef}
|
|
87
99
|
onSend={handleSend}
|
|
88
|
-
buttonDisabled={!chatInputValue.trim()}
|
|
100
|
+
buttonDisabled={!chatInputValue.trim() || !isSparkieReady}
|
|
89
101
|
/>
|
|
90
102
|
}>
|
|
91
103
|
<div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
|
|
92
|
-
<div
|
|
93
|
-
|
|
94
|
-
[
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
<div className='grid-area-[a] flex min-h-0 flex-col max-md:px-[1.125rem] max-md:pt-[1.125rem] md:px-5 md:pt-5'>
|
|
105
|
+
<WidgetHeader
|
|
106
|
+
enabledButtons={isSparkieReady ? ['close', 'archive', 'info'] : ['close', 'info']}
|
|
107
|
+
showContent={false}
|
|
108
|
+
tutorName={name}
|
|
109
|
+
/>
|
|
97
110
|
|
|
98
111
|
<div className='my-auto'>
|
|
99
112
|
<GreetingsCard
|
|
100
|
-
author={settings?.user?.name}
|
|
113
|
+
author={settings?.user?.name?.split(' ')?.[0]}
|
|
101
114
|
tutorName={name}
|
|
102
115
|
isDarkTheme={settings?.config?.theme === 'dark'}
|
|
103
116
|
/>
|
|
104
117
|
</div>
|
|
105
118
|
</div>
|
|
106
|
-
<HorizontalDraggableScroll className='grid-area-[b]
|
|
119
|
+
<HorizontalDraggableScroll className='grid-area-[b] my-4 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
|
-
|
|
110
|
-
|
|
122
|
+
style={actionButtonStyle}
|
|
123
|
+
className='ml-5 shrink-0 text-sm'
|
|
124
|
+
onClick={handleAskQuestion}
|
|
125
|
+
loading={!isSparkieReady}>
|
|
111
126
|
<span>🤖 </span>
|
|
112
127
|
{t('starter_page.what_does_tutor_do')}
|
|
113
128
|
</Button>
|
|
114
129
|
<Button
|
|
115
130
|
variant='gradient-outline'
|
|
116
|
-
|
|
117
|
-
|
|
131
|
+
style={actionButtonStyle}
|
|
132
|
+
className='mr-5 shrink-0 text-sm'
|
|
133
|
+
onClick={handleAskQuestion}
|
|
134
|
+
loading={!isSparkieReady}>
|
|
118
135
|
<span>📝 </span>
|
|
119
|
-
{t('starter_page.
|
|
136
|
+
{t('starter_page.test_me')}
|
|
120
137
|
</Button>
|
|
121
138
|
</HorizontalDraggableScroll>
|
|
122
139
|
</div>
|
|
@@ -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)
|