app-tutor-ai-consumer 1.18.0 → 1.18.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/package.json +1 -1
  3. package/src/config/theme/init-theme.ts +11 -3
  4. package/src/index.tsx +7 -2
  5. package/src/lib/components/icons/gallery.svg +3 -0
  6. package/src/lib/components/icons/icon-names.d.ts +1 -1
  7. package/src/lib/components/icons/interrogation.svg +2 -8
  8. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +9 -0
  9. package/src/lib/hooks/index.ts +0 -1
  10. package/src/main/main.tsx +2 -3
  11. package/src/modules/messages/components/message-item/message-item.tsx +1 -1
  12. package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +10 -2
  13. package/src/modules/widget/components/chat-page/chat-page.tsx +1 -5
  14. package/src/modules/widget/components/constants.tsx +0 -2
  15. package/src/modules/widget/components/greetings-card/greetings-card.tsx +20 -10
  16. package/src/modules/widget/components/header/__tests__/widget-header-props.builder.ts +0 -7
  17. package/src/modules/widget/components/header/header.tsx +18 -11
  18. package/src/modules/widget/components/header/types.ts +1 -1
  19. package/src/modules/widget/components/information-page/constants.ts +6 -2
  20. package/src/modules/widget/components/information-page/information-card/information-card.tsx +22 -5
  21. package/src/modules/widget/components/information-page/information-page.tsx +16 -3
  22. package/src/modules/widget/components/loading-page/loading-page.tsx +5 -5
  23. package/src/modules/widget/components/starter-page/starter-page.tsx +21 -10
  24. package/src/modules/widget/events.ts +24 -5
  25. package/src/modules/widget/hooks/index.ts +1 -0
  26. package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +0 -2
  27. package/src/modules/widget/hooks/use-listen-to-theme-change-event/index.ts +2 -0
  28. package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +30 -0
  29. package/src/lib/hooks/use-default-id/index.ts +0 -1
  30. package/src/lib/hooks/use-default-id/use-default-id.tsx +0 -13
  31. package/src/modules/widget/components/greetings-card/styles.module.css +0 -3
  32. package/src/modules/widget/components/onboarding-page/index.ts +0 -1
  33. package/src/modules/widget/components/onboarding-page/onboarding-page.tsx +0 -41
  34. package/src/modules/widget/components/onboarding-page/styles.module.css +0 -7
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [1.18.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.18.1...v1.18.2) (2025-07-28)
2
+
3
+ ## [1.18.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.18.0...v1.18.1) (2025-07-24)
4
+
1
5
  # [1.18.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.17.0...v1.18.0) (2025-07-24)
2
6
 
3
7
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.18.0",
3
+ "version": "1.18.2",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -1,7 +1,15 @@
1
1
  import type { Theme } from '@/src/types'
2
2
 
3
- export function initTheme(theme?: Theme) {
4
- if (theme === 'dark') return document.body.classList.add(theme, 'bg-ai-dark')
3
+ export function initTheme(theme: Theme) {
4
+ const rootElement = document.querySelector('#hotmart-app-tutor-ai-consumer-root')
5
5
 
6
- return document.body.classList.add('bg-neutral-100')
6
+ if (!rootElement) return
7
+
8
+ if (theme === 'dark') {
9
+ rootElement.classList.remove('bg-neutral-100')
10
+ return rootElement.classList.add(theme, 'bg-ai-dark')
11
+ }
12
+
13
+ rootElement.classList.remove('dark', 'bg-ai-dark')
14
+ return rootElement.classList.add('bg-neutral-0')
7
15
  }
package/src/index.tsx CHANGED
@@ -4,11 +4,13 @@ import './config/styles/index.css'
4
4
  import { StrictMode } from 'react'
5
5
  import { createRoot } from 'react-dom/client'
6
6
 
7
+ import { initTheme } from '@/src/config/theme'
8
+
7
9
  import { initLanguage } from './config/i18n'
8
10
  import { devMode, productionMode } from './lib/utils'
9
11
  import { Main } from './main'
10
12
  import { TutorWidgetEvents } from './modules/widget'
11
- import type { WidgetSettingProps } from './types'
13
+ import type { Theme, WidgetSettingProps } from './types'
12
14
 
13
15
  const loadMainStyles = () => {
14
16
  const isProduction = productionMode
@@ -35,14 +37,17 @@ window.startChatWidget = async (
35
37
  }
36
38
 
37
39
  const rootElement = document.getElementById(elementId) as HTMLElement
40
+ rootElement.setAttribute('id', 'hotmart-app-tutor-ai-consumer-root')
41
+ const theme = (rootElement.getAttribute('data-theme') ?? 'dark') as Theme
38
42
  const root = createRoot(rootElement)
39
43
 
40
44
  await initLanguage(settings.locale)
45
+ initTheme(theme)
41
46
 
42
47
  if (root) {
43
48
  root.render(
44
49
  <StrictMode>
45
- <Main settings={settings} />
50
+ <Main settings={{ ...settings, config: { ...settings.config, theme } }} />
46
51
  </StrictMode>
47
52
  )
48
53
  }
@@ -0,0 +1,3 @@
1
+ <svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M6.7166 0.880408C6.88983 0.963668 7 1.13886 7 1.33106V3.83106H9.23488C9.93358 3.83106 10.5944 4.14867 11.0309 4.69426L12.8904 7.01871C13.0105 7.1688 13.0339 7.37442 12.9507 7.54765C12.8674 7.72089 12.6922 7.83106 12.5 7.83106H10V10.0659C10 10.7646 9.6824 11.4255 9.1368 11.8619L6.81235 13.7215C6.66227 13.8416 6.45664 13.865 6.28341 13.7817C6.11018 13.6985 6 13.5233 6 13.3311V10.8311H3.76513C3.06643 10.8311 2.40561 10.5135 1.96913 9.96786L0.10957 7.64341C-0.0104977 7.49332 -0.0339069 7.2877 0.0493531 7.11447C0.132613 6.94123 0.307802 6.83106 0.500005 6.83106H3V4.59618C3 3.89748 3.31761 3.23666 3.86321 2.80019L6.18766 0.940625C6.33774 0.820557 6.54337 0.797148 6.7166 0.880408ZM1.54032 7.83106L2.75 9.34316C2.9967 9.65154 3.37021 9.83106 3.76513 9.83106H6V7.83106H1.54032ZM6 6.83106H4V4.59618C4 4.20127 4.17952 3.82776 4.4879 3.58105L6 2.37137V6.83106ZM7 7.83106V12.2907L8.51211 11.0811C8.82049 10.8344 9 10.4609 9 10.0659V7.83106H7ZM7 6.83106V4.83106H9.23488C9.6298 4.83106 10.0033 5.01058 10.25 5.31896L11.4597 6.83106H7Z" fill="currentColor"/>
3
+ </svg>
@@ -11,4 +11,4 @@ export type ValidIconNames =
11
11
  | 'stop'
12
12
  | 'warning'
13
13
  | 'interrogation'
14
- | 'book'
14
+ | 'gallery'
@@ -1,9 +1,3 @@
1
- <svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <path d="M8 0.73584C3.5625 0.73584 0 4.32959 0 8.73584C0 13.1733 3.5625 16.7358 8 16.7358C12.4062 16.7358 16 13.1733 16 8.73584C16 4.32959 12.4062 0.73584 8 0.73584ZM8 15.7358C4.125 15.7358 1 12.6108 1 8.73584C1 4.89209 4.125 1.73584 8 1.73584C11.8438 1.73584 15 4.89209 15 8.73584C15 12.6108 11.8438 15.7358 8 15.7358ZM7.5 11.4858C7.0625 11.4858 6.75 11.8296 6.75 12.2358C6.75 12.6733 7.0625 12.9858 7.5 12.9858C7.90625 12.9858 8.25 12.6733 8.25 12.2358C8.25 11.8296 7.90625 11.4858 7.5 11.4858ZM8.90625 4.73584H7.0625C5.90625 4.73584 5 5.67334 5 6.82959V7.11084C5 7.39209 5.21875 7.61084 5.5 7.61084C5.75 7.61084 6 7.39209 6 7.11084V6.82959C6 6.23584 6.46875 5.73584 7.0625 5.73584H8.90625C9.5 5.73584 10 6.23584 10 6.82959C10 7.20459 9.78125 7.57959 9.4375 7.76709L7.5625 8.70459C7.21875 8.89209 7 9.26709 7 9.67334V10.2358C7 10.5171 7.21875 10.7358 7.5 10.7358C7.75 10.7358 8 10.5171 8 10.2358V9.67334C8 9.64209 8 9.61084 8.03125 9.57959L9.90625 8.64209C10.5625 8.26709 11 7.57959 11 6.82959C11 5.67334 10.0625 4.73584 8.90625 4.73584Z" fill="url(#paint0_linear_21475_7939)"/>
3
- <defs>
4
- <linearGradient id="paint0_linear_21475_7939" x1="0" y1="8.73584" x2="16" y2="8.73584" gradientUnits="userSpaceOnUse">
5
- <stop stop-color="#44D0FF"/>
6
- <stop offset="1" stop-color="#B48EFF"/>
7
- </linearGradient>
8
- </defs>
1
+ <svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M6 1.83105C3.23858 1.83105 1 4.06963 1 6.83105C1 9.59248 3.23858 11.8311 6 11.8311C8.76142 11.8311 11 9.59248 11 6.83105C11 4.06963 8.76142 1.83105 6 1.83105ZM6 12.8311C2.68629 12.8311 0 10.1448 0 6.83105C0 3.51735 2.68629 0.831055 6 0.831055C9.31371 0.831055 12 3.51735 12 6.83105C12 10.1448 9.31371 12.8311 6 12.8311ZM6 8.83105C6.41421 8.83105 6.75 9.16684 6.75 9.58105C6.75 9.99527 6.41421 10.3311 6 10.3311C5.58579 10.3311 5.25 9.99527 5.25 9.58105C5.25 9.16684 5.58579 8.83105 6 8.83105ZM5 5.33105C5 4.77877 5.44772 4.33105 6 4.33105H6.5C7.05228 4.33105 7.5 4.77877 7.5 5.33105V5.49772C7.5 5.95796 7.1269 6.33105 6.66667 6.33105C6.02233 6.33105 5.5 6.85339 5.5 7.49772V7.83105H6.5V7.49772C6.5 7.40567 6.57462 7.33105 6.66667 7.33105C7.67919 7.33105 8.5 6.51024 8.5 5.49772V5.33105C8.5 4.22649 7.60457 3.33105 6.5 3.33105H6C4.89543 3.33105 4 4.22649 4 5.33105H5Z" fill="currentColor"/>
9
3
  </svg>
@@ -78,6 +78,15 @@ const mdComponents: Partial<Components> = {
78
78
  },
79
79
  p({ children }) {
80
80
  return <span className='my-3 inline-block'>{children}</span>
81
+ },
82
+ ul({ children }) {
83
+ return <ul className='my-3 list-inside list-disc'>{children}</ul>
84
+ },
85
+ ol({ children }) {
86
+ return <ol className='my-3 list-inside list-decimal'>{children}</ol>
87
+ },
88
+ li({ children }) {
89
+ return <li className='[&:not(:last-child)]:mb-1'>{children}</li>
81
90
  }
82
91
  }
83
92
 
@@ -1,4 +1,3 @@
1
- export * from './use-default-id'
2
1
  export * from './use-intersection-observer-reverse-scroll'
3
2
  export * from './use-ref-client-height'
4
3
  export * from './use-ref-event-listener'
package/src/main/main.tsx CHANGED
@@ -1,11 +1,10 @@
1
1
  import '@/config/styles/index.css'
2
2
 
3
3
  import { ErrorBoundary, GenericError } from '@/src/lib/components/errors'
4
- import { useDefaultId } from '@/src/lib/hooks'
5
4
  import { useAppLang } from '../config/i18n'
6
5
  import { GlobalProviders } from '../modules/global-providers'
7
6
  import { WidgetContainer } from '../modules/widget'
8
- import { useInitWidget } from '../modules/widget/hooks'
7
+ import { useInitWidget, useListenToThemeChangeEvent } from '../modules/widget/hooks'
9
8
  import type { WidgetSettingProps } from '../types'
10
9
 
11
10
  export type MainProps = {
@@ -14,8 +13,8 @@ export type MainProps = {
14
13
 
15
14
  function Main({ settings }: MainProps) {
16
15
  const { completeSetup } = useInitWidget(settings)
17
- useDefaultId()
18
16
  useAppLang(settings.locale)
17
+ useListenToThemeChangeEvent()
19
18
 
20
19
  return (
21
20
  <ErrorBoundary fallback={<GenericError />}>
@@ -17,7 +17,7 @@ function MessageItem({ message }: { message: ParsedMessage }) {
17
17
  'max-w-[min(80%,52rem)] overflow-x-hidden rounded-lg px-3 text-sm/normal text-neutral-900',
18
18
  {
19
19
  'self-end bg-neutral-200': message.metadata.author === 'user',
20
- 'bg-ai-chat-response': message.metadata.author === 'ai'
20
+ 'border border-neutral-300 bg-neutral-100': message.metadata.author === 'ai'
21
21
  }
22
22
  )}>
23
23
  <MarkdownRenderer content={message?.text ?? message?.name} imgComponent={imgComponent} />
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo } from 'react'
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react'
2
2
  import type { InfiniteData } from '@tanstack/react-query'
3
3
  import { useQueryClient } from '@tanstack/react-query'
4
4
  import { produce } from 'immer'
@@ -18,6 +18,7 @@ const useSubscribeMessageReceivedEvent = () => {
18
18
  const [, setWidgetLoading] = useWidgetLoadingAtom()
19
19
  const [, addUnreadMessagesToSet] = useUnreadMessagesSetAtom()
20
20
  const useUpdateCursorMutation = useUpdateCursor()
21
+ const idsList = useRef<Set<string>>(new Set())
21
22
 
22
23
  const conversationId = useMemo(() => String(settings?.conversationId), [settings?.conversationId])
23
24
  const profileId = useMemo(() => String(profileQuery?.data?.id), [profileQuery?.data?.id])
@@ -28,8 +29,10 @@ const useSubscribeMessageReceivedEvent = () => {
28
29
  enabled: true
29
30
  })
30
31
 
31
- useEffect(() => {
32
+ const execute = useCallback(() => {
32
33
  const messageReceived = (data: IMessageWithSenderData) => {
34
+ if (idsList.current.has(data.id)) return
35
+
33
36
  const queryKey = query.queryKey
34
37
 
35
38
  if (!queryKey || !queryClient) return
@@ -71,6 +74,7 @@ const useSubscribeMessageReceivedEvent = () => {
71
74
  // The cursor should update only with my messages
72
75
  useUpdateCursorMutation.mutate(data.conversationId)
73
76
  }
77
+ idsList.current.add(data.id)
74
78
  }
75
79
 
76
80
  SparkieService.subscribeEvents({
@@ -90,6 +94,10 @@ const useSubscribeMessageReceivedEvent = () => {
90
94
  setWidgetLoading,
91
95
  useUpdateCursorMutation
92
96
  ])
97
+
98
+ useEffect(() => {
99
+ execute()
100
+ }, [execute])
93
101
  }
94
102
 
95
103
  export default useSubscribeMessageReceivedEvent
@@ -47,11 +47,7 @@ function ChatPage() {
47
47
  }>
48
48
  <>
49
49
  <div className='mt-4 px-6 py-4'>
50
- <WidgetHeader
51
- enabledButtons={['info', 'close']}
52
- clubName={settings?.clubName}
53
- tutorName={settings?.tutorName}
54
- />
50
+ <WidgetHeader enabledButtons={['info', 'close']} tutorName={settings?.tutorName} />
55
51
  </div>
56
52
  <MessagesList />
57
53
  </>
@@ -1,11 +1,9 @@
1
1
  import { ChatPage } from './chat-page'
2
2
  import { WidgetInformationPage } from './information-page'
3
3
  import { WidgetLoadingPage } from './loading-page'
4
- import { WidgetOnboardingPage } from './onboarding-page'
5
4
  import { WidgetStarterPage } from './starter-page'
6
5
 
7
6
  export const WIDGET_TABS = {
8
- onboarding: <WidgetOnboardingPage />,
9
7
  starter: <WidgetStarterPage />,
10
8
  chat: <ChatPage />,
11
9
  loading: <WidgetLoadingPage />,
@@ -1,31 +1,41 @@
1
1
  import clsx from 'clsx'
2
2
  import { useTranslation } from 'react-i18next'
3
3
 
4
- import { AIAvatar } from '../ai-avatar'
5
-
6
- import styles from './styles.module.css'
7
-
8
4
  export type GreetingsCardProps = {
9
5
  tutorName: string
10
6
  author: string
7
+ isDarkTheme?: boolean
11
8
  }
12
9
 
13
- function GreetingsCard({ author, tutorName }: GreetingsCardProps) {
10
+ function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCardProps) {
14
11
  const { t } = useTranslation()
15
12
 
16
13
  return (
17
- <div className='flex flex-col items-center justify-center text-neutral-900'>
14
+ <div className='flex flex-col items-center justify-center'>
18
15
  <div className='flex flex-col items-center justify-center gap-4 text-center'>
19
- <AIAvatar />
20
16
  <div className='flex flex-col gap-2'>
21
- <span className='text-base font-light'>
17
+ <span
18
+ className={clsx('text-base font-light', {
19
+ 'text-white': isDarkTheme,
20
+ 'text-gray-900': !isDarkTheme
21
+ })}>
22
22
  {t('general.greetings.hello', { name: author })}
23
23
  </span>
24
- <h3 className={clsx('text-xl', styles.faceTxt)}>
24
+ <h3
25
+ className={clsx('text-xl font-bold', {
26
+ 'text-white': isDarkTheme,
27
+ 'text-gray-900': !isDarkTheme
28
+ })}>
25
29
  {t('general.greetings.firstMessage', { tutorName })}
26
30
  </h3>
27
31
  </div>
28
- <p className='text-sm font-normal text-neutral-600'>{t('general.greetings.description')}</p>
32
+ <p
33
+ className={clsx('text-sm font-normal', {
34
+ 'text-gray-400': isDarkTheme,
35
+ 'text-neutral-600': !isDarkTheme
36
+ })}>
37
+ {t('general.greetings.description')}
38
+ </p>
29
39
  </div>
30
40
  </div>
31
41
  )
@@ -5,7 +5,6 @@ class WidgetHeaderPropsBuilder implements WidgetHeaderProps {
5
5
  enabledButtons: ValidIconNames[]
6
6
  showContent?: boolean
7
7
  showContentWithoutMeta?: boolean
8
- clubName?: string
9
8
  tutorName?: string
10
9
 
11
10
  constructor() {
@@ -30,12 +29,6 @@ class WidgetHeaderPropsBuilder implements WidgetHeaderProps {
30
29
  return this
31
30
  }
32
31
 
33
- withClubName(clubName: typeof this.clubName) {
34
- this.clubName = clubName
35
-
36
- return this
37
- }
38
-
39
32
  withTutorName(tutorName: typeof this.tutorName) {
40
33
  this.tutorName = tutorName
41
34
 
@@ -5,13 +5,12 @@ import { AIAvatar } from '../ai-avatar'
5
5
 
6
6
  import type { WidgetHeaderContentProps, WidgetHeaderProps } from './types'
7
7
 
8
- export function WidgetHeaderContent({ clubName, tutorName }: WidgetHeaderContentProps) {
8
+ export function WidgetHeaderContent({ tutorName }: WidgetHeaderContentProps) {
9
9
  return (
10
10
  <div className='flex w-full gap-2'>
11
11
  <AIAvatar />
12
- <div className='flex flex-col'>
13
- {tutorName && <h4 className='text-base'>{tutorName}</h4>}
14
- {clubName && <p className='text-sm/normal text-neutral-600'>{clubName}</p>}
12
+ <div className='flex flex-col justify-center'>
13
+ {tutorName && <h4 className='text-base font-bold'>{tutorName}</h4>}
15
14
  </div>
16
15
  </div>
17
16
  )
@@ -32,32 +31,40 @@ export function WidgetHeaderContentWithoutMeta({ name }: { name?: string }) {
32
31
 
33
32
  function WidgetHeader({
34
33
  enabledButtons,
35
- clubName,
36
34
  tutorName,
37
35
  showContentWithoutMeta,
38
36
  showContent = true
39
37
  }: WidgetHeaderProps) {
40
- const [, setWidgetTabs] = useWidgetTabsAtom()
38
+ const [, setTab] = useWidgetTabsAtom()
39
+
40
+ const handleClickArchive = () => {
41
+ setTab('chat')
42
+ }
43
+
44
+ const handleClickInfo = () => {
45
+ setTab('information')
46
+ }
41
47
 
42
48
  return (
43
49
  <div className='grid-areas-[a_b] mt-0.5 grid grid-cols-[1fr_auto] items-center text-neutral-1000'>
44
50
  <div className='grid-area-[a]'>
45
- {showContent && !showContentWithoutMeta && (
46
- <WidgetHeaderContent clubName={clubName} tutorName={tutorName} />
47
- )}
51
+ {showContent && !showContentWithoutMeta && <WidgetHeaderContent tutorName={tutorName} />}
48
52
  {showContentWithoutMeta && !showContent && (
49
53
  <WidgetHeaderContentWithoutMeta name={tutorName} />
50
54
  )}
51
55
  </div>
52
56
  <div className='shrink-0'>
53
57
  <div className='grid-area-[b] ml-auto flex max-w-max gap-3 text-neutral-700'>
54
- <Button show={enabledButtons.includes('archive')} aria-label='Archive Icon'>
58
+ <Button
59
+ show={enabledButtons.includes('archive')}
60
+ onClick={handleClickArchive}
61
+ aria-label='Archive Icon'>
55
62
  <Icon name='archive' className='h-4 w-4' aria-hidden />
56
63
  </Button>
57
64
  <Button
58
65
  show={enabledButtons.includes('info')}
59
66
  aria-label='Info Icon'
60
- onClick={() => setWidgetTabs('information')}>
67
+ onClick={handleClickInfo}>
61
68
  <Icon name='info' className='h-4 w-4' aria-hidden />
62
69
  </Button>
63
70
  <Button
@@ -1,6 +1,6 @@
1
1
  import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
2
2
 
3
- export type WidgetHeaderContentProps = { clubName?: string; tutorName?: string }
3
+ export type WidgetHeaderContentProps = { tutorName?: string }
4
4
 
5
5
  export type WidgetHeaderProps = {
6
6
  enabledButtons: ValidIconNames[]
@@ -12,6 +12,10 @@ export const infoItems: InfoItem[] = [
12
12
  titleKey: 'info.what_it_does_question',
13
13
  descKey: 'info.what_it_does_answer'
14
14
  },
15
- { icon: 'book', titleKey: 'info.how_it_learns_question', descKey: 'info.how_it_learns_answer' },
16
- { icon: 'warning', titleKey: 'info.limitations_question', descKey: 'info.limitations_answer' }
15
+ {
16
+ icon: 'gallery',
17
+ titleKey: 'info.how_it_learns_question',
18
+ descKey: 'info.how_it_learns_answer'
19
+ },
20
+ { icon: 'info', titleKey: 'info.limitations_question', descKey: 'info.limitations_answer' }
17
21
  ]
@@ -1,3 +1,5 @@
1
+ import clsx from 'clsx'
2
+
1
3
  import { Icon } from '@/src/lib/components'
2
4
  import type { ValidIconNames } from '@/src/lib/components/icons/icon-names'
3
5
 
@@ -5,18 +7,33 @@ export type InformationCardProps = {
5
7
  icon: ValidIconNames
6
8
  title: string
7
9
  description: string
10
+ isDarkMode: boolean
8
11
  }
9
12
 
10
- function InformationCard({ icon, title, description }: InformationCardProps) {
13
+ function InformationCard({ icon, title, description, isDarkMode }: InformationCardProps) {
11
14
  return (
12
- <div className='flex gap-3 border-b border-white/10 pb-5 last:border-none'>
13
- <div className='flex h-5 w-5 items-start justify-center'>
15
+ <div
16
+ className={clsx('flex justify-center gap-3 border-b pb-5 last:border-none', {
17
+ 'border-white/10': isDarkMode,
18
+ 'border-black/10': !isDarkMode
19
+ })}>
20
+ <div
21
+ className={clsx('flex h-5 w-5 items-start justify-center', {
22
+ 'text-white': isDarkMode,
23
+ 'text-neutral-900': !isDarkMode
24
+ })}>
14
25
  <Icon name={icon} width={16} height={16} />
15
26
  </div>
16
27
 
17
28
  <div className='flex flex-col gap-1'>
18
- <p className='text-sm font-bold'>{title}</p>
19
- <p className='text-xs text-gray-300'>{description}</p>
29
+ <p className='text-sm font-bold text-neutral-900'>{title}</p>
30
+ <p
31
+ className={clsx('text-xs', {
32
+ 'text-gray-300': isDarkMode,
33
+ 'text-gray-500': !isDarkMode
34
+ })}>
35
+ {description}
36
+ </p>
20
37
  </div>
21
38
  </div>
22
39
  )
@@ -1,3 +1,4 @@
1
+ import clsx from 'clsx'
1
2
  import { useTranslation } from 'react-i18next'
2
3
 
3
4
  import { Icon } from '@/src/lib/components'
@@ -12,10 +13,15 @@ function WidgetInformationPage() {
12
13
  const { t } = useTranslation()
13
14
  const [, setWidgetTabs] = useWidgetTabsAtom()
14
15
  const [settings] = useWidgetSettingsAtom()
16
+ const isDarkMode = settings?.config?.theme === 'dark'
15
17
 
16
18
  return (
17
19
  <PageLayout className='p-5 text-white'>
18
- <div className='relative mb-8 flex h-12 items-center justify-center'>
20
+ <div
21
+ className={clsx('relative mb-8 flex h-12 items-center justify-center', {
22
+ 'text-white': isDarkMode,
23
+ 'text-neutral-700': !isDarkMode
24
+ })}>
19
25
  <button
20
26
  className='absolute left-0'
21
27
  aria-label='Return Button'
@@ -25,11 +31,17 @@ function WidgetInformationPage() {
25
31
  <h1 className='mx-auto font-bold'>{t('info.title')}</h1>
26
32
  </div>
27
33
 
28
- <div className='mb-8 flex justify-center'>
34
+ <div className='mb-10 flex justify-center'>
29
35
  <div className='flex flex-col items-center gap-2'>
30
36
  <AIAvatar />
31
37
 
32
- <h3 className='font-bold'>{settings?.tutorName ?? ''}</h3>
38
+ <h3
39
+ className={clsx('font-bold', {
40
+ 'text-white': isDarkMode,
41
+ 'text-neutral-700': !isDarkMode
42
+ })}>
43
+ {settings?.tutorName ?? t('general.name')}
44
+ </h3>
33
45
  </div>
34
46
  </div>
35
47
 
@@ -40,6 +52,7 @@ function WidgetInformationPage() {
40
52
  icon={item.icon}
41
53
  title={t(item.titleKey)}
42
54
  description={t(item.descKey)}
55
+ isDarkMode={isDarkMode}
43
56
  />
44
57
  ))}
45
58
  </div>
@@ -6,13 +6,12 @@ import {
6
6
  MessageSkeleton,
7
7
  useChatInputValueAtom
8
8
  } from '@/src/modules/messages/components'
9
- import { useWidgetSettingsAtomValue } from '../../store'
9
+ import { WidgetHeader } from '../header'
10
10
  import { PageLayout } from '../page-layout'
11
11
 
12
12
  function WidgetLoadingPage() {
13
13
  const chatInputRef = useRef<HTMLTextAreaElement>(null)
14
14
  const [, setChatInputValue] = useChatInputValueAtom()
15
- const settings = useWidgetSettingsAtomValue()
16
15
 
17
16
  const handler = useCallback(
18
17
  (e: Event) => {
@@ -33,11 +32,12 @@ function WidgetLoadingPage() {
33
32
  return (
34
33
  <PageLayout
35
34
  asideChild={<ChatInput name='new-chat-msg-input' ref={chatInputRef} loading={true} />}>
36
- {settings?.config?.theme && (
37
- <div className='flex h-full flex-col justify-end px-5 py-4'>
35
+ <div className='mt-4 flex h-full flex-col justify-start px-6 py-4'>
36
+ <WidgetHeader enabledButtons={['close']} showContent={false} />
37
+ <div className='mt-auto'>
38
38
  <MessageSkeleton />
39
39
  </div>
40
- )}
40
+ </div>
41
41
  </PageLayout>
42
42
  )
43
43
  }
@@ -33,41 +33,52 @@ function WidgetStarterPage() {
33
33
  }
34
34
  })
35
35
 
36
- const handleAskQuestion: MouseEventHandler<HTMLButtonElement> = (e) => {
37
- const textContent = e?.currentTarget?.querySelector(
38
- 'span[data-label=text-content]'
39
- )?.textContent
40
-
36
+ const sendText = (textContent?: string | null) => {
41
37
  if (!textContent) return
42
38
 
43
39
  sendTextMessageMutation.mutate(textContent, {
44
40
  onSuccess() {
45
41
  setWidgetTabs('chat')
42
+ if (chatInputRef.current) chatInputRef.current.value = ''
43
+ setChatInputValue('')
46
44
  }
47
45
  })
48
46
  }
49
47
 
48
+ const handleAskQuestion: MouseEventHandler<HTMLButtonElement> = (e) => {
49
+ sendText(e?.currentTarget?.querySelector('span[data-label=text-content]')?.textContent)
50
+ }
51
+
52
+ const handleSend = () => {
53
+ sendText(chatInputRef.current?.value)
54
+ }
55
+
50
56
  return (
51
57
  <PageLayout
52
58
  asideChild={
53
59
  <ChatInput
54
60
  name='new-chat-msg-input'
55
61
  ref={chatInputRef}
56
- onSend={() => setWidgetTabs('chat')}
62
+ onSend={handleSend}
57
63
  buttonDisabled={!chatInputValue.trim()}
58
64
  />
59
65
  }>
60
66
  <div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
61
- <div className={clsx('grid-area-[a] flex min-h-0 flex-col px-5 py-4', styles.bg)}>
67
+ <div
68
+ className={clsx('grid-area-[a] flex min-h-0 flex-col px-5 py-4', {
69
+ [styles.bg]: settings?.config?.theme === 'dark'
70
+ })}>
62
71
  <WidgetHeader
63
72
  enabledButtons={['archive', 'info', 'close']}
64
- clubName={settings?.clubName}
65
73
  tutorName={settings?.tutorName}
66
- showContent={false}
67
74
  />
68
75
 
69
76
  <div className='my-auto'>
70
- <GreetingsCard author={settings?.author ?? ''} tutorName={settings?.tutorName ?? ''} />
77
+ <GreetingsCard
78
+ author={settings?.author ?? ''}
79
+ tutorName={settings?.tutorName ?? ''}
80
+ isDarkTheme={settings?.config?.theme === 'dark'}
81
+ />
71
82
  </div>
72
83
  </div>
73
84
  <div className='grid-area-[b] mx-5 my-6 flex flex-shrink-0 snap-x snap-mandatory gap-2 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
@@ -1,13 +1,16 @@
1
+ import type { Theme } from '@/src/types'
2
+
1
3
  import type { ITutorWidgetEvent } from './types'
2
4
 
3
5
  export const TutorWidgetEventTypes = {
4
6
  OPEN: 'c3po-app-widget-open',
5
7
  CLOSE: 'c3po-app-widget-close',
6
8
  HIDE: 'c3po-app-widget-hide',
7
- LOADED: 'tutor-app-widget-loaded'
9
+ LOADED: 'tutor-app-widget-loaded',
10
+ THEME_CHANGE: 'c3po-app-widget-theme-change'
8
11
  } as const
9
12
 
10
- const TutorWidgetEventsObject = {
13
+ export const TutorWidgetEvents = {
11
14
  [TutorWidgetEventTypes.OPEN]: {
12
15
  name: TutorWidgetEventTypes.OPEN,
13
16
  handler: (callback) => {
@@ -73,10 +76,26 @@ const TutorWidgetEventsObject = {
73
76
  dispatch: (payload = { detail: { isSuccess: true } }) => {
74
77
  window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.LOADED, payload))
75
78
  }
76
- } as ITutorWidgetEvent<{ isSuccess: boolean }>
77
- } as const
79
+ } as ITutorWidgetEvent<{ isSuccess: boolean }>,
80
+
81
+ [TutorWidgetEventTypes.THEME_CHANGE]: {
82
+ name: TutorWidgetEventTypes.THEME_CHANGE,
83
+ handler: (callback: (payload: { theme: Theme }) => void) => {
84
+ const listener: EventListener = (e) => {
85
+ const evt = e as CustomEvent<{ theme: Theme }>
86
+ callback(evt.detail)
87
+ }
88
+ window.addEventListener(TutorWidgetEventTypes.THEME_CHANGE, listener)
78
89
 
79
- export const TutorWidgetEvents = TutorWidgetEventsObject
90
+ return () => {
91
+ window.removeEventListener(TutorWidgetEventTypes.THEME_CHANGE, listener)
92
+ }
93
+ },
94
+ dispatch: (payload) => {
95
+ window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.THEME_CHANGE, payload))
96
+ }
97
+ } as ITutorWidgetEvent<{ theme: Theme }>
98
+ } as const
80
99
 
81
100
  export const ACTION_EVENTS = {
82
101
  SCROLL: 'c3po-app-widget-scroll-to-bottom'
@@ -1,2 +1,3 @@
1
1
  export * from './use-init-widget'
2
+ export * from './use-listen-to-theme-change-event'
2
3
  export * from './use-listen-to-visibility-events'
@@ -2,14 +2,12 @@ import { useEffect, useState } from 'react'
2
2
 
3
3
  import { initDayjs } from '@/src/config/dayjs'
4
4
  import { initAxios } from '@/src/config/request/api'
5
- import { initTheme } from '@/src/config/theme'
6
5
  import { SparkieService } from '@/src/modules/sparkie'
7
6
  import type { WidgetSettingProps } from '@/src/types'
8
7
  import { TutorWidgetEvents } from '../../events'
9
8
 
10
9
  const init = async (settings: WidgetSettingProps) => {
11
10
  try {
12
- initTheme(settings.config?.theme)
13
11
  initAxios(settings.hotmartToken)
14
12
  await initDayjs(settings.locale)
15
13
  await SparkieService.initSparkie({
@@ -0,0 +1,2 @@
1
+ export * from './use-listen-to-theme-change-event'
2
+ export { default as useListenToThemeChangeEvent } from './use-listen-to-theme-change-event'
@@ -0,0 +1,30 @@
1
+ import { useLayoutEffect } from 'react'
2
+ import { produce } from 'immer'
3
+
4
+ import { initTheme } from '@/src/config/theme'
5
+ import { TutorWidgetEvents } from '../../events'
6
+ import { useWidgetSettingsAtom } from '../../store'
7
+
8
+ function useListenToThemeChangeEvent() {
9
+ const [widgetSettings, setWidgetSettings] = useWidgetSettingsAtom()
10
+
11
+ useLayoutEffect(() => {
12
+ const clear = TutorWidgetEvents['c3po-app-widget-theme-change'].handler(({ theme }) => {
13
+ initTheme(theme)
14
+
15
+ if (!widgetSettings || theme === widgetSettings?.config?.theme) return
16
+
17
+ setWidgetSettings(
18
+ produce(widgetSettings, (draft) => {
19
+ draft.config = { ...draft.config, theme }
20
+
21
+ return draft
22
+ })
23
+ )
24
+ })
25
+
26
+ return () => clear?.()
27
+ }, [setWidgetSettings, widgetSettings])
28
+ }
29
+
30
+ export default useListenToThemeChangeEvent
@@ -1 +0,0 @@
1
- export { default as useDefaultId } from './use-default-id'
@@ -1,13 +0,0 @@
1
- import { useLayoutEffect } from 'react'
2
-
3
- const useDefaultId = () => {
4
- useLayoutEffect(() => {
5
- document.body.setAttribute('id', 'hotmart-app-tutor-ai-consumer-root')
6
-
7
- return () => {
8
- document.body.removeAttribute('id')
9
- }
10
- })
11
- }
12
-
13
- export default useDefaultId
@@ -1,3 +0,0 @@
1
- .faceTxt {
2
- composes: gradientText from '../../../../config/styles/utilities/text-utilities.module.css';
3
- }
@@ -1 +0,0 @@
1
- export { default as WidgetOnboardingPage } from './onboarding-page'
@@ -1,41 +0,0 @@
1
- import clsx from 'clsx'
2
- import { useTranslation } from 'react-i18next'
3
-
4
- import TutorOnboardingSVG from '@/public/assets/svg/tutor-onboarding.svg?url'
5
- import { Button } from '@/src/lib/components'
6
- import { useWidgetTabsAtom } from '../../store'
7
- import { PageLayout } from '../page-layout'
8
-
9
- import styles from './styles.module.css'
10
-
11
- function WidgetOnboardingPage() {
12
- const [, setWidgetTabs] = useWidgetTabsAtom()
13
- const { t } = useTranslation()
14
-
15
- return (
16
- <PageLayout>
17
- <div className={clsx('flex-1', styles.bg)}>
18
- <div className='mx-4 flex h-full flex-col justify-center gap-6 px-0.5'>
19
- <div className='mx-auto max-w-[67%]'>
20
- <img src={TutorOnboardingSVG} aria-hidden />
21
- </div>
22
- <div className='flex flex-col gap-2'>
23
- <h3 className={clsx(styles.gradientTxt, 'text-center text-xl/tight font-semibold')}>
24
- {t('onboarding.title')}
25
- </h3>
26
- <p className='text-center text-sm/snug font-normal text-gray-400'>
27
- {t('onboarding.description')}
28
- </p>
29
- </div>
30
- </div>
31
- </div>
32
- <div className='mx-4 mb-4 mt-auto flex flex-col gap-4'>
33
- <Button variant='brand' className='flex-1' onClick={() => setWidgetTabs('starter')}>
34
- {t('general.buttons.start')}
35
- </Button>
36
- </div>
37
- </PageLayout>
38
- )
39
- }
40
-
41
- export default WidgetOnboardingPage
@@ -1,7 +0,0 @@
1
- .gradientTxt {
2
- composes: gradientText from '../../../../config/styles/utilities/text-utilities.module.css';
3
- }
4
-
5
- .bg {
6
- composes: gradientBg from '../../../../config/styles/utilities/bg-utilities.module.css';
7
- }