77 WebRenderingOutput ,
88 type TextCatalogOutput ,
99 type GlossaryOutput ,
10+ type QuizGenerationOutput ,
1011 type TTSOutput ,
12+ type Quiz ,
1113} from "@adt/types"
1214import { createBookStorage , type Storage } from "@adt/storage"
1315import {
@@ -18,6 +20,10 @@ import {
1820 buildGlossaryJson ,
1921 getBaseLanguage ,
2022 normalizeLocale ,
23+ renderQuizHtml ,
24+ buildQuizAnswers ,
25+ buildTextCatalog ,
26+ pad3 ,
2127} from "@adt/pipeline"
2228
2329// ---------------------------------------------------------------------------
@@ -44,6 +50,7 @@ const MIME_TYPES: Record<string, string> = {
4450 ".wav" : "audio/wav" ,
4551 ".ogg" : "audio/ogg" ,
4652 ".map" : "application/json" ,
53+ ".dic" : "application/octet-stream" ,
4754}
4855
4956function getMimeType ( filePath : string ) : string {
@@ -54,7 +61,7 @@ function getMimeType(filePath: string): string {
5461// Caches (built lazily per book)
5562// ---------------------------------------------------------------------------
5663
57- const tailwindCssCache = new Map < string , string > ( )
64+ // No Tailwind cache — preview is a dev tool; staleness causes more pain than rebuild cost
5865
5966// ---------------------------------------------------------------------------
6067// Helpers to read book data from storage
@@ -72,16 +79,21 @@ function getBookTitle(storage: Storage): string {
7279 return metadata ?. title ?? "ADT Preview"
7380}
7481
75- function getTextCatalog ( storage : Storage ) : TextCatalogOutput | undefined {
76- const row = storage . getLatestNodeData ( "text-catalog" , "book" )
77- return row ?. data as TextCatalogOutput | undefined
82+ function getTextCatalog ( storage : Storage ) : TextCatalogOutput {
83+ const pages = storage . getPages ( )
84+ return buildTextCatalog ( storage , pages )
7885}
7986
8087function getGlossary ( storage : Storage ) : GlossaryOutput | undefined {
8188 const row = storage . getLatestNodeData ( "glossary" , "book" )
8289 return row ?. data as GlossaryOutput | undefined
8390}
8491
92+ function getQuizData ( storage : Storage ) : QuizGenerationOutput | undefined {
93+ const row = storage . getLatestNodeData ( "quiz-generation" , "book" )
94+ return row ?. data as QuizGenerationOutput | undefined
95+ }
96+
8597function buildTextsMap (
8698 storage : Storage ,
8799 lang : string ,
@@ -109,14 +121,38 @@ function buildTextsMap(
109121 return textsMap
110122}
111123
112- /** Build the pages.json manifest from all rendered pages */
124+ /** Build the pages.json manifest from all rendered pages, interleaving quiz pages */
113125function buildPagesManifest ( storage : Storage ) : Array < { section_id : string ; href : string } > {
114126 const pages = storage . getPages ( )
127+ const quizData = getQuizData ( storage )
128+
129+ // Build a map from afterPageId -> quizzes for interleaving
130+ const quizzesByAfterPageId = new Map < string , Quiz [ ] > ( )
131+ if ( quizData ?. quizzes ) {
132+ for ( const quiz of quizData . quizzes ) {
133+ const existing = quizzesByAfterPageId . get ( quiz . afterPageId ) ?? [ ]
134+ existing . push ( quiz )
135+ quizzesByAfterPageId . set ( quiz . afterPageId , existing )
136+ }
137+ }
138+
115139 const list : Array < { section_id : string ; href : string } > = [ ]
116140 for ( const page of pages ) {
117141 const renderRow = storage . getLatestNodeData ( "web-rendering" , page . pageId )
118- if ( renderRow ) {
119- list . push ( { section_id : page . pageId , href : `${ page . pageId } .html` } )
142+ if ( ! renderRow ) continue
143+ const parsed = WebRenderingOutput . safeParse ( renderRow . data )
144+ if ( ! parsed . success || parsed . data . sections . length === 0 ) continue
145+
146+ list . push ( { section_id : page . pageId , href : `${ page . pageId } .html` } )
147+
148+ // Insert quiz pages after this page
149+ const quizzes = quizzesByAfterPageId . get ( page . pageId )
150+ if ( quizzes ) {
151+ for ( const quiz of quizzes ) {
152+ const quizIndex = quizData ! . quizzes . indexOf ( quiz )
153+ const quizId = `qz${ pad3 ( quizIndex + 1 ) } `
154+ list . push ( { section_id : quizId , href : `${ quizId } .html` } )
155+ }
120156 }
121157 }
122158 return list
@@ -179,6 +215,12 @@ export function createAdtPreviewRoutes(
179215) : Hono {
180216 const app = new Hono ( )
181217
218+ // Disable browser caching for all preview responses — this is a dev tool
219+ app . use ( "*" , async ( c , next ) => {
220+ await next ( )
221+ c . header ( "Cache-Control" , "no-store" )
222+ } )
223+
182224 // Helper: resolve book + validate
183225 function resolveBook ( label : string ) {
184226 const safeLabel = parseBookLabel ( label )
@@ -252,31 +294,39 @@ export function createAdtPreviewRoutes(
252294 return c . body ( NAV_HTML )
253295 } )
254296
255- // /content/tailwind_output.css
297+ // /content/tailwind_output.css — rebuilt on every request (no cache, dev tool)
256298 app . get ( "/books/:label/adt-preview/content/tailwind_output.css" , async ( c ) => {
257299 const { safeLabel } = resolveBook ( c . req . param ( "label" ) )
258300
259- let css = tailwindCssCache . get ( safeLabel )
260- if ( ! css ) {
261- const storage = createBookStorage ( safeLabel , booksDir )
262- try {
263- const pages = storage . getPages ( )
264- let allHtml = ""
265- for ( const page of pages ) {
266- const renderRow = storage . getLatestNodeData ( "web-rendering" , page . pageId )
267- if ( renderRow ) {
268- const parsed = WebRenderingOutput . safeParse ( renderRow . data )
269- if ( parsed . success ) {
270- const { html } = combineSections ( parsed . data )
271- allHtml += html + "\n"
272- }
301+ const storage = createBookStorage ( safeLabel , booksDir )
302+ let css : string
303+ try {
304+ const pages = storage . getPages ( )
305+ let allHtml = ""
306+ for ( const page of pages ) {
307+ const renderRow = storage . getLatestNodeData ( "web-rendering" , page . pageId )
308+ if ( renderRow ) {
309+ const parsed = WebRenderingOutput . safeParse ( renderRow . data )
310+ if ( parsed . success ) {
311+ const { html } = combineSections ( parsed . data )
312+ allHtml += html + "\n"
273313 }
274314 }
275- css = await buildPreviewTailwindCss ( allHtml , webAssetsDir )
276- tailwindCssCache . set ( safeLabel , css )
277- } finally {
278- storage . close ( )
279315 }
316+
317+ // Include quiz HTML so Tailwind scans quiz classes
318+ const quizData = getQuizData ( storage )
319+ const catalog = getTextCatalog ( storage )
320+ if ( quizData ?. quizzes ) {
321+ for ( let i = 0 ; i < quizData . quizzes . length ; i ++ ) {
322+ const quizId = `qz${ pad3 ( i + 1 ) } `
323+ allHtml += renderQuizHtml ( quizData . quizzes [ i ] , quizId , catalog ) + "\n"
324+ }
325+ }
326+
327+ css = await buildPreviewTailwindCss ( allHtml , webAssetsDir )
328+ } finally {
329+ storage . close ( )
280330 }
281331
282332 c . header ( "Content-Type" , "text/css" )
@@ -393,6 +443,42 @@ export function createAdtPreviewRoutes(
393443 const pageId = filename . replace ( / \. h t m l $ / , "" )
394444
395445 return withStorage ( label , ( storage ) => {
446+ const title = getBookTitle ( storage )
447+ const language = getBookLanguage ( storage )
448+
449+ // Check if this is a quiz page (qzNNN)
450+ const quizMatch = pageId . match ( / ^ q z ( \d { 3 } ) $ / )
451+ if ( quizMatch ) {
452+ const quizIndex = parseInt ( quizMatch [ 1 ] , 10 ) - 1
453+ const quizData = getQuizData ( storage )
454+ if ( ! quizData ?. quizzes || quizIndex < 0 || quizIndex >= quizData . quizzes . length ) {
455+ throw new HTTPException ( 404 , { message : `No quiz data for: ${ pageId } ` } )
456+ }
457+ const quiz = quizData . quizzes [ quizIndex ]
458+ const catalog = getTextCatalog ( storage )
459+
460+ const quizHtmlContent = renderQuizHtml ( quiz , pageId , catalog )
461+ // Determine page index from the manifest
462+ const manifest = buildPagesManifest ( storage )
463+ const manifestIndex = manifest . findIndex ( ( e ) => e . section_id === pageId )
464+
465+ const html = renderPageHtml ( {
466+ content : quizHtmlContent ,
467+ language,
468+ sectionId : pageId ,
469+ pageTitle : title ,
470+ pageIndex : manifestIndex >= 0 ? manifestIndex + 1 : 1 ,
471+ activityAnswers : buildQuizAnswers ( quiz , pageId ) ,
472+ hasMath : false ,
473+ bundleVersion : "1" ,
474+ skipContentWrapper : true ,
475+ } )
476+
477+ c . header ( "Content-Type" , "text/html; charset=utf-8" )
478+ return c . body ( html )
479+ }
480+
481+ // Regular content page
396482 const renderRow = storage . getLatestNodeData ( "web-rendering" , pageId )
397483 if ( ! renderRow ) {
398484 throw new HTTPException ( 404 , { message : `No rendering data for page: ${ pageId } ` } )
@@ -404,30 +490,17 @@ export function createAdtPreviewRoutes(
404490 }
405491
406492 const { html : sectionHtml , activityAnswers } = combineSections ( parsed . data )
407- const title = getBookTitle ( storage )
408- const language = getBookLanguage ( storage )
409493
410- // Determine page index from all rendered pages
411- const pages = storage . getPages ( )
412- let pageIndex = 1
413- let idx = 0
414- for ( const p of pages ) {
415- const row = storage . getLatestNodeData ( "web-rendering" , p . pageId )
416- if ( row ) {
417- idx ++
418- if ( p . pageId === pageId ) {
419- pageIndex = idx
420- break
421- }
422- }
423- }
494+ // Determine page index from manifest (includes quiz pages)
495+ const manifest = buildPagesManifest ( storage )
496+ const manifestIndex = manifest . findIndex ( ( e ) => e . section_id === pageId )
424497
425498 const html = renderPageHtml ( {
426499 content : sectionHtml ,
427500 language,
428501 sectionId : pageId ,
429502 pageTitle : title ,
430- pageIndex,
503+ pageIndex : manifestIndex >= 0 ? manifestIndex + 1 : 1 ,
431504 activityAnswers,
432505 hasMath : false ,
433506 bundleVersion : "1" ,
0 commit comments