@@ -38,6 +38,7 @@ export class StackgazerApp {
3838 private filterInputValue : string = '' ;
3939 private stackDisplayMode : 'combined' | 'side-by-side' | 'functions' | 'locations' = 'combined' ;
4040 private filterDebounceTimer : number | null = null ;
41+ private loadingDelayTimer : number | null = null ;
4142 private tooltip ! : HTMLElement ;
4243 private currentTheme : 'dark' | 'light' = 'dark' ;
4344 private initialTheme ?: 'dark' | 'light' ;
@@ -91,6 +92,9 @@ export class StackgazerApp {
9192 ancestryEmptyState : {
9293 content : { cloneNode : ( deep ?: boolean ) => DocumentFragment ; firstElementChild : HTMLElement } ;
9394 } ;
95+ loadingOverlay : {
96+ content : { cloneNode : ( deep ?: boolean ) => DocumentFragment ; firstElementChild : HTMLElement } ;
97+ } ;
9498 } ;
9599
96100 constructor ( containerId : string , defaultsModifier ?: ( defaults : AppSettings ) => AppSettings ) {
@@ -171,6 +175,12 @@ export class StackgazerApp {
171175 ) as DocumentFragment ;
172176 const settingsModalElement = settingsModalFragment . firstElementChild as HTMLElement ;
173177
178+ // Create loading overlay
179+ const loadingOverlayFragment = this . templates . loadingOverlay . content . cloneNode (
180+ true
181+ ) as DocumentFragment ;
182+ const loadingOverlayElement = loadingOverlayFragment . firstElementChild as HTMLElement ;
183+
174184 // Insert sidebar and main content into container
175185 const sidebarContainer = containerElement . querySelector ( '#sidebar' ) as HTMLElement ;
176186 const mainContentContainer = containerElement . querySelector ( '#mainContent' ) as HTMLElement ;
@@ -183,6 +193,7 @@ export class StackgazerApp {
183193 // Add everything to the main container
184194 this . container . appendChild ( containerElement ) ;
185195 this . container . appendChild ( settingsModalElement ) ;
196+ this . container . appendChild ( loadingOverlayElement ) ;
186197 }
187198
188199 private initializeUI ( ) : void {
@@ -769,8 +780,27 @@ export class StackgazerApp {
769780 }
770781
771782 private async handleFiles ( files : File [ ] ) : Promise < void > {
783+ const fileCount = files . length ;
784+ const fileNames = files . map ( f => f . name ) . join ( ', ' ) ;
785+
786+ // Show loading overlay
787+ this . showLoadingOverlay (
788+ fileCount === 1 ? 'Processing file...' : `Processing ${ fileCount } files...` ,
789+ fileCount === 1 ? files [ 0 ] . name : fileNames
790+ ) ;
791+
772792 try {
773- for ( const file of files ) {
793+ for ( let i = 0 ; i < files . length ; i ++ ) {
794+ const file = files [ i ] ;
795+
796+ // Update progress
797+ if ( fileCount > 1 ) {
798+ this . updateLoadingMessage (
799+ `Processing file ${ i + 1 } of ${ fileCount } ...` ,
800+ file . name
801+ ) ;
802+ }
803+
774804 // Check if it's a zip file
775805 if ( file . name . toLowerCase ( ) . endsWith ( '.zip' ) || file . type === 'application/zip' ) {
776806 await this . handleZipFile ( file ) ;
@@ -786,32 +816,61 @@ export class StackgazerApp {
786816 } else {
787817 console . timeEnd ( `📄 File Import: ${ file . name } ` ) ;
788818 console . error ( `Failed to parse ${ file . name } :` , result . error ) ;
819+ this . hideLoadingOverlay ( ) ;
789820 alert ( `Failed to parse ${ file . name } : ${ result . error } ` ) ;
821+ return ;
790822 }
791823 }
792824 }
793825
826+ // Update loading message for rendering
827+ this . updateLoadingMessage ( 'Rendering results...' , 'Organizing data and applying filters' ) ;
828+
794829 // Render new content but preserve filter state
795830 this . render ( ) ;
796831
797832 // Always reapply current filter to ensure proper visibility state
798833 this . setFilter ( this . buildCurrentFilter ( ) ) ;
834+
835+ // Hide loading overlay
836+ this . hideLoadingOverlay ( ) ;
799837 } catch ( error ) {
800838 console . error ( 'Error handling files:' , error ) ;
839+ this . hideLoadingOverlay ( ) ;
801840 alert ( `Error handling files: ${ error } ` ) ;
802841 }
803842 }
804843
805844 private async handleZipFile ( file : File ) : Promise < void > {
806845 try {
807846 console . time ( `🗜 Zip Import: ${ file . name } ` ) ;
847+
848+ // Update loading message for ZIP extraction
849+ this . updateLoadingMessage ( 'Extracting ZIP file...' , `Reading ${ file . name } ` ) ;
808850
809851 // Extract files using ZipHandler with settings pattern
810852 const pattern = this . settingsManager . getZipFilePatternRegex ( ) ;
811853 const extractResult = await ZipHandler . extractFiles ( file , pattern ) ;
812854
813- for ( const zipFile of extractResult . files ) {
855+ if ( extractResult . files . length === 0 ) {
856+ console . warn ( `No stack trace files found in zip matching pattern: ${ pattern } ` ) ;
857+ throw new Error ( `No stack trace files found in zip file. Looking for files matching: ${ pattern } ` ) ;
858+ }
859+
860+ for ( let i = 0 ; i < extractResult . files . length ; i ++ ) {
861+ const zipFile = extractResult . files [ i ] ;
814862 const baseName = zipFile . path . split ( '/' ) . pop ( ) || zipFile . path ;
863+
864+ // Update progress for multiple files in ZIP
865+ if ( extractResult . files . length > 1 ) {
866+ this . updateLoadingMessage (
867+ `Processing ZIP entry ${ i + 1 } of ${ extractResult . files . length } ...` ,
868+ baseName
869+ ) ;
870+ } else {
871+ this . updateLoadingMessage ( 'Processing ZIP entry...' , baseName ) ;
872+ }
873+
815874 console . time ( ` 📄 Zip Entry: ${ baseName } ` ) ;
816875 const result = await this . parser . parseFile ( zipFile . content , baseName ) ;
817876
@@ -821,22 +880,19 @@ export class StackgazerApp {
821880 } else {
822881 console . timeEnd ( ` 📄 Zip Entry: ${ baseName } ` ) ;
823882 console . error ( `Failed to parse ${ baseName } from zip:` , result . error ) ;
824- alert ( `Failed to parse ${ baseName } from zip: ${ result . error } ` ) ;
883+ throw new Error ( `Failed to parse ${ baseName } from zip: ${ result . error } ` ) ;
825884 }
826885 }
827886
828- if ( extractResult . files . length === 0 ) {
829- console . warn ( `No stack trace files found in zip matching pattern: ${ pattern } ` ) ;
830- alert ( `No stack trace files found in zip file. Looking for files matching: ${ pattern } ` ) ;
831- }
832887 console . timeEnd ( `🗜 Zip Import: ${ file . name } ` ) ;
833888 } catch ( error ) {
834889 console . timeEnd ( `🗜 Zip Import: ${ file . name } ` ) ;
835890 console . error ( `Error processing zip file ${ file . name } :` , error ) ;
836- alert ( `Failed to process zip file ${ file . name } : ${ error } ` ) ;
891+ throw error ; // Re-throw to be handled by handleFiles
837892 }
838893 }
839894
895+
840896 private debouncedSetFilter ( query : string ) : void {
841897 // Clear any existing timer
842898 if ( this . filterDebounceTimer !== null ) {
@@ -2343,20 +2399,91 @@ export class StackgazerApp {
23432399 fileNameSpan . addEventListener ( 'blur' , onBlur ) ;
23442400 }
23452401
2402+ private showLoadingOverlay ( message : string , details ?: string ) : void {
2403+ // Clear any existing delay timer
2404+ if ( this . loadingDelayTimer !== null ) {
2405+ clearTimeout ( this . loadingDelayTimer ) ;
2406+ this . loadingDelayTimer = null ;
2407+ }
2408+
2409+ // Set up delayed showing with 300ms delay
2410+ this . loadingDelayTimer = window . setTimeout ( ( ) => {
2411+ const overlay = document . getElementById ( 'loadingOverlay' ) as HTMLElement ;
2412+ const messageEl = document . getElementById ( 'loadingMessage' ) as HTMLElement ;
2413+ const detailsEl = document . getElementById ( 'loadingDetails' ) as HTMLElement ;
2414+
2415+ if ( overlay && messageEl ) {
2416+ messageEl . textContent = message ;
2417+ if ( detailsEl && details ) {
2418+ detailsEl . textContent = details ;
2419+ detailsEl . style . display = 'block' ;
2420+ } else if ( detailsEl ) {
2421+ detailsEl . style . display = 'none' ;
2422+ }
2423+ overlay . style . display = 'flex' ;
2424+ }
2425+ this . loadingDelayTimer = null ;
2426+ } , 300 ) ;
2427+ }
2428+
2429+ private hideLoadingOverlay ( ) : void {
2430+ // Cancel the delayed showing if it hasn't triggered yet
2431+ if ( this . loadingDelayTimer !== null ) {
2432+ clearTimeout ( this . loadingDelayTimer ) ;
2433+ this . loadingDelayTimer = null ;
2434+ }
2435+
2436+ // Hide overlay if it's already shown
2437+ const overlay = document . getElementById ( 'loadingOverlay' ) as HTMLElement ;
2438+ if ( overlay ) {
2439+ overlay . style . display = 'none' ;
2440+ }
2441+ }
2442+
2443+ private updateLoadingMessage ( message : string , details ?: string ) : void {
2444+ // If we have a delay timer running, cancel it and show immediately
2445+ if ( this . loadingDelayTimer !== null ) {
2446+ clearTimeout ( this . loadingDelayTimer ) ;
2447+ this . loadingDelayTimer = null ;
2448+
2449+ // Show overlay immediately with updated message
2450+ const overlay = document . getElementById ( 'loadingOverlay' ) as HTMLElement ;
2451+ if ( overlay ) {
2452+ overlay . style . display = 'flex' ;
2453+ }
2454+ }
2455+
2456+ // Update the message content
2457+ const messageEl = document . getElementById ( 'loadingMessage' ) as HTMLElement ;
2458+ const detailsEl = document . getElementById ( 'loadingDetails' ) as HTMLElement ;
2459+
2460+ if ( messageEl ) {
2461+ messageEl . textContent = message ;
2462+ }
2463+ if ( detailsEl && details ) {
2464+ detailsEl . textContent = details ;
2465+ detailsEl . style . display = 'block' ;
2466+ } else if ( detailsEl ) {
2467+ detailsEl . style . display = 'none' ;
2468+ }
2469+ }
2470+
23462471 private setupDemoButtons ( ) : void {
23472472 const demoSingleBtn = document . getElementById ( 'demoSingleBtn' ) ;
23482473 const demoZipBtn = document . getElementById ( 'demoZipBtn' ) ;
23492474
23502475 if ( demoSingleBtn ) {
23512476 demoSingleBtn . addEventListener ( 'click' , async e => {
23522477 e . preventDefault ( ) ;
2478+ this . showLoadingOverlay ( 'Loading demo file...' , 'Downloading single node goroutine dump' ) ;
23532479 try {
23542480 const rawUrl =
23552481 'https://raw.githubusercontent.com/dt/crdb-stacks-examples/refs/heads/main/stacks/files/1/stacks.txt' ;
23562482 await this . loadFromUrl ( rawUrl , 'crdb-demo-single.txt' ) ;
23572483 } catch ( error ) {
23582484 const msg = error && ( error as any ) . message ? ( error as any ) . message : String ( error ) ;
23592485 console . error ( 'Demo file load error:' , error ) ;
2486+ this . hideLoadingOverlay ( ) ;
23602487 alert (
23612488 `Failed to load demo file. Please try again or check your internet connection (${ msg } ).`
23622489 ) ;
@@ -2367,12 +2494,14 @@ export class StackgazerApp {
23672494 if ( demoZipBtn ) {
23682495 demoZipBtn . addEventListener ( 'click' , async e => {
23692496 e . preventDefault ( ) ;
2497+ this . showLoadingOverlay ( 'Loading demo ZIP...' , 'Downloading ZIP file with 4 node stack dumps' ) ;
23702498 try {
23712499 const url =
23722500 'https://raw.githubusercontent.com/dt/crdb-stacks-examples/refs/heads/main/stacks.zip' ;
23732501 await this . loadFromUrl ( url , 'crdb-demo.zip' ) ;
23742502 } catch ( error ) {
23752503 console . error ( 'Demo zip load error:' , error ) ;
2504+ this . hideLoadingOverlay ( ) ;
23762505 alert (
23772506 'Failed to load demo zip file. Please try again or check your internet connection.'
23782507 ) ;
@@ -2435,10 +2564,16 @@ export class StackgazerApp {
24352564 }
24362565 }
24372566
2567+ // Update loading message for rendering
2568+ this . updateLoadingMessage ( 'Rendering results...' , 'Organizing data and applying filters' ) ;
2569+
24382570 this . render ( ) ;
24392571
24402572 // Always reapply current filter to ensure proper visibility state
24412573 this . setFilter ( this . buildCurrentFilter ( ) ) ;
2574+
2575+ // Hide loading overlay
2576+ this . hideLoadingOverlay ( ) ;
24422577 } catch ( error ) {
24432578 // End any ongoing timers in case of error
24442579 if ( fileName . endsWith ( '.zip' ) ) {
0 commit comments