@@ -21,7 +21,6 @@ import { ParticleSystem } from './viz/particles.js';
2121import * as controls from './ui/controls.js' ;
2222import * as quiz from './ui/quiz.js' ;
2323import * as modes from './ui/modes.js' ;
24- // Re-export isAutoAdvance check used in handleAnswer auto-advance logic
2524import * as insights from './ui/insights.js' ;
2625import * as share from './ui/share.js' ;
2726import { showDownload , hideDownload , updateConfidence , initConfidence } from './ui/progress.js' ;
@@ -31,9 +30,6 @@ const GLOBAL_REGION = { x_min: 0, x_max: 1, y_min: 0, y_max: 1 };
3130const GLOBAL_GRID_SIZE = 50 ;
3231
3332// Uniform length scale for all observations — no per-domain variation.
34- // Previously sub-domains used 0.075 and "all" used 0.30, which created jagged,
35- // inconsistent smoothness. A single scale of 0.18 produces natural gradients
36- // while preserving spatial structure in the Matérn 3/2 kernel.
3733const UNIFORM_LENGTH_SCALE = 0.18 ;
3834
3935let renderer = null ;
@@ -42,13 +38,15 @@ let particleSystem = null;
4238let estimator = null ;
4339let globalEstimator = null ; // Always covers GLOBAL_REGION for minimap
4440let sampler = null ;
45- let currentDomainBundle = null ;
41+ let allDomainBundle = null ; // Permanent "all" domain data — never replaced
42+ let currentDomainBundle = null ; // Points to allDomainBundle once loaded
4643let currentViewport = { x_min : 0 , x_max : 1 , y_min : 0 , y_max : 1 } ;
4744let currentDomainRegion = GLOBAL_REGION ;
4845let currentGridSize = GLOBAL_GRID_SIZE ;
4946let domainQuestionCount = 0 ;
5047let switchGeneration = 0 ;
5148let questionIndex = new Map ( ) ;
49+ let mapInitialized = false ; // True once articles/questions/labels are set on the renderer
5250
5351async function boot ( ) {
5452 const storageAvailable = isAvailable ( ) ;
@@ -87,6 +85,21 @@ async function boot() {
8785 globalEstimator = new Estimator ( ) ;
8886 globalEstimator . init ( GLOBAL_GRID_SIZE , GLOBAL_REGION ) ;
8987 sampler = new Sampler ( ) ;
88+ sampler . configure ( GLOBAL_GRID_SIZE , GLOBAL_REGION ) ;
89+
90+ // Eagerly load the "all" domain — this is the permanent, full dataset.
91+ // All articles, questions, and labels come from here; domain selection
92+ // only pans/zooms the viewport rather than replacing data.
93+ try {
94+ allDomainBundle = await loadDomain ( 'all' , { } ) ;
95+ indexQuestions ( allDomainBundle . questions ) ;
96+ questionIndex = new Map ( allDomainBundle . questions . map ( q => [ q . id , q ] ) ) ;
97+ insights . setConcepts ( allDomainBundle . questions , allDomainBundle . articles ) ;
98+ } catch ( err ) {
99+ console . error ( '[app] Failed to pre-load "all" domain:' , err ) ;
100+ showLandingError ( 'Could not load map data. Please try refreshing.' ) ;
101+ return ;
102+ }
90103
91104 const headerEl = document . getElementById ( 'app-header' ) ;
92105 controls . init ( headerEl ) ;
@@ -119,7 +132,6 @@ async function boot() {
119132 if ( trophyBtn ) {
120133 trophyBtn . addEventListener ( 'click' , ( ) => {
121134 if ( ! globalEstimator ) return ;
122- // Use global estimator so leaderboard reflects the full map, not just the current domain
123135 const ck = insights . computeConceptKnowledge (
124136 globalEstimator . predict ( ) ,
125137 GLOBAL_REGION ,
@@ -134,7 +146,6 @@ async function boot() {
134146 if ( suggestBtn ) {
135147 suggestBtn . addEventListener ( 'click' , ( ) => {
136148 if ( ! globalEstimator ) return ;
137- // Use global estimator so suggestions reflect the full map, not just the current domain
138149 const ck = insights . computeConceptKnowledge (
139150 globalEstimator . predict ( ) ,
140151 GLOBAL_REGION ,
@@ -177,12 +188,10 @@ async function boot() {
177188 else renderer . jumpTo ( region ) ;
178189 } ) ;
179190
180- // Load "all" domain articles for a static minimap background
181- loadDomain ( 'all' , { } ) . then ( ( allBundle ) => {
182- if ( minimap && allBundle ) {
183- minimap . setArticles ( articlesToPoints ( allBundle . articles ) ) ;
184- }
185- } ) . catch ( ( ) => { } ) ; // Non-critical, minimap will just lack article dots
191+ // Use the pre-loaded "all" domain for minimap background
192+ if ( allDomainBundle ) {
193+ minimap . setArticles ( articlesToPoints ( allDomainBundle . articles ) ) ;
194+ }
186195 }
187196
188197 if ( import . meta. env . DEV ) {
@@ -261,6 +270,11 @@ function responsesToAnsweredDots(responses, qIndex) {
261270 return dots ;
262271}
263272
273+ /**
274+ * Switch to a domain — now only pans/zooms the viewport.
275+ * All articles, questions, and labels remain from the "all" domain loaded at boot.
276+ * The first call also initializes the map display (articles, labels, estimator restore).
277+ */
264278async function switchDomain ( domainId ) {
265279 const generation = ++ switchGeneration ;
266280 const landing = document . getElementById ( 'landing' ) ;
@@ -274,11 +288,13 @@ async function switchDomain(domainId) {
274288 particleSystem = null ;
275289 }
276290
277- const quizPanel = document . getElementById ( 'quiz-panel' ) ;
278- const previousBundle = currentDomainBundle ;
279-
280291 renderer . abortTransition ( ) ;
281292
293+ if ( ! allDomainBundle ) return ;
294+
295+ // Look up the target domain's region for viewport navigation.
296+ // Load the domain JSON (cached after first fetch) just for its region metadata.
297+ let targetRegion = GLOBAL_REGION ;
282298 try {
283299 const bundle = await loadDomain ( domainId , {
284300 onProgress : ( { loaded, total } ) => showDownload ( loaded , total ) ,
@@ -287,30 +303,23 @@ async function switchDomain(domainId) {
287303 announce ( `Failed to load domain. ${ err . message } ` ) ;
288304 } ,
289305 } ) ;
290-
291306 if ( generation !== switchGeneration ) return ;
307+ if ( bundle && bundle . domain && bundle . domain . region ) {
308+ targetRegion = bundle . domain . region ;
309+ }
310+ } catch ( err ) {
311+ if ( generation === switchGeneration ) {
312+ console . error ( '[app] switchDomain region lookup failed:' , err ) ;
313+ }
314+ }
315+
316+ // First-time map initialization: set all articles, questions, labels, and restore GP
317+ if ( ! mapInitialized ) {
318+ currentDomainBundle = allDomainBundle ;
319+ renderer . addQuestions ( allDomainBundle . questions ) ;
320+ renderer . setLabels ( allDomainBundle . labels , GLOBAL_REGION , GLOBAL_GRID_SIZE ) ;
292321
293- currentDomainBundle = bundle ;
294- indexQuestions ( bundle . questions ) ;
295- questionIndex = new Map ( bundle . questions . map ( q => [ q . id , q ] ) ) ;
296- renderer . clearQuestions ( ) ;
297- renderer . addQuestions ( bundle . questions ) ;
298- insights . setConcepts ( bundle . questions , bundle . articles ) ;
299- domainQuestionCount = $responses . get ( ) . filter ( r => r . domain_id === domainId ) . length ;
300- modes . updateAvailability ( domainQuestionCount ) ;
301- updateInsightButtons ( $responses . get ( ) . length ) ;
302-
303- const domain = bundle . domain ;
304- const domainRegion = domain . region || GLOBAL_REGION ;
305- const domainGrid = domain . grid_size || GLOBAL_GRID_SIZE ;
306- currentDomainRegion = domainRegion ;
307- currentGridSize = domainGrid ;
308-
309- estimator . init ( domainGrid , domainRegion ) ;
310- sampler . configure ( domainGrid , domainRegion ) ;
311-
312- // Enrich any responses missing x/y from the freshly-loaded question index.
313- // This handles older exports that lacked coordinates.
322+ // Enrich any responses missing x/y from the question index.
314323 let allResponses = $responses . get ( ) ;
315324 let enriched = 0 ;
316325 const patched = allResponses . map ( r => {
@@ -328,9 +337,7 @@ async function switchDomain(domainId) {
328337 allResponses = patched ;
329338 }
330339
331- const relevantResponses = allResponses . filter (
332- ( r ) => r . x != null && r . y != null
333- ) ;
340+ const relevantResponses = allResponses . filter ( r => r . x != null && r . y != null ) ;
334341 if ( relevantResponses . length > 0 ) {
335342 estimator . restore ( relevantResponses , UNIFORM_LENGTH_SCALE ) ;
336343 globalEstimator . restore ( relevantResponses , UNIFORM_LENGTH_SCALE ) ;
@@ -339,47 +346,37 @@ async function switchDomain(domainId) {
339346 const estimates = estimator . predict ( ) ;
340347 $estimates . set ( estimates ) ;
341348
342- const targetPoints = articlesToPoints ( bundle . articles ) ;
349+ renderer . setPoints ( articlesToPoints ( allDomainBundle . articles ) ) ;
350+ renderer . setAnsweredQuestions ( responsesToAnsweredDots ( $responses . get ( ) , questionIndex ) ) ;
343351
344- if ( previousBundle ) {
345- const sourcePoints = articlesToPoints ( previousBundle . articles ) ;
346- const sourceRegion = previousBundle . domain . region || GLOBAL_REGION ;
347- const targetRegion = domain . region || GLOBAL_REGION ;
352+ mapInitialized = true ;
353+ }
348354
349- renderer . setLabels ( bundle . labels , domainRegion , domainGrid ) ;
350- await renderer . transitionPoints (
351- sourcePoints , targetPoints , sourceRegion , targetRegion
352- ) ;
353- } else {
354- renderer . setPoints ( targetPoints ) ;
355- renderer . setLabels ( bundle . labels , domainRegion , domainGrid ) ;
356- await renderer . transitionTo ( domain . region || GLOBAL_REGION ) ;
357- }
355+ // Update domain-scoped tracking
356+ domainQuestionCount = $responses . get ( ) . length ;
357+ modes . updateAvailability ( domainQuestionCount ) ;
358+ updateInsightButtons ( $responses . get ( ) . length ) ;
358359
359- if ( generation !== switchGeneration ) return ;
360+ // Pan/zoom to the target domain's region
361+ await renderer . transitionTo ( targetRegion ) ;
360362
361- if ( minimap ) {
362- minimap . setActive ( domainId ) ;
363- minimap . setViewport ( renderer . getViewport ( ) ) ;
364- }
363+ if ( generation !== switchGeneration ) return ;
365364
366- toggleQuizPanel ( true ) ;
367- const toggleBtn = document . getElementById ( 'quiz-toggle' ) ;
368- if ( toggleBtn ) toggleBtn . removeAttribute ( 'hidden' ) ;
369- hideDownload ( ) ;
370- controls . showActionButtons ( ) ;
365+ if ( minimap ) {
366+ minimap . setActive ( domainId ) ;
367+ minimap . setViewport ( renderer . getViewport ( ) ) ;
368+ }
371369
372- announce ( `Loaded ${ domain . name } . ${ bundle . questions . length } questions available.` ) ;
370+ toggleQuizPanel ( true ) ;
371+ const toggleBtn = document . getElementById ( 'quiz-toggle' ) ;
372+ if ( toggleBtn ) toggleBtn . removeAttribute ( 'hidden' ) ;
373+ hideDownload ( ) ;
374+ controls . showActionButtons ( ) ;
373375
374- renderer . setAnsweredQuestions ( responsesToAnsweredDots ( $responses . get ( ) , questionIndex ) ) ;
376+ const domainName = registry . getDomains ( ) . find ( d => d . id === domainId ) ?. name || domainId ;
377+ announce ( `Navigated to ${ domainName } . ${ allDomainBundle . questions . length } questions available.` ) ;
375378
376- selectAndShowNextQuestion ( ) ;
377- } catch ( err ) {
378- if ( generation === switchGeneration ) {
379- hideDownload ( ) ;
380- console . error ( '[app] switchDomain failed:' , err ) ;
381- }
382- }
379+ selectAndShowNextQuestion ( ) ;
383380}
384381
385382function selectAndShowNextQuestion ( ) {
@@ -389,7 +386,7 @@ function selectAndShowNextQuestion() {
389386 const available = getAvailableQuestions ( currentDomainBundle , answeredIds ) ;
390387
391388 if ( available . length === 0 ) {
392- announce ( 'Domain fully mapped! All questions answered. Try another domain .' ) ;
389+ announce ( 'All questions answered! Great work exploring the knowledge map .' ) ;
393390 quiz . showQuestion ( null ) ;
394391 return ;
395392 }
@@ -419,9 +416,13 @@ function handleAnswer(selectedKey, question) {
419416
420417 const isCorrect = selectedKey === question . correct_answer ;
421418
419+ // Tag the response with the user's currently selected domain (for tracking),
420+ // not the bundle's domain id (which is always "all" now).
421+ const activeDomainId = $activeDomain . get ( ) || 'all' ;
422+
422423 const response = {
423424 question_id : question . id ,
424- domain_id : currentDomainBundle . domain . id ,
425+ domain_id : activeDomainId ,
425426 selected : selectedKey ,
426427 is_correct : isCorrect ,
427428 timestamp : Date . now ( ) ,
@@ -448,9 +449,9 @@ function handleAnswer(selectedKey, question) {
448449 renderer . setAnsweredQuestions ( responsesToAnsweredDots ( $responses . get ( ) , questionIndex ) ) ;
449450
450451 const feedback = isCorrect ? 'Correct!' : 'Incorrect.' ;
451-
452+
452453 const coverage = Math . round ( $coverage . get ( ) * 100 ) ;
453- announce ( `${ feedback } ${ coverage } % of domain mapped. ${ 50 - domainQuestionCount } questions remaining .` ) ;
454+ announce ( `${ feedback } ${ coverage } % mapped.` ) ;
454455
455456 // Auto-advance after a short delay if the toggle is on
456457 if ( modes . isAutoAdvance ( ) ) {
@@ -462,6 +463,7 @@ function handleReset() {
462463 if ( ! confirm ( 'Are you sure? This will clear all progress.' ) ) return ;
463464 resetAll ( ) ;
464465 currentDomainBundle = null ;
466+ mapInitialized = false ;
465467 domainQuestionCount = 0 ;
466468 currentDomainRegion = GLOBAL_REGION ;
467469 currentGridSize = GLOBAL_GRID_SIZE ;
@@ -477,11 +479,19 @@ function handleReset() {
477479 renderer . setAnsweredQuestions ( [ ] ) ;
478480 renderer . clearQuestions ( ) ;
479481 insights . resetGlobalConcepts ( ) ;
480- questionIndex = new Map ( ) ;
482+ // Re-set concepts from the permanent "all" bundle so insights work on next domain select
483+ if ( allDomainBundle ) {
484+ insights . setConcepts ( allDomainBundle . questions , allDomainBundle . articles ) ;
485+ }
486+ questionIndex = allDomainBundle
487+ ? new Map ( allDomainBundle . questions . map ( q => [ q . id , q ] ) )
488+ : new Map ( ) ;
481489 if ( minimap ) {
482490 minimap . setActive ( null ) ;
483491 minimap . setEstimates ( [ ] ) ;
484492 }
493+ // Reset viewport to full map
494+ renderer . jumpTo ( GLOBAL_REGION ) ;
485495 toggleQuizPanel ( false ) ;
486496 const toggleBtn = document . getElementById ( 'quiz-toggle' ) ;
487497 if ( toggleBtn ) toggleBtn . setAttribute ( 'hidden' , '' ) ;
@@ -557,11 +567,8 @@ function handleImport(data) {
557567 const estimates = estimator . predict ( ) ;
558568 $estimates . set ( estimates ) ;
559569
560- if ( currentDomainBundle ) {
561- const domainId = currentDomainBundle . domain . id ;
562- domainQuestionCount = merged . filter ( r => r . domain_id === domainId ) . length ;
563- modes . updateAvailability ( domainQuestionCount ) ;
564- }
570+ domainQuestionCount = merged . length ;
571+ modes . updateAvailability ( domainQuestionCount ) ;
565572 renderer . setAnsweredQuestions ( responsesToAnsweredDots ( merged , questionIndex ) ) ;
566573 }
567574
@@ -571,7 +578,6 @@ function handleImport(data) {
571578 console . log ( '[import]' , msg ) ;
572579
573580 // If we're still on the welcome screen, switch to map view with "all" domain.
574- // switchDomain will re-restore the GP from $responses (now including imports).
575581 if ( ! currentDomainBundle ) {
576582 controls . setSelectedDomain ( 'all' ) ;
577583 $activeDomain . set ( 'all' ) ;
@@ -645,7 +651,10 @@ function _showBanner(message, type = 'warning') {
645651 const dismissBtn = document . createElement ( 'button' ) ;
646652 dismissBtn . className = 'notice-banner-dismiss' ;
647653 dismissBtn . setAttribute ( 'aria-label' , 'Dismiss notification' ) ;
648- dismissBtn . innerHTML = '<i class="fa fa-times"></i>' ;
654+
655+ const iconEl = document . createElement ( 'i' ) ;
656+ iconEl . className = 'fa fa-times' ;
657+ dismissBtn . appendChild ( iconEl ) ;
649658
650659 const removeBanner = ( ) => {
651660 banner . classList . add ( 'dismissing' ) ;
0 commit comments