@@ -45,15 +45,25 @@ const TEXT_EDITABLE_EXTENSIONS = new Set([
4545const IFRAME_PREVIEWABLE_MIME_TYPES = new Set ( [ 'application/pdf' ] )
4646const IFRAME_PREVIEWABLE_EXTENSIONS = new Set ( [ 'pdf' ] )
4747
48- type FileCategory = 'text-editable' | 'iframe-previewable' | 'unsupported'
48+ const IMAGE_PREVIEWABLE_MIME_TYPES = new Set ( [
49+ 'image/png' ,
50+ 'image/jpeg' ,
51+ 'image/gif' ,
52+ 'image/webp' ,
53+ ] )
54+ const IMAGE_PREVIEWABLE_EXTENSIONS = new Set ( [ 'png' , 'jpg' , 'jpeg' , 'gif' , 'webp' ] )
55+
56+ type FileCategory = 'text-editable' | 'iframe-previewable' | 'image-previewable' | 'unsupported'
4957
5058function resolveFileCategory ( mimeType : string | null , filename : string ) : FileCategory {
5159 if ( mimeType && TEXT_EDITABLE_MIME_TYPES . has ( mimeType ) ) return 'text-editable'
5260 if ( mimeType && IFRAME_PREVIEWABLE_MIME_TYPES . has ( mimeType ) ) return 'iframe-previewable'
61+ if ( mimeType && IMAGE_PREVIEWABLE_MIME_TYPES . has ( mimeType ) ) return 'image-previewable'
5362
5463 const ext = getFileExtension ( filename )
5564 if ( TEXT_EDITABLE_EXTENSIONS . has ( ext ) ) return 'text-editable'
5665 if ( IFRAME_PREVIEWABLE_EXTENSIONS . has ( ext ) ) return 'iframe-previewable'
66+ if ( IMAGE_PREVIEWABLE_EXTENSIONS . has ( ext ) ) return 'image-previewable'
5767
5868 return 'unsupported'
5969}
@@ -115,6 +125,10 @@ export function FileViewer({
115125 return < IframePreview file = { file } />
116126 }
117127
128+ if ( category === 'image-previewable' ) {
129+ return < ImagePreview file = { file } />
130+ }
131+
118132 return < UnsupportedPreview file = { file } />
119133}
120134
@@ -265,6 +279,40 @@ function TextEditor({
265279 const isStreaming = streamingContent !== undefined
266280 const revealedContent = useStreamingText ( content , isStreaming )
267281
282+ const textareaStuckRef = useRef ( true )
283+
284+ useEffect ( ( ) => {
285+ if ( ! isStreaming ) return
286+ textareaStuckRef . current = true
287+
288+ const el = textareaRef . current
289+ if ( ! el ) return
290+
291+ const onWheel = ( e : WheelEvent ) => {
292+ if ( e . deltaY < 0 ) textareaStuckRef . current = false
293+ }
294+
295+ const onScroll = ( ) => {
296+ const dist = el . scrollHeight - el . scrollTop - el . clientHeight
297+ if ( dist <= 5 ) textareaStuckRef . current = true
298+ }
299+
300+ el . addEventListener ( 'wheel' , onWheel , { passive : true } )
301+ el . addEventListener ( 'scroll' , onScroll , { passive : true } )
302+
303+ return ( ) => {
304+ el . removeEventListener ( 'wheel' , onWheel )
305+ el . removeEventListener ( 'scroll' , onScroll )
306+ }
307+ } , [ isStreaming ] )
308+
309+ useEffect ( ( ) => {
310+ if ( ! isStreaming || ! textareaStuckRef . current ) return
311+ const el = textareaRef . current
312+ if ( ! el ) return
313+ el . scrollTop = el . scrollHeight
314+ } , [ isStreaming , revealedContent ] )
315+
268316 if ( streamingContent === undefined ) {
269317 if ( isLoading ) {
270318 return (
@@ -286,8 +334,11 @@ function TextEditor({
286334 }
287335 }
288336
289- const showEditor = previewMode !== 'preview'
290- const showPreviewPane = previewMode !== 'editor'
337+ const previewType = resolvePreviewType ( file . type , file . name )
338+ const isIframeRendered = previewType === 'html' || previewType === 'svg'
339+ const effectiveMode = isStreaming && isIframeRendered ? 'editor' : previewMode
340+ const showEditor = effectiveMode !== 'preview'
341+ const showPreviewPane = effectiveMode !== 'editor'
291342
292343 return (
293344 < div ref = { containerRef } className = 'relative flex flex-1 overflow-hidden' >
@@ -351,6 +402,21 @@ function IframePreview({ file }: { file: WorkspaceFileRecord }) {
351402 )
352403}
353404
405+ function ImagePreview ( { file } : { file : WorkspaceFileRecord } ) {
406+ const serveUrl = `/api/files/serve/${ encodeURIComponent ( file . key ) } ?context=workspace`
407+
408+ return (
409+ < div className = 'flex flex-1 items-center justify-center overflow-auto bg-[var(--surface-1)] p-6' >
410+ < img
411+ src = { serveUrl }
412+ alt = { file . name }
413+ className = 'max-h-full max-w-full rounded-md object-contain'
414+ loading = 'eager'
415+ />
416+ </ div >
417+ )
418+ }
419+
354420function UnsupportedPreview ( { file } : { file : WorkspaceFileRecord } ) {
355421 const ext = getFileExtension ( file . name )
356422
0 commit comments