app-tutor-ai-consumer 1.2.0 → 1.3.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 +11 -0
- package/config/vitest/__mocks__/sparkie.tsx +12 -0
- package/config/vitest/vitest.config.mts +1 -0
- package/environments/.env.test +2 -2
- package/package.json +2 -1
- package/public/assets/svg/tutor-onboarding.svg +128 -0
- package/src/@types/declarations.d.ts +4 -3
- package/src/config/optimizely/optimizely-provider.tsx +3 -3
- package/src/config/optimizely/optimizely.ts +1 -1
- package/src/config/styles/global.css +1 -0
- package/src/config/styles/utilities/bg-utilities.module.css +11 -0
- package/src/config/styles/utilities/text-utilities.module.css +6 -0
- package/src/config/tests/handlers.ts +3 -0
- package/src/lib/components/button/button.tsx +86 -0
- package/src/lib/components/button/index.ts +1 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/hooks/index.ts +1 -0
- package/src/lib/hooks/use-ref-event-listener/index.ts +2 -0
- package/src/lib/hooks/use-ref-event-listener/use-ref-event-listener.tsx +32 -0
- package/src/main/main.spec.tsx +17 -7
- package/src/main/main.tsx +2 -13
- package/src/modules/messages/components/chat-input/chat-input.atom.ts +12 -0
- package/src/modules/{create-message → messages}/components/chat-input/chat-input.tsx +6 -2
- package/src/modules/{create-message → messages}/components/chat-input/index.ts +1 -0
- package/src/modules/{create-message → messages}/components/chat-input/types.ts +1 -0
- package/src/modules/messages/components/index.ts +2 -0
- package/src/modules/messages/components/messages-list/index.ts +2 -0
- package/src/modules/messages/components/messages-list/messages-list.tsx +24 -0
- package/src/modules/messages/constants.ts +1 -0
- package/src/modules/messages/index.ts +3 -0
- package/src/modules/messages/service.ts +63 -0
- package/src/modules/messages/types.ts +47 -0
- package/src/modules/sparkie/constants.ts +21 -0
- package/src/modules/sparkie/index.ts +3 -0
- package/src/modules/sparkie/service.ts +94 -0
- package/src/modules/sparkie/types.ts +47 -0
- package/src/modules/sparkie/utils/validate-firebase-config.spec.ts +17 -0
- package/src/modules/sparkie/utils/validate-firebase-config.ts +12 -0
- package/src/modules/widget/__tests__/widget-settings-props.builder.ts +121 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +20 -0
- package/src/modules/widget/components/chat-page/index.ts +2 -0
- package/src/modules/widget/components/constants.tsx +9 -0
- package/src/modules/widget/components/container/container.tsx +32 -0
- package/src/modules/widget/components/container/index.ts +2 -0
- package/src/{main → modules/widget/components/container}/styles.module.css +1 -5
- package/src/modules/widget/components/container/types.ts +3 -0
- package/src/modules/widget/components/greetings-card/greetings-card.tsx +1 -1
- package/src/modules/widget/components/greetings-card/styles.module.css +1 -3
- package/src/modules/widget/components/index.ts +3 -0
- package/src/modules/widget/components/onboarding-page/index.ts +1 -0
- package/src/modules/widget/components/onboarding-page/onboarding-page.tsx +38 -0
- package/src/modules/widget/components/onboarding-page/styles.module.css +7 -0
- package/src/modules/widget/components/starter-page/index.ts +1 -0
- package/src/modules/widget/components/starter-page/starter-page.tsx +41 -0
- package/src/modules/widget/hooks/index.ts +1 -0
- package/src/modules/widget/hooks/use-init-sparkie/index.ts +1 -0
- package/src/modules/widget/hooks/use-init-sparkie/use-init-sparkie.tsx +18 -0
- package/src/modules/widget/store/index.ts +1 -0
- package/src/modules/widget/store/widget-tabs.atom.ts +53 -0
- package/tailwind.config.js +95 -1
- package/config/vitest/index.ts +0 -1
- package/src/modules/create-message/components/index.ts +0 -1
|
@@ -13,9 +13,10 @@ declare module '*.jpg'
|
|
|
13
13
|
|
|
14
14
|
declare module '*.gif'
|
|
15
15
|
|
|
16
|
-
declare module '
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
declare module '*.svg?url' {
|
|
17
|
+
const content: string
|
|
18
|
+
export default content
|
|
19
|
+
}
|
|
19
20
|
|
|
20
21
|
declare module '*.svg' {
|
|
21
22
|
const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>
|
|
@@ -8,7 +8,7 @@ import optimizelyClient from './optimizely'
|
|
|
8
8
|
|
|
9
9
|
function OptimizelyProvider({
|
|
10
10
|
settings,
|
|
11
|
-
children
|
|
11
|
+
children
|
|
12
12
|
}: PropsWithChildren<{ settings: WidgetSettingProps }>) {
|
|
13
13
|
const userInfo = {
|
|
14
14
|
id: settings.user?.ucode ?? '',
|
|
@@ -17,8 +17,8 @@ function OptimizelyProvider({
|
|
|
17
17
|
appSystem: APP_SYSTEM,
|
|
18
18
|
appDebug: !productionMode,
|
|
19
19
|
locale: settings.locale ?? '',
|
|
20
|
-
slug: settings.membershipSlug ?? ''
|
|
21
|
-
}
|
|
20
|
+
slug: settings.membershipSlug ?? ''
|
|
21
|
+
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
return (
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
.gradientBg {
|
|
2
|
+
background:
|
|
3
|
+
linear-gradient(
|
|
4
|
+
186.9deg,
|
|
5
|
+
rgb(from var(--ai-color-primary) r g b / 0.1) 6.65%,
|
|
6
|
+
rgb(from var(--ai-color-secondary) r g b / 0.1) 28.99%,
|
|
7
|
+
rgb(from var(--ai-color-dark) r g b / 0.1) 46.97%,
|
|
8
|
+
rgb(from var(--hc-color-neutral-900) r g b / 0.8) 57.9%
|
|
9
|
+
),
|
|
10
|
+
linear-gradient(0deg, var(--hc-color-neutral-900), var(--hc-color-neutral-900));
|
|
11
|
+
}
|
|
@@ -3,5 +3,8 @@ import { http, HttpResponse } from 'msw'
|
|
|
3
3
|
export const handlers = [
|
|
4
4
|
http.all('https://tracking-api.buildstaging.com/rest/track/event/json/sync', () => {
|
|
5
5
|
return HttpResponse.json({ ok: true })
|
|
6
|
+
}),
|
|
7
|
+
http.all('https://c3po-api-auth.buildstaging.com/v1/auth/sparkie', () => {
|
|
8
|
+
return HttpResponse.json({ ok: true })
|
|
6
9
|
})
|
|
7
10
|
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import type { HTMLAttributes, PropsWithChildren } from 'react'
|
|
3
|
+
|
|
4
|
+
export type ButtonProps = PropsWithChildren<
|
|
5
|
+
HTMLAttributes<HTMLButtonElement> & {
|
|
6
|
+
variant?: 'brand' | 'secondary' | 'primary' | 'tertiary' | 'gradient-outline'
|
|
7
|
+
}
|
|
8
|
+
>
|
|
9
|
+
|
|
10
|
+
function Button({ children, className, variant = 'brand', ...props }: ButtonProps) {
|
|
11
|
+
const defaultClasses =
|
|
12
|
+
'rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 text-base font-medium border border-transparent'
|
|
13
|
+
const defaultPadding = 'px-4 py-2'
|
|
14
|
+
|
|
15
|
+
switch (variant) {
|
|
16
|
+
case 'gradient-outline':
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
className={clsx(
|
|
20
|
+
defaultClasses,
|
|
21
|
+
'group relative inline-flex items-center justify-center overflow-hidden bg-gradient-to-br from-purple-600 to-blue-500 p-0.5 hover:text-neutral-0 group-hover:from-purple-600 group-hover:to-blue-500',
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{...props}>
|
|
25
|
+
<span className='relative flex-1 rounded-md bg-neutral-900 px-5 py-2.5 text-neutral-0 transition-all duration-75 ease-in group-hover:bg-transparent'>
|
|
26
|
+
{children}
|
|
27
|
+
</span>
|
|
28
|
+
</button>
|
|
29
|
+
)
|
|
30
|
+
case 'primary':
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
className={clsx(
|
|
34
|
+
defaultPadding,
|
|
35
|
+
defaultClasses,
|
|
36
|
+
'bg-primary-500 text-neutral-0 hover:bg-primary-600 focus:outline-none',
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}>
|
|
40
|
+
{children}
|
|
41
|
+
</button>
|
|
42
|
+
)
|
|
43
|
+
case 'secondary':
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
className={clsx(
|
|
47
|
+
defaultPadding,
|
|
48
|
+
defaultClasses,
|
|
49
|
+
'border-neutral-100 bg-transparent text-neutral-100 hover:bg-neutral-100 hover:text-neutral-900',
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
{...props}>
|
|
53
|
+
{children}
|
|
54
|
+
</button>
|
|
55
|
+
)
|
|
56
|
+
case 'tertiary':
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
className={clsx(
|
|
60
|
+
defaultPadding,
|
|
61
|
+
defaultClasses,
|
|
62
|
+
'bg-transparent text-neutral-0 hover:bg-neutral-900',
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
{...props}>
|
|
66
|
+
{children}
|
|
67
|
+
</button>
|
|
68
|
+
)
|
|
69
|
+
case 'brand':
|
|
70
|
+
default:
|
|
71
|
+
return (
|
|
72
|
+
<button
|
|
73
|
+
className={clsx(
|
|
74
|
+
defaultPadding,
|
|
75
|
+
defaultClasses,
|
|
76
|
+
'rounded bg-neutral-100 text-neutral-900 outline-none hover:bg-neutral-200',
|
|
77
|
+
className
|
|
78
|
+
)}
|
|
79
|
+
{...props}>
|
|
80
|
+
{children}
|
|
81
|
+
</button>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default Button
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Button } from './button'
|
package/src/lib/hooks/index.ts
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
export type UseRefEventListenerConfig<T extends HTMLElement> = {
|
|
4
|
+
config: {
|
|
5
|
+
ref: React.RefObject<T | null>
|
|
6
|
+
eventTypes: string[]
|
|
7
|
+
handler: (event: Event) => void
|
|
8
|
+
options?: AddEventListenerOptions
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function useRefEventListener<T extends HTMLElement>({
|
|
13
|
+
config: { eventTypes, handler, ref, options }
|
|
14
|
+
}: UseRefEventListenerConfig<T>) {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const element = ref.current
|
|
17
|
+
|
|
18
|
+
if (!element) return
|
|
19
|
+
|
|
20
|
+
eventTypes.forEach((ev) => {
|
|
21
|
+
element.addEventListener(ev, handler, options)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
eventTypes.forEach((ev) => {
|
|
26
|
+
element.removeEventListener(ev, handler, options)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}, [ref, eventTypes, handler, options])
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default useRefEventListener
|
package/src/main/main.spec.tsx
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import { render, screen, waitFor } from '@/config/tests'
|
|
2
|
-
import
|
|
1
|
+
import { chance, render, screen, waitFor } from '@/config/tests'
|
|
2
|
+
import WidgetSettingPropsBuilder from '../modules/widget/__tests__/widget-settings-props.builder'
|
|
3
3
|
import { Main } from '.'
|
|
4
4
|
|
|
5
5
|
describe('Main', () => {
|
|
6
|
-
const
|
|
6
|
+
const defaultProps = new WidgetSettingPropsBuilder()
|
|
7
|
+
const renderComponent = (props = { settings: defaultProps }) => render(<Main {...props} />)
|
|
8
|
+
|
|
9
|
+
it('should render empty element when settings.tutorName is not defined', async () => {
|
|
10
|
+
const { container } = renderComponent()
|
|
11
|
+
|
|
12
|
+
await waitFor(() => {
|
|
13
|
+
expect(container).toBeEmptyDOMElement()
|
|
14
|
+
})
|
|
15
|
+
})
|
|
7
16
|
|
|
8
17
|
it('should render without errors', async () => {
|
|
9
|
-
|
|
18
|
+
const props = new WidgetSettingPropsBuilder().withTutorName(chance.name())
|
|
19
|
+
renderComponent({ settings: props })
|
|
10
20
|
|
|
11
|
-
await waitFor(() =>
|
|
12
|
-
expect(screen.getByText(/
|
|
13
|
-
)
|
|
21
|
+
await waitFor(() => {
|
|
22
|
+
expect(screen.getByText(/onboarding.description/i)).toBeInTheDocument()
|
|
23
|
+
})
|
|
14
24
|
})
|
|
15
25
|
})
|
package/src/main/main.tsx
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import '@/config/styles/index.css'
|
|
2
2
|
|
|
3
|
-
import clsx from 'clsx'
|
|
4
|
-
|
|
5
3
|
import { ErrorBoundary, GenericError } from '@/src/lib/components/errors'
|
|
6
4
|
import { useDefaultId } from '@/src/lib/hooks'
|
|
7
5
|
import { useAppLang } from '../config/i18n'
|
|
8
|
-
import { ChatInput } from '../modules/create-message/components'
|
|
9
6
|
import { GlobalProviders } from '../modules/global-providers'
|
|
10
|
-
import {
|
|
7
|
+
import { WidgetContainer } from '../modules/widget'
|
|
11
8
|
import type { WidgetSettingProps } from '../types'
|
|
12
9
|
|
|
13
|
-
import styles from './styles.module.css'
|
|
14
|
-
|
|
15
10
|
function Main({ settings }: { settings: WidgetSettingProps }) {
|
|
16
11
|
useDefaultId()
|
|
17
12
|
useAppLang(settings.locale)
|
|
@@ -19,13 +14,7 @@ function Main({ settings }: { settings: WidgetSettingProps }) {
|
|
|
19
14
|
return (
|
|
20
15
|
<ErrorBoundary fallback={<GenericError />}>
|
|
21
16
|
<GlobalProviders settings={settings}>
|
|
22
|
-
<
|
|
23
|
-
className={clsx('flex min-h-svh flex-col items-center justify-center p-5', styles.main)}>
|
|
24
|
-
<div className='flex flex-1 flex-col justify-center gap-6 lg:max-w-sm'>
|
|
25
|
-
<GreetingsCard author={settings.author ?? ''} tutorName={settings.tutorName ?? ''} />
|
|
26
|
-
<ChatInput name='new-chat-msg-input' />
|
|
27
|
-
</div>
|
|
28
|
-
</div>
|
|
17
|
+
<WidgetContainer />
|
|
29
18
|
</GlobalProviders>
|
|
30
19
|
</ErrorBoundary>
|
|
31
20
|
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { atom, useAtom } from 'jotai'
|
|
2
|
+
|
|
3
|
+
const chatInputValueAtom = atom<string>('')
|
|
4
|
+
|
|
5
|
+
export const chatInputAtom = atom(
|
|
6
|
+
(get) => get(chatInputValueAtom),
|
|
7
|
+
(_, set, config: string) => {
|
|
8
|
+
set(chatInputValueAtom, config)
|
|
9
|
+
}
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
export const useChatInputValueAtom = () => useAtom(chatInputAtom)
|
|
@@ -3,10 +3,13 @@ import { useTranslation } from 'react-i18next'
|
|
|
3
3
|
|
|
4
4
|
import { Icon } from '@/src/lib/components'
|
|
5
5
|
|
|
6
|
+
import { useChatInputValueAtom } from './chat-input.atom'
|
|
6
7
|
import type { ChatInputProps } from './types'
|
|
7
8
|
|
|
8
|
-
const ChatInput = forwardRef<HTMLInputElement, ChatInputProps>(function ({ name }, ref) {
|
|
9
|
+
const ChatInput = forwardRef<HTMLInputElement, ChatInputProps>(function ({ name, onSend }, ref) {
|
|
9
10
|
const { t } = useTranslation()
|
|
11
|
+
const [value] = useChatInputValueAtom()
|
|
12
|
+
|
|
10
13
|
return (
|
|
11
14
|
<div className='flex items-center rounded-full border-neutral-800 bg-neutral-800 px-4 py-2'>
|
|
12
15
|
<input
|
|
@@ -16,8 +19,9 @@ const ChatInput = forwardRef<HTMLInputElement, ChatInputProps>(function ({ name
|
|
|
16
19
|
type='text'
|
|
17
20
|
className='h-6 w-full border-none bg-transparent text-neutral-400 outline-0 placeholder:text-neutral-400'
|
|
18
21
|
placeholder={t('send_message.field.placeholder')}
|
|
22
|
+
defaultValue={value}
|
|
19
23
|
/>
|
|
20
|
-
<button>
|
|
24
|
+
<button onClick={onSend}>
|
|
21
25
|
<Icon name='send' className='h-4 w-4 text-neutral-50' />
|
|
22
26
|
</button>
|
|
23
27
|
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function MessagesList() {
|
|
2
|
+
return (
|
|
3
|
+
<div className='flex flex-1 flex-col items-start justify-center gap-6'>
|
|
4
|
+
<div className='max-w-[80%] self-end rounded-lg bg-neutral-800 p-3 text-sm/normal text-neutral-0'>
|
|
5
|
+
<span>Quero saber o que Tutor do BEM faz</span>
|
|
6
|
+
</div>
|
|
7
|
+
<div className='flex max-w-[80%] flex-col gap-1 rounded-lg bg-ai-chat-response p-3 text-sm/normal text-neutral-0'>
|
|
8
|
+
<span>
|
|
9
|
+
Sou seu assistente de IA, criado pelo Marcelo Horta, para te acompanhar durante todo o seu
|
|
10
|
+
aprendizado. Confira o que posso fazer por você:
|
|
11
|
+
</span>
|
|
12
|
+
<span>
|
|
13
|
+
🤓 Dúvidas sobre as aulas:Me faça perguntas sobre o conteúdo das aulas e eu ajudo você a
|
|
14
|
+
aprender melhor.
|
|
15
|
+
</span>
|
|
16
|
+
<span>
|
|
17
|
+
📝 ResumosPosso gerar um resumo do que foi visto na aula pra facilitar seus estudos.
|
|
18
|
+
</span>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default MessagesList
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MSG_MAX_COUNT = 20
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Message, MessageWithSenderData } from '@hotmart/sparkie/dist/MessageService'
|
|
2
|
+
|
|
3
|
+
import { ApiError } from '@/src/config/request'
|
|
4
|
+
import { HttpCodes } from '@/src/lib/utils'
|
|
5
|
+
import { SparkieService } from '../sparkie'
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
IGetMessagesPayload,
|
|
9
|
+
ISendImageMessagePayload,
|
|
10
|
+
ISendTextMessagePayload
|
|
11
|
+
} from './types'
|
|
12
|
+
|
|
13
|
+
class MessagesService {
|
|
14
|
+
private get sparkieMessageService() {
|
|
15
|
+
try {
|
|
16
|
+
const messageService = SparkieService.sparkieInstance.messageService
|
|
17
|
+
|
|
18
|
+
if (!messageService) throw new Error()
|
|
19
|
+
|
|
20
|
+
return messageService
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new ApiError({
|
|
23
|
+
statusCode: HttpCodes.UNPROCESSABLE_ENTITY,
|
|
24
|
+
message: 'sparkie.messageService not defined',
|
|
25
|
+
extra: { error }
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getMessages({
|
|
31
|
+
conversationId,
|
|
32
|
+
before
|
|
33
|
+
}: IGetMessagesPayload): Promise<Array<MessageWithSenderData>> {
|
|
34
|
+
const data = await this.sparkieMessageService.getAll(conversationId, {
|
|
35
|
+
before
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return data ?? []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async sendTextMessage({
|
|
42
|
+
content,
|
|
43
|
+
conversationId,
|
|
44
|
+
metadata
|
|
45
|
+
}: ISendTextMessagePayload): Promise<Message> {
|
|
46
|
+
const data = await this.sparkieMessageService.post(conversationId, {
|
|
47
|
+
content,
|
|
48
|
+
metadata
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return data
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async sendImageMessage({ content, conversationId }: ISendImageMessagePayload): Promise<Message> {
|
|
55
|
+
const data = await this.sparkieMessageService.post(conversationId, {
|
|
56
|
+
content
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return data
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default new MessagesService()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Message } from '@hotmart/sparkie/dist/MessageService'
|
|
2
|
+
|
|
3
|
+
export type IMessage = Message & {
|
|
4
|
+
sending?: boolean // indicates when the current message is being sent
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type IGetMessagesPayload = {
|
|
8
|
+
conversationId: string
|
|
9
|
+
before?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ISendTextMessagePayload = {
|
|
13
|
+
conversationId: string
|
|
14
|
+
content: {
|
|
15
|
+
type: string
|
|
16
|
+
text: string
|
|
17
|
+
}
|
|
18
|
+
metadata: {
|
|
19
|
+
[k: string]: string | undefined | number | { [k: string]: string | undefined | number }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ISendImageMessagePayload = {
|
|
24
|
+
conversationId: string
|
|
25
|
+
content: {
|
|
26
|
+
type: string
|
|
27
|
+
name: string
|
|
28
|
+
dimensions: IDimensions
|
|
29
|
+
thumbnails: {
|
|
30
|
+
lg: string
|
|
31
|
+
md: string
|
|
32
|
+
sm: string
|
|
33
|
+
}
|
|
34
|
+
url: string
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type IDimensions = {
|
|
39
|
+
width: number
|
|
40
|
+
height: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type IFetchMessagesOptions = {
|
|
44
|
+
currentMessages: Array<IMessage>
|
|
45
|
+
conversationId: string
|
|
46
|
+
loadFirstPage?: boolean
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const firebaseConfig = {
|
|
2
|
+
apiKey: process.env.FIREBASE_API_KEY,
|
|
3
|
+
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
|
|
4
|
+
databaseURL: process.env.FIREBASE_DATABASE_URL,
|
|
5
|
+
projectId: process.env.FIREBASE_PROJECT_ID,
|
|
6
|
+
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
|
|
7
|
+
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const firebaseConfigRequiredFields = new Set(Object.keys(firebaseConfig))
|
|
11
|
+
|
|
12
|
+
export const ACTION_EVENT_MAP = {
|
|
13
|
+
conversationAdded: 'conversations.create',
|
|
14
|
+
conversationUpdated: 'conversations.update',
|
|
15
|
+
cursorsUpdate: 'cursors.update',
|
|
16
|
+
messageReceived: 'messages.create',
|
|
17
|
+
messageDeleted: 'messages.delete',
|
|
18
|
+
threadClosed: 'threads.close',
|
|
19
|
+
threadJoined: 'threads.join',
|
|
20
|
+
contactTyping: 'contacts.typing'
|
|
21
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import Sparkie from '@hotmart/sparkie'
|
|
2
|
+
|
|
3
|
+
import { MSG_MAX_COUNT } from '../messages'
|
|
4
|
+
|
|
5
|
+
import { ACTION_EVENT_MAP, firebaseConfig } from './constants'
|
|
6
|
+
import type { InitSparkiePayload, SparkieActions, TrackTypingPayload } from './types'
|
|
7
|
+
import { validateFirebaseConfig } from './utils/validate-firebase-config'
|
|
8
|
+
|
|
9
|
+
class SparkieService {
|
|
10
|
+
private sparkie: Sparkie | null = null
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
if (validateFirebaseConfig()) {
|
|
14
|
+
this.sparkie = new Sparkie({
|
|
15
|
+
firebaseConfig,
|
|
16
|
+
options: {
|
|
17
|
+
conversationAPI: process.env.API_CONVERSATION_URL,
|
|
18
|
+
authURL: process.env.API_AUTH_URL,
|
|
19
|
+
numMaxMessages: MSG_MAX_COUNT
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get sparkieInstance(): Sparkie {
|
|
26
|
+
if (!this.sparkie)
|
|
27
|
+
throw new Error('Sparkie instance not available. Check Firebase configuration.')
|
|
28
|
+
|
|
29
|
+
return this.sparkie
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async initSparkie({ token, skipPresenceSetup = false }: InitSparkiePayload): Promise<boolean> {
|
|
33
|
+
const sparkie = this.sparkieInstance
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
if (!token || !token.trim())
|
|
37
|
+
throw new Error('Invalid or missing token for Sparkie initialization')
|
|
38
|
+
|
|
39
|
+
await sparkie.init(token, { skipPresenceSetup })
|
|
40
|
+
return true
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Error initializing realtime engine', error)
|
|
43
|
+
|
|
44
|
+
if (error instanceof Error) {
|
|
45
|
+
console.error('Error details:', {
|
|
46
|
+
message: error.message,
|
|
47
|
+
stack: error.stack,
|
|
48
|
+
token: token ? 'provided' : 'missing',
|
|
49
|
+
skipPresenceSetup
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
updateToken(token: string): void {
|
|
58
|
+
this.sparkieInstance.setAPIToken(token)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
removeEventSubscription(actions: SparkieActions): void {
|
|
62
|
+
Object.entries(actions).forEach(([actionName, listener]) => {
|
|
63
|
+
this.sparkieInstance.off(
|
|
64
|
+
ACTION_EVENT_MAP[actionName as keyof typeof ACTION_EVENT_MAP],
|
|
65
|
+
listener as (data: Record<string, unknown>) => void
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
subscribeEvents({
|
|
71
|
+
messageReceived,
|
|
72
|
+
threadClosed,
|
|
73
|
+
threadJoined,
|
|
74
|
+
contactTyping
|
|
75
|
+
}: SparkieActions): void {
|
|
76
|
+
if (messageReceived) this.sparkieInstance.on(ACTION_EVENT_MAP.messageReceived, messageReceived)
|
|
77
|
+
|
|
78
|
+
if (threadClosed) this.sparkieInstance.on(ACTION_EVENT_MAP.threadClosed, threadClosed)
|
|
79
|
+
|
|
80
|
+
if (threadJoined) this.sparkieInstance.on(ACTION_EVENT_MAP.threadJoined, threadJoined)
|
|
81
|
+
|
|
82
|
+
if (contactTyping) this.sparkieInstance.on(ACTION_EVENT_MAP.contactTyping, contactTyping)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
trackTyping({ contactIds, conversationId }: TrackTypingPayload): void {
|
|
86
|
+
this.sparkieInstance.listener?.trackTyping(conversationId, contactIds)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
destroySparkie(): Promise<void> {
|
|
90
|
+
return this.sparkieInstance.destroy({ skipSignOut: true })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default new SparkieService()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { IMessage } from '../messages'
|
|
2
|
+
|
|
3
|
+
export type InitSparkiePayload = {
|
|
4
|
+
token: string
|
|
5
|
+
webinarConversationId?: string
|
|
6
|
+
skipPresenceSetup?: boolean
|
|
7
|
+
includeSupportEvents?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type ThreadMetadata = Record<string, unknown>
|
|
11
|
+
|
|
12
|
+
export type IContact = {
|
|
13
|
+
id: string
|
|
14
|
+
name: string
|
|
15
|
+
picture: string
|
|
16
|
+
userId: string | number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type IThread = {
|
|
20
|
+
id: string
|
|
21
|
+
conversationId: string
|
|
22
|
+
contact: IContact
|
|
23
|
+
startedAt: number
|
|
24
|
+
subject?: string
|
|
25
|
+
metadata?: ThreadMetadata
|
|
26
|
+
endedAt?: number
|
|
27
|
+
updatedAt?: number
|
|
28
|
+
deletedAt?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ITyping = {
|
|
32
|
+
contactId: string
|
|
33
|
+
conversationId: string
|
|
34
|
+
status: 'typing' | 'idle'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SparkieActions = {
|
|
38
|
+
messageReceived?: (data: IMessage) => void
|
|
39
|
+
threadClosed?: (data: IThread) => void
|
|
40
|
+
threadJoined?: (data: IThread) => void
|
|
41
|
+
contactTyping?: (data: ITyping) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type TrackTypingPayload = {
|
|
45
|
+
conversationId: string
|
|
46
|
+
contactIds: Array<string>
|
|
47
|
+
}
|