app-tutor-ai-consumer 1.33.1 → 1.34.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 +31 -0
- package/config/vitest/__mocks__/sparkie.tsx +2 -2
- package/package.json +2 -2
- package/src/@types/index.d.ts +3 -2
- package/src/bootstrap.ts +40 -0
- package/src/config/tanstack/query-provider.tsx +7 -3
- package/src/config/tests/handlers.ts +5 -4
- package/src/config/theme/init-theme.ts +11 -5
- package/src/index.backup.tsx +61 -0
- package/src/index.tsx +80 -17
- package/src/lib/components/dropdown-actions/dropdown-actions.tsx +87 -0
- package/src/lib/components/dropdown-actions/dropdownActions.builder.ts +58 -0
- package/src/lib/components/dropdown-actions/dropdownActions.spec.tsx +76 -0
- package/src/lib/components/dropdown-actions/index.ts +1 -0
- package/src/lib/components/dropdown-actions/types.ts +16 -0
- package/src/lib/components/errors/generic/generic-error.tsx +11 -8
- package/src/lib/components/icons/document.svg +3 -0
- package/src/lib/components/icons/file.svg +3 -0
- package/src/lib/components/icons/icon-names.d.ts +7 -0
- package/src/lib/components/icons/image.svg +3 -0
- package/src/lib/components/icons/pdf.svg +3 -0
- package/src/lib/components/icons/plus.svg +3 -0
- package/src/lib/components/icons/retry.svg +3 -0
- package/src/lib/components/icons/spreadsheet.svg +3 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdownrenderer/markdownrenderer.tsx +1 -3
- package/src/lib/hooks/index.ts +1 -0
- package/src/lib/hooks/use-chat-file-upload/constants.ts +11 -0
- package/src/lib/hooks/use-chat-file-upload/index.ts +1 -0
- package/src/lib/hooks/use-chat-file-upload/types.ts +14 -0
- package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.spec.ts +59 -0
- package/src/lib/hooks/use-chat-file-upload/use-chat-file-upload.ts +28 -0
- package/src/lib/hooks/use-click-outside/index.ts +1 -0
- package/src/lib/hooks/use-click-outside/use-click-outside.tsx +23 -0
- package/src/lib/hooks/use-click-outside/useClickOutside.spec.ts +102 -0
- package/src/lib/utils/index.ts +1 -0
- package/src/lib/utils/is-theme-dark.ts +21 -0
- package/src/main/hooks/use-initial-store/index.ts +1 -0
- package/src/main/hooks/use-initial-store/use-initial-store.tsx +64 -0
- package/src/main/hooks/use-initial-tab/index.ts +1 -0
- package/src/main/hooks/use-initial-tab/use-initial-tab.tsx +84 -0
- package/src/main/index.ts +1 -0
- package/src/main/main-content.tsx +14 -0
- package/src/main/main-wrapper.tsx +16 -0
- package/src/main/main.spec.tsx +5 -3
- package/src/main/main.tsx +7 -16
- package/src/main/types.ts +5 -0
- package/src/modules/global-providers/global-providers.tsx +2 -21
- package/src/modules/messages/__tests__/imessage-with-sender-data.builder.ts +1 -1
- package/src/modules/messages/__tests__/signed-urls.builder.ts +42 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.builder.ts +121 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.spec.tsx +107 -0
- package/src/modules/messages/components/chat-file-preview/chat-file-preview.tsx +45 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/close-button.tsx +31 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/close-button/types.ts +4 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/document-preview.tsx +63 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/document-preview/types.ts +10 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/error-preview.tsx +37 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/error-preview/types.ts +4 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/image-preview.tsx +32 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/components/image-preview/types.ts +6 -0
- package/src/modules/messages/components/chat-file-preview/constants.ts +22 -0
- package/src/modules/messages/components/chat-file-preview/index.ts +1 -0
- package/src/modules/messages/components/chat-file-preview/types.ts +13 -0
- package/src/modules/messages/components/chat-file-preview/utils.spec.ts +38 -0
- package/src/modules/messages/components/chat-file-preview/utils.ts +13 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.builder.ts +19 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.spec.tsx +58 -0
- package/src/modules/messages/components/chat-file-uploader/chat-file-uploader.tsx +80 -0
- package/src/modules/messages/components/chat-file-uploader/index.ts +1 -0
- package/src/modules/messages/components/chat-file-uploader/types.ts +4 -0
- package/src/modules/messages/components/index.ts +1 -0
- package/src/modules/messages/constants.ts +2 -1
- package/src/modules/messages/hooks/use-get-signed-urls/index.ts +1 -0
- package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.spec.tsx +27 -0
- package/src/modules/messages/hooks/use-get-signed-urls/use-get-signed-urls.tsx +38 -0
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +1 -2
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +3 -8
- package/src/modules/messages/hooks/use-prefetch-messages/index.ts +1 -0
- package/src/modules/messages/hooks/use-prefetch-messages/use-prefetch-messages.tsx +28 -0
- package/src/modules/messages/hooks/use-suspense-messages/index.ts +2 -0
- package/src/modules/messages/hooks/use-suspense-messages/types.ts +4 -0
- package/src/modules/messages/hooks/use-suspense-messages/use-suspense-messages.tsx +21 -0
- package/src/modules/messages/service.direct.ts +19 -1
- package/src/modules/messages/service.ts +1 -1
- package/src/modules/messages/store/messages-max-count.atom.ts +2 -2
- package/src/modules/messages/types.ts +15 -1
- package/src/modules/messages/utils/set-messages-cache/utils.ts +1 -1
- package/src/modules/profile/hooks/use-get-profile/use-get-profile.tsx +7 -6
- package/src/modules/sparkie/__tests__/sparkie.mock.ts +1 -4
- package/src/modules/sparkie/hooks/use-init-sparkie/use-init-sparkie.tsx +34 -28
- package/src/modules/sparkie/service.ts +1 -1
- package/src/modules/sparkie/store/index.ts +1 -0
- package/src/modules/sparkie/store/sparkie-state.atom.ts +13 -0
- package/src/modules/widget/__tests__/widget-settings-props.builder.ts +20 -1
- package/src/modules/widget/components/constants.tsx +3 -1
- package/src/modules/widget/components/error-page/error-page.spec.tsx +17 -0
- package/src/modules/widget/components/error-page/error-page.tsx +10 -0
- package/src/modules/widget/components/error-page/index.ts +1 -0
- package/src/modules/widget/components/loading-page/loading-page.tsx +9 -15
- package/src/modules/widget/components/starter-page/starter-page-actions/index.ts +1 -0
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.spec.tsx +68 -0
- package/src/modules/widget/components/starter-page/starter-page-actions/starter-page-actions.tsx +34 -0
- package/src/modules/widget/components/starter-page/starter-page-content/index.ts +1 -0
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.spec.tsx +16 -0
- package/src/modules/widget/components/starter-page/starter-page-content/starter-page-content.tsx +28 -0
- package/src/modules/widget/components/starter-page/starter-page-header/index.ts +1 -0
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.spec.tsx +45 -0
- package/src/modules/widget/components/starter-page/starter-page-header/starter-page-header.tsx +36 -0
- package/src/modules/widget/components/starter-page/starter-page.spec.tsx +13 -3
- package/src/modules/widget/components/starter-page/starter-page.tsx +15 -109
- package/src/modules/widget/hooks/index.ts +0 -1
- package/src/modules/widget/hooks/use-listen-to-theme-change-event/use-listen-to-theme-change-event.tsx +8 -6
- package/src/modules/widget/store/create-store.ts +7 -0
- package/src/modules/widget/store/index.ts +1 -0
- package/src/modules/widget/store/widget-settings-config.atom.ts +1 -6
- package/src/modules/widget/store/widget-tabs.atom.ts +17 -6
- package/src/types.ts +10 -0
- package/src/wrapper.tsx +52 -0
- package/src/modules/widget/hooks/use-init-widget/index.ts +0 -1
- package/src/modules/widget/hooks/use-init-widget/use-init-widget.tsx +0 -56
|
@@ -1,31 +1,12 @@
|
|
|
1
|
-
import { type PropsWithChildren
|
|
2
|
-
import { v4 } from 'uuid'
|
|
1
|
+
import { type PropsWithChildren } from 'react'
|
|
3
2
|
|
|
4
3
|
import { OptimizelyProvider } from '@/src/config/optimizely'
|
|
5
|
-
import { QueryProvider } from '@/src/config/tanstack'
|
|
6
|
-
import { useWidgetSettingsAtom } from '@/src/modules/widget'
|
|
7
4
|
import type { WidgetSettingProps } from '@/src/types'
|
|
8
5
|
|
|
9
6
|
export type GlobalProvidersProps = PropsWithChildren<{ settings: WidgetSettingProps }>
|
|
10
7
|
|
|
11
8
|
function GlobalProviders({ children, settings }: GlobalProvidersProps) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (!settings || !Object.keys(settings)?.length) return
|
|
16
|
-
|
|
17
|
-
if (!settings?.sessionId) {
|
|
18
|
-
settings.sessionId = v4()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
setWidgetSettings(settings)
|
|
22
|
-
}, [setWidgetSettings, settings])
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<OptimizelyProvider settings={settings}>
|
|
26
|
-
<QueryProvider>{children}</QueryProvider>
|
|
27
|
-
</OptimizelyProvider>
|
|
28
|
-
)
|
|
9
|
+
return <OptimizelyProvider settings={settings}>{children}</OptimizelyProvider>
|
|
29
10
|
}
|
|
30
11
|
|
|
31
12
|
export default GlobalProviders
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { chance } from '@/src/config/tests'
|
|
2
|
+
import type { IGetSignedUrlsResponse } from '../types'
|
|
3
|
+
|
|
4
|
+
class SignedUrlsResponseBuilder implements IGetSignedUrlsResponse {
|
|
5
|
+
uploadUrl: string
|
|
6
|
+
futureDownloadUrl: string
|
|
7
|
+
imagePreviewUrl: string
|
|
8
|
+
fileNameWithExtension: string
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.uploadUrl = chance.url()
|
|
12
|
+
this.futureDownloadUrl = chance.url()
|
|
13
|
+
this.imagePreviewUrl = chance.url()
|
|
14
|
+
this.fileNameWithExtension = chance.string()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
withUploadUrl(uploadUrl: typeof this.uploadUrl) {
|
|
18
|
+
this.uploadUrl = uploadUrl
|
|
19
|
+
|
|
20
|
+
return this
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
withFutureDownloadUrl(futureDownloadUrl: typeof this.futureDownloadUrl) {
|
|
24
|
+
this.futureDownloadUrl = futureDownloadUrl
|
|
25
|
+
|
|
26
|
+
return this
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
withImagePreviewUrl(imagePreviewUrl: typeof this.imagePreviewUrl) {
|
|
30
|
+
this.imagePreviewUrl = imagePreviewUrl
|
|
31
|
+
|
|
32
|
+
return this
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
withFileNameWithExtension(fileNameWithExtension: typeof this.fileNameWithExtension) {
|
|
36
|
+
this.fileNameWithExtension = fileNameWithExtension
|
|
37
|
+
|
|
38
|
+
return this
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default SignedUrlsResponseBuilder
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { chance } from '@/src/config/tests'
|
|
2
|
+
|
|
3
|
+
import type { FilePreviewProps, FilePreviewTypes } from './types'
|
|
4
|
+
|
|
5
|
+
class ChatFilePreviewBuilder implements FilePreviewProps {
|
|
6
|
+
name: string
|
|
7
|
+
size: number
|
|
8
|
+
type: FilePreviewTypes
|
|
9
|
+
showCloseButton: boolean
|
|
10
|
+
hasError: boolean
|
|
11
|
+
isLoading: boolean
|
|
12
|
+
imageUrl?: string
|
|
13
|
+
onClose?: () => void
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this.name = chance.word() + '.pdf'
|
|
17
|
+
this.size = chance.integer({ min: 1000, max: 10000000 })
|
|
18
|
+
this.type = 'pdf'
|
|
19
|
+
this.showCloseButton = false
|
|
20
|
+
this.hasError = false
|
|
21
|
+
this.isLoading = false
|
|
22
|
+
this.onClose = vi.fn()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
withName(name: typeof this.name) {
|
|
26
|
+
this.name = name
|
|
27
|
+
|
|
28
|
+
return this
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
withSize(size: typeof this.size) {
|
|
32
|
+
this.size = size
|
|
33
|
+
|
|
34
|
+
return this
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
withType(type: typeof this.type) {
|
|
38
|
+
this.type = type
|
|
39
|
+
|
|
40
|
+
return this
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
withShowCloseButton(showCloseButton: typeof this.showCloseButton) {
|
|
44
|
+
this.showCloseButton = showCloseButton
|
|
45
|
+
|
|
46
|
+
return this
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
withImageUrl(imageUrl: typeof this.imageUrl) {
|
|
50
|
+
this.imageUrl = imageUrl
|
|
51
|
+
|
|
52
|
+
return this
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
withHasError(hasError: typeof this.hasError) {
|
|
56
|
+
this.hasError = hasError
|
|
57
|
+
|
|
58
|
+
return this
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
withOnClose(onClose: typeof this.onClose) {
|
|
62
|
+
this.onClose = onClose
|
|
63
|
+
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
withIsLoading(isLoading: typeof this.isLoading) {
|
|
68
|
+
this.isLoading = isLoading
|
|
69
|
+
|
|
70
|
+
return this
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
asPdf() {
|
|
74
|
+
this.type = 'pdf'
|
|
75
|
+
this.name = chance.word() + '.pdf'
|
|
76
|
+
|
|
77
|
+
return this
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
asDocument() {
|
|
81
|
+
this.type = 'document'
|
|
82
|
+
this.name = chance.word() + '.docx'
|
|
83
|
+
|
|
84
|
+
return this
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
asSpreadsheet() {
|
|
88
|
+
this.type = 'spreadsheet'
|
|
89
|
+
this.name = chance.word() + '.xlsx'
|
|
90
|
+
|
|
91
|
+
return this
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
asImage() {
|
|
95
|
+
this.type = 'image'
|
|
96
|
+
this.name = chance.word() + '.jpg'
|
|
97
|
+
this.imageUrl = chance.url()
|
|
98
|
+
|
|
99
|
+
return this
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
withError() {
|
|
103
|
+
this.hasError = true
|
|
104
|
+
|
|
105
|
+
return this
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
withLoading() {
|
|
109
|
+
this.isLoading = true
|
|
110
|
+
|
|
111
|
+
return this
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
withCloseButton() {
|
|
115
|
+
this.showCloseButton = true
|
|
116
|
+
|
|
117
|
+
return this
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default ChatFilePreviewBuilder
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { render, screen } from '@/src/config/tests'
|
|
2
|
+
import { useMediaQuery } from '@/src/lib/hooks'
|
|
3
|
+
import * as WidgetStore from '@/src/modules/widget/store'
|
|
4
|
+
|
|
5
|
+
import ChatFilePreview from './chat-file-preview'
|
|
6
|
+
import ChatFilePreviewBuilder from './chat-file-preview.builder'
|
|
7
|
+
|
|
8
|
+
vi.mock('@/src/lib/hooks', async () => ({
|
|
9
|
+
...(await vi.importActual('@/src/lib/hooks')),
|
|
10
|
+
useMediaQuery: vi.fn()
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
describe('ChatFilePreview', () => {
|
|
14
|
+
const mockSettings = {
|
|
15
|
+
config: {
|
|
16
|
+
theme: 'dark'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultProps = new ChatFilePreviewBuilder()
|
|
21
|
+
|
|
22
|
+
const renderComponent = (props = defaultProps) => render(<ChatFilePreview {...props} />)
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.spyOn(WidgetStore, 'useWidgetSettingsAtom').mockReturnValue([mockSettings, vi.fn()] as never)
|
|
26
|
+
vi.mocked(useMediaQuery).mockReturnValue(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.clearAllMocks()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('Error state', () => {
|
|
34
|
+
it('should render error message when hasError is true', () => {
|
|
35
|
+
renderComponent(new ChatFilePreviewBuilder().withError())
|
|
36
|
+
|
|
37
|
+
expect(screen.getByText(/send_message.file_upload.error.load_file/i)).toBeInTheDocument()
|
|
38
|
+
expect(screen.getByText(/general.buttons.try_again/i)).toBeInTheDocument()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should render spinner when hasError and isLoading are both true', () => {
|
|
42
|
+
renderComponent(new ChatFilePreviewBuilder().withError().withLoading())
|
|
43
|
+
|
|
44
|
+
expect(screen.queryByText(/general.buttons.try_again/i)).not.toBeInTheDocument()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('Image type', () => {
|
|
49
|
+
it('should render image when type is image', () => {
|
|
50
|
+
const builder = new ChatFilePreviewBuilder().asImage()
|
|
51
|
+
renderComponent(builder)
|
|
52
|
+
|
|
53
|
+
const image = screen.getByRole('img', { name: builder.name })
|
|
54
|
+
expect(image).toHaveAttribute('src', builder.imageUrl)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('Document types (PDF, document, spreadsheet)', () => {
|
|
59
|
+
it('should render PDF file with correct icon and label', () => {
|
|
60
|
+
renderComponent()
|
|
61
|
+
|
|
62
|
+
expect(screen.getByText(defaultProps.name)).toBeInTheDocument()
|
|
63
|
+
expect(screen.getByText(/file_upload.file_type.pdf/i)).toBeInTheDocument()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should render document file with correct icon and label', () => {
|
|
67
|
+
const builder = new ChatFilePreviewBuilder().asDocument().withName('file.docx')
|
|
68
|
+
renderComponent(builder)
|
|
69
|
+
|
|
70
|
+
expect(screen.getByText('file.docx')).toBeInTheDocument()
|
|
71
|
+
expect(screen.getByText(/file_upload.file_type.document/i)).toBeInTheDocument()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should render spreadsheet file with correct icon and label', () => {
|
|
75
|
+
const builder = new ChatFilePreviewBuilder().asSpreadsheet().withName('data.xlsx')
|
|
76
|
+
renderComponent(builder)
|
|
77
|
+
|
|
78
|
+
expect(screen.getByText('data.xlsx')).toBeInTheDocument()
|
|
79
|
+
expect(screen.getByText(/file_upload.file_type.spreadsheet/i)).toBeInTheDocument()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should render spinner when isLoading is true', () => {
|
|
83
|
+
renderComponent(new ChatFilePreviewBuilder().withLoading())
|
|
84
|
+
|
|
85
|
+
expect(screen.queryByText(/file_upload.file_type/i)).toBeInTheDocument()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('Close button behavior', () => {
|
|
90
|
+
it('should call onClose when close button is clicked', async () => {
|
|
91
|
+
const props = new ChatFilePreviewBuilder().withCloseButton()
|
|
92
|
+
const { user } = renderComponent(props)
|
|
93
|
+
|
|
94
|
+
await user.click(screen.getByRole('button', { name: 'general.buttons.remove_attachment' }))
|
|
95
|
+
|
|
96
|
+
expect(props.onClose).toHaveBeenCalledTimes(1)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should not render close button when showCloseButton is false', () => {
|
|
100
|
+
renderComponent()
|
|
101
|
+
|
|
102
|
+
expect(
|
|
103
|
+
screen.queryByRole('button', { name: 'general.buttons.remove_attachment' })
|
|
104
|
+
).not.toBeInTheDocument()
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DocumentPreview } from './components/document-preview'
|
|
2
|
+
import { ErrorPreview } from './components/error-preview'
|
|
3
|
+
import { ImagePreview } from './components/image-preview'
|
|
4
|
+
import type { FilePreviewProps } from './types'
|
|
5
|
+
|
|
6
|
+
function ChatFilePreview({
|
|
7
|
+
name,
|
|
8
|
+
size,
|
|
9
|
+
type,
|
|
10
|
+
showCloseButton = false,
|
|
11
|
+
onClose,
|
|
12
|
+
imageUrl,
|
|
13
|
+
onRetry,
|
|
14
|
+
hasError = false,
|
|
15
|
+
isLoading = false
|
|
16
|
+
}: FilePreviewProps) {
|
|
17
|
+
if (hasError) {
|
|
18
|
+
return <ErrorPreview isLoading={isLoading} onRetry={onRetry} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (type === 'image') {
|
|
22
|
+
return (
|
|
23
|
+
<ImagePreview
|
|
24
|
+
imageUrl={imageUrl}
|
|
25
|
+
name={name}
|
|
26
|
+
isLoading={isLoading}
|
|
27
|
+
showCloseButton={showCloseButton}
|
|
28
|
+
onClose={onClose}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<DocumentPreview
|
|
35
|
+
name={name}
|
|
36
|
+
size={size}
|
|
37
|
+
type={type}
|
|
38
|
+
isLoading={isLoading}
|
|
39
|
+
showCloseButton={showCloseButton}
|
|
40
|
+
onClose={onClose}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default ChatFilePreview
|
package/src/modules/messages/components/chat-file-preview/components/close-button/close-button.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
import { Icon } from '@/src/lib/components/icons'
|
|
5
|
+
|
|
6
|
+
import type { CloseButtonProps } from './types'
|
|
7
|
+
|
|
8
|
+
export default function CloseButton({ onClick, variant = 'overlay' }: CloseButtonProps) {
|
|
9
|
+
const { t } = useTranslation()
|
|
10
|
+
const baseClasses =
|
|
11
|
+
'flex items-center justify-center transition-colors duration-150 outline-none focus:outline-none'
|
|
12
|
+
const variantClasses = {
|
|
13
|
+
overlay: 'absolute right-1 top-1 h-6 w-6 rounded-full bg-white shadow-sm hover:bg-neutral-100',
|
|
14
|
+
inline: 'ml-7 shrink-0 cursor-pointer text-neutral-900 hover:text-neutral-500'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
onClick={onClick}
|
|
20
|
+
className={clsx(baseClasses, variantClasses[variant])}
|
|
21
|
+
aria-label={t('general.buttons.remove_attachment')}>
|
|
22
|
+
<Icon
|
|
23
|
+
name='close'
|
|
24
|
+
className={clsx('h-3 w-3', {
|
|
25
|
+
'text-neutral-900': variant === 'overlay',
|
|
26
|
+
'mr-3': variant === 'inline'
|
|
27
|
+
})}
|
|
28
|
+
/>
|
|
29
|
+
</button>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CloseButton } from './close-button'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
import { Icon, Spinner } from '@/src/lib/components'
|
|
5
|
+
import { useMediaQuery } from '@/src/lib/hooks'
|
|
6
|
+
import { useWidgetSettingsAtom } from '@/src/modules/widget/store'
|
|
7
|
+
import { CHAT_FILE_PREVIEW_CONFIG } from '../../constants'
|
|
8
|
+
import { formatFileSize } from '../../utils'
|
|
9
|
+
import { CloseButton } from '../close-button'
|
|
10
|
+
|
|
11
|
+
import type { DocumentPreviewProps } from './types'
|
|
12
|
+
|
|
13
|
+
export default function DocumentPreview({
|
|
14
|
+
name,
|
|
15
|
+
size,
|
|
16
|
+
type,
|
|
17
|
+
isLoading,
|
|
18
|
+
showCloseButton,
|
|
19
|
+
onClose
|
|
20
|
+
}: DocumentPreviewProps) {
|
|
21
|
+
const { t } = useTranslation()
|
|
22
|
+
const [settings] = useWidgetSettingsAtom()
|
|
23
|
+
const isMobile = useMediaQuery({ maxSize: 'md' })
|
|
24
|
+
const formattedSize = formatFileSize(size)
|
|
25
|
+
const isDarkMode = settings?.config?.theme === 'dark'
|
|
26
|
+
const config = CHAT_FILE_PREVIEW_CONFIG[type]
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={clsx(
|
|
31
|
+
'flex max-w-[345px] items-center justify-between rounded-lg border border-neutral-200 p-3',
|
|
32
|
+
{
|
|
33
|
+
'bg-neutral-100': isDarkMode,
|
|
34
|
+
'bg-white': !isDarkMode,
|
|
35
|
+
'w-full max-w-full': isMobile && showCloseButton
|
|
36
|
+
}
|
|
37
|
+
)}>
|
|
38
|
+
<div className='flex min-w-0 flex-1 items-center gap-3 overflow-hidden'>
|
|
39
|
+
<div
|
|
40
|
+
className='flex h-10 w-10 shrink-0 items-center justify-center rounded-md'
|
|
41
|
+
style={{ backgroundColor: config.bgColor }}>
|
|
42
|
+
{isLoading ? (
|
|
43
|
+
<Spinner className='h-[18px] w-[18px] text-white' />
|
|
44
|
+
) : (
|
|
45
|
+
<Icon name={config.icon} className='h-[18px] w-[18px] text-white' />
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
<div className='min-w-0 flex-1 overflow-hidden'>
|
|
49
|
+
<p className='mb-0 mr-3 overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium text-neutral-900'>
|
|
50
|
+
{name}
|
|
51
|
+
</p>
|
|
52
|
+
<div className='flex items-center gap-1 text-xs text-neutral-600'>
|
|
53
|
+
<span className='mb-0'>{t(config.label)}</span>
|
|
54
|
+
<span className='mb-0'>-</span>
|
|
55
|
+
<span className='mb-0'>{formattedSize}</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{showCloseButton && <CloseButton onClick={onClose} variant='inline' />}
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
package/src/modules/messages/components/chat-file-preview/components/document-preview/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as DocumentPreview } from './document-preview'
|
package/src/modules/messages/components/chat-file-preview/components/error-preview/error-preview.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
import { Icon, Spinner } from '@/src/lib/components'
|
|
5
|
+
import { useWidgetSettingsAtom } from '@/src/modules/widget/store'
|
|
6
|
+
|
|
7
|
+
import type { ErrorPreviewProps } from './types'
|
|
8
|
+
|
|
9
|
+
export default function ErrorPreview({ isLoading, onRetry }: ErrorPreviewProps) {
|
|
10
|
+
const { t } = useTranslation()
|
|
11
|
+
const [settings] = useWidgetSettingsAtom()
|
|
12
|
+
const isDarkMode = settings?.config?.theme === 'dark'
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className={clsx('w-fit rounded-lg border border-neutral-200 px-3 py-2', {
|
|
17
|
+
'bg-neutral-100': isDarkMode,
|
|
18
|
+
'bg-white': !isDarkMode
|
|
19
|
+
})}>
|
|
20
|
+
{isLoading ? (
|
|
21
|
+
<Spinner className='h-[18px] w-[18px] text-neutral-900' />
|
|
22
|
+
) : (
|
|
23
|
+
<div className='flex h-full w-full flex-col items-start'>
|
|
24
|
+
<p className='mb-1 text-xs text-red-700'>
|
|
25
|
+
{t('send_message.file_upload.error.load_file')}
|
|
26
|
+
</p>
|
|
27
|
+
<button
|
|
28
|
+
onClick={onRetry}
|
|
29
|
+
className='flex items-center gap-1 rounded border border-neutral-300 px-2 py-1 text-xs text-neutral-900 hover:bg-neutral-200'>
|
|
30
|
+
<Icon name='retry' className='h-4 w-3' />
|
|
31
|
+
{t('general.buttons.try_again')}
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ErrorPreview } from './error-preview'
|
package/src/modules/messages/components/chat-file-preview/components/image-preview/image-preview.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
|
|
3
|
+
import { Spinner } from '@/src/lib/components/spinner'
|
|
4
|
+
import { CloseButton } from '../close-button'
|
|
5
|
+
|
|
6
|
+
import type { ImagePreviewProps } from './types'
|
|
7
|
+
|
|
8
|
+
export default function ImagePreview({
|
|
9
|
+
imageUrl,
|
|
10
|
+
name,
|
|
11
|
+
isLoading,
|
|
12
|
+
showCloseButton,
|
|
13
|
+
onClose
|
|
14
|
+
}: ImagePreviewProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className='relative h-16 w-16'>
|
|
17
|
+
<img
|
|
18
|
+
src={imageUrl}
|
|
19
|
+
alt={name}
|
|
20
|
+
className={clsx('h-full w-full rounded-lg object-cover', {
|
|
21
|
+
'backdrop-blur-sm': isLoading
|
|
22
|
+
})}
|
|
23
|
+
/>
|
|
24
|
+
{isLoading && (
|
|
25
|
+
<div className='absolute inset-0 flex items-center justify-center rounded-lg bg-black/30 backdrop-blur-sm'>
|
|
26
|
+
<Spinner className='h-[18px] w-[18px] text-white' />
|
|
27
|
+
</div>
|
|
28
|
+
)}
|
|
29
|
+
{showCloseButton && !isLoading && <CloseButton onClick={onClose} variant='overlay' />}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ImagePreview } from './image-preview'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const CHAT_FILE_PREVIEW_CONFIG = {
|
|
2
|
+
pdf: {
|
|
3
|
+
bgColor: '#D2444B',
|
|
4
|
+
label: 'send_message.file_upload.file_type.pdf',
|
|
5
|
+
icon: 'pdf'
|
|
6
|
+
},
|
|
7
|
+
document: {
|
|
8
|
+
bgColor: '#007EA8',
|
|
9
|
+
label: 'send_message.file_upload.file_type.document',
|
|
10
|
+
icon: 'document'
|
|
11
|
+
},
|
|
12
|
+
spreadsheet: {
|
|
13
|
+
bgColor: '#00992B',
|
|
14
|
+
label: 'send_message.file_upload.file_type.spreadsheet',
|
|
15
|
+
icon: 'spreadsheet'
|
|
16
|
+
}
|
|
17
|
+
} as const
|
|
18
|
+
|
|
19
|
+
export const FILE_SIZE_UNITS = {
|
|
20
|
+
KB: 1024,
|
|
21
|
+
MB: 1024 * 1024
|
|
22
|
+
} as const
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ChatFilePreview } from './chat-file-preview'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type FilePreviewTypes = 'pdf' | 'document' | 'spreadsheet' | 'image'
|
|
2
|
+
|
|
3
|
+
export type FilePreviewProps = {
|
|
4
|
+
name: string
|
|
5
|
+
size: number
|
|
6
|
+
type: FilePreviewTypes
|
|
7
|
+
showCloseButton?: boolean
|
|
8
|
+
imageUrl?: string
|
|
9
|
+
hasError?: boolean
|
|
10
|
+
onRetry?: () => void
|
|
11
|
+
onClose?: () => void
|
|
12
|
+
isLoading?: boolean
|
|
13
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { formatFileSize } from './utils'
|
|
2
|
+
|
|
3
|
+
describe('formatFileSize', () => {
|
|
4
|
+
it('should format bytes correctly when size is less than 1KB', () => {
|
|
5
|
+
expect(formatFileSize(0)).toBe('0B')
|
|
6
|
+
expect(formatFileSize(100)).toBe('100B')
|
|
7
|
+
expect(formatFileSize(512)).toBe('512B')
|
|
8
|
+
expect(formatFileSize(1023)).toBe('1023B')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should format kilobytes correctly when size is between 1KB and 1MB', () => {
|
|
12
|
+
expect(formatFileSize(1024)).toBe('1KB')
|
|
13
|
+
expect(formatFileSize(2048)).toBe('2KB')
|
|
14
|
+
expect(formatFileSize(1536)).toBe('1.5KB')
|
|
15
|
+
expect(formatFileSize(10240)).toBe('10KB')
|
|
16
|
+
expect(formatFileSize(102400)).toBe('100KB')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should format megabytes correctly when size is greater than or equal to 1MB', () => {
|
|
20
|
+
expect(formatFileSize(1048576)).toBe('1MB')
|
|
21
|
+
expect(formatFileSize(2097152)).toBe('2MB')
|
|
22
|
+
expect(formatFileSize(1572864)).toBe('1.5MB')
|
|
23
|
+
expect(formatFileSize(10485760)).toBe('10MB')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should round to one decimal place', () => {
|
|
27
|
+
expect(formatFileSize(1638)).toBe('1.6KB')
|
|
28
|
+
expect(formatFileSize(1740)).toBe('1.7KB')
|
|
29
|
+
expect(formatFileSize(1100000)).toBe('1MB')
|
|
30
|
+
expect(formatFileSize(1600000)).toBe('1.5MB')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should handle edge cases', () => {
|
|
34
|
+
expect(formatFileSize(1025)).toBe('1KB')
|
|
35
|
+
expect(formatFileSize(1048575)).toBe('1024KB')
|
|
36
|
+
expect(formatFileSize(1048577)).toBe('1MB')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FILE_SIZE_UNITS } from './constants'
|
|
2
|
+
|
|
3
|
+
export function formatFileSize(size: number): string {
|
|
4
|
+
if (size < FILE_SIZE_UNITS.KB) {
|
|
5
|
+
return `${size}B`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (size < FILE_SIZE_UNITS.MB) {
|
|
9
|
+
return `${Math.round((size / FILE_SIZE_UNITS.KB) * 10) / 10}KB`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return `${Math.round((size / FILE_SIZE_UNITS.MB) * 10) / 10}MB`
|
|
13
|
+
}
|