app-tutor-ai-consumer 1.22.0 → 1.22.2
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__/window.ts +11 -0
- package/config/vitest/vitest.config.mts +1 -0
- package/package.json +1 -1
- package/src/config/datahub/actions.ts +6 -0
- package/src/config/datahub/constants.ts +3 -6
- package/src/config/datahub/entities.ts +2 -1
- package/src/config/datahub/schemas/base-click-schema.ts +48 -0
- package/src/config/datahub/schemas/base-try-schema.ts +64 -0
- package/src/config/datahub/schemas/tutor/__tests__/click-hotmart-tutor.spec.ts +2 -1
- package/src/config/datahub/schemas/tutor/click-hotmart-tutor.ts +2 -1
- package/src/config/datahub/schemas/tutor/click-tutor-minimize.ts +24 -0
- package/src/config/datahub/schemas/tutor/index.ts +1 -0
- package/src/config/datahub/schemas/tutor/try-product-tutor.ts +24 -0
- package/src/config/datahub/types.ts +3 -1
- package/src/config/theme/constants.ts +7 -0
- package/src/development-bootstrap.tsx +3 -0
- package/src/lib/components/button/button-default.tsx +1 -1
- package/src/lib/components/button/button.tsx +16 -14
- package/src/lib/components/button/styles.module.css +6 -0
- package/src/lib/components/errors/generic/generic-error.tsx +9 -25
- package/src/lib/components/icons/copy-solid.svg +5 -0
- package/src/lib/components/icons/icon-names.d.ts +4 -0
- package/src/lib/components/icons/like-solid.svg +5 -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/lib/components/index.ts +1 -0
- package/src/lib/components/tooltip/index.ts +2 -0
- package/src/lib/components/tooltip/styles.module.css +39 -0
- package/src/lib/components/tooltip/tooltip.tsx +41 -0
- package/src/lib/hooks/index.ts +1 -0
- package/src/lib/hooks/use-media-query/index.ts +2 -0
- package/src/lib/hooks/use-media-query/use-media-query.tsx +20 -0
- package/src/main/main.tsx +1 -1
- package/src/modules/messages/components/chat-input/chat-input.tsx +5 -4
- package/src/modules/messages/components/chat-input/styles.module.css +4 -0
- package/src/modules/messages/components/message-actions/message-actions.tsx +21 -14
- package/src/modules/messages/components/message-item/message-item.tsx +1 -1
- 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/greetings-card/greetings-card.tsx +5 -0
- package/src/modules/widget/components/header/header.spec.tsx +34 -6
- package/src/modules/widget/components/header/header.tsx +35 -23
- 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 +1 -1
- package/src/modules/widget/components/starter-page/starter-page.tsx +24 -22
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import type { PropsWithChildren, ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import styles from './styles.module.css'
|
|
5
|
+
|
|
6
|
+
const POSITION = {
|
|
7
|
+
top: 'left-1/2 transform -translate-x-1/2 bottom-full mb-3',
|
|
8
|
+
right: 'left-full top-1/2 transform -translate-y-1/2 ml-3',
|
|
9
|
+
bottom: 'left-1/2 transform -translate-x-1/2 top-full mt-3',
|
|
10
|
+
left: 'right-full top-1/2 transform -translate-y-1/2 mr-3'
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
export type TooltipProps = PropsWithChildren<{
|
|
14
|
+
content?: ReactNode
|
|
15
|
+
position?: keyof typeof POSITION
|
|
16
|
+
show?: boolean
|
|
17
|
+
}>
|
|
18
|
+
|
|
19
|
+
function Tooltip({ children, content, position = 'top', show = true }: TooltipProps) {
|
|
20
|
+
if (!show) return children
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className='group relative flex items-center justify-center'>
|
|
24
|
+
{children}
|
|
25
|
+
{content && (
|
|
26
|
+
<div
|
|
27
|
+
className={clsx(
|
|
28
|
+
'absolute hidden rounded-lg group-hover:block',
|
|
29
|
+
'bg-neutral-300 px-4 py-2 text-xs text-neutral-900',
|
|
30
|
+
POSITION[position],
|
|
31
|
+
styles.triangle,
|
|
32
|
+
styles[position]
|
|
33
|
+
)}>
|
|
34
|
+
{content}
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default Tooltip
|
package/src/lib/hooks/index.ts
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { SCREEN_SIZES } from '@/src/config/theme/constants'
|
|
4
|
+
|
|
5
|
+
function useMediaQuery({ maxSize }: { maxSize: keyof typeof SCREEN_SIZES }) {
|
|
6
|
+
const [matches, setMatches] = useState(false)
|
|
7
|
+
|
|
8
|
+
useLayoutEffect(() => {
|
|
9
|
+
const mediaquery = window.matchMedia(`(max-width: ${SCREEN_SIZES[maxSize]}px)`)
|
|
10
|
+
const listener = () => setMatches(mediaquery.matches)
|
|
11
|
+
|
|
12
|
+
mediaquery.addEventListener('change', listener)
|
|
13
|
+
|
|
14
|
+
return () => mediaquery.removeEventListener('change', listener)
|
|
15
|
+
}, [maxSize])
|
|
16
|
+
|
|
17
|
+
return matches
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default useMediaQuery
|
package/src/main/main.tsx
CHANGED
|
@@ -17,7 +17,7 @@ function Main({ settings }: MainProps) {
|
|
|
17
17
|
useListenToThemeChangeEvent()
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
|
-
<ErrorBoundary fallback={<GenericError />}>
|
|
20
|
+
<ErrorBoundary fallback={<GenericError isDarkMode={settings.config?.theme === 'dark'} />}>
|
|
21
21
|
<GlobalProviders settings={settings}>
|
|
22
22
|
<WidgetContainer />
|
|
23
23
|
</GlobalProviders>
|
|
@@ -85,15 +85,16 @@ 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
|
-
|
|
88
|
+
className={clsx('flex flex-col items-center justify-center', styles.send, {
|
|
89
|
+
'bg-neutral-900 text-neutral-100 hover:text-neutral-900 focus:text-neutral-900':
|
|
90
|
+
!buttonDisabled,
|
|
91
|
+
'text-neutral-900': buttonDisabled
|
|
91
92
|
})}
|
|
92
93
|
loading={loading}
|
|
93
94
|
aria-label='Submit Button'>
|
|
94
95
|
<Icon
|
|
95
96
|
name='send'
|
|
96
|
-
className='h-4 w-4
|
|
97
|
+
className={clsx('ml-0.5 h-4 w-4 p-0.5 text-current transition-colors duration-150')}
|
|
97
98
|
/>
|
|
98
99
|
</Button>
|
|
99
100
|
</div>
|
|
@@ -50,34 +50,41 @@ 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-neutral-500 focus:!bg-transparent'
|
|
59
|
+
onClick={() => handleReaction()}
|
|
60
|
+
aria-label={t('general.buttons.like')}>
|
|
58
61
|
<Icon
|
|
59
|
-
name='like'
|
|
60
|
-
className={clsx('
|
|
61
|
-
'text-
|
|
62
|
+
name={reaction === ButtonReactions.LIKE ? 'like-solid' : 'like'}
|
|
63
|
+
className={clsx('size-3 transition-colors duration-100', {
|
|
64
|
+
'text-neutral-900': 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-neutral-500 focus:!bg-transparent'
|
|
67
70
|
onClick={() => handleReaction(ButtonReactions.DISLIKE)}
|
|
68
71
|
aria-label={t('general.buttons.dislike')}>
|
|
69
72
|
<Icon
|
|
70
|
-
name='like'
|
|
71
|
-
className={clsx('
|
|
72
|
-
'text-
|
|
73
|
+
name={reaction === ButtonReactions.DISLIKE ? 'like-solid' : 'like'}
|
|
74
|
+
className={clsx('size-3 transition-colors duration-100', {
|
|
75
|
+
'text-neutral-900': reaction === ButtonReactions.DISLIKE
|
|
73
76
|
})}
|
|
74
77
|
/>
|
|
75
78
|
</Button>
|
|
76
|
-
<Button
|
|
79
|
+
<Button
|
|
80
|
+
className='hover:!bg-transparent hover:text-neutral-500 focus:!bg-transparent'
|
|
81
|
+
onClick={copyToClipboard}
|
|
82
|
+
aria-label={t('general.buttons.copy')}
|
|
83
|
+
disabled={copying}>
|
|
77
84
|
<Icon
|
|
78
|
-
name={copied ? '
|
|
79
|
-
className={clsx('
|
|
80
|
-
'text-
|
|
85
|
+
name={copied ? 'copy-solid' : 'copy'}
|
|
86
|
+
className={clsx('size-3 transition-colors duration-100', {
|
|
87
|
+
'text-neutral-900': copied
|
|
81
88
|
})}
|
|
82
89
|
/>
|
|
83
90
|
</Button>
|
|
@@ -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
|
}
|
|
@@ -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,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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, screen } from '@/src/config/tests'
|
|
2
|
+
import * as Hooks from '@/src/lib/hooks'
|
|
2
3
|
|
|
3
4
|
import WidgetHeaderPropsBuilder from './__tests__/widget-header-props.builder'
|
|
4
5
|
import WidgetHeader from './header'
|
|
@@ -12,14 +13,18 @@ describe('<WidgetHeader />', () => {
|
|
|
12
13
|
|
|
13
14
|
expect(screen.getByRole('button', { name: /Close Icon/i })).toBeInTheDocument()
|
|
14
15
|
|
|
15
|
-
expect(
|
|
16
|
-
|
|
16
|
+
expect(
|
|
17
|
+
screen.queryByRole('button', { name: /general.buttons.archive Icon/i })
|
|
18
|
+
).not.toBeInTheDocument()
|
|
19
|
+
expect(
|
|
20
|
+
screen.queryByRole('button', { name: /general.buttons.info Icon/i })
|
|
21
|
+
).not.toBeInTheDocument()
|
|
17
22
|
})
|
|
18
23
|
|
|
19
24
|
it('should render WidgetHeaderContent when prop showContent is true', () => {
|
|
20
25
|
renderComponent()
|
|
21
26
|
|
|
22
|
-
expect(screen.getByText(/
|
|
27
|
+
expect(screen.getByText(/sparkle-tutor-light/i)).toBeInTheDocument()
|
|
23
28
|
|
|
24
29
|
expect(screen.queryByRole('button', { name: /Arrow Left Icon/i })).not.toBeInTheDocument()
|
|
25
30
|
})
|
|
@@ -33,7 +38,7 @@ describe('<WidgetHeader />', () => {
|
|
|
33
38
|
|
|
34
39
|
expect(screen.getByRole('button', { name: /Arrow Left Icon/i })).toBeInTheDocument()
|
|
35
40
|
|
|
36
|
-
expect(screen.queryByText(/
|
|
41
|
+
expect(screen.queryByText(/sparkle-tutor-light/i)).not.toBeInTheDocument()
|
|
37
42
|
})
|
|
38
43
|
|
|
39
44
|
it('should be able to render the remaining icons', () => {
|
|
@@ -41,7 +46,30 @@ describe('<WidgetHeader />', () => {
|
|
|
41
46
|
|
|
42
47
|
renderComponent(props)
|
|
43
48
|
|
|
44
|
-
expect(
|
|
45
|
-
|
|
49
|
+
expect(
|
|
50
|
+
screen.getByRole('button', { name: /general.buttons.archive Icon/i })
|
|
51
|
+
).toBeInTheDocument()
|
|
52
|
+
expect(screen.getByRole('button', { name: /general.buttons.info Icon/i })).toBeInTheDocument()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should show header buttons tooltip when window is not in mobile view', () => {
|
|
56
|
+
const props = new WidgetHeaderPropsBuilder().withEnabledButtons(['archive', 'info'])
|
|
57
|
+
|
|
58
|
+
renderComponent(props)
|
|
59
|
+
|
|
60
|
+
expect(screen.getByText(/general.buttons.archive/i)).toBeInTheDocument()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should not show header buttons tooltip when window is in mobile view', () => {
|
|
64
|
+
vi.spyOn(Hooks, 'useMediaQuery').mockReturnValue(true)
|
|
65
|
+
|
|
66
|
+
const props = new WidgetHeaderPropsBuilder().withEnabledButtons(['archive', 'info'])
|
|
67
|
+
|
|
68
|
+
renderComponent(props)
|
|
69
|
+
|
|
70
|
+
expect(
|
|
71
|
+
screen.getByRole('button', { name: /general.buttons.archive Icon/i })
|
|
72
|
+
).toBeInTheDocument()
|
|
73
|
+
expect(screen.queryByText(/general.buttons.archive/i)).not.toBeInTheDocument()
|
|
46
74
|
})
|
|
47
75
|
})
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
2
|
import { useTranslation } from 'react-i18next'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { DataHubService } from '@/src/config/datahub'
|
|
5
|
+
import { ClickTutorMinimizeSchema } from '@/src/config/datahub/schemas/tutor'
|
|
6
|
+
import { Button, Icon, Tooltip } from '@/src/lib/components'
|
|
7
|
+
import { useMediaQuery } from '@/src/lib/hooks'
|
|
5
8
|
import { TutorWidgetEvents } from '../../events'
|
|
6
9
|
import { useWidgetGoBackTabAtom, useWidgetTabsAtom } from '../../store'
|
|
7
10
|
import { AIAvatar } from '../ai-avatar'
|
|
@@ -22,7 +25,9 @@ export function WidgetHeaderContent({ tutorName }: WidgetHeaderContentProps) {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export function WidgetHeaderContentWithoutMeta({ name }: { name?: string }) {
|
|
28
|
+
const { t } = useTranslation()
|
|
25
29
|
const [, goBack] = useWidgetGoBackTabAtom()
|
|
30
|
+
const tutorName = name ?? t('general.name')
|
|
26
31
|
|
|
27
32
|
return (
|
|
28
33
|
<div
|
|
@@ -35,7 +40,7 @@ export function WidgetHeaderContentWithoutMeta({ name }: { name?: string }) {
|
|
|
35
40
|
</Button>
|
|
36
41
|
<div className='grid-area-[b] flex min-h-6 justify-center text-center'>
|
|
37
42
|
<span className='absolute bottom-0 left-1/2 -translate-x-1/2 text-base font-bold'>
|
|
38
|
-
{
|
|
43
|
+
{tutorName}
|
|
39
44
|
</span>
|
|
40
45
|
</div>
|
|
41
46
|
</div>
|
|
@@ -43,7 +48,7 @@ export function WidgetHeaderContentWithoutMeta({ name }: { name?: string }) {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
function WidgetHeader({
|
|
46
|
-
enabledButtons,
|
|
51
|
+
enabledButtons = [],
|
|
47
52
|
tutorName,
|
|
48
53
|
showContentWithoutMeta,
|
|
49
54
|
showContent = true
|
|
@@ -51,6 +56,7 @@ function WidgetHeader({
|
|
|
51
56
|
const { t } = useTranslation()
|
|
52
57
|
const [, setTab] = useWidgetTabsAtom()
|
|
53
58
|
const name = tutorName ?? t('general.name')
|
|
59
|
+
const isMobile = useMediaQuery({ maxSize: 'md' })
|
|
54
60
|
|
|
55
61
|
const handleClickArchive = () => {
|
|
56
62
|
setTab('chat')
|
|
@@ -60,6 +66,11 @@ function WidgetHeader({
|
|
|
60
66
|
setTab('information')
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
const handleHideWidget = () => {
|
|
70
|
+
TutorWidgetEvents['c3po-app-widget-hide'].dispatch()
|
|
71
|
+
DataHubService.sendEvent({ schema: new ClickTutorMinimizeSchema() })
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
return (
|
|
64
75
|
<div className='mt-0.5 flex flex-col gap-2 text-neutral-900'>
|
|
65
76
|
<div className='flex justify-end'>
|
|
@@ -67,35 +78,36 @@ function WidgetHeader({
|
|
|
67
78
|
<Button
|
|
68
79
|
className='text-neutral-500'
|
|
69
80
|
show={enabledButtons.includes('close')}
|
|
70
|
-
onClick={
|
|
81
|
+
onClick={handleHideWidget}
|
|
71
82
|
aria-label='Close Icon'>
|
|
72
83
|
<Icon name='close' className='h-3 w-3' aria-hidden />
|
|
73
84
|
</Button>
|
|
74
85
|
</div>
|
|
75
86
|
</div>
|
|
76
|
-
<div className='grid-areas-[a_b] grid grid-cols-[1fr_auto] items-center'>
|
|
87
|
+
<div className='grid-areas-[a_b] grid grid-cols-[1fr_auto] items-center pb-4'>
|
|
77
88
|
<div className='grid-area-[a] relative'>
|
|
78
89
|
{showContent && !showContentWithoutMeta && <WidgetHeaderContent tutorName={name} />}
|
|
79
90
|
{showContentWithoutMeta && !showContent && <WidgetHeaderContentWithoutMeta name={name} />}
|
|
80
91
|
</div>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
|
|
93
|
+
<div className='grid-area-[b] ml-auto shrink-0'>
|
|
94
|
+
<div className={clsx('flex max-w-max gap-3 text-neutral-700', styles.btnContainer)}>
|
|
95
|
+
<Tooltip show={!isMobile} content={t('general.buttons.archive')}>
|
|
96
|
+
<Button
|
|
97
|
+
show={enabledButtons.includes('archive')}
|
|
98
|
+
onClick={handleClickArchive}
|
|
99
|
+
aria-label={t('general.buttons.archive') + ' Icon'}>
|
|
100
|
+
<Icon name='archive' className='h-4 w-4' aria-hidden />
|
|
101
|
+
</Button>
|
|
102
|
+
</Tooltip>
|
|
103
|
+
<Tooltip show={!isMobile} content={t('general.buttons.info')} position='left'>
|
|
104
|
+
<Button
|
|
105
|
+
show={enabledButtons.includes('info')}
|
|
106
|
+
onClick={handleClickInfo}
|
|
107
|
+
aria-label={t('general.buttons.info') + ' Icon'}>
|
|
108
|
+
<Icon name='info' className='h-4 w-4' aria-hidden />
|
|
109
|
+
</Button>
|
|
110
|
+
</Tooltip>
|
|
99
111
|
</div>
|
|
100
112
|
</div>
|
|
101
113
|
</div>
|
|
@@ -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: t('general.name') }).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}
|
|
@@ -27,7 +27,7 @@ describe('WidgetStarterPage', () => {
|
|
|
27
27
|
expect(
|
|
28
28
|
screen.getByRole('button', { name: /starter_page.what_does_tutor_do/i })
|
|
29
29
|
).toBeInTheDocument()
|
|
30
|
-
expect(screen.getByRole('button', { name: /starter_page.
|
|
30
|
+
expect(screen.getByRole('button', { name: /starter_page.test_me/i })).toBeInTheDocument()
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
it('should post the slider button text content to the backend', async () => {
|
|
@@ -1,11 +1,9 @@
|
|
|
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'
|
|
8
|
-
import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
|
|
9
7
|
import { useRefEventListener } from '@/src/lib/hooks'
|
|
10
8
|
import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/components'
|
|
11
9
|
import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/hooks'
|
|
@@ -17,8 +15,6 @@ import { GreetingsCard } from '../greetings-card'
|
|
|
17
15
|
import { WidgetHeader } from '../header'
|
|
18
16
|
import { PageLayout } from '../page-layout'
|
|
19
17
|
|
|
20
|
-
import styles from './styles.module.css'
|
|
21
|
-
|
|
22
18
|
function WidgetStarterPage() {
|
|
23
19
|
const { t } = useTranslation()
|
|
24
20
|
const [settings] = useWidgetSettingsAtom()
|
|
@@ -30,6 +26,7 @@ function WidgetStarterPage() {
|
|
|
30
26
|
const limit = useMessagesMaxCount()
|
|
31
27
|
const queryClient = useQueryClient()
|
|
32
28
|
const name = settings?.tutorName ?? t('general.name')
|
|
29
|
+
const isDarkTheme = settings?.config?.theme === 'dark'
|
|
33
30
|
const isSparkieReady = useInitSparkie()
|
|
34
31
|
|
|
35
32
|
useRefEventListener<HTMLTextAreaElement>({
|
|
@@ -77,13 +74,15 @@ function WidgetStarterPage() {
|
|
|
77
74
|
[conversationId, limit, profileId]
|
|
78
75
|
)
|
|
79
76
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
const actionButtonStyle = useMemo(
|
|
78
|
+
() =>
|
|
79
|
+
isDarkTheme
|
|
80
|
+
? undefined
|
|
81
|
+
: ({
|
|
82
|
+
'--gradient-btn-hover-foreground': '#0d0d0d'
|
|
83
|
+
} as CSSProperties),
|
|
84
|
+
[isDarkTheme]
|
|
85
|
+
)
|
|
87
86
|
|
|
88
87
|
useEffect(() => {
|
|
89
88
|
if (!conversationId || !profileId) return
|
|
@@ -102,24 +101,26 @@ function WidgetStarterPage() {
|
|
|
102
101
|
/>
|
|
103
102
|
}>
|
|
104
103
|
<div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
|
|
105
|
-
<div
|
|
106
|
-
|
|
107
|
-
[
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
/>
|
|
110
110
|
|
|
111
111
|
<div className='my-auto'>
|
|
112
112
|
<GreetingsCard
|
|
113
|
-
author={settings?.user?.name}
|
|
113
|
+
author={settings?.user?.name?.split(' ')?.[0]}
|
|
114
114
|
tutorName={name}
|
|
115
115
|
isDarkTheme={settings?.config?.theme === 'dark'}
|
|
116
116
|
/>
|
|
117
117
|
</div>
|
|
118
118
|
</div>
|
|
119
|
-
<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'>
|
|
120
120
|
<Button
|
|
121
121
|
variant='gradient-outline'
|
|
122
|
-
|
|
122
|
+
style={actionButtonStyle}
|
|
123
|
+
className='ml-5 shrink-0 text-sm'
|
|
123
124
|
onClick={handleAskQuestion}
|
|
124
125
|
loading={!isSparkieReady}>
|
|
125
126
|
<span>🤖 </span>
|
|
@@ -127,11 +128,12 @@ function WidgetStarterPage() {
|
|
|
127
128
|
</Button>
|
|
128
129
|
<Button
|
|
129
130
|
variant='gradient-outline'
|
|
130
|
-
|
|
131
|
+
style={actionButtonStyle}
|
|
132
|
+
className='mr-5 shrink-0 text-sm'
|
|
131
133
|
onClick={handleAskQuestion}
|
|
132
134
|
loading={!isSparkieReady}>
|
|
133
135
|
<span>📝 </span>
|
|
134
|
-
{t('starter_page.
|
|
136
|
+
{t('starter_page.test_me')}
|
|
135
137
|
</Button>
|
|
136
138
|
</HorizontalDraggableScroll>
|
|
137
139
|
</div>
|