perf: keep recent html and pdf viewers mounted to preserve state

This commit is contained in:
Gagancreates 2026-05-08 03:00:40 +05:30
parent a4cd6abb3a
commit 49a50279da
2 changed files with 74 additions and 10 deletions

View file

@ -13,11 +13,10 @@ import { ChatInputWithMentions, type StagedAttachment } from './components/chat-
import { ChatMessageAttachments } from '@/components/chat-message-attachments' import { ChatMessageAttachments } from '@/components/chat-message-attachments'
import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view'; import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view';
import { BasesView, type BaseConfig, DEFAULT_BASE_CONFIG } from '@/components/bases-view'; import { BasesView, type BaseConfig, DEFAULT_BASE_CONFIG } from '@/components/bases-view';
import { HtmlFileViewer } from '@/components/html-file-viewer';
import { ImageFileViewer } from '@/components/image-file-viewer'; import { ImageFileViewer } from '@/components/image-file-viewer';
import { VideoFileViewer } from '@/components/video-file-viewer'; import { VideoFileViewer } from '@/components/video-file-viewer';
import { PdfFileViewer } from '@/components/pdf-file-viewer';
import { AudioFileViewer } from '@/components/audio-file-viewer'; import { AudioFileViewer } from '@/components/audio-file-viewer';
import { PersistentViewerCache } from '@/components/persistent-viewer-cache';
import { useDebounce } from './hooks/use-debounce'; import { useDebounce } from './hooks/use-debounce';
import { SidebarContentPanel } from '@/components/sidebar-content'; import { SidebarContentPanel } from '@/components/sidebar-content';
import { SuggestedTopicsView } from '@/components/suggested-topics-view'; import { SuggestedTopicsView } from '@/components/suggested-topics-view';
@ -4722,6 +4721,15 @@ function App() {
/> />
</div> </div>
) : selectedPath ? ( ) : selectedPath ? (
<>
{/* Always-mounted persistent cache for HTML/PDF — hidden when active file is something else, so iframes preserve scroll/page/zoom across switches. */}
<div
className="flex-1 min-h-0 overflow-hidden"
style={{ display: /\.(html?|pdf)$/i.test(selectedPath) ? 'block' : 'none' }}
>
<PersistentViewerCache activePath={selectedPath} />
</div>
{!/\.(html?|pdf)$/i.test(selectedPath) && (
selectedPath.endsWith('.md') ? ( selectedPath.endsWith('.md') ? (
<div className="flex-1 min-h-0 flex flex-row overflow-hidden"> <div className="flex-1 min-h-0 flex flex-row overflow-hidden">
<div className="flex-1 min-h-0 flex flex-col overflow-hidden"> <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
@ -4831,10 +4839,6 @@ function App() {
/> />
)} )}
</div> </div>
) : selectedPath?.toLowerCase().endsWith('.html') || selectedPath?.toLowerCase().endsWith('.htm') ? (
<div className="flex-1 min-h-0 overflow-hidden">
<HtmlFileViewer path={selectedPath} />
</div>
) : selectedPath && /\.(png|jpe?g|webp|gif|svg|avif|bmp|ico)$/i.test(selectedPath) ? ( ) : selectedPath && /\.(png|jpe?g|webp|gif|svg|avif|bmp|ico)$/i.test(selectedPath) ? (
<div className="flex-1 min-h-0 overflow-hidden"> <div className="flex-1 min-h-0 overflow-hidden">
<ImageFileViewer path={selectedPath} /> <ImageFileViewer path={selectedPath} />
@ -4843,10 +4847,6 @@ function App() {
<div className="flex-1 min-h-0 overflow-hidden"> <div className="flex-1 min-h-0 overflow-hidden">
<VideoFileViewer path={selectedPath} /> <VideoFileViewer path={selectedPath} />
</div> </div>
) : selectedPath?.toLowerCase().endsWith('.pdf') ? (
<div className="flex-1 min-h-0 overflow-hidden">
<PdfFileViewer path={selectedPath} />
</div>
) : selectedPath && /\.(mp3|wav|m4a|ogg|flac|aac)$/i.test(selectedPath) ? ( ) : selectedPath && /\.(mp3|wav|m4a|ogg|flac|aac)$/i.test(selectedPath) ? (
<div className="flex-1 min-h-0 overflow-hidden"> <div className="flex-1 min-h-0 overflow-hidden">
<AudioFileViewer path={selectedPath} /> <AudioFileViewer path={selectedPath} />
@ -4858,6 +4858,8 @@ function App() {
</pre> </pre>
</div> </div>
) )
)}
</>
) : selectedTask ? ( ) : selectedTask ? (
<div className="flex-1 min-h-0 overflow-hidden"> <div className="flex-1 min-h-0 overflow-hidden">
<BackgroundTaskDetail <BackgroundTaskDetail

View file

@ -0,0 +1,62 @@
import { useEffect, useState } from 'react'
import { HtmlFileViewer } from './html-file-viewer'
import { PdfFileViewer } from './pdf-file-viewer'
const CACHE_LIMIT = 5
function isCacheable(path: string): boolean {
const lower = path.toLowerCase()
return lower.endsWith('.html') || lower.endsWith('.htm') || lower.endsWith('.pdf')
}
function renderViewer(path: string): JSX.Element | null {
const lower = path.toLowerCase()
if (lower.endsWith('.html') || lower.endsWith('.htm')) {
return <HtmlFileViewer path={path} />
}
if (lower.endsWith('.pdf')) {
return <PdfFileViewer path={path} />
}
return null
}
interface PersistentViewerCacheProps {
activePath: string
}
/**
* Keeps recently-opened HTML and PDF viewers mounted in the DOM,
* toggling visibility instead of unmounting. This preserves iframe
* state (PDF page/zoom, HTML scroll/JS state) across file switches.
*/
export function PersistentViewerCache({ activePath }: PersistentViewerCacheProps) {
const [mountedPaths, setMountedPaths] = useState<string[]>(() =>
isCacheable(activePath) ? [activePath] : []
)
useEffect(() => {
if (!isCacheable(activePath)) return
setMountedPaths((prev) => {
if (prev.includes(activePath)) {
// Move to most-recent position
return [...prev.filter((p) => p !== activePath), activePath]
}
const next = [...prev, activePath]
return next.length > CACHE_LIMIT ? next.slice(-CACHE_LIMIT) : next
})
}, [activePath])
return (
<div className="relative h-full w-full">
{mountedPaths.map((p) => (
<div
key={p}
className="absolute inset-0"
style={{ display: p === activePath ? 'block' : 'none' }}
>
{renderViewer(p)}
</div>
))}
</div>
)
}