let documents = []; let currentDocument = null; let documentCards = null; let currentLanguage = 'en'; // Default language // Active sidebar filters const activeFilters = { document_type: '', audience: '' }; // UI translations for full page i18n const UI_TRANSLATIONS = { en: { pageTitle: 'Framework Documentation', pageSubtitle: 'Technical specifications, guides, and reference materials', documentsHeading: 'Documents', searchButton: 'Search', backToDocuments: 'Back to Documents', selectDocument: 'Select a Document', selectDocumentDesc: 'Choose a document from the sidebar to begin reading', loadingDocument: 'Loading document...', errorLoadingDoc: 'Error loading document', tableOfContents: 'Table of Contents', downloadPdf: 'Download PDF', sourceCode: 'Source Code', sovereignRepo: 'Sovereign Repository', sovereignRepoDesc: 'Source code hosted on sovereign infrastructure', implGuide: 'Implementation Guide', implGuideDesc: 'Architecture, deployment & integration', categories: { 'getting-started': 'Getting Started', 'resources': 'Resources', 'research-theory': 'Research & Theory', 'technical-reference': 'Technical Reference', 'advanced-topics': 'Advanced Topics', 'business-leadership': 'Business & Leadership' }, filters: { allTypes: 'All types', allAudiences: 'All audiences', clear: 'Clear', showingDocs: '{count} documents', documentTypes: { 'working-paper': 'Working paper', 'case-study': 'Case study', 'technical-report': 'Technical report', 'guide': 'Guide', 'reference': 'Reference', 'brief': 'Brief' }, audiences: { 'researcher': 'Researcher', 'implementer': 'Implementer', 'leader': 'Leader', 'general': 'General' } } }, de: { pageTitle: 'Framework-Dokumentation', pageSubtitle: 'Technische Spezifikationen, Leitfäden und Referenzmaterialien', documentsHeading: 'Dokumente', searchButton: 'Suchen', backToDocuments: 'Zurück zu Dokumenten', selectDocument: 'Dokument auswählen', selectDocumentDesc: 'Wählen Sie ein Dokument aus der Seitenleiste, um mit dem Lesen zu beginnen', loadingDocument: 'Dokument wird geladen...', errorLoadingDoc: 'Fehler beim Laden des Dokuments', tableOfContents: 'Inhaltsverzeichnis', downloadPdf: 'PDF herunterladen', sourceCode: 'Source Code', publicRepository: 'Öffentliches Repository', publicRepositoryDesc: 'Quellcode, Beispiele und Beiträge', readmeQuickStart: 'README & Schnellstart', readmeQuickStartDesc: 'Installation und Einstiegsanleitung', categories: { 'getting-started': 'Erste Schritte', 'resources': 'Ressourcen', 'research-theory': 'Forschung & Theorie', 'technical-reference': 'Technische Referenz', 'advanced-topics': 'Fortgeschrittene Themen', 'business-leadership': 'Business & Führung' }, filters: { allTypes: 'Alle Typen', allAudiences: 'Alle Zielgruppen', clear: 'Zurücksetzen', showingDocs: '{count} Dokumente', documentTypes: { 'working-paper': 'Arbeitspapier', 'case-study': 'Fallstudie', 'technical-report': 'Technischer Bericht', 'guide': 'Leitfaden', 'reference': 'Referenz', 'brief': 'Kurzfassung' }, audiences: { 'researcher': 'Forscher', 'implementer': 'Umsetzer', 'leader': 'Führungskraft', 'general': 'Allgemein' } } }, fr: { pageTitle: 'Documentation du Framework', pageSubtitle: 'Spécifications techniques, guides et matériels de référence', documentsHeading: 'Documents', searchButton: 'Rechercher', backToDocuments: 'Retour aux documents', selectDocument: 'Sélectionner un document', selectDocumentDesc: 'Choisissez un document dans la barre latérale pour commencer la lecture', loadingDocument: 'Chargement du document...', errorLoadingDoc: 'Erreur lors du chargement du document', tableOfContents: 'Table des matières', downloadPdf: 'Télécharger PDF', sourceCode: 'Source Code', publicRepository: 'Dépôt public', publicRepositoryDesc: 'Code source, exemples et contributions', readmeQuickStart: 'README & Démarrage rapide', readmeQuickStartDesc: 'Guide d\'installation et de démarrage', categories: { 'getting-started': 'Premiers pas', 'resources': 'Ressources', 'research-theory': 'Recherche & Théorie', 'technical-reference': 'Référence technique', 'advanced-topics': 'Sujets avancés', 'business-leadership': 'Business & Leadership' }, filters: { allTypes: 'Tous les types', allAudiences: 'Tous les publics', clear: 'Effacer', showingDocs: '{count} documents', documentTypes: { 'working-paper': 'Document de travail', 'case-study': 'Étude de cas', 'technical-report': 'Rapport technique', 'guide': 'Guide', 'reference': 'Référence', 'brief': 'Résumé' }, audiences: { 'researcher': 'Chercheur', 'implementer': 'Implémenteur', 'leader': 'Dirigeant', 'general': 'Général' } } }, mi: { pageTitle: 'Ngā Tuhinga Anga', pageSubtitle: 'Ngā tautukunga hangarau, ngā aratohu, me ngā rauemi tohutoro', documentsHeading: 'Ngā Tuhinga', searchButton: 'Rapu', backToDocuments: 'Hoki ki ngā Tuhinga', selectDocument: 'Kōwhiria he Tuhinga', selectDocumentDesc: 'Kōwhiria he tuhinga mai i te taha ki te tīmata pānui', loadingDocument: 'Kei te uta te tuhinga...', errorLoadingDoc: 'He hapa i te utanga o te tuhinga', tableOfContents: 'Rārangi Kaupapa', downloadPdf: 'Tikiake PDF', sourceCode: 'Source Code', publicRepository: 'Pūtahi Tūmatanui', publicRepositoryDesc: 'Waehere pūtake, tauira me ngā koha', readmeQuickStart: 'README & Tīmata Tere', readmeQuickStartDesc: 'Aratohu whakauru me te tīmata', categories: { 'getting-started': 'Tīmata', 'resources': 'Ngā Rauemi', 'research-theory': 'Rangahau & Ariā', 'technical-reference': 'Tohutoro Hangarau', 'advanced-topics': 'Kaupapa Matatau', 'business-leadership': 'Pakihi & Hautūtanga' }, filters: { allTypes: 'Ngā momo katoa', allAudiences: 'Ngā hunga katoa', clear: 'Ūkui', showingDocs: '{count} tuhinga', documentTypes: { 'working-paper': 'Pepa mahi', 'case-study': 'Rangahau āhua', 'technical-report': 'Pūrongo hangarau', 'guide': 'Aratohu', 'reference': 'Tohutoro', 'brief': 'Whakarāpopototanga' }, audiences: { 'researcher': 'Kairangahau', 'implementer': 'Kaiwhakatinana', 'leader': 'Kaihautū', 'general': 'Whānui' } } } }; // Get current UI translations function getUITranslations(lang = currentLanguage) { return UI_TRANSLATIONS[lang] || UI_TRANSLATIONS.en; } // Update all page UI elements with current language translations function updatePageUI(lang = currentLanguage) { const t = getUITranslations(lang); // Update page header const allPageTitles = Object.values(UI_TRANSLATIONS).map(t => t.pageTitle); const pageTitle = document.querySelector('h1'); if (pageTitle && allPageTitles.includes(pageTitle.textContent)) { pageTitle.textContent = t.pageTitle; } const allPageSubtitles = Object.values(UI_TRANSLATIONS).map(t => t.pageSubtitle); const pageSubtitle = document.querySelector('.text-gray-600.mt-2'); if (pageSubtitle && allPageSubtitles.includes(pageSubtitle.textContent)) { pageSubtitle.textContent = t.pageSubtitle; } // Update search button const searchBtn = document.querySelector('#open-search-modal-btn span'); if (searchBtn) { searchBtn.textContent = t.searchButton; } // Update sidebar Documents heading const allDocsHeadings = Object.values(UI_TRANSLATIONS).map(t => t.documentsHeading); const docsHeading = document.querySelector('aside h3'); if (docsHeading && allDocsHeadings.includes(docsHeading.textContent)) { docsHeading.textContent = t.documentsHeading; } // Update Source Code section heading const allSourceLabels = Object.values(UI_TRANSLATIONS).map(t => t.sourceCode); const sourceHeadings = document.querySelectorAll('aside h3'); sourceHeadings.forEach(heading => { if (allSourceLabels.includes(heading.textContent.trim())) { const textNode = Array.from(heading.childNodes).find(node => node.nodeType === Node.TEXT_NODE); if (textNode) { textNode.textContent = t.sourceCode; } } }); // Update Source Code links const allRepoTitles = Object.values(UI_TRANSLATIONS).map(t => t.sovereignRepo); const allReadmeTitles = Object.values(UI_TRANSLATIONS).map(t => t.implGuide); const allRepoDescs = Object.values(UI_TRANSLATIONS).map(t => t.sovereignRepoDesc); const allReadmeDescs = Object.values(UI_TRANSLATIONS).map(t => t.implGuideDesc); const sourceLinks = document.querySelectorAll('aside a[href*="source-code"]'); sourceLinks.forEach(link => { const titleDiv = link.querySelector('.text-sm.font-medium'); const descDiv = link.querySelector('.text-xs.text-gray-500'); if (titleDiv) { if (allRepoTitles.includes(titleDiv.textContent)) { titleDiv.textContent = t.sovereignRepo; } else if (allReadmeTitles.includes(titleDiv.textContent)) { titleDiv.textContent = t.implGuide; } } if (descDiv) { if (allRepoDescs.includes(descDiv.textContent)) { descDiv.textContent = t.sovereignRepoDesc; } else if (allReadmeDescs.includes(descDiv.textContent)) { descDiv.textContent = t.implGuideDesc; } } }); // Update Back to Documents bar label const backLabel = document.querySelector('#back-to-docs-btn [data-back-label]'); if (backLabel) { backLabel.textContent = t.documentsHeading; } // Update Select Document placeholder (if visible) const selectDocHeading = document.getElementById('select-document-heading'); if (selectDocHeading) { selectDocHeading.textContent = t.selectDocument; } const selectDocDesc = document.getElementById('select-document-desc'); if (selectDocDesc) { selectDocDesc.textContent = t.selectDocumentDesc; } // Update page title tag document.title = `${t.pageTitle} | Tractatus AI Safety`; // Update filter dropdown labels populateFilters(); updateFilterBar(); } // Initialize card-based viewer if (typeof DocumentCards !== 'undefined') { documentCards = new DocumentCards('document-content'); } // Detect language from URL, localStorage, or i18n system function detectLanguage() { // Priority 1: URL parameter const urlParams = new URLSearchParams(window.location.search); const urlLang = urlParams.get('lang'); if (urlLang) { return urlLang; } // Priority 2: localStorage const storedLang = localStorage.getItem('tractatus-lang'); if (storedLang) { return storedLang; } // Priority 3: i18n system if (window.I18n && window.I18n.currentLang) { return window.I18n.currentLang; } // Default: English return 'en'; } // Update URL with language parameter function updateURL(slug, lang) { const url = new URL(window.location); url.searchParams.set('doc', slug); if (lang && lang !== 'en') { url.searchParams.set('lang', lang); } else { url.searchParams.delete('lang'); } window.history.pushState({}, '', url); } // Listen for language changes from i18n system if (typeof window !== 'undefined') { window.addEventListener('languageChanged', async (e) => { const newLang = e.detail.language; currentLanguage = newLang; // Update page UI (hero section, sidebar headings, etc.) updatePageUI(newLang); // Remember current document slug before reloading list const currentSlug = currentDocument ? currentDocument.slug : null; // Update URL lang parameter BEFORE reloading documents // This ensures detectLanguage() reads the correct language from URL if (currentSlug) { updateURL(currentSlug, newLang); } // Reload document list to show translated category labels and document titles await loadDocuments(); // Explicitly reload current document to ensure it updates // (loadDocuments auto-loads from URL, but explicit call ensures it happens) if (currentSlug) { await loadDocument(currentSlug, newLang); } }); // Initialize language on i18n ready window.addEventListener('i18nInitialized', (e) => { currentLanguage = e.detail.language; }); } // Document categorization - Final 5 categories (curated for public docs) const CATEGORIES = { 'getting-started': { icon: '📚', description: 'Introduction, core concepts, and glossary', order: 1, color: 'blue', bgColor: 'bg-blue-50', borderColor: 'border-l-4 border-blue-500', textColor: 'text-blue-700', collapsed: false }, 'resources': { icon: '📖', description: 'Implementation guides and reference materials', order: 2, color: 'amber', bgColor: 'bg-amber-50', borderColor: 'border-l-4 border-amber-500', textColor: 'text-amber-700', collapsed: false }, 'research-theory': { icon: '🔬', description: 'Research papers, case studies, theoretical foundations', order: 3, color: 'purple', bgColor: 'bg-purple-50', borderColor: 'border-l-4 border-purple-500', textColor: 'text-purple-700', collapsed: true }, 'technical-reference': { icon: '🔌', description: 'API documentation, code examples, architecture', order: 4, color: 'green', bgColor: 'bg-green-50', borderColor: 'border-l-4 border-green-500', textColor: 'text-green-700', collapsed: true }, 'advanced-topics': { icon: '🎓', description: 'Value pluralism, organizational theory, advanced concepts', order: 5, color: 'teal', bgColor: 'bg-teal-50', borderColor: 'border-l-4 border-teal-500', textColor: 'text-teal-700', collapsed: true }, 'business-leadership': { icon: '💼', description: 'Business cases, ROI analysis, executive briefs', order: 6, color: 'pink', bgColor: 'bg-pink-50', borderColor: 'border-l-4 border-pink-500', textColor: 'text-pink-700', collapsed: true } }; // Documents to hide (internal/confidential) const HIDDEN_DOCS = [ 'security-audit-report', 'koha-production-deployment', 'koha-stripe-payment', 'appendix-e-contact', 'cover-letter' ]; // Categorize a document using database category field // New granular category system for better document organization function categorizeDocument(doc) { const slug = doc.slug.toLowerCase(); // Skip hidden documents if (HIDDEN_DOCS.some(hidden => slug.includes(hidden))) { return null; } // Use category from database const category = doc.category || 'resources'; // Validate category exists in CATEGORIES constant if (CATEGORIES[category]) { return category; } // Fallback to resources for uncategorized console.warn(`Document "${doc.title}" has invalid category "${category}", using fallback to resources`); return 'resources'; } // Group documents by category function groupDocuments(docs) { const grouped = {}; // Initialize all categories Object.keys(CATEGORIES).forEach(key => { grouped[key] = []; }); // Categorize each document (already sorted by order from API) docs.forEach(doc => { const category = categorizeDocument(doc); if (category && grouped[category]) { grouped[category].push(doc); } }); return grouped; } // Render document link with download button function renderDocLink(doc, isHighlighted = false) { const highlightClass = isHighlighted ? 'text-blue-700 bg-blue-50 border border-blue-200' : ''; // Get translated title if available and language is not English let displayTitle = doc.title; if (currentLanguage !== 'en' && doc.translations && doc.translations[currentLanguage]) { displayTitle = doc.translations[currentLanguage].title || doc.title; } // Determine if PDF download is available and get PDF path // First check if document has explicit download_formats.pdf let pdfPath = null; let hasPDF = false; if (doc.download_formats && doc.download_formats.pdf) { pdfPath = doc.download_formats.pdf; hasPDF = true; } // Add download button styling const paddingClass = hasPDF ? 'pr-10' : 'pr-3'; return `
${hasPDF ? ` ` : ''}
`; } // Populate filter dropdowns with translated labels from loaded documents function populateFilters() { const t = getUITranslations(currentLanguage); const f = t.filters || {}; // Document type dropdown const typeSelect = document.getElementById('filter-doc-type'); if (typeSelect) { const types = [...new Set(documents.map(d => d.document_type).filter(Boolean))].sort(); const currentVal = typeSelect.value; typeSelect.innerHTML = ``; types.forEach(type => { const label = (f.documentTypes && f.documentTypes[type]) || type; typeSelect.innerHTML += ``; }); typeSelect.value = currentVal || activeFilters.document_type; } // Audience dropdown const audSelect = document.getElementById('filter-doc-audience'); if (audSelect) { const audiences = [...new Set(documents.map(d => d.audience).filter(Boolean))].sort(); const currentVal = audSelect.value; audSelect.innerHTML = ``; audiences.forEach(aud => { const label = (f.audiences && f.audiences[aud]) || aud; audSelect.innerHTML += ``; }); audSelect.value = currentVal || activeFilters.audience; } // Clear button label const clearBtn = document.getElementById('clear-sidebar-filters'); if (clearBtn) { clearBtn.textContent = f.clear || 'Clear'; } } // Apply active filters to documents array function getFilteredDocuments() { return documents.filter(doc => { if (activeFilters.document_type && doc.document_type !== activeFilters.document_type) return false; if (activeFilters.audience && doc.audience !== activeFilters.audience) return false; return true; }); } // Update the active filters bar (count + clear button visibility) function updateFilterBar() { const bar = document.getElementById('active-filters-bar'); const countEl = document.getElementById('filter-result-count'); if (!bar || !countEl) return; const hasFilters = activeFilters.document_type || activeFilters.audience; if (hasFilters) { const filtered = getFilteredDocuments(); const t = getUITranslations(currentLanguage); const f = t.filters || {}; const template = f.showingDocs || '{count} documents'; countEl.textContent = template.replace('{count}', filtered.length); bar.classList.remove('hidden'); bar.classList.add('flex'); } else { bar.classList.add('hidden'); bar.classList.remove('flex'); } } // Re-render the document list with current filters applied function renderFilteredDocuments() { const filtered = getFilteredDocuments(); const grouped = groupDocuments(filtered); const listEl = document.getElementById('document-list'); if (!listEl) return; let html = ''; const sortedCategories = Object.entries(CATEGORIES).sort((a, b) => a[1].order - b[1].order); const t = getUITranslations(currentLanguage); sortedCategories.forEach(([categoryId, category]) => { const docs = grouped[categoryId] || []; if (docs.length === 0) return; const categoryLabel = t.categories[categoryId] || categoryId; html += `
`; docs.forEach(doc => { const isHighlighted = categoryId === 'getting-started' && doc.order === 1; html += renderDocLink(doc, isHighlighted); }); html += `
`; }); if (html === '') { html = '
No documents match the selected filters
'; } listEl.innerHTML = html; updateFilterBar(); // Re-highlight current document if loaded if (currentDocument) { document.querySelectorAll('.doc-link').forEach(el => { if (el.dataset.slug === currentDocument.slug) { el.classList.add('bg-blue-100', 'text-blue-900'); } }); } } // Load document list async function loadDocuments() { try { // Fetch public documents const response = await fetch('/api/documents'); const data = await response.json(); documents = data.documents || []; // Populate filter dropdowns with values from loaded documents populateFilters(); // Read filters from URL on initial load const urlFilters = new URLSearchParams(window.location.search); if (urlFilters.get('type') && !activeFilters.document_type) { activeFilters.document_type = urlFilters.get('type'); const typeSelect = document.getElementById('filter-doc-type'); if (typeSelect) typeSelect.value = activeFilters.document_type; } if (urlFilters.get('audience') && !activeFilters.audience) { activeFilters.audience = urlFilters.get('audience'); const audSelect = document.getElementById('filter-doc-audience'); if (audSelect) audSelect.value = activeFilters.audience; } // Apply filters const filteredDocs = getFilteredDocuments(); const listEl = document.getElementById('document-list'); if (filteredDocs.length === 0 && documents.length === 0) { listEl.innerHTML = '
No documents available
'; return; } // Group documents by category (filtered) const grouped = groupDocuments(filteredDocs); let html = ''; // Render categories in order const sortedCategories = Object.entries(CATEGORIES) .sort((a, b) => a[1].order - b[1].order); const t = getUITranslations(currentLanguage); // On mobile with no ?doc= param, expand all categories for browsing const isMobileView = window.innerWidth < 1024; const noDocParam = !new URLSearchParams(window.location.search).get('doc'); sortedCategories.forEach(([categoryId, category]) => { const docs = grouped[categoryId] || []; if (docs.length === 0) return; const isCollapsed = (isMobileView && noDocParam) ? false : (category.collapsed || false); const categoryLabel = t.categories[categoryId] || categoryId; // Category header html += `
`; // Render documents in category docs.forEach(doc => { // Highlight the first document in Getting Started category const isHighlighted = categoryId === 'getting-started' && doc.order === 1; html += renderDocLink(doc, isHighlighted); }); html += `
`; }); listEl.innerHTML = html; updateFilterBar(); // Apply collapsed state to categories (CSP-compliant - no inline styles) listEl.querySelectorAll('.category-docs[data-collapsed="true"]').forEach(docsEl => { docsEl.style.display = 'none'; }); listEl.querySelectorAll('.category-toggle[data-collapsed="true"] .category-arrow').forEach(arrowEl => { arrowEl.style.transform = 'rotate(-90deg)'; }); // Add event delegation for document links listEl.addEventListener('click', function(e) { // Check for download link first (prevent document load when clicking download) const downloadLink = e.target.closest('.doc-download-link'); if (downloadLink) { e.stopPropagation(); return; } const button = e.target.closest('.doc-link'); if (button && button.dataset.slug) { e.preventDefault(); loadDocument(button.dataset.slug); return; } // Category toggle const toggle = e.target.closest('.category-toggle'); if (toggle) { const categoryId = toggle.dataset.category; const docsEl = listEl.querySelector(`.category-docs[data-category="${categoryId}"]`); const arrowEl = toggle.querySelector('.category-arrow'); if (docsEl.style.display === 'none') { docsEl.style.display = 'block'; arrowEl.style.transform = 'rotate(0deg)'; } else { docsEl.style.display = 'none'; arrowEl.style.transform = 'rotate(-90deg)'; } } }); // Check for URL parameter to auto-load document or category const urlParams = new URLSearchParams(window.location.search); const docParam = urlParams.get('doc'); const categoryParam = urlParams.get('category'); // Priority 1: Load specific document by slug if provided if (docParam) { const doc = documents.find(d => d.slug === docParam); if (doc) { // Find and expand the category containing this document const docCategory = categorizeDocument(doc); if (docCategory) { const categoryDocsEl = listEl.querySelector(`.category-docs[data-category="${docCategory}"]`); const categoryArrowEl = listEl.querySelector(`.category-toggle[data-category="${docCategory}"] .category-arrow`); if (categoryDocsEl) { categoryDocsEl.style.display = 'block'; if (categoryArrowEl) { categoryArrowEl.style.transform = 'rotate(0deg)'; } } } // Load the requested document loadDocument(docParam); } else { console.warn(`Document with slug "${docParam}" not found`); } } // Priority 2: Load category if provided but no specific document else if (categoryParam && grouped[categoryParam] && grouped[categoryParam].length > 0) { // Expand the specified category const categoryDocsEl = listEl.querySelector(`.category-docs[data-category="${categoryParam}"]`); const categoryArrowEl = listEl.querySelector(`.category-toggle[data-category="${categoryParam}"] .category-arrow`); if (categoryDocsEl) { categoryDocsEl.style.display = 'block'; if (categoryArrowEl) { categoryArrowEl.style.transform = 'rotate(0deg)'; } } // Load first document in the category const firstDoc = grouped[categoryParam][0]; if (firstDoc) { loadDocument(firstDoc.slug); } } // Priority 3: Default behavior else { // On mobile, show sidebar (don't auto-load) so user can browse categories const isMobile = window.innerWidth < 1024; if (!isMobile) { // Desktop: auto-load first Getting Started doc as before const gettingStartedDocs = grouped['getting-started'] || []; if (gettingStartedDocs.length > 0) { const firstDoc = gettingStartedDocs.find(d => d.order === 1); if (firstDoc) { loadDocument(firstDoc.slug); } else { loadDocument(gettingStartedDocs[0].slug); } } else if (documents.length > 0) { const firstCategory = sortedCategories.find(([catId]) => grouped[catId] && grouped[catId].length > 0); if (firstCategory) { loadDocument(grouped[firstCategory[0]][0].slug); } } } // Mobile: sidebar remains visible, user taps to choose a document } } catch (error) { console.error('Error loading documents:', error); document.getElementById('document-list').innerHTML = '
Error loading documents
'; } } // Load specific document let isLoading = false; async function loadDocument(slug, lang = null) { // Prevent multiple simultaneous loads if (isLoading) return; // Use provided lang or detect from i18n system const language = lang || detectLanguage(); try { isLoading = true; const t = getUITranslations(language); // Show loading state const contentEl = document.getElementById('document-content'); contentEl.innerHTML = `

${t.loadingDocument}

`; // Build API URL with language parameter const apiUrl = language && language !== 'en' ? `/api/documents/${slug}?lang=${language}` : `/api/documents/${slug}`; const response = await fetch(apiUrl); const data = await response.json(); if (!data.success) { // If translation not available, fall back to English if (response.status === 404 && language !== 'en') { console.warn(`Translation not available for ${language}, falling back to English`); const enResponse = await fetch(`/api/documents/${slug}`); const enData = await enResponse.json(); if (enData.success) { // Translation not available - falling back to English // (console warning already logged above) const fallbackData = enData; fallbackData.document.language = 'en'; fallbackData.document.fallback = true; // Use English version currentDocument = fallbackData.document; currentLanguage = 'en'; updateURL(slug, 'en'); // Continue with rendering data.success = true; data.document = fallbackData.document; } else { throw new Error(data.error || 'Failed to load document'); } } else { throw new Error(data.error || 'Failed to load document'); } } else { currentDocument = data.document; currentLanguage = language; updateURL(slug, language); } // Update active state document.querySelectorAll('.doc-link').forEach(el => { if (el.dataset.slug === slug) { el.classList.add('bg-blue-100', 'text-blue-900'); } else { el.classList.remove('bg-blue-100', 'text-blue-900'); } }); // Render with card-based viewer if available and document has sections if (documentCards && currentDocument.sections && currentDocument.sections.length > 0) { documentCards.render(currentDocument); } else { // Fallback to traditional view with header const hasToC = currentDocument.toc && currentDocument.toc.length > 0; // Check if PDF is available and get PDF path let pdfPath = null; let hasPDF = false; if (currentDocument.download_formats && currentDocument.download_formats.pdf) { pdfPath = currentDocument.download_formats.pdf; hasPDF = true; } let headerHTML = `

${currentDocument.title}

${hasToC ? ` ` : ''} ${hasPDF ? ` ` : ''}
`; // Remove duplicate title H1 from content (it's already in header) let contentHtml = currentDocument.content_html; const firstH1Match = contentHtml.match(/]*>.*?<\/h1>/); if (firstH1Match) { contentHtml = contentHtml.replace(firstH1Match[0], ''); } contentEl.innerHTML = headerHTML + `
${contentHtml}
`; } // Add ToC button event listener (works for both card and traditional views) setTimeout(() => { const tocButton = document.getElementById('toc-button'); if (tocButton) { tocButton.addEventListener('click', () => openToCModal()); } }, 100); // Mobile navigation: Add document-active class to show document view document.body.classList.add('document-active'); // Update mobile sticky bar with current document title const mobileDocTitle = document.getElementById('mobile-doc-title'); if (mobileDocTitle && currentDocument) { mobileDocTitle.textContent = currentDocument.title; } // Scroll to top window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (error) { console.error('Error loading document:', error); const t = getUITranslations(currentLanguage); document.getElementById('document-content').innerHTML = `

${t.errorLoadingDoc}

${error.message}

`; } finally { isLoading = false; } } // Open ToC modal function openToCModal() { if (!currentDocument || !currentDocument.toc || currentDocument.toc.length === 0) { return; } const modal = document.getElementById('toc-modal'); if (!modal) return; // Render ToC content const tocContent = document.getElementById('toc-modal-content'); const tocHTML = currentDocument.toc .filter(item => item.level <= 3) // Only show H1, H2, H3 .map(item => { return ` ${item.title} `; }).join(''); tocContent.innerHTML = tocHTML; // Show modal modal.classList.add('show'); // Prevent body scroll and reset modal content scroll document.body.style.overflow = 'hidden'; tocContent.scrollTop = 0; // Add event listeners to ToC links tocContent.querySelectorAll('.toc-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetId = link.getAttribute('href').substring(1); const targetEl = document.getElementById(targetId); if (targetEl) { closeToCModal(); setTimeout(() => { targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 200); } }); }); } // Close ToC modal function closeToCModal() { const modal = document.getElementById('toc-modal'); if (modal) { modal.classList.remove('show'); document.body.style.overflow = ''; } } // Initialize loadDocuments(); // Add ESC key listener for closing modal document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeToCModal(); } }); // Add close button listener for ToC modal (script loads after DOM, so elements exist) const closeButton = document.getElementById('toc-close-button'); if (closeButton) { closeButton.addEventListener('click', closeToCModal); } // Click outside modal to close const modal = document.getElementById('toc-modal'); if (modal) { modal.addEventListener('click', function(e) { if (e.target === this) { closeToCModal(); } }); } // Mobile navigation: Back to documents button const backButton = document.getElementById('back-to-docs-btn'); if (backButton) { backButton.addEventListener('click', function() { // Remove document-active class to show sidebar document.body.classList.remove('document-active'); // Scroll to top window.scrollTo({ top: 0, behavior: 'smooth' }); }); } // Sidebar filter event listeners const filterTypeSelect = document.getElementById('filter-doc-type'); const filterAudienceSelect = document.getElementById('filter-doc-audience'); const clearSidebarFilters = document.getElementById('clear-sidebar-filters'); function onFilterChange() { if (filterTypeSelect) activeFilters.document_type = filterTypeSelect.value; if (filterAudienceSelect) activeFilters.audience = filterAudienceSelect.value; // Update URL with filter params const url = new URL(window.location); if (activeFilters.document_type) { url.searchParams.set('type', activeFilters.document_type); } else { url.searchParams.delete('type'); } if (activeFilters.audience) { url.searchParams.set('audience', activeFilters.audience); } else { url.searchParams.delete('audience'); } window.history.replaceState({}, '', url); renderFilteredDocuments(); } if (filterTypeSelect) filterTypeSelect.addEventListener('change', onFilterChange); if (filterAudienceSelect) filterAudienceSelect.addEventListener('change', onFilterChange); if (clearSidebarFilters) { clearSidebarFilters.addEventListener('click', function() { activeFilters.document_type = ''; activeFilters.audience = ''; if (filterTypeSelect) filterTypeSelect.value = ''; if (filterAudienceSelect) filterAudienceSelect.value = ''; // Clear URL filter params const url = new URL(window.location); url.searchParams.delete('type'); url.searchParams.delete('audience'); window.history.replaceState({}, '', url); renderFilteredDocuments(); }); }