app-tutor-ai-consumer 1.13.0 → 1.15.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 CHANGED
@@ -1,3 +1,15 @@
1
+ # [1.15.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.14.0...v1.15.0) (2025-07-21)
2
+
3
+ ### Features
4
+
5
+ - adding firebase production keys ([c64db9a](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/c64db9a6007725ae3bcc15ca3566c3c0b7516bf4))
6
+
7
+ # [1.14.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.13.0...v1.14.0) (2025-07-21)
8
+
9
+ ### Features
10
+
11
+ - add tutor initial state page UI ([f9bc12e](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/f9bc12eb0ac2d5e0fc408795ac2a1837514d14ee))
12
+
1
13
  # [1.13.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.12.0...v1.13.0) (2025-07-21)
2
14
 
3
15
  ### Features
@@ -19,12 +19,12 @@ API_HOTMART_TUTOR=https://api-smart-search-content.cp.hotmart.com
19
19
  OPTIMIZELY_SDK_KEY=GQb1vccBnQhjtEkXqAonY
20
20
 
21
21
  # FIREBASE
22
- FIREBASE_API_KEY=AIzaSyAq3KtkkTsJC5maHM_A9YJ-roI2IbvI5fM
23
- FIREBASE_AUTH_DOMAIN=omnichat-cad8c.firebaseapp.com
24
- FIREBASE_DATABASE_URL=https://omnichat-cad8c.firebaseio.com
25
- FIREBASE_MESSAGING_SENDER_ID=78513709264
26
- FIREBASE_PROJECT_ID=omnichat-cad8c
27
- FIREBASE_STORAGE_BUCKET=omnichat-cad8c.appspot.com
22
+ FIREBASE_API_KEY=AIzaSyClTc2hCt5yODPBqNlxZtWrsI2fsGpYrmQ
23
+ FIREBASE_AUTH_DOMAIN=omnichat-prod.firebaseapp.com
24
+ FIREBASE_DATABASE_URL=https://omnichat-prod.firebaseio.com
25
+ FIREBASE_PROJECT_ID=omnichat-prod
26
+ FIREBASE_STORAGE_BUCKET=omnichat-prod.appspot.com
27
+ FIREBASE_MESSAGING_SENDER_ID=87249988839
28
28
 
29
29
  # C3PO
30
30
  API_CONVERSATION_URL=https://c3po-api-conversations.vulcano.rocks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -77,6 +77,8 @@
77
77
  --ai-color-secondary: #6ba1f0;
78
78
  --ai-color-dark: #1a1c1f;
79
79
  --ai-color-chat-response: #1e1926;
80
+ --ai-color-gradient-primary: #44d0ff;
81
+ --ai-color-gradient-accent: #b48eff;
80
82
 
81
83
  /* Size */
82
84
  --hc-size-spacing-1: 0.25rem;
@@ -9,7 +9,8 @@ export type ButtonProps = PropsWithChildren<
9
9
 
10
10
  function Button({ children, className, variant = 'brand', ...props }: ButtonProps) {
11
11
  const defaultClasses =
12
- 'rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 text-base font-medium border border-transparent'
12
+ 'rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 text-base font-medium'
13
+ const defaultBorder = 'border border-transparent'
13
14
  const defaultPadding = 'px-4 py-2'
14
15
 
15
16
  switch (variant) {
@@ -18,11 +19,13 @@ function Button({ children, className, variant = 'brand', ...props }: ButtonProp
18
19
  <button
19
20
  className={clsx(
20
21
  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
+ 'group relative inline-flex items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-[var(--ai-color-gradient-primary)] to-[var(--ai-color-gradient-accent)] p-[1px] hover:text-neutral-0 group-hover:from-[var(--ai-color-gradient-primary)] group-hover:to-[var(--ai-color-gradient-accent)]',
22
23
  className
23
24
  )}
24
25
  {...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
+ <span
27
+ data-label='text-content'
28
+ className='relative flex-1 rounded-lg bg-neutral-900 px-5 py-2.5 text-neutral-0 transition-all duration-75 ease-in group-hover:bg-transparent'>
26
29
  {children}
27
30
  </span>
28
31
  </button>
@@ -33,6 +36,7 @@ function Button({ children, className, variant = 'brand', ...props }: ButtonProp
33
36
  className={clsx(
34
37
  defaultPadding,
35
38
  defaultClasses,
39
+ defaultBorder,
36
40
  'bg-primary-500 text-neutral-0 hover:bg-primary-600 focus:outline-none',
37
41
  className
38
42
  )}
@@ -46,6 +50,7 @@ function Button({ children, className, variant = 'brand', ...props }: ButtonProp
46
50
  className={clsx(
47
51
  defaultPadding,
48
52
  defaultClasses,
53
+ defaultBorder,
49
54
  'border-neutral-100 bg-transparent text-neutral-100 hover:bg-neutral-100 hover:text-neutral-900',
50
55
  className
51
56
  )}
@@ -59,6 +64,7 @@ function Button({ children, className, variant = 'brand', ...props }: ButtonProp
59
64
  className={clsx(
60
65
  defaultPadding,
61
66
  defaultClasses,
67
+ defaultBorder,
62
68
  'bg-transparent text-neutral-0 hover:bg-neutral-900',
63
69
  className
64
70
  )}
@@ -73,6 +79,7 @@ function Button({ children, className, variant = 'brand', ...props }: ButtonProp
73
79
  className={clsx(
74
80
  defaultPadding,
75
81
  defaultClasses,
82
+ defaultBorder,
76
83
  'rounded bg-neutral-100 text-neutral-900 outline-none hover:bg-neutral-200',
77
84
  className
78
85
  )}
@@ -1,3 +1,5 @@
1
+ import { UAParser } from 'ua-parser-js'
2
+
1
3
  import { act, chance, renderHook, waitFor } from '@/src/config/tests'
2
4
  import { MessagesService } from '@/src/modules/messages'
3
5
  import { useGetProfile } from '@/src/modules/profile'
@@ -11,6 +13,8 @@ vi.mock('@/src/modules/profile', () => ({
11
13
  useGetProfile: vi.fn()
12
14
  }))
13
15
 
16
+ vi.mock('ua-parser-js', () => ({ UAParser: vi.fn() }))
17
+
14
18
  describe('useSendTextMessage', () => {
15
19
  const message = chance.animal()
16
20
  const getProfileMock = { data: { userId: chance.integer() } }
@@ -18,12 +22,15 @@ describe('useSendTextMessage', () => {
18
22
  .withClassHashId(chance.guid())
19
23
  .withOwner_id(chance.guid())
20
24
  .withCurrent_media_codes(chance.profession())
25
+ .withUser({ id: chance.guid(), ucode: chance.guid() })
26
+ const UAParserMock = { browser: { version: chance.android_id(), name: chance.name_prefix() } }
21
27
 
22
28
  const render = () => renderHook(useSendTextMessage)
23
29
 
24
30
  beforeEach(() => {
25
31
  vi.mocked(useGetProfile).mockReturnValue(getProfileMock as never)
26
32
  vi.spyOn(Store, 'useWidgetSettingsAtomValue').mockReturnValue(defaultSettings)
33
+ vi.mocked(UAParser).mockReturnValue(UAParserMock as never)
27
34
  })
28
35
 
29
36
  it('should throw when conversationId is not defined', async () => {
@@ -75,7 +82,10 @@ describe('useSendTextMessage', () => {
75
82
  owner_id: defaultSettings.owner_id,
76
83
  current_media_codes: defaultSettings.current_media_codes,
77
84
  question: text,
78
- router: text
85
+ router: text,
86
+ osVersion: UAParserMock.browser.version,
87
+ platformDetail: UAParserMock.browser.name,
88
+ ucode: defaultSettings.user?.ucode
79
89
  },
80
90
  externalId: expect.any(String),
81
91
  namespace: defaultSettings.namespace,
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from 'react'
2
2
  import { useMutation } from '@tanstack/react-query'
3
+ import { UAParser } from 'ua-parser-js'
3
4
  import { v4 } from 'uuid'
4
5
 
5
6
  import { MessagesService } from '@/src/modules/messages'
@@ -16,6 +17,9 @@ function useSendTextMessage() {
16
17
 
17
18
  return useMutation({
18
19
  mutationFn(message: string) {
20
+ const userAgentParser = UAParser(navigator.userAgent)
21
+ const browserInfo = userAgentParser.browser
22
+
19
23
  let processedMessage = message
20
24
 
21
25
  if (!settings?.conversationId) throw new Error('Conversation Id must be defined')
@@ -42,11 +46,16 @@ function useSendTextMessage() {
42
46
  clubName: settings.clubName,
43
47
  productName: settings.productName,
44
48
  productId: settings.productId,
49
+ productType: settings.productType,
50
+ classType: settings.classType,
45
51
  classHashId: settings.classHashId,
46
52
  owner_id: settings?.owner_id,
47
53
  current_media_codes: settings?.current_media_codes,
48
54
  question: questionParam,
49
- router: questionParam ? 'summary' : undefined
55
+ router: questionParam ? 'summary' : undefined,
56
+ osVersion: browserInfo.version,
57
+ platformDetail: browserInfo.name,
58
+ ucode: settings.user?.ucode
50
59
  },
51
60
  externalId: v4(),
52
61
  namespace: settings.namespace,
@@ -15,7 +15,7 @@ function WidgetContainer({ completeSetup = false }: { completeSetup?: boolean })
15
15
 
16
16
  useEffect(() => {
17
17
  if (completeSetup) {
18
- setTab('chat')
18
+ setTab('starter')
19
19
  }
20
20
  }, [completeSetup, setTab])
21
21
 
@@ -0,0 +1,41 @@
1
+ import { render, screen } from '@/src/config/tests'
2
+ import { useSendTextMessage } from '@/src/modules/messages/hooks'
3
+
4
+ import WidgetStarterPage from './starter-page'
5
+
6
+ vi.mock('@/src/modules/messages/hooks', () => ({ useSendTextMessage: vi.fn() }))
7
+
8
+ describe('WidgetStarterPage', () => {
9
+ const useSendTextMessageMock = { mutate: vi.fn() }
10
+
11
+ const renderComponent = () => render(<WidgetStarterPage />)
12
+
13
+ beforeEach(() => {
14
+ vi.mocked(useSendTextMessage).mockReturnValue(useSendTextMessageMock as never)
15
+ })
16
+
17
+ it('should render without errors', () => {
18
+ renderComponent()
19
+
20
+ expect(
21
+ screen.getByRole('button', { name: /starter_page.what_does_tutor_do/i })
22
+ ).toBeInTheDocument()
23
+ expect(screen.getByRole('button', { name: /starter_page.wanna_summary/i })).toBeInTheDocument()
24
+ })
25
+
26
+ it('should post the slider button text content to the backend', async () => {
27
+ const { user } = renderComponent()
28
+
29
+ const button = screen.getByRole('button', { name: /starter_page.what_does_tutor_do/i })
30
+
31
+ await user.click(button)
32
+
33
+ expect(useSendTextMessageMock.mutate).toHaveBeenNthCalledWith(
34
+ 1,
35
+ '🤖 starter_page.what_does_tutor_do',
36
+ {
37
+ onSuccess: expect.any(Function)
38
+ }
39
+ )
40
+ })
41
+ })
@@ -1,16 +1,25 @@
1
1
  import { useRef } from 'react'
2
+ import clsx from 'clsx'
3
+ import type { MouseEventHandler } from 'react'
4
+ import { useTranslation } from 'react-i18next'
2
5
 
6
+ import { Button } from '@/src/lib/components'
3
7
  import { useRefEventListener } from '@/src/lib/hooks'
4
8
  import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/components'
9
+ import { useSendTextMessage } from '@/src/modules/messages/hooks'
5
10
  import { useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
6
11
  import { GreetingsCard } from '../greetings-card'
7
12
  import { PageLayout } from '../page-layout'
8
13
 
14
+ import styles from './styles.module.css'
15
+
9
16
  function WidgetStarterPage() {
17
+ const { t } = useTranslation()
10
18
  const [settings] = useWidgetSettingsAtom()
11
19
  const chatInputRef = useRef<HTMLTextAreaElement>(null)
12
20
  const [, setChatInputValue] = useChatInputValueAtom()
13
21
  const [, setWidgetTabs] = useWidgetTabsAtom()
22
+ const sendTextMessageMutation = useSendTextMessage()
14
23
 
15
24
  useRefEventListener<HTMLTextAreaElement>({
16
25
  config: {
@@ -23,6 +32,20 @@ function WidgetStarterPage() {
23
32
  }
24
33
  })
25
34
 
35
+ const handleAskQuestion: MouseEventHandler<HTMLButtonElement> = (e) => {
36
+ const textContent = e?.currentTarget?.querySelector(
37
+ 'span[data-label=text-content]'
38
+ )?.textContent
39
+
40
+ if (!textContent) return
41
+
42
+ sendTextMessageMutation.mutate(textContent, {
43
+ onSuccess() {
44
+ setWidgetTabs('chat')
45
+ }
46
+ })
47
+ }
48
+
26
49
  return (
27
50
  <PageLayout
28
51
  asideChild={
@@ -32,8 +55,30 @@ function WidgetStarterPage() {
32
55
  onSend={() => setWidgetTabs('chat')}
33
56
  />
34
57
  }>
35
- <div className='flex flex-col justify-center px-5 py-4'>
36
- <GreetingsCard author={settings?.author ?? ''} tutorName={settings?.tutorName ?? ''} />
58
+ <div className='grid-areas-[a_b] grid h-full grid-cols-1 grid-rows-[1fr_auto]'>
59
+ <div
60
+ className={clsx(
61
+ 'grid-area-[a] flex min-h-0 flex-col justify-center px-5 py-4',
62
+ styles.bg
63
+ )}>
64
+ <GreetingsCard author={settings?.author ?? ''} tutorName={settings?.tutorName ?? ''} />
65
+ </div>
66
+ <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'>
67
+ <Button
68
+ variant='gradient-outline'
69
+ className='shrink-0 snap-end text-sm'
70
+ onClick={handleAskQuestion}>
71
+ <span>🤖 </span>
72
+ {t('starter_page.what_does_tutor_do')}
73
+ </Button>
74
+ <Button
75
+ variant='gradient-outline'
76
+ className='shrink-0 snap-end text-sm'
77
+ onClick={handleAskQuestion}>
78
+ <span>📝 </span>
79
+ {t('starter_page.wanna_summary')}
80
+ </Button>
81
+ </div>
37
82
  </div>
38
83
  </PageLayout>
39
84
  )
@@ -0,0 +1,3 @@
1
+ .bg {
2
+ composes: gradientBg from '../../../../config/styles/utilities/bg-utilities.module.css';
3
+ }
@@ -32,13 +32,21 @@ function useInitWidget(settings: WidgetSettingProps) {
32
32
  const [error, setError] = useState<unknown>(null)
33
33
 
34
34
  useEffect(() => {
35
+ let isMounted = true
36
+
35
37
  if (completeSetup) return
36
38
 
37
39
  init(settings)
38
40
  .then(() => {
39
- setCompleteSetup(true)
41
+ if (isMounted) setCompleteSetup(true)
42
+ })
43
+ .catch((e) => {
44
+ if (isMounted) setError(e)
40
45
  })
41
- .catch(setError)
46
+
47
+ return () => {
48
+ isMounted = false
49
+ }
42
50
  }, [completeSetup, settings])
43
51
 
44
52
  return { completeSetup, error }
package/src/types.ts CHANGED
@@ -16,6 +16,16 @@ export type User = {
16
16
  sessionId?: string
17
17
  }
18
18
 
19
+ export enum ClassTypes {
20
+ Html = 'HTML',
21
+ Media = 'MEDIA',
22
+ Advertisement = 'ADVERTISEMENT',
23
+ QuizExercise = 'QUIZ_EXERCISE',
24
+ QuizProfile = 'QUIZ_PROFILE',
25
+ Webinar = 'WEBINAR',
26
+ Other = 'OTHER'
27
+ }
28
+
19
29
  export type WidgetSettingProps = {
20
30
  hotmartToken: string
21
31
  locale: ILanguages
@@ -35,6 +45,8 @@ export type WidgetSettingProps = {
35
45
  classHashId?: string
36
46
  owner_id?: string
37
47
  current_media_codes?: string
48
+ productType?: string
49
+ classType?: ClassTypes
38
50
  }
39
51
 
40
52
  export interface ICustomEvent<T = object> {
@@ -93,7 +93,9 @@ module.exports = {
93
93
  primary: 'var(--ai-color-primary)',
94
94
  secondary: 'var(--ai-color-secondary)',
95
95
  dark: 'var(--ai-color-dark)',
96
- 'chat-response': 'var(--ai-color-chat-response)'
96
+ 'chat-response': 'var(--ai-color-chat-response)',
97
+ 'gradient-primary': 'var(--ai-color-gradient-primary)',
98
+ 'gradient-accent': 'var(--ai-color-gradient-accent)'
97
99
  }
98
100
  }
99
101
  }