Skip to content

Commit 4d6657b

Browse files
committed
refactor(files): split 2281-line file-viewer.tsx into focused modules
TextEditor, DocxPreview, PptxPreview, XlsxPreview, ImagePreview each moved to their own files. Shared utilities (PreviewError, resolvePreviewError, shouldSuppressStreamingDocumentError, PDF_PAGE_SKELETON) extracted to preview-shared.tsx. file-viewer.tsx is now the orchestrator + MIME constants + small stateless previews (~495 lines).
1 parent bdae6d3 commit 4d6657b

7 files changed

Lines changed: 1880 additions & 1802 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
'use client'
2+
3+
import { memo, useEffect, useRef, useState } from 'react'
4+
import { createLogger } from '@sim/logger'
5+
import { toError } from '@sim/utils/errors'
6+
import { cn } from '@/lib/core/utils/cn'
7+
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
8+
import { useWorkspaceFileBinary } from '@/hooks/queries/workspace-files'
9+
import {
10+
PDF_PAGE_SKELETON,
11+
PreviewError,
12+
resolvePreviewError,
13+
shouldSuppressStreamingDocumentError,
14+
} from './preview-shared'
15+
16+
const logger = createLogger('DocxPreview')
17+
18+
export const DocxPreview = memo(function DocxPreview({
19+
file,
20+
workspaceId,
21+
streamingContent,
22+
}: {
23+
file: WorkspaceFileRecord
24+
workspaceId: string
25+
streamingContent?: string
26+
}) {
27+
const containerRef = useRef<HTMLDivElement>(null)
28+
const lastSuccessfulHtmlRef = useRef('')
29+
const {
30+
data: fileData,
31+
isLoading,
32+
error: fetchError,
33+
} = useWorkspaceFileBinary(workspaceId, file.id, file.key)
34+
const [renderError, setRenderError] = useState<string | null>(null)
35+
const [rendering, setRendering] = useState(false)
36+
const [hasRenderedPreview, setHasRenderedPreview] = useState(false)
37+
38+
useEffect(() => {
39+
if (!containerRef.current || !fileData || streamingContent !== undefined) return
40+
41+
let cancelled = false
42+
43+
async function render() {
44+
try {
45+
setRendering(true)
46+
const { renderAsync } = await import('docx-preview')
47+
if (cancelled || !containerRef.current) return
48+
setRenderError(null)
49+
containerRef.current.innerHTML = ''
50+
await renderAsync(fileData, containerRef.current, undefined, {
51+
inWrapper: true,
52+
ignoreWidth: false,
53+
ignoreHeight: false,
54+
})
55+
if (!cancelled && containerRef.current) {
56+
const wrapper = containerRef.current.querySelector<HTMLElement>('.docx-wrapper')
57+
if (wrapper) wrapper.style.background = 'transparent'
58+
containerRef.current.querySelectorAll<HTMLElement>('section.docx').forEach((page) => {
59+
page.style.boxShadow = 'var(--shadow-medium)'
60+
})
61+
lastSuccessfulHtmlRef.current = containerRef.current.innerHTML
62+
setHasRenderedPreview(true)
63+
}
64+
} catch (err) {
65+
if (!cancelled) {
66+
const msg = toError(err).message || 'Failed to render document'
67+
logger.error('DOCX render failed', { error: msg })
68+
setRenderError(msg)
69+
}
70+
} finally {
71+
if (!cancelled) {
72+
setRendering(false)
73+
}
74+
}
75+
}
76+
77+
render()
78+
return () => {
79+
cancelled = true
80+
}
81+
}, [fileData, streamingContent])
82+
83+
useEffect(() => {
84+
if (streamingContent === undefined || !containerRef.current) return
85+
86+
let cancelled = false
87+
const controller = new AbortController()
88+
89+
const debounceTimer = setTimeout(async () => {
90+
const container = containerRef.current
91+
if (!container || cancelled) return
92+
93+
const previousHtml = lastSuccessfulHtmlRef.current
94+
95+
try {
96+
setRendering(true)
97+
setRenderError(null)
98+
99+
const response = await fetch(`/api/workspaces/${workspaceId}/docx/preview`, {
100+
method: 'POST',
101+
headers: { 'Content-Type': 'application/json' },
102+
body: JSON.stringify({ code: streamingContent }),
103+
signal: controller.signal,
104+
})
105+
if (!response.ok) {
106+
const err = await response.json().catch(() => ({ error: 'Preview failed' }))
107+
throw new Error(err.error || 'Preview failed')
108+
}
109+
110+
const arrayBuffer = await response.arrayBuffer()
111+
if (cancelled || !containerRef.current) return
112+
113+
const { renderAsync } = await import('docx-preview')
114+
if (cancelled || !containerRef.current) return
115+
116+
containerRef.current.innerHTML = ''
117+
await renderAsync(new Uint8Array(arrayBuffer), containerRef.current, undefined, {
118+
inWrapper: true,
119+
ignoreWidth: false,
120+
ignoreHeight: false,
121+
})
122+
123+
if (!cancelled && containerRef.current) {
124+
const wrapper = containerRef.current.querySelector<HTMLElement>('.docx-wrapper')
125+
if (wrapper) wrapper.style.background = 'transparent'
126+
containerRef.current.querySelectorAll<HTMLElement>('section.docx').forEach((page) => {
127+
page.style.boxShadow = 'var(--shadow-medium)'
128+
})
129+
lastSuccessfulHtmlRef.current = containerRef.current.innerHTML
130+
setHasRenderedPreview(true)
131+
}
132+
} catch (err) {
133+
if (!cancelled && !(err instanceof DOMException && err.name === 'AbortError')) {
134+
if (containerRef.current && previousHtml) {
135+
containerRef.current.innerHTML = previousHtml
136+
setHasRenderedPreview(true)
137+
}
138+
const msg = toError(err).message || 'Failed to render document'
139+
if (previousHtml || shouldSuppressStreamingDocumentError(msg)) {
140+
logger.info('Suppressing transient DOCX streaming preview error', { error: msg })
141+
} else {
142+
logger.error('DOCX render failed', { error: msg })
143+
setRenderError(msg)
144+
}
145+
}
146+
} finally {
147+
if (!cancelled) {
148+
setRendering(false)
149+
}
150+
}
151+
}, 500)
152+
153+
return () => {
154+
cancelled = true
155+
clearTimeout(debounceTimer)
156+
controller.abort()
157+
}
158+
}, [streamingContent, workspaceId])
159+
160+
const error =
161+
hasRenderedPreview && streamingContent !== undefined
162+
? null
163+
: streamingContent !== undefined
164+
? renderError
165+
: resolvePreviewError(fetchError, renderError)
166+
if (error) return <PreviewError label='document' error={error} />
167+
168+
const showSkeleton =
169+
!hasRenderedPreview &&
170+
((streamingContent !== undefined && rendering) || (streamingContent === undefined && isLoading))
171+
172+
return (
173+
<div className='relative h-full w-full overflow-auto bg-[var(--surface-1)]'>
174+
{showSkeleton && (
175+
<div className='absolute inset-0 z-10 bg-[var(--surface-1)]'>{PDF_PAGE_SKELETON}</div>
176+
)}
177+
<div
178+
ref={containerRef}
179+
className={cn('h-full w-full overflow-auto', showSkeleton && 'opacity-0')}
180+
/>
181+
</div>
182+
)
183+
})

0 commit comments

Comments
 (0)