app-tutor-ai-consumer 1.4.0 → 1.5.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.
Files changed (77) hide show
  1. package/.github/workflows/staging-staging.yml +148 -0
  2. package/.github/workflows/staging.yml +1 -2
  3. package/CHANGELOG.md +13 -0
  4. package/config/rspack/rspack.config.js +5 -1
  5. package/config/vitest/__mocks__/icons.tsx +3 -0
  6. package/config/vitest/__mocks__/intersection-observer.ts +10 -0
  7. package/config/vitest/__mocks__/sparkie.tsx +2 -11
  8. package/config/vitest/__mocks__/use-init-sparkie.tsx +14 -0
  9. package/config/vitest/vitest.config.mts +13 -8
  10. package/environments/.env.test +2 -0
  11. package/package.json +3 -3
  12. package/public/index.html +3 -4
  13. package/src/config/styles/global.css +2 -2
  14. package/src/config/tanstack/query-client.ts +2 -1
  15. package/src/config/tests/utils.tsx +3 -2
  16. package/src/config/tests/wrappers.tsx +4 -1
  17. package/src/index.tsx +22 -0
  18. package/src/lib/components/icons/arrow-down.svg +5 -0
  19. package/src/lib/components/icons/chevron-down.svg +4 -0
  20. package/src/lib/components/icons/icon-names.d.ts +1 -1
  21. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +7 -9
  22. package/src/lib/hooks/index.ts +3 -0
  23. package/src/lib/hooks/use-intersection-observer-reverse-scroll/index.ts +2 -0
  24. package/src/lib/hooks/use-intersection-observer-reverse-scroll/use-intersection-observer-reverse-scroll.tsx +147 -0
  25. package/src/lib/hooks/use-ref-client-height/index.ts +2 -0
  26. package/src/lib/hooks/use-ref-client-height/use-ref-client-height.tsx +38 -0
  27. package/src/lib/hooks/use-scroll-to-ref/index.ts +2 -0
  28. package/src/lib/hooks/use-scroll-to-ref/use-scroll-to-ref.tsx +14 -0
  29. package/src/lib/hooks/use-throttle/index.ts +3 -0
  30. package/src/lib/hooks/use-throttle/types.ts +13 -0
  31. package/src/lib/hooks/use-throttle/use-throttle.spec.tsx +296 -0
  32. package/src/lib/hooks/use-throttle/use-throttle.tsx +91 -0
  33. package/src/main/main.spec.tsx +9 -0
  34. package/src/modules/cursor/__tests__/icursor-update.builder.ts +42 -0
  35. package/src/modules/cursor/hooks/index.ts +1 -0
  36. package/src/modules/cursor/hooks/use-update-cursor/index.ts +2 -0
  37. package/src/modules/cursor/hooks/use-update-cursor/use-update-cursor.spec.tsx +23 -0
  38. package/src/modules/cursor/hooks/use-update-cursor/use-update-cursor.ts +11 -0
  39. package/src/modules/cursor/index.ts +2 -0
  40. package/src/modules/cursor/service.ts +15 -0
  41. package/src/modules/cursor/types.ts +9 -0
  42. package/src/modules/global-providers/index.ts +1 -0
  43. package/src/modules/messages/__tests__/parsed-message.builder.ts +164 -0
  44. package/src/modules/messages/components/message-item/message-item.spec.tsx +2 -2
  45. package/src/modules/messages/components/message-item/message-item.tsx +14 -1
  46. package/src/modules/messages/components/message-item-end-of-scroll/index.ts +2 -0
  47. package/src/modules/messages/components/message-item-end-of-scroll/message-item-end-of-scroll.tsx +14 -0
  48. package/src/modules/messages/components/message-item-error/index.ts +2 -0
  49. package/src/modules/messages/components/message-item-error/message-item-error.tsx +25 -0
  50. package/src/modules/messages/components/message-item-loading/index.ts +2 -0
  51. package/src/modules/messages/components/message-item-loading/message-item-loading.tsx +16 -0
  52. package/src/modules/messages/components/messages-list/index.ts +1 -1
  53. package/src/modules/messages/components/messages-list/messages-list.tsx +69 -39
  54. package/src/modules/messages/constants.ts +1 -0
  55. package/src/modules/messages/hooks/index.ts +3 -0
  56. package/src/modules/messages/hooks/use-all-messages/index.ts +2 -0
  57. package/src/modules/messages/hooks/use-all-messages/use-all-messages.tsx +30 -0
  58. package/src/modules/messages/hooks/use-infinite-get-messages/index.ts +2 -0
  59. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +58 -0
  60. package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +97 -0
  61. package/src/modules/messages/hooks/use-manage-scroll/index.ts +2 -0
  62. package/src/modules/messages/hooks/use-manage-scroll/use-manage-scroll.tsx +66 -0
  63. package/src/modules/messages/utils/has-to-update-cursor/has-to-update-cursor.spec.tsx +58 -0
  64. package/src/modules/messages/utils/has-to-update-cursor/has-to-update-cursor.ts +30 -0
  65. package/src/modules/messages/utils/has-to-update-cursor/index.ts +2 -0
  66. package/src/modules/sparkie/__tests__/sparkie.mock.ts +33 -0
  67. package/src/modules/widget/__tests__/widget-settings-props.builder.ts +6 -0
  68. package/src/modules/widget/components/chat-page/chat-page.spec.tsx +28 -0
  69. package/src/modules/widget/components/chat-page/chat-page.tsx +1 -3
  70. package/src/modules/widget/components/container/container.tsx +20 -14
  71. package/src/modules/widget/components/index.ts +1 -0
  72. package/src/modules/widget/components/scroll-to-bottom-button/index.ts +2 -0
  73. package/src/modules/widget/components/scroll-to-bottom-button/scroll-to-bottom-button.tsx +32 -0
  74. package/src/modules/widget/events.ts +4 -0
  75. package/src/modules/widget/hooks/use-init-sparkie/use-init-sparkie.tsx +8 -6
  76. package/src/modules/widget/store/index.ts +1 -0
  77. package/src/modules/widget/store/widget-container-intrinsic-height.atom.ts +13 -0
@@ -0,0 +1,97 @@
1
+ import { useCallback, useEffect } from 'react'
2
+ import type { UndefinedInitialDataInfiniteOptions } from '@tanstack/react-query'
3
+ import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
4
+
5
+ import { formatTime } from '@/src/config/dayjs'
6
+ import { SparkieService } from '@/src/modules/sparkie'
7
+ import { MessagesService } from '../..'
8
+ import { MSG_MAX_COUNT } from '../../constants'
9
+ import type { FetchMessagesResponse, IMessage, ParsedMessage } from '../../types'
10
+ import { messagesParser } from '../../utils'
11
+ import type { UseFetchMessagesProps } from '../use-fetch-messages'
12
+
13
+ export type UseGetMessagesQueryProps = {
14
+ conversationId: string
15
+ profileId: string
16
+ enabled?: boolean
17
+ }
18
+
19
+ export const getMessagesInfiniteQuery = ({
20
+ conversationId,
21
+ profileId,
22
+ enabled
23
+ }: UseGetMessagesQueryProps) =>
24
+ ({
25
+ queryKey: ['sparkie:messageService:getAll', conversationId, profileId],
26
+ queryFn: async ({ pageParam }: { pageParam?: number }) => {
27
+ const messages = await MessagesService.getMessages({ conversationId, before: pageParam })
28
+
29
+ return {
30
+ messages,
31
+ hasMore: messages.length === MSG_MAX_COUNT
32
+ } as FetchMessagesResponse
33
+ },
34
+ initialPageParam: undefined,
35
+ getNextPageParam: (lastPage: FetchMessagesResponse) => {
36
+ if (!lastPage.hasMore) return undefined
37
+
38
+ const minSentAt = Math.min(...lastPage.messages.map((msg) => msg.sentAt))
39
+
40
+ return minSentAt
41
+ },
42
+ enabled,
43
+ select(data) {
44
+ const messages = data.pages?.flatMap((page) => page.messages) ?? []
45
+ return messagesParser({ messages, profileId }).reduce((messagesMap, currentMessage) => {
46
+ const timestamp = formatTime(currentMessage.timestamp, true)
47
+
48
+ if (!messagesMap.has(timestamp)) {
49
+ messagesMap.set(timestamp, [currentMessage])
50
+ return messagesMap
51
+ }
52
+
53
+ const existingTimestampValues = Array.from(messagesMap.get(timestamp) ?? [])
54
+
55
+ messagesMap.set(
56
+ timestamp,
57
+ [...existingTimestampValues, currentMessage].sort((a, b) => a.timestamp - b.timestamp)
58
+ )
59
+
60
+ return messagesMap
61
+ }, new Map<string, ParsedMessage[]>())
62
+ }
63
+ }) as UndefinedInitialDataInfiniteOptions<
64
+ FetchMessagesResponse,
65
+ Error,
66
+ Map<string, ParsedMessage[]>
67
+ >
68
+
69
+ function useInfiniteGetMessages({
70
+ conversationId,
71
+ profileId,
72
+ enabled = false
73
+ }: Omit<UseFetchMessagesProps, 'currentMessages' | 'loadFirstPage'>) {
74
+ const queryClient = useQueryClient()
75
+ const query = getMessagesInfiniteQuery({ conversationId, profileId, enabled })
76
+
77
+ const messageReceived = useCallback(
78
+ (data: IMessage) => {
79
+ if (data.conversationId !== conversationId) return
80
+
81
+ void queryClient.invalidateQueries({ queryKey: query.queryKey })
82
+ },
83
+ [conversationId, queryClient, query.queryKey]
84
+ )
85
+
86
+ useEffect(() => {
87
+ SparkieService.subscribeEvents({ messageReceived })
88
+
89
+ return () => {
90
+ SparkieService.removeEventSubscription({ messageReceived })
91
+ }
92
+ }, [messageReceived])
93
+
94
+ return useInfiniteQuery(query)
95
+ }
96
+
97
+ export default useInfiniteGetMessages
@@ -0,0 +1,2 @@
1
+ export * from './use-manage-scroll'
2
+ export { default as useManageScroll } from './use-manage-scroll'
@@ -0,0 +1,66 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import type { RefObject } from 'react'
3
+
4
+ import { useAllMessages } from '../use-all-messages'
5
+
6
+ const threshold = 250 // min scroll pos to show scroller button
7
+
8
+ function useManageScroll<T extends HTMLElement>(scrollerRef: RefObject<T | null>) {
9
+ const { messagesQuery, hasMessages } = useAllMessages()
10
+ const [heightBeforeRender, setHeightBeforeRender] = useState(0)
11
+ const [showScrollButton, setShowScrollButton] = useState(false)
12
+
13
+ const handleScroll = useCallback(() => {
14
+ const scroller = scrollerRef.current
15
+
16
+ if (!scroller) return
17
+
18
+ if (messagesQuery.hasNextPage && scroller && scroller.scrollTop < 50) {
19
+ setHeightBeforeRender(scroller.scrollHeight)
20
+ void messagesQuery.fetchNextPage()
21
+ }
22
+ }, [messagesQuery, scrollerRef])
23
+
24
+ useEffect(() => {
25
+ const scroller = scrollerRef.current
26
+
27
+ if (!scroller || !hasMessages || !messagesQuery.isSuccess) return
28
+
29
+ scroller.addEventListener('scrollend', handleScroll)
30
+
31
+ return () => {
32
+ scroller.removeEventListener('scrollend', handleScroll)
33
+ }
34
+ }, [handleScroll, hasMessages, messagesQuery.isSuccess, scrollerRef])
35
+
36
+ useEffect(() => {
37
+ const scroller = scrollerRef.current
38
+
39
+ if (!scroller || !hasMessages || !messagesQuery.isSuccess) return
40
+
41
+ const handleShowBtn = () => {
42
+ const scrollPosition =
43
+ Number(scroller.scrollHeight) - Number(scroller.scrollTop) - Number(scroller.clientHeight)
44
+
45
+ setShowScrollButton(scrollPosition > threshold)
46
+ }
47
+
48
+ scroller.addEventListener('scroll', handleShowBtn)
49
+
50
+ return () => {
51
+ scroller.removeEventListener('scroll', handleShowBtn)
52
+ }
53
+ }, [hasMessages, messagesQuery.isSuccess, scrollerRef])
54
+
55
+ useEffect(() => {
56
+ const scroller = scrollerRef.current
57
+
58
+ if (scroller && !messagesQuery.isFetching) {
59
+ scroller.scrollTop = scroller.scrollHeight - heightBeforeRender
60
+ }
61
+ }, [heightBeforeRender, messagesQuery.isFetching, scrollerRef])
62
+
63
+ return { showScrollButton }
64
+ }
65
+
66
+ export default useManageScroll
@@ -0,0 +1,58 @@
1
+ import { chance } from '@/src/config/tests'
2
+ import ParsedMessageBuilder from '../../__tests__/parsed-message.builder'
3
+ import type { ParsedMessage } from '../../types'
4
+
5
+ import hasToUpdateCursor from './has-to-update-cursor'
6
+
7
+ describe('hasToUpdateCursor', () => {
8
+ const messages = [new ParsedMessageBuilder()]
9
+ const currentCursor = chance.timestamp()
10
+ const profileId = messages?.at?.(0)?.contact.id ?? ''
11
+
12
+ it('should return false when given a empty profileId', () => {
13
+ expect(hasToUpdateCursor({ messages, currentCursor, profileId: '' })).toBe(false)
14
+ })
15
+
16
+ it('should return false when given a empty messages list', () => {
17
+ expect(hasToUpdateCursor({ messages: [], currentCursor, profileId })).toBe(false)
18
+ })
19
+
20
+ it('should return false when the given messages list last item contact.id is equals to `hotmartContactId`', () => {
21
+ expect(
22
+ hasToUpdateCursor({
23
+ messages: [new ParsedMessageBuilder().withContact({ id: 'hotmartContactId' })],
24
+ currentCursor,
25
+ profileId
26
+ })
27
+ ).toBe(false)
28
+ })
29
+
30
+ it('should return false when the given messages list last item is mine', () => {
31
+ expect(
32
+ hasToUpdateCursor({
33
+ messages: [
34
+ new ParsedMessageBuilder().withContact({ id: profileId }).withMetadata({
35
+ ...((messages.at(0)?.metadata ?? {}) as ParsedMessage['metadata']),
36
+ author: 'user'
37
+ })
38
+ ],
39
+ currentCursor,
40
+ profileId
41
+ })
42
+ ).toBe(false)
43
+ })
44
+
45
+ it('should return false when current cursor timestamp is greater than the last message item timestamp', () => {
46
+ expect(
47
+ hasToUpdateCursor({
48
+ messages: [new ParsedMessageBuilder().withTimestamp(currentCursor - 1)],
49
+ currentCursor,
50
+ profileId
51
+ })
52
+ ).toBe(false)
53
+ })
54
+
55
+ it('should return true when it has to update the cursor', () => {
56
+ expect(hasToUpdateCursor({ messages, currentCursor, profileId })).toBe(true)
57
+ })
58
+ })
@@ -0,0 +1,30 @@
1
+ import type { ParsedMessage } from '../../types'
2
+
3
+ export type HasToUpdateCursorProps = {
4
+ messages: Array<ParsedMessage>
5
+ currentCursor: number
6
+ profileId: string
7
+ }
8
+
9
+ function hasToUpdateCursor({
10
+ currentCursor,
11
+ messages,
12
+ profileId
13
+ }: HasToUpdateCursorProps): boolean {
14
+ if (!profileId || !messages?.length) return false
15
+
16
+ const lastMessage = messages
17
+ .slice()
18
+ .sort((a, b) => b.timestamp - a.timestamp)
19
+ ?.at(0)
20
+
21
+ if (!lastMessage || lastMessage.contact.id === 'hotmartContactId') return false
22
+
23
+ const isMine = lastMessage.metadata.author === 'user' && lastMessage.contact.id === profileId
24
+
25
+ if (isMine || currentCursor > lastMessage.timestamp) return false
26
+
27
+ return true
28
+ }
29
+
30
+ export default hasToUpdateCursor
@@ -0,0 +1,2 @@
1
+ export * from './has-to-update-cursor'
2
+ export { default as hasToUpdateCursor } from './has-to-update-cursor'
@@ -0,0 +1,33 @@
1
+ import ICursorUpdateBuilder from '../../cursor/__tests__/icursor-update.builder'
2
+ import type { IMessageWithSenderData } from '../../messages'
3
+ import IMessageWithSenderDataMock from '../../messages/__tests__/imessage-with-sender-data.mock'
4
+
5
+ export const SparkieMessageServiceMock = {
6
+ getAll: vi
7
+ .fn()
8
+ .mockReturnValue(new IMessageWithSenderDataMock().getMany(10) as IMessageWithSenderData[]),
9
+ post: vi.fn(),
10
+ postDirect: vi.fn(),
11
+ remove: vi.fn()
12
+ }
13
+
14
+ export const SparkieCursorServiceMock = {
15
+ update: vi.fn(() => new ICursorUpdateBuilder())
16
+ }
17
+
18
+ export const SparkieDefaultMethodsMock = {
19
+ destroy: vi.fn(),
20
+ init: vi.fn(),
21
+ off: vi.fn(),
22
+ on: vi.fn(),
23
+ listener: { trackTyping: vi.fn() },
24
+ setAPIToken: vi.fn()
25
+ }
26
+
27
+ const SparkieMock = vi.fn(() => ({
28
+ ...SparkieDefaultMethodsMock,
29
+ messageService: SparkieMessageServiceMock,
30
+ cursorService: SparkieCursorServiceMock
31
+ }))
32
+
33
+ export default SparkieMock
@@ -25,6 +25,12 @@ class WidgetSettingPropsBuilder implements WidgetSettingProps {
25
25
  this.conversationId = chance.guid()
26
26
  this.productId = 4234
27
27
  this.productName = 'Berim CDs'
28
+ this.conversationId = chance.guid()
29
+ this.contactId = chance.guid()
30
+ this.clubName = 'Test Standard Club'
31
+ this.membershipId = 'test-standard-club'
32
+ this.tutorName = 'Professor Test'
33
+ this.userId = chance.guid()
28
34
  }
29
35
 
30
36
  withHotmartToken(hotmartToken: typeof this.hotmartToken) {
@@ -0,0 +1,28 @@
1
+ import { render, screen, waitFor } from '@/src/config/tests'
2
+ import type { IMessageWithSenderData } from '@/src/modules/messages'
3
+ import IMessageWithSenderDataMock from '@/src/modules/messages/__tests__/imessage-with-sender-data.mock'
4
+ import WidgetSettingPropsBuilder from '../../__tests__/widget-settings-props.builder'
5
+ import * as Store from '../../store'
6
+
7
+ import ChatPage from './chat-page'
8
+
9
+ describe('ChatPage', () => {
10
+ const defaultSettings = new WidgetSettingPropsBuilder()
11
+ const getMessagesMock = new IMessageWithSenderDataMock().getMany(10) as IMessageWithSenderData[]
12
+
13
+ const renderComponent = () => render(<ChatPage />)
14
+
15
+ beforeEach(() => {
16
+ vi.spyOn(Store, 'useWidgetSettingsAtomValue').mockReturnValue(defaultSettings)
17
+ })
18
+
19
+ it('should render each fetched message item from API', async () => {
20
+ renderComponent()
21
+
22
+ await waitFor(() =>
23
+ expect(screen.getAllByTestId('messages-item')).toHaveLength(getMessagesMock.length)
24
+ )
25
+
26
+ expect(screen.getByPlaceholderText(/send_message.field.placeholder/)).toBeInTheDocument()
27
+ })
28
+ })
@@ -7,9 +7,7 @@ function ChatPage() {
7
7
 
8
8
  return (
9
9
  <>
10
- <div className='overflow-auto px-5 py-4'>
11
- <MessagesList />
12
- </div>
10
+ <MessagesList />
13
11
  <div className='border-t border-t-neutral-700 px-5 py-4'>
14
12
  <ChatInput name='new-chat-msg-input' ref={chatInputRef} />
15
13
  </div>
@@ -1,27 +1,33 @@
1
- import { useEffect } from 'react'
2
- import { useQueryClient } from '@tanstack/react-query'
3
-
4
- import { getInitSparkieQuery } from '../../hooks'
1
+ import { Spinner } from '@/src/lib/components'
2
+ import { useInitSparkie } from '../../hooks'
5
3
  import { useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
6
4
  import { WIDGET_TABS } from '../constants'
7
5
 
8
6
  function WidgetContainer() {
9
7
  const [settings] = useWidgetSettingsAtom()
10
- const initSparkieQuery = settings ? getInitSparkieQuery(settings) : null
11
- const queryClient = useQueryClient()
8
+ const sparkieQuery = useInitSparkie(settings)
12
9
  const [widgetTabs] = useWidgetTabsAtom()
13
10
 
14
- useEffect(() => {
15
- if (!initSparkieQuery) return
16
- void (async () => {
17
- await queryClient.prefetchQuery(initSparkieQuery)
18
- })()
19
- }, [initSparkieQuery, queryClient])
20
-
21
11
  if (!settings?.tutorName) return null
22
12
 
13
+ // TODO: change it for the general API error design from FIGMA as soon as it is available
14
+ if (sparkieQuery.isError)
15
+ return (
16
+ <div className='text-neutral-50'>
17
+ <span>Error initializing sparkie</span>
18
+ <button onClick={() => void sparkieQuery.refetch()}>Try again</button>
19
+ </div>
20
+ )
21
+
22
+ if (sparkieQuery.isLoading)
23
+ return (
24
+ <div className='flex flex-col items-center justify-center p-8'>
25
+ <Spinner className='inline-flex h-6 w-6 animate-spin text-neutral-200' />
26
+ </div>
27
+ )
28
+
23
29
  return (
24
- <div className='flex min-h-svh flex-col items-center justify-center bg-neutral-900'>
30
+ <div className='flex min-h-svh flex-col items-center justify-center'>
25
31
  <div className='grid h-svh w-full grid-rows-[1fr_max-content]'>
26
32
  {WIDGET_TABS[widgetTabs.currentTab]}
27
33
  </div>
@@ -2,3 +2,4 @@ export * from './ai-avatar'
2
2
  export * from './chat-page'
3
3
  export * from './container'
4
4
  export * from './greetings-card'
5
+ export * from './scroll-to-bottom-button'
@@ -0,0 +1,2 @@
1
+ export * from './scroll-to-bottom-button'
2
+ export { default as ScrollToBottomButton } from './scroll-to-bottom-button'
@@ -0,0 +1,32 @@
1
+ import { type ButtonHTMLAttributes, type DetailedHTMLProps, forwardRef } from 'react'
2
+ import clsx from 'clsx'
3
+
4
+ import { Icon } from '@/src/lib/components'
5
+
6
+ export interface IScrollToBottomButtonProps
7
+ extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
8
+ show?: boolean
9
+ top?: number
10
+ }
11
+
12
+ const ScrollToBottomButton = forwardRef<HTMLButtonElement, IScrollToBottomButtonProps>(
13
+ ({ show = false, top = 0, onClick, className, ...props }, ref) => (
14
+ <button
15
+ {...props}
16
+ style={{ top }}
17
+ ref={ref}
18
+ className={clsx(
19
+ 'fixed inset-x-1/2 flex size-7 cursor-pointer flex-col items-center justify-center rounded-full bg-neutral-600 text-sm text-neutral-50 outline-none transition-colors duration-300 ease-in hover:scale-110 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-2',
20
+ { 'pointer-events-none opacity-0': !show },
21
+ className
22
+ )}
23
+ onClick={onClick}
24
+ aria-label='Scroller Button'>
25
+ <Icon name='arrow-down' className='size-4' aria-label='Scroller Button Icon' />
26
+ </button>
27
+ )
28
+ )
29
+
30
+ ScrollToBottomButton.displayName = 'ScrollToBottomButton'
31
+
32
+ export default ScrollToBottomButton
@@ -19,3 +19,7 @@ const TutorWidgetEventsList: Array<ITutorWidgetEvent> = [
19
19
  ] as const
20
20
 
21
21
  export const TutorWidgetEvents = new Map(TutorWidgetEventsList.map((e) => [e.name, e]))
22
+
23
+ export const ACTION_EVENTS = {
24
+ SCROLL: 'c3po-app-widget-scroll-to-bottom'
25
+ }
@@ -4,15 +4,17 @@ import { SparkieService } from '@/src/modules/sparkie'
4
4
  import type { WidgetSettingProps } from '@/src/types'
5
5
 
6
6
  export const getInitSparkieQuery = (settings: WidgetSettingProps) => ({
7
- queryKey: ['SparkieService:initializeSparkie', settings.hotmartToken],
7
+ queryKey: ['SparkieService:initializeSparkie', settings?.hotmartToken ?? ''],
8
8
  queryFn: () =>
9
9
  SparkieService.initSparkie({
10
- token: settings.hotmartToken,
10
+ token: settings?.hotmartToken,
11
11
  skipPresenceSetup: true
12
- }),
13
- enabled: Boolean(settings?.hotmartToken?.trim())
12
+ })
14
13
  })
15
14
 
16
- export function useInitSparkie(settings: WidgetSettingProps) {
17
- return useQuery(getInitSparkieQuery(settings))
15
+ export function useInitSparkie(settings: WidgetSettingProps | null) {
16
+ return useQuery({
17
+ ...getInitSparkieQuery(settings as WidgetSettingProps),
18
+ enabled: Boolean(settings?.hotmartToken?.trim())
19
+ })
18
20
  }
@@ -1,2 +1,3 @@
1
+ export * from './widget-container-intrinsic-height.atom'
1
2
  export * from './widget-settings.atom'
2
3
  export * from './widget-tabs.atom'
@@ -0,0 +1,13 @@
1
+ import { atom, useAtom, useAtomValue } from 'jotai'
2
+
3
+ const intrinsicHeightAtom = atom('100svh')
4
+ const setIntrinsicHeightAtom = atom(
5
+ (get) => get(intrinsicHeightAtom),
6
+ (_, set, value: string) => {
7
+ set(intrinsicHeightAtom, value)
8
+ }
9
+ )
10
+
11
+ export const useIntrinsicHeightAtom = () => useAtom(setIntrinsicHeightAtom)
12
+
13
+ export const useIntrinsicHeightAtomValue = () => useAtomValue(intrinsicHeightAtom)