deepspace 0.7.0 → 0.8.1
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 +37 -0
- package/README.md +22 -8
- package/dist/cli.js +9812 -4674
- package/dist/cli.js.map +1 -1
- package/dist/feature-installer.js +638 -0
- package/dist/feature-installer.js.map +1 -0
- package/dist/index.d.ts +107 -117
- package/dist/index.js +610 -779
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +403 -167
- package/dist/server.js +498 -367
- package/dist/server.js.map +1 -1
- package/dist/testing.d.ts +115 -0
- package/dist/testing.js +3 -3
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +397 -160
- package/dist/worker.js +506 -367
- package/dist/worker.js.map +1 -1
- package/features/ai-chat/feature.json +2 -0
- package/features/ai-chat/src/AiChatPage.tsx +2 -3
- package/features/ai-chat/src/ChatPanel.messages.tsx +344 -0
- package/features/ai-chat/src/ChatPanel.stream.ts +397 -0
- package/features/ai-chat/src/ChatPanel.tsx +158 -862
- package/features/ai-chat/src/__tests__/ChatPanel.stream.test.ts +163 -0
- package/features/canvas/src/CanvasListPage.tsx +1 -1
- package/features/docs/feature.json +4 -8
- package/features/docs/src/DocsEditorPage.tsx +33 -326
- package/features/docs/src/DocsListPage.tsx +33 -29
- package/features/docs/src/InviteDialog.tsx +11 -40
- package/features/docs/src/__tests__/docs-presence-access.test.ts +71 -0
- package/features/docs/src/docs-library-types.ts +28 -3
- package/features/docs/src/docs-ui.css +1 -4
- package/features/docs/src/use-docs-presence-access.ts +330 -0
- package/features/file-attachments/feature.json +2 -2
- package/features/file-attachments/src/AttachmentPreview.tsx +428 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +100 -462
- package/features/file-attachments/src/useFilePreview.ts +54 -68
- package/features/file-manager/src/FileManagerPage.tsx +1 -1
- package/features/integration-test/src/integration.spec.ts +1 -1
- package/features/items/feature.json +1 -5
- package/features/items/src/ItemsPage.tsx +207 -135
- package/features/landing/feature.json +3 -0
- package/features/landing/src/LandingPage.tsx +4 -18
- package/features/leaderboard/feature.json +1 -5
- package/features/leaderboard/src/LeaderboardPage.tsx +333 -198
- package/features/messaging/feature.json +0 -4
- package/features/messaging/src/ChatMultiPage.tsx +1 -3
- package/features/messaging/src/ChatPage.tsx +1 -3
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +40 -16
- package/features/search-bar/feature.json +8 -3
- package/features/search-bar/src/SearchOverlay.tsx +108 -252
- package/features/search-bar/src/__tests__/search-model.test.ts +40 -0
- package/features/search-bar/src/search-model.ts +223 -0
- package/features/tasks/src/TasksPage.tsx +3 -3
- package/features/topbar/src/Topbar.tsx +0 -17
- package/package.json +2 -3
- package/features/docs/src/docs-favorites.ts +0 -18
- package/features/docs/src/use-yjs-room-with-awareness.ts +0 -289
- package/features/file-attachments/src/attachments-constants.ts +0 -9
- package/features/items/src/items-constants.ts +0 -10
- package/features/leaderboard/src/leaderboard-constants.ts +0 -21
- package/features/messaging/src/components/chat/ReactionPicker.tsx +0 -2
- package/scripts/add-feature.cjs +0 -807
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"name": "File Attachments",
|
|
4
4
|
"category": "data",
|
|
5
5
|
"description": "File upload with inline previews for images, videos, audio, PDF, code, CSV, DOCX, XLSX",
|
|
6
|
-
"details": "Adds a file attachments collection backed by R2 storage with useR2Files.
|
|
6
|
+
"details": "Adds a file attachments collection backed by R2 storage with useR2Files. MIME detection and preview loading live in focused hooks; one preview component owns thumbnail and full rendering for PDF.js, CodeMirror, CSV, DOCX, XLSX, audio, video, and images. Members can upload and manage their own files, admins can manage all.",
|
|
7
7
|
"files": [
|
|
8
8
|
{ "src": "src/attachments-schema.ts", "dest": "src/schemas/attachments-schema.ts" },
|
|
9
|
-
{ "src": "src/attachments-constants.ts", "dest": "src/constants/attachments-constants.ts" },
|
|
10
9
|
{ "src": "src/useMimeTypeDetection.ts", "dest": "src/hooks/useMimeTypeDetection.ts" },
|
|
11
10
|
{ "src": "src/useFilePreview.ts", "dest": "src/hooks/useFilePreview.ts" },
|
|
11
|
+
{ "src": "src/AttachmentPreview.tsx", "dest": "src/components/AttachmentPreview.tsx" },
|
|
12
12
|
{ "src": "src/AttachmentsPage.tsx", "dest": "src/pages/attachments.tsx" }
|
|
13
13
|
],
|
|
14
14
|
"schema": {
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useFilePreview, type XlsxData } from '../hooks/useFilePreview'
|
|
4
|
+
import type { FileCategory } from '../hooks/useMimeTypeDetection'
|
|
5
|
+
|
|
6
|
+
const ICON_PATHS: Record<FileCategory, string> = {
|
|
7
|
+
image:
|
|
8
|
+
'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z',
|
|
9
|
+
video:
|
|
10
|
+
'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z',
|
|
11
|
+
audio:
|
|
12
|
+
'M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z',
|
|
13
|
+
pdf: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z',
|
|
14
|
+
document:
|
|
15
|
+
'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
|
16
|
+
spreadsheet:
|
|
17
|
+
'M3 10h18M3 14h18M9 4v16M15 4v16M3 6a2 2 0 012-2h14a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6z',
|
|
18
|
+
presentation:
|
|
19
|
+
'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
|
20
|
+
archive: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4',
|
|
21
|
+
code: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4',
|
|
22
|
+
text: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
|
23
|
+
other:
|
|
24
|
+
'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function FileIcon({ category }: { category: FileCategory }) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className="flex h-48 w-full items-center justify-center rounded-lg bg-muted/20"
|
|
31
|
+
role="img"
|
|
32
|
+
aria-label={`${category} file`}
|
|
33
|
+
>
|
|
34
|
+
<svg
|
|
35
|
+
className="h-12 w-12 text-muted-foreground/50"
|
|
36
|
+
fill="none"
|
|
37
|
+
viewBox="0 0 24 24"
|
|
38
|
+
stroke="currentColor"
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
>
|
|
41
|
+
<path
|
|
42
|
+
strokeLinecap="round"
|
|
43
|
+
strokeLinejoin="round"
|
|
44
|
+
strokeWidth={1.5}
|
|
45
|
+
d={ICON_PATHS[category]}
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function AttachmentThumbnail({
|
|
53
|
+
url,
|
|
54
|
+
fileName,
|
|
55
|
+
category,
|
|
56
|
+
}: {
|
|
57
|
+
url: string | null
|
|
58
|
+
fileName: string
|
|
59
|
+
category: FileCategory
|
|
60
|
+
}) {
|
|
61
|
+
if (!url) return <FileIcon category={category} />
|
|
62
|
+
|
|
63
|
+
switch (category) {
|
|
64
|
+
case 'image':
|
|
65
|
+
return (
|
|
66
|
+
<img
|
|
67
|
+
src={url}
|
|
68
|
+
alt={fileName}
|
|
69
|
+
className="h-48 w-full rounded-t-xl object-cover"
|
|
70
|
+
loading="lazy"
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
case 'video':
|
|
74
|
+
return (
|
|
75
|
+
<video
|
|
76
|
+
src={url}
|
|
77
|
+
aria-label={`${fileName} preview`}
|
|
78
|
+
className="h-48 w-full rounded-t-xl bg-black"
|
|
79
|
+
preload="metadata"
|
|
80
|
+
muted
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
case 'audio':
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex h-48 w-full flex-col items-center justify-center gap-2 rounded-t-xl bg-muted/20 px-4">
|
|
86
|
+
<svg
|
|
87
|
+
className="h-8 w-8 text-muted-foreground/50"
|
|
88
|
+
fill="none"
|
|
89
|
+
viewBox="0 0 24 24"
|
|
90
|
+
stroke="currentColor"
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
>
|
|
93
|
+
<path
|
|
94
|
+
strokeLinecap="round"
|
|
95
|
+
strokeLinejoin="round"
|
|
96
|
+
strokeWidth={1.5}
|
|
97
|
+
d={ICON_PATHS.audio}
|
|
98
|
+
/>
|
|
99
|
+
</svg>
|
|
100
|
+
<audio
|
|
101
|
+
src={url}
|
|
102
|
+
aria-label={fileName}
|
|
103
|
+
controls
|
|
104
|
+
className="w-full"
|
|
105
|
+
preload="metadata"
|
|
106
|
+
onClick={(event) => event.stopPropagation()}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
default:
|
|
111
|
+
return <FileIcon category={category} />
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function TabularPreview({
|
|
116
|
+
headers,
|
|
117
|
+
rows,
|
|
118
|
+
truncated,
|
|
119
|
+
}: {
|
|
120
|
+
headers: string[]
|
|
121
|
+
rows: string[][]
|
|
122
|
+
truncated: boolean
|
|
123
|
+
}) {
|
|
124
|
+
return (
|
|
125
|
+
<div className="w-full overflow-auto rounded-lg border border-border">
|
|
126
|
+
<table className="w-full border-collapse font-mono text-sm">
|
|
127
|
+
<thead>
|
|
128
|
+
<tr>
|
|
129
|
+
<th className="sticky top-0 border-b-2 border-border bg-muted/50 px-3 py-2 text-right text-xs text-muted-foreground">
|
|
130
|
+
#
|
|
131
|
+
</th>
|
|
132
|
+
{headers.map((header, index) => (
|
|
133
|
+
<th
|
|
134
|
+
key={`${index}-${header}`}
|
|
135
|
+
className="sticky top-0 whitespace-nowrap border-b-2 border-border bg-muted/50 px-3 py-2 text-left font-semibold text-foreground"
|
|
136
|
+
>
|
|
137
|
+
{header}
|
|
138
|
+
</th>
|
|
139
|
+
))}
|
|
140
|
+
</tr>
|
|
141
|
+
</thead>
|
|
142
|
+
<tbody>
|
|
143
|
+
{rows.map((row, rowIndex) => (
|
|
144
|
+
<tr key={rowIndex} className={rowIndex % 2 === 0 ? '' : 'bg-muted/20'}>
|
|
145
|
+
<td className="border-b border-border/50 px-3 py-1.5 text-right text-xs text-muted-foreground">
|
|
146
|
+
{rowIndex + 1}
|
|
147
|
+
</td>
|
|
148
|
+
{headers.map((_, columnIndex) => (
|
|
149
|
+
<td
|
|
150
|
+
key={columnIndex}
|
|
151
|
+
className="max-w-[300px] truncate whitespace-nowrap border-b border-border/50 px-3 py-1.5 text-foreground"
|
|
152
|
+
title={row[columnIndex] ?? ''}
|
|
153
|
+
>
|
|
154
|
+
{row[columnIndex] ?? ''}
|
|
155
|
+
</td>
|
|
156
|
+
))}
|
|
157
|
+
</tr>
|
|
158
|
+
))}
|
|
159
|
+
</tbody>
|
|
160
|
+
</table>
|
|
161
|
+
{truncated && (
|
|
162
|
+
<div className="border-t border-border bg-muted/30 px-4 py-2 text-center text-xs text-muted-foreground">
|
|
163
|
+
Showing first 500 rows. Download the file to see all data.
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function DocxPreview({
|
|
171
|
+
data,
|
|
172
|
+
module: docxModule,
|
|
173
|
+
}: {
|
|
174
|
+
data: ArrayBuffer
|
|
175
|
+
module: typeof import('docx-preview')
|
|
176
|
+
}) {
|
|
177
|
+
const styleRef = useRef<HTMLDivElement>(null)
|
|
178
|
+
const contentRef = useRef<HTMLDivElement>(null)
|
|
179
|
+
const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
const content = contentRef.current
|
|
183
|
+
const styles = styleRef.current
|
|
184
|
+
if (!content || !styles) return
|
|
185
|
+
|
|
186
|
+
let active = true
|
|
187
|
+
setStatus('loading')
|
|
188
|
+
content.replaceChildren()
|
|
189
|
+
styles.replaceChildren()
|
|
190
|
+
|
|
191
|
+
void docxModule
|
|
192
|
+
.renderAsync(data, content, styles, {
|
|
193
|
+
className: 'docx-preview-page',
|
|
194
|
+
inWrapper: true,
|
|
195
|
+
ignoreWidth: true,
|
|
196
|
+
ignoreHeight: false,
|
|
197
|
+
ignoreFonts: false,
|
|
198
|
+
breakPages: true,
|
|
199
|
+
renderHeaders: true,
|
|
200
|
+
renderFooters: true,
|
|
201
|
+
renderFootnotes: true,
|
|
202
|
+
renderEndnotes: true,
|
|
203
|
+
useBase64URL: true,
|
|
204
|
+
})
|
|
205
|
+
.then(
|
|
206
|
+
() => {
|
|
207
|
+
if (active) setStatus('ready')
|
|
208
|
+
},
|
|
209
|
+
() => {
|
|
210
|
+
if (active) setStatus('error')
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return () => {
|
|
215
|
+
active = false
|
|
216
|
+
content.replaceChildren()
|
|
217
|
+
styles.replaceChildren()
|
|
218
|
+
}
|
|
219
|
+
}, [data, docxModule])
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div
|
|
223
|
+
className="min-h-[400px] w-full overflow-auto rounded-lg"
|
|
224
|
+
style={{ background: '#e8e8e8' }}
|
|
225
|
+
>
|
|
226
|
+
{status === 'loading' && (
|
|
227
|
+
<div className="p-8 text-center text-muted-foreground">Loading document...</div>
|
|
228
|
+
)}
|
|
229
|
+
{status === 'error' && (
|
|
230
|
+
<div className="p-8 text-center text-muted-foreground">Failed to render document.</div>
|
|
231
|
+
)}
|
|
232
|
+
<div ref={styleRef} />
|
|
233
|
+
<div ref={contentRef} />
|
|
234
|
+
<style>{`
|
|
235
|
+
.docx-preview-page-wrapper {
|
|
236
|
+
background: #e8e8e8;
|
|
237
|
+
padding: 16px 0;
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
240
|
+
align-items: center;
|
|
241
|
+
gap: 16px;
|
|
242
|
+
}
|
|
243
|
+
.docx-preview-page-wrapper > section.docx-preview-page {
|
|
244
|
+
background: #ffffff !important;
|
|
245
|
+
color: #1a1a1a !important;
|
|
246
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
|
247
|
+
border-radius: 4px;
|
|
248
|
+
overflow: hidden;
|
|
249
|
+
width: 100% !important;
|
|
250
|
+
padding: 24px !important;
|
|
251
|
+
box-sizing: border-box !important;
|
|
252
|
+
}
|
|
253
|
+
.docx-preview-page-wrapper > section.docx-preview-page * {
|
|
254
|
+
color: inherit;
|
|
255
|
+
}
|
|
256
|
+
`}</style>
|
|
257
|
+
</div>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function XlsxPreview({ data }: { data: XlsxData }) {
|
|
262
|
+
const [activeSheet, setActiveSheet] = useState(0)
|
|
263
|
+
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
setActiveSheet(0)
|
|
266
|
+
}, [data])
|
|
267
|
+
|
|
268
|
+
const sheet = data.sheets[activeSheet] ?? data.sheets[0]
|
|
269
|
+
if (!sheet) {
|
|
270
|
+
return <div className="p-8 text-center text-muted-foreground">This workbook has no sheets.</div>
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<div className="w-full">
|
|
275
|
+
{data.sheets.length > 1 && (
|
|
276
|
+
<div className="flex gap-1 overflow-x-auto border-b border-border bg-muted/30 px-2 py-1">
|
|
277
|
+
{data.sheets.map((candidate, index) => (
|
|
278
|
+
<button
|
|
279
|
+
key={candidate.name}
|
|
280
|
+
type="button"
|
|
281
|
+
onClick={() => setActiveSheet(index)}
|
|
282
|
+
className={`whitespace-nowrap rounded px-3 py-1 text-xs font-medium transition-colors ${
|
|
283
|
+
index === activeSheet
|
|
284
|
+
? 'bg-primary text-primary-foreground'
|
|
285
|
+
: 'text-muted-foreground hover:bg-muted'
|
|
286
|
+
}`}
|
|
287
|
+
>
|
|
288
|
+
{candidate.name}
|
|
289
|
+
</button>
|
|
290
|
+
))}
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
<TabularPreview headers={sheet.headers} rows={sheet.rows} truncated={sheet.truncated} />
|
|
294
|
+
</div>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function PdfPage({ canvas }: { canvas: HTMLCanvasElement }) {
|
|
299
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
300
|
+
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
const container = containerRef.current
|
|
303
|
+
if (!container) return
|
|
304
|
+
canvas.style.width = '100%'
|
|
305
|
+
canvas.style.height = 'auto'
|
|
306
|
+
canvas.style.display = 'block'
|
|
307
|
+
container.replaceChildren(canvas)
|
|
308
|
+
return () => {
|
|
309
|
+
if (canvas.parentNode === container) canvas.remove()
|
|
310
|
+
}
|
|
311
|
+
}, [canvas])
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<div
|
|
315
|
+
ref={containerRef}
|
|
316
|
+
className="w-full flex-shrink-0 overflow-hidden rounded-lg border border-border bg-white"
|
|
317
|
+
style={{ aspectRatio: `${canvas.width / canvas.height}` }}
|
|
318
|
+
/>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function AttachmentPreview({
|
|
323
|
+
url,
|
|
324
|
+
fileName,
|
|
325
|
+
mimeType,
|
|
326
|
+
}: {
|
|
327
|
+
url: string
|
|
328
|
+
fileName: string
|
|
329
|
+
mimeType: string
|
|
330
|
+
}) {
|
|
331
|
+
const preview = useFilePreview({ url, fileName, mimeType })
|
|
332
|
+
|
|
333
|
+
if (preview.isLoading) {
|
|
334
|
+
return (
|
|
335
|
+
<div className="flex items-center justify-center py-16">
|
|
336
|
+
<div className="h-8 w-8 animate-spin rounded-full border-2 border-primary/20 border-t-primary" />
|
|
337
|
+
</div>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (preview.error) {
|
|
342
|
+
return (
|
|
343
|
+
<div className="p-8 text-center text-muted-foreground">
|
|
344
|
+
Failed to load preview: {preview.error}
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (preview.category === 'image') {
|
|
350
|
+
return <img src={url} alt={fileName} className="w-full rounded-lg" />
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (preview.category === 'video') {
|
|
354
|
+
return <video src={url} aria-label={fileName} controls className="w-full rounded-lg bg-black" />
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (preview.category === 'audio') {
|
|
358
|
+
return (
|
|
359
|
+
<div className="flex flex-col items-center gap-4 p-8">
|
|
360
|
+
<FileIcon category="audio" />
|
|
361
|
+
<audio src={url} aria-label={fileName} controls className="w-full" />
|
|
362
|
+
</div>
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (preview.category === 'pdf' && preview.pdfPages) {
|
|
367
|
+
return (
|
|
368
|
+
<div className="flex max-h-[80vh] flex-col gap-4 overflow-auto">
|
|
369
|
+
{preview.pdfPages.map((canvas, index) => (
|
|
370
|
+
<PdfPage key={index} canvas={canvas} />
|
|
371
|
+
))}
|
|
372
|
+
</div>
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (
|
|
377
|
+
(preview.category === 'code' || preview.category === 'text') &&
|
|
378
|
+
preview.textContent !== null
|
|
379
|
+
) {
|
|
380
|
+
if (preview.codeEditorKit) {
|
|
381
|
+
const { Component, langExtensions, darkTheme } = preview.codeEditorKit
|
|
382
|
+
return (
|
|
383
|
+
<div className="w-full overflow-hidden rounded-lg border border-border">
|
|
384
|
+
<Component
|
|
385
|
+
value={preview.textContent}
|
|
386
|
+
extensions={[...langExtensions, darkTheme]}
|
|
387
|
+
readOnly
|
|
388
|
+
editable={false}
|
|
389
|
+
theme="dark"
|
|
390
|
+
height="400px"
|
|
391
|
+
basicSetup={{ lineNumbers: true, foldGutter: true, highlightActiveLine: false }}
|
|
392
|
+
style={{ fontSize: '13px' }}
|
|
393
|
+
/>
|
|
394
|
+
</div>
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<pre className="max-h-[400px] w-full overflow-auto whitespace-pre-wrap rounded-lg border border-border bg-muted/20 p-4 text-sm text-foreground">
|
|
400
|
+
{preview.textContent}
|
|
401
|
+
</pre>
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (preview.csvData) {
|
|
406
|
+
return <TabularPreview {...preview.csvData} />
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (preview.category === 'document' && preview.docxData && preview.docxModule) {
|
|
410
|
+
return <DocxPreview data={preview.docxData} module={preview.docxModule} />
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (preview.xlsxData) {
|
|
414
|
+
return <XlsxPreview data={preview.xlsxData} />
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<div className="flex flex-col items-center gap-4 p-12 text-center">
|
|
419
|
+
<FileIcon category={preview.category} />
|
|
420
|
+
<div className="space-y-1">
|
|
421
|
+
<p className="text-sm font-medium text-foreground">No preview available</p>
|
|
422
|
+
<p className="text-xs text-muted-foreground">
|
|
423
|
+
Preview is not supported for this file type. You can download the file to view it.
|
|
424
|
+
</p>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
)
|
|
428
|
+
}
|