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 %} |