app-tutor-ai-consumer 1.47.0 → 1.51.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 +36 -0
- package/package.json +10 -10
- package/src/config/tests/handlers.ts +11 -1
- package/src/modules/conversation/constants.ts +6 -0
- package/src/modules/conversation/events.ts +27 -0
- package/src/modules/conversation/hooks/update-conversation-title/index.ts +2 -0
- package/src/modules/conversation/hooks/update-conversation-title/types.ts +5 -0
- package/src/modules/conversation/hooks/update-conversation-title/update-conversation-title.spec.tsx +34 -0
- package/src/modules/conversation/hooks/update-conversation-title/update-conversation-title.tsx +18 -0
- package/src/modules/conversation/index.ts +1 -0
- package/src/modules/conversation/service.ts +20 -0
- package/src/modules/conversation/types.ts +5 -0
- package/src/modules/messages/components/messages-container/messages-container.tsx +12 -10
- package/src/modules/messages/components/messages-list/messages-list-empty-state.tsx +12 -0
- package/src/modules/messages/components/messages-list/messages-list.tsx +12 -1
- package/src/modules/messages/constants.ts +1 -0
- package/src/modules/messages/hooks/use-send-first-message/index.ts +1 -0
- package/src/modules/messages/hooks/use-send-first-message/use-send-first-message.spec.tsx +150 -0
- package/src/modules/messages/hooks/use-send-first-message/use-send-first-message.tsx +52 -0
- package/src/modules/messages/store/messages-count.atom.ts +12 -0
- package/src/modules/messages/utils/excerpt-message/excerpt-message.spec.ts +77 -0
- package/src/modules/messages/utils/excerpt-message/excerpt-message.ts +11 -0
- package/src/modules/messages/utils/excerpt-message/index.ts +1 -0
- package/src/modules/messages/utils/index.ts +1 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +4 -3
- package/src/modules/widget/components/header/widget-header.tsx +4 -1
- package/src/modules/widget/store/widget-settings.atom.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
# [1.51.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.50.0...v1.51.0) (2026-01-13)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- update some dependencies ([034b34d](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/034b34d3e17ed832ef2653b2aa0a7e8dac654ff6))
|
|
6
|
+
|
|
7
|
+
# [1.50.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.49.1...v1.50.0) (2026-01-07)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- update lock ([dd908e7](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/dd908e72794afcaca83ca92452533bb1ba6ac4a0))
|
|
12
|
+
|
|
13
|
+
## [1.49.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.49.0...v1.49.1) (2026-01-07)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- eslint ([8c4d8f6](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/8c4d8f6572de29bbd43c90a86fd0de6798f054a4))
|
|
18
|
+
|
|
19
|
+
# [1.49.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.48.1...v1.49.0) (2026-01-07)
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
- update lock ([7fb3166](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/7fb31662317f0d5ec06a5fe201ce4482fa77d6ab))
|
|
24
|
+
|
|
25
|
+
## [1.48.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.48.0...v1.48.1) (2026-01-06)
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
- productId ([66d4a7d](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/66d4a7d3cd60e7f826e32f64be45b061334f7844))
|
|
30
|
+
|
|
31
|
+
# [1.48.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.47.0...v1.48.0) (2026-01-05)
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
|
|
35
|
+
- add new conversation handling ([e075045](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/e075045f8f0aa5833c2b11d0cd4ef1ff4bef5bfd))
|
|
36
|
+
|
|
1
37
|
# [1.47.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.46.2...v1.47.0) (2026-01-02)
|
|
2
38
|
|
|
3
39
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-tutor-ai-consumer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.51.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@eslint/js": "~9.28.0",
|
|
40
40
|
"@hotmart/api-languages-cli": "~2.0.5",
|
|
41
41
|
"@rsbuild/plugin-svgr": "~1.2.0",
|
|
42
|
-
"@rsdoctor/rspack-plugin": "~1.
|
|
42
|
+
"@rsdoctor/rspack-plugin": "~1.4.0",
|
|
43
43
|
"@rspack/cli": "~1.3.15",
|
|
44
44
|
"@rspack/core": "~1.3.15",
|
|
45
45
|
"@rspack/plugin-react-refresh": "~1.4.3",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"@types/chance": "~1.1.6",
|
|
60
60
|
"@types/linkify-it": "~5.0.0",
|
|
61
61
|
"@types/node": "~24.0.0",
|
|
62
|
-
"@types/react": "~19.
|
|
63
|
-
"@types/react-dom": "~19.
|
|
62
|
+
"@types/react": "~19.2.8",
|
|
63
|
+
"@types/react-dom": "~19.2.3",
|
|
64
64
|
"@types/react-router-dom": "~5.3.3",
|
|
65
65
|
"@types/ua-parser-js": "~0.7.39",
|
|
66
66
|
"@types/uuid": "~10.0.0",
|
|
@@ -73,9 +73,9 @@
|
|
|
73
73
|
"compression-webpack-plugin": "~11.1.0",
|
|
74
74
|
"css-loader": "~7.1.2",
|
|
75
75
|
"dotenv": "~16.5.0",
|
|
76
|
-
"eslint": "~9.
|
|
77
|
-
"eslint-config-prettier": "~10.1.
|
|
78
|
-
"eslint-plugin-prettier": "~5.4
|
|
76
|
+
"eslint": "~9.39.2",
|
|
77
|
+
"eslint-config-prettier": "~10.1.8",
|
|
78
|
+
"eslint-plugin-prettier": "~5.5.4",
|
|
79
79
|
"eslint-plugin-react": "~7.37.5",
|
|
80
80
|
"eslint-plugin-react-hooks": "~5.2.0",
|
|
81
81
|
"eslint-plugin-simple-import-sort": "~12.1.1",
|
|
@@ -83,13 +83,13 @@
|
|
|
83
83
|
"globals": "~16.2.0",
|
|
84
84
|
"husky": "~9.1.7",
|
|
85
85
|
"jsdom": "~26.1.0",
|
|
86
|
-
"lint-staged": "~16.
|
|
86
|
+
"lint-staged": "~16.2.7",
|
|
87
87
|
"loader": "~2.1.1",
|
|
88
88
|
"msw": "~2.10.2",
|
|
89
|
-
"postcss": "~8.5.
|
|
89
|
+
"postcss": "~8.5.6",
|
|
90
90
|
"postcss-import": "~16.1.0",
|
|
91
91
|
"postcss-loader": "~8.1.1",
|
|
92
|
-
"prettier": "~3.
|
|
92
|
+
"prettier": "~3.7.4",
|
|
93
93
|
"prettier-plugin-tailwindcss": "~0.6.12",
|
|
94
94
|
"process": "~0.11.10",
|
|
95
95
|
"react-refresh": "~0.17.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { http, HttpResponse } from 'msw'
|
|
2
2
|
|
|
3
|
+
import { ConversationEndpoints } from '@/src/modules/conversation/constants'
|
|
3
4
|
import { MessagesEndpoints, MSG_MAX_COUNT } from '@/src/modules/messages'
|
|
4
5
|
import SignedFilesUrlsResponseBuilder from '@/src/modules/messages/__tests__/files-signed-urls.builder'
|
|
5
6
|
import IMessageWithSenderDataMock from '@/src/modules/messages/__tests__/imessage-with-sender-data.mock'
|
|
@@ -28,5 +29,14 @@ export const handlers = [
|
|
|
28
29
|
return HttpResponse.json(
|
|
29
30
|
new IMessageWithSenderDataMock().getMany(isNaN(limit) ? MSG_MAX_COUNT : limit)
|
|
30
31
|
)
|
|
31
|
-
})
|
|
32
|
+
}),
|
|
33
|
+
http.patch(
|
|
34
|
+
ConversationEndpoints.updateTitle({
|
|
35
|
+
productId: ':productId' as unknown as number,
|
|
36
|
+
conversationId: ':conversationId'
|
|
37
|
+
}),
|
|
38
|
+
() => {
|
|
39
|
+
return HttpResponse.json({ ok: true })
|
|
40
|
+
}
|
|
41
|
+
)
|
|
32
42
|
]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ConversationUpdateRequestProps } from './types'
|
|
2
|
+
|
|
3
|
+
export const ConversationEndpoints = {
|
|
4
|
+
updateTitle: ({ productId, conversationId }: Omit<ConversationUpdateRequestProps, 'title'>) =>
|
|
5
|
+
`${process.env.API_HOTMART_TUTOR}/api/v1/chat/product/${productId}/buyer/chat/${conversationId}`
|
|
6
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ICustomEvent } from '@/src/types'
|
|
2
|
+
|
|
3
|
+
import type { UpdateConversationTitleParams } from './hooks/update-conversation-title'
|
|
4
|
+
|
|
5
|
+
export const ConversationEventTypes = {
|
|
6
|
+
UPDATE_CONVERSATION_TITLE: 'tutor-app-widget:update-conversation-title'
|
|
7
|
+
} as const
|
|
8
|
+
|
|
9
|
+
export const ConversationEvents = {
|
|
10
|
+
[ConversationEventTypes.UPDATE_CONVERSATION_TITLE]: {
|
|
11
|
+
name: ConversationEventTypes.UPDATE_CONVERSATION_TITLE,
|
|
12
|
+
handler(callback) {
|
|
13
|
+
window.addEventListener(ConversationEventTypes.UPDATE_CONVERSATION_TITLE, callback)
|
|
14
|
+
|
|
15
|
+
return () => {
|
|
16
|
+
window.removeEventListener(ConversationEventTypes.UPDATE_CONVERSATION_TITLE, callback)
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
dispatch(payload: UpdateConversationTitleParams) {
|
|
20
|
+
window.dispatchEvent(
|
|
21
|
+
new CustomEvent(ConversationEventTypes.UPDATE_CONVERSATION_TITLE, {
|
|
22
|
+
detail: payload
|
|
23
|
+
})
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
} as ICustomEvent<typeof ConversationEventTypes>
|
|
27
|
+
} as const
|
package/src/modules/conversation/hooks/update-conversation-title/update-conversation-title.spec.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { chance, renderHook, waitFor } from '@/src/config/tests'
|
|
2
|
+
import { ConversationEvents, ConversationEventTypes } from '../../events'
|
|
3
|
+
|
|
4
|
+
import useUpdateConversationTitle from './update-conversation-title'
|
|
5
|
+
|
|
6
|
+
describe('useUpdateConversationTitle', () => {
|
|
7
|
+
const payload = {
|
|
8
|
+
conversationId: chance.guid({ version: 4 }),
|
|
9
|
+
productId: chance.integer({ min: 1, max: 100 }),
|
|
10
|
+
subject: chance.sentence({ words: 5 })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const customRenderHook = () => renderHook(useUpdateConversationTitle)
|
|
14
|
+
|
|
15
|
+
it('should update the conversation title without errors', () => {
|
|
16
|
+
const { result } = customRenderHook()
|
|
17
|
+
|
|
18
|
+
expect(result.current).toBeDefined()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should dispatch event when update is done successfully', async () => {
|
|
22
|
+
const evt = ConversationEvents[ConversationEventTypes.UPDATE_CONVERSATION_TITLE]
|
|
23
|
+
|
|
24
|
+
vi.spyOn(evt, 'dispatch')
|
|
25
|
+
|
|
26
|
+
const { result } = customRenderHook()
|
|
27
|
+
|
|
28
|
+
await waitFor(async () => {
|
|
29
|
+
await result.current.mutateAsync(payload)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(evt.dispatch).toHaveBeenCalledExactlyOnceWith(payload)
|
|
33
|
+
})
|
|
34
|
+
})
|
package/src/modules/conversation/hooks/update-conversation-title/update-conversation-title.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
import { ConversationService } from '../..'
|
|
4
|
+
import { ConversationEvents, ConversationEventTypes } from '../../events'
|
|
5
|
+
|
|
6
|
+
import type { UpdateConversationTitleParams } from './types'
|
|
7
|
+
|
|
8
|
+
function useUpdateConversationTitle() {
|
|
9
|
+
return useMutation({
|
|
10
|
+
mutationFn: async ({ conversationId, productId, subject }: UpdateConversationTitleParams) =>
|
|
11
|
+
ConversationService.updateTitle({ conversationId, productId, title: subject }),
|
|
12
|
+
onSuccess: (_, variables) => {
|
|
13
|
+
ConversationEvents[ConversationEventTypes.UPDATE_CONVERSATION_TITLE].dispatch(variables)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default useUpdateConversationTitle
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ConversationService } from './service'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { api } from '@/src/config/request'
|
|
2
|
+
|
|
3
|
+
import { ConversationEndpoints } from './constants'
|
|
4
|
+
import type { ConversationUpdateRequestProps } from './types'
|
|
5
|
+
|
|
6
|
+
class ConversationService {
|
|
7
|
+
async updateTitle({ productId, conversationId, title }: ConversationUpdateRequestProps) {
|
|
8
|
+
const { data } = await api.patch<object>(
|
|
9
|
+
ConversationEndpoints.updateTitle({
|
|
10
|
+
productId,
|
|
11
|
+
conversationId
|
|
12
|
+
}),
|
|
13
|
+
{ subject: title }
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
return data
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default new ConversationService()
|
|
@@ -64,16 +64,18 @@ const MessagesContainer = forwardRef<HTMLDivElement, MessagesContainerProps>(
|
|
|
64
64
|
<div
|
|
65
65
|
ref={scrollerRef}
|
|
66
66
|
className='flex h-full flex-col gap-2 overflow-auto max-md:p-[1.125rem] md:p-5'>
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
{showButton && (
|
|
68
|
+
<div className='mb-auto flex-1 self-center'>
|
|
69
|
+
<Button
|
|
70
|
+
className='max-w-max rounded-full border border-neutral-300 bg-neutral-200 px-2 py-1 text-xs/normal tracking-wide text-neutral-900 hover:text-neutral-900 focus:text-neutral-900 active:text-neutral-900'
|
|
71
|
+
onClick={handleClickShowMore}
|
|
72
|
+
loading={loading}
|
|
73
|
+
show={showButton}>
|
|
74
|
+
<Icon name='arrow-up' className='h-4 w-3' aria-hidden />
|
|
75
|
+
<span className='text-nowrap'>{t('general.buttons.show_more')}</span>
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
77
79
|
{children}
|
|
78
80
|
|
|
79
81
|
{error?.show &&
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useIsAgentParentAtomValue } from '@/src/modules/widget'
|
|
2
|
+
import { WidgetStarterPageContent } from '@/src/modules/widget/components/starter-page/starter-page-content'
|
|
3
|
+
|
|
4
|
+
function MessagesListEmptyState() {
|
|
5
|
+
const isAgentMode = useIsAgentParentAtomValue()
|
|
6
|
+
|
|
7
|
+
if (!isAgentMode) return null
|
|
8
|
+
|
|
9
|
+
return <WidgetStarterPageContent />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default MessagesListEmptyState
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
1
3
|
import type { ParsedMessage } from '@/src/modules/messages'
|
|
2
4
|
import { MessageItem } from '@/src/modules/messages/components'
|
|
5
|
+
import { useMessagesCountAtom } from '../../store/messages-count.atom'
|
|
6
|
+
|
|
7
|
+
import MessagesListEmptyState from './messages-list-empty-state'
|
|
3
8
|
|
|
4
9
|
function MessagesList({ messagesMap }: { messagesMap: Map<string, ParsedMessage[]> }) {
|
|
5
|
-
|
|
10
|
+
const [, setMessagesCount] = useMessagesCountAtom()
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setMessagesCount(messagesMap?.size ?? 0)
|
|
14
|
+
}, [messagesMap, setMessagesCount])
|
|
15
|
+
|
|
16
|
+
if (!(messagesMap.size > 0)) return <MessagesListEmptyState />
|
|
6
17
|
|
|
7
18
|
return Array.from(messagesMap).map(([, messages], i) => (
|
|
8
19
|
<div key={i} className='flex flex-1 flex-col justify-center gap-6'>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as useSendFirstMessage } from './use-send-first-message'
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { renderHook } from '@/src/config/tests'
|
|
2
|
+
import { useUpdateConversationTitle } from '@/src/modules/conversation/hooks/update-conversation-title'
|
|
3
|
+
import { useWidgetSettingsAtomValue } from '@/src/modules/widget/store'
|
|
4
|
+
import { useMessagesCountAtomValue } from '../../store/messages-count.atom'
|
|
5
|
+
import { useSendTextMessage } from '../use-send-text-message'
|
|
6
|
+
|
|
7
|
+
import useSendFirstMessage from './use-send-first-message'
|
|
8
|
+
|
|
9
|
+
vi.mock('@/src/modules/conversation/hooks/update-conversation-title', () => ({
|
|
10
|
+
useUpdateConversationTitle: vi.fn()
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
vi.mock('@/src/modules/widget/store', () => ({
|
|
14
|
+
useWidgetSettingsAtomValue: vi.fn()
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
vi.mock('../../store/messages-count.atom', () => ({
|
|
18
|
+
useMessagesCountAtomValue: vi.fn()
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
vi.mock('../use-send-text-message', () => ({
|
|
22
|
+
useSendTextMessage: vi.fn()
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
describe('useSendFirstMessage', () => {
|
|
26
|
+
const mockSendMessage = vi.fn()
|
|
27
|
+
const mockUpdateTitle = vi.fn()
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks()
|
|
31
|
+
|
|
32
|
+
vi.mocked(useSendTextMessage).mockReturnValue({
|
|
33
|
+
mutateAsync: mockSendMessage,
|
|
34
|
+
isPending: false,
|
|
35
|
+
error: null
|
|
36
|
+
} as never)
|
|
37
|
+
|
|
38
|
+
vi.mocked(useUpdateConversationTitle).mockReturnValue({
|
|
39
|
+
mutateAsync: mockUpdateTitle,
|
|
40
|
+
isPending: false,
|
|
41
|
+
error: null
|
|
42
|
+
} as never)
|
|
43
|
+
|
|
44
|
+
vi.mocked(useWidgetSettingsAtomValue).mockReturnValue({
|
|
45
|
+
conversationId: null,
|
|
46
|
+
productId: 'prod-456'
|
|
47
|
+
} as never)
|
|
48
|
+
|
|
49
|
+
vi.mocked(useMessagesCountAtomValue).mockReturnValue(0)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
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' }
|
|
67
|
+
mockSendMessage.mockResolvedValue(mockMessage)
|
|
68
|
+
mockUpdateTitle.mockResolvedValue({})
|
|
69
|
+
|
|
70
|
+
const { result } = renderHook(() => useSendFirstMessage())
|
|
71
|
+
|
|
72
|
+
await result.current.sendFirstMessage('Hello world')
|
|
73
|
+
|
|
74
|
+
expect(mockSendMessage).toHaveBeenCalledWith('Hello world', undefined)
|
|
75
|
+
expect(mockUpdateTitle).toHaveBeenCalledWith({
|
|
76
|
+
conversationId: 'conv-123',
|
|
77
|
+
productId: 'agent-prod-42',
|
|
78
|
+
subject: 'Hello world'
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('when not first message', () => {
|
|
84
|
+
it('should only send message without updating title', async () => {
|
|
85
|
+
vi.mocked(useMessagesCountAtomValue).mockReturnValue(5)
|
|
86
|
+
|
|
87
|
+
const mockMessage = { id: 'msg-1', content: 'Hello' }
|
|
88
|
+
mockSendMessage.mockResolvedValue(mockMessage)
|
|
89
|
+
|
|
90
|
+
const { result } = renderHook(() => useSendFirstMessage())
|
|
91
|
+
|
|
92
|
+
await result.current.sendFirstMessage('Hello world')
|
|
93
|
+
|
|
94
|
+
expect(mockSendMessage).toHaveBeenCalledWith('Hello world', undefined)
|
|
95
|
+
expect(mockUpdateTitle).not.toHaveBeenCalled()
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('when missing settings', () => {
|
|
100
|
+
it('should not update title if conversationId is missing', async () => {
|
|
101
|
+
const mockMessage = { id: 'msg-1', content: 'Hello' }
|
|
102
|
+
mockSendMessage.mockResolvedValue(mockMessage)
|
|
103
|
+
|
|
104
|
+
const { result } = renderHook(() => useSendFirstMessage())
|
|
105
|
+
|
|
106
|
+
await result.current.sendFirstMessage('Hello world')
|
|
107
|
+
|
|
108
|
+
expect(mockSendMessage).toHaveBeenCalled()
|
|
109
|
+
expect(mockUpdateTitle).not.toHaveBeenCalled()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('error handling', () => {
|
|
114
|
+
it('should throw error when send message fails', async () => {
|
|
115
|
+
vi.spyOn(console, 'error').mockImplementationOnce(() => {})
|
|
116
|
+
const error = new Error('Send failed')
|
|
117
|
+
mockSendMessage.mockRejectedValue(error)
|
|
118
|
+
|
|
119
|
+
const { result } = renderHook(() => useSendFirstMessage())
|
|
120
|
+
|
|
121
|
+
await expect(result.current.sendFirstMessage('Hello')).rejects.toThrow('Send failed')
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('loading states', () => {
|
|
126
|
+
it('should return loading state when sending message', () => {
|
|
127
|
+
vi.mocked(useSendTextMessage).mockReturnValue({
|
|
128
|
+
mutateAsync: mockSendMessage,
|
|
129
|
+
isPending: true,
|
|
130
|
+
error: null
|
|
131
|
+
} as never)
|
|
132
|
+
|
|
133
|
+
const { result } = renderHook(() => useSendFirstMessage())
|
|
134
|
+
|
|
135
|
+
expect(result.current.isLoading).toBe(true)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should return loading state when updating title', () => {
|
|
139
|
+
vi.mocked(useUpdateConversationTitle).mockReturnValue({
|
|
140
|
+
mutateAsync: mockUpdateTitle,
|
|
141
|
+
isPending: true,
|
|
142
|
+
error: null
|
|
143
|
+
} as never)
|
|
144
|
+
|
|
145
|
+
const { result } = renderHook(() => useSendFirstMessage())
|
|
146
|
+
|
|
147
|
+
expect(result.current.isLoading).toBe(true)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
import type { Message } from '@hotmart-org-ca/sparkie/dist/MessageService'
|
|
3
|
+
import type { MutateOptions } from '@tanstack/react-query'
|
|
4
|
+
|
|
5
|
+
import { useUpdateConversationTitle } from '@/src/modules/conversation/hooks/update-conversation-title'
|
|
6
|
+
import { useWidgetSettingsAtomValue } from '@/src/modules/widget/store'
|
|
7
|
+
import { useMessagesCountAtomValue } from '../../store/messages-count.atom'
|
|
8
|
+
import { excerptMessage } from '../../utils'
|
|
9
|
+
import { useSendTextMessage } from '../use-send-text-message'
|
|
10
|
+
|
|
11
|
+
function useSendFirstMessage() {
|
|
12
|
+
const messagesCount = useMessagesCountAtomValue()
|
|
13
|
+
const sendMessageMutation = useSendTextMessage()
|
|
14
|
+
const updateTitleMutation = useUpdateConversationTitle()
|
|
15
|
+
const settings = useWidgetSettingsAtomValue()
|
|
16
|
+
|
|
17
|
+
const sendFirstMessage = useCallback(
|
|
18
|
+
async (message: string, options?: MutateOptions<Message, Error, string, void>) => {
|
|
19
|
+
const isFirstMessage = messagesCount === 0
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const messageResult = await sendMessageMutation.mutateAsync(message, options)
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
isFirstMessage &&
|
|
26
|
+
settings?.conversationId &&
|
|
27
|
+
settings?.config?.metadata?.agentProductId
|
|
28
|
+
) {
|
|
29
|
+
await updateTitleMutation.mutateAsync({
|
|
30
|
+
conversationId: settings.conversationId,
|
|
31
|
+
productId: settings?.config?.metadata?.agentProductId,
|
|
32
|
+
subject: excerptMessage({ message })
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return messageResult
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Failed to send first message:', error)
|
|
39
|
+
throw error
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[messagesCount, sendMessageMutation, updateTitleMutation, settings]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
sendFirstMessage,
|
|
47
|
+
isLoading: sendMessageMutation.isPending || updateTitleMutation.isPending,
|
|
48
|
+
error: sendMessageMutation.error || updateTitleMutation.error
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default useSendFirstMessage
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { atom, useAtom, useAtomValue } from 'jotai'
|
|
2
|
+
|
|
3
|
+
const messagesCountAtom = atom(0)
|
|
4
|
+
|
|
5
|
+
const setMessagesCountAtom = atom(
|
|
6
|
+
(get) => get(messagesCountAtom),
|
|
7
|
+
(_, set, count: number) => set(messagesCountAtom, count)
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export const useMessagesCountAtom = () => useAtom(setMessagesCountAtom)
|
|
11
|
+
|
|
12
|
+
export const useMessagesCountAtomValue = () => useAtomValue(messagesCountAtom)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { EXCERPT_SIZE } from '../../constants'
|
|
2
|
+
|
|
3
|
+
import { excerptMessage } from './excerpt-message'
|
|
4
|
+
|
|
5
|
+
describe('excerptMessage', () => {
|
|
6
|
+
describe('when message is shorter than excerpt size', () => {
|
|
7
|
+
it('should return the original message without ellipsis', () => {
|
|
8
|
+
const message = 'Short message'
|
|
9
|
+
const result = excerptMessage({ message })
|
|
10
|
+
|
|
11
|
+
expect(result).toBe('Short message')
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('when message is longer than excerpt size', () => {
|
|
16
|
+
it('should return truncated message with ellipsis', () => {
|
|
17
|
+
const message = 'This is a very long message that exceeds the default excerpt size limit'
|
|
18
|
+
const result = excerptMessage({ message })
|
|
19
|
+
|
|
20
|
+
expect(result).toBe('This is a very long message that exceeds the de...')
|
|
21
|
+
expect(result.length).toBe(EXCERPT_SIZE + 3)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('when message is exactly the excerpt size', () => {
|
|
26
|
+
it('should return the original message without ellipsis', () => {
|
|
27
|
+
const message = 'A'.repeat(EXCERPT_SIZE)
|
|
28
|
+
const result = excerptMessage({ message })
|
|
29
|
+
|
|
30
|
+
expect(result).toBe(message)
|
|
31
|
+
expect(result).not.toContain('...')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('when custom excerpt size is provided', () => {
|
|
36
|
+
it('should use custom size for truncation', () => {
|
|
37
|
+
const message = 'This is a test message'
|
|
38
|
+
const customSize = 10
|
|
39
|
+
const result = excerptMessage({ message, excerptSize: customSize })
|
|
40
|
+
|
|
41
|
+
expect(result).toBe(message.slice(0, customSize).trim() + '...')
|
|
42
|
+
expect(result.length).toBe(customSize + 2)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('when message is empty', () => {
|
|
47
|
+
it('should return empty string', () => {
|
|
48
|
+
const result = excerptMessage({ message: '' })
|
|
49
|
+
|
|
50
|
+
expect(result).toBe('')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('when message has leading/trailing whitespace', () => {
|
|
55
|
+
it('should trim whitespace from truncated message', () => {
|
|
56
|
+
const customSize = 15
|
|
57
|
+
const message = ' This is a message with spaces '
|
|
58
|
+
const result = excerptMessage({ message, excerptSize: customSize })
|
|
59
|
+
|
|
60
|
+
expect(result).toBe(message.slice(0, customSize).trim() + '...')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('when message is null or undefined', () => {
|
|
65
|
+
it('should handle null message', () => {
|
|
66
|
+
const result = excerptMessage({ message: null as never })
|
|
67
|
+
|
|
68
|
+
expect(result).toBe(null)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should handle undefined message', () => {
|
|
72
|
+
const result = excerptMessage({ message: undefined as never })
|
|
73
|
+
|
|
74
|
+
expect(result).toBe(undefined)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EXCERPT_SIZE } from '../../constants'
|
|
2
|
+
|
|
3
|
+
type ExcerptMessageProps = {
|
|
4
|
+
message: string
|
|
5
|
+
excerptSize?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const excerptMessage = ({ message, excerptSize = EXCERPT_SIZE }: ExcerptMessageProps) =>
|
|
9
|
+
Number(message?.length) > 0
|
|
10
|
+
? message.slice(0, excerptSize).trim() + (message.length > excerptSize ? '...' : '')
|
|
11
|
+
: message
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './excerpt-message'
|
|
@@ -7,6 +7,7 @@ import { ChatInput, MessagesList, useChatInputValueAtom } from '@/src/modules/me
|
|
|
7
7
|
import { ChatFileUploaderWrapper } from '@/src/modules/messages/components/chat-file-uploader-wrapper'
|
|
8
8
|
import { MessagesContainer } from '@/src/modules/messages/components/messages-container'
|
|
9
9
|
import { getAllMessagesQuery, useSendTextMessage } from '@/src/modules/messages/hooks'
|
|
10
|
+
import { useSendFirstMessage } from '@/src/modules/messages/hooks/use-send-first-message'
|
|
10
11
|
import { useMessagesMaxCount } from '@/src/modules/messages/store'
|
|
11
12
|
import { useAttachedFileAtom } from '@/src/modules/messages/store/attached-file.atom'
|
|
12
13
|
import { useGetProfile } from '@/src/modules/profile'
|
|
@@ -30,7 +31,7 @@ function ChatPage() {
|
|
|
30
31
|
const [attachedFileAtom] = useAttachedFileAtom()
|
|
31
32
|
const profileQuery = useGetProfile()
|
|
32
33
|
const widgetTabs = useWidgetTabsValueAtom()
|
|
33
|
-
const sendTextMessageMutation =
|
|
34
|
+
const sendTextMessageMutation = useSendFirstMessage()
|
|
34
35
|
const sendInitialMessageMutation = useSendTextMessage()
|
|
35
36
|
const limit = useMessagesMaxCount()
|
|
36
37
|
const [value, setValue] = useChatInputValueAtom()
|
|
@@ -59,7 +60,7 @@ function ChatPage() {
|
|
|
59
60
|
|
|
60
61
|
if (!isTextEmpty(text)) return
|
|
61
62
|
|
|
62
|
-
sendTextMessageMutation.
|
|
63
|
+
void sendTextMessageMutation.sendFirstMessage(text, {
|
|
63
64
|
onSuccess() {
|
|
64
65
|
if (chatInputRef.current?.value) chatInputRef.current.value = ''
|
|
65
66
|
setValue('')
|
|
@@ -117,7 +118,7 @@ function ChatPage() {
|
|
|
117
118
|
name='new-chat-msg-input'
|
|
118
119
|
ref={chatInputRef}
|
|
119
120
|
onSend={widgetTabs.currentTab === 'chat' ? handleSendMessage : undefined}
|
|
120
|
-
loading={sendTextMessageMutation
|
|
121
|
+
loading={sendTextMessageMutation?.isLoading}
|
|
121
122
|
inputDisabled={messagesQuery?.isLoading}
|
|
122
123
|
buttonDisabled={
|
|
123
124
|
widgetLoading ||
|
|
@@ -13,7 +13,7 @@ import { AiIconCircle, Button, Icon, Tooltip } from '@/src/lib/components'
|
|
|
13
13
|
import { useMediaQuery } from '@/src/lib/hooks'
|
|
14
14
|
import { TutorWidgetEvents } from '../../events'
|
|
15
15
|
import { useMembershipColor } from '../../hooks'
|
|
16
|
-
import { useWidgetGoBackTabAtom, useWidgetTabsAtom } from '../../store'
|
|
16
|
+
import { useIsAgentParentAtomValue, useWidgetGoBackTabAtom, useWidgetTabsAtom } from '../../store'
|
|
17
17
|
|
|
18
18
|
import type { WidgetHeaderProps } from './types'
|
|
19
19
|
|
|
@@ -93,6 +93,7 @@ function WidgetHeader({
|
|
|
93
93
|
const [, goBack] = useWidgetGoBackTabAtom()
|
|
94
94
|
const membershipColor = useMembershipColor()
|
|
95
95
|
const name = tutorName ?? t('general.name')
|
|
96
|
+
const isAgentMode = useIsAgentParentAtomValue()
|
|
96
97
|
|
|
97
98
|
const handleHideWidget = () => {
|
|
98
99
|
TutorWidgetEvents['c3po-app-widget-hide'].dispatch()
|
|
@@ -111,6 +112,8 @@ function WidgetHeader({
|
|
|
111
112
|
DataHubService.sendEvent({ schema: new ClickTutorBackSchema() })
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
if (isAgentMode) return null
|
|
116
|
+
|
|
114
117
|
return (
|
|
115
118
|
<div
|
|
116
119
|
id='tutor-ai-consumer-widget-header'
|
|
@@ -11,4 +11,4 @@ export const setWidgetSettingsAtom = atom(
|
|
|
11
11
|
|
|
12
12
|
export const useWidgetSettingsAtom = () => useAtom(setWidgetSettingsAtom)
|
|
13
13
|
|
|
14
|
-
export const useWidgetSettingsAtomValue = () => useAtomValue(
|
|
14
|
+
export const useWidgetSettingsAtomValue = () => useAtomValue(widgetSettingsAtom)
|