app-tutor-ai-consumer 1.53.0 → 1.55.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,19 @@
1
+ # [1.55.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.54.0...v1.55.0) (2026-01-30)
2
+
3
+ ### Features
4
+
5
+ - add concierge labels ([f845eb5](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/f845eb5a3016d582ae9687f569764b14bca00d15))
6
+
7
+ # [1.54.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.53.0...v1.54.0) (2026-01-28)
8
+
9
+ ### Bug Fixes
10
+
11
+ - code review issues ([6d0a17a](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/6d0a17a8874001b7d30d889152a432fbf5046467))
12
+
13
+ ### Features
14
+
15
+ - add create thread datahub event ([3f8fa1e](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/3f8fa1e5e98f9bef7201a7835a56252943a35122))
16
+
1
17
  # [1.53.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.52.1...v1.53.0) (2026-01-27)
2
18
 
3
19
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.53.0",
3
+ "version": "1.55.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -23,5 +23,6 @@ export const ActionNames = {
23
23
  CLOSE_TUTOR_ONBOARDING: `${DataHubActions.CLOSE}_tutor_onboarding`,
24
24
  VIEW_PRODUCT_TUTOR: `${DataHubActions.VIEW}_product_tutor`,
25
25
  VIEW_ANSWER_MESSAGE: `${DataHubActions.VIEW}_answer_message`,
26
- VIEW_CHAT: `${DataHubActions.VIEW}_chat`
26
+ VIEW_CHAT: `${DataHubActions.VIEW}_chat`,
27
+ TRY_CREATE_THREAD: `${DataHubActions.TRY}_create_thread`
27
28
  }
@@ -0,0 +1,60 @@
1
+ import HttpCodes from '@/src/lib/utils/http-codes'
2
+ import { ActionNames } from '../../actions'
3
+ import { DataHubEntities } from '../../entities'
4
+ import type { ActionNamesType, DataHubEntityTypes } from '../../types'
5
+ import BaseTrySchema from '../base-try-schema'
6
+ import { ProductCategories } from '../constants'
7
+
8
+ type Result = 'SUCCESSFUL' | 'FAILURE'
9
+
10
+ type ConstructorParams = {
11
+ title: string
12
+ componentName: string
13
+ conversationId: string
14
+ entity?: DataHubEntityTypes
15
+ result?: Result
16
+ statusCode?: number
17
+ failureDescription?: string
18
+ }
19
+
20
+ export class TryCreateThreadSchema extends BaseTrySchema {
21
+ action: ActionNamesType
22
+
23
+ private agent: 'HOTMART_TUTOR' | 'PRODUCT_AGENT'
24
+ private conversationId: string
25
+ private title: string
26
+
27
+ constructor({
28
+ title,
29
+ componentName,
30
+ conversationId,
31
+ entity = DataHubEntities.CONVERSATIONAL_AGENT,
32
+ result = 'SUCCESSFUL',
33
+ statusCode = HttpCodes.OK,
34
+ failureDescription = 'NOT_FAILURE_RESULT_EVENT'
35
+ }: ConstructorParams) {
36
+ super({
37
+ result,
38
+ componentName,
39
+ statusCode,
40
+ failureDescription,
41
+ componentSource: 'CONTENT_LESSON',
42
+ productType: ProductCategories.ConversationalAgent
43
+ })
44
+
45
+ this.entity = entity
46
+ this.action = ActionNames.TRY_CREATE_THREAD
47
+ this.agent = 'PRODUCT_AGENT'
48
+ this.conversationId = conversationId
49
+ this.title = title
50
+ }
51
+
52
+ getDataHubEventData(): Record<string, unknown> {
53
+ return {
54
+ ...super.prepare(),
55
+ agent: this.agent,
56
+ conversationId: this.conversationId,
57
+ title: this.title
58
+ }
59
+ }
60
+ }
@@ -4,6 +4,7 @@ import { DataHubEntities } from '../entities'
4
4
  import type { DataHubEntityTypes, ResultType, ScreenNamesType } from '../types'
5
5
 
6
6
  import BaseProductSchema from './base-product-schema'
7
+ import type { ProductCategoriesType } from './types'
7
8
 
8
9
  export type BaseTrySchemaConstructorArgs = {
9
10
  componentName: string
@@ -14,6 +15,7 @@ export type BaseTrySchemaConstructorArgs = {
14
15
  statusCode?: number
15
16
  failureDescription?: string
16
17
  screenName?: ScreenNamesType
18
+ productType?: ProductCategoriesType
17
19
  }
18
20
 
19
21
  abstract class BaseTrySchema extends BaseProductSchema {
@@ -31,6 +33,7 @@ abstract class BaseTrySchema extends BaseProductSchema {
31
33
  const {
32
34
  componentName,
33
35
  componentSource,
36
+ productType,
34
37
  entity = DataHubEntities.HOME,
35
38
  failureDescription = Result.FAILURE_DESCRIPTION,
36
39
  isLogged = true,
@@ -39,7 +42,7 @@ abstract class BaseTrySchema extends BaseProductSchema {
39
42
  screenName = ScreenNames.HOME_CONSUMER
40
43
  } = args
41
44
 
42
- super()
45
+ super({ productType })
43
46
 
44
47
  this.componentName = componentName
45
48
  this.componentSource = componentSource
@@ -11,6 +11,7 @@ import type { WidgetTabsProps } from '@/src/modules/widget'
11
11
  import {
12
12
  widgetSettingsAtom,
13
13
  widgetSettingsConfigAgentParentAtom,
14
+ widgetSettingsConfigConciergeParentAtom,
14
15
  widgetTabsAtom
15
16
  } from '@/src/modules/widget'
16
17
  import type { WidgetSettingProps } from '@/src/types'
@@ -50,12 +51,18 @@ const useInitialStore = ({ settings }: UseInitialStoreProps) => {
50
51
  [settings?.config?.metadata?.parent]
51
52
  )
52
53
 
54
+ const isConciergeMode = useMemo(
55
+ () => settings?.config?.metadata?.parent === 'CONCIERGE',
56
+ [settings?.config?.metadata?.parent]
57
+ )
58
+
53
59
  useHydrateAtoms(
54
60
  [
55
61
  [widgetSettingsAtom, getDefaultSettings(settings)],
56
62
  [messagesMaxCountAtom, MSG_MAX_COUNT],
57
63
  [widgetTabsAtom, initialTab],
58
- [widgetSettingsConfigAgentParentAtom, isAgentMode]
64
+ [widgetSettingsConfigAgentParentAtom, isAgentMode],
65
+ [widgetSettingsConfigConciergeParentAtom, isConciergeMode]
59
66
  ],
60
67
  { store }
61
68
  )
@@ -1,6 +1,9 @@
1
+ import { DataHubService } from '@/src/config/datahub'
2
+ import { TryCreateThreadSchema } from '@/src/config/datahub/schemas/agent/tryCreateThreadSchema'
1
3
  import { renderHook } from '@/src/config/tests'
4
+ import HttpCodes from '@/src/lib/utils/http-codes'
2
5
  import { useUpdateConversationTitle } from '@/src/modules/conversation/hooks/update-conversation-title'
3
- import { useWidgetSettingsAtomValue } from '@/src/modules/widget/store'
6
+ import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '@/src/modules/widget/store'
4
7
  import { useMessagesCountAtomValue } from '../../store/messages-count.atom'
5
8
  import { useSendTextMessage } from '../use-send-text-message'
6
9
 
@@ -11,7 +14,8 @@ vi.mock('@/src/modules/conversation/hooks/update-conversation-title', () => ({
11
14
  }))
12
15
 
13
16
  vi.mock('@/src/modules/widget/store', () => ({
14
- useWidgetSettingsAtomValue: vi.fn()
17
+ useWidgetSettingsAtomValue: vi.fn(),
18
+ useIsAgentParentAtomValue: vi.fn()
15
19
  }))
16
20
 
17
21
  vi.mock('../../store/messages-count.atom', () => ({
@@ -25,6 +29,19 @@ vi.mock('../use-send-text-message', () => ({
25
29
  describe('useSendFirstMessage', () => {
26
30
  const mockSendMessage = vi.fn()
27
31
  const mockUpdateTitle = vi.fn()
32
+ const mockMessage = { id: 'msg-1', content: 'Hello' }
33
+ const mockSettings = {
34
+ conversationId: 'conv-123',
35
+ productId: 'prod-456',
36
+ config: {
37
+ metadata: {
38
+ agentProductId: 'agent-prod-42',
39
+ agentName: 'Agent Name',
40
+ courseName: 'Course Name',
41
+ source: 'Source'
42
+ }
43
+ }
44
+ }
28
45
 
29
46
  beforeEach(() => {
30
47
  vi.clearAllMocks()
@@ -47,35 +64,55 @@ describe('useSendFirstMessage', () => {
47
64
  } as never)
48
65
 
49
66
  vi.mocked(useMessagesCountAtomValue).mockReturnValue(0)
67
+
68
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValue(true)
50
69
  })
51
70
 
52
71
  describe('when sending first message', () => {
53
- it('should send message and update conversation title', async () => {
54
- vi.mocked(useWidgetSettingsAtomValue).mockReturnValue({
55
- conversationId: 'conv-123',
56
- productId: 'prod-456',
57
- config: {
58
- metadata: {
59
- agentProductId: 'agent-prod-42',
60
- agentName: 'Agent Name',
61
- courseName: 'Course Name',
62
- source: 'Source'
63
- }
64
- }
65
- } as never)
66
- const mockMessage = { id: 'msg-1', content: 'Hello' }
72
+ it('should not dispatch create thread event when in Tutor Mode', async () => {
73
+ vi.spyOn(DataHubService, 'sendEvent').mockImplementationOnce(() => null)
74
+ vi.mocked(useIsAgentParentAtomValue).mockReturnValue(false)
75
+ vi.mocked(useWidgetSettingsAtomValue).mockReturnValue(mockSettings as never)
67
76
  mockSendMessage.mockResolvedValue(mockMessage)
68
77
  mockUpdateTitle.mockResolvedValue({})
69
78
 
79
+ const expectedResult = {
80
+ conversationId: 'conv-123',
81
+ productId: 'agent-prod-42',
82
+ subject: 'Hello world'
83
+ }
84
+
70
85
  const { result } = renderHook(() => useSendFirstMessage())
71
86
 
72
- await result.current.sendFirstMessage('Hello world')
87
+ await result.current.sendFirstMessage(expectedResult.subject)
73
88
 
74
- expect(mockSendMessage).toHaveBeenCalledWith('Hello world', undefined)
75
- expect(mockUpdateTitle).toHaveBeenCalledWith({
89
+ expect(DataHubService.sendEvent).not.toHaveBeenCalled()
90
+ })
91
+
92
+ it('should send message and update conversation title', async () => {
93
+ vi.spyOn(DataHubService, 'sendEvent').mockImplementation(() => null)
94
+ vi.mocked(useWidgetSettingsAtomValue).mockReturnValue(mockSettings as never)
95
+ mockSendMessage.mockResolvedValue(mockMessage)
96
+ mockUpdateTitle.mockResolvedValue({})
97
+
98
+ const expectedResult = {
76
99
  conversationId: 'conv-123',
77
100
  productId: 'agent-prod-42',
78
101
  subject: 'Hello world'
102
+ }
103
+
104
+ const { result } = renderHook(() => useSendFirstMessage())
105
+
106
+ await result.current.sendFirstMessage(expectedResult.subject)
107
+
108
+ expect(mockSendMessage).toHaveBeenCalledWith(expectedResult.subject, undefined)
109
+ expect(mockUpdateTitle).toHaveBeenCalledWith(expectedResult)
110
+ expect(DataHubService.sendEvent).toHaveBeenCalledExactlyOnceWith({
111
+ schema: new TryCreateThreadSchema({
112
+ title: expectedResult.subject,
113
+ componentName: 'AGENT_CREATE_THREAD',
114
+ conversationId: expectedResult.conversationId
115
+ })
79
116
  })
80
117
  })
81
118
  })
@@ -112,6 +149,7 @@ describe('useSendFirstMessage', () => {
112
149
 
113
150
  describe('error handling', () => {
114
151
  it('should throw error when send message fails', async () => {
152
+ vi.mocked(useWidgetSettingsAtomValue).mockReturnValue(mockSettings as never)
115
153
  vi.spyOn(console, 'error').mockImplementationOnce(() => {})
116
154
  const error = new Error('Send failed')
117
155
  mockSendMessage.mockRejectedValue(error)
@@ -119,6 +157,17 @@ describe('useSendFirstMessage', () => {
119
157
  const { result } = renderHook(() => useSendFirstMessage())
120
158
 
121
159
  await expect(result.current.sendFirstMessage('Hello')).rejects.toThrow('Send failed')
160
+
161
+ expect(DataHubService.sendEvent).toHaveBeenCalledExactlyOnceWith({
162
+ schema: new TryCreateThreadSchema({
163
+ title: mockMessage.content,
164
+ componentName: 'AGENT_CREATE_THREAD',
165
+ conversationId: mockSettings.conversationId,
166
+ result: 'FAILURE',
167
+ statusCode: HttpCodes.BAD_REQUEST,
168
+ failureDescription: 'Send failed'
169
+ })
170
+ })
122
171
  })
123
172
  })
124
173
 
@@ -2,8 +2,11 @@ import { useCallback } from 'react'
2
2
  import type { Message } from '@hotmart-org-ca/sparkie/dist/MessageService'
3
3
  import type { MutateOptions } from '@tanstack/react-query'
4
4
 
5
+ import { DataHubService } from '@/src/config/datahub'
6
+ import { TryCreateThreadSchema } from '@/src/config/datahub/schemas/agent/tryCreateThreadSchema'
7
+ import HttpCodes from '@/src/lib/utils/http-codes'
5
8
  import { useUpdateConversationTitle } from '@/src/modules/conversation/hooks/update-conversation-title'
6
- import { useWidgetSettingsAtomValue } from '@/src/modules/widget/store'
9
+ import { useIsAgentParentAtomValue, useWidgetSettingsAtomValue } from '@/src/modules/widget/store'
7
10
  import { useMessagesCountAtomValue } from '../../store/messages-count.atom'
8
11
  import { excerptMessage } from '../../utils'
9
12
  import { useSendTextMessage } from '../use-send-text-message'
@@ -13,10 +16,12 @@ function useSendFirstMessage() {
13
16
  const sendMessageMutation = useSendTextMessage()
14
17
  const updateTitleMutation = useUpdateConversationTitle()
15
18
  const settings = useWidgetSettingsAtomValue()
19
+ const isAgent = useIsAgentParentAtomValue()
16
20
 
17
21
  const sendFirstMessage = useCallback(
18
22
  async (message: string, options?: MutateOptions<Message, Error, string, void>) => {
19
23
  const isFirstMessage = messagesCount === 0
24
+ const subject = excerptMessage({ message })
20
25
 
21
26
  try {
22
27
  const messageResult = await sendMessageMutation.mutateAsync(message, options)
@@ -29,17 +34,46 @@ function useSendFirstMessage() {
29
34
  await updateTitleMutation.mutateAsync({
30
35
  conversationId: settings.conversationId,
31
36
  productId: settings?.config?.metadata?.agentProductId,
32
- subject: excerptMessage({ message })
37
+ subject
38
+ })
39
+ }
40
+
41
+ if (isAgent) {
42
+ DataHubService.sendEvent({
43
+ schema: new TryCreateThreadSchema({
44
+ title: subject,
45
+ componentName: 'AGENT_CREATE_THREAD',
46
+ conversationId: settings?.conversationId || ''
47
+ })
33
48
  })
34
49
  }
35
50
 
36
51
  return messageResult
37
52
  } catch (error) {
38
- console.error('Failed to send first message:', error)
53
+ if (isAgent) {
54
+ DataHubService.sendEvent({
55
+ schema: new TryCreateThreadSchema({
56
+ title: subject,
57
+ componentName: 'AGENT_CREATE_THREAD',
58
+ conversationId: settings?.conversationId || '',
59
+ result: 'FAILURE',
60
+ statusCode: HttpCodes.BAD_REQUEST,
61
+ failureDescription: error instanceof Error ? error?.message : String(error)
62
+ })
63
+ })
64
+ }
65
+
39
66
  throw error
40
67
  }
41
68
  },
42
- [messagesCount, sendMessageMutation, updateTitleMutation, settings]
69
+ [
70
+ messagesCount,
71
+ sendMessageMutation,
72
+ settings?.conversationId,
73
+ settings?.config?.metadata?.agentProductId,
74
+ isAgent,
75
+ updateTitleMutation
76
+ ]
43
77
  )
44
78
 
45
79
  return {
@@ -3,7 +3,9 @@ import { useTranslation } from 'react-i18next'
3
3
 
4
4
  import { AiIconCircle } from '@/src/lib/components'
5
5
  import { useMembershipColor } from '../../hooks'
6
- import { useIsAgentParentAtomValue } from '../../store'
6
+ import { useIsAgentParentAtomValue, useIsConciergeParentAtomValue } from '../../store'
7
+
8
+ import { getGreetingsLabels } from './utils'
7
9
 
8
10
  export type GreetingsCardProps = {
9
11
  tutorName: string
@@ -13,8 +15,12 @@ export type GreetingsCardProps = {
13
15
 
14
16
  function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCardProps) {
15
17
  const { t } = useTranslation()
16
- const isAgentMode = useIsAgentParentAtomValue()
17
18
  const membershipColor = useMembershipColor()
19
+ const isAgentMode = useIsAgentParentAtomValue()
20
+ const isConciergeMode = useIsConciergeParentAtomValue()
21
+
22
+ const chatMode = isConciergeMode ? 'CONCIERGE' : isAgentMode ? 'AGENT' : 'TUTOR'
23
+ const labels = getGreetingsLabels(chatMode)
18
24
 
19
25
  return (
20
26
  <div className='flex flex-col items-center justify-center'>
@@ -28,19 +34,14 @@ function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCard
28
34
  'text-white': isDarkTheme,
29
35
  'text-gray-900': !isDarkTheme
30
36
  })}>
31
- {t('general.greetings.hello', { name: author })}
37
+ {t(labels.hello, { name: author })}
32
38
  </span>
33
39
  <h3
34
40
  className={clsx('text-xl font-bold', {
35
41
  'text-white': isDarkTheme,
36
42
  'text-gray-900': !isDarkTheme
37
43
  })}>
38
- {t(
39
- isAgentMode
40
- ? 'general.greetings.agentFirstMessage'
41
- : 'general.greetings.firstMessage',
42
- { tutorName }
43
- )}
44
+ {t(labels.firstMessage, { tutorName })}
44
45
  </h3>
45
46
  </div>
46
47
  <p
@@ -48,7 +49,7 @@ function GreetingsCard({ author, tutorName, isDarkTheme = false }: GreetingsCard
48
49
  'text-gray-400': isDarkTheme,
49
50
  'text-neutral-600': !isDarkTheme
50
51
  })}>
51
- {t(isAgentMode ? 'general.greetings.agentDescription' : 'general.greetings.description')}
52
+ {t(labels.description)}
52
53
  </p>
53
54
  </div>
54
55
  </div>
@@ -0,0 +1,7 @@
1
+ export type WidgetMode = 'TUTOR' | 'AGENT' | 'CONCIERGE'
2
+
3
+ export type GreetingsLabels = {
4
+ hello: string
5
+ firstMessage: string
6
+ description: string
7
+ }
@@ -0,0 +1,25 @@
1
+ import type { GreetingsLabels, WidgetMode } from './types'
2
+
3
+ export const getGreetingsLabels = (mode: WidgetMode, isFirstTime = false): GreetingsLabels => {
4
+ const interaction = isFirstTime ? 'first_interaction' : 'other_interactions'
5
+
6
+ const greetingsMap = {
7
+ AGENT: {
8
+ hello: 'general.greetings.hello',
9
+ firstMessage: 'general.greetings.agentFirstMessage',
10
+ description: 'general.greetings.agentDescription'
11
+ },
12
+ CONCIERGE: {
13
+ hello: `concierge.welcome.${interaction}.hello`,
14
+ firstMessage: `concierge.welcome.${interaction}.title`,
15
+ description: 'concierge.welcome.description'
16
+ },
17
+ TUTOR: {
18
+ hello: 'general.greetings.hello',
19
+ firstMessage: 'general.greetings.firstMessage',
20
+ description: 'general.greetings.description'
21
+ }
22
+ }
23
+
24
+ return greetingsMap[mode]
25
+ }
@@ -3,3 +3,8 @@ import { atom, useAtomValue } from 'jotai'
3
3
  export const widgetSettingsConfigAgentParentAtom = atom(false)
4
4
 
5
5
  export const useIsAgentParentAtomValue = () => useAtomValue(widgetSettingsConfigAgentParentAtom)
6
+
7
+ export const widgetSettingsConfigConciergeParentAtom = atom(false)
8
+
9
+ export const useIsConciergeParentAtomValue = () =>
10
+ useAtomValue(widgetSettingsConfigConciergeParentAtom)
package/src/types.ts CHANGED
@@ -65,7 +65,7 @@ export type WidgetSettingProps = {
65
65
  config?: {
66
66
  theme?: Theme
67
67
  metadata?: {
68
- parent?: 'AGENT' | 'TUTOR'
68
+ parent?: 'AGENT' | 'TUTOR' | 'CONCIERGE'
69
69
  agentProductId?: number
70
70
  agentName?: string
71
71
  courseName?: string