mirror of
https://github.com/f/awesome-chatgpt-prompts.git
synced 2026-02-12 15:52:47 +00:00
4252 lines
148 KiB
TypeScript
4252 lines
148 KiB
TypeScript
#!/usr/bin/env npx tsx
|
||
/**
|
||
* Generate PDF version of the Interactive Book of Prompting
|
||
*
|
||
* Usage:
|
||
* npx tsx scripts/generate-book-pdf.ts [locale]
|
||
*
|
||
* Examples:
|
||
* npx tsx scripts/generate-book-pdf.ts # Generate English version (default)
|
||
* npx tsx scripts/generate-book-pdf.ts tr # Generate Turkish version
|
||
* npx tsx scripts/generate-book-pdf.ts --all # Generate all locales
|
||
* npx tsx scripts/generate-book-pdf.ts --print # Print-ready with bleed & CMYK colors
|
||
* npx tsx scripts/generate-book-pdf.ts --all --print # All locales, print-ready
|
||
*/
|
||
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import { parts, type Chapter } from '../src/lib/book/chapters';
|
||
import { getLocaleData, type LocaleData } from '../src/components/book/elements/locales';
|
||
|
||
// Configuration
|
||
const BOOK_DIR = path.join(process.cwd(), 'src/content/book');
|
||
const MESSAGES_DIR = path.join(process.cwd(), 'messages');
|
||
const OUTPUT_DIR = path.join(process.cwd(), 'public/book-pdf');
|
||
const SITE_URL = 'https://prompts.chat';
|
||
|
||
// Print-ready mode flag (set by --print CLI argument)
|
||
const PRINT_READY = process.argv.includes('--print');
|
||
|
||
// Bleed dimensions for print-ready output
|
||
const BLEED = '0.125in'; // 3mm standard bleed
|
||
const TRIM_WIDTH = '6in';
|
||
const TRIM_HEIGHT = '9in';
|
||
const BLEED_WIDTH = '6.25in'; // 6 + 0.125*2
|
||
const BLEED_HEIGHT = '9.25in'; // 9 + 0.125*2
|
||
|
||
// Components that truly need interactivity (API calls, complex animations)
|
||
// Everything else gets static rendering
|
||
const INTERACTIVE_ONLY_COMPONENTS = [
|
||
'PromptAnalyzer', // Needs live API calls
|
||
'RunPromptButton',
|
||
'CodeEditor',
|
||
];
|
||
|
||
/**
|
||
* Load UI messages from messages/*.json
|
||
*/
|
||
function loadMessages(locale: string): Record<string, unknown> {
|
||
try {
|
||
const filePath = path.join(MESSAGES_DIR, `${locale}.json`);
|
||
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||
} catch {
|
||
// Fallback to English
|
||
const filePath = path.join(MESSAGES_DIR, 'en.json');
|
||
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get a nested translation key from messages
|
||
*/
|
||
function t(messages: Record<string, unknown>, key: string): string {
|
||
const keys = key.split('.');
|
||
let current: unknown = messages;
|
||
for (const k of keys) {
|
||
if (current && typeof current === 'object' && k in (current as Record<string, unknown>)) {
|
||
current = (current as Record<string, unknown>)[k];
|
||
} else {
|
||
return key; // Fallback to key itself
|
||
}
|
||
}
|
||
return typeof current === 'string' ? current : key;
|
||
}
|
||
|
||
// Components with explicit static renderers (handled individually in transformMdxForPdf)
|
||
const STATICALLY_RENDERED_COMPONENTS = [
|
||
'TryIt', 'Quiz', 'Callout', 'InfoGrid', 'Checklist', 'Compare',
|
||
'FillInTheBlank', 'InteractiveChecklist', 'PromptDebugger',
|
||
'PromptChallenge', 'BeforeAfterEditor', 'DiffView', 'VersionDiff',
|
||
'TokenizerDemo', 'TokenPredictionDemo', 'ContextWindowDemo',
|
||
'TemperatureDemo', 'StructuredOutputDemo', 'FewShotDemo',
|
||
'JsonYamlDemo', 'IterativeRefinementDemo', 'CostCalculatorDemo',
|
||
'EmbeddingsDemo', 'LLMCapabilitiesDemo',
|
||
'JailbreakDemo', 'TextToImageDemo', 'TextToVideoDemo',
|
||
'SummarizationDemo', 'ContextPlayground',
|
||
'ValidationDemo', 'FallbackDemo', 'ContentPipelineDemo',
|
||
'ChainExample', 'ChainFlowDemo', 'ChainErrorDemo',
|
||
'FrameworkDemo', 'CRISPEFramework', 'BREAKFramework', 'RTFFramework',
|
||
'PromptBreakdown', 'SpecificitySpectrum', 'PrinciplesSummary',
|
||
'PromptBuilder', 'BookPartsNav',
|
||
'Collapsible', 'CopyableCode',
|
||
];
|
||
|
||
|
||
// Localization for the interactive notice
|
||
const INTERACTIVE_NOTICES: Record<string, string> = {
|
||
en: '📖 This is an interactive element. Visit prompts.chat/book to try it live!',
|
||
tr: '📖 Bu etkileşimli bir öğedir. Canlı denemek için prompts.chat/book adresini ziyaret edin!',
|
||
es: '📖 Este es un elemento interactivo. ¡Visita prompts.chat/book para probarlo en vivo!',
|
||
de: '📖 Dies ist ein interaktives Element. Besuchen Sie prompts.chat/book, um es live auszuprobieren!',
|
||
fr: '📖 Ceci est un élément interactif. Visitez prompts.chat/book pour l\'essayer en direct!',
|
||
pt: '📖 Este é um elemento interativo. Visite prompts.chat/book para experimentá-lo ao vivo!',
|
||
zh: '📖 这是一个互动元素。访问 prompts.chat/book 进行在线体验!',
|
||
ja: '📖 これはインタラクティブな要素です。prompts.chat/book でライブで試してみてください!',
|
||
ko: '📖 이것은 인터랙티브 요소입니다. prompts.chat/book을 방문하여 직접 체험해 보세요!',
|
||
ar: '📖 هذا عنصر تفاعلي. قم بزيارة prompts.chat/book لتجربته مباشرة!',
|
||
it: '📖 Questo è un elemento interattivo. Visita prompts.chat/book per provarlo dal vivo!',
|
||
ru: '📖 Это интерактивный элемент. Посетите prompts.chat/book, чтобы попробовать вживую!',
|
||
fa: '📖 این یک عنصر تعاملی است. برای امتحان زنده به prompts.chat/book مراجعه کنید!',
|
||
nl: '📖 Dit is een interactief element. Bezoek prompts.chat/book om het live te proberen!',
|
||
el: '📖 Αυτό είναι ένα διαδραστικό στοιχείο. Επισκεφθείτε το prompts.chat/book για να το δοκιμάσετε ζωντανά!',
|
||
az: '📖 Bu interaktiv elementdir. Canlı sınamaq üçün prompts.chat/book saytına daxil olun!',
|
||
he: '📖 זהו אלמנט אינטראקטיבי. בקרו ב-prompts.chat/book כדי לנסות אותו בזמן אמת!',
|
||
};
|
||
|
||
/**
|
||
* Inline SVG icons for print-ready mode (emojis don't render reliably in CMYK print).
|
||
* Each icon is a 14x14 inline SVG with currentColor stroke.
|
||
*/
|
||
const SVG_ICONS: Record<string, string> = {
|
||
zap: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
|
||
quiz: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>',
|
||
info: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
|
||
warning: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||
tip: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 017 7c0 2.38-1.19 4.47-3 5.74V17H8v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 017-7z"/></svg>',
|
||
pencil: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',
|
||
search: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
||
trophy: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 010-5H6"/><path d="M18 9h1.5a2.5 2.5 0 000-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0012 0V2z"/></svg>',
|
||
refresh: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>',
|
||
palette: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="13.5" cy="6.5" r="0.5" fill="currentColor"/><circle cx="17.5" cy="10.5" r="0.5" fill="currentColor"/><circle cx="8.5" cy="7.5" r="0.5" fill="currentColor"/><circle cx="6.5" cy="12.5" r="0.5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 011.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>',
|
||
video: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>',
|
||
shield: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
|
||
book: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>',
|
||
gem: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="6 3 18 3 22 9 12 22 2 9"/><line x1="2" y1="9" x2="22" y2="9"/><line x1="12" y1="22" x2="6" y2="9"/><line x1="12" y1="22" x2="18" y2="9"/></svg>',
|
||
target: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>',
|
||
crown: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 4l3 12h14l3-12-6 7-4-7-4 7-6-7z"/><line x1="2" y1="21" x2="22" y2="21"/></svg>',
|
||
compass: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>',
|
||
sparkles: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l1.912 5.813L20 12l-6.088 3.187L12 21l-1.912-5.813L4 12l6.088-3.187z"/></svg>',
|
||
ruler: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.73 18l-8-14a2 2 0 00-3.48 0l-8 14A2 2 0 004 21h16a2 2 0 001.73-3z"/></svg>',
|
||
check: '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
||
checkbox: '<svg class="ico-sm" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="1" width="14" height="14" rx="2"/></svg>',
|
||
};
|
||
|
||
/**
|
||
* Return icon markup: SVG for print, emoji for screen
|
||
*/
|
||
function icon(name: string, emoji: string): string {
|
||
if (PRINT_READY && SVG_ICONS[name]) {
|
||
return SVG_ICONS[name];
|
||
}
|
||
return emoji;
|
||
}
|
||
|
||
// Fallback book titles (used if messages don't have book.title)
|
||
const BOOK_TITLES_FALLBACK: Record<string, string> = {
|
||
en: 'The Interactive Book of Prompting',
|
||
tr: 'İnteraktif Prompt Yazma Kitabı',
|
||
};
|
||
|
||
interface ProcessedChapter {
|
||
slug: string;
|
||
title: string;
|
||
part: string;
|
||
content: string;
|
||
}
|
||
|
||
/**
|
||
* Get available locales from the book directory
|
||
*/
|
||
function getAvailableLocales(): string[] {
|
||
const entries = fs.readdirSync(BOOK_DIR, { withFileTypes: true });
|
||
const locales = entries
|
||
.filter(entry => entry.isDirectory())
|
||
.map(entry => entry.name);
|
||
|
||
// Add 'en' as it's the default (files in root)
|
||
if (!locales.includes('en')) {
|
||
locales.unshift('en');
|
||
}
|
||
|
||
return locales;
|
||
}
|
||
|
||
/**
|
||
* Get the path to MDX files for a locale
|
||
*/
|
||
function getLocalePath(locale: string): string {
|
||
if (locale === 'en') {
|
||
return BOOK_DIR;
|
||
}
|
||
return path.join(BOOK_DIR, locale);
|
||
}
|
||
|
||
/**
|
||
* Check if a chapter exists for a locale
|
||
*/
|
||
function chapterExists(locale: string, slug: string): boolean {
|
||
const localePath = getLocalePath(locale);
|
||
const filePath = path.join(localePath, `${slug}.mdx`);
|
||
return fs.existsSync(filePath);
|
||
}
|
||
|
||
/**
|
||
* Extract prompt content from TryIt component
|
||
*/
|
||
function extractTryItPrompt(content: string): string | null {
|
||
// Match prompt={`...`} or prompt="..."
|
||
const backtickMatch = content.match(/prompt=\{`([\s\S]*?)`\}/);
|
||
if (backtickMatch) return backtickMatch[1];
|
||
|
||
const quoteMatch = content.match(/prompt="([^"]*?)"/);
|
||
if (quoteMatch) return quoteMatch[1];
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Extract Quiz content
|
||
*/
|
||
function extractQuizContent(content: string): { question: string; options: string[]; explanation: string } | null {
|
||
const questionMatch = content.match(/question="((?:[^"\\]|\\.)*)"/);
|
||
const optionsMatch = content.match(/options=\{\[([\s\S]*?)\]\}/);
|
||
const explanationMatch = content.match(/explanation="((?:[^"\\]|\\.)*)"/);
|
||
|
||
if (!questionMatch || !optionsMatch) return null;
|
||
|
||
const question = questionMatch[1].replace(/\\"/g, '"');
|
||
const optionsStr = optionsMatch[1];
|
||
const options: string[] = [];
|
||
const optRegex = /"((?:[^"\\]|\\.)*)"/g;
|
||
let m;
|
||
while ((m = optRegex.exec(optionsStr)) !== null) {
|
||
options.push(m[1].replace(/\\"/g, '"'));
|
||
}
|
||
const explanation = explanationMatch ? explanationMatch[1].replace(/\\"/g, '"') : '';
|
||
|
||
return { question, options, explanation };
|
||
}
|
||
|
||
/**
|
||
* Extract Callout content
|
||
*/
|
||
function extractCalloutContent(match: string): { type: string; title: string; content: string } {
|
||
const typeMatch = match.match(/type="([^"]*?)"/);
|
||
const titleMatch = match.match(/title="([^"]*?)"/);
|
||
|
||
// Extract content between > and </Callout>
|
||
const contentMatch = match.match(/>\s*([\s\S]*?)\s*<\/Callout>/);
|
||
|
||
return {
|
||
type: typeMatch ? typeMatch[1] : 'info',
|
||
title: titleMatch ? titleMatch[1] : '',
|
||
content: contentMatch ? contentMatch[1].trim() : '',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Extract InfoGrid items
|
||
*/
|
||
function extractInfoGridItems(content: string): { label: string; description: string }[] {
|
||
const itemsMatch = content.match(/items=\{\[([\s\S]*?)\]\}/);
|
||
if (!itemsMatch) return [];
|
||
|
||
const items: { label: string; description: string }[] = [];
|
||
// Handle escaped quotes inside strings: match "..." allowing \" inside
|
||
const itemRegex = /\{\s*label:\s*"((?:[^"\\]|\\.)*)"\s*,\s*description:\s*"((?:[^"\\]|\\.)*)"/g;
|
||
let match;
|
||
|
||
while ((match = itemRegex.exec(itemsMatch[1])) !== null) {
|
||
// Unescape the \" back to "
|
||
const label = match[1].replace(/\\"/g, '"');
|
||
const description = match[2].replace(/\\"/g, '"');
|
||
items.push({ label, description });
|
||
}
|
||
|
||
return items;
|
||
}
|
||
|
||
/**
|
||
* Extract Checklist items
|
||
*/
|
||
function extractChecklistItems(content: string): { title: string; items: string[] } {
|
||
const titleMatch = content.match(/title="([^"]*?)"/);
|
||
const itemsMatch = content.match(/items=\{\[([\s\S]*?)\]\}/);
|
||
|
||
const title = titleMatch ? titleMatch[1] : '';
|
||
const items: string[] = [];
|
||
|
||
if (itemsMatch) {
|
||
const textRegex = /text:\s*"([^"]*?)"/g;
|
||
let match;
|
||
while ((match = textRegex.exec(itemsMatch[1])) !== null) {
|
||
items.push(match[1]);
|
||
}
|
||
}
|
||
|
||
return { title, items };
|
||
}
|
||
|
||
/**
|
||
* Extract props from a JSX-like component string
|
||
*/
|
||
function extractProps(content: string): Record<string, string> {
|
||
const props: Record<string, string> = {};
|
||
// Simple string props: key="value" (handles escaped quotes)
|
||
const stringRegex = /(\w+)="((?:[^"\\]|\\.)*)"/g;
|
||
let m;
|
||
while ((m = stringRegex.exec(content)) !== null) {
|
||
props[m[1]] = m[2].replace(/\\"/g, '"');
|
||
}
|
||
// Template literal props: key={`value`}
|
||
const templateRegex = /(\w+)=\{`([\s\S]*?)`\}/g;
|
||
while ((m = templateRegex.exec(content)) !== null) {
|
||
props[m[1]] = m[2];
|
||
}
|
||
return props;
|
||
}
|
||
|
||
/**
|
||
* Extract array of objects from items={[...]} prop
|
||
*/
|
||
function extractArrayProp(content: string, propName: string): Record<string, string>[] {
|
||
const regex = new RegExp(`${propName}=\\{\\[([\\s\\S]*?)\\]\\}`, 'g');
|
||
const match = regex.exec(content);
|
||
if (!match) return [];
|
||
|
||
const items: Record<string, string>[] = [];
|
||
const objRegex = /\{([^{}]*?)\}/g;
|
||
let m;
|
||
while ((m = objRegex.exec(match[1])) !== null) {
|
||
const obj: Record<string, string> = {};
|
||
// Handle escaped quotes in field values
|
||
const fieldRegex = /(\w+):\s*"((?:[^"\\]|\\.)*)"/g;
|
||
let fm;
|
||
while ((fm = fieldRegex.exec(m[1])) !== null) {
|
||
obj[fm[1]] = fm[2].replace(/\\"/g, '"');
|
||
}
|
||
if (Object.keys(obj).length > 0) items.push(obj);
|
||
}
|
||
return items;
|
||
}
|
||
|
||
/**
|
||
* Transform MDX content for PDF - convert interactive components to static
|
||
*/
|
||
function transformMdxForPdf(content: string, locale: string, localeData?: LocaleData, messages?: Record<string, unknown>): string {
|
||
let result = content;
|
||
const noticeRaw = INTERACTIVE_NOTICES[locale] || INTERACTIVE_NOTICES.en;
|
||
const notice = PRINT_READY ? noticeRaw.replace('📖', icon('book', '📖')) : noticeRaw;
|
||
const msg = messages || {};
|
||
const ld = localeData || getLocaleData('en');
|
||
|
||
// Translated UI labels
|
||
const labels = {
|
||
tryIt: t(msg, 'book.interactive.tryIt') || 'Try This Prompt',
|
||
quiz: t(msg, 'book.interactive.quiz') || 'Quiz',
|
||
answer: t(msg, 'book.interactive.answer') || 'Answer',
|
||
prompt: t(msg, 'book.interactive.prompt') || 'Prompt',
|
||
output: t(msg, 'book.interactive.outputLabel') || 'Output',
|
||
answers: t(msg, 'book.interactive.answers') || 'Answers',
|
||
completePrompt: t(msg, 'book.interactive.completePrompt') || 'Complete Prompt',
|
||
examplePrompts: t(msg, 'book.interactive.examplePrompts') || 'Example prompts',
|
||
};
|
||
|
||
// In print mode, remove author social links from preface
|
||
if (PRINT_READY) {
|
||
result = result.replace(/<div className="flex gap-3 mt-3">[\s\S]*?<\/div>/g, '');
|
||
}
|
||
|
||
// Remove navigation components entirely
|
||
result = result.replace(/<NavButton[^>]*\/>/g, '');
|
||
result = result.replace(/<NavFooter[^>]*\/>/g, '');
|
||
result = result.replace(/<BookPartsNav[^>]*\/>/g, '');
|
||
|
||
// ============================================================
|
||
// TryIt - Show prompt as styled code block
|
||
// ============================================================
|
||
result = result.replace(/<TryIt\s+([\s\S]*?)\/>/g, (match, attrs) => {
|
||
const prompt = extractTryItPrompt(match);
|
||
const titleMatch = attrs.match(/title="([^"]*?)"/);
|
||
const descMatch = attrs.match(/description="([^"]*?)"/);
|
||
|
||
const title = titleMatch ? titleMatch[1] : labels.tryIt;
|
||
const desc = descMatch ? descMatch[1] : '';
|
||
|
||
if (prompt) {
|
||
const printablePrompt = convertPromptVariables(prompt);
|
||
const protectedPrompt = protectCodeBlock(escapeHtml(printablePrompt));
|
||
return `
|
||
<div class="tryit-box">
|
||
<div class="tryit-header">${icon('zap', '⚡')} ${escapeHtml(title)}</div>
|
||
${desc ? `<p class="tryit-desc">${escapeHtml(desc)}</p>` : ''}
|
||
<pre class="prompt-code">${protectedPrompt}</pre>
|
||
</div>
|
||
`;
|
||
}
|
||
return '';
|
||
});
|
||
|
||
// ============================================================
|
||
// Quiz - Show question, options, and answer
|
||
// ============================================================
|
||
result = result.replace(/<Quiz\s+([\s\S]*?)\/>/g, (match) => {
|
||
const quiz = extractQuizContent(match);
|
||
if (quiz) {
|
||
const correctIdx = match.match(/correctIndex=\{(\d+)\}/);
|
||
const correctNum = correctIdx ? parseInt(correctIdx[1]) : -1;
|
||
const optionsHtml = quiz.options.map((opt, i) => {
|
||
const marker = i === correctNum ? '●' : '○';
|
||
const cls = i === correctNum ? ' class="quiz-correct"' : '';
|
||
return `<div${cls}>${marker} ${opt}</div>`;
|
||
}).join('\n');
|
||
return `
|
||
<div class="quiz-box">
|
||
<div class="quiz-header">${icon('quiz', '📝')} Quiz</div>
|
||
<p class="quiz-question"><strong>${quiz.question}</strong></p>
|
||
<div class="quiz-options">${optionsHtml}</div>
|
||
${quiz.explanation ? `<p class="quiz-explanation"><strong>Answer:</strong> ${quiz.explanation}</p>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
return '';
|
||
});
|
||
|
||
// ============================================================
|
||
// Callout - Static info boxes
|
||
// ============================================================
|
||
result = result.replace(/<Callout\s+([\s\S]*?)<\/Callout>/g, (match) => {
|
||
const callout = extractCalloutContent(match);
|
||
const calloutIcons: Record<string, [string, string]> = { info: ['info', 'ℹ️'], warning: ['warning', '⚠️'], tip: ['tip', '💡'], example: ['zap', '⚡'] };
|
||
const ci = calloutIcons[callout.type] || calloutIcons.info;
|
||
return `
|
||
<div class="callout callout-${callout.type}">
|
||
<div class="callout-header">${icon(ci[0], ci[1])} ${callout.title}</div>
|
||
<div class="callout-content">${callout.content}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// InfoGrid - Static grid
|
||
// ============================================================
|
||
result = result.replace(/<InfoGrid\s+([\s\S]*?)\/>/g, (match) => {
|
||
const items = extractInfoGridItems(match);
|
||
if (items.length > 0) {
|
||
const itemsHtml = items.map(item =>
|
||
`<div class="info-item"><strong>${item.label}</strong>: ${item.description}</div>`
|
||
).join('\n');
|
||
return `<div class="info-grid">\n${itemsHtml}\n</div>`;
|
||
}
|
||
return '';
|
||
});
|
||
|
||
// ============================================================
|
||
// Checklist (simple) - Printable checkbox list
|
||
// ============================================================
|
||
result = result.replace(/<Checklist\s+([\s\S]*?)\/>/g, (match) => {
|
||
const checklist = extractChecklistItems(match);
|
||
const cb = icon('checkbox', '☐');
|
||
const itemsHtml = checklist.items.map(item => `<li>${cb} ${item}</li>`).join('\n');
|
||
return `
|
||
<div class="checklist">
|
||
${checklist.title ? `<div class="checklist-title">${checklist.title}</div>` : ''}
|
||
<ul>${itemsHtml}</ul>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// Compare - Side by side (handles object syntax {{ label, content }})
|
||
// ============================================================
|
||
result = result.replace(/<Compare\s+([\s\S]*?)\/>/g, (match) => {
|
||
// Try object syntax: before={{ label: "...", content: "..." or `...` }}
|
||
const extractSide = (side: string): { label: string; content: string } | null => {
|
||
const objRegex = new RegExp(`${side}=\\{\\{[\\s\\S]*?\\}\\}`);
|
||
const objMatch = match.match(objRegex);
|
||
if (!objMatch) return null;
|
||
const block = objMatch[0];
|
||
const labelMatch = block.match(/label:\s*"((?:[^"\\]|\\.)*)"/);
|
||
const contentQuote = block.match(/content:\s*"((?:[^"\\]|\\.)*)"/);
|
||
const contentBt = block.match(/content:\s*`([\s\S]*?)`/);
|
||
const label = labelMatch ? labelMatch[1].replace(/\\"/g, '"') : side;
|
||
const content = contentBt ? contentBt[1] : (contentQuote ? contentQuote[1].replace(/\\"/g, '"').replace(/\\n/g, '\n') : '');
|
||
return { label, content };
|
||
};
|
||
|
||
const before = extractSide('before');
|
||
const after = extractSide('after');
|
||
|
||
// Fallback: simple string props
|
||
if (!before || !after) {
|
||
const beforeSimple = match.match(/before="((?:[^"\\]|\\.)*)"/);
|
||
const afterSimple = match.match(/after="((?:[^"\\]|\\.)*)"/);
|
||
if (beforeSimple && afterSimple) {
|
||
return `
|
||
<div class="compare-box">
|
||
<div class="compare-item compare-before"><strong>Before:</strong> ${beforeSimple[1].replace(/\\"/g, '"')}</div>
|
||
<div class="compare-item compare-after"><strong>After:</strong> ${afterSimple[1].replace(/\\"/g, '"')}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
const beforeContent = protectCodeBlock(escapeHtml(before.content));
|
||
const afterContent = protectCodeBlock(escapeHtml(after.content));
|
||
|
||
return `
|
||
<div class="compare-box">
|
||
<div class="compare-item compare-before"><strong>${escapeHtml(before.label)}</strong><pre class="prompt-code">${beforeContent}</pre></div>
|
||
<div class="compare-item compare-after"><strong>${escapeHtml(after.label)}</strong><pre class="prompt-code">${afterContent}</pre></div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// FillInTheBlank - Printable exercise with blanks
|
||
// ============================================================
|
||
result = result.replace(/<FillInTheBlank\s+([\s\S]*?)\/>/g, (match) => {
|
||
const props = extractProps(match);
|
||
const title = props.title || 'Fill in the Blanks';
|
||
|
||
// Extract template
|
||
const templateMatch = match.match(/template="([^"]*?)"/);
|
||
const templateBt = match.match(/template=\{`([\s\S]*?)`\}/);
|
||
const template = templateBt ? templateBt[1] : (templateMatch ? templateMatch[1] : '');
|
||
|
||
// Extract blanks with correct answers
|
||
const blanks = extractArrayProp(match, 'blanks');
|
||
|
||
// Replace {{id}} in template with labeled blanks
|
||
let rendered = template.replace(/\{\{(\w+)\}\}/g, (_, id) => {
|
||
const blank = blanks.find(b => b.id === id);
|
||
const hint = blank?.correctAnswers || blank?.hint || '';
|
||
if (hint) {
|
||
return `_______ (${id}, e.g. ${hint})`;
|
||
}
|
||
return `_______ (${id})`;
|
||
});
|
||
|
||
const answersHtml = blanks.length > 0 ? blanks.map(b => {
|
||
const answers = b.correctAnswers || '';
|
||
return `<li><strong>${b.id}:</strong> ${answers}</li>`;
|
||
}).join('\n') : '';
|
||
|
||
const protectedTemplate = protectCodeBlock(escapeHtml(rendered));
|
||
|
||
return `
|
||
<div class="exercise-box">
|
||
<div class="exercise-header">${icon('pencil', '✏️')} ${escapeHtml(title)}</div>
|
||
<pre class="prompt-code">${protectedTemplate}</pre>
|
||
${answersHtml ? `<div class="exercise-answers"><strong>Answers:</strong><ul>${answersHtml}</ul></div>` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// InteractiveChecklist - Printable checklist
|
||
// ============================================================
|
||
result = result.replace(/<InteractiveChecklist\s+([\s\S]*?)\/>/g, (match) => {
|
||
const props = extractProps(match);
|
||
const title = props.title || 'Checklist';
|
||
const items = extractArrayProp(match, 'items');
|
||
const itemsHtml = items.map(item =>
|
||
`<li>${icon('checkbox', '☐')} <strong>${item.label || ''}</strong>${item.description ? ` — ${item.description}` : ''}</li>`
|
||
).join('\n');
|
||
return `
|
||
<div class="checklist">
|
||
<div class="checklist-title">${escapeHtml(title)}</div>
|
||
<ul>${itemsHtml}</ul>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// PromptDebugger - Show prompt, bad output, and options
|
||
// ============================================================
|
||
result = result.replace(/<PromptDebugger\s+([\s\S]*?)\/>/g, (match) => {
|
||
const props = extractProps(match);
|
||
const title = props.title || 'Debug This Prompt';
|
||
const badPrompt = props.badPrompt || '';
|
||
const badOutput = props.badOutput || '';
|
||
const hint = props.hint || '';
|
||
const options = extractArrayProp(match, 'options');
|
||
|
||
const protectedBadPrompt = protectCodeBlock(escapeHtml(badPrompt));
|
||
const protectedBadOutput = protectCodeBlock(escapeHtml(badOutput));
|
||
|
||
const optionsHtml = options.map(opt => {
|
||
const marker = opt.isCorrect === 'true' ? '✓' : '○';
|
||
const cls = opt.isCorrect === 'true' ? ' class="quiz-correct"' : '';
|
||
return `<div${cls}>${marker} ${opt.label || ''}</div>`;
|
||
}).join('\n');
|
||
|
||
return `
|
||
<div class="exercise-box">
|
||
<div class="exercise-header">${icon('search', '🔍')} ${escapeHtml(title)}</div>
|
||
<div class="exercise-section"><strong>The Prompt:</strong></div>
|
||
<pre class="prompt-code">${protectedBadPrompt}</pre>
|
||
<div class="exercise-section"><strong>The Output (problematic):</strong></div>
|
||
<pre class="prompt-code prompt-code-error">${protectedBadOutput}</pre>
|
||
${hint ? `<p class="exercise-hint">${icon('tip', '💡')} Hint: ${escapeHtml(hint)}</p>` : ''}
|
||
<div class="exercise-section"><strong>What's wrong?</strong></div>
|
||
<div class="quiz-options">${optionsHtml}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// PromptChallenge - Show task, criteria, and example solution
|
||
// ============================================================
|
||
result = result.replace(/<PromptChallenge\s+([\s\S]*?)\/>/g, (match) => {
|
||
const props = extractProps(match);
|
||
const title = props.title || 'Prompt Challenge';
|
||
const task = props.task || '';
|
||
const difficulty = props.difficulty || 'intermediate';
|
||
const exampleSolution = props.exampleSolution || '';
|
||
const criteria: string[] = [];
|
||
const criteriaMatch = match.match(/criteria=\{\[([\s\S]*?)\]\}/);
|
||
if (criteriaMatch) {
|
||
const strRegex = /"([^"]*?)"/g;
|
||
let cm;
|
||
while ((cm = strRegex.exec(criteriaMatch[1])) !== null) {
|
||
criteria.push(cm[1]);
|
||
}
|
||
}
|
||
|
||
const criteriaHtml = criteria.map(c => `<li>${icon('checkbox', '☐')} ${c}</li>`).join('\n');
|
||
|
||
return `
|
||
<div class="exercise-box">
|
||
<div class="exercise-header">${icon('trophy', '🏆')} ${escapeHtml(title)} <span class="difficulty-badge">${difficulty}</span></div>
|
||
<p>${escapeHtml(task)}</p>
|
||
${criteria.length > 0 ? `<div class="exercise-section"><strong>Criteria:</strong></div><ul>${criteriaHtml}</ul>` : ''}
|
||
${exampleSolution ? `<div class="exercise-section"><strong>Example Solution:</strong></div><pre class="prompt-code">${protectCodeBlock(escapeHtml(exampleSolution))}</pre>` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// BeforeAfterEditor - Show both prompts side by side
|
||
// ============================================================
|
||
result = result.replace(/<BeforeAfterEditor\s+([\s\S]*?)\/>/g, (match) => {
|
||
const props = extractProps(match);
|
||
const title = props.title || 'Before & After';
|
||
const badPrompt = props.badPrompt || '';
|
||
const idealPrompt = props.idealPrompt || '';
|
||
const task = props.task || '';
|
||
|
||
return `
|
||
<div class="exercise-box">
|
||
<div class="exercise-header">${icon('refresh', '🔄')} ${escapeHtml(title)}</div>
|
||
${task ? `<p>${escapeHtml(task)}</p>` : ''}
|
||
<div class="compare-box">
|
||
<div class="compare-item compare-before"><strong>Before:</strong><pre class="prompt-code">${protectCodeBlock(escapeHtml(badPrompt))}</pre></div>
|
||
<div class="compare-item compare-after"><strong>After:</strong><pre class="prompt-code">${protectCodeBlock(escapeHtml(idealPrompt))}</pre></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// DiffView - Show before/after comparison
|
||
// ============================================================
|
||
result = result.replace(/<DiffView\s+([\s\S]*?)\/>/g, (match) => {
|
||
const props = extractProps(match);
|
||
const beforeLabel = props.beforeLabel || 'Before';
|
||
const afterLabel = props.afterLabel || 'After';
|
||
const before = props.before || '';
|
||
const after = props.after || '';
|
||
return `
|
||
<div class="compare-box">
|
||
<div class="compare-item compare-before"><strong>${escapeHtml(beforeLabel)}:</strong><pre class="prompt-code">${protectCodeBlock(escapeHtml(before))}</pre></div>
|
||
<div class="compare-item compare-after"><strong>${escapeHtml(afterLabel)}:</strong><pre class="prompt-code">${protectCodeBlock(escapeHtml(after))}</pre></div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// TokenizerDemo - Static example with colored tokens
|
||
// ============================================================
|
||
result = result.replace(/<TokenizerDemo\s*\/>/g, () => {
|
||
const sample = ld.tokenizer.samples[ld.tokenizer.default];
|
||
const example = sample?.text || 'Hello, world!';
|
||
const tokens = sample?.tokens || ['Hel', 'lo', ',', ' wor', 'ld', '!'];
|
||
const colors = ['#dbeafe', '#dcfce7', '#f3e8ff', '#fef3c7', '#fce7f3', '#cffafe'];
|
||
const tokensHtml = tokens.map((tk, i) =>
|
||
`<span style="background:${colors[i % colors.length]};padding:2px 6px;border-radius:3px;margin:2px;display:inline-block;font-family:var(--font-mono);font-size:8.5pt;">${tk === ' ' ? '␣' : escapeHtml(tk)}</span>`
|
||
).join('');
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">Tokenizer</div>
|
||
<p class="demo-label">Input: "${escapeHtml(example)}"</p>
|
||
<p class="demo-label">Tokens (${tokens.length}):</p>
|
||
<div style="margin:0.5em 0;">${tokensHtml}</div>
|
||
<p class="demo-note">${escapeHtml(ld.tokenizer.tryExamples)}</p>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// TokenPredictionDemo - Static next-token prediction
|
||
// ============================================================
|
||
result = result.replace(/<TokenPredictionDemo\s*\/>/g, () => {
|
||
const tp = ld.tokenPrediction;
|
||
const stepKeys = Object.keys(tp.predictions.steps);
|
||
const stepsHtml = stepKeys.slice(0, 3).map(key => {
|
||
const preds = tp.predictions.steps[key];
|
||
const predsHtml = preds.map(p => `<span class="prediction-token">${escapeHtml(p.token)} <span class="prediction-prob">${Math.round(p.probability * 100)}%</span></span>`).join(' ');
|
||
return `<div class="prediction-step"><div class="prediction-context">"${escapeHtml(key)} ▁▁▁"</div><div class="prediction-options">→ ${predsHtml}</div></div>`;
|
||
}).join('\n');
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">Next-Token Prediction</div>
|
||
<p class="demo-note">${escapeHtml(tp.fullText)}</p>
|
||
${stepsHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// ContextWindowDemo - Static diagram
|
||
// ============================================================
|
||
result = result.replace(/<ContextWindowDemo\s*\/>/g, () => {
|
||
const cwLabel = t(msg, 'book.interactive.contextWindow') || 'Context Window';
|
||
const promptLabel = t(msg, 'book.interactive.prompt') || 'Prompt';
|
||
const responseLabel = t(msg, 'book.interactive.response') || 'Response';
|
||
const remainingLabel = t(msg, 'book.interactive.remaining') || 'Remaining';
|
||
const tipText = t(msg, 'book.interactive.contextTip') || '';
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">${escapeHtml(cwLabel)} — 8,000 tokens</div>
|
||
<table style="width:100%;border-collapse:collapse;font-size:8pt;margin:0.8em 0;">
|
||
<tr>
|
||
<td style="width:25%;padding:0.4em;border:1px solid #ccc;text-align:center;font-weight:600;">${escapeHtml(promptLabel)}<br/>2,000 tokens</td>
|
||
<td style="width:12.5%;padding:0.4em;border:1px solid #ccc;text-align:center;font-weight:600;">${escapeHtml(responseLabel)}<br/>1,000 tokens</td>
|
||
<td style="width:62.5%;padding:0.4em;border:1px solid #ccc;text-align:center;color:#666;">${escapeHtml(remainingLabel)} — 5,000 tokens</td>
|
||
</tr>
|
||
</table>
|
||
${tipText ? `<p class="demo-note">${escapeHtml(tipText)}</p>` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// TemperatureDemo - Show all temperature levels
|
||
// ============================================================
|
||
result = result.replace(/<TemperatureDemo\s*\/>/g, () => {
|
||
const te = ld.temperatureExamples;
|
||
const levels = [
|
||
{ temp: '0.0–0.2', label: t(msg, 'book.interactive.deterministic') || 'Deterministic', examples: te.lowTemp.slice(0, 2) },
|
||
{ temp: '0.5–0.7', label: t(msg, 'book.interactive.balanced') || 'Balanced', examples: te.mediumHighTemp.slice(0, 2) },
|
||
{ temp: '0.8–1.0', label: t(msg, 'book.interactive.veryCreative') || 'Creative', examples: te.highTemp.slice(0, 2) },
|
||
];
|
||
const levelsHtml = levels.map(l => `
|
||
<div class="temp-level">
|
||
<div class="temp-label"><strong>${l.temp}</strong> — ${escapeHtml(l.label)}</div>
|
||
<div class="temp-examples">${l.examples.map(e => `<div class="temp-example">"${escapeHtml(e)}"</div>`).join('')}</div>
|
||
</div>
|
||
`).join('');
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">${t(msg, 'book.interactive.temperatureDemo') || 'Temperature'}</div>
|
||
<p class="demo-note">${t(msg, 'book.interactive.prompt') || 'Prompt'}: "${escapeHtml(te.prompt)}"</p>
|
||
${levelsHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// StructuredOutputDemo - Show all three formats
|
||
// ============================================================
|
||
result = result.replace(/<StructuredOutputDemo\s*\/>/g, () => {
|
||
const unstructured = 'Here are some popular programming languages: Python is great for data science and AI. JavaScript is used for web development. Rust is known for performance and safety.';
|
||
const json = `{
|
||
"languages": [
|
||
{ "name": "Python", "best_for": ["data science", "AI"], "difficulty": "easy" },
|
||
{ "name": "JavaScript", "best_for": ["web development"], "difficulty": "medium" },
|
||
{ "name": "Rust", "best_for": ["performance", "safety"], "difficulty": "hard" }
|
||
]
|
||
}`;
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">Structured Output Comparison</div>
|
||
<div class="demo-section"><strong>Unstructured:</strong></div>
|
||
<div class="demo-text">${escapeHtml(unstructured)}</div>
|
||
<div class="demo-section"><strong>Structured (JSON):</strong></div>
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(json))}</pre>
|
||
<p class="demo-note">Structured output allows programmatic parsing, comparison across queries, and integration into workflows.</p>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// FewShotDemo - Show progression of examples
|
||
// ============================================================
|
||
result = result.replace(/<FewShotDemo\s*\/>/g, () => {
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">Few-Shot Learning</div>
|
||
<p class="demo-note">More examples help the model understand the pattern:</p>
|
||
<table class="demo-table">
|
||
<thead><tr><th>Examples</th><th>Prediction</th><th>Confidence</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>0 (zero-shot)</td><td>Positive ✗</td><td>45%</td></tr>
|
||
<tr><td>1 (one-shot)</td><td>Positive ✗</td><td>62%</td></tr>
|
||
<tr><td>2 (two-shot)</td><td>Mixed ✓</td><td>71%</td></tr>
|
||
<tr><td>3 (three-shot)</td><td>Mixed ✓</td><td>94%</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="demo-note">Test input: "Great quality but shipping was slow" → Expected: Mixed</p>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// JsonYamlDemo - Show all three formats
|
||
// ============================================================
|
||
result = result.replace(/<JsonYamlDemo\s*\/>/g, () => {
|
||
const ts = `interface ChatPersona {
|
||
name?: string;
|
||
role?: string;
|
||
tone?: PersonaTone | PersonaTone[];
|
||
expertise?: PersonaExpertise[];
|
||
}`;
|
||
const json = `{
|
||
"name": "CodeReviewer",
|
||
"role": "Senior Software Engineer",
|
||
"tone": ["professional", "analytical"],
|
||
"expertise": ["coding", "engineering"]
|
||
}`;
|
||
const yaml = `name: CodeReviewer
|
||
role: Senior Software Engineer
|
||
tone:
|
||
- professional
|
||
- analytical
|
||
expertise:
|
||
- coding
|
||
- engineering`;
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">Format Comparison: TypeScript / JSON / YAML</div>
|
||
<div class="demo-section"><strong>TypeScript (define schema):</strong></div>
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(ts))}</pre>
|
||
<div class="demo-section"><strong>JSON (APIs & parsing):</strong></div>
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(json))}</pre>
|
||
<div class="demo-section"><strong>YAML (config files):</strong></div>
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(yaml))}</pre>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// IterativeRefinementDemo - Show all versions
|
||
// ============================================================
|
||
result = result.replace(/<IterativeRefinementDemo\s*\/>/g, () => {
|
||
const versions = [
|
||
{ v: 1, prompt: 'Write a product description.', output: 'This is a great product. It has many features. You should buy it.', quality: 20, issue: 'Too vague, no specific details' },
|
||
{ v: 2, prompt: 'Write a product description for wireless earbuds.', output: 'These wireless earbuds offer great sound quality and comfortable fit. They have long battery life.', quality: 45, issue: 'Better, but still generic' },
|
||
{ v: 3, prompt: 'Write a 50-word product description for premium wireless earbuds. Highlight: noise cancellation, 8-hour battery, water resistance.', output: 'Experience pure audio bliss with our premium wireless earbuds. Advanced noise cancellation blocks distractions while delivering crystal-clear sound.', quality: 72, issue: 'Good details, needs stronger hook' },
|
||
{ v: 4, prompt: 'Write a compelling 50-word product description for premium wireless earbuds.\nKey features: noise cancellation, 8-hour battery, IPX5\nTone: Premium but approachable\nStart with a benefit, end with a call to action.', output: 'Escape the noise and immerse yourself in studio-quality sound. Our premium wireless earbuds feature advanced noise cancellation, 8-hour battery life, and IPX5 water resistance.', quality: 95, issue: null },
|
||
];
|
||
const versionsHtml = versions.map(v => `
|
||
<div class="iteration-step">
|
||
<div class="iteration-header">Version ${v.v} — Quality: ${v.quality}%</div>
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(v.prompt))}</pre>
|
||
<div class="iteration-output">${escapeHtml(v.output)}</div>
|
||
${v.issue ? `<div class="iteration-issue">⚠ ${v.issue}</div>` : '<div class="iteration-success">✓ Strong prompt with clear structure</div>'}
|
||
</div>
|
||
`).join('');
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">Iterative Refinement</div>
|
||
<p class="demo-note">Watch how a prompt improves through successive iterations:</p>
|
||
${versionsHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// CostCalculatorDemo - Static calculation example
|
||
// ============================================================
|
||
result = result.replace(/<CostCalculatorDemo\s*\/>/g, () => {
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">API Cost Calculator</div>
|
||
<table class="demo-table">
|
||
<thead><tr><th>Parameter</th><th>Value</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Input tokens per request</td><td>500</td></tr>
|
||
<tr><td>Output tokens per request</td><td>200</td></tr>
|
||
<tr><td>Input price</td><td>$0.15 / 1M tokens</td></tr>
|
||
<tr><td>Output price</td><td>$0.60 / 1M tokens</td></tr>
|
||
<tr><td>Requests per day</td><td>1,000</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="cost-results">
|
||
<div class="cost-item"><strong>Per request:</strong> $0.0002</div>
|
||
<div class="cost-item"><strong>Daily:</strong> $0.20</div>
|
||
<div class="cost-item"><strong>Monthly:</strong> $5.85</div>
|
||
</div>
|
||
<p class="demo-note" style="font-family:var(--font-mono);font-size:8pt;text-align:center;">(500 × $0.15/1M) + (200 × $0.60/1M) = $0.000195/request</p>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// EmbeddingsDemo - Show word vectors and similarity
|
||
// ============================================================
|
||
result = result.replace(/<EmbeddingsDemo\s*\/>/g, () => {
|
||
const words = ld.embeddingWords;
|
||
const rowsHtml = words.map(w =>
|
||
`<tr><td>${escapeHtml(w.word)}</td><td>[${w.vector.join(', ')}]</td><td>${escapeHtml(w.color)}</td></tr>`
|
||
).join('\n');
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">Word Embeddings</div>
|
||
<table class="demo-table">
|
||
<thead><tr><th>Word</th><th>Vector</th><th>Group</th></tr></thead>
|
||
<tbody>${rowsHtml}</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// LLMCapabilitiesDemo - Static capability list
|
||
// ============================================================
|
||
result = result.replace(/<LLMCapabilitiesDemo\s*\/>/g, () => {
|
||
const canDo = ld.capabilities.filter(c => c.canDo);
|
||
const cantDo = ld.capabilities.filter(c => !c.canDo);
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="compare-box">
|
||
<div class="compare-item compare-after">
|
||
<strong>✓</strong>
|
||
<ul>${canDo.map(c => `<li><strong>${escapeHtml(c.title)}</strong> — ${escapeHtml(c.description)}</li>`).join('\n')}</ul>
|
||
</div>
|
||
<div class="compare-item compare-before">
|
||
<strong>✗</strong>
|
||
<ul>${cantDo.map(c => `<li><strong>${escapeHtml(c.title)}</strong> — ${escapeHtml(c.description)}</li>`).join('\n')}</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// JailbreakDemo - Show attack/defense examples
|
||
// ============================================================
|
||
result = result.replace(/<JailbreakDemo\s*\/>/g, () => {
|
||
const examplesHtml = ld.jailbreakExamples.slice(0, 3).map(ex => `
|
||
<div class="jailbreak-example">
|
||
<div class="jailbreak-name"><strong>${escapeHtml(ex.name)}</strong> — ${escapeHtml(ex.description)}</div>
|
||
<div class="compare-box">
|
||
<div class="compare-item compare-after"><strong>${icon('shield', '🛡️')}</strong><br/>${escapeHtml(ex.systemPrompt)}</div>
|
||
<div class="compare-item compare-before"><strong>${icon('warning', '⚠️')}</strong><br/>${escapeHtml(ex.attack)}</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
return `
|
||
<div class="demo-box">
|
||
${examplesHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// Framework demos - CRISPE, BREAK, RTF with steps and example
|
||
// ============================================================
|
||
const fwColors = ['#dbeafe', '#dcfce7', '#f3e8ff', '#fef3c7', '#fce7f3', '#cffafe'];
|
||
const fwBorderColors = ['#93c5fd', '#86efac', '#c4b5fd', '#fcd34d', '#f9a8d4', '#67e8f9'];
|
||
|
||
const fwMap: Record<string, keyof typeof ld.frameworks> = {
|
||
CRISPEFramework: 'crispe',
|
||
BREAKFramework: 'break',
|
||
RTFFramework: 'rtf',
|
||
};
|
||
|
||
for (const [comp, fwKey] of Object.entries(fwMap)) {
|
||
const regex = new RegExp(`<${comp}\\s*/>`, 'g');
|
||
result = result.replace(regex, () => {
|
||
const fw = ld.frameworks[fwKey];
|
||
const stepsHtml = fw.steps.map((s, i) => `
|
||
<div class="fw-step">
|
||
<div class="fw-letter" style="background:${fwColors[i % fwColors.length]};border:1px solid ${fwBorderColors[i % fwBorderColors.length]};">${s.letter}</div>
|
||
<div class="fw-step-body">
|
||
<div class="fw-step-label"><strong>${escapeHtml(s.label)}</strong> — ${escapeHtml(s.description)}</div>
|
||
${s.example ? `<div class="fw-step-example">${escapeHtml(s.example)}</div>` : ''}
|
||
</div>
|
||
</div>
|
||
`).join('\n');
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">${escapeHtml(fw.name)}</div>
|
||
${stepsHtml}
|
||
<div class="demo-section"><strong>${labels.completePrompt}:</strong></div>
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(fw.examplePrompt))}</pre>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// PrinciplesSummary - Numbered principles list (from locale data)
|
||
// ============================================================
|
||
result = result.replace(/<PrinciplesSummary\s*\/>/g, () => {
|
||
const iconMap: Record<string, [string, string]> = {
|
||
Gem: ['gem', '💎'], Target: ['target', '🎯'], Crown: ['crown', '👑'], Compass: ['compass', '🧭'],
|
||
RefreshCw: ['refresh', '🔄'], Sparkles: ['sparkles', '✨'], Ruler: ['ruler', '📏'], CheckCircle: ['check', '✅'],
|
||
};
|
||
const itemsHtml = ld.principles.map(p => {
|
||
const ic = iconMap[p.iconName] || ['info', '•'];
|
||
return `<div class="principle-item"><span class="principle-icon">${icon(ic[0], ic[1])}</span><span><strong>${escapeHtml(p.title)}</strong> — ${escapeHtml(p.description)}</span></div>`;
|
||
}).join('\n');
|
||
return `
|
||
<div class="demo-box">
|
||
${itemsHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// VersionDiff - Show versions with printable diff
|
||
// ============================================================
|
||
result = result.replace(/<VersionDiff\s+versions=\{\[([\s\S]*?)\]\}\s*\/>/g, (match, inner) => {
|
||
// Parse versions: { label: "...", content: `...`, note: "..." }
|
||
const versions: { label: string; content: string; note: string }[] = [];
|
||
const versionBlocks = inner.split(/\},\s*\{/).map((b: string) => b.replace(/^\{|\}$/g, ''));
|
||
|
||
for (const block of versionBlocks) {
|
||
const labelMatch = block.match(/label:\s*"([^"]*?)"/);
|
||
const noteMatch = block.match(/note:\s*"([^"]*?)"/);
|
||
// content can be quoted or template-literal
|
||
const contentQuoted = block.match(/content:\s*"([^"]*?)"/);
|
||
const contentBt = block.match(/content:\s*`([\s\S]*?)`/);
|
||
const content = contentBt ? contentBt[1] : (contentQuoted ? contentQuoted[1] : '');
|
||
if (labelMatch) {
|
||
versions.push({ label: labelMatch[1], content, note: noteMatch ? noteMatch[1] : '' });
|
||
}
|
||
}
|
||
|
||
if (versions.length === 0) return '';
|
||
|
||
const versionsHtml = versions.map((v, i) => `
|
||
<div class="version-block">
|
||
<div class="version-header">
|
||
<span class="version-label">${escapeHtml(v.label)}</span>
|
||
${v.note ? `<span class="version-note">${escapeHtml(v.note)}</span>` : ''}
|
||
</div>
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(v.content))}</pre>
|
||
</div>
|
||
`).join('\n');
|
||
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">Prompt Evolution</div>
|
||
${versionsHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// ChainExample - Full chain with prompts and outputs per type
|
||
// ============================================================
|
||
result = result.replace(/<ChainExample\s*\n?\s*type="(\w+)"\s*\n?\s*steps=\{\[([\s\S]*?)\]\}\s*\n?\s*\/>/g, (match, type, stepsInner) => {
|
||
// Parse steps: { step: "...", prompt: "...", output: "..." }
|
||
const steps: { step: string; prompt: string; output: string }[] = [];
|
||
const stepRegex = /\{\s*step:\s*"([^"]*?)"\s*,\s*prompt:\s*"([^"]*?)"\s*,\s*output:\s*(?:'([^']*?)'|"([^"]*?)")/g;
|
||
let sm;
|
||
while ((sm = stepRegex.exec(stepsInner)) !== null) {
|
||
steps.push({ step: sm[1], prompt: sm[2], output: sm[3] || sm[4] || '' });
|
||
}
|
||
|
||
// If regex didn't match (complex multiline prompts), try a simpler approach
|
||
if (steps.length === 0) {
|
||
const blockRegex = /\{\s*step:\s*"([^"]*?)"/g;
|
||
let bm;
|
||
while ((bm = blockRegex.exec(stepsInner)) !== null) {
|
||
steps.push({ step: bm[1], prompt: '', output: '' });
|
||
}
|
||
}
|
||
|
||
const typeLabels: Record<string, string> = {
|
||
sequential: 'Sequential Chain',
|
||
parallel: 'Parallel Chain',
|
||
conditional: 'Conditional Chain',
|
||
iterative: 'Iterative Chain',
|
||
};
|
||
|
||
const typeIcons: Record<string, string> = {
|
||
sequential: '→',
|
||
parallel: '⇉',
|
||
conditional: '◇',
|
||
iterative: '↻',
|
||
};
|
||
|
||
const stepsHtml = steps.map((s, i) => {
|
||
const isSkipped = s.step.toLowerCase().includes('skipped');
|
||
const stepClass = isSkipped ? 'chain-step-skipped' : 'chain-step-item';
|
||
const connector = type === 'parallel' && i > 0 && i < steps.length - 1
|
||
? 'chain-connector-parallel'
|
||
: i < steps.length - 1 ? 'chain-connector' : '';
|
||
|
||
return `
|
||
<div class="${stepClass}">
|
||
<div class="chain-step-num">${isSkipped ? '✗' : (i + 1)}</div>
|
||
<div class="chain-step-body">
|
||
<div class="chain-step-name">${escapeHtml(s.step)}</div>
|
||
${!isSkipped && s.prompt ? `<div class="chain-step-prompt"><span class="chain-label">Prompt:</span> ${protectCodeBlock(escapeHtml(s.prompt))}</div>` : ''}
|
||
${!isSkipped && s.output ? `<div class="chain-step-output"><span class="chain-label">Output:</span> ${protectCodeBlock(escapeHtml(s.output))}</div>` : ''}
|
||
${isSkipped ? '<div class="chain-step-skipped-note">Skipped — condition not met</div>' : ''}
|
||
</div>
|
||
</div>${connector ? `<div class="${connector}"></div>` : ''}`;
|
||
}).join('\n');
|
||
|
||
const loopNote = type === 'iterative' ? '<div class="chain-loop-note">↻ Loop until quality threshold is met</div>' : '';
|
||
|
||
return `
|
||
<div class="chain-box chain-${type}">
|
||
<div class="chain-box-header">${typeIcons[type] || '→'} ${typeLabels[type] || type}</div>
|
||
${stepsHtml}
|
||
${loopNote}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// TextToImageDemo - Show prompt anatomy with all categories
|
||
// ============================================================
|
||
result = result.replace(/<TextToImageDemo\s*\/>/g, () => {
|
||
const imgOpts = ld.imagePromptOptions;
|
||
const imgLabels = ld.imageCategoryLabels;
|
||
const catColors: Record<string, string> = { subject: '#dbeafe', style: '#f3e8ff', lighting: '#fef3c7', composition: '#dcfce7', mood: '#fce7f3' };
|
||
const categories = Object.keys(imgOpts).map(key => ({
|
||
label: imgLabels[key] || key,
|
||
options: imgOpts[key],
|
||
color: catColors[key] || '#f5f5f4',
|
||
}));
|
||
|
||
const categoriesHtml = categories.map(cat => {
|
||
const optionsHtml = cat.options.map((opt, i) => {
|
||
const style = i === 0 ? ` style="background:${cat.color};border:1px solid #ccc;font-weight:600;"` : '';
|
||
return `<span class="image-option"${style}>${opt}</span>`;
|
||
}).join(' ');
|
||
return `<div class="image-category"><span class="image-cat-label" style="color:#555;">${cat.label}:</span> ${optionsHtml}</div>`;
|
||
}).join('\n');
|
||
|
||
const examplePrompts = [
|
||
{ prompt: 'a cat, photorealistic, golden hour, close-up portrait, peaceful', note: 'Realistic pet photography feel' },
|
||
{ prompt: 'a castle, oil painting, dramatic shadows, wide landscape, mysterious', note: 'Dark fantasy atmosphere' },
|
||
{ prompt: 'an astronaut, 3D render, neon glow, symmetrical, energetic', note: 'Sci-fi poster style' },
|
||
];
|
||
|
||
const examplesHtml = examplePrompts.map(ex => `
|
||
<div class="image-example">
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(ex.prompt))}</pre>
|
||
<p class="demo-note">${ex.note}</p>
|
||
</div>
|
||
`).join('');
|
||
|
||
const diffusionSteps = [
|
||
'1. Parse prompt → identify subject, style, and modifiers',
|
||
'2. Start with random noise (pure static)',
|
||
'3. Denoise step 1 → rough shapes emerge',
|
||
'4. Denoise step 2 → details and colors form',
|
||
'5. Denoise step 3 → final refinement and sharpness',
|
||
];
|
||
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">${icon('palette', '🎨')} ${t(msg, 'book.interactive.textToImageBuildPrompt') || 'Text-to-Image: Building an Image Prompt'}</div>
|
||
<p class="demo-note">Image generation prompts combine categories. Select one option from each row to build a complete prompt:</p>
|
||
${categoriesHtml}
|
||
<div class="demo-section"><strong>Example prompts built from these categories:</strong></div>
|
||
${examplesHtml}
|
||
<div class="demo-section"><strong>How Diffusion Models Work:</strong></div>
|
||
<div class="diffusion-steps">
|
||
${diffusionSteps.map(s => `<div class="diffusion-step">${s}</div>`).join('\n')}
|
||
</div>
|
||
<p class="demo-note">The model starts with random noise and gradually removes it, guided by your text prompt, until a coherent image forms. More specific prompts give the model stronger guidance at each step.</p>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// TextToVideoDemo - Show prompt anatomy for video
|
||
// ============================================================
|
||
result = result.replace(/<TextToVideoDemo\s*\/>/g, () => {
|
||
const vidOpts = ld.videoPromptOptions;
|
||
const vidLabels = ld.videoCategoryLabels;
|
||
const vidColors: Record<string, string> = { subject: '#dbeafe', action: '#dcfce7', camera: '#f3e8ff', duration: '#fef3c7' };
|
||
const categories = Object.keys(vidOpts).map(key => ({
|
||
label: vidLabels[key] || key,
|
||
options: vidOpts[key],
|
||
color: vidColors[key] || '#f5f5f4',
|
||
}));
|
||
|
||
const categoriesHtml = categories.map(cat => {
|
||
const optionsHtml = cat.options.map((opt, i) => {
|
||
const style = i === 0 ? ` style="background:${cat.color};border:1px solid #ccc;font-weight:600;"` : '';
|
||
return `<span class="image-option"${style}>${opt}</span>`;
|
||
}).join(' ');
|
||
return `<div class="image-category"><span class="image-cat-label" style="color:#555;">${cat.label}:</span> ${optionsHtml}</div>`;
|
||
}).join('\n');
|
||
|
||
const examplePrompts = [
|
||
{ prompt: 'A bird takes flight, slow pan left, 4 seconds', note: 'Nature documentary style' },
|
||
{ prompt: 'A wave crashes on rocks, static shot, 6 seconds', note: 'Dramatic landscape footage' },
|
||
{ prompt: 'A flower blooms in timelapse, dolly zoom, 8 seconds', note: 'Macro nature timelapse' },
|
||
];
|
||
|
||
const examplesHtml = examplePrompts.map(ex => `
|
||
<div class="image-example">
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(ex.prompt))}</pre>
|
||
<p class="demo-note">${ex.note}</p>
|
||
</div>
|
||
`).join('');
|
||
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">${icon('video', '🎬')} ${t(msg, 'book.interactive.textToVideoBuildPrompt') || 'Text-to-Video: Building a Video Prompt'}</div>
|
||
<p class="demo-note">Video prompts need subject, action, camera movement, and duration. Select one from each row:</p>
|
||
${categoriesHtml}
|
||
<div class="demo-section"><strong>Example prompts:</strong></div>
|
||
${examplesHtml}
|
||
<div class="demo-section"><strong>Key challenges for video models:</strong></div>
|
||
<ul style="font-size:9pt;margin:0.5em 0 0.5em 1.5em;">
|
||
<li><strong>Temporal consistency</strong> — keeping the subject looking the same across frames</li>
|
||
<li><strong>Natural motion</strong> — realistic movement physics and speed</li>
|
||
<li><strong>Camera coherence</strong> — smooth, intentional camera movement</li>
|
||
</ul>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// PromptBreakdown - Color-labeled prompt segments
|
||
// ============================================================
|
||
result = result.replace(/<PromptBreakdown\s+parts=\{\[([\s\S]*?)\]\}\s*\/>/g, (match, inner) => {
|
||
const colorValues: Record<string, string> = {
|
||
blue: '#dbeafe', green: '#dcfce7', purple: '#f3e8ff',
|
||
amber: '#fef3c7', pink: '#fce7f3', cyan: '#cffafe',
|
||
};
|
||
const borderColors: Record<string, string> = {
|
||
blue: '#93c5fd', green: '#86efac', purple: '#c4b5fd',
|
||
amber: '#fcd34d', pink: '#f9a8d4', cyan: '#67e8f9',
|
||
};
|
||
const defaultColorOrder = ['blue', 'green', 'purple', 'amber', 'pink', 'cyan'];
|
||
|
||
// Parse parts
|
||
const parts: { label: string; text: string; color: string }[] = [];
|
||
const partRegex = /\{\s*label:\s*"([^"]*?)"\s*,\s*text:\s*"([^"]*?)"(?:\s*,\s*color:\s*"([^"]*?)")?\s*\}/g;
|
||
let pm;
|
||
while ((pm = partRegex.exec(inner)) !== null) {
|
||
parts.push({ label: pm[1], text: pm[2], color: pm[3] || defaultColorOrder[parts.length % defaultColorOrder.length] });
|
||
}
|
||
|
||
if (parts.length === 0) return '';
|
||
|
||
const segmentsHtml = parts.map(p => {
|
||
const bg = colorValues[p.color] || colorValues.blue;
|
||
const border = borderColors[p.color] || borderColors.blue;
|
||
return `<span class="pb-segment"><span class="pb-label" style="color:${border};">${escapeHtml(p.label)}</span><span class="pb-text" style="border-bottom:2px solid ${border};background:${bg};">${escapeHtml(p.text)}</span></span>`;
|
||
}).join(' ');
|
||
|
||
return `
|
||
<div class="prompt-breakdown">
|
||
${segmentsHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// SpecificitySpectrum - Show all levels from vague to specific
|
||
// ============================================================
|
||
result = result.replace(/<SpecificitySpectrum\s+levels=\{\[([\s\S]*?)\]\}\s*\/>/g, (match, inner) => {
|
||
const levels: { level: string; text: string }[] = [];
|
||
const levelRegex = /\{\s*level:\s*"([^"]*?)"\s*,\s*text:\s*"([^"]*?)"\s*\}/g;
|
||
let lm;
|
||
while ((lm = levelRegex.exec(inner)) !== null) {
|
||
levels.push({ level: lm[1], text: lm[2] });
|
||
}
|
||
|
||
if (levels.length === 0) return '';
|
||
|
||
const barColors = ['#ef4444', '#f97316', '#f59e0b', '#22c55e'];
|
||
const levelsHtml = levels.map((l, i) => {
|
||
const width = ((i + 1) / levels.length * 100).toFixed(0);
|
||
const color = barColors[i] || barColors[barColors.length - 1];
|
||
return `
|
||
<div class="spectrum-level">
|
||
<div class="spectrum-header">
|
||
<span class="spectrum-badge" style="background:${color};color:white;">${escapeHtml(l.level)}</span>
|
||
<span class="spectrum-bar-wrap"><span class="spectrum-bar" style="width:${width}%;background:${color};"></span></span>
|
||
</div>
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(l.text))}</pre>
|
||
</div>`;
|
||
}).join('\n');
|
||
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">Specificity Spectrum</div>
|
||
${levelsHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// ChainFlowDemo - Static overview of all 4 chain types
|
||
// ============================================================
|
||
result = result.replace(/<ChainFlowDemo\s*\/>/g, () => {
|
||
const ct = ld.chainTypes;
|
||
const seq = ct.find(c => c.id === 'sequential') || ct[0];
|
||
const par = ct.find(c => c.id === 'parallel') || ct[1];
|
||
const cond = ct.find(c => c.id === 'conditional') || ct[2];
|
||
const iter = ct.find(c => c.id === 'iterative') || ct[3];
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="chain-types-grid">
|
||
<div class="chain-type-card">
|
||
<div class="chain-type-name" style="color:#2563eb;">${escapeHtml(seq.name)}</div>
|
||
<div class="chain-type-desc">${escapeHtml(seq.description)}</div>
|
||
<div class="chain-type-diagram">
|
||
<span class="chain-type-step" style="background:#dbeafe;border-color:#93c5fd;">Extract</span>
|
||
<span class="chain-type-arrow">→</span>
|
||
<span class="chain-type-step" style="background:#dbeafe;border-color:#93c5fd;">Analyze</span>
|
||
<span class="chain-type-arrow">→</span>
|
||
<span class="chain-type-step" style="background:#dbeafe;border-color:#93c5fd;">Generate</span>
|
||
</div>
|
||
</div>
|
||
<div class="chain-type-card">
|
||
<div class="chain-type-name" style="color:#7c3aed;">${escapeHtml(par.name)}</div>
|
||
<div class="chain-type-desc">${escapeHtml(par.description)}</div>
|
||
<div class="chain-type-diagram chain-type-diagram-parallel">
|
||
<div style="text-align:center;"><span class="chain-type-step" style="background:#f3e8ff;border-color:#c4b5fd;">Input</span></div>
|
||
<div class="chain-type-arrow">↓</div>
|
||
<div style="display:flex;justify-content:center;gap:0.3em;">
|
||
<span class="chain-type-step" style="background:#f3e8ff;border-color:#c4b5fd;">Sentiment</span>
|
||
<span class="chain-type-step" style="background:#f3e8ff;border-color:#c4b5fd;">Entities</span>
|
||
<span class="chain-type-step" style="background:#f3e8ff;border-color:#c4b5fd;">Topics</span>
|
||
</div>
|
||
<div class="chain-type-arrow">↓</div>
|
||
<div style="text-align:center;"><span class="chain-type-step" style="background:#f3e8ff;border-color:#c4b5fd;">Merge</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="chain-type-card">
|
||
<div class="chain-type-name" style="color:#d97706;">${escapeHtml(cond.name)}</div>
|
||
<div class="chain-type-desc">${escapeHtml(cond.description)}</div>
|
||
<div class="chain-type-diagram chain-type-diagram-parallel">
|
||
<div style="text-align:center;"><span class="chain-type-step" style="background:#fef3c7;border-color:#fcd34d;">Classify</span></div>
|
||
<div style="display:flex;justify-content:center;gap:1em;">
|
||
<div style="text-align:center;"><div class="chain-type-arrow">↙</div><span class="chain-type-step" style="background:#fef3c7;border-color:#fcd34d;">If complaint</span></div>
|
||
<div style="text-align:center;"><div class="chain-type-arrow">↘</div><span class="chain-type-step" style="background:#fef3c7;border-color:#fcd34d;">If question</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="chain-type-card">
|
||
<div class="chain-type-name" style="color:#16a34a;">${escapeHtml(iter.name)}</div>
|
||
<div class="chain-type-desc">${escapeHtml(iter.description)}</div>
|
||
<div class="chain-type-diagram">
|
||
<span class="chain-type-step" style="background:#dcfce7;border-color:#86efac;">Generate</span>
|
||
<span class="chain-type-arrow">→</span>
|
||
<span class="chain-type-step" style="background:#dcfce7;border-color:#86efac;">Evaluate</span>
|
||
<span class="chain-type-arrow">→</span>
|
||
<span class="chain-type-step" style="background:#dcfce7;border-color:#86efac;">Refine</span>
|
||
<span class="chain-type-arrow" style="font-size:10pt;">↻</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// CodeEditor - Static code block with filename header
|
||
// ============================================================
|
||
result = result.replace(/<CodeEditor\s*\n?\s*([\s\S]*?)\/>/g, (match, attrs) => {
|
||
const props = extractProps(match);
|
||
const lang = props.language || '';
|
||
const filename = props.filename || '';
|
||
// Extract code from code={`...`}
|
||
const codeMatch = match.match(/code=\{`([\s\S]*?)`\}/);
|
||
const code = codeMatch ? codeMatch[1] : '';
|
||
if (!code) return '';
|
||
return `
|
||
<div class="code-editor-box">
|
||
<div class="code-editor-header">
|
||
<span class="code-editor-dots"><span style="background:#ff5f56;"></span><span style="background:#ffbd2e;"></span><span style="background:#27c93f;"></span></span>
|
||
${filename ? `<span class="code-editor-filename">${escapeHtml(filename)}</span>` : ''}
|
||
<span class="code-editor-lang">${escapeHtml(lang)}</span>
|
||
</div>
|
||
<pre class="prompt-code">${protectCodeBlock(escapeHtml(code))}</pre>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// ChainErrorDemo - Show all 3 scenarios statically
|
||
// ============================================================
|
||
result = result.replace(/<ChainErrorDemo\s*\/>/g, () => {
|
||
const scenarios = ld.scenarios;
|
||
const steps = ld.steps;
|
||
const stepsStr = steps.map(s => escapeHtml(s.name)).join(' → ');
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="chain-types-grid">
|
||
${scenarios.map(sc => `
|
||
<div class="chain-type-card">
|
||
<div class="chain-type-name">${escapeHtml(sc.name)}</div>
|
||
<div class="chain-type-desc">${escapeHtml(sc.description)}</div>
|
||
<div style="font-size:8pt;font-family:var(--font-mono);color:#666;">${stepsStr}</div>
|
||
</div>`).join('\n')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// ValidationDemo - Show validation flow
|
||
// ============================================================
|
||
result = result.replace(/<ValidationDemo\s*\/>/g, () => {
|
||
const vd = ld.validationDemo;
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">${escapeHtml(vd.title)}</div>
|
||
<div class="chain-types-grid">
|
||
<div class="chain-type-card">
|
||
<div class="chain-type-name" style="color:#d97706;">${escapeHtml(vd.invalidRetry)}</div>
|
||
<div style="font-size:8pt;font-family:var(--font-mono);margin-top:0.4em;">
|
||
${vd.steps.map((s, i) => `${i + 1}. ${escapeHtml(s.name)}`).join('<br/>')}
|
||
<br/>✗ ${escapeHtml(vd.outputs.ageMustBeNumber)}<br/>↻ ${escapeHtml(vd.outputs.retryingWithFeedback)}<br/>✓ ${escapeHtml(vd.outputs.allFieldsValid)}<br/>✓ ${escapeHtml(vd.outputs.dataProcessedSuccessfully)}
|
||
</div>
|
||
</div>
|
||
<div class="chain-type-card">
|
||
<div class="chain-type-name" style="color:#16a34a;">${escapeHtml(vd.validData)}</div>
|
||
<div style="font-size:8pt;font-family:var(--font-mono);margin-top:0.4em;">
|
||
${vd.steps.map((s, i) => `${i + 1}. ${escapeHtml(s.name)}`).join('<br/>')}
|
||
<br/>✓ ${escapeHtml(vd.outputs.allFieldsValid)}<br/>✓ ${escapeHtml(vd.outputs.dataProcessedSuccessfully)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// FallbackDemo - Show primary vs fallback paths
|
||
// ============================================================
|
||
result = result.replace(/<FallbackDemo\s*\/>/g, () => {
|
||
const fd = ld.fallbackDemo;
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">${escapeHtml(fd.title)}</div>
|
||
<div class="chain-types-grid">
|
||
<div class="chain-type-card">
|
||
<div class="chain-type-name" style="color:#16a34a;">${escapeHtml(fd.primarySucceeds)}</div>
|
||
<div style="font-size:8pt;font-family:var(--font-mono);margin-top:0.4em;">
|
||
${escapeHtml(fd.steps[0].name)} → ✓<br/>
|
||
${escapeHtml(fd.outputs.deepAnalysisComplete)}<br/>
|
||
${escapeHtml(fd.outputs.resultFromPrimary)}
|
||
</div>
|
||
</div>
|
||
<div class="chain-type-card">
|
||
<div class="chain-type-name" style="color:#7c3aed;">${escapeHtml(fd.useFallback)}</div>
|
||
<div style="font-size:8pt;font-family:var(--font-mono);margin-top:0.4em;">
|
||
${escapeHtml(fd.steps[0].name)} → ✗<br/>
|
||
${escapeHtml(fd.steps[1].name)} → ✓<br/>
|
||
${escapeHtml(fd.outputs.resultFromFallback)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// ContentPipelineDemo - Show pipeline steps with prompts
|
||
// ============================================================
|
||
result = result.replace(/<ContentPipelineDemo\s*\/>/g, () => {
|
||
const cpd = ld.contentPipelineDemo;
|
||
const stepsHtml = cpd.steps.map((s, i) => {
|
||
const prompt = cpd.prompts[s.id] || '';
|
||
const outputKey = s.id as keyof typeof cpd.outputs;
|
||
const output = cpd.outputs[outputKey] || '';
|
||
return `
|
||
<div class="chain-step-item">
|
||
<div class="chain-step-num">${i + 1}</div>
|
||
<div class="chain-step-body">
|
||
<div class="chain-step-name">${escapeHtml(s.name)}</div>
|
||
${prompt && s.id !== 'input' ? `<div class="chain-step-prompt"><span class="chain-label">${labels.prompt}:</span> ${protectCodeBlock(escapeHtml(prompt))}</div>` : ''}
|
||
${output ? `<div class="chain-step-output"><span class="chain-label">${labels.output}:</span> ${escapeHtml(output)}</div>` : ''}
|
||
</div>
|
||
</div>${i < cpd.steps.length - 1 ? '<div class="chain-connector"></div>' : ''}`;
|
||
}).join('\n');
|
||
return `
|
||
<div class="chain-box chain-sequential">
|
||
<div class="chain-box-header">→ ${escapeHtml(cpd.title)}</div>
|
||
${stepsHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// SummarizationDemo - Show all 4 strategies
|
||
// ============================================================
|
||
result = result.replace(/<SummarizationDemo\s*\/>/g, () => {
|
||
const html = ld.strategies.map(s => `
|
||
<div class="chain-type-card">
|
||
<div class="chain-type-name">${escapeHtml(s.name)}</div>
|
||
<div class="chain-type-desc">${escapeHtml(s.description)}</div>
|
||
${s.summary ? `<div style="font-size:8pt;font-family:var(--font-mono);color:#666;margin-top:0.3em;">${escapeHtml(s.summary)}</div>` : ''}
|
||
</div>
|
||
`).join('');
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="chain-types-grid">${html}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// ContextPlayground - Show context blocks with token counts
|
||
// ============================================================
|
||
result = result.replace(/<ContextPlayground\s*\/>/g, () => {
|
||
const blocks = ld.contextBlocks;
|
||
const total = blocks.filter(b => b.enabled).reduce((s, b) => s + b.tokens, 0);
|
||
const html = blocks.map(b => `
|
||
<div class="context-block ${b.enabled ? 'context-block-on' : 'context-block-off'}">
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:0.2em;">
|
||
<span style="font-weight:600;">${b.enabled ? '✓' : '○'} ${escapeHtml(b.label)}</span>
|
||
<span style="color:#78716c;">${b.tokens} tokens</span>
|
||
</div>
|
||
<div style="font-family:var(--font-mono);font-size:7.5pt;color:#78716c;">${escapeHtml(b.content)}</div>
|
||
</div>
|
||
`).join('');
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">Context — ${total} / 200 tokens</div>
|
||
${html}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// PromptBuilder - Static template with writable fields
|
||
// ============================================================
|
||
result = result.replace(/<PromptBuilder\s*[^>]*\/>/g, (match) => {
|
||
const props = extractProps(match);
|
||
const title = props.title || 'Prompt Builder';
|
||
const fields = ld.builderFields.map(f => ({
|
||
label: f.label,
|
||
placeholder: f.placeholder,
|
||
hint: f.hint,
|
||
required: f.required || false,
|
||
}));
|
||
const fieldsHtml = fields.map(f => `
|
||
<div class="builder-field">
|
||
<div class="builder-field-label">${f.label}${f.required ? ' *' : ''}</div>
|
||
<div class="builder-field-hint">${f.hint}</div>
|
||
<div class="builder-field-input">${escapeHtml(f.placeholder)}</div>
|
||
</div>
|
||
`).join('');
|
||
return `
|
||
<div class="demo-box">
|
||
<div class="demo-header">${icon('pencil', '✏️')} ${escapeHtml(title)}</div>
|
||
<p class="demo-note">Fill in the fields below to construct your prompt. Not all fields are required — use what fits your task.</p>
|
||
${fieldsHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// FrameworkDemo (generic, with props) — fallback
|
||
// ============================================================
|
||
const simpleStaticDemos: string[] = [];
|
||
|
||
for (const component of simpleStaticDemos) {
|
||
const selfClosingRegex = new RegExp(`<${component}\\s*[^>]*/>`, 'g');
|
||
result = result.replace(selfClosingRegex, `<p class="interactive-notice">${notice}</p>`);
|
||
const withContentRegex = new RegExp(`<${component}[^>]*>[\\s\\S]*?</${component}>`, 'g');
|
||
result = result.replace(withContentRegex, `<p class="interactive-notice">${notice}</p>`);
|
||
}
|
||
|
||
// ============================================================
|
||
// Truly interactive-only components → notice
|
||
// ============================================================
|
||
for (const component of INTERACTIVE_ONLY_COMPONENTS) {
|
||
const selfClosingRegex = new RegExp(`<${component}\\s*[^>]*/>`, 'g');
|
||
result = result.replace(selfClosingRegex, `<p class="interactive-notice">${notice}</p>`);
|
||
const withContentRegex = new RegExp(`<${component}[^>]*>[\\s\\S]*?</${component}>`, 'g');
|
||
result = result.replace(withContentRegex, `<p class="interactive-notice">${notice}</p>`);
|
||
}
|
||
|
||
// ============================================================
|
||
// Collapsible - render open
|
||
// ============================================================
|
||
result = result.replace(/<Collapsible\s+([\s\S]*?)>([\s\S]*?)<\/Collapsible>/g, (match, attrs, children) => {
|
||
const props = extractProps(attrs);
|
||
const title = props.title || '';
|
||
return `
|
||
<div class="callout callout-info">
|
||
<div class="callout-header">${escapeHtml(title)}</div>
|
||
<div class="callout-content">${children}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// Multi-Agent Systems diagram
|
||
// ============================================================
|
||
result = result.replace(/<div className="my-6 p-6 bg-muted\/30 rounded-lg">\s*<div className="flex flex-col md:flex-row items-center justify-center gap-6">[\s\S]*?Coordinator[\s\S]*?Coder[\s\S]*?<\/div>\s*<\/div>/g, () => {
|
||
return `
|
||
<div class="demo-box" style="text-align:center;page-break-inside:avoid;">
|
||
<div class="demo-header">Multi-Agent System</div>
|
||
<div style="display:flex;align-items:center;justify-content:center;gap:1.5em;flex-wrap:wrap;margin:1em 0;">
|
||
<div style="padding:0.6em 1.2em;border:2px solid #999;border-radius:8px;font-family:var(--font-sans);font-weight:700;font-size:10pt;">
|
||
Coordinator<br/><span style="font-size:7pt;font-weight:400;color:#666;">Manages workflow</span>
|
||
</div>
|
||
<div style="font-size:16pt;color:#999;">⟷</div>
|
||
<div style="display:flex;gap:0.6em;flex-wrap:wrap;justify-content:center;">
|
||
<div style="padding:0.4em 0.8em;border:1px solid #ccc;border-radius:6px;font-family:var(--font-sans);font-size:8.5pt;font-weight:500;">Researcher</div>
|
||
<div style="padding:0.4em 0.8em;border:1px solid #ccc;border-radius:6px;font-family:var(--font-sans);font-size:8.5pt;font-weight:500;">Writer</div>
|
||
<div style="padding:0.4em 0.8em;border:1px solid #ccc;border-radius:6px;font-family:var(--font-sans);font-size:8.5pt;font-weight:500;">Critic</div>
|
||
<div style="padding:0.4em 0.8em;border:1px solid #ccc;border-radius:6px;font-family:var(--font-sans);font-size:8.5pt;font-weight:500;">Coder</div>
|
||
</div>
|
||
</div>
|
||
<p class="demo-note">Each agent has its own system prompt. The coordinator orchestrates their collaboration through structured messages.</p>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// Prompt Orchestration pipeline diagram
|
||
// ============================================================
|
||
result = result.replace(/<div className="my-6 flex flex-col items-center gap-2 p-6 bg-muted\/30 rounded-lg">\s*<div className="px-6 py-3 bg-slate[\s\S]*?User Request[\s\S]*?Final Output[\s\S]*?<\/div>\s*<\/div>/g, () => {
|
||
const steps = [
|
||
{ name: 'User Request', desc: '' },
|
||
{ name: 'Planner Agent', desc: 'Breaks down task' },
|
||
{ name: 'Researcher Agent', desc: 'Gathers information' },
|
||
{ name: 'Writer Agent', desc: 'Creates content' },
|
||
{ name: 'Reviewer Agent', desc: 'Quality checks' },
|
||
{ name: 'Final Output', desc: '' },
|
||
];
|
||
const stepsHtml = steps.map((s, i) => {
|
||
const isEndpoint = i === 0 || i === steps.length - 1;
|
||
const border = isEndpoint ? '2px solid #999' : '1px solid #ccc';
|
||
const weight = isEndpoint ? '700' : '600';
|
||
const arrow = i < steps.length - 1 ? '<div style="font-size:12pt;color:#999;margin:0.15em 0;">↓</div>' : '';
|
||
return `
|
||
<div style="padding:0.5em 1.5em;border:${border};border-radius:6px;font-family:var(--font-sans);font-size:9pt;font-weight:${weight};text-align:center;">
|
||
${s.name}${s.desc ? `<br/><span style="font-size:7pt;font-weight:400;color:#666;">${s.desc}</span>` : ''}
|
||
</div>
|
||
${arrow}`;
|
||
}).join('\n');
|
||
return `
|
||
<div class="demo-box" style="page-break-inside:avoid;">
|
||
<div class="demo-header">Prompt Orchestration Pipeline</div>
|
||
<div style="display:flex;flex-direction:column;align-items:center;margin:0.8em 0;">
|
||
${stepsHtml}
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// Agent → Skills → Prompts hierarchy diagram
|
||
// ============================================================
|
||
result = result.replace(/<div className="my-8 p-6 bg-muted\/20 rounded-xl border">\s*<div className="flex flex-col items-center gap-6">[\s\S]*?Prompts are atoms[\s\S]*?<\/div>\s*<\/div>/g, () => {
|
||
return `
|
||
<div class="demo-box" style="text-align:center;">
|
||
<div class="demo-header">Agent → Skills → Prompts</div>
|
||
<div style="margin:1em 0;">
|
||
<div style="display:inline-block;padding:0.6em 1.5em;background:#dbeafe;border:2px solid #93c5fd;border-radius:50px;font-family:var(--font-sans);font-weight:700;font-size:11pt;">Agent</div>
|
||
<div style="font-size:8pt;color:#78716c;margin:0.2em 0;">Autonomous AI system</div>
|
||
</div>
|
||
<div style="font-size:9pt;color:#78716c;">powered by ↓</div>
|
||
<div style="display:flex;justify-content:center;gap:1em;margin:0.8em 0;">
|
||
<div style="display:inline-block;padding:0.5em 1.2em;background:#f3e8ff;border:2px solid #c4b5fd;border-radius:8px;font-family:var(--font-sans);font-weight:600;font-size:9pt;">Skill</div>
|
||
<div style="display:inline-block;padding:0.5em 1.2em;background:#f3e8ff;border:2px solid #c4b5fd;border-radius:8px;font-family:var(--font-sans);font-weight:600;font-size:9pt;">Skill</div>
|
||
<div style="display:inline-block;padding:0.5em 1.2em;background:#f3e8ff;border:2px solid #c4b5fd;border-radius:8px;font-family:var(--font-sans);font-weight:600;font-size:9pt;">Skill</div>
|
||
</div>
|
||
<div style="font-size:8pt;color:#78716c;margin:0.2em 0;">Reusable expertise packages</div>
|
||
<div style="font-size:9pt;color:#78716c;">composed of ↓</div>
|
||
<div style="display:flex;justify-content:center;gap:0.5em;margin:0.8em 0;flex-wrap:wrap;">
|
||
<span style="display:inline-block;padding:0.3em 0.7em;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:8pt;font-family:var(--font-sans);font-weight:500;">Prompt</span>
|
||
<span style="display:inline-block;padding:0.3em 0.7em;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:8pt;font-family:var(--font-sans);font-weight:500;">Prompt</span>
|
||
<span style="display:inline-block;padding:0.3em 0.7em;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:8pt;font-family:var(--font-sans);font-weight:500;">Prompt</span>
|
||
<span style="display:inline-block;padding:0.3em 0.7em;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:8pt;font-family:var(--font-sans);font-weight:500;">Prompt</span>
|
||
<span style="display:inline-block;padding:0.3em 0.7em;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:8pt;font-family:var(--font-sans);font-weight:500;">Prompt</span>
|
||
</div>
|
||
<div style="font-size:8pt;color:#78716c;font-style:italic;margin-top:0.8em;">Prompts are atoms → Skills are molecules → Agents are complete structures</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// ============================================================
|
||
// Cleanup
|
||
// ============================================================
|
||
|
||
// Remove Icon components - they're decorative
|
||
result = result.replace(/<Icon[A-Za-z]+\s*[^>]*\/>/g, '');
|
||
|
||
// Clean up React className to class
|
||
result = result.replace(/className=/g, 'class=');
|
||
|
||
// Remove JSX expressions that won't render
|
||
result = result.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
|
||
|
||
// Clean up any remaining unknown self-closing components
|
||
result = result.replace(/<[A-Z][a-zA-Z]+\s*[^>]*\/>/g, '');
|
||
// Clean up any remaining unknown components with children
|
||
result = result.replace(/<[A-Z][a-zA-Z]+[^>]*>[\s\S]*?<\/[A-Z][a-zA-Z]+>/g, '');
|
||
|
||
// ============================================================
|
||
// Convert Tailwind className divs to inline styles for PDF
|
||
// ============================================================
|
||
result = convertTailwindToInline(result);
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Convert common Tailwind class patterns in raw HTML blocks to inline styles
|
||
* so they render correctly in the PDF without a Tailwind runtime.
|
||
*/
|
||
function convertTailwindToInline(html: string): string {
|
||
// Map of Tailwind class → CSS property
|
||
const tw: Record<string, string> = {
|
||
// Display & Flex
|
||
'flex': 'display:flex;',
|
||
'grid': 'display:grid;',
|
||
'inline-block': 'display:inline-block;',
|
||
'inline-flex': 'display:inline-flex;',
|
||
'hidden': 'display:none;',
|
||
'block': 'display:block;',
|
||
'flex-col': 'flex-direction:column;',
|
||
'flex-row': 'flex-direction:row;',
|
||
'flex-wrap': 'flex-wrap:wrap;',
|
||
'flex-1': 'flex:1 1 0%;',
|
||
'items-center': 'align-items:center;',
|
||
'items-start': 'align-items:flex-start;',
|
||
'justify-center': 'justify-content:center;',
|
||
'justify-between': 'justify-content:space-between;',
|
||
'text-center': 'text-align:center;',
|
||
'text-left': 'text-align:left;',
|
||
'text-right': 'text-align:right;',
|
||
'shrink-0': 'flex-shrink:0;',
|
||
// Gaps
|
||
'gap-1': 'gap:0.25em;', 'gap-2': 'gap:0.5em;', 'gap-3': 'gap:0.75em;', 'gap-4': 'gap:1em;', 'gap-6': 'gap:1.5em;', 'gap-8': 'gap:2em;',
|
||
// Padding
|
||
'p-2': 'padding:0.5em;', 'p-3': 'padding:0.75em;', 'p-4': 'padding:1em;', 'p-6': 'padding:1.5em;',
|
||
'px-2': 'padding-left:0.5em;padding-right:0.5em;', 'px-3': 'padding-left:0.75em;padding-right:0.75em;', 'px-4': 'padding-left:1em;padding-right:1em;',
|
||
'py-2': 'padding-top:0.5em;padding-bottom:0.5em;', 'py-3': 'padding-top:0.75em;padding-bottom:0.75em;',
|
||
'pt-2': 'padding-top:0.5em;', 'pt-3': 'padding-top:0.75em;',
|
||
// Margin
|
||
'm-0!': 'margin:0!important;', 'mt-1': 'margin-top:0.25em;', 'mt-2': 'margin-top:0.5em;', 'mb-1': 'margin-bottom:0.25em;', 'mb-2': 'margin-bottom:0.5em;', 'mb-3': 'margin-bottom:0.75em;', 'mb-4': 'margin-bottom:1em;', 'mb-8': 'margin-bottom:2em;',
|
||
'my-4': 'margin-top:1em;margin-bottom:1em;', 'my-6': 'margin-top:1.5em;margin-bottom:1.5em;',
|
||
// Sizing
|
||
'w-full': 'width:100%;', 'w-24': 'width:6rem;', 'w-px': 'width:1px;', 'w-20': 'width:5rem;',
|
||
'min-w-24': 'min-width:6rem;', 'min-w-20': 'min-width:5rem;',
|
||
'h-4': 'height:1rem;', 'w-4': 'width:1rem;',
|
||
'h-5': 'height:1.25rem;', 'w-5': 'width:1.25rem;',
|
||
'w-32': 'width:3rem;', 'h-32': 'height:3rem;',
|
||
// Typography
|
||
'text-xs': 'font-size:8pt;', 'text-sm': 'font-size:9pt;', 'text-lg': 'font-size:12pt;', 'text-2xl': 'font-size:16pt;',
|
||
'font-medium': 'font-weight:500;', 'font-semibold': 'font-weight:600;', 'font-bold': 'font-weight:700;',
|
||
'capitalize': 'text-transform:capitalize;',
|
||
'whitespace-pre-wrap': 'white-space:pre-wrap;',
|
||
'font-mono': 'font-family:var(--font-mono);',
|
||
'leading-relaxed': 'line-height:1.65;',
|
||
'line-through': 'text-decoration:line-through;',
|
||
// Borders & Radius
|
||
'border': PRINT_READY ? 'border:1px solid #ccc;' : 'border:1px solid #e7e5e4;',
|
||
'border-t': PRINT_READY ? 'border-top:1px solid #ccc;' : 'border-top:1px solid #e7e5e4;',
|
||
'border-b': PRINT_READY ? 'border-bottom:1px solid #ccc;' : 'border-bottom:1px solid #e7e5e4;',
|
||
'rounded': 'border-radius:4px;', 'rounded-lg': 'border-radius:6px;', 'rounded-full': 'border-radius:9999px;',
|
||
'overflow-hidden': 'overflow:hidden;', 'overflow-x-auto': 'overflow-x:auto;',
|
||
// Spacing between children
|
||
'space-y-1': '', 'space-y-2': '', 'space-y-4': '',
|
||
// Colors — grayscale for print, RGB for screen
|
||
'text-muted-foreground': PRINT_READY ? 'color:#666;' : 'color:#78716c;',
|
||
'text-blue-700': PRINT_READY ? 'color:#333;' : 'color:#1d4ed8;',
|
||
'text-blue-600': PRINT_READY ? 'color:#333;' : 'color:#2563eb;',
|
||
'text-blue-400': PRINT_READY ? 'color:#666;' : 'color:#60a5fa;',
|
||
'text-green-700': PRINT_READY ? 'color:#333;' : 'color:#15803d;',
|
||
'text-green-600': PRINT_READY ? 'color:#333;' : 'color:#16a34a;',
|
||
'text-green-400': PRINT_READY ? 'color:#666;' : 'color:#4ade80;',
|
||
'text-purple-700': PRINT_READY ? 'color:#333;' : 'color:#7e22ce;',
|
||
'text-purple-600': PRINT_READY ? 'color:#333;' : 'color:#9333ea;',
|
||
'text-purple-400': PRINT_READY ? 'color:#666;' : 'color:#c084fc;',
|
||
'text-red-700': PRINT_READY ? 'color:#333;' : 'color:#b91c1c;',
|
||
'text-red-600': PRINT_READY ? 'color:#333;' : 'color:#dc2626;',
|
||
'text-red-400': PRINT_READY ? 'color:#666;' : 'color:#f87171;',
|
||
'text-amber-700': PRINT_READY ? 'color:#333;' : 'color:#b45309;',
|
||
'text-amber-400': PRINT_READY ? 'color:#666;' : 'color:#fbbf24;',
|
||
'text-cyan-700': PRINT_READY ? 'color:#333;' : 'color:#0e7490;',
|
||
'text-cyan-400': PRINT_READY ? 'color:#666;' : 'color:#22d3ee;',
|
||
'text-white': 'color:white;',
|
||
// Backgrounds
|
||
'bg-muted/30': PRINT_READY ? 'background:#f2f2f2;' : 'background:#f5f5f4;',
|
||
'bg-muted/50': PRINT_READY ? 'background:#f2f2f2;' : 'background:#f5f5f4;',
|
||
'bg-muted': PRINT_READY ? 'background:#f2f2f2;' : 'background:#f0f0ee;',
|
||
'bg-background': 'background:white;', 'bg-card': 'background:white;',
|
||
'bg-blue-100': PRINT_READY ? 'background:#f2f2f2;' : 'background:#dbeafe;',
|
||
'bg-blue-50': PRINT_READY ? 'background:#fff;' : 'background:#eff6ff;',
|
||
'bg-blue-50/50': PRINT_READY ? 'background:#fff;' : 'background:#f7fbff;',
|
||
'bg-green-100': PRINT_READY ? 'background:#f2f2f2;' : 'background:#dcfce7;',
|
||
'bg-green-50': PRINT_READY ? 'background:#fff;' : 'background:#f0fdf4;',
|
||
'bg-green-50/50': PRINT_READY ? 'background:#fff;' : 'background:#f8fef9;',
|
||
'bg-purple-100': PRINT_READY ? 'background:#f2f2f2;' : 'background:#f3e8ff;',
|
||
'bg-purple-50': PRINT_READY ? 'background:#fff;' : 'background:#faf5ff;',
|
||
'bg-purple-50/50': PRINT_READY ? 'background:#fff;' : 'background:#fdfaff;',
|
||
'bg-red-50/50': PRINT_READY ? 'background:#f2f2f2;' : 'background:#fef7f7;',
|
||
'bg-red-50': PRINT_READY ? 'background:#f2f2f2;' : 'background:#fef2f2;',
|
||
'bg-amber-50/50': PRINT_READY ? 'background:#fff;' : 'background:#fffdf7;',
|
||
'bg-amber-50': PRINT_READY ? 'background:#fff;' : 'background:#fffbeb;',
|
||
'bg-cyan-50/50': PRINT_READY ? 'background:#fff;' : 'background:#f0fdff;',
|
||
// Border colors
|
||
'border-blue-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#bfdbfe;',
|
||
'border-blue-800': '',
|
||
'border-blue-300': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#93c5fd;',
|
||
'border-green-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#bbf7d0;',
|
||
'border-green-900': '',
|
||
'border-green-300': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#86efac;',
|
||
'border-purple-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#e9d5ff;',
|
||
'border-purple-800': '',
|
||
'border-purple-300': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#c4b5fd;',
|
||
'border-red-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#fecaca;',
|
||
'border-red-900': '',
|
||
'border-amber-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#fde68a;',
|
||
'border-amber-900': '',
|
||
'border-cyan-200': PRINT_READY ? 'border-color:#ccc;' : 'border-color:#a5f3fc;',
|
||
'border-cyan-900': '',
|
||
// Grid
|
||
'grid-cols-2': 'grid-template-columns:1fr 1fr;', 'grid-cols-4': 'grid-template-columns:1fr 1fr 1fr 1fr;',
|
||
'md:grid-cols-2': 'grid-template-columns:1fr 1fr;', 'md:grid-cols-3': 'grid-template-columns:1fr 1fr 1fr;', 'md:grid-cols-4': 'grid-template-columns:1fr 1fr 1fr 1fr;',
|
||
// Extra borders & bg
|
||
'border-2': 'border-width:2px;',
|
||
'border-primary': PRINT_READY ? 'border-color:#000;' : 'border-color:#7c3aed;',
|
||
'bg-primary/10': PRINT_READY ? 'background:#f2f2f2;' : 'background:rgba(124,58,237,0.1);',
|
||
};
|
||
|
||
// Process each element that has a class attribute
|
||
return html.replace(/class="([^"]+)"/g, (match, classes: string) => {
|
||
const classList = classes.split(/\s+/);
|
||
const styles: string[] = [];
|
||
const remainingClasses: string[] = [];
|
||
|
||
for (const cls of classList) {
|
||
// Skip dark mode variants
|
||
if (cls.startsWith('dark:')) continue;
|
||
// Skip responsive prefixes that we can't handle (except md: grid which we keep)
|
||
if (cls.startsWith('sm:') || cls.startsWith('lg:') || cls.startsWith('xl:')) continue;
|
||
// Handle md: prefix
|
||
const effectiveCls = cls.startsWith('md:') ? cls : cls;
|
||
|
||
if (tw[effectiveCls]) {
|
||
styles.push(tw[effectiveCls]);
|
||
} else {
|
||
remainingClasses.push(cls);
|
||
}
|
||
}
|
||
|
||
if (styles.length === 0) return match;
|
||
|
||
const styleStr = styles.filter(Boolean).join('');
|
||
const classStr = remainingClasses.length > 0 ? ` class="${remainingClasses.join(' ')}"` : '';
|
||
return `${classStr} style="${styleStr}"`.trim();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Convert all inline color values in style attributes to grayscale for print.
|
||
* Replaces colored hex values and rgba with gray equivalents.
|
||
*/
|
||
function convertStylesToGrayscale(html: string): string {
|
||
if (!PRINT_READY) return html;
|
||
|
||
// Map of specific hex colors to grayscale
|
||
const colorToGray: Record<string, string> = {
|
||
// Blues
|
||
'#3b82f6': '#666', '#2563eb': '#333', '#1d4ed8': '#333', '#60a5fa': '#999',
|
||
'#dbeafe': '#f2f2f2', '#bfdbfe': '#ccc', '#93c5fd': '#ccc', '#eff6ff': '#fff',
|
||
// Greens
|
||
'#22c55e': '#666', '#16a34a': '#333', '#15803d': '#333', '#4ade80': '#999',
|
||
'#dcfce7': '#f2f2f2', '#bbf7d0': '#ccc', '#86efac': '#ccc', '#f0fdf4': '#fff',
|
||
'#166534': '#333',
|
||
// Purples
|
||
'#7c3aed': '#000', '#9333ea': '#333', '#7e22ce': '#333', '#c084fc': '#999', '#a78bfa': '#666',
|
||
'#f3e8ff': '#f2f2f2', '#e9d5ff': '#ccc', '#c4b5fd': '#ccc', '#faf5ff': '#fff',
|
||
'#5b2d8e': '#333',
|
||
// Reds
|
||
'#dc2626': '#333', '#b91c1c': '#333', '#f87171': '#999',
|
||
'#fef2f2': '#f2f2f2', '#fecaca': '#ccc', '#450a0a': '#333',
|
||
// Ambers
|
||
'#f59e0b': '#666', '#d97706': '#333', '#b45309': '#333', '#fbbf24': '#999', '#92400e': '#333',
|
||
'#fef3c7': '#f2f2f2', '#fde68a': '#ccc', '#fffbeb': '#fff', '#fcd34d': '#ccc',
|
||
// Cyan
|
||
'#0e7490': '#333', '#22d3ee': '#999',
|
||
'#cffafe': '#f2f2f2', '#a5f3fc': '#ccc', '#67e8f9': '#ccc',
|
||
// Pinks
|
||
'#fce7f3': '#f2f2f2', '#f9a8d4': '#ccc',
|
||
// Rose
|
||
'#ef4444': '#333', '#f97316': '#555',
|
||
// Misc
|
||
'#e5e7eb': '#ccc',
|
||
'#78716c': '#666', '#c41d7f': '#333',
|
||
// Traffic light dots
|
||
'#ff5f56': '#999', '#ffbd2e': '#999', '#27c93f': '#999',
|
||
'#f8fafc': '#fff', '#f8fdf9': '#fff', '#faf8ff': '#fff', '#fffbf5': '#fff',
|
||
'#fefce8': '#fff',
|
||
};
|
||
|
||
// Replace hex colors in both inline style attributes and CSS
|
||
let result = html;
|
||
for (const [color, gray] of Object.entries(colorToGray)) {
|
||
result = result.split(color).join(gray);
|
||
}
|
||
// Replace rgba colors with gray
|
||
result = result.replace(/rgba\(\d+,\s*\d+,\s*\d+,\s*[\d.]+\)/g, '#f2f2f2');
|
||
// Replace linear-gradient colors
|
||
result = result.replace(/linear-gradient\([^)]*\)/g, (match) => {
|
||
let g = match;
|
||
for (const [color, gray] of Object.entries(colorToGray)) {
|
||
g = g.split(color).join(gray);
|
||
}
|
||
return g;
|
||
});
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Convert prompt variables like ${field} and ${years:10} to printable blanks
|
||
* \${name} or ${name} → _______ (name)
|
||
* \${name:default} or ${name:default} → _______ (name, e.g. default)
|
||
*/
|
||
function convertPromptVariables(text: string): string {
|
||
return text.replace(/\\?\$\{([^:}]+)(?::([^}]*))?\}/g, (_, name, defaultVal) => {
|
||
if (defaultVal) {
|
||
return `_______ (${name}, e.g. ${defaultVal})`;
|
||
}
|
||
return `_______ (${name})`;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Escape HTML entities
|
||
*/
|
||
function escapeHtml(text: string): string {
|
||
return text
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
/**
|
||
* Convert MDX to HTML
|
||
*/
|
||
function mdxToHtml(mdx: string): string {
|
||
let html = mdx;
|
||
|
||
// Code blocks FIRST - protect content from all markdown processing
|
||
// Handle quadruple-backtick blocks first (which can contain triple backticks)
|
||
// Fences must be at start of line
|
||
html = html.replace(/^````(\w*)\n([\s\S]*?)^````\s*$/gm, (_, lang, code) => {
|
||
const protectedCode = protectCodeBlock(escapeHtml(code.trim()));
|
||
return `<pre class="code-block${lang ? ` language-${lang}` : ''}"><code>${protectedCode}</code></pre>`;
|
||
});
|
||
// Then handle triple-backtick blocks (fences at start of line)
|
||
html = html.replace(/^```(\w*)\n([\s\S]*?)^```\s*$/gm, (_, lang, code) => {
|
||
const protectedCode = protectCodeBlock(escapeHtml(code.trim()));
|
||
return `<pre class="code-block${lang ? ` language-${lang}` : ''}"><code>${protectedCode}</code></pre>`;
|
||
});
|
||
|
||
// Inline code - protect before italic/bold
|
||
html = html.replace(/`([^`]+)`/g, (_, code) => {
|
||
const protectedCode = protectCodeBlock(escapeHtml(code));
|
||
return `<code>${protectedCode}</code>`;
|
||
});
|
||
|
||
// Headers
|
||
html = html.replace(/^######\s+(.*)$/gm, '<h6>$1</h6>');
|
||
html = html.replace(/^#####\s+(.*)$/gm, '<h5>$1</h5>');
|
||
html = html.replace(/^####\s+(.*)$/gm, '<h4>$1</h4>');
|
||
html = html.replace(/^###\s+(.*)$/gm, '<h3>$1</h3>');
|
||
html = html.replace(/^##\s+(.*)$/gm, '<h2>$1</h2>');
|
||
html = html.replace(/^#\s+(.*)$/gm, '<h1>$1</h1>');
|
||
|
||
// Bold and italic
|
||
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||
// Underscore italic: only match single words wrapped in underscores, not runs of underscores
|
||
html = html.replace(/(?<![_\w])_([^_\n]+?)_(?![_\w])/g, '<em>$1</em>');
|
||
|
||
// Links
|
||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||
|
||
// Images
|
||
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />');
|
||
|
||
// Horizontal rules
|
||
html = html.replace(/^---$/gm, '<hr />');
|
||
|
||
// Lists (basic)
|
||
html = html.replace(/^-\s+(.*)$/gm, '<li>$1</li>');
|
||
html = html.replace(/^(\d+)\.\s+(.*)$/gm, '<li>$2</li>');
|
||
|
||
// Wrap consecutive <li> in <ul>
|
||
html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => `<ul>\n${match}</ul>\n`);
|
||
|
||
// Blockquotes: lines starting with >
|
||
html = html.replace(/(^>\s?.+$\n?)+/gm, (match) => {
|
||
const content = match.replace(/^>\s?/gm, '').trim();
|
||
return `<blockquote>${content}</blockquote>\n`;
|
||
});
|
||
|
||
// Paragraphs (lines that aren't already HTML)
|
||
html = html.split('\n\n').map(block => {
|
||
block = block.trim();
|
||
if (!block) return '';
|
||
if (block.startsWith('<')) return block;
|
||
if (block.match(/^<(div|p|ul|ol|h[1-6]|pre|blockquote|hr|table)/)) return block;
|
||
return `<p>${block}</p>`;
|
||
}).join('\n\n');
|
||
|
||
return html;
|
||
}
|
||
|
||
// Storage for protected code blocks that shouldn't be markdown-processed
|
||
const protectedBlocks: Map<string, string> = new Map();
|
||
let blockCounter = 0;
|
||
|
||
/**
|
||
* Protect a code block from markdown processing
|
||
* Uses a format that won't be affected by markdown conversion
|
||
*/
|
||
function protectCodeBlock(content: string): string {
|
||
const id = `<!--CODEBLOCK${blockCounter++}-->`;
|
||
protectedBlocks.set(id, content);
|
||
blockCounter++;
|
||
return id;
|
||
}
|
||
|
||
/**
|
||
* Restore protected code blocks after markdown processing
|
||
*/
|
||
function restoreProtectedBlocks(html: string): string {
|
||
let result = html;
|
||
for (const [id, content] of protectedBlocks) {
|
||
result = result.split(id).join(content);
|
||
}
|
||
protectedBlocks.clear();
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Read and process a chapter
|
||
*/
|
||
function processChapter(locale: string, chapter: Chapter, localeData: LocaleData, messages: Record<string, unknown>): ProcessedChapter | null {
|
||
const localePath = getLocalePath(locale);
|
||
const filePath = path.join(localePath, `${chapter.slug}.mdx`);
|
||
|
||
if (!fs.existsSync(filePath)) {
|
||
console.log(` ⚠ Skipping ${chapter.slug} (not found for ${locale})`);
|
||
return null;
|
||
}
|
||
|
||
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
||
const transformedContent = transformMdxForPdf(rawContent, locale, localeData, messages);
|
||
const htmlContent = mdxToHtml(transformedContent);
|
||
const restoredContent = restoreProtectedBlocks(htmlContent);
|
||
const withEndnotes = convertLinksToEndnotes(restoredContent, messages, locale);
|
||
const finalContent = convertStylesToGrayscale(withEndnotes);
|
||
|
||
return {
|
||
slug: chapter.slug,
|
||
title: chapter.title,
|
||
part: chapter.part,
|
||
content: finalContent,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Convert external <a href="..."> links to numbered endnotes at the end of the chapter.
|
||
* Internal links (starting with /) are left as plain text references.
|
||
*/
|
||
function convertLinksToEndnotes(html: string, messages: Record<string, unknown> = {}, _locale: string = 'en'): string {
|
||
const msgLinks = t(messages, 'book.interactive.links');
|
||
const linksLabel = (msgLinks !== 'book.interactive.links') ? msgLinks : 'Links';
|
||
const footnotes: { num: number; text: string; url: string }[] = [];
|
||
let counter = 0;
|
||
|
||
// Replace external links with text + superscript number
|
||
const processed = html.replace(/<a href="((?:https?:\/\/)[^"]+)"[^>]*>([^<]+)<\/a>/g, (_, url, text) => {
|
||
counter++;
|
||
footnotes.push({ num: counter, text, url });
|
||
return `${text}<sup class="fn-ref">${counter}</sup>`;
|
||
});
|
||
|
||
// Replace internal links with just the text (no footnote needed)
|
||
const cleaned = processed.replace(/<a href="\/[^"]*"[^>]*>([^<]+)<\/a>/g, '$1');
|
||
|
||
if (footnotes.length === 0) return cleaned;
|
||
|
||
// Build endnotes section
|
||
const notesHtml = footnotes.map(fn =>
|
||
`<div class="fn-item"><span class="fn-num">${fn.num}.</span> <span class="fn-url">${fn.url}</span></div>`
|
||
).join('\n');
|
||
|
||
return `${cleaned}
|
||
<div class="fn-section">
|
||
<div class="fn-title">${linksLabel}</div>
|
||
${notesHtml}
|
||
</div>`;
|
||
}
|
||
|
||
/**
|
||
* Generate the HTML document for PDF
|
||
*/
|
||
// Map part slugs to message keys for translation
|
||
const PART_TRANSLATION_KEYS: Record<string, string> = {
|
||
'Introduction': 'introduction',
|
||
'Foundations': 'foundations',
|
||
'Techniques': 'techniques',
|
||
'Advanced': 'advanced',
|
||
'Advanced Strategies': 'advanced',
|
||
'Best Practices': 'bestPractices',
|
||
'Use Cases': 'useCases',
|
||
'Conclusion': 'conclusion',
|
||
};
|
||
|
||
function generateHtmlDocument(chapters: ProcessedChapter[], locale: string, messages: Record<string, unknown> = {}): string {
|
||
// Print version uses shorter printTitle, screen uses full title
|
||
const titleKey = PRINT_READY ? 'book.printTitle' : 'book.title';
|
||
const msgTitle = t(messages, titleKey);
|
||
const title = (msgTitle !== titleKey) ? msgTitle : (BOOK_TITLES_FALLBACK[locale] || BOOK_TITLES_FALLBACK.en);
|
||
const subtitleKey = PRINT_READY ? 'book.printSubtitle' : 'book.subtitle';
|
||
const msgSubtitle = t(messages, subtitleKey);
|
||
const subtitle = (msgSubtitle !== subtitleKey) ? msgSubtitle : 'A Comprehensive Guide to AI Prompt Engineering';
|
||
const isRtl = ['ar', 'he', 'fa'].includes(locale);
|
||
|
||
// Helper to translate part name
|
||
const translatePart = (partName: string): string => {
|
||
const key = PART_TRANSLATION_KEYS[partName];
|
||
if (key) {
|
||
const translated = t(messages, `book.parts.${key}`);
|
||
if (translated !== `book.parts.${key}`) return translated;
|
||
}
|
||
return partName;
|
||
};
|
||
|
||
// Helper to translate chapter title
|
||
const translateChapter = (slug: string, fallback: string): string => {
|
||
const translated = t(messages, `book.chapters.${slug}`);
|
||
if (translated !== `book.chapters.${slug}`) return translated;
|
||
return fallback;
|
||
};
|
||
|
||
// TOC heading
|
||
const contentsLabel = t(messages, 'book.tableOfContents');
|
||
const tocTitle = (contentsLabel !== 'book.tableOfContents') ? contentsLabel : 'Contents';
|
||
|
||
// Group chapters by part — only break pages at new parts, not every chapter
|
||
let chapterNumber = 0;
|
||
let currentPart = '';
|
||
const chaptersHtml = chapters.map((chapter) => {
|
||
chapterNumber++;
|
||
const translatedPart = translatePart(chapter.part);
|
||
const translatedTitle = translateChapter(chapter.slug, chapter.title);
|
||
const isNewPart = chapter.part !== currentPart;
|
||
currentPart = chapter.part;
|
||
const sectionClass = isNewPart ? 'chapter chapter-new-part' : 'chapter';
|
||
return `
|
||
<section class="${sectionClass}" id="${chapter.slug}">
|
||
<div class="chapter-opener">
|
||
<div class="chapter-number">${chapterNumber}</div>
|
||
<div class="chapter-meta">
|
||
<span class="chapter-part">${translatedPart}</span>
|
||
<h1 class="chapter-title">${translatedTitle}</h1>
|
||
</div>
|
||
<div class="chapter-rule"></div>
|
||
</div>
|
||
<div class="chapter-content">
|
||
${chapter.content}
|
||
</div>
|
||
</section>
|
||
`;
|
||
}).join('\n');
|
||
|
||
return `<!DOCTYPE html>
|
||
<html lang="${locale}" dir="${isRtl ? 'rtl' : 'ltr'}">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>${title}</title>
|
||
<style>
|
||
/* ========================================
|
||
BOOK SIZE: 6" x 9" (US Trade)
|
||
${PRINT_READY ? 'PRINT-READY: includes 0.125in bleed on all sides' : 'Screen-optimized'}
|
||
======================================== */
|
||
@page {
|
||
size: ${PRINT_READY ? BLEED_WIDTH + ' ' + BLEED_HEIGHT : '6in 9in'};
|
||
margin: ${PRINT_READY ? '0.7in 0.65in 0.75in 0.65in' : '0.55in 0.5in 0.6in 0.5in'};
|
||
${PRINT_READY ? 'marks: crop cross;\n bleed: ' + BLEED + ';' : ''}
|
||
}
|
||
|
||
/* ========================================
|
||
BASE TYPOGRAPHY
|
||
======================================== */
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
:root {
|
||
/* ${PRINT_READY ? 'B&W grayscale palette for print' : 'Screen RGB palette'} */
|
||
--color-text: ${PRINT_READY ? '#000000' : '#1c1917'};
|
||
--color-text-muted: ${PRINT_READY ? '#333333' : '#57534e'};
|
||
--color-text-light: ${PRINT_READY ? '#666666' : '#78716c'};
|
||
--color-accent: ${PRINT_READY ? '#000000' : '#7c3aed'};
|
||
--color-accent-light: ${PRINT_READY ? '#666666' : '#a78bfa'};
|
||
--color-bg-subtle: ${PRINT_READY ? '#ffffff' : '#fafaf9'};
|
||
--color-bg-muted: ${PRINT_READY ? '#f2f2f2' : '#f5f5f4'};
|
||
--color-border: ${PRINT_READY ? '#cccccc' : '#e7e5e4'};
|
||
--color-border-dark: ${PRINT_READY ? '#999999' : '#d6d3d1'};
|
||
--font-serif: 'Palatino Linotype', 'Book Antiqua', Palatino, Georgia, 'Times New Roman', serif;
|
||
--font-sans: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||
--font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', monospace;
|
||
}
|
||
|
||
body {
|
||
font-family: var(--font-serif);
|
||
font-size: 10.5pt;
|
||
line-height: 1.65;
|
||
color: var(--color-text);
|
||
text-rendering: optimizeLegibility;
|
||
-webkit-font-smoothing: antialiased;
|
||
font-feature-settings: 'liga' 1, 'kern' 1;
|
||
hyphens: auto;
|
||
orphans: 3;
|
||
widows: 3;
|
||
|
||
}
|
||
|
||
/* ========================================
|
||
COVER PAGE
|
||
======================================== */
|
||
.cover {
|
||
page-break-after: always;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
min-height: 100vh;
|
||
padding: 0 2em 3em 2em;
|
||
}
|
||
|
||
.cover-rule {
|
||
width: 100%;
|
||
height: 3px;
|
||
background: linear-gradient(90deg, var(--color-accent), var(--color-accent-light), transparent);
|
||
margin-bottom: 2em;
|
||
}
|
||
|
||
.cover h1 {
|
||
font-family: var(--font-sans);
|
||
font-size: 30pt;
|
||
font-weight: 800;
|
||
color: var(--color-text);
|
||
letter-spacing: -0.03em;
|
||
line-height: 1.1;
|
||
margin-bottom: 0.3em;
|
||
}
|
||
|
||
.cover .subtitle {
|
||
font-family: var(--font-serif);
|
||
font-size: 11pt;
|
||
font-style: italic;
|
||
color: var(--color-text-muted);
|
||
margin-bottom: 2.5em;
|
||
}
|
||
|
||
.cover-author {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.8em;
|
||
margin-bottom: 2em;
|
||
}
|
||
|
||
.cover-avatar {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.cover-author-info {
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.cover .author-name {
|
||
font-family: var(--font-sans);
|
||
font-size: 11pt;
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
display: block;
|
||
}
|
||
|
||
.cover .author-desc {
|
||
font-family: var(--font-sans);
|
||
font-size: 8pt;
|
||
color: var(--color-text-light);
|
||
display: block;
|
||
}
|
||
|
||
.cover .url {
|
||
font-family: var(--font-sans);
|
||
font-size: 8pt;
|
||
color: var(--color-text-light);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
/* ========================================
|
||
TABLE OF CONTENTS
|
||
======================================== */
|
||
.toc {
|
||
page-break-after: always;
|
||
padding-top: 1.5em;
|
||
}
|
||
|
||
.toc-title {
|
||
font-family: var(--font-sans);
|
||
font-size: 18pt;
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
margin-bottom: 1.5em;
|
||
padding-bottom: 0.5em;
|
||
border-bottom: 2px solid var(--color-text);
|
||
}
|
||
|
||
.toc-part {
|
||
font-family: var(--font-sans);
|
||
font-size: 10pt;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.1em;
|
||
color: var(--color-accent);
|
||
margin-top: 1.5em;
|
||
margin-bottom: 0.6em;
|
||
padding-top: 0.8em;
|
||
border-top: 1px solid var(--color-border);
|
||
}
|
||
|
||
.toc-part:first-of-type {
|
||
border-top: none;
|
||
padding-top: 0;
|
||
}
|
||
|
||
.toc-chapter {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: baseline;
|
||
margin-bottom: 0.35em;
|
||
padding-left: 1em;
|
||
}
|
||
|
||
.toc-chapter a {
|
||
font-family: var(--font-serif);
|
||
font-size: 10pt;
|
||
color: var(--color-text);
|
||
text-decoration: none;
|
||
flex: 1;
|
||
}
|
||
|
||
.toc-dots {
|
||
flex: 1;
|
||
border-bottom: 1px dotted var(--color-border-dark);
|
||
margin: 0 0.5em 0.3em 0.5em;
|
||
}
|
||
|
||
/* ========================================
|
||
CHAPTERS
|
||
======================================== */
|
||
.chapter {
|
||
page-break-before: always;
|
||
}
|
||
|
||
.chapter-opener {
|
||
margin-top: 2em;
|
||
margin-bottom: 1.5em;
|
||
padding-top: 1em;
|
||
}
|
||
|
||
.chapter-new-part .chapter-opener {
|
||
margin-top: 0;
|
||
padding-top: 1.5em;
|
||
}
|
||
|
||
.chapter-number {
|
||
font-family: var(--font-sans);
|
||
font-size: 42pt;
|
||
font-weight: 100;
|
||
color: var(--color-accent-light);
|
||
line-height: 1;
|
||
margin-bottom: 0.1em;
|
||
}
|
||
|
||
.chapter-meta {
|
||
padding: 0;
|
||
}
|
||
|
||
.chapter-part {
|
||
font-family: var(--font-sans);
|
||
font-size: 7.5pt;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.15em;
|
||
color: var(--color-text-light);
|
||
display: block;
|
||
margin-bottom: 0.4em;
|
||
}
|
||
|
||
.chapter-title {
|
||
font-family: var(--font-sans);
|
||
font-size: 22pt;
|
||
font-weight: 700;
|
||
color: var(--color-text);
|
||
line-height: 1.15;
|
||
letter-spacing: -0.02em;
|
||
margin: 0 0 0.5em 0;
|
||
}
|
||
|
||
.chapter-rule {
|
||
width: 3em;
|
||
height: 1px;
|
||
background: var(--color-accent);
|
||
}
|
||
|
||
.chapter-content {
|
||
columns: 1;
|
||
}
|
||
|
||
/* ========================================
|
||
HEADINGS
|
||
======================================== */
|
||
h1 {
|
||
font-family: var(--font-sans);
|
||
font-size: 15pt;
|
||
font-weight: 700;
|
||
color: var(--color-text);
|
||
margin-top: 2em;
|
||
margin-bottom: 0.5em;
|
||
line-height: 1.25;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
|
||
h2 {
|
||
font-family: var(--font-sans);
|
||
font-size: 12.5pt;
|
||
font-weight: 700;
|
||
color: var(--color-text);
|
||
margin-top: 1.8em;
|
||
margin-bottom: 0.5em;
|
||
padding-bottom: 0.25em;
|
||
border-bottom: 1px solid var(--color-border);
|
||
line-height: 1.3;
|
||
letter-spacing: -0.005em;
|
||
}
|
||
|
||
h3 {
|
||
font-family: var(--font-sans);
|
||
font-size: 10.5pt;
|
||
font-weight: 700;
|
||
color: var(--color-text);
|
||
margin-top: 1.5em;
|
||
margin-bottom: 0.4em;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
h4 {
|
||
font-family: var(--font-sans);
|
||
font-size: 10pt;
|
||
font-weight: 600;
|
||
color: var(--color-text-muted);
|
||
margin-top: 1.3em;
|
||
margin-bottom: 0.3em;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* ========================================
|
||
BODY TEXT
|
||
======================================== */
|
||
p {
|
||
margin-bottom: 0.9em;
|
||
text-align: justify;
|
||
text-justify: inter-word;
|
||
}
|
||
|
||
/* First paragraph after heading - no indent, with drop cap option */
|
||
h1 + p, h2 + p, h3 + p, h4 + p,
|
||
.chapter-content > p:first-child {
|
||
text-indent: 0;
|
||
}
|
||
|
||
/* Subsequent paragraphs - indented */
|
||
p + p {
|
||
text-indent: 1.5em;
|
||
margin-top: -0.2em;
|
||
}
|
||
|
||
strong {
|
||
font-weight: 600;
|
||
}
|
||
|
||
em {
|
||
font-style: italic;
|
||
}
|
||
|
||
a {
|
||
color: var(--color-accent);
|
||
text-decoration: none;
|
||
border-bottom: 1px solid var(--color-accent-light);
|
||
}
|
||
|
||
/* ========================================
|
||
LISTS
|
||
======================================== */
|
||
ul, ol {
|
||
margin: 1em 0;
|
||
padding-left: 1.5em;
|
||
}
|
||
|
||
li {
|
||
margin-bottom: 0.4em;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
li p {
|
||
margin-bottom: 0.3em;
|
||
}
|
||
|
||
/* ========================================
|
||
CODE
|
||
======================================== */
|
||
code {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.85em;
|
||
background: var(--color-bg-muted);
|
||
padding: 0.15em 0.35em;
|
||
border-radius: 3px;
|
||
color: #c41d7f;
|
||
}
|
||
|
||
pre, .code-block {
|
||
font-family: var(--font-mono);
|
||
font-size: 8.5pt;
|
||
line-height: 1.5;
|
||
background: #1e1e1e;
|
||
color: #d4d4d4;
|
||
padding: 1em 1.2em;
|
||
border-radius: 4px;
|
||
margin: 1.2em 0;
|
||
overflow-x: auto;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
pre code, .code-block code {
|
||
background: none;
|
||
padding: 0;
|
||
color: inherit;
|
||
font-size: inherit;
|
||
}
|
||
|
||
.prompt-code {
|
||
font-family: var(--font-mono);
|
||
font-size: 8.5pt;
|
||
line-height: 1.5;
|
||
background: #1e1e1e;
|
||
color: #d4d4d4;
|
||
padding: 1em 1.2em;
|
||
border-radius: 4px;
|
||
white-space: pre-wrap;
|
||
margin: 0.8em 0;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
/* ========================================
|
||
TRY IT BOX
|
||
======================================== */
|
||
.tryit-box {
|
||
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
|
||
border: 1px solid #e9d5ff;
|
||
border-left: 4px solid var(--color-accent);
|
||
border-radius: 0 6px 6px 0;
|
||
padding: 1.2em;
|
||
margin: 1.5em 0;
|
||
}
|
||
|
||
.tryit-header {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--color-accent);
|
||
margin-bottom: 0.6em;
|
||
}
|
||
|
||
.tryit-desc {
|
||
font-size: 9pt;
|
||
color: var(--color-text-muted);
|
||
margin-bottom: 0.8em;
|
||
font-style: italic;
|
||
}
|
||
|
||
/* ========================================
|
||
QUIZ BOX
|
||
======================================== */
|
||
.quiz-box {
|
||
background: var(--color-bg-subtle);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 6px;
|
||
padding: 1.2em;
|
||
margin: 1.5em 0;
|
||
}
|
||
|
||
.quiz-header {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--color-text-muted);
|
||
margin-bottom: 0.8em;
|
||
}
|
||
|
||
.quiz-question {
|
||
font-size: 10pt;
|
||
margin-bottom: 0.8em;
|
||
}
|
||
|
||
.quiz-options {
|
||
font-size: 9.5pt;
|
||
margin: 0.8em 0;
|
||
padding-left: 0.5em;
|
||
white-space: pre-line;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.quiz-explanation {
|
||
font-size: 9pt;
|
||
color: var(--color-text-muted);
|
||
font-style: italic;
|
||
margin-top: 1em;
|
||
padding-top: 0.8em;
|
||
border-top: 1px dashed var(--color-border);
|
||
}
|
||
|
||
/* ========================================
|
||
CALLOUT BOXES
|
||
======================================== */
|
||
.callout {
|
||
background: var(--color-bg-subtle);
|
||
border-radius: 4px;
|
||
padding: 1em 1.2em;
|
||
margin: 1.5em 0;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.callout-info {
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.callout-warning {
|
||
background: #fffbf5;
|
||
}
|
||
|
||
.callout-tip {
|
||
background: #f8fdf9;
|
||
}
|
||
|
||
.callout-example {
|
||
background: #faf8ff;
|
||
}
|
||
|
||
.callout-header {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
font-weight: 600;
|
||
margin-bottom: 0.5em;
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
.callout-content {
|
||
font-size: 9.5pt;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
/* ========================================
|
||
INFO GRID
|
||
======================================== */
|
||
.info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 0.8em;
|
||
margin: 1.5em 0;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.info-item {
|
||
background: var(--color-bg-muted);
|
||
padding: 0.8em;
|
||
border-radius: 4px;
|
||
font-size: 9pt;
|
||
line-height: 1.5;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.info-item strong {
|
||
font-family: var(--font-sans);
|
||
font-size: 8.5pt;
|
||
}
|
||
|
||
/* ========================================
|
||
CHECKLIST
|
||
======================================== */
|
||
.checklist {
|
||
background: var(--color-bg-subtle);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 6px;
|
||
padding: 1em 1.2em;
|
||
margin: 1.5em 0;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.checklist-title {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
font-weight: 600;
|
||
margin-bottom: 0.6em;
|
||
}
|
||
|
||
.checklist ul {
|
||
list-style: none;
|
||
padding-left: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.checklist li {
|
||
font-size: 9.5pt;
|
||
margin-bottom: 0.3em;
|
||
padding-left: 0.3em;
|
||
}
|
||
|
||
/* ========================================
|
||
COMPARE BOX
|
||
======================================== */
|
||
.compare-box {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0.8em;
|
||
margin: 1.5em 0;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.compare-item {
|
||
padding: 0.8em;
|
||
border-radius: 4px;
|
||
font-size: 9pt;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.compare-before {
|
||
background: #fef2f2;
|
||
border: 1px solid #fecaca;
|
||
}
|
||
|
||
.compare-after {
|
||
background: #f0fdf4;
|
||
border: 1px solid #bbf7d0;
|
||
}
|
||
|
||
/* ========================================
|
||
DEMO BOXES (static rendered components)
|
||
======================================== */
|
||
.demo-box {
|
||
background: var(--color-bg-subtle);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 6px;
|
||
padding: 1.2em;
|
||
margin: 1.5em 0;
|
||
}
|
||
|
||
.demo-header {
|
||
font-family: var(--font-sans);
|
||
font-size: 9.5pt;
|
||
font-weight: 600;
|
||
color: var(--color-text-muted);
|
||
margin-bottom: 0.8em;
|
||
padding-bottom: 0.5em;
|
||
border-bottom: 1px solid var(--color-border);
|
||
}
|
||
|
||
.demo-label {
|
||
font-size: 9pt;
|
||
color: var(--color-text-muted);
|
||
margin: 0.4em 0;
|
||
}
|
||
|
||
.demo-note {
|
||
font-size: 8.5pt;
|
||
color: var(--color-text-light);
|
||
font-style: italic;
|
||
margin: 0.6em 0;
|
||
}
|
||
|
||
.demo-section {
|
||
font-size: 9pt;
|
||
margin-top: 1em;
|
||
margin-bottom: 0.3em;
|
||
}
|
||
|
||
.demo-text {
|
||
font-size: 9pt;
|
||
background: var(--color-bg-muted);
|
||
padding: 0.6em;
|
||
border-radius: 4px;
|
||
margin: 0.3em 0;
|
||
}
|
||
|
||
.demo-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 8.5pt;
|
||
margin: 0.8em 0;
|
||
}
|
||
|
||
.demo-table th {
|
||
font-family: var(--font-sans);
|
||
background: var(--color-bg-muted);
|
||
padding: 0.5em 0.8em;
|
||
text-align: left;
|
||
border-bottom: 2px solid var(--color-border);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.demo-table td {
|
||
padding: 0.4em 0.8em;
|
||
border-bottom: 1px solid var(--color-border);
|
||
}
|
||
|
||
/* ========================================
|
||
EXERCISE BOXES (fill-in-blank, debugger, challenges)
|
||
======================================== */
|
||
.exercise-box {
|
||
background: #fefce8;
|
||
border: 1px solid #fde68a;
|
||
border-radius: 6px;
|
||
padding: 1.2em;
|
||
margin: 1.5em 0;
|
||
}
|
||
|
||
.exercise-header {
|
||
font-family: var(--font-sans);
|
||
font-size: 9.5pt;
|
||
font-weight: 600;
|
||
color: #92400e;
|
||
margin-bottom: 0.8em;
|
||
}
|
||
|
||
.exercise-section {
|
||
font-size: 9pt;
|
||
margin-top: 0.8em;
|
||
margin-bottom: 0.3em;
|
||
}
|
||
|
||
.exercise-answers {
|
||
font-size: 8.5pt;
|
||
margin-top: 0.8em;
|
||
padding-top: 0.6em;
|
||
border-top: 1px dashed #fde68a;
|
||
}
|
||
|
||
.exercise-hint {
|
||
font-size: 8.5pt;
|
||
color: #92400e;
|
||
font-style: italic;
|
||
margin: 0.6em 0;
|
||
}
|
||
|
||
.difficulty-badge {
|
||
font-size: 7pt;
|
||
padding: 2px 6px;
|
||
border-radius: 10px;
|
||
background: var(--color-bg-muted);
|
||
color: var(--color-text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.prompt-code-error {
|
||
border: 1px solid #fecaca;
|
||
background: #450a0a;
|
||
}
|
||
|
||
/* ========================================
|
||
PREDICTION TOKENS
|
||
======================================== */
|
||
.prediction-step {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 0.8em;
|
||
margin: 0.5em 0;
|
||
font-size: 9pt;
|
||
}
|
||
|
||
.prediction-context {
|
||
font-family: var(--font-mono);
|
||
font-size: 8.5pt;
|
||
color: var(--color-text-muted);
|
||
min-width: 40%;
|
||
}
|
||
|
||
.prediction-options {
|
||
font-size: 8.5pt;
|
||
}
|
||
|
||
.prediction-token {
|
||
display: inline-block;
|
||
background: var(--color-bg-muted);
|
||
padding: 1px 6px;
|
||
border-radius: 3px;
|
||
margin: 0 2px;
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
.prediction-prob {
|
||
font-size: 7pt;
|
||
color: var(--color-text-light);
|
||
}
|
||
|
||
/* ========================================
|
||
QUIZ ENHANCEMENTS
|
||
======================================== */
|
||
.quiz-correct {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.quiz-options div {
|
||
font-size: 9pt;
|
||
padding: 0.15em 0;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* ========================================
|
||
TEMPERATURE LEVELS
|
||
======================================== */
|
||
.temp-level {
|
||
margin: 0.8em 0;
|
||
padding: 0.6em;
|
||
background: var(--color-bg-muted);
|
||
border-radius: 4px;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.temp-label {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
margin-bottom: 0.3em;
|
||
}
|
||
|
||
.temp-example {
|
||
font-size: 8.5pt;
|
||
font-family: var(--font-mono);
|
||
color: var(--color-text-muted);
|
||
margin: 0.2em 0;
|
||
}
|
||
|
||
.temp-use {
|
||
font-size: 8pt;
|
||
color: var(--color-text-light);
|
||
font-style: italic;
|
||
margin-top: 0.3em;
|
||
}
|
||
|
||
/* ========================================
|
||
ITERATION STEPS
|
||
======================================== */
|
||
.iteration-step {
|
||
margin: 1em 0;
|
||
padding: 0.8em;
|
||
background: var(--color-bg-muted);
|
||
border-radius: 4px;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.iteration-header {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
font-weight: 600;
|
||
margin-bottom: 0.5em;
|
||
}
|
||
|
||
.iteration-output {
|
||
font-size: 9pt;
|
||
color: var(--color-text-muted);
|
||
padding: 0.5em;
|
||
background: white;
|
||
border-radius: 3px;
|
||
margin-top: 0.5em;
|
||
}
|
||
|
||
.iteration-issue {
|
||
font-size: 8.5pt;
|
||
color: #92400e;
|
||
margin-top: 0.4em;
|
||
}
|
||
|
||
.iteration-success {
|
||
font-size: 8.5pt;
|
||
color: #166534;
|
||
margin-top: 0.4em;
|
||
}
|
||
|
||
/* ========================================
|
||
COST RESULTS
|
||
======================================== */
|
||
.cost-results {
|
||
display: flex;
|
||
gap: 0.8em;
|
||
margin: 1em 0;
|
||
}
|
||
|
||
.cost-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 0.6em;
|
||
background: var(--color-bg-muted);
|
||
border-radius: 4px;
|
||
font-size: 9pt;
|
||
}
|
||
|
||
/* ========================================
|
||
CHAIN BOXES (ChainExample)
|
||
======================================== */
|
||
.chain-box {
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 6px;
|
||
margin: 1.2em 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.chain-box-header {
|
||
font-family: var(--font-sans);
|
||
font-size: 8.5pt;
|
||
font-weight: 500;
|
||
color: var(--color-text-muted);
|
||
padding: 0.4em 1em;
|
||
background: var(--color-bg-muted);
|
||
border-bottom: 1px solid var(--color-border);
|
||
}
|
||
|
||
.chain-step-item, .chain-step-skipped {
|
||
display: flex;
|
||
gap: 0.8em;
|
||
padding: 0.6em 1em;
|
||
}
|
||
|
||
.chain-step-skipped {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.chain-step-num {
|
||
font-family: var(--font-sans);
|
||
font-size: 8pt;
|
||
font-weight: 600;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
background: var(--color-bg-muted);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.chain-step-body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.chain-step-name {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
font-weight: 600;
|
||
margin-bottom: 0.3em;
|
||
}
|
||
|
||
.chain-step-prompt, .chain-step-output {
|
||
font-family: var(--font-mono);
|
||
font-size: 7.5pt;
|
||
line-height: 1.4;
|
||
padding: 0.4em 0.6em;
|
||
border-radius: 3px;
|
||
margin: 0.2em 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.chain-step-prompt {
|
||
background: var(--color-bg-muted);
|
||
}
|
||
|
||
.chain-step-output {
|
||
background: #f0fdf4;
|
||
border: 1px solid #bbf7d0;
|
||
}
|
||
|
||
.chain-label {
|
||
font-family: var(--font-sans);
|
||
font-size: 7pt;
|
||
font-weight: 600;
|
||
color: var(--color-text-light);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
.chain-step-skipped-note {
|
||
font-size: 8pt;
|
||
font-style: italic;
|
||
color: var(--color-text-light);
|
||
}
|
||
|
||
.chain-connector {
|
||
height: 1px;
|
||
background: var(--color-border);
|
||
margin: 0 1em 0 2.6em;
|
||
}
|
||
|
||
.chain-connector-parallel {
|
||
height: 1px;
|
||
background: var(--color-border);
|
||
margin: 0 1em 0 2.6em;
|
||
border-top-style: dashed;
|
||
}
|
||
|
||
.chain-loop-note {
|
||
font-family: var(--font-sans);
|
||
font-size: 8pt;
|
||
color: var(--color-text-light);
|
||
text-align: center;
|
||
padding: 0.5em;
|
||
border-top: 1px dashed var(--color-border);
|
||
}
|
||
|
||
/* ========================================
|
||
CHAIN FLOW DEMO (type overview)
|
||
======================================== */
|
||
.chain-types-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0.8em;
|
||
}
|
||
|
||
.chain-type-card {
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 4px;
|
||
padding: 0.8em;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.chain-type-name {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
font-weight: 700;
|
||
margin-bottom: 0.2em;
|
||
}
|
||
|
||
.chain-type-desc {
|
||
font-size: 8pt;
|
||
color: var(--color-text-muted);
|
||
margin-bottom: 0.5em;
|
||
}
|
||
|
||
.chain-type-diagram {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.2em;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.chain-type-diagram-parallel {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.chain-type-step {
|
||
display: inline-block;
|
||
font-family: var(--font-sans);
|
||
font-size: 7pt;
|
||
font-weight: 500;
|
||
padding: 2px 6px;
|
||
border-radius: 3px;
|
||
border: 1px solid;
|
||
}
|
||
|
||
.chain-type-arrow {
|
||
font-size: 8pt;
|
||
color: var(--color-text-light);
|
||
}
|
||
|
||
/* ========================================
|
||
FRAMEWORK STEPS
|
||
======================================== */
|
||
.fw-step {
|
||
display: flex;
|
||
gap: 0.8em;
|
||
margin: 0.6em 0;
|
||
align-items: flex-start;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.fw-letter {
|
||
font-family: var(--font-sans);
|
||
font-size: 12pt;
|
||
font-weight: 700;
|
||
width: 2em;
|
||
height: 2em;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.fw-step-body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding-top: 0.2em;
|
||
}
|
||
|
||
.fw-step-label {
|
||
font-size: 9pt;
|
||
margin-bottom: 0.15em;
|
||
}
|
||
|
||
.fw-step-example {
|
||
font-size: 8pt;
|
||
font-family: var(--font-mono);
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
/* ========================================
|
||
PRINCIPLES LIST
|
||
======================================== */
|
||
.principle-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6em;
|
||
padding: 0.4em 0;
|
||
font-size: 9pt;
|
||
border-bottom: 1px solid var(--color-border);
|
||
}
|
||
|
||
.principle-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.principle-icon {
|
||
font-size: 11pt;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ========================================
|
||
VERSION DIFF
|
||
======================================== */
|
||
.version-block {
|
||
margin: 0.6em 0;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.version-header {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
margin-bottom: 0.3em;
|
||
}
|
||
|
||
.version-label {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.version-note {
|
||
font-size: 7.5pt;
|
||
color: var(--color-text-light);
|
||
font-style: italic;
|
||
}
|
||
|
||
/* ========================================
|
||
JAILBREAK EXAMPLES
|
||
======================================== */
|
||
.jailbreak-example {
|
||
margin: 0.8em 0;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.jailbreak-name {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
margin-bottom: 0.4em;
|
||
}
|
||
|
||
/* ========================================
|
||
PROMPT BREAKDOWN
|
||
======================================== */
|
||
.prompt-breakdown {
|
||
margin: 1.5em 0;
|
||
padding: 1.5em 1em 1em 1em;
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 6px;
|
||
font-family: var(--font-mono);
|
||
font-size: 9pt;
|
||
line-height: 2.2;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.pb-segment {
|
||
display: inline;
|
||
position: relative;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pb-label {
|
||
font-family: var(--font-sans);
|
||
font-size: 7pt;
|
||
font-weight: 600;
|
||
display: block;
|
||
margin-bottom: -2px;
|
||
}
|
||
|
||
.pb-text {
|
||
padding: 1px 4px;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
/* ========================================
|
||
SPECIFICITY SPECTRUM
|
||
======================================== */
|
||
.spectrum-level {
|
||
margin: 0.6em 0;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.spectrum-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6em;
|
||
margin-bottom: 0.3em;
|
||
}
|
||
|
||
.spectrum-badge {
|
||
font-family: var(--font-sans);
|
||
font-size: 7.5pt;
|
||
font-weight: 600;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.spectrum-bar-wrap {
|
||
flex: 1;
|
||
height: 4px;
|
||
background: var(--color-bg-muted);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.spectrum-bar {
|
||
display: block;
|
||
height: 100%;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.spectrum-level .prompt-code {
|
||
margin: 0.2em 0;
|
||
font-size: 8pt;
|
||
padding: 0.6em 0.8em;
|
||
}
|
||
|
||
/* ========================================
|
||
IMAGE / VIDEO PROMPT BUILDER
|
||
======================================== */
|
||
.image-category {
|
||
display: flex;
|
||
align-items: baseline;
|
||
flex-wrap: wrap;
|
||
gap: 0.4em;
|
||
margin: 0.5em 0;
|
||
font-size: 8.5pt;
|
||
}
|
||
|
||
.image-cat-label {
|
||
font-family: var(--font-sans);
|
||
font-weight: 600;
|
||
min-width: 6em;
|
||
font-size: 8.5pt;
|
||
}
|
||
|
||
.image-option {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
border-radius: 3px;
|
||
font-size: 8pt;
|
||
background: var(--color-bg-muted);
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.image-example {
|
||
margin: 0.6em 0;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.image-example .prompt-code {
|
||
margin: 0.3em 0;
|
||
}
|
||
|
||
.image-example .demo-note {
|
||
margin: 0.2em 0;
|
||
}
|
||
|
||
.diffusion-steps {
|
||
margin: 0.5em 0;
|
||
padding-left: 0.5em;
|
||
}
|
||
|
||
.diffusion-step {
|
||
font-size: 8.5pt;
|
||
padding: 0.25em 0;
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
/* ========================================
|
||
CODE EDITOR
|
||
======================================== */
|
||
.code-editor-box {
|
||
border: 1px solid #3c3c3c;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
margin: 1.2em 0;
|
||
}
|
||
|
||
.code-editor-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6em;
|
||
padding: 0.5em 0.8em;
|
||
background: #252526;
|
||
border-bottom: 1px solid #3c3c3c;
|
||
font-size: 8pt;
|
||
}
|
||
|
||
.code-editor-dots {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
|
||
.code-editor-dots span {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
display: inline-block;
|
||
${PRINT_READY ? 'background: #999 !important;' : ''}
|
||
}
|
||
|
||
.code-editor-filename {
|
||
font-family: var(--font-mono);
|
||
color: #ccc;
|
||
margin-left: 0.4em;
|
||
}
|
||
|
||
.code-editor-lang {
|
||
margin-left: auto;
|
||
text-transform: uppercase;
|
||
color: #6e6e6e;
|
||
font-family: var(--font-sans);
|
||
}
|
||
|
||
.code-editor-box .prompt-code {
|
||
margin: 0;
|
||
border-radius: 0;
|
||
}
|
||
|
||
/* ========================================
|
||
CONTEXT BLOCKS
|
||
======================================== */
|
||
.context-block {
|
||
padding: 0.6em 0.8em;
|
||
border-radius: 4px;
|
||
margin: 0.4em 0;
|
||
font-size: 8.5pt;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.context-block-on {
|
||
background: var(--color-bg-subtle);
|
||
border: 1px solid var(--color-border);
|
||
}
|
||
|
||
.context-block-off {
|
||
background: var(--color-bg-muted);
|
||
opacity: 0.5;
|
||
border: 1px dashed var(--color-border);
|
||
}
|
||
|
||
/* ========================================
|
||
PROMPT BUILDER
|
||
======================================== */
|
||
.builder-field {
|
||
margin: 0.8em 0;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
.builder-field-label {
|
||
font-family: var(--font-sans);
|
||
font-size: 9pt;
|
||
font-weight: 600;
|
||
margin-bottom: 0.15em;
|
||
}
|
||
|
||
.builder-field-hint {
|
||
font-size: 7.5pt;
|
||
color: var(--color-text-light);
|
||
font-style: italic;
|
||
margin-bottom: 0.3em;
|
||
}
|
||
|
||
.builder-field-input {
|
||
font-family: var(--font-mono);
|
||
font-size: 8pt;
|
||
color: var(--color-text-light);
|
||
padding: 0.5em 0.6em;
|
||
border: 1px dashed var(--color-border);
|
||
border-radius: 4px;
|
||
min-height: 2em;
|
||
background: white;
|
||
}
|
||
|
||
/* ========================================
|
||
INTERACTIVE NOTICE
|
||
======================================== */
|
||
.interactive-notice {
|
||
font-family: var(--font-sans);
|
||
font-size: 8pt;
|
||
color: var(--color-accent);
|
||
background: linear-gradient(135deg, #faf5ff, #f3e8ff);
|
||
border: 1px dashed var(--color-accent-light);
|
||
border-radius: 4px;
|
||
padding: 0.6em 1em;
|
||
margin: 0.8em 0;
|
||
text-align: center;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
/* ========================================
|
||
HORIZONTAL RULES & IMAGES
|
||
======================================== */
|
||
/* ========================================
|
||
INLINE SVG ICONS (print-ready mode)
|
||
======================================== */
|
||
.ico {
|
||
width: 14px;
|
||
height: 14px;
|
||
display: inline-block;
|
||
vertical-align: -2px;
|
||
margin-right: 3px;
|
||
}
|
||
|
||
.ico-sm {
|
||
width: 10px;
|
||
height: 10px;
|
||
display: inline-block;
|
||
vertical-align: -1px;
|
||
margin-right: 2px;
|
||
}
|
||
|
||
/* ========================================
|
||
ENDNOTES / FOOTNOTES
|
||
======================================== */
|
||
.fn-ref {
|
||
font-family: var(--font-sans);
|
||
font-size: 7pt;
|
||
color: var(--color-accent);
|
||
vertical-align: super;
|
||
line-height: 0;
|
||
margin-left: 1px;
|
||
}
|
||
|
||
.fn-section {
|
||
margin-top: 2em;
|
||
padding-top: 1em;
|
||
border-top: 1px solid var(--color-border);
|
||
page-break-inside: avoid;
|
||
}
|
||
|
||
.fn-title {
|
||
font-family: var(--font-sans);
|
||
font-size: 8pt;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.1em;
|
||
color: var(--color-text-light);
|
||
margin-bottom: 0.5em;
|
||
}
|
||
|
||
.fn-item {
|
||
font-size: 7.5pt;
|
||
line-height: 1.5;
|
||
margin-bottom: 0.2em;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.fn-num {
|
||
font-family: var(--font-sans);
|
||
font-weight: 600;
|
||
color: var(--color-accent);
|
||
min-width: 1.5em;
|
||
display: inline-block;
|
||
}
|
||
|
||
.fn-url {
|
||
font-family: var(--font-mono);
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
blockquote {
|
||
margin: 1.2em 0;
|
||
padding: 0.6em 1em;
|
||
border-left: 3px solid var(--color-border-dark);
|
||
color: var(--color-text-muted);
|
||
font-style: italic;
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
blockquote p {
|
||
margin: 0;
|
||
text-indent: 0;
|
||
}
|
||
|
||
hr {
|
||
border: none;
|
||
border-top: 1px solid var(--color-border);
|
||
margin: 2em 0;
|
||
}
|
||
|
||
img {
|
||
max-width: 100%;
|
||
height: auto;
|
||
border-radius: 4px;
|
||
margin: 1em 0;
|
||
}
|
||
|
||
/* ========================================
|
||
BACK MATTER
|
||
======================================== */
|
||
.back-matter {
|
||
page-break-before: always;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
min-height: 100vh;
|
||
padding: 3em 2em;
|
||
}
|
||
|
||
.back-matter h2 {
|
||
font-family: var(--font-sans);
|
||
font-size: 18pt;
|
||
font-weight: 700;
|
||
border: none;
|
||
margin-bottom: 0.8em;
|
||
}
|
||
|
||
.back-matter p {
|
||
text-align: left;
|
||
font-size: 10pt;
|
||
}
|
||
|
||
.back-matter ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 1.2em 0;
|
||
}
|
||
|
||
.back-matter li {
|
||
font-size: 10pt;
|
||
margin-bottom: 0.4em;
|
||
padding-left: 1.2em;
|
||
position: relative;
|
||
}
|
||
|
||
.back-matter li::before {
|
||
content: "—";
|
||
position: absolute;
|
||
left: 0;
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
.colophon {
|
||
font-size: 7.5pt;
|
||
color: var(--color-text-light);
|
||
margin-top: 4em;
|
||
padding-top: 1.5em;
|
||
border-top: 1px solid var(--color-border);
|
||
}
|
||
|
||
/* ========================================
|
||
HALF TITLE PAGE (before TOC)
|
||
======================================== */
|
||
.half-title {
|
||
page-break-after: always;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 100vh;
|
||
text-align: center;
|
||
}
|
||
|
||
.half-title h1 {
|
||
font-family: var(--font-sans);
|
||
font-size: 18pt;
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
border: none;
|
||
margin: 0;
|
||
}
|
||
|
||
.half-title p {
|
||
text-align: center;
|
||
font-size: 9pt;
|
||
color: var(--color-text-light);
|
||
margin-top: 0.5em;
|
||
}
|
||
|
||
/* ========================================
|
||
PAGE BREAKS
|
||
======================================== */
|
||
.page-break {
|
||
page-break-after: always;
|
||
}
|
||
|
||
/* ========================================
|
||
PRINT OPTIMIZATIONS
|
||
======================================== */
|
||
@media print {
|
||
body {
|
||
font-size: 10pt;
|
||
}
|
||
|
||
/* Only new parts get page breaks */
|
||
.chapter-new-part {
|
||
page-break-before: always;
|
||
}
|
||
|
||
pre, .code-block, .prompt-code {
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
a {
|
||
text-decoration: none;
|
||
border-bottom: none;
|
||
}
|
||
|
||
/* Small elements: avoid page breaks inside */
|
||
.callout,
|
||
.info-item,
|
||
.checklist,
|
||
.interactive-notice,
|
||
.chain-type-card,
|
||
.prompt-breakdown,
|
||
.chapter-opener,
|
||
.fw-step,
|
||
.iteration-step,
|
||
.version-block,
|
||
.builder-field,
|
||
.context-block,
|
||
.image-example,
|
||
.jailbreak-example,
|
||
.compare-box,
|
||
.fn-section {
|
||
page-break-inside: avoid;
|
||
break-inside: avoid;
|
||
}
|
||
|
||
/* Large elements: ALLOW page breaks inside to avoid huge gaps.
|
||
These can be multi-page so forcing avoid wastes space. */
|
||
/* .demo-box, .chain-box, .exercise-box, .code-editor-box — intentionally no avoid */
|
||
|
||
/* Keep headings with following content */
|
||
h1, h2, h3, h4, h5, h6 {
|
||
page-break-after: avoid;
|
||
break-after: avoid;
|
||
}
|
||
|
||
/* Ensure cover and half-title use full pages */
|
||
.cover, .half-title {
|
||
page-break-after: always;
|
||
}
|
||
}
|
||
|
||
/* ========================================
|
||
RTL SUPPORT
|
||
======================================== */
|
||
[dir="rtl"] {
|
||
text-align: right;
|
||
}
|
||
|
||
[dir="rtl"] p {
|
||
text-align: right;
|
||
}
|
||
|
||
[dir="rtl"] p + p {
|
||
text-indent: 0;
|
||
}
|
||
|
||
[dir="rtl"] .tryit-box {
|
||
border-left: 1px solid #e9d5ff;
|
||
border-right: 4px solid var(--color-accent);
|
||
border-radius: 6px 0 0 6px;
|
||
}
|
||
|
||
[dir="rtl"] ul, [dir="rtl"] ol {
|
||
padding-left: 0;
|
||
padding-right: 1.5em;
|
||
}
|
||
|
||
[dir="rtl"] .toc-chapter {
|
||
padding-left: 0;
|
||
padding-right: 1em;
|
||
}
|
||
|
||
[dir="rtl"] .chapter-opener {
|
||
flex-direction: row-reverse;
|
||
}
|
||
${PRINT_READY ? `
|
||
/* ========================================
|
||
PRINT-READY OVERRIDES
|
||
B&W grayscale + bleed extensions
|
||
======================================== */
|
||
|
||
/* Print: no border radius anywhere */
|
||
* {
|
||
border-radius: 0 !important;
|
||
}
|
||
|
||
/* Print: remove outer borders from interactive containers */
|
||
.demo-box,
|
||
.exercise-box,
|
||
.tryit-box,
|
||
.quiz-box,
|
||
.chain-box,
|
||
.code-editor-box,
|
||
.checklist,
|
||
.prompt-breakdown,
|
||
.compare-box {
|
||
border: none !important;
|
||
border-top: 1px solid #ccc !important;
|
||
border-bottom: 1px solid #ccc !important;
|
||
background: transparent !important;
|
||
padding-left: 0 !important;
|
||
padding-right: 0 !important;
|
||
margin-top: 1.2em !important;
|
||
margin-bottom: 1.2em !important;
|
||
padding-top: 0.8em !important;
|
||
padding-bottom: 0.8em !important;
|
||
}
|
||
|
||
/* Nested items inside bordered containers: no borders */
|
||
.compare-item,
|
||
.chain-type-card,
|
||
.info-item,
|
||
.chain-step-item,
|
||
.context-block,
|
||
.iteration-step,
|
||
.version-block,
|
||
.temp-level,
|
||
.image-example,
|
||
.jailbreak-example,
|
||
.fw-step,
|
||
.builder-field,
|
||
.prediction-step {
|
||
border: none !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
/* Code blocks: no bg, no border, black text */
|
||
pre, .code-block, .prompt-code {
|
||
background: transparent !important;
|
||
color: #000000 !important;
|
||
border: none !important;
|
||
}
|
||
pre code, .code-block code {
|
||
color: #000000 !important;
|
||
background: transparent !important;
|
||
}
|
||
.code-editor-box {
|
||
border: none !important;
|
||
}
|
||
.code-editor-header {
|
||
background: transparent !important;
|
||
border: none !important;
|
||
}
|
||
.code-editor-filename { color: #333 !important; }
|
||
.code-editor-lang { color: #666 !important; }
|
||
|
||
/* TryIt: no border in print */
|
||
|
||
/* Callouts: all white with gray left border */
|
||
.callout, .callout-info, .callout-warning, .callout-tip, .callout-example {
|
||
background: #f2f2f2 !important;
|
||
border: none !important;
|
||
padding: 1em 1.2em !important;
|
||
}
|
||
|
||
/* Inline code: no bg */
|
||
code {
|
||
color: #000 !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
/* Exercise elements */
|
||
.exercise-header {
|
||
color: #333 !important;
|
||
}
|
||
.exercise-hint {
|
||
color: #333 !important;
|
||
}
|
||
.exercise-answers {
|
||
border-top: none !important;
|
||
}
|
||
|
||
/* Prompt code error variant */
|
||
.prompt-code-error {
|
||
background: #fff !important;
|
||
border-color: #999 !important;
|
||
}
|
||
|
||
/* Quiz: white */
|
||
/* Info items, chain cards: no bg */
|
||
.info-item {
|
||
background: transparent !important;
|
||
}
|
||
.chain-type-card {
|
||
border: none !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
/* Compare boxes: no bg, no border in print */
|
||
|
||
/* Cover and chapter rules: black */
|
||
.cover-rule {
|
||
background: #000000 !important;
|
||
}
|
||
.chapter-rule {
|
||
background: #000000 !important;
|
||
}
|
||
|
||
/* Chain elements: grayscale */
|
||
.chain-step-output {
|
||
background: #f2f2f2 !important;
|
||
border-color: #cccccc !important;
|
||
}
|
||
.chain-step-prompt {
|
||
background: #f2f2f2 !important;
|
||
}
|
||
|
||
/* Iteration steps */
|
||
.iteration-step {
|
||
background: #f2f2f2 !important;
|
||
}
|
||
.iteration-output {
|
||
background: #ffffff !important;
|
||
}
|
||
|
||
/* Temperature levels */
|
||
.temp-level {
|
||
background: #f2f2f2 !important;
|
||
}
|
||
|
||
/* Context blocks */
|
||
.context-block-on {
|
||
background: #ffffff !important;
|
||
border-color: #cccccc !important;
|
||
}
|
||
.context-block-off {
|
||
background: #f2f2f2 !important;
|
||
border-color: #cccccc !important;
|
||
}
|
||
|
||
/* Builder fields */
|
||
.builder-field-input {
|
||
background: #ffffff !important;
|
||
border-color: #cccccc !important;
|
||
}
|
||
|
||
/* Prompt breakdown segments: grayscale */
|
||
.pb-text {
|
||
background: #f2f2f2 !important;
|
||
border-bottom-color: #000 !important;
|
||
}
|
||
.pb-label {
|
||
color: #333 !important;
|
||
}
|
||
|
||
/* Spectrum badges: grayscale */
|
||
.spectrum-badge {
|
||
background: #333 !important;
|
||
color: #fff !important;
|
||
}
|
||
.spectrum-bar {
|
||
background: #333 !important;
|
||
}
|
||
|
||
/* Framework letters: grayscale */
|
||
.fw-letter {
|
||
background: #f2f2f2 !important;
|
||
border-color: #cccccc !important;
|
||
}
|
||
|
||
/* Image/video option pills */
|
||
.image-option {
|
||
background: #f2f2f2 !important;
|
||
border-color: #ccc !important;
|
||
}
|
||
|
||
/* Footnote refs */
|
||
.fn-ref {
|
||
color: #000 !important;
|
||
}
|
||
.fn-num {
|
||
color: #000 !important;
|
||
}
|
||
|
||
/* Blockquotes */
|
||
blockquote {
|
||
border-left-color: #999 !important;
|
||
color: #333 !important;
|
||
}
|
||
|
||
/* Interactive notice */
|
||
.interactive-notice {
|
||
background: #f2f2f2 !important;
|
||
border-color: #ccc !important;
|
||
color: #333 !important;
|
||
}
|
||
|
||
/* Links */
|
||
a {
|
||
color: #000 !important;
|
||
}
|
||
|
||
/* Print: avoid breaking interactive elements across pages */
|
||
.chapter-opener,
|
||
.tryit-box,
|
||
.quiz-box,
|
||
.callout,
|
||
.demo-box,
|
||
.exercise-box,
|
||
.chain-box,
|
||
.chain-type-card,
|
||
.code-editor-box,
|
||
.compare-box,
|
||
.info-grid,
|
||
.prompt-breakdown,
|
||
.chain-step-item,
|
||
.fw-step,
|
||
.iteration-step,
|
||
.version-block,
|
||
.builder-field,
|
||
.context-block,
|
||
.image-example,
|
||
.jailbreak-example,
|
||
blockquote {
|
||
page-break-inside: avoid !important;
|
||
break-inside: avoid !important;
|
||
}
|
||
` : ''}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Cover Page -->
|
||
<div class="cover">
|
||
<div class="cover-rule"></div>
|
||
<h1>${title}</h1>
|
||
<p class="subtitle">${subtitle}</p>
|
||
<div class="cover-author">
|
||
<img class="cover-avatar" src="https://github.com/f.png" alt="Fatih Kadir Akın" />
|
||
<div class="cover-author-info">
|
||
<span class="author-name">Fatih Kadir Akın</span>
|
||
<span class="author-desc">Creator of prompts.chat, GitHub Star</span>
|
||
</div>
|
||
</div>
|
||
<p class="url">${SITE_URL}/book</p>
|
||
</div>
|
||
|
||
<!-- Half Title -->
|
||
<div class="half-title">
|
||
<h1>${title}</h1>
|
||
<p>${SITE_URL}</p>
|
||
</div>
|
||
|
||
<!-- Table of Contents -->
|
||
<div class="toc">
|
||
<h2 class="toc-title">${tocTitle}</h2>
|
||
${parts.map(part => `
|
||
<div class="toc-part">${translatePart(part.title)}</div>
|
||
${part.chapters.map(ch => `
|
||
<div class="toc-chapter">
|
||
<a href="#${ch.slug}">${translateChapter(ch.slug, ch.title)}</a>
|
||
<span class="toc-dots"></span>
|
||
</div>
|
||
`).join('')}
|
||
`).join('')}
|
||
</div>
|
||
|
||
<!-- Chapters -->
|
||
${chaptersHtml}
|
||
|
||
<!-- Back Matter -->
|
||
<div class="back-matter">
|
||
<h2>Thank You for Reading</h2>
|
||
<p>This book was designed as a companion to <strong>${SITE_URL}/book</strong>, where you can experience the full interactive version:</p>
|
||
<ul>
|
||
<li>Try every prompt directly in your browser</li>
|
||
<li>Interactive quizzes with instant feedback</li>
|
||
<li>Live demos and hands-on coding tools</li>
|
||
<li>Available in 17+ languages</li>
|
||
</ul>
|
||
<p style="margin-top: 1.5em;">If you found this book helpful, consider sharing it with others or contributing to the open-source project on GitHub.</p>
|
||
<div class="colophon">
|
||
<p>${title}</p>
|
||
<p>© ${new Date().getFullYear()} Fatih Kadir Akın — prompts.chat</p>
|
||
<p style="margin-top: 0.6em;">
|
||
Set in Palatino and Helvetica Neue. 6″ × 9″
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
/**
|
||
* Main function
|
||
*/
|
||
async function main() {
|
||
const args = process.argv.slice(2);
|
||
const generateAll = args.includes('--all');
|
||
const requestedLocale = args.find(arg => !arg.startsWith('--')) || 'en';
|
||
|
||
// Ensure output directory exists
|
||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||
}
|
||
|
||
const availableLocales = getAvailableLocales();
|
||
const localesToGenerate = generateAll ? availableLocales : [requestedLocale];
|
||
|
||
console.log('📚 The Interactive Book of Prompting - PDF Generator\n');
|
||
|
||
if (!generateAll && !availableLocales.includes(requestedLocale)) {
|
||
console.error(`❌ Locale '${requestedLocale}' not found.`);
|
||
console.log(`Available locales: ${availableLocales.join(', ')}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
for (const locale of localesToGenerate) {
|
||
console.log(`\n📖 Generating PDF for locale: ${locale}`);
|
||
|
||
// Process all chapters
|
||
const chapters: ProcessedChapter[] = [];
|
||
|
||
// Load locale data and messages for this locale
|
||
const localeData = getLocaleData(locale);
|
||
const messages = loadMessages(locale);
|
||
|
||
for (const part of parts) {
|
||
for (const chapter of part.chapters) {
|
||
const processed = processChapter(locale, chapter, localeData, messages);
|
||
if (processed) {
|
||
chapters.push(processed);
|
||
console.log(` ✓ ${chapter.title}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (chapters.length === 0) {
|
||
console.log(` ⚠ No chapters found for ${locale}, skipping...`);
|
||
continue;
|
||
}
|
||
|
||
// Generate HTML
|
||
const html = generateHtmlDocument(chapters, locale, messages);
|
||
|
||
// Write HTML file (can be converted to PDF with browser print or puppeteer)
|
||
const suffix = PRINT_READY ? '-print' : '';
|
||
const htmlPath = path.join(OUTPUT_DIR, `book-${locale}${suffix}.html`);
|
||
// Apply grayscale conversion to the entire document for print-ready mode
|
||
const finalHtml = convertStylesToGrayscale(html);
|
||
fs.writeFileSync(htmlPath, finalHtml, 'utf-8');
|
||
console.log(`\n 📄 HTML saved to: ${htmlPath}`);
|
||
|
||
console.log(`\n ℹ️ To generate PDF:`);
|
||
console.log(` 1. Open ${htmlPath} in a browser`);
|
||
console.log(` 2. Press Cmd/Ctrl + P to print`);
|
||
console.log(` 3. Select "Save as PDF"`);
|
||
console.log(` Or use: npx puppeteer print ${htmlPath} book-${locale}.pdf`);
|
||
}
|
||
|
||
console.log('\n✅ Done!\n');
|
||
}
|
||
|
||
// Run
|
||
main().catch(console.error);
|