app-tutor-ai-consumer 1.4.0 → 1.6.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/.github/workflows/staging-staging.yml +148 -0
- package/.github/workflows/staging.yml +1 -2
- package/CHANGELOG.md +19 -0
- package/config/rspack/rspack.config.js +5 -1
- package/config/vitest/__mocks__/icons.tsx +3 -0
- package/config/vitest/__mocks__/intersection-observer.ts +10 -0
- package/config/vitest/__mocks__/sparkie.tsx +9 -9
- package/config/vitest/__mocks__/use-init-sparkie.tsx +14 -0
- package/config/vitest/vitest.config.mts +13 -8
- package/environments/.env.test +2 -0
- package/eslint.config.mjs +27 -0
- package/package.json +8 -4
- package/public/index.html +3 -4
- package/src/@types/index.d.ts +5 -2
- package/src/config/styles/global.css +2 -2
- package/src/config/tanstack/query-client.ts +3 -2
- package/src/config/tests/utils.tsx +3 -2
- package/src/config/tests/wrappers.tsx +4 -1
- package/src/development-bootstrap.tsx +15 -15
- package/src/index.tsx +37 -5
- package/src/lib/components/icons/ai-color.svg +17 -0
- package/src/lib/components/icons/arrow-down.svg +5 -0
- package/src/lib/components/icons/chevron-down.svg +4 -0
- package/src/lib/components/icons/icon-names.d.ts +1 -1
- package/src/lib/components/markdownrenderer/markdownrenderer.tsx +7 -9
- package/src/lib/hooks/index.ts +3 -0
- package/src/lib/hooks/use-intersection-observer-reverse-scroll/index.ts +2 -0
- package/src/lib/hooks/use-intersection-observer-reverse-scroll/use-intersection-observer-reverse-scroll.tsx +147 -0
- package/src/lib/hooks/use-ref-client-height/index.ts +2 -0
- package/src/lib/hooks/use-ref-client-height/use-ref-client-height.tsx +38 -0
- package/src/lib/hooks/use-scroll-to-ref/index.ts +2 -0
- package/src/lib/hooks/use-scroll-to-ref/use-scroll-to-ref.tsx +14 -0
- package/src/lib/hooks/use-throttle/index.ts +3 -0
- package/src/lib/hooks/use-throttle/types.ts +13 -0
- package/src/lib/hooks/use-throttle/use-throttle.spec.tsx +296 -0
- package/src/lib/hooks/use-throttle/use-throttle.tsx +91 -0
- package/src/lib/utils/is-text-empty.ts +3 -0
- package/src/main/main.spec.tsx +7 -6
- package/src/modules/cursor/__tests__/icursor-update.builder.ts +42 -0
- package/src/modules/cursor/hooks/index.ts +1 -0
- package/src/modules/cursor/hooks/use-update-cursor/index.ts +2 -0
- package/src/modules/cursor/hooks/use-update-cursor/use-update-cursor.spec.tsx +23 -0
- package/src/modules/cursor/hooks/use-update-cursor/use-update-cursor.ts +11 -0
- package/src/modules/cursor/index.ts +2 -0
- package/src/modules/cursor/service.ts +15 -0
- package/src/modules/cursor/types.ts +9 -0
- package/src/modules/global-providers/index.ts +1 -0
- package/src/modules/messages/__tests__/parsed-message.builder.ts +164 -0
- package/src/modules/messages/components/chat-input/chat-input.spec.tsx +72 -0
- package/src/modules/messages/components/chat-input/chat-input.tsx +52 -6
- package/src/modules/messages/components/index.ts +1 -0
- package/src/modules/messages/components/message-item/message-item.spec.tsx +2 -2
- package/src/modules/messages/components/message-item/message-item.tsx +14 -1
- package/src/modules/messages/components/message-item-end-of-scroll/index.ts +2 -0
- package/src/modules/messages/components/message-item-end-of-scroll/message-item-end-of-scroll.tsx +14 -0
- package/src/modules/messages/components/message-item-error/index.ts +2 -0
- package/src/modules/messages/components/message-item-error/message-item-error.tsx +25 -0
- package/src/modules/messages/components/message-item-loading/index.ts +2 -0
- package/src/modules/messages/components/message-item-loading/message-item-loading.tsx +16 -0
- package/src/modules/messages/components/message-skeleton/index.ts +1 -0
- package/src/modules/messages/components/message-skeleton/message-skeleton.tsx +23 -0
- package/src/modules/messages/components/messages-list/index.ts +1 -1
- package/src/modules/messages/components/messages-list/messages-list.tsx +82 -39
- package/src/modules/messages/constants.ts +1 -0
- package/src/modules/messages/hooks/index.ts +5 -0
- package/src/modules/messages/hooks/use-all-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-all-messages/use-all-messages.tsx +30 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +65 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +81 -0
- package/src/modules/messages/hooks/use-manage-scroll/index.ts +2 -0
- package/src/modules/messages/hooks/use-manage-scroll/use-manage-scroll.tsx +70 -0
- package/src/modules/messages/hooks/use-send-text-message/index.ts +2 -0
- package/src/modules/messages/hooks/use-send-text-message/use-send-text-message.spec.tsx +86 -0
- package/src/modules/messages/hooks/use-send-text-message/use-send-text-message.tsx +60 -0
- package/src/modules/messages/hooks/use-skeleton-ref/index.ts +2 -0
- package/src/modules/messages/hooks/use-skeleton-ref/use-skeleton-ref.tsx +34 -0
- package/src/modules/messages/hooks/use-subscribe-message-received-event/index.ts +2 -0
- package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +80 -0
- package/src/modules/messages/service.ts +8 -7
- package/src/modules/messages/utils/has-to-update-cursor/has-to-update-cursor.spec.tsx +58 -0
- package/src/modules/messages/utils/has-to-update-cursor/has-to-update-cursor.ts +30 -0
- package/src/modules/messages/utils/has-to-update-cursor/index.ts +2 -0
- package/src/modules/sparkie/__tests__/sparkie.mock.ts +33 -0
- package/src/modules/sparkie/service.ts +182 -35
- package/src/modules/sparkie/types.ts +10 -2
- package/src/modules/widget/__tests__/widget-settings-props.builder.ts +29 -1
- package/src/modules/widget/components/ai-avatar/ai-avatar.tsx +11 -53
- package/src/modules/widget/components/chat-page/chat-page.spec.tsx +28 -0
- package/src/modules/widget/components/chat-page/chat-page.tsx +23 -4
- package/src/modules/widget/components/container/container.tsx +5 -19
- package/src/modules/widget/components/index.ts +1 -0
- package/src/modules/widget/components/scroll-to-bottom-button/index.ts +2 -0
- package/src/modules/widget/components/scroll-to-bottom-button/scroll-to-bottom-button.tsx +32 -0
- package/src/modules/widget/events.ts +4 -0
- package/src/modules/widget/hooks/use-init-sparkie/use-init-sparkie.tsx +8 -6
- package/src/modules/widget/store/index.ts +3 -0
- package/src/modules/widget/store/widget-container-intrinsic-height.atom.ts +13 -0
- package/src/modules/widget/store/widget-loading.atom.ts +11 -0
- package/src/modules/widget/store/widget-scrolling.atom.ts +11 -0
- package/src/modules/widget/store/widget-tabs.atom.ts +2 -1
- package/src/types.ts +4 -1
package/src/main/main.spec.tsx
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { chance, render, screen, waitFor } from '@/config/tests'
|
|
2
2
|
import WidgetSettingPropsBuilder from '../modules/widget/__tests__/widget-settings-props.builder'
|
|
3
|
+
import { useWidgetSettingsAtom } from '../modules/widget/store/widget-settings.atom'
|
|
3
4
|
import { Main } from '.'
|
|
4
5
|
|
|
6
|
+
vi.mock('../modules/widget/store/widget-settings.atom', () => ({ useWidgetSettingsAtom: vi.fn() }))
|
|
7
|
+
|
|
5
8
|
describe('Main', () => {
|
|
6
9
|
const defaultProps = new WidgetSettingPropsBuilder()
|
|
7
10
|
const renderComponent = (props = { settings: defaultProps }) => render(<Main {...props} />)
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
await waitFor(() => {
|
|
13
|
-
expect(container).toBeEmptyDOMElement()
|
|
14
|
-
})
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.mocked(useWidgetSettingsAtom).mockReturnValue([null, vi.fn()])
|
|
15
14
|
})
|
|
16
15
|
|
|
17
16
|
it('should render without errors', async () => {
|
|
18
17
|
const props = new WidgetSettingPropsBuilder().withTutorName(chance.name())
|
|
18
|
+
vi.mocked(useWidgetSettingsAtom).mockReturnValue([props, vi.fn()])
|
|
19
|
+
|
|
19
20
|
renderComponent({ settings: props })
|
|
20
21
|
|
|
21
22
|
await waitFor(() => {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { chance } from '@/src/config/tests'
|
|
2
|
+
import type { ICursorUpdate } from '../types'
|
|
3
|
+
|
|
4
|
+
class ICursorUpdateBuilder implements ICursorUpdate {
|
|
5
|
+
threadId: string
|
|
6
|
+
contactId: string
|
|
7
|
+
cursor: number
|
|
8
|
+
conversationId?: string
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.threadId = chance.guid()
|
|
12
|
+
this.contactId = chance.guid()
|
|
13
|
+
this.cursor = chance.integer()
|
|
14
|
+
this.conversationId = chance.guid()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
withThreadId(threadId: typeof this.threadId) {
|
|
18
|
+
this.threadId = threadId
|
|
19
|
+
|
|
20
|
+
return this
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
withContactId(contactId: typeof this.contactId) {
|
|
24
|
+
this.contactId = contactId
|
|
25
|
+
|
|
26
|
+
return this
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
withCursor(cursor: typeof this.cursor) {
|
|
30
|
+
this.cursor = cursor
|
|
31
|
+
|
|
32
|
+
return this
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
withConversationId(conversationId: typeof this.conversationId) {
|
|
36
|
+
this.conversationId = conversationId
|
|
37
|
+
|
|
38
|
+
return this
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default ICursorUpdateBuilder
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './use-update-cursor'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { chance, renderHook, waitFor } from '@/src/config/tests'
|
|
2
|
+
|
|
3
|
+
import useUpdateCursor from './use-update-cursor'
|
|
4
|
+
|
|
5
|
+
describe('useUpdateCursor', () => {
|
|
6
|
+
const conversationId = chance.guid()
|
|
7
|
+
const createHook = () => renderHook(useUpdateCursor)
|
|
8
|
+
|
|
9
|
+
it('should update the cursor given a conversationId', async () => {
|
|
10
|
+
const { result } = createHook()
|
|
11
|
+
|
|
12
|
+
result.current.mutate(conversationId)
|
|
13
|
+
|
|
14
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
|
15
|
+
|
|
16
|
+
expect(result.current.data).toMatchObject({
|
|
17
|
+
threadId: expect.any(String) as string,
|
|
18
|
+
contactId: expect.any(String) as string,
|
|
19
|
+
cursor: expect.any(Number) as number,
|
|
20
|
+
conversationId: expect.any(String) as string
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
import { CursorService } from '../..'
|
|
4
|
+
|
|
5
|
+
function useUpdateCursor() {
|
|
6
|
+
return useMutation({
|
|
7
|
+
mutationFn: (conversationId: string) => CursorService.updateCursor(conversationId)
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default useUpdateCursor
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SparkieService } from '../sparkie'
|
|
2
|
+
|
|
3
|
+
import type { ICursorUpdate } from './types'
|
|
4
|
+
|
|
5
|
+
class CursorService {
|
|
6
|
+
constructor(private sparkie = SparkieService.sparkieInstance) {}
|
|
7
|
+
|
|
8
|
+
async updateCursor(conversationId: string): Promise<ICursorUpdate | null> {
|
|
9
|
+
const data = await this.sparkie.cursorService?.update(conversationId)
|
|
10
|
+
|
|
11
|
+
return data ?? null
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default new CursorService()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { ParsedMessage } from '../types'
|
|
2
|
+
import { msgParser } from '../utils'
|
|
3
|
+
|
|
4
|
+
import IMessageWithSenderDataBuilder from './imessage-with-sender-data.builder'
|
|
5
|
+
|
|
6
|
+
const msg = new IMessageWithSenderDataBuilder()
|
|
7
|
+
const parsedMsg = msgParser(msg, msg.contactId)
|
|
8
|
+
|
|
9
|
+
class ParsedMessageBuilder implements ParsedMessage {
|
|
10
|
+
from: string
|
|
11
|
+
id: string
|
|
12
|
+
isRead: boolean
|
|
13
|
+
time: string
|
|
14
|
+
contact: { id: string; name?: string; picture?: string; userId?: number }
|
|
15
|
+
metadata: Record<string, unknown> & {
|
|
16
|
+
author: 'ai' | 'user'
|
|
17
|
+
sessionId: string
|
|
18
|
+
externalId: string
|
|
19
|
+
correlationId: string
|
|
20
|
+
}
|
|
21
|
+
parentId: string
|
|
22
|
+
sending: boolean
|
|
23
|
+
threadId: string
|
|
24
|
+
timestamp: number
|
|
25
|
+
type: string
|
|
26
|
+
text?: string
|
|
27
|
+
name?: string
|
|
28
|
+
url?: string
|
|
29
|
+
dimensions?: { width: number; height: number }
|
|
30
|
+
thumbnails?: { lg: string; md: string; sm: string }
|
|
31
|
+
caption?: string
|
|
32
|
+
loading?: boolean
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
this.from = parsedMsg.from
|
|
36
|
+
this.id = parsedMsg.id
|
|
37
|
+
this.isRead = parsedMsg.isRead
|
|
38
|
+
this.time = parsedMsg.time
|
|
39
|
+
this.contact = parsedMsg.contact
|
|
40
|
+
this.metadata = parsedMsg.metadata
|
|
41
|
+
this.parentId = parsedMsg.parentId ?? 'no-parent'
|
|
42
|
+
this.sending = parsedMsg.sending ?? false
|
|
43
|
+
this.threadId = parsedMsg.threadId
|
|
44
|
+
this.timestamp = parsedMsg.timestamp
|
|
45
|
+
this.type = parsedMsg.type
|
|
46
|
+
this.text = parsedMsg.text
|
|
47
|
+
this.name = parsedMsg.name
|
|
48
|
+
this.url = parsedMsg.url
|
|
49
|
+
this.dimensions = parsedMsg.dimensions
|
|
50
|
+
this.thumbnails = parsedMsg.thumbnails
|
|
51
|
+
this.caption = parsedMsg.caption
|
|
52
|
+
this.loading = parsedMsg.loading
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
withFrom(from: typeof this.from) {
|
|
56
|
+
this.from = from
|
|
57
|
+
|
|
58
|
+
return this
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
withId(id: typeof this.id) {
|
|
62
|
+
this.id = id
|
|
63
|
+
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
withIsRead(isRead: typeof this.isRead) {
|
|
68
|
+
this.isRead = isRead
|
|
69
|
+
|
|
70
|
+
return this
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
withTime(time: typeof this.time) {
|
|
74
|
+
this.time = time
|
|
75
|
+
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
withContact(contact: typeof this.contact) {
|
|
80
|
+
this.contact = contact
|
|
81
|
+
|
|
82
|
+
return this
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
withMetadata(metadata: typeof this.metadata) {
|
|
86
|
+
this.metadata = metadata
|
|
87
|
+
|
|
88
|
+
return this
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
withParentId(parentId: typeof this.parentId) {
|
|
92
|
+
this.parentId = parentId
|
|
93
|
+
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
withSending(sending: typeof this.sending) {
|
|
98
|
+
this.sending = sending
|
|
99
|
+
|
|
100
|
+
return this
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
withThreadId(threadId: typeof this.threadId) {
|
|
104
|
+
this.threadId = threadId
|
|
105
|
+
|
|
106
|
+
return this
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
withTimestamp(timestamp: typeof this.timestamp) {
|
|
110
|
+
this.timestamp = timestamp
|
|
111
|
+
|
|
112
|
+
return this
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
withType(type: typeof this.type) {
|
|
116
|
+
this.type = type
|
|
117
|
+
|
|
118
|
+
return this
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
withText(text: typeof this.text) {
|
|
122
|
+
this.text = text
|
|
123
|
+
|
|
124
|
+
return this
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
withName(name: typeof this.name) {
|
|
128
|
+
this.name = name
|
|
129
|
+
|
|
130
|
+
return this
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
withUrl(url: typeof this.url) {
|
|
134
|
+
this.url = url
|
|
135
|
+
|
|
136
|
+
return this
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
withDimensions(dimensions: typeof this.dimensions) {
|
|
140
|
+
this.dimensions = dimensions
|
|
141
|
+
|
|
142
|
+
return this
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
withThumbnails(thumbnails: typeof this.thumbnails) {
|
|
146
|
+
this.thumbnails = thumbnails
|
|
147
|
+
|
|
148
|
+
return this
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
withCaption(caption: typeof this.caption) {
|
|
152
|
+
this.caption = caption
|
|
153
|
+
|
|
154
|
+
return this
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
withLoading(loading: typeof this.loading) {
|
|
158
|
+
this.loading = loading
|
|
159
|
+
|
|
160
|
+
return this
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default ParsedMessageBuilder
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { chance, fireEvent, render, screen } from '@/src/config/tests'
|
|
4
|
+
|
|
5
|
+
import ChatInput from './chat-input'
|
|
6
|
+
import { useChatInputValueAtom } from './chat-input.atom'
|
|
7
|
+
|
|
8
|
+
vi.mock('./chat-input.atom', () => ({ useChatInputValueAtom: vi.fn() }))
|
|
9
|
+
|
|
10
|
+
describe('ChatInput', () => {
|
|
11
|
+
const ref = createRef<HTMLInputElement>()
|
|
12
|
+
const chatInputValueAtomMock = { val: '', setVal: vi.fn() }
|
|
13
|
+
const defaultProps = { name: chance.name() }
|
|
14
|
+
|
|
15
|
+
const renderComponent = (props = defaultProps) => render(<ChatInput {...props} ref={ref} />)
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.mocked(useChatInputValueAtom).mockReturnValue([
|
|
19
|
+
chatInputValueAtomMock.val,
|
|
20
|
+
chatInputValueAtomMock.setVal
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should call focus when rendering the input', () => {
|
|
25
|
+
renderComponent()
|
|
26
|
+
|
|
27
|
+
expect(ref.current).toHaveFocus()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should call setValue when ref change event is called', () => {
|
|
31
|
+
renderComponent()
|
|
32
|
+
|
|
33
|
+
expect(ref.current).not.toBeNull()
|
|
34
|
+
|
|
35
|
+
const event = { target: { value: 'Test message' } }
|
|
36
|
+
|
|
37
|
+
fireEvent.change(ref.current!, event)
|
|
38
|
+
|
|
39
|
+
expect(chatInputValueAtomMock.setVal).toHaveBeenCalledTimes(1)
|
|
40
|
+
expect(chatInputValueAtomMock.setVal).toHaveBeenNthCalledWith(1, event.target.value)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should call onSend prop when it is defined and user presses the EnterKey', () => {
|
|
44
|
+
const onSend = vi.fn()
|
|
45
|
+
renderComponent({ ...defaultProps, onSend } as never)
|
|
46
|
+
|
|
47
|
+
expect(ref.current).not.toBeNull()
|
|
48
|
+
|
|
49
|
+
const event = { code: 'Enter' }
|
|
50
|
+
|
|
51
|
+
fireEvent.keyDown(ref.current!, event)
|
|
52
|
+
|
|
53
|
+
expect(onSend).toHaveBeenCalledTimes(1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should disable the button when there is no value', () => {
|
|
57
|
+
renderComponent()
|
|
58
|
+
|
|
59
|
+
expect(screen.getByRole('button', { name: /Submit Button/i })).toBeDisabled()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should the button be enabled and input have defaultValue when there is value', () => {
|
|
63
|
+
const name = chance.name()
|
|
64
|
+
vi.mocked(useChatInputValueAtom).mockReturnValue([name, vi.fn()])
|
|
65
|
+
|
|
66
|
+
renderComponent()
|
|
67
|
+
|
|
68
|
+
expect(ref.current?.defaultValue).toBe(name)
|
|
69
|
+
|
|
70
|
+
expect(screen.getByRole('button', { name: /Submit Button/i })).toBeEnabled()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { forwardRef } from 'react'
|
|
1
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import type { ChangeEvent, KeyboardEvent } from 'react'
|
|
2
4
|
import { useTranslation } from 'react-i18next'
|
|
3
5
|
|
|
4
6
|
import { Icon } from '@/src/lib/components'
|
|
@@ -6,9 +8,34 @@ import { Icon } from '@/src/lib/components'
|
|
|
6
8
|
import { useChatInputValueAtom } from './chat-input.atom'
|
|
7
9
|
import type { ChatInputProps } from './types'
|
|
8
10
|
|
|
9
|
-
const ChatInput = forwardRef<HTMLInputElement, ChatInputProps>(
|
|
11
|
+
const ChatInput = forwardRef<HTMLInputElement, ChatInputProps>(({ name, onSend }, forwardedRef) => {
|
|
10
12
|
const { t } = useTranslation()
|
|
11
|
-
const [value] = useChatInputValueAtom()
|
|
13
|
+
const [value, setValue] = useChatInputValueAtom()
|
|
14
|
+
const ref = useRef<HTMLInputElement>(null)
|
|
15
|
+
|
|
16
|
+
useImperativeHandle(forwardedRef, () => ref?.current as HTMLInputElement)
|
|
17
|
+
|
|
18
|
+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
19
|
+
setValue(e.target.value?.trim())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
23
|
+
const isEnterKey = e.code === 'Enter'
|
|
24
|
+
const isShiftKey = e.shiftKey
|
|
25
|
+
|
|
26
|
+
if (isEnterKey && !isShiftKey) {
|
|
27
|
+
e.preventDefault()
|
|
28
|
+
onSend?.()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const input = ref?.current
|
|
34
|
+
|
|
35
|
+
if (input) {
|
|
36
|
+
input.focus()
|
|
37
|
+
}
|
|
38
|
+
}, [])
|
|
12
39
|
|
|
13
40
|
return (
|
|
14
41
|
<div className='flex items-center rounded-full border-neutral-800 bg-neutral-800 px-4 py-2'>
|
|
@@ -17,12 +44,31 @@ const ChatInput = forwardRef<HTMLInputElement, ChatInputProps>(function ({ name,
|
|
|
17
44
|
name={name}
|
|
18
45
|
ref={ref}
|
|
19
46
|
type='text'
|
|
20
|
-
className='h-6 w-full border-none bg-transparent text-neutral-400 outline-0 placeholder:text-neutral-400'
|
|
47
|
+
className='h-6 w-full border-none bg-transparent text-neutral-400 outline-none outline-0 placeholder:text-neutral-400'
|
|
21
48
|
placeholder={t('send_message.field.placeholder')}
|
|
22
49
|
defaultValue={value}
|
|
50
|
+
onChange={handleChange}
|
|
51
|
+
onKeyDown={handleKeyDown}
|
|
23
52
|
/>
|
|
24
|
-
<button
|
|
25
|
-
|
|
53
|
+
<button
|
|
54
|
+
onClick={onSend}
|
|
55
|
+
disabled={!value}
|
|
56
|
+
className={clsx(
|
|
57
|
+
'flex size-8 flex-col items-center justify-center rounded-full outline-none transition-colors duration-300 ease-in',
|
|
58
|
+
{
|
|
59
|
+
'cursor-pointer hover:scale-110 hover:bg-neutral-600 focus:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-2':
|
|
60
|
+
Boolean(value),
|
|
61
|
+
'cursor-not-allowed': !value
|
|
62
|
+
}
|
|
63
|
+
)}
|
|
64
|
+
aria-label='Submit Button'>
|
|
65
|
+
<Icon
|
|
66
|
+
name='send'
|
|
67
|
+
className={clsx('h-4 w-4 pr-0.5 pt-0.5 transition-colors duration-150', {
|
|
68
|
+
'text-neutral-50': Boolean(value),
|
|
69
|
+
'text-neutral-500': !value
|
|
70
|
+
})}
|
|
71
|
+
/>
|
|
26
72
|
</button>
|
|
27
73
|
</div>
|
|
28
74
|
)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { render, screen } from '@/src/config/tests'
|
|
2
2
|
import { TEST_MARKDOWN_STUB } from '@/src/lib/components/markdownrenderer/__tests__/markdown.stub'
|
|
3
|
-
import
|
|
3
|
+
import ParsedMessageBuilder from '../../__tests__/parsed-message.builder'
|
|
4
4
|
|
|
5
5
|
import MessageItem from './message-item'
|
|
6
6
|
|
|
7
7
|
describe('MessageItem', () => {
|
|
8
|
-
const message =
|
|
8
|
+
const message = new ParsedMessageBuilder().withText(TEST_MARKDOWN_STUB)
|
|
9
9
|
|
|
10
10
|
const renderComponent = (props = { message }) => render(<MessageItem {...props} />)
|
|
11
11
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
1
2
|
import type { Components } from 'react-markdown'
|
|
2
3
|
|
|
3
4
|
import { MarkdownRenderer } from '@/src/lib/components'
|
|
@@ -9,7 +10,19 @@ const imgComponent: Components['img'] = ({ src }) => {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
function MessageItem({ message }: { message: ParsedMessage }) {
|
|
12
|
-
return
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
data-test='messages-item'
|
|
16
|
+
className={clsx(
|
|
17
|
+
'max-w-[min(80%,52rem)] overflow-x-hidden rounded-lg px-3 text-sm/normal text-neutral-50',
|
|
18
|
+
{
|
|
19
|
+
'self-end bg-neutral-800': message.metadata.author === 'user',
|
|
20
|
+
'bg-ai-chat-response': message.metadata.author === 'ai'
|
|
21
|
+
}
|
|
22
|
+
)}>
|
|
23
|
+
<MarkdownRenderer content={message?.text ?? message?.name} imgComponent={imgComponent} />
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
export default MessageItem
|
package/src/modules/messages/components/message-item-end-of-scroll/message-item-end-of-scroll.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type MessageItemEndOfScrollProps = { show?: boolean }
|
|
2
|
+
|
|
3
|
+
// TODO: [PLACEHOLDER] Refactor using the PD choice for End of Scroll
|
|
4
|
+
function MessageItemEndOfScroll({ show = true }: MessageItemEndOfScrollProps) {
|
|
5
|
+
if (!show) return null
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className='rounded bg-gray-800/50 p-4 text-center text-gray-400'>
|
|
9
|
+
🎉 You've reached the beginning of the conversation!
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default MessageItemEndOfScroll
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MouseEventHandler, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type MessageItemErrorProps = {
|
|
4
|
+
message?: ReactNode
|
|
5
|
+
retry?: MouseEventHandler<HTMLButtonElement>
|
|
6
|
+
show?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// TODO: [PLACEHOLDER] Refactor using the PD choice for Error Handling
|
|
10
|
+
function MessageItemError({ message, retry, show = true }: MessageItemErrorProps) {
|
|
11
|
+
if (!show) return null
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className='rounded bg-red-900/20 p-4 text-center text-red-400'>
|
|
15
|
+
{message}
|
|
16
|
+
<button
|
|
17
|
+
onClick={retry}
|
|
18
|
+
className='ml-2 cursor-pointer rounded bg-danger-600 px-3 py-1 text-sm text-danger-100 transition-colors hover:bg-danger-700'>
|
|
19
|
+
Retry
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default MessageItemError
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Spinner } from '@/src/lib/components'
|
|
2
|
+
|
|
3
|
+
export type MessageItemLoadingProps = { show?: boolean }
|
|
4
|
+
|
|
5
|
+
// TODO: [PLACEHOLDER] Refactor using the PD choice for loading new messages
|
|
6
|
+
function MessageItemLoading({ show = true }: MessageItemLoadingProps) {
|
|
7
|
+
if (!show) return null
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className='flex flex-col items-center justify-center p-8'>
|
|
11
|
+
<Spinner className='inline-flex h-6 w-6 animate-spin text-neutral-200' />
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default MessageItemLoading
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as MessageSkeleton } from './message-skeleton'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { AIAvatarIcon } from '@/src/modules/widget'
|
|
4
|
+
|
|
5
|
+
const MessageSkeleton = forwardRef<HTMLDivElement>((_, ref) => {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
ref={ref}
|
|
9
|
+
className='flex max-w-[86%] flex-col items-start gap-2'
|
|
10
|
+
aria-label='Loading Component'>
|
|
11
|
+
<AIAvatarIcon className='rounded-lg bg-ai-chat-response' />
|
|
12
|
+
<div className='flex w-full flex-col items-start gap-2'>
|
|
13
|
+
<div className='h-3 w-full animate-pulse rounded-full bg-neutral-800 transition-colors' />
|
|
14
|
+
<div className='h-3 w-[83%] animate-pulse rounded-full bg-neutral-800 transition-colors delay-75' />
|
|
15
|
+
<div className='h-3 w-[56%] animate-pulse rounded-full bg-neutral-800 transition-colors delay-100' />
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
MessageSkeleton.displayName = 'MessageSkeleton'
|
|
22
|
+
|
|
23
|
+
export default MessageSkeleton
|