let documents = []; let currentDocument = null; let documentCards = null; let currentLanguage = 'en'; // Default language // 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', github: 'GitHub', publicRepository: 'Public Repository', publicRepositoryDesc: 'Source code, examples & contributions', readmeQuickStart: 'README & Quick Start', readmeQuickStartDesc: 'Installation and getting started guide', categories: { 'getting-started': 'Getting Started', 'resources': 'Resources', 'research-theory': 'Research & Theory', 'technical-reference': 'Technical Reference', 'advanced-topics': 'Advanced Topics', 'business-leadership': 'Business & Leadership' } }, 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', github: 'GitHub', 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' } }, 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', github: 'GitHub', 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' } } }; // 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 pageTitle = document.querySelector('h1'); if (pageTitle && pageTitle.textContent === UI_TRANSLATIONS.en.pageTitle || pageTitle.textContent === UI_TRANSLATIONS.de.pageTitle || pageTitle.textContent === UI_TRANSLATIONS.fr.pageTitle) { pageTitle.textContent = t.pageTitle; } const pageSubtitle = document.querySelector('.text-gray-600.mt-2'); if (pageSubtitle && (pageSubtitle.textContent === UI_TRANSLATIONS.en.pageSubtitle || pageSubtitle.textContent === UI_TRANSLATIONS.de.pageSubtitle || pageSubtitle.textContent === UI_TRANSLATIONS.fr.pageSubtitle)) { 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 docsHeading = document.querySelector('aside h3'); if (docsHeading && (docsHeading.textContent === UI_TRANSLATIONS.en.documentsHeading || docsHeading.textContent === UI_TRANSLATIONS.de.documentsHeading || docsHeading.textContent === UI_TRANSLATIONS.fr.documentsHeading)) { docsHeading.textContent = t.documentsHeading; } // Update GitHub section heading const githubHeadings = document.querySelectorAll('aside h3'); githubHeadings.forEach(heading => { if (heading.textContent.trim() === UI_TRANSLATIONS.en.github || heading.textContent.trim() === UI_TRANSLATIONS.de.github || heading.textContent.trim() === UI_TRANSLATIONS.fr.github) { // Keep the SVG icon, just update text const textNode = Array.from(heading.childNodes).find(node => node.nodeType === Node.TEXT_NODE); if (textNode) { textNode.textContent = t.github; } } }); // Update GitHub links const githubLinks = document.querySelectorAll('aside a[href*="github.com"]'); githubLinks.forEach(link => { const titleDiv = link.querySelector('.text-sm.font-medium'); const descDiv = link.querySelector('.text-xs.text-gray-500'); if (titleDiv) { if (titleDiv.textContent === UI_TRANSLATIONS.en.publicRepository || titleDiv.textContent === UI_TRANSLATIONS.de.publicRepository || titleDiv.textContent === UI_TRANSLATIONS.fr.publicRepository) { titleDiv.textContent = t.publicRepository; } else if (titleDiv.textContent === UI_TRANSLATIONS.en.readmeQuickStart || titleDiv.textContent === UI_TRANSLATIONS.de.readmeQuickStart || titleDiv.textContent === UI_TRANSLATIONS.fr.readmeQuickStart) { titleDiv.textContent = t.readmeQuickStart; } } if (descDiv) { if (descDiv.textContent === UI_TRANSLATIONS.en.publicRepositoryDesc || descDiv.textContent === UI_TRANSLATIONS.de.publicRepositoryDesc || descDiv.textContent === UI_TRANSLATIONS.fr.publicRepositoryDesc) { descDiv.textContent = t.publicRepositoryDesc; } else if (descDiv.textContent === UI_TRANSLATIONS.en.readmeQuickStartDesc || descDiv.textContent === UI_TRANSLATIONS.de.readmeQuickStartDesc || descDiv.textContent === UI_TRANSLATIONS.fr.readmeQuickStartDesc) { descDiv.textContent = t.readmeQuickStartDesc; } } }); // Update Back to Documents button const backBtn = document.querySelector('#back-to-docs-btn span'); if (backBtn) { backBtn.textContent = t.backToDocuments; } // 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`; } // 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_language'); 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', (e) => { const newLang = e.detail.language; currentLanguage = newLang; // Reload current document in new language if (currentDocument) { loadDocument(currentDocument.slug, 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 || 'downloads-resources'; // Validate category exists in CATEGORIES constant if (CATEGORIES[category]) { return category; } // Fallback to downloads-resources for uncategorized console.warn(`Document "${doc.title}" has invalid category "${category}", using fallback`); return 'downloads-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; } else if (!doc.slug.includes('api-reference-complete') && !doc.slug.includes('openapi-specification') && !doc.slug.includes('api-javascript-examples') && !doc.slug.includes('api-python-examples') && !doc.slug.includes('technical-architecture-diagram')) { // Fallback to default /downloads/ path for documents that typically have PDFs pdfPath = `/downloads/${doc.slug}.pdf`; hasPDF = true; } // Add download button styling const paddingClass = hasPDF ? 'pr-10' : 'pr-3'; return `
${hasPDF ? ` ` : ''}
`; } // Load document list async function loadDocuments() { try { // Fetch public documents const response = await fetch('/api/documents'); const data = await response.json(); documents = data.documents || []; const listEl = document.getElementById('document-list'); if (documents.length === 0) { listEl.innerHTML = '
No documents available
'; return; } // Group documents by category const grouped = groupDocuments(documents); let html = ''; // Render categories in order 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 isCollapsed = 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; // 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 { // Default: Auto-load first document in "Getting Started" category (order: 1) const gettingStartedDocs = grouped['getting-started'] || []; if (gettingStartedDocs.length > 0) { // Load the first document (order: 1) if available const firstDoc = gettingStartedDocs.find(d => d.order === 1); if (firstDoc) { loadDocument(firstDoc.slug); } else { loadDocument(gettingStartedDocs[0].slug); } } else if (documents.length > 0) { // Fallback to first available document in any category const firstCategory = sortedCategories.find(([catId]) => grouped[catId] && grouped[catId].length > 0); if (firstCategory) { loadDocument(grouped[firstCategory[0]][0].slug); } } } } 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) { // Show notification that translation isn't available showTranslationFallbackNotice(language); 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; } else if (!currentDocument.slug.includes('api-reference-complete') && !currentDocument.slug.includes('openapi-specification') && !currentDocument.slug.includes('api-javascript-examples') && !currentDocument.slug.includes('api-python-examples') && !currentDocument.slug.includes('technical-architecture-diagram')) { pdfPath = `/downloads/${currentDocument.slug}.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'); // 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(); // Initialize language selector and detect initial language (function initLanguageSelector() { const selector = document.getElementById('language-selector'); if (!selector) return; // Set initial value from current language const initialLang = detectLanguage(); selector.value = initialLang; currentLanguage = initialLang; // Update page UI with initial language updatePageUI(initialLang); // Handle language change selector.addEventListener('change', async (e) => { const newLang = e.target.value; // Save to localStorage localStorage.setItem('tractatus_language', newLang); // Update current language currentLanguage = newLang; // Update all page UI elements updatePageUI(newLang); // Reload document list to show translated category labels and document titles const currentSlug = currentDocument ? currentDocument.slug : null; await loadDocuments(); // If a document was loaded, reload it in the new language if (currentSlug) { loadDocument(currentSlug, newLang); } }); })(); // 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' }); }); }