app-tutor-ai-consumer 1.8.1 → 1.8.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 CHANGED
@@ -1,3 +1,5 @@
1
+ ## [1.8.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.8.1...v1.8.2) (2025-07-16)
2
+
1
3
  ## [1.8.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.8.0...v1.8.1) (2025-07-15)
2
4
 
3
5
  # [1.8.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.7.0...v1.8.0) (2025-07-14)
@@ -11,6 +11,7 @@ require('dotenv').config({
11
11
  })
12
12
 
13
13
  module.exports = async function (env) {
14
+ const standaloneMode = env?.local
14
15
  const productionMode = env?.production || env?.staging
15
16
  const releaseFullName = `app-tutor-ai-consumer_v${packageJSON.version}`
16
17
  const fileVersion = `${productionMode ? 'prod' : 'dev'}-${packageJSON?.version}`
@@ -23,7 +24,7 @@ module.exports = async function (env) {
23
24
  */
24
25
  const config = {
25
26
  mode: productionMode ? 'production' : 'development',
26
- entry: productionMode ? paths.INDEX : paths.DEV_BOOTSTRAP,
27
+ entry: standaloneMode ? paths.DEV_BOOTSTRAP : paths.INDEX,
27
28
  ...(productionMode ? {} : require('./utils/devserver.config')),
28
29
  optimization: {
29
30
  usedExports: true,
@@ -159,17 +160,14 @@ module.exports = async function (env) {
159
160
  new rspack.DefinePlugin({
160
161
  'process.env.PROJECT_VERSION': JSON.stringify(packageJSON.version),
161
162
  'process.env.TARGET_ENV': JSON.stringify(process.env.NODE_ENV),
162
- 'process.env.RELEASE_FULL_NAME': JSON.stringify(releaseFullName)
163
+ 'process.env.RELEASE_FULL_NAME': JSON.stringify(releaseFullName),
164
+ 'process.env.STANDALONE_MODE': JSON.stringify(standaloneMode)
163
165
  }),
164
166
  new rspack.ProgressPlugin(),
165
167
  new TsCheckerRspackPlugin(),
166
168
  new rspack.HtmlRspackPlugin({
167
169
  template: path.resolve(paths.PUBLIC, 'index.html'),
168
170
  favicon: path.resolve(paths.PUBLIC, 'favicon.ico')
169
- }),
170
- new rspack.CssExtractRspackPlugin({
171
- filename: productionMode ? `app-tutor-ai-consumer.css` : '[name].[contenthash].css',
172
- chunkFilename: productionMode ? '[id].[contenthash].css' : '[id].[contenthash].css'
173
171
  })
174
172
  ].concat(await getEnvironmentPlugins(!productionMode)),
175
173
  watchOptions: {
@@ -27,8 +27,8 @@ async function getEnvironmentPlugins(isDevelopment) {
27
27
  return [
28
28
  new rspack.EnvironmentPlugin(allEnvs),
29
29
  new rspack.CssExtractRspackPlugin({
30
- filename: '[name].[contenthash].css',
31
- chunkFilename: '[id]-app-tutor-ai-consumer.css'
30
+ filename: 'app-tutor-ai-consumer.css',
31
+ chunkFilename: '[id].[contenthash].css'
32
32
  }),
33
33
  new CompressionPlugin({
34
34
  algorithm: 'gzip'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -5,10 +5,25 @@ import { v4 } from 'uuid'
5
5
  import { LANGUAGES } from './config/i18n'
6
6
  import { devMode } from './lib/utils'
7
7
 
8
+ const rootId = 'app-tutor-ai-widget'
9
+
8
10
  if (devMode) {
9
11
  window.TOKEN = process.env.TOKEN ?? ''
10
12
  void (async () => {
11
- await window.startChatWidget('c3po-app-widget', {
13
+ // TODO: Remove after sidebar implementation
14
+ // Add Local popup css config
15
+ const root = document.getElementById('c3po-app-widget')
16
+ const container = document.createElement('div')
17
+
18
+ container.setAttribute('id', rootId)
19
+ container.setAttribute(
20
+ 'class',
21
+ '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-800 shadow-lg overflow-hidden flex flex-col'
22
+ )
23
+
24
+ root?.appendChild(container)
25
+
26
+ await window.startChatWidget(rootId, {
12
27
  hotmartToken: window.TOKEN,
13
28
  locale: LANGUAGES.PT_BR,
14
29
  conversationId: '21506473-a93c-4b38-9c32-68a5ca37ce73', // OWNER
package/src/index.tsx CHANGED
@@ -7,7 +7,7 @@ import { createRoot } from 'react-dom/client'
7
7
  import { initDayjs } from './config/dayjs'
8
8
  import { initLanguage } from './config/i18n'
9
9
  import { initAxios } from './config/request/api'
10
- import { productionMode } from './lib/utils'
10
+ import { devMode, productionMode } from './lib/utils'
11
11
  import { Main } from './main'
12
12
  import { SparkieService } from './modules/sparkie'
13
13
  import { TutorWidgetEvents, TutorWidgetEventTypes } from './modules/widget'
@@ -33,24 +33,34 @@ window.startChatWidget = async (
33
33
  elementId = 'tutor-chat-app-widget',
34
34
  settings: WidgetSettingProps
35
35
  ) => {
36
- loadMainStyles()
36
+ if (!devMode) {
37
+ loadMainStyles()
38
+ }
37
39
 
38
40
  const rootElement = document.getElementById(elementId) as HTMLElement
39
41
  const root = createRoot(rootElement)
40
42
 
41
- await SparkieService.initSparkie({
42
- token: settings.hotmartToken,
43
- skipPresenceSetup: true,
44
- retryOptions: {
45
- maxRetries: 5,
46
- retryDelay: 2000,
47
- backoffMultiplier: 1.5
48
- }
49
- })
50
43
  initAxios(settings.hotmartToken)
51
44
  await initLanguage(settings.locale)
52
45
  await initDayjs(settings.locale)
53
46
 
47
+ try {
48
+ await SparkieService.initSparkie({
49
+ token: settings?.hotmartToken,
50
+ skipPresenceSetup: true,
51
+ retryOptions: {
52
+ maxRetries: 5,
53
+ retryDelay: 2000,
54
+ backoffMultiplier: 1.5
55
+ }
56
+ })
57
+ await SparkieService.ensureInitialized()
58
+ TutorWidgetEvents.get(TutorWidgetEventTypes.LOADED)?.dispatch()
59
+ } catch (error) {
60
+ console.error(error)
61
+ TutorWidgetEvents.get(TutorWidgetEventTypes.LOADED)?.dispatch({ detail: { isSuccess: false } })
62
+ }
63
+
54
64
  if (root)
55
65
  root.render(
56
66
  <StrictMode>
@@ -0,0 +1 @@
1
+ export * from './shared-ref'
@@ -0,0 +1 @@
1
+ export { default as createSharedRefContext } from './shared-ref'
@@ -0,0 +1,26 @@
1
+ import { createContext, useContext, useRef } from 'react'
2
+ import type { PropsWithChildren, RefObject } from 'react'
3
+
4
+ function createSharedRefContext<T extends HTMLElement>() {
5
+ const SharedRefContext = createContext<RefObject<T | null> | null>(null)
6
+
7
+ const SharedRefContextProvider = ({ children }: PropsWithChildren) => {
8
+ const sharedRef = useRef<T>(null)
9
+ return <SharedRefContext.Provider value={sharedRef}>{children}</SharedRefContext.Provider>
10
+ }
11
+
12
+ const useSharedRefContext = () => {
13
+ const ctx = useContext(SharedRefContext)
14
+
15
+ if (!ctx) throw new Error('SharedRefContext must be used inside a SharedRefContextProvider')
16
+ return ctx
17
+ }
18
+
19
+ return {
20
+ SharedRefContext,
21
+ SharedRefContextProvider,
22
+ useSharedRefContext
23
+ }
24
+ }
25
+
26
+ export default createSharedRefContext
@@ -1,9 +1,12 @@
1
1
  import { chance, render, screen, waitFor } from '@/config/tests'
2
+ import { useWidgetSettingsAtom } from '@/src/modules/widget/store/widget-settings.atom'
2
3
  import WidgetSettingPropsBuilder from '../modules/widget/__tests__/widget-settings-props.builder'
3
- import { useWidgetSettingsAtom } from '../modules/widget/store/widget-settings.atom'
4
4
  import { Main } from '.'
5
5
 
6
- vi.mock('../modules/widget/store/widget-settings.atom', () => ({ useWidgetSettingsAtom: vi.fn() }))
6
+ vi.mock('@/src/modules/widget/store/widget-settings.atom', async (importOriginal) => ({
7
+ ...(await importOriginal()),
8
+ useWidgetSettingsAtom: vi.fn()
9
+ }))
7
10
 
8
11
  describe('Main', () => {
9
12
  const defaultProps = new WidgetSettingPropsBuilder()
@@ -20,7 +23,7 @@ describe('Main', () => {
20
23
  renderComponent({ settings: props })
21
24
 
22
25
  await waitFor(() => {
23
- expect(screen.getByText(/onboarding.description/i)).toBeInTheDocument()
26
+ expect(screen.getByText(/send/i)).toBeInTheDocument()
24
27
  })
25
28
  })
26
29
  })
@@ -1,4 +1,5 @@
1
1
  import { type PropsWithChildren, useEffect } from 'react'
2
+ import { v4 } from 'uuid'
2
3
 
3
4
  import { OptimizelyProvider } from '@/src/config/optimizely'
4
5
  import { QueryProvider } from '@/src/config/tanstack'
@@ -13,6 +14,10 @@ function GlobalProviders({ children, settings }: GlobalProvidersProps) {
13
14
  useEffect(() => {
14
15
  if (!settings || !Object.keys(settings)?.length) return
15
16
 
17
+ if (!settings?.sessionId) {
18
+ settings.sessionId = v4()
19
+ }
20
+
16
21
  setWidgetSettings(settings)
17
22
  }, [setWidgetSettings, settings])
18
23
 
@@ -1,7 +1,8 @@
1
1
  import { lazy, useCallback, useRef } from 'react'
2
2
  import clsx from 'clsx'
3
+ import { createPortal } from 'react-dom'
3
4
 
4
- import { useWidgetLoadingAtomValue } from '@/src/modules/widget'
5
+ import { usePageLayoutMainRefContext, useWidgetLoadingAtomValue } from '@/src/modules/widget'
5
6
  import { useAllMessages, useManageScroll } from '../../hooks'
6
7
  import { useSkeletonRef } from '../../hooks/use-skeleton-ref'
7
8
  import { MessageItem } from '../message-item'
@@ -26,6 +27,7 @@ function MessagesList() {
26
27
  const widgetIsLoading = useWidgetLoadingAtomValue()
27
28
  const skeletonRef = useSkeletonRef()
28
29
  const { showScrollButton } = useManageScroll(scrollerRef)
30
+ const mainLayoutRef = usePageLayoutMainRefContext()
29
31
 
30
32
  const scrollToBottom = useCallback(() => {
31
33
  const { current: scroller } = scrollerRef
@@ -39,23 +41,21 @@ function MessagesList() {
39
41
  }, [])
40
42
 
41
43
  return (
42
- <div ref={scrollerRef} className='relative mx-2 my-4 flex flex-col gap-2 overflow-auto px-4'>
44
+ <div ref={scrollerRef} className='mx-2 my-4 flex flex-col gap-2 overflow-auto px-4'>
43
45
  <MessageItemLoading show={messagesQuery.isFetching} />
44
46
 
45
47
  <MessageItemEndOfScroll
46
48
  show={!messagesQuery.isFetching && !messagesQuery.hasNextPage && allMessages.length > 0}
47
49
  />
48
-
49
- <ScrollToBottomButton
50
- ref={scrollToButtonRef}
51
- top={Math.abs(
52
- Number(scrollerRef.current?.clientHeight) -
53
- Number(scrollToButtonRef.current?.clientHeight) -
54
- 24
50
+ {mainLayoutRef.current &&
51
+ createPortal(
52
+ <ScrollToBottomButton
53
+ ref={scrollToButtonRef}
54
+ show={showScrollButton}
55
+ onClick={scrollToBottom}
56
+ />,
57
+ mainLayoutRef.current
55
58
  )}
56
- show={showScrollButton}
57
- onClick={scrollToBottom}
58
- />
59
59
 
60
60
  {allMessages?.map(([publishingDate, messages], i) => (
61
61
  <div key={i} className='flex flex-1 flex-col justify-center gap-6'>
@@ -153,7 +153,7 @@ class SparkieService {
153
153
  return false
154
154
  }
155
155
 
156
- private async ensureInitialized(): Promise<void> {
156
+ async ensureInitialized(): Promise<void> {
157
157
  if (this.isInitialized) return
158
158
 
159
159
  switch (this.initializationState) {
@@ -0,0 +1,8 @@
1
+ import { createSharedRefContext } from '@/src/lib/contexts'
2
+
3
+ const { useSharedRefContext, SharedRefContext, SharedRefContextProvider } =
4
+ createSharedRefContext<HTMLDivElement>()
5
+
6
+ export const usePageLayoutMainRefContext = useSharedRefContext
7
+ export const PageLayoutMainRefContext = SharedRefContext
8
+ export const PageLayoutMainRefContextProvider = SharedRefContextProvider
@@ -1,2 +1,3 @@
1
+ export * from './constants'
1
2
  export * from './page-layout'
2
3
  export { default as PageLayout } from './page-layout'
@@ -1,18 +1,25 @@
1
+ import { type PropsWithChildren, type ReactNode } from 'react'
1
2
  import clsx from 'clsx'
2
- import type { PropsWithChildren, ReactNode } from 'react'
3
+
4
+ import { PageLayoutMainRefContextProvider, usePageLayoutMainRefContext } from './constants'
3
5
 
4
6
  export type PageLayoutProps = PropsWithChildren<{
5
7
  asideChild?: ReactNode
6
8
  className?: string
7
9
  }>
10
+
8
11
  function PageLayout({ asideChild, children, className }: PageLayoutProps) {
12
+ const mainLayoutRef = usePageLayoutMainRefContext()
13
+
9
14
  return (
10
15
  <div
11
16
  className={clsx(
12
17
  'grid-areas-[main_aside] grid h-full min-h-0 w-full grid-cols-1 grid-rows-[1fr_auto]',
13
18
  className
14
19
  )}>
15
- <div className='grid-area-[main] flex min-h-0 flex-col overflow-y-auto overflow-x-hidden'>
20
+ <div
21
+ ref={mainLayoutRef}
22
+ className='grid-area-[main] relative flex min-h-0 flex-col overflow-y-auto overflow-x-hidden'>
16
23
  {children}
17
24
  </div>
18
25
  {asideChild && (
@@ -24,4 +31,12 @@ function PageLayout({ asideChild, children, className }: PageLayoutProps) {
24
31
  )
25
32
  }
26
33
 
27
- export default PageLayout
34
+ function PageLayoutWrapper(props: PageLayoutProps) {
35
+ return (
36
+ <PageLayoutMainRefContextProvider>
37
+ <PageLayout {...props} />
38
+ </PageLayoutMainRefContextProvider>
39
+ )
40
+ }
41
+
42
+ export default PageLayoutWrapper
@@ -10,14 +10,13 @@ export interface IScrollToBottomButtonProps
10
10
  }
11
11
 
12
12
  const ScrollToBottomButton = forwardRef<HTMLButtonElement, IScrollToBottomButtonProps>(
13
- ({ show = false, top = 0, onClick, className, ...props }, ref) => (
13
+ ({ show = false, onClick, className, ...props }, ref) => (
14
14
  <button
15
15
  {...props}
16
- style={isNaN(Number(top)) ? undefined : { top }}
17
16
  ref={ref}
18
17
  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 },
18
+ 'absolute bottom-4 left-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-neutral-500 focus:ring-offset-2 focus-visible:ring-2 active:ring-2',
19
+ { 'opacity-85': show, 'pointer-events-none opacity-0': !show },
21
20
  className
22
21
  )}
23
22
  onClick={onClick}
@@ -1,21 +1,46 @@
1
1
  import type { ITutorWidgetEvent } from './types'
2
2
 
3
3
  export const TutorWidgetEventTypes = {
4
- OPEN: 'tutor-app-widget-open',
5
- CLOSE: 'tutor-app-widget-close'
4
+ OPEN: 'c3po-app-widget-open',
5
+ CLOSE: 'c3po-app-widget-close',
6
+ LOADED: 'tutor-app-widget-loaded'
6
7
  } as const
7
8
 
8
- const TutorWidgetEventsList: Array<ITutorWidgetEvent> = [
9
+ const TutorWidgetEventsList = [
9
10
  {
10
11
  name: TutorWidgetEventTypes.OPEN,
11
12
  handler: () => () => undefined,
12
- dispatch: () => window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.OPEN))
13
+ dispatch: () => {
14
+ window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.OPEN))
15
+ }
13
16
  },
14
17
  {
15
18
  name: TutorWidgetEventTypes.CLOSE,
16
19
  handler: () => () => undefined,
17
- dispatch: () => window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.CLOSE))
18
- }
20
+ dispatch: () => {
21
+ window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.CLOSE))
22
+ }
23
+ },
24
+ {
25
+ name: TutorWidgetEventTypes.LOADED,
26
+ handler: (callback) => {
27
+ const listener: EventListener = (e) => {
28
+ const evt = e as CustomEvent<{ isSuccess: boolean }>
29
+
30
+ console.log(evt.detail)
31
+
32
+ callback(evt.detail)
33
+ }
34
+ window.addEventListener(TutorWidgetEventTypes.LOADED, listener)
35
+
36
+ return () => {
37
+ window.removeEventListener(TutorWidgetEventTypes.LOADED, listener)
38
+ }
39
+ },
40
+ dispatch: (payload = { detail: { isSuccess: true } }) => {
41
+ window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.LOADED, payload))
42
+ }
43
+ } as ITutorWidgetEvent<{ isSuccess: boolean }>
19
44
  ] as const
20
45
 
21
46
  export const TutorWidgetEvents = new Map(TutorWidgetEventsList.map((e) => [e.name, e]))
@@ -8,8 +8,8 @@ export type WidgetTabsProps = {
8
8
  }
9
9
 
10
10
  const INITIAL_PROPS: WidgetTabsProps = {
11
- currentTab: 'onboarding',
12
- history: new Set(['onboarding'])
11
+ currentTab: 'chat',
12
+ history: new Set(['chat'])
13
13
  }
14
14
 
15
15
  export const widgetTabsAtom = atom<WidgetTabsProps>(INITIAL_PROPS)
@@ -1,7 +1,7 @@
1
1
  import type { TutorWidgetEventTypes } from './events'
2
2
 
3
- export type ITutorWidgetEvent = {
3
+ export type ITutorWidgetEvent<T = unknown> = {
4
4
  name: (typeof TutorWidgetEventTypes)[keyof typeof TutorWidgetEventTypes]
5
- handler: (listener: EventListenerOrEventListenerObject) => () => void
6
- dispatch: <T = unknown>(payload?: CustomEventInit<T>) => void
5
+ handler: (callback: (payload: T) => void) => () => void
6
+ dispatch: (payload?: CustomEventInit<T>) => void
7
7
  }