1010 button { font : inherit; padding : 0.5rem 1rem ; cursor : pointer; }
1111 pre { background : # 111 ; color : # b9f ; padding : 1rem ; border-radius : 6px ; overflow : auto; }
1212 .ok { color : # 5c5 ; } .bad { color : # f55 ; } .dim { color : # 888 ; }
13+ .loader { display : none; width : 16px ; height : 16px ; border : 2px solid # ddd ; border-top-color : # 5c5 ; border-radius : 50% ; animation : spin .6s linear infinite; margin-left : .5rem ; vertical-align : middle; }
14+ @keyframes spin { to { transform : rotate (360deg ); } }
1315 </ style >
1416 </ head >
1517 < body >
@@ -35,6 +37,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
3537 < button id ="persist "> Test persistence (M2)</ button >
3638 < button id ="encrypt "> Encrypted persist (M6)</ button >
3739 < button id ="delete "> Delete + compaction</ button >
40+ < span class ="loader " id ="loader "> </ span >
3841 </ p >
3942 < pre id ="out "> ready.</ pre >
4043
@@ -44,6 +47,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
4447 const out = document . getElementById ( 'out' ) ;
4548 const log = ( msg , cls = '' ) =>
4649 ( out . innerHTML += `\n${ cls ? `<span class="${ cls } ">${ msg } </span>` : msg } ` ) ;
50+ const showLoader = ( ) => document . getElementById ( 'loader' ) . style . display = 'inline-block' ;
51+ const hideLoader = ( ) => document . getElementById ( 'loader' ) . style . display = 'none' ;
4752
4853 function randVec ( dim ) {
4954 const v = new Float32Array ( dim ) ;
@@ -75,9 +80,10 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
7580
7681 document . getElementById ( 'run' ) . onclick = async ( ) => {
7782 out . textContent = '' ;
83+ showLoader ( ) ;
7884 const support = BrowserVec . isSupported ( ) ;
7985 log ( `support: ${ JSON . stringify ( support ) } ` ) ;
80- if ( ! support . webgpu ) return log ( 'WebGPU not available in this browser.' , 'bad' ) ;
86+ if ( ! support . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available in this browser.' , 'bad' ) ; }
8187
8288 const n = + document . getElementById ( 'n' ) . value ;
8389 const dim = + document . getElementById ( 'dim' ) . value ;
@@ -112,12 +118,14 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
112118 log ( `top hit: ${ gpuIds [ 0 ] } (gpu score ${ hits [ 0 ] . score . toFixed ( 4 ) } , cpu ${ ref [ 0 ] . score . toFixed ( 4 ) } )` ) ;
113119
114120 db . destroy ( ) ;
121+ hideLoader ( ) ;
115122 } ;
116123
117124 // M3: compare int8 TurboQuant (with GPU kernel) against fp32 flat ground truth.
118125 document . getElementById ( 'quant' ) . onclick = async ( ) => {
119126 out . textContent = '' ;
120- if ( ! BrowserVec . isSupported ( ) . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
127+ showLoader ( ) ;
128+ if ( ! BrowserVec . isSupported ( ) . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
121129
122130 const n = + document . getElementById ( 'n' ) . value ;
123131 const dim = + document . getElementById ( 'dim' ) . value ;
@@ -166,6 +174,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
166174 qd . destroy ( ) ;
167175 }
168176 log ( `(raw recall drops as bits shrink — int4, then 1-bit; exact re-rank recovers it — that's the design)` , 'dim' ) ;
177+ hideLoader ( ) ;
169178 } ;
170179 function nextPow2 ( n ) { let p = 4 ; while ( p < n ) p <<= 1 ; return p ; }
171180
@@ -191,6 +200,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
191200 // adapter limits, OPFS/WASM-SIMD/Worker support, and the CPU-fallback path.
192201 document . getElementById ( 'm6report' ) . onclick = async ( ) => {
193202 out . textContent = '' ;
203+ showLoader ( ) ;
194204 log ( 'Running M6 device report — ~10–40s depending on device. Keep this tab focused.\n' , 'dim' ) ;
195205
196206 // Deterministic PRNG (mulberry32) → identical corpus on every device.
@@ -309,12 +319,14 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
309319 log ( JSON . stringify ( report ) ) ;
310320 log ( '=== END BROWSERVEC M6 REPORT ===' , 'ok' ) ;
311321 log ( '\nSelect everything between the BEGIN/END lines and paste it back (one per device).' , 'dim' ) ;
322+ hideLoader ( ) ;
312323 } ;
313324
314325 // M4: IVF approximate index vs fp32 flat ground truth, sweeping nprobe.
315326 document . getElementById ( 'ivf' ) . onclick = async ( ) => {
316327 out . textContent = '' ;
317- if ( ! BrowserVec . isSupported ( ) . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
328+ showLoader ( ) ;
329+ if ( ! BrowserVec . isSupported ( ) . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
318330
319331 const n = + document . getElementById ( 'n' ) . value ;
320332 const dim = + document . getElementById ( 'dim' ) . value ;
@@ -365,13 +377,15 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
365377 ) ;
366378 }
367379 db . destroy ( ) ;
380+ hideLoader ( ) ;
368381 } ;
369382
370383 // M4 combo: IVF × int8 — clusters + quantizes so 1M fits AND only nprobe lists
371384 // are scanned. Compares against fp32 flat truth and reports memory + recall.
372385 document . getElementById ( 'combo' ) . onclick = async ( ) => {
373386 out . textContent = '' ;
374- if ( ! BrowserVec . isSupported ( ) . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
387+ showLoader ( ) ;
388+ if ( ! BrowserVec . isSupported ( ) . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
375389
376390 const n = + document . getElementById ( 'n' ) . value ;
377391 const dim = + document . getElementById ( 'dim' ) . value ;
@@ -441,12 +455,14 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
441455 log ( ` nprobe=${ nlist } (full scan, diagnostic): recall@${ k } =${ ( dh / ( queries * k ) ) . toFixed ( 3 ) } ` , 'dim' ) ;
442456 db . destroy ( ) ;
443457 }
458+ hideLoader ( ) ;
444459 } ;
445460
446461 // M5: end-to-end text retrieval, fully offline (zero-dep hashing embedder).
447462 document . getElementById ( 'text' ) . onclick = async ( ) => {
448463 out . textContent = '' ;
449- if ( ! BrowserVec . isSupported ( ) . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
464+ showLoader ( ) ;
465+ if ( ! BrowserVec . isSupported ( ) . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
450466
451467 const embedder = hashingEmbedder ( { dimension : 384 } ) ;
452468 const db = await BrowserVec . create ( { dimension : 384 , metric : 'cosine' , embedder } ) ;
@@ -470,6 +486,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
470486 log ( `\nnote: hashingEmbedder is lexical (keyword), not semantic.` , 'dim' ) ;
471487 log ( `swap transformersEmbedder() for meaning — same store/query code.` , 'dim' ) ;
472488 db . destroy ( ) ;
489+ hideLoader ( ) ;
473490 } ;
474491
475492 // NFR-8: the CPU-heavy rotate+quantize of a quantized ingest runs in a Web
@@ -479,7 +496,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
479496 // the UI would stay interactive during ingest.
480497 document . getElementById ( 'worker' ) . onclick = async ( ) => {
481498 out . textContent = '' ;
482- if ( ! BrowserVec . isSupported ( ) . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
499+ showLoader ( ) ;
500+ if ( ! BrowserVec . isSupported ( ) . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
483501
484502 const n = + document . getElementById ( 'n' ) . value ;
485503 const dim = + document . getElementById ( 'dim' ) . value ;
@@ -573,6 +591,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
573591 log ( `(centroid mean-update ran ${ tmode === 'worker' ? 'in the Worker' : 'in-thread' } ; ` +
574592 `results are byte-identical either way)` , 'dim' ) ;
575593 idb . destroy ( ) ;
594+ hideLoader ( ) ;
576595 } ;
577596
578597 // NFR-10: a single GPU storage buffer is capped (128 MiB on many devices),
@@ -582,7 +601,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
582601 // store (flat is exact, so results must be identical, not just close).
583602 document . getElementById ( 'chunk' ) . onclick = async ( ) => {
584603 out . textContent = '' ;
585- if ( ! BrowserVec . isSupported ( ) . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
604+ showLoader ( ) ;
605+ if ( ! BrowserVec . isSupported ( ) . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
586606
587607 const n = + document . getElementById ( 'n' ) . value ;
588608 const dim = + document . getElementById ( 'dim' ) . value ;
@@ -641,6 +661,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
641661 db . destroy ( ) ;
642662 }
643663 log ( `\n→ chunked scan is exact — a corpus can now exceed one GPU buffer.` , 'ok' ) ;
664+ hideLoader ( ) ;
644665 } ;
645666
646667 // GPU top-k: past a few thousand rows the flat index reduces on the GPU
@@ -650,7 +671,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
650671 // switch-over threshold to show the readback that was eliminated.
651672 document . getElementById ( 'gputopk' ) . onclick = async ( ) => {
652673 out . textContent = '' ;
653- if ( ! BrowserVec . isSupported ( ) . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
674+ showLoader ( ) ;
675+ if ( ! BrowserVec . isSupported ( ) . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
654676
655677 const dim = + document . getElementById ( 'dim' ) . value ;
656678 const k = 10 ;
@@ -774,6 +796,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
774796
775797 log ( `\n→ GPU top-k returns the same neighbors while reading back only` +
776798 ` ceil(N/256)·k pairs instead of all N scores — now on the quantized and IVF paths too.` , 'ok' ) ;
799+ hideLoader ( ) ;
777800 } ;
778801
779802 // M6 / NFR-7: on browsers without WebGPU the library falls back to an exact
@@ -782,7 +805,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
782805 // returns the same top-k as the real GPU path on the same data.
783806 document . getElementById ( 'cpufallback' ) . onclick = async ( ) => {
784807 out . textContent = '' ;
785- if ( ! BrowserVec . isSupported ( ) . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
808+ showLoader ( ) ;
809+ if ( ! BrowserVec . isSupported ( ) . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
786810
787811 const dim = + document . getElementById ( 'dim' ) . value ;
788812 const n = Math . min ( 20000 , + document . getElementById ( 'n' ) . value ) ;
@@ -826,14 +850,16 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
826850 } finally {
827851 delete globalThis . __BROWSERVEC_FORCE_CPU__ ;
828852 }
853+ hideLoader ( ) ;
829854 } ;
830855
831856 // M2: save to OPFS/IndexedDB, drop the instance, reload, confirm it survives.
832857 document . getElementById ( 'persist' ) . onclick = async ( ) => {
833858 out . textContent = '' ;
859+ showLoader ( ) ;
834860 const support = BrowserVec . isSupported ( ) ;
835861 log ( `support: ${ JSON . stringify ( support ) } ` ) ;
836- if ( ! support . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
862+ if ( ! support . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
837863
838864 const dim = 64 ;
839865 const name = 'demo-session' ;
@@ -873,15 +899,17 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
873899
874900 db2 . destroy ( ) ;
875901 db3 . destroy ( ) ;
902+ hideLoader ( ) ;
876903 } ;
877904
878905 // M6: persisted snapshots + exported blobs encrypted at rest with a passphrase
879906 // (AES-256-GCM, PBKDF2 key). Auto-load decrypts; a wrong passphrase or tamper
880907 // fails loudly. Data never leaves the device — and now it's safe at rest too.
881908 document . getElementById ( 'encrypt' ) . onclick = async ( ) => {
882909 out . textContent = '' ;
883- if ( ! BrowserVec . isSupported ( ) . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
884- if ( ! globalThis . crypto ?. subtle ) return log ( 'WebCrypto (crypto.subtle) unavailable.' , 'bad' ) ;
910+ showLoader ( ) ;
911+ if ( ! BrowserVec . isSupported ( ) . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
912+ if ( ! globalThis . crypto ?. subtle ) { hideLoader ( ) ; return log ( 'WebCrypto (crypto.subtle) unavailable.' , 'bad' ) ; }
885913
886914 const dim = 64 ;
887915 const name = 'enc-demo' ;
@@ -936,6 +964,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
936964 log ( `\nencrypted export→import round-trip: [${ imported } ]` , imported === before ? 'ok' : 'bad' ) ;
937965 db2 . destroy ( ) ;
938966 db3 . destroy ( ) ;
967+ hideLoader ( ) ;
939968 } ;
940969
941970 // Deletion: rows are tombstoned (filtered from results immediately, cheap) and
@@ -944,7 +973,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
944973 // that a reload drops them for good. Runs on fp32 and int8×IVF.
945974 document . getElementById ( 'delete' ) . onclick = async ( ) => {
946975 out . textContent = '' ;
947- if ( ! BrowserVec . isSupported ( ) . webgpu ) return log ( 'WebGPU not available.' , 'bad' ) ;
976+ showLoader ( ) ;
977+ if ( ! BrowserVec . isSupported ( ) . webgpu ) { hideLoader ( ) ; return log ( 'WebGPU not available.' , 'bad' ) ; }
948978
949979 const dim = 128 ;
950980 const n = 5000 ;
@@ -985,6 +1015,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
9851015 db . destroy ( ) ;
9861016 reloaded . destroy ( ) ;
9871017 }
1018+ hideLoader ( ) ;
9881019 } ;
9891020 </ script >
9901021 </ body >
0 commit comments