{% extends "base.html" %} {% block title %}{{ post.title }} - {{ config.title }}{% endblock %} {% block content %} <div class="article-container"> <article class="article-content"> <header class="article-header"> <h1 class="article-title">{{ post.title }}</h1> <div class="article-meta"> <time class="article-date">{{ post.date }}</time> {% if post.language %} <span class="article-lang">{{ post.language }}</span> {% endif %} </div> <div class="article-actions"> {% if post.markdown_url %} <a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown"> .md </a> {% endif %} {% if post.translation_url %} <a href="{{ post.translation_url }}" class="action-btn translation-btn" title="View Translation"> 🌐 {% if post.language == 'ja' %}English{% else %}日本語{% endif %} </a> {% endif %} </div> </header> <div class="article-body"> {{ post.content | safe }} </div> <!-- Comment Section --> <section class="comment-section"> <div class="comment-container"> <h3>Comments</h3> <!-- ATProto Auth Widget Container --> <div id="atproto-auth-widget" class="comment-auth"></div> <div id="commentForm" class="comment-form" style="display: none;"> <textarea id="commentText" placeholder="Share your thoughts..." rows="4"></textarea> <button onclick="submitComment()" class="submit-btn">Post Comment</button> </div> <div id="commentsList" class="comments-list"> <!-- Comments will be loaded here --> </div> </div> </section> </article> <aside class="article-sidebar"> <nav class="toc"> <h3>Contents</h3> <div id="toc-content"> <!-- TOC will be generated by JavaScript --> </div> </nav> </aside> </div> {% endblock %} {% block sidebar %} <!-- Include ATProto Libraries via script tags (more reliable than dynamic imports) --> <script src="https://cdn.jsdelivr.net/npm/@atproto/oauth-client-browser@latest/dist/index.js"></script> <script src="https://cdn.jsdelivr.net/npm/@atproto/api@latest/dist/index.js"></script> <!-- Fallback: Try multiple CDNs --> <script> console.log('Checking ATProto library availability...'); // Check if libraries loaded successfully if (typeof ATProto === 'undefined' && typeof window.ATProto === 'undefined') { console.log('Primary CDN failed, trying fallback...'); // Create fallback script elements const fallbackScripts = [ 'https://unpkg.com/@atproto/oauth-client-browser@latest/dist/index.js', 'https://esm.sh/@atproto/oauth-client-browser', 'https://cdn.skypack.dev/@atproto/oauth-client-browser' ]; // Load fallback scripts sequentially let scriptIndex = 0; function loadNextScript() { if (scriptIndex < fallbackScripts.length) { const script = document.createElement('script'); script.src = fallbackScripts[scriptIndex]; script.onload = () => { console.log(`Loaded from fallback CDN: ${fallbackScripts[scriptIndex]}`); window.atprotoLibrariesReady = true; }; script.onerror = () => { console.log(`Failed to load from: ${fallbackScripts[scriptIndex]}`); scriptIndex++; loadNextScript(); }; document.head.appendChild(script); } else { console.error('All CDN fallbacks failed'); window.atprotoLibrariesReady = false; } } loadNextScript(); } else { console.log('✅ ATProto libraries loaded from primary CDN'); window.atprotoLibrariesReady = true; } </script> <!-- Simple ATProto Widget (no external dependency) --> <link rel="stylesheet" href="/atproto-auth-widget/dist/atproto-auth.min.css"> <script> // Initialize auth widget let authWidget = null; document.addEventListener('DOMContentLoaded', function() { generateTableOfContents(); initializeAuthWidget(); loadComments(); }); function generateTableOfContents() { const tocContainer = document.getElementById('toc-content'); const headings = document.querySelectorAll('.article-body h1, .article-body h2, .article-body h3, .article-body h4, .article-body h5, .article-body h6'); if (headings.length === 0) { tocContainer.innerHTML = '<p class="no-toc">No headings found</p>'; return; } const tocList = document.createElement('ul'); tocList.className = 'toc-list'; headings.forEach((heading, index) => { const id = `heading-${index}`; heading.id = id; const listItem = document.createElement('li'); listItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`; const link = document.createElement('a'); link.href = `#${id}`; link.textContent = heading.textContent; link.className = 'toc-link'; // Smooth scroll behavior link.addEventListener('click', function(e) { e.preventDefault(); heading.scrollIntoView({ behavior: 'smooth' }); }); listItem.appendChild(link); tocList.appendChild(listItem); }); tocContainer.appendChild(tocList); } // Initialize ATProto Auth Widget async function initializeAuthWidget() { try { // Check WebCrypto API availability console.log('WebCrypto check:', { available: !!window.crypto && !!window.crypto.subtle, secureContext: window.isSecureContext, protocol: window.location.protocol, hostname: window.location.hostname }); if (!window.crypto || !window.crypto.subtle) { throw new Error('WebCrypto API is not available. This requires HTTPS or localhost.'); } if (!window.isSecureContext) { console.warn('Not in secure context - WebCrypto may not work properly'); } // Simplified approach: Show manual OAuth form console.log('Using simplified OAuth approach...'); showSimpleOAuthForm(); // Fallback to widget initialization authWidget = await window.initATProtoWidget('#atproto-auth-widget', { clientId: clientId, onLogin: (session) => { console.log('User logged in:', session.handle); document.getElementById('commentForm').style.display = 'block'; }, onLogout: () => { console.log('User logged out'); document.getElementById('commentForm').style.display = 'none'; }, onError: (error) => { console.error('ATProto Auth Error:', error); // Show user-friendly error message const authContainer = document.getElementById('atproto-auth-widget'); if (authContainer) { let errorMessage = 'Authentication service is temporarily unavailable.'; let suggestion = 'Please try refreshing the page.'; if (error.message && error.message.includes('WebCrypto')) { errorMessage = 'This feature requires a secure HTTPS connection.'; suggestion = 'Please ensure you are accessing via https://log.syui.ai'; } authContainer.innerHTML = ` <div class="atproto-auth__fallback"> <p>${errorMessage}</p> <p>${suggestion}</p> <details style="margin-top: 10px; font-size: 0.8em; color: #666;"> <summary>Technical details</summary> <pre>${error.message || 'Unknown error'}</pre> </details> </div> `; } }, theme: 'default' }); } else if (typeof window.ATProtoAuthWidget === 'function') { // Fallback to direct widget initialization authWidget = new window.ATProtoAuthWidget({ containerSelector: '#atproto-auth-widget', clientId: clientId, onLogin: (session) => { console.log('User logged in:', session.handle); document.getElementById('commentForm').style.display = 'block'; }, onLogout: () => { console.log('User logged out'); document.getElementById('commentForm').style.display = 'none'; }, onError: (error) => { console.error('ATProto Auth Error:', error); const authContainer = document.getElementById('atproto-auth-widget'); if (authContainer) { authContainer.innerHTML = ` <div class="atproto-auth__fallback"> <p>Authentication service is temporarily unavailable.</p> <p>Please try refreshing the page.</p> </div> `; } }, theme: 'default' }); await authWidget.init(); } else { throw new Error('ATProto widget not available'); } } catch (error) { console.error('Failed to initialize auth widget:', error); // Show fallback UI const authContainer = document.getElementById('atproto-auth-widget'); if (authContainer) { authContainer.innerHTML = ` <div class="atproto-auth__fallback"> <p>Authentication widget failed to load.</p> <p>Please check your internet connection and refresh the page.</p> </div> `; } } } async function submitComment() { const commentText = document.getElementById('commentText').value.trim(); if (!commentText || !authWidget.isLoggedIn()) { alert('Please login and enter a comment'); return; } try { const postSlug = '{{ post.slug }}'; const postUrl = window.location.href; const createdAt = new Date().toISOString(); // Create comment record using the auth widget const response = await authWidget.createRecord('ai.log.comment', { $type: 'ai.log.comment', text: commentText, post_slug: postSlug, post_url: postUrl, createdAt: createdAt }); console.log('Comment posted:', response); document.getElementById('commentText').value = ''; loadComments(); } catch (error) { console.error('Comment submission failed:', error); alert('Failed to post comment: ' + error.message); } } function showAuthenticatedState(session) { const authContainer = document.getElementById('atproto-auth-widget'); const agent = new window.ATProtoAgent(session); authContainer.innerHTML = ` <div class="atproto-auth__authenticated"> <p>✅ Authenticated as: <strong>${session.did}</strong></p> <button id="logout-btn" class="atproto-auth__button">Logout</button> </div> `; document.getElementById('logout-btn').onclick = async () => { await session.signOut(); window.location.reload(); }; // Show comment form document.getElementById('commentForm').style.display = 'block'; window.currentSession = session; window.currentAgent = agent; } function showLoginForm(oauthClient) { const authContainer = document.getElementById('atproto-auth-widget'); authContainer.innerHTML = ` <div class="atproto-auth__login"> <h4>Login with ATProto</h4> <input type="text" id="handle-input" placeholder="user.bsky.social" /> <button id="login-btn" class="atproto-auth__button">Connect</button> </div> `; document.getElementById('login-btn').onclick = async () => { const handle = document.getElementById('handle-input').value.trim(); if (!handle) { alert('Please enter your handle'); return; } try { const url = await oauthClient.authorize(handle); window.open(url, '_self', 'noopener'); } catch (error) { console.error('OAuth authorization failed:', error); alert('Authentication failed: ' + error.message); } }; // Enter key support document.getElementById('handle-input').onkeypress = (e) => { if (e.key === 'Enter') { document.getElementById('login-btn').click(); } }; } async function loadComments() { try { const commentsList = document.getElementById('commentsList'); commentsList.innerHTML = '<p class="loading">Loading comments from ATProto network...</p>'; // In a real implementation, you would query an aggregation service // For demo, show empty state setTimeout(() => { commentsList.innerHTML = '<p class="no-comments">Comments will appear here when posted via ATProto.</p>'; }, 1000); } catch (error) { console.error('Failed to load comments:', error); document.getElementById('commentsList').innerHTML = '<p class="error">Failed to load comments</p>'; } } </script> {% endblock %}