373 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			373 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| {% 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 %} |