360 lines
15 KiB
HTML
360 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="{{ config.language }}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}{{ config.title }}{% endblock %}</title>
|
|
<link rel="stylesheet" href="/css/style.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
{% include "oauth-assets.html" %}
|
|
{% block head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header class="main-header">
|
|
<div class="header-content">
|
|
<h1><a href="/" class="site-title">{{ config.title }}</a></h1>
|
|
<div class="header-actions">
|
|
<!-- Ask AI button on all pages -->
|
|
<button class="ask-ai-btn" onclick="toggleAskAI()" id="askAiButton">
|
|
<span class="ai-icon">🤖</span>
|
|
Ask AI
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Ask AI panel on all pages -->
|
|
<div class="ask-ai-panel" id="askAiPanel" style="display: none;">
|
|
<div class="ask-ai-content">
|
|
<!-- Authentication check -->
|
|
<div id="authCheck" class="auth-check">
|
|
<p>🔒 Please login with ATProto to use Ask AI feature</p>
|
|
</div>
|
|
|
|
<!-- Chat form (hidden until authenticated) -->
|
|
<div id="chatForm" class="ask-ai-form" style="display: none;">
|
|
<input type="text" id="aiQuestion" placeholder="What would you like to know?" />
|
|
<button onclick="askQuestion()" id="askButton">Ask</button>
|
|
</div>
|
|
|
|
<!-- Chat history -->
|
|
<div id="chatHistory" class="chat-history" style="display: none;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<main class="main-content">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
{% block sidebar %}{% endblock %}
|
|
</div>
|
|
|
|
<footer class="main-footer">
|
|
<p>© {{ config.author }}</p>
|
|
</footer>
|
|
|
|
<script>
|
|
function toggleAskAI() {
|
|
const panel = document.getElementById('askAiPanel');
|
|
const isVisible = panel.style.display !== 'none';
|
|
panel.style.display = isVisible ? 'none' : 'block';
|
|
|
|
if (!isVisible) {
|
|
checkAuthenticationStatus();
|
|
}
|
|
}
|
|
|
|
function checkAuthenticationStatus() {
|
|
const userSections = document.querySelectorAll('.user-section');
|
|
const isAuthenticated = userSections.length > 0;
|
|
|
|
if (isAuthenticated) {
|
|
// User is authenticated - show Ask AI UI
|
|
document.getElementById('authCheck').style.display = 'none';
|
|
document.getElementById('chatForm').style.display = 'block';
|
|
document.getElementById('chatHistory').style.display = 'block';
|
|
|
|
// Show initial greeting if chat history is empty
|
|
const chatHistory = document.getElementById('chatHistory');
|
|
if (chatHistory.children.length === 0) {
|
|
showInitialGreeting();
|
|
}
|
|
|
|
// Focus after a small delay to ensure element is visible
|
|
setTimeout(() => {
|
|
document.getElementById('aiQuestion').focus();
|
|
}, 50);
|
|
} else {
|
|
// User is not authenticated - show login message only
|
|
document.getElementById('authCheck').style.display = 'block';
|
|
document.getElementById('chatForm').style.display = 'none';
|
|
document.getElementById('chatHistory').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
let isAIChatReady = false;
|
|
let aiProfileData = null;
|
|
|
|
// Listen for AI ready signal
|
|
window.addEventListener('aiChatReady', function() {
|
|
isAIChatReady = true;
|
|
console.log('AI Chat is ready');
|
|
});
|
|
|
|
|
|
// Listen for AI profile updates from OAuth app
|
|
window.addEventListener('aiProfileLoaded', function(event) {
|
|
aiProfileData = event.detail;
|
|
console.log('AI profile loaded:', aiProfileData);
|
|
updateAskAIButton();
|
|
});
|
|
|
|
function updateAskAIButton() {
|
|
const button = document.getElementById('askAiButton');
|
|
const iconSpan = button.querySelector('.ai-icon');
|
|
|
|
if (aiProfileData && aiProfileData.avatar) {
|
|
iconSpan.innerHTML = `<img src="${aiProfileData.avatar}" alt="${aiProfileData.displayName || 'AI'}" class="ai-avatar-small">`;
|
|
}
|
|
|
|
if (aiProfileData && aiProfileData.displayName) {
|
|
button.childNodes[2].textContent = `Ask ${aiProfileData.displayName}`;
|
|
}
|
|
}
|
|
|
|
function showInitialGreeting() {
|
|
const chatHistory = document.getElementById('chatHistory');
|
|
const greetingDiv = document.createElement('div');
|
|
greetingDiv.className = 'chat-message ai-message comment-style initial-greeting';
|
|
|
|
if (!aiProfileData) {
|
|
return; // Don't show greeting if no AI profile data
|
|
}
|
|
|
|
let avatarElement = '🤖';
|
|
if (aiProfileData.avatar) {
|
|
avatarElement = `<img src="${aiProfileData.avatar}" alt="${aiProfileData.displayName}" class="profile-avatar">`;
|
|
}
|
|
|
|
const displayName = aiProfileData.displayName;
|
|
const handle = aiProfileData.handle;
|
|
|
|
greetingDiv.innerHTML = `
|
|
<div class="message-header">
|
|
<div class="avatar">${avatarElement}</div>
|
|
<div class="user-info">
|
|
<div class="display-name">${displayName}</div>
|
|
<div class="handle">@${handle}</div>
|
|
<div class="timestamp">${new Date().toLocaleString()}</div>
|
|
</div>
|
|
</div>
|
|
<div class="message-content">
|
|
Hello! I'm an AI assistant trained on this blog's content. I can answer questions about the articles, provide insights, and help you understand the topics discussed here. What would you like to know?
|
|
</div>
|
|
`;
|
|
chatHistory.appendChild(greetingDiv);
|
|
}
|
|
|
|
async function askQuestion() {
|
|
const question = document.getElementById('aiQuestion').value;
|
|
const chatHistory = document.getElementById('chatHistory');
|
|
const askButton = document.getElementById('askButton');
|
|
|
|
if (!question.trim()) return;
|
|
|
|
// Wait for AI to be ready
|
|
if (!isAIChatReady) {
|
|
console.log('Waiting for AI Chat to be ready...');
|
|
await new Promise(resolve => {
|
|
const checkReady = setInterval(() => {
|
|
if (isAIChatReady) {
|
|
clearInterval(checkReady);
|
|
resolve();
|
|
}
|
|
}, 100);
|
|
});
|
|
}
|
|
|
|
// Disable button and show loading
|
|
askButton.disabled = true;
|
|
askButton.textContent = 'Posting...';
|
|
|
|
// Get user info from OAuth component
|
|
const userSection = document.querySelector('.user-section');
|
|
let userAvatar = '👤';
|
|
let userDisplay = 'You';
|
|
let userHandle = 'user';
|
|
|
|
if (userSection) {
|
|
const avatarImg = userSection.querySelector('.user-avatar');
|
|
const displayName = userSection.querySelector('.user-display-name');
|
|
const handle = userSection.querySelector('.user-handle');
|
|
|
|
if (avatarImg && avatarImg.src) {
|
|
userAvatar = `<img src="${avatarImg.src}" alt="${displayName?.textContent || 'User'}" class="profile-avatar">`;
|
|
}
|
|
if (displayName?.textContent) {
|
|
userDisplay = displayName.textContent;
|
|
}
|
|
if (handle?.textContent) {
|
|
userHandle = handle.textContent.replace('@', '');
|
|
}
|
|
}
|
|
|
|
// Add question to chat history in comment style
|
|
const questionDiv = document.createElement('div');
|
|
questionDiv.className = 'chat-message user-message comment-style';
|
|
questionDiv.innerHTML = `
|
|
<div class="message-header">
|
|
<div class="avatar">${userAvatar}</div>
|
|
<div class="user-info">
|
|
<div class="display-name">${userDisplay}</div>
|
|
<div class="handle">@${userHandle}</div>
|
|
<div class="timestamp">${new Date().toLocaleString()}</div>
|
|
</div>
|
|
</div>
|
|
<div class="message-content">${question}</div>
|
|
`;
|
|
chatHistory.appendChild(questionDiv);
|
|
|
|
// Clear input
|
|
document.getElementById('aiQuestion').value = '';
|
|
|
|
try {
|
|
// Show loading immediately
|
|
const loadingDiv = document.createElement('div');
|
|
loadingDiv.className = 'ai-loading-simple';
|
|
loadingDiv.innerHTML = `
|
|
<i class="fas fa-robot"></i>
|
|
<span>考えています</span>
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
`;
|
|
chatHistory.appendChild(loadingDiv);
|
|
|
|
// Post question to ATProto via OAuth app
|
|
const event = new CustomEvent('postAIQuestion', {
|
|
detail: { question: question }
|
|
});
|
|
window.dispatchEvent(event);
|
|
|
|
} catch (error) {
|
|
// Remove loading indicator and show error
|
|
const loadingMsg = chatHistory.querySelector('.ai-loading-simple');
|
|
if (loadingMsg) {
|
|
loadingMsg.remove();
|
|
}
|
|
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'chat-message error-message comment-style';
|
|
errorDiv.innerHTML = `
|
|
<div class="message-header">
|
|
<div class="avatar">⚠️</div>
|
|
<div class="user-info">
|
|
<div class="display-name">System</div>
|
|
<div class="handle">@system</div>
|
|
<div class="timestamp">${new Date().toLocaleString()}</div>
|
|
</div>
|
|
</div>
|
|
<div class="message-content">Sorry, I encountered an error. Please try again.</div>
|
|
`;
|
|
chatHistory.appendChild(errorDiv);
|
|
} finally {
|
|
askButton.disabled = false;
|
|
askButton.textContent = 'Ask';
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
document.getElementById('askAiPanel').style.display = 'none';
|
|
}
|
|
|
|
// Enter key to send message
|
|
if (e.key === 'Enter' && e.target.id === 'aiQuestion' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
askQuestion();
|
|
}
|
|
});
|
|
|
|
// Monitor authentication state changes
|
|
const authObserver = new MutationObserver(function(mutations) {
|
|
const userSections = document.querySelectorAll('.user-section');
|
|
if (userSections.length > 0) {
|
|
checkAuthenticationStatus();
|
|
// Stop observing once authenticated
|
|
authObserver.disconnect();
|
|
}
|
|
});
|
|
|
|
// Start observing for authentication changes
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initial authentication check with slight delay for OAuth component
|
|
setTimeout(() => {
|
|
checkAuthenticationStatus();
|
|
}, 500);
|
|
|
|
authObserver.observe(document.body, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
});
|
|
|
|
// Listen for AI responses from OAuth app
|
|
window.addEventListener('aiResponseReceived', function(event) {
|
|
const chatHistory = document.getElementById('chatHistory');
|
|
const loadingMsg = chatHistory.querySelector('.ai-loading-simple');
|
|
|
|
if (loadingMsg) {
|
|
loadingMsg.remove();
|
|
}
|
|
|
|
const aiProfile = event.detail.aiProfile;
|
|
if (!aiProfile || !aiProfile.handle || !aiProfile.displayName) {
|
|
console.error('AI profile data is missing, cannot display response');
|
|
return;
|
|
}
|
|
|
|
const timestamp = new Date(event.detail.timestamp || Date.now());
|
|
|
|
// Create comment-style AI response
|
|
const answerDiv = document.createElement('div');
|
|
answerDiv.className = 'chat-message ai-message comment-style';
|
|
|
|
// Prepare avatar
|
|
let avatarElement = '🤖';
|
|
if (aiProfile.avatar) {
|
|
avatarElement = `<img src="${aiProfile.avatar}" alt="${aiProfile.displayName}" class="profile-avatar">`;
|
|
}
|
|
|
|
answerDiv.innerHTML = `
|
|
<div class="message-header">
|
|
<div class="avatar">${avatarElement}</div>
|
|
<div class="user-info">
|
|
<div class="display-name">${aiProfile.displayName}</div>
|
|
<div class="handle">@${aiProfile.handle}</div>
|
|
<div class="timestamp">${timestamp.toLocaleString()}</div>
|
|
</div>
|
|
</div>
|
|
<div class="message-content">${event.detail.answer}</div>
|
|
`;
|
|
chatHistory.appendChild(answerDiv);
|
|
|
|
// Auto-expand content instead of scrolling
|
|
if (chatHistory.children.length > 5) {
|
|
const oldestMessage = chatHistory.children[0];
|
|
if (oldestMessage && oldestMessage.classList.contains('user-message')) {
|
|
// Keep the latest 5 exchanges (10 messages)
|
|
if (chatHistory.children.length > 10) {
|
|
chatHistory.removeChild(oldestMessage);
|
|
if (chatHistory.children.length > 0) {
|
|
chatHistory.removeChild(chatHistory.children[0]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |