app-tutor-ai-consumer 1.3.0 → 1.4.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/config/vitest/setupTests.ts +6 -1
  3. package/package.json +11 -2
  4. package/public/assets/images/default-image.png +0 -0
  5. package/src/@types/declarations.d.ts +12 -3
  6. package/src/config/dayjs/index.ts +2 -0
  7. package/src/config/dayjs/init.ts +28 -0
  8. package/src/config/dayjs/utils/format-fulldate.ts +7 -0
  9. package/src/config/dayjs/utils/format-time.ts +20 -0
  10. package/src/config/dayjs/utils/index.ts +2 -0
  11. package/src/config/styles/global.css +19 -1
  12. package/src/config/tanstack/query-provider.tsx +1 -1
  13. package/src/config/tests/handlers.ts +6 -0
  14. package/src/index.tsx +4 -0
  15. package/src/lib/components/index.ts +1 -0
  16. package/src/lib/components/markdownrenderer/__tests__/markdown.stub.ts +334 -0
  17. package/src/lib/components/markdownrenderer/components/index.ts +1 -0
  18. package/src/lib/components/markdownrenderer/components/md-code-block/index.ts +1 -0
  19. package/src/lib/components/markdownrenderer/components/md-code-block/md-code-block.tsx +71 -0
  20. package/src/lib/components/markdownrenderer/index.ts +2 -0
  21. package/src/lib/components/markdownrenderer/markdownrenderer.tsx +115 -0
  22. package/src/lib/utils/constants.ts +1 -1
  23. package/src/lib/utils/copy-text-to-clipboard.tsx +13 -0
  24. package/src/lib/utils/extract-text-from-react-nodes.ts +23 -0
  25. package/src/lib/utils/index.ts +3 -0
  26. package/src/lib/utils/urls.ts +20 -0
  27. package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +113 -0
  28. package/src/modules/messages/__tests__/imessage-with-sender-data.mock.ts +15 -0
  29. package/src/modules/messages/components/index.ts +2 -0
  30. package/src/modules/messages/components/message-img/index.ts +1 -0
  31. package/src/modules/messages/components/message-img/message-img.tsx +47 -0
  32. package/src/modules/messages/components/message-item/index.ts +2 -0
  33. package/src/modules/messages/components/message-item/message-item.spec.tsx +26 -0
  34. package/src/modules/messages/components/message-item/message-item.tsx +15 -0
  35. package/src/modules/messages/components/messages-list/messages-list.tsx +46 -17
  36. package/src/modules/messages/hooks/index.ts +1 -0
  37. package/src/modules/messages/hooks/use-fetch-messages/index.ts +2 -0
  38. package/src/modules/messages/hooks/use-fetch-messages/use-fetch-messages.spec.tsx +46 -0
  39. package/src/modules/messages/hooks/use-fetch-messages/use-fetch-messages.tsx +103 -0
  40. package/src/modules/messages/service.ts +26 -3
  41. package/src/modules/messages/types.ts +33 -2
  42. package/src/modules/messages/utils/index.ts +1 -0
  43. package/src/modules/messages/utils/messages-parser/index.ts +1 -0
  44. package/src/modules/messages/utils/messages-parser/utils.ts +28 -0
  45. package/src/modules/profile/__tests__/profile-api-props.builder.ts +74 -0
  46. package/src/modules/profile/__tests__/profile-props.builder.ts +42 -0
  47. package/src/modules/profile/constants.ts +3 -0
  48. package/src/modules/profile/hooks/index.ts +1 -0
  49. package/src/modules/profile/hooks/use-get-profile/index.ts +1 -0
  50. package/src/modules/profile/hooks/use-get-profile/use-get-profile.spec.tsx +20 -0
  51. package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +14 -0
  52. package/src/modules/profile/index.ts +4 -0
  53. package/src/modules/profile/service.tsx +19 -0
  54. package/src/modules/profile/types.ts +17 -0
  55. package/src/modules/widget/components/chat-page/chat-page.tsx +3 -3
  56. package/src/modules/widget/components/container/container.tsx +1 -1
  57. package/src/modules/widget/components/onboarding-page/onboarding-page.tsx +16 -14
  58. package/src/modules/widget/components/starter-page/starter-page.tsx +3 -3
  59. package/src/modules/widget/store/widget-settings.atom.ts +3 -1
  60. package/src/config/styles/shared-styles.module.css +0 -16
  61. package/src/modules/widget/components/container/styles.module.css +0 -11
@@ -1,11 +1,15 @@
1
- import type { Message, MessageWithSenderData } from '@hotmart/sparkie/dist/MessageService'
1
+ import type { Message } from '@hotmart/sparkie/dist/MessageService'
2
2
 
3
3
  import { ApiError } from '@/src/config/request'
4
4
  import { HttpCodes } from '@/src/lib/utils'
5
5
  import { SparkieService } from '../sparkie'
6
6
 
7
+ import { MSG_MAX_COUNT } from './constants'
7
8
  import type {
9
+ FetchMessagesResponse,
10
+ IFetchMessagesOptions,
8
11
  IGetMessagesPayload,
12
+ IMessageWithSenderData,
9
13
  ISendImageMessagePayload,
10
14
  ISendTextMessagePayload
11
15
  } from './types'
@@ -30,12 +34,31 @@ class MessagesService {
30
34
  async getMessages({
31
35
  conversationId,
32
36
  before
33
- }: IGetMessagesPayload): Promise<Array<MessageWithSenderData>> {
37
+ }: IGetMessagesPayload): Promise<Array<IMessageWithSenderData>> {
34
38
  const data = await this.sparkieMessageService.getAll(conversationId, {
35
39
  before
36
40
  })
37
41
 
38
- return data ?? []
42
+ return (data ?? []) as Array<IMessageWithSenderData>
43
+ }
44
+
45
+ async fetchMessages({
46
+ currentMessages,
47
+ conversationId,
48
+ loadFirstPage
49
+ }: IFetchMessagesOptions): Promise<FetchMessagesResponse> {
50
+ let before: number | undefined
51
+
52
+ if (currentMessages && currentMessages.length && !loadFirstPage) {
53
+ before = currentMessages.slice().sort((a, b) => a.sentAt - b.sentAt)[0].sentAt
54
+ }
55
+
56
+ const messages = (await this.getMessages({ conversationId, before })) ?? []
57
+
58
+ return {
59
+ messages,
60
+ hasMore: messages.length === MSG_MAX_COUNT
61
+ }
39
62
  }
40
63
 
41
64
  async sendTextMessage({
@@ -1,6 +1,12 @@
1
- import type { Message } from '@hotmart/sparkie/dist/MessageService'
1
+ import type { Message as SparkieMsg, SenderData } from '@hotmart/sparkie/dist/MessageService'
2
2
 
3
- export type IMessage = Message & {
3
+ export type IMessage = SparkieMsg & {
4
+ metadata: {
5
+ author: 'ai' | 'user'
6
+ sessionId: string
7
+ externalId: string
8
+ correlationId: string
9
+ }
4
10
  sending?: boolean // indicates when the current message is being sent
5
11
  }
6
12
 
@@ -45,3 +51,28 @@ export type IFetchMessagesOptions = {
45
51
  conversationId: string
46
52
  loadFirstPage?: boolean
47
53
  }
54
+
55
+ export type IMessageWithSenderData = IMessage & SenderData
56
+
57
+ export type MessageParserArgs = {
58
+ messages: Array<IMessageWithSenderData>
59
+ profileId: string
60
+ }
61
+
62
+ export type ParsedMessage = {
63
+ from: string
64
+ id: string
65
+ isRead: boolean
66
+ time: string
67
+ contact: IMessageWithSenderData['contact']
68
+ metadata: IMessageWithSenderData['metadata']
69
+ parentId: IMessageWithSenderData['parentId']
70
+ sending: IMessageWithSenderData['sending']
71
+ threadId: IMessageWithSenderData['threadId']
72
+ timestamp: IMessageWithSenderData['sentAt']
73
+ } & IMessageWithSenderData['content']
74
+
75
+ export type FetchMessagesResponse = {
76
+ messages: IMessageWithSenderData[]
77
+ hasMore: boolean
78
+ }
@@ -0,0 +1 @@
1
+ export * from './messages-parser'
@@ -0,0 +1 @@
1
+ export * from './utils'
@@ -0,0 +1,28 @@
1
+ import dayjs from 'dayjs'
2
+
3
+ import type { IMessageWithSenderData, MessageParserArgs, ParsedMessage } from '../../types'
4
+
5
+ export const msgParser = (msg: IMessageWithSenderData, profileId: string) => ({
6
+ id: msg.id,
7
+ time: dayjs(msg.sentAt).format('LT'),
8
+ timestamp: msg.sentAt,
9
+ from: msg.contactId === profileId ? 'me' : msg.contactId,
10
+ contact: msg.contact,
11
+ sending: msg.sending,
12
+ parentId: msg.parentId,
13
+ threadId: msg.threadId,
14
+ isRead: false,
15
+ metadata: msg.metadata || {},
16
+ ...msg.content
17
+ })
18
+
19
+ export const messagesParser = ({
20
+ messages,
21
+ profileId
22
+ }: MessageParserArgs): Array<ParsedMessage> => {
23
+ if (!(Number(messages.length) > 0) || !profileId) return []
24
+
25
+ return messages
26
+ .map((msg) => msgParser(msg, profileId))
27
+ .sort((a, b) => a.timestamp - b.timestamp) as Array<ParsedMessage>
28
+ }
@@ -0,0 +1,74 @@
1
+ import { chance } from '@/src/config/tests'
2
+ import type { ProfileAPIProps } from '../types'
3
+
4
+ class ProfileAPIPropsBuilder implements ProfileAPIProps {
5
+ id: string
6
+ name: string
7
+ user_id: number
8
+ picture: string
9
+ is_anonymous: boolean
10
+ created_at: string
11
+ updated_at: string
12
+ deleted_at: string
13
+
14
+ constructor() {
15
+ this.id = chance.guid()
16
+ this.name = chance.name()
17
+ this.user_id = chance.integer({ min: 1000 })
18
+ this.picture = chance.avatar()
19
+ this.is_anonymous = false
20
+ this.created_at = Date.now().toString()
21
+ this.updated_at = Date.now().toString()
22
+ this.deleted_at = ''
23
+ }
24
+
25
+ withId(id: typeof this.id) {
26
+ this.id = id
27
+
28
+ return this
29
+ }
30
+
31
+ withName(name: typeof this.name) {
32
+ this.name = name
33
+
34
+ return this
35
+ }
36
+
37
+ withUser_id(user_id: typeof this.user_id) {
38
+ this.user_id = user_id
39
+
40
+ return this
41
+ }
42
+
43
+ withPicture(picture: typeof this.picture) {
44
+ this.picture = picture
45
+
46
+ return this
47
+ }
48
+
49
+ withIs_anonymous(is_anonymous: typeof this.is_anonymous) {
50
+ this.is_anonymous = is_anonymous
51
+
52
+ return this
53
+ }
54
+
55
+ withCreated_at(created_at: typeof this.created_at) {
56
+ this.created_at = created_at
57
+
58
+ return this
59
+ }
60
+
61
+ withUpdated_at(updated_at: typeof this.updated_at) {
62
+ this.updated_at = updated_at
63
+
64
+ return this
65
+ }
66
+
67
+ withDeleted_at(deleted_at: typeof this.deleted_at) {
68
+ this.deleted_at = deleted_at
69
+
70
+ return this
71
+ }
72
+ }
73
+
74
+ export default ProfileAPIPropsBuilder
@@ -0,0 +1,42 @@
1
+ import { chance } from '@/src/config/tests'
2
+ import type { ProfileProps } from '../types'
3
+
4
+ class ProfilePropsBuilder implements ProfileProps {
5
+ id: string
6
+ name: string
7
+ avatar: string
8
+ userId: string | number
9
+
10
+ constructor() {
11
+ this.id = chance.guid()
12
+ this.name = chance.name()
13
+ this.avatar = chance.avatar()
14
+ this.userId = chance.guid()
15
+ }
16
+
17
+ withId(id: typeof this.id) {
18
+ this.id = id
19
+
20
+ return this
21
+ }
22
+
23
+ withName(name: typeof this.name) {
24
+ this.name = name
25
+
26
+ return this
27
+ }
28
+
29
+ withAvatar(avatar: typeof this.avatar) {
30
+ this.avatar = avatar
31
+
32
+ return this
33
+ }
34
+
35
+ withUserId(userId: typeof this.userId) {
36
+ this.userId = userId
37
+
38
+ return this
39
+ }
40
+ }
41
+
42
+ export default ProfilePropsBuilder
@@ -0,0 +1,3 @@
1
+ export const ProfileEndpoints = {
2
+ getProfile: () => `${process.env.API_CONVERSATION_URL}/v1/contacts/me`
3
+ }
@@ -0,0 +1 @@
1
+ export * from './use-get-profile'
@@ -0,0 +1 @@
1
+ export * from './use-get-profile'
@@ -0,0 +1,20 @@
1
+ import { renderHook, waitFor } from '@/src/config/tests'
2
+
3
+ import { useGetProfile } from './use-get-profile'
4
+
5
+ describe('useGetProfile', () => {
6
+ const render = (enabled = true) => renderHook(() => useGetProfile(enabled))
7
+
8
+ it('should render without errors', async () => {
9
+ const { result } = render()
10
+
11
+ await waitFor(() => expect(result.current.isSuccess).toBeTruthy())
12
+
13
+ expect(result.current.data).toMatchObject({
14
+ id: expect.any(String) as string,
15
+ name: expect.any(String) as string,
16
+ avatar: expect.any(String) as string,
17
+ userId: expect.any(Number) as number
18
+ })
19
+ })
20
+ })
@@ -0,0 +1,14 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+
3
+ import { ProfileEndpoints } from '../../constants'
4
+ import ProfileService from '../../service'
5
+
6
+ export const getProfileQuery = (enabled: boolean) => ({
7
+ queryKey: [ProfileEndpoints.getProfile()],
8
+ queryFn: () => ProfileService.getProfile(),
9
+ enabled
10
+ })
11
+
12
+ export function useGetProfile(enabled: boolean = true) {
13
+ return useQuery(getProfileQuery(enabled))
14
+ }
@@ -0,0 +1,4 @@
1
+ export * from './constants'
2
+ export * from './hooks'
3
+ export { default as ProfileService } from './service'
4
+ export * from './types'
@@ -0,0 +1,19 @@
1
+ import { api } from '@/src/config/request'
2
+
3
+ import { ProfileEndpoints } from './constants'
4
+ import type { ProfileAPIProps, ProfileProps } from './types'
5
+
6
+ class ProfileService {
7
+ async getProfile(): Promise<ProfileProps> {
8
+ const { data } = await api.get<ProfileAPIProps>(ProfileEndpoints.getProfile())
9
+
10
+ return {
11
+ id: data.id,
12
+ name: data.name,
13
+ avatar: data.picture,
14
+ userId: data.user_id
15
+ } as ProfileProps
16
+ }
17
+ }
18
+
19
+ export default new ProfileService()
@@ -0,0 +1,17 @@
1
+ export type ProfileAPIProps = {
2
+ id: string
3
+ user_id: number
4
+ name: string
5
+ picture: string
6
+ is_anonymous: boolean
7
+ created_at: string
8
+ updated_at: string
9
+ deleted_at: string
10
+ }
11
+
12
+ export type ProfileProps = {
13
+ id: string
14
+ name: string
15
+ avatar: string
16
+ userId: string | number
17
+ }
@@ -6,14 +6,14 @@ function ChatPage() {
6
6
  const chatInputRef = useRef<HTMLInputElement>(null)
7
7
 
8
8
  return (
9
- <div className='flex flex-1 flex-col justify-center'>
10
- <div className='flex flex-1 flex-col justify-center px-5 py-4'>
9
+ <>
10
+ <div className='overflow-auto px-5 py-4'>
11
11
  <MessagesList />
12
12
  </div>
13
13
  <div className='border-t border-t-neutral-700 px-5 py-4'>
14
14
  <ChatInput name='new-chat-msg-input' ref={chatInputRef} />
15
15
  </div>
16
- </div>
16
+ </>
17
17
  )
18
18
  }
19
19
 
@@ -22,7 +22,7 @@ function WidgetContainer() {
22
22
 
23
23
  return (
24
24
  <div className='flex min-h-svh flex-col items-center justify-center bg-neutral-900'>
25
- <div className='flex flex-1 flex-col justify-center gap-6'>
25
+ <div className='grid h-svh w-full grid-rows-[1fr_max-content]'>
26
26
  {WIDGET_TABS[widgetTabs.currentTab]}
27
27
  </div>
28
28
  </div>
@@ -12,26 +12,28 @@ function WidgetOnboardingPage() {
12
12
  const { t } = useTranslation()
13
13
 
14
14
  return (
15
- <div className={clsx('flex flex-1 flex-col justify-center gap-6 px-4 py-6', styles.bg)}>
16
- <div className='flex flex-1 flex-col justify-center gap-6 px-0.5'>
17
- <div className='mx-auto max-w-[67%]'>
18
- <img src={TutorOnboardingSVG} aria-hidden />
19
- </div>
20
- <div className='flex flex-col gap-2'>
21
- <h3 className={clsx(styles.gradientTxt, 'text-center text-xl/tight font-semibold')}>
22
- {t('onboarding.title')}
23
- </h3>
24
- <p className='text-center text-sm/snug font-normal text-gray-400'>
25
- {t('onboarding.description')}
26
- </p>
15
+ <>
16
+ <div className={styles.bg}>
17
+ <div className='mx-4 flex h-full flex-col justify-center gap-6 px-0.5'>
18
+ <div className='mx-auto max-w-[67%]'>
19
+ <img src={TutorOnboardingSVG} aria-hidden />
20
+ </div>
21
+ <div className='flex flex-col gap-2'>
22
+ <h3 className={clsx(styles.gradientTxt, 'text-center text-xl/tight font-semibold')}>
23
+ {t('onboarding.title')}
24
+ </h3>
25
+ <p className='text-center text-sm/snug font-normal text-gray-400'>
26
+ {t('onboarding.description')}
27
+ </p>
28
+ </div>
27
29
  </div>
28
30
  </div>
29
- <div className='flex gap-4'>
31
+ <div className='mx-4 mb-4 mt-auto flex flex-col gap-4'>
30
32
  <Button variant='brand' className='flex-1' onClick={() => setWidgetTabs('starter')}>
31
33
  {t('general.buttons.start')}
32
34
  </Button>
33
35
  </div>
34
- </div>
36
+ </>
35
37
  )
36
38
  }
37
39
 
@@ -23,8 +23,8 @@ function WidgetStarterPage() {
23
23
  })
24
24
 
25
25
  return (
26
- <div className='flex flex-1 flex-col justify-center'>
27
- <div className='flex flex-1 flex-col justify-center px-5 py-4'>
26
+ <>
27
+ <div className='flex flex-col justify-center px-5 py-4'>
28
28
  <GreetingsCard author={settings?.author ?? ''} tutorName={settings?.tutorName ?? ''} />
29
29
  </div>
30
30
  <div className='border-t border-t-neutral-700 px-5 py-4'>
@@ -34,7 +34,7 @@ function WidgetStarterPage() {
34
34
  onSend={() => setWidgetTabs('chat')}
35
35
  />
36
36
  </div>
37
- </div>
37
+ </>
38
38
  )
39
39
  }
40
40
 
@@ -1,4 +1,4 @@
1
- import { atom, useAtom } from 'jotai'
1
+ import { atom, useAtom, useAtomValue } from 'jotai'
2
2
 
3
3
  import type { WidgetSettingProps } from '@/src/types'
4
4
 
@@ -10,3 +10,5 @@ export const setWidgetSettingsAtom = atom(
10
10
  )
11
11
 
12
12
  export const useWidgetSettingsAtom = () => useAtom(setWidgetSettingsAtom)
13
+
14
+ export const useWidgetSettingsAtomValue = () => useAtomValue(setWidgetSettingsAtom)
@@ -1,16 +0,0 @@
1
- .scrollbar {
2
- &::-webkit-scrollbar {
3
- width: var(--hc-size-spacing-2);
4
- height: var(--hc-size-spacing-2);
5
- }
6
-
7
- &::-webkit-scrollbar-track {
8
- background: transparent;
9
- }
10
-
11
- &::-webkit-scrollbar-thumb {
12
- background: var(--hc-color-neutral-400);
13
- border-radius: var(--hc-size-border-medium);
14
- border: calc(var(--hc-size-border-medium) / 2) solid transparent;
15
- }
16
- }
@@ -1,11 +0,0 @@
1
- .main {
2
- background:
3
- linear-gradient(
4
- 186.9deg,
5
- rgb(from var(--ai-color-primary) r g b / 0.2) 6.65%,
6
- rgb(from var(--ai-color-secondary) r g b / 0.2) 28.99%,
7
- rgb(from var(--ai-color-dark) r g b / 0.2) 46.97%,
8
- rgb(from var(--hc-color-neutral-1000) r g b / 0.2) 57.9%
9
- ),
10
- linear-gradient(0deg, var(--hc-color-neutral-1000), var(--hc-color-neutral-1000));
11
- }