33import type { ToolCallMessagePartProps } from "@assistant-ui/react" ;
44import { useAtomValue , useSetAtom } from "jotai" ;
55import { useParams , usePathname } from "next/navigation" ;
6- import { useEffect , useRef , useState } from "react" ;
6+ import { useCallback , useEffect , useRef , useState } from "react" ;
77import * as pdfjsLib from "pdfjs-dist" ;
88import { z } from "zod" ;
99import { openReportPanelAtom , reportPanelAtom } from "@/atoms/chat/report-panel.atom" ;
1010import { TextShimmerLoader } from "@/components/prompt-kit/loader" ;
11- import { Spinner } from "@/components/ui/spinner" ;
1211import { useMediaQuery } from "@/hooks/use-media-query" ;
1312import { getAuthHeaders } from "@/lib/auth-utils" ;
1413
@@ -88,10 +87,22 @@ function ResumeCancelledState() {
8887 ) ;
8988}
9089
91- function PdfThumbnail ( { pdfUrl } : { pdfUrl : string } ) {
90+ function ThumbnailSkeleton ( ) {
91+ return (
92+ < div className = "h-[7rem] space-y-2" >
93+ < div className = "h-3 w-full rounded bg-muted/60 animate-pulse" />
94+ < div className = "h-3 w-[92%] rounded bg-muted/60 animate-pulse [animation-delay:100ms]" />
95+ < div className = "h-3 w-[75%] rounded bg-muted/60 animate-pulse [animation-delay:200ms]" />
96+ < div className = "h-3 w-[85%] rounded bg-muted/60 animate-pulse [animation-delay:300ms]" />
97+ < div className = "h-3 w-[60%] rounded bg-muted/60 animate-pulse [animation-delay:400ms]" />
98+ </ div >
99+ ) ;
100+ }
101+
102+ function PdfThumbnail ( { pdfUrl, onLoad, onError } : { pdfUrl : string ; onLoad : ( ) => void ; onError : ( ) => void } ) {
103+ const wrapperRef = useRef < HTMLDivElement > ( null ) ;
92104 const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
93- const [ loading , setLoading ] = useState ( true ) ;
94- const [ error , setError ] = useState ( false ) ;
105+ const [ ready , setReady ] = useState ( false ) ;
95106
96107 useEffect ( ( ) => {
97108 let cancelled = false ;
@@ -112,53 +123,43 @@ function PdfThumbnail({ pdfUrl }: { pdfUrl: string }) {
112123 const canvas = canvasRef . current ;
113124 if ( ! canvas ) { pdf . destroy ( ) ; return ; }
114125
115- const containerWidth = canvas . parentElement ?. clientWidth || 400 ;
126+ const containerWidth = wrapperRef . current ?. clientWidth || 400 ;
116127 const unscaledViewport = page . getViewport ( { scale : 1 } ) ;
117128 const fitScale = containerWidth / unscaledViewport . width ;
118129 const viewport = page . getViewport ( { scale : fitScale } ) ;
119130 const dpr = window . devicePixelRatio || 1 ;
120131
121- canvas . width = Math . floor ( viewport . width * dpr ) ;
122- canvas . height = Math . floor ( viewport . height * dpr ) ;
123- canvas . style . width = `${ Math . floor ( viewport . width ) } px` ;
124- canvas . style . height = `${ Math . floor ( viewport . height ) } px` ;
132+ canvas . width = Math . ceil ( viewport . width * dpr ) ;
133+ canvas . height = Math . ceil ( viewport . height * dpr ) ;
125134
126135 await page . render ( {
127136 canvas,
128137 viewport,
129138 transform : dpr !== 1 ? [ dpr , 0 , 0 , dpr , 0 , 0 ] : undefined ,
130139 } ) . promise ;
131- if ( ! cancelled ) setLoading ( false ) ;
132140
133- pdf . destroy ( ) ;
134- } catch {
135141 if ( ! cancelled ) {
136- setError ( true ) ;
137- setLoading ( false ) ;
142+ setReady ( true ) ;
143+ onLoad ( ) ;
138144 }
145+
146+ pdf . destroy ( ) ;
147+ } catch {
148+ if ( ! cancelled ) onError ( ) ;
139149 }
140150 } ;
141151
142152 renderThumbnail ( ) ;
143153 return ( ) => { cancelled = true ; } ;
144- } , [ pdfUrl ] ) ;
145-
146- if ( error ) {
147- return < p className = "text-sm text-muted-foreground italic" > Preview unavailable</ p > ;
148- }
154+ } , [ pdfUrl , onLoad , onError ] ) ;
149155
150156 return (
151- < >
152- { loading && (
153- < div className = "flex items-center justify-center h-[7rem]" >
154- < Spinner size = "md" />
155- </ div >
156- ) }
157+ < div ref = { wrapperRef } >
157158 < canvas
158159 ref = { canvasRef }
159- className = { loading ? "hidden" : " w-full h-auto"}
160+ className = { ready ? "w-full h-auto" : "hidden "}
160161 />
161- </ >
162+ </ div >
162163 ) ;
163164}
164165
@@ -178,6 +179,7 @@ function ResumeCard({
178179 const isDesktop = useMediaQuery ( "(min-width: 768px)" ) ;
179180 const autoOpenedRef = useRef ( false ) ;
180181 const [ pdfUrl , setPdfUrl ] = useState < string | null > ( null ) ;
182+ const [ thumbState , setThumbState ] = useState < "loading" | "ready" | "error" > ( "loading" ) ;
181183
182184 useEffect ( ( ) => {
183185 setPdfUrl (
@@ -195,6 +197,9 @@ function ResumeCard({
195197 }
196198 } , [ reportId , title , shareToken , autoOpen , isDesktop , openPanel ] ) ;
197199
200+ const onThumbLoad = useCallback ( ( ) => setThumbState ( "ready" ) , [ ] ) ;
201+ const onThumbError = useCallback ( ( ) => setThumbState ( "error" ) , [ ] ) ;
202+
198203 const isActive = panelState . isOpen && panelState . reportId === reportId ;
199204
200205 const handleOpen = ( ) => {
@@ -223,22 +228,22 @@ function ResumeCard({
223228 < div className = "mx-5 h-px bg-border/50" />
224229
225230 < div className = "px-5 pt-3 pb-4" >
226- { pdfUrl ? (
231+ { thumbState === "loading" && < ThumbnailSkeleton /> }
232+ { thumbState === "error" && (
233+ < p className = "text-sm text-muted-foreground italic" > Preview unavailable</ p >
234+ ) }
235+ { pdfUrl && (
227236 < div
228- className = " max-h-[7rem] overflow-hidden pointer-events-none mix-blend-multiply dark:mix-blend-screen"
237+ className = { ` max-h-[7rem] overflow-hidden pointer-events-none mix-blend-multiply dark:mix-blend-screen ${ thumbState !== "ready" ? "hidden" : "" } ` }
229238 style = { {
230239 maskImage : "linear-gradient(to bottom, black 50%, transparent 100%)" ,
231240 WebkitMaskImage : "linear-gradient(to bottom, black 50%, transparent 100%)" ,
232241 } }
233242 >
234243 < div className = "dark:invert dark:hue-rotate-180" >
235- < PdfThumbnail pdfUrl = { pdfUrl } />
244+ < PdfThumbnail pdfUrl = { pdfUrl } onLoad = { onThumbLoad } onError = { onThumbError } />
236245 </ div >
237246 </ div >
238- ) : (
239- < div className = "flex items-center justify-center h-[7rem]" >
240- < Spinner size = "md" />
241- </ div >
242247 ) }
243248 </ div >
244249 </ button >
0 commit comments