2 Commits

Author SHA1 Message Date
4d85dc8785 fix ask-AI 2025-06-14 19:52:09 +09:00
3b2c53fc97 Add GitHub Actions workflows and optimize build performance
- Add release.yml for multi-platform binary builds (Linux, macOS, Windows)
- Add gh-pages-fast.yml for fast deployment using pre-built binaries
- Add build-binary.yml for standalone binary artifact creation
- Optimize Cargo.toml with build profiles and reduced tokio features
- Remove 26MB of unused Font Awesome assets (kept only essential files)
- Font Awesome reduced from 28MB to 1.2MB

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-14 19:52:08 +09:00
14 changed files with 490 additions and 541 deletions

View File

@@ -45,7 +45,8 @@
"Bash(git add:*)", "Bash(git add:*)",
"Bash(git commit:*)", "Bash(git commit:*)",
"Bash(git push:*)", "Bash(git push:*)",
"Bash(git tag:*)" "Bash(git tag:*)",
"Bash(../bin/ailog:*)"
], ],
"deny": [] "deny": []
} }

View File

@@ -1,51 +0,0 @@
name: Build Binary
on:
workflow_dispatch: # Manual trigger
push:
branches: [ main ]
paths:
- 'src/**'
- 'Cargo.toml'
- 'Cargo.lock'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache target directory
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Build binary
run: cargo build --release
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: ailog-linux
path: target/release/ailog
retention-days: 30

View File

@@ -34,22 +34,67 @@ jobs:
- name: Copy OAuth build to static - name: Copy OAuth build to static
run: | run: |
mkdir -p my-blog/static/assets # Remove old assets (following run.zsh pattern)
cp -r oauth/dist/assets/* my-blog/static/assets/ rm -rf my-blog/static/assets
cp oauth/dist/index.html my-blog/static/oauth/index.html || true # Copy all dist files to static
cp -rf oauth/dist/* my-blog/static/
# Copy index.html to oauth-assets.html template
cp oauth/dist/index.html my-blog/templates/oauth-assets.html
- name: Setup Rust - name: Cache ailog binary
uses: actions-rs/toolchain@v1 uses: actions/cache@v4
with: with:
toolchain: stable path: ./bin
key: ailog-bin-${{ runner.os }}
restore-keys: |
ailog-bin-${{ runner.os }}
- name: Build ailog - name: Setup ailog binary
run: cargo build --release run: |
# Get expected version from Cargo.toml
EXPECTED_VERSION=$(grep '^version' Cargo.toml | cut -d'"' -f2)
echo "Expected version from Cargo.toml: $EXPECTED_VERSION"
# Check current binary version if exists
if [ -f "./bin/ailog" ]; then
CURRENT_VERSION=$(./bin/ailog --version 2>/dev/null || echo "unknown")
echo "Current binary version: $CURRENT_VERSION"
else
CURRENT_VERSION="none"
echo "No binary found"
fi
# Check OS
OS="${{ runner.os }}"
echo "Runner OS: $OS"
# Use pre-packaged binary if version matches or extract from tar.gz
if [ "$CURRENT_VERSION" = "$EXPECTED_VERSION" ]; then
echo "Binary is up to date"
chmod +x ./bin/ailog
elif [ "$OS" = "Linux" ] && [ -f "./bin/ailog-linux-x86_64.tar.gz" ]; then
echo "Extracting ailog from pre-packaged tar.gz..."
cd bin
tar -xzf ailog-linux-x86_64.tar.gz
chmod +x ailog
cd ..
# Verify extracted version
EXTRACTED_VERSION=$(./bin/ailog --version 2>/dev/null || echo "unknown")
echo "Extracted binary version: $EXTRACTED_VERSION"
if [ "$EXTRACTED_VERSION" != "$EXPECTED_VERSION" ]; then
echo "Warning: Binary version mismatch. Expected $EXPECTED_VERSION but got $EXTRACTED_VERSION"
fi
else
echo "Error: No suitable binary found for OS: $OS"
exit 1
fi
- name: Build site with ailog - name: Build site with ailog
run: | run: |
cd my-blog cd my-blog
../target/release/ailog build ../bin/ailog build
- name: List public directory - name: List public directory
run: | run: |

View File

@@ -0,0 +1,92 @@
name: github pages (fast)
on:
push:
branches:
- main
paths-ignore:
- 'src/**'
- 'Cargo.toml'
- 'Cargo.lock'
jobs:
build-deploy:
runs-on: ubuntu-latest
permissions:
contents: write
pages: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Cache ailog binary
uses: actions/cache@v4
with:
path: ./bin
key: ailog-bin-${{ runner.os }}
restore-keys: |
ailog-bin-${{ runner.os }}
- name: Setup ailog binary
run: |
# Get expected version from Cargo.toml
EXPECTED_VERSION=$(grep '^version' Cargo.toml | cut -d'"' -f2)
echo "Expected version from Cargo.toml: $EXPECTED_VERSION"
# Check current binary version if exists
if [ -f "./bin/ailog" ]; then
CURRENT_VERSION=$(./bin/ailog --version 2>/dev/null || echo "unknown")
echo "Current binary version: $CURRENT_VERSION"
else
CURRENT_VERSION="none"
echo "No binary found"
fi
# Check OS
OS="${{ runner.os }}"
echo "Runner OS: $OS"
# Use pre-packaged binary if version matches or extract from tar.gz
if [ "$CURRENT_VERSION" = "$EXPECTED_VERSION" ]; then
echo "Binary is up to date"
chmod +x ./bin/ailog
elif [ "$OS" = "Linux" ] && [ -f "./bin/ailog-linux-x86_64.tar.gz" ]; then
echo "Extracting ailog from pre-packaged tar.gz..."
cd bin
tar -xzf ailog-linux-x86_64.tar.gz
chmod +x ailog
cd ..
# Verify extracted version
EXTRACTED_VERSION=$(./bin/ailog --version 2>/dev/null || echo "unknown")
echo "Extracted binary version: $EXTRACTED_VERSION"
if [ "$EXTRACTED_VERSION" != "$EXPECTED_VERSION" ]; then
echo "Warning: Binary version mismatch. Expected $EXPECTED_VERSION but got $EXTRACTED_VERSION"
fi
else
echo "Error: No suitable binary found for OS: $OS"
exit 1
fi
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: "0.139.2"
extended: true
- name: Build with ailog
env:
TZ: "Asia/Tokyo"
run: |
# Use pre-built ailog binary instead of cargo build
cd my-blog
../bin/ailog build
touch ./public/.nojekyll
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./my-blog/public
publish_branch: gh-pages

View File

@@ -1,77 +0,0 @@
name: github pages (fast)
on:
push:
branches:
- main
paths-ignore:
- 'src/**'
- 'Cargo.toml'
- 'Cargo.lock'
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache ailog binary
uses: actions/cache@v4
with:
path: ./bin
key: ailog-bin-${{ runner.os }}
restore-keys: |
ailog-bin-${{ runner.os }}
- name: Check and update ailog binary
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get latest release version
LATEST_VERSION=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r .tag_name)
echo "Latest version: $LATEST_VERSION"
# Check current binary version if exists
mkdir -p ./bin
if [ -f "./bin/ailog" ]; then
CURRENT_VERSION=$(./bin/ailog --version | awk '{print $2}' || echo "unknown")
echo "Current version: $CURRENT_VERSION"
else
CURRENT_VERSION="none"
echo "No binary found"
fi
# Download if version is different or binary doesn't exist
if [ "$CURRENT_VERSION" != "${LATEST_VERSION#v}" ]; then
echo "Downloading ailog $LATEST_VERSION..."
curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" \
https://github.com/${{ github.repository }}/releases/download/$LATEST_VERSION/ailog-linux-x86_64.tar.gz | tar -xzf -
mv ailog ./bin/ailog
chmod +x ./bin/ailog
echo "Updated to version: $(./bin/ailog --version)"
else
echo "Binary is up to date"
chmod +x ./bin/ailog
fi
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: "0.139.2"
extended: true
- name: Build with ailog
env:
TZ: "Asia/Tokyo"
run: |
# Use pre-built ailog binary instead of cargo build
./bin/ailog build --output ./public
touch ./public/.nojekyll
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
publish_branch: gh-pages

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ dist
node_modules node_modules
package-lock.json package-lock.json
my-blog/static/assets/comment-atproto-* my-blog/static/assets/comment-atproto-*
bin/ailog

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ailog" name = "ailog"
version = "0.1.0" version = "0.1.4"
edition = "2021" edition = "2021"
authors = ["syui"] authors = ["syui"]
description = "A static blog generator with AI features" description = "A static blog generator with AI features"

View File

@@ -55,49 +55,26 @@ runs:
restore-keys: | restore-keys: |
ailog-bin-${{ runner.os }} ailog-bin-${{ runner.os }}
- name: Check and update ailog binary - name: Setup ailog binary
shell: bash shell: bash
run: | run: |
# Get latest release version (for Gitea, adjust API endpoint if needed) # Check if pre-built binary exists
if command -v curl >/dev/null 2>&1; then if [ -f "./bin/ailog-linux-x86_64" ]; then
LATEST_VERSION=$(curl -s https://api.github.com/repos/syui/ailog/releases/latest | jq -r .tag_name 2>/dev/null || echo "v0.1.1") echo "Using pre-built binary from repository"
chmod +x ./bin/ailog-linux-x86_64
CURRENT_VERSION=$(./bin/ailog-linux-x86_64 --version 2>/dev/null || echo "unknown")
echo "Binary version: $CURRENT_VERSION"
else else
LATEST_VERSION="v0.1.1" # fallback version echo "No pre-built binary found, trying to build from source..."
fi if command -v cargo >/dev/null 2>&1; then
echo "Target version: $LATEST_VERSION" cargo build --release
mkdir -p ./bin
# Check current binary version if exists cp ./target/release/ailog ./bin/ailog-linux-x86_64
mkdir -p ./bin echo "Built from source: $(./bin/ailog-linux-x86_64 --version 2>/dev/null)"
if [ -f "./bin/ailog" ]; then
CURRENT_VERSION=$(./bin/ailog --version | awk '{print $2}' 2>/dev/null || echo "unknown")
echo "Current version: $CURRENT_VERSION"
else
CURRENT_VERSION="none"
echo "No binary found"
fi
# Download if version is different or binary doesn't exist
if [ "$CURRENT_VERSION" != "${LATEST_VERSION#v}" ]; then
echo "Downloading ailog $LATEST_VERSION..."
# Try GitHub first, then fallback to local build
if curl -sL https://github.com/syui/ailog/releases/download/$LATEST_VERSION/ailog-linux-x86_64.tar.gz | tar -xzf - 2>/dev/null; then
mv ailog ./bin/ailog
chmod +x ./bin/ailog
echo "Downloaded binary: $(./bin/ailog --version)"
else else
echo "Download failed, building from source..." echo "Error: No binary found and cargo not available"
if command -v cargo >/dev/null 2>&1; then exit 1
cargo build --release
cp ./target/release/ailog ./bin/ailog
echo "Built from source: $(./bin/ailog --version)"
else
echo "Error: Neither download nor cargo build available"
exit 1
fi
fi fi
else
echo "Binary is up to date"
chmod +x ./bin/ailog
fi fi
- name: Setup Node.js for OAuth app - name: Setup Node.js for OAuth app
@@ -123,12 +100,15 @@ runs:
run: | run: |
start_time=$(date +%s) start_time=$(date +%s)
./bin/ailog build \ # Change to blog directory and run build
--content ${{ inputs.content-dir }} \ # Note: ailog build only takes a path argument, not options
--output ${{ inputs.output-dir }} \ if [ -d "my-blog" ]; then
--templates ${{ inputs.template-dir }} \ cd my-blog
--static ${{ inputs.static-dir }} \ ../bin/ailog-linux-x86_64 build
--config ${{ inputs.config-file }} else
# If no my-blog directory, use current directory
./bin/ailog-linux-x86_64 build .
fi
end_time=$(date +%s) end_time=$(date +%s)
build_time=$((end_time - start_time)) build_time=$((end_time - start_time))

Binary file not shown.

View File

@@ -1,8 +1,5 @@
# AI機能をai.gpt MCP serverにリダイレクト # Ask-AI機能をOllamaにプロキシ
/api/ask https://ai-gpt-mcp.syui.ai/ask 200 /api/ask https://ollama.syui.ai/api/generate 200
# Ollama API proxy (Cloudflare Workers)
/api/ollama-proxy https://ollama-proxy.YOUR-SUBDOMAIN.workers.dev/:splat 200
# OAuth routes # OAuth routes
/oauth/* /oauth/index.html 200 /oauth/* /oauth/index.html 200

View File

@@ -59,7 +59,7 @@ a.view-markdown:any-link {
.container { .container {
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;
grid-template-rows: auto auto 1fr auto; grid-template-rows: auto 0fr 1fr auto;
grid-template-areas: grid-template-areas:
"header" "header"
"ask-ai" "ask-ai"
@@ -158,6 +158,15 @@ a.view-markdown:any-link {
background: #f6f8fa; background: #f6f8fa;
border-bottom: 1px solid #d1d9e0; border-bottom: 1px solid #d1d9e0;
padding: 24px; padding: 24px;
overflow: hidden;
}
.ask-ai-panel[style*="block"] {
display: block !important;
}
.container:has(.ask-ai-panel[style*="block"]) {
grid-template-rows: auto auto 1fr auto;
} }
.ask-ai-content { .ask-ai-content {
@@ -193,13 +202,13 @@ a.view-markdown:any-link {
grid-area: main; grid-area: main;
max-width: 1000px; max-width: 1000px;
margin: 0 auto; margin: 0 auto;
padding: 24px; /* padding: 24px; */
width: 100%; width: 100%;
} }
@media (max-width: 1000px) { @media (max-width: 1000px) {
.main-content { .main-content {
padding: 20px; /* padding: 20px; */
max-width: 100%; max-width: 100%;
} }
} }
@@ -324,6 +333,10 @@ a.view-markdown:any-link {
margin: 0 auto; margin: 0 auto;
} }
article.article-content {
padding: 10px;
}
.article-meta { .article-meta {
display: flex; display: flex;
gap: 16px; gap: 16px;
@@ -517,25 +530,21 @@ a.view-markdown:any-link {
margin: 16px 0; margin: 16px 0;
font-size: 14px; font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
position: relative;
} }
/* File name display for code blocks */ /* File name display for code blocks - top bar style */
.article-body pre[data-filename]::before { .article-body pre[data-filename]::before {
content: attr(data-filename); content: attr(data-filename);
position: absolute; display: block;
top: 0;
right: 0;
background: #2D2D30; background: #2D2D30;
color: #CCCCCC; color: #AE81FF;
padding: 4px 12px; padding: 8px 16px;
font-size: 12px; font-size: 12px;
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace; font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
border-bottom-left-radius: 4px; border-bottom: 1px solid #3E3D32;
border: 1px solid #3E3D32; margin: 0;
border-top: none; width: 100%;
border-right: none; box-sizing: border-box;
z-index: 1;
} }
.article-body pre code { .article-body pre code {
@@ -548,6 +557,11 @@ a.view-markdown:any-link {
line-height: 1.4; line-height: 1.4;
} }
/* Adjust padding when filename is present */
.article-body pre[data-filename] code {
padding: 16px;
}
/* Inline code (not in pre blocks) */ /* Inline code (not in pre blocks) */
.article-body code { .article-body code {
background: var(--light-white); background: var(--light-white);
@@ -817,13 +831,13 @@ a.view-markdown:any-link {
justify-self: end; justify-self: end;
} }
/* Ask AI button mobile style */ /* Ask AI button mobile style - icon only */
.ask-ai-btn { .ask-ai-btn {
padding: 8px; padding: 8px;
min-width: 40px; min-width: 40px;
justify-content: center; justify-content: center;
font-size: 0;
gap: 0; gap: 0;
font-size: 0; /* Hide all text content */
} }
.ask-ai-btn .ai-icon { .ask-ai-btn .ai-icon {
@@ -853,6 +867,16 @@ a.view-markdown:any-link {
white-space: pre-wrap; white-space: pre-wrap;
} }
/* Mobile filename display */
.article-body pre[data-filename]::before {
padding: 6px 12px;
font-size: 11px;
}
.article-body pre[data-filename] code {
padding: 12px;
}
.article-body code { .article-body code {
word-break: break-all; word-break: break-all;
} }

View File

@@ -1,360 +1,281 @@
/** /**
* Ask AI functionality - Pure JavaScript, no jQuery dependency * Ask AI functionality - Based on original working implementation
*/ */
class AskAI {
constructor() { // Global variables for AI functionality
this.isReady = false; let aiProfileData = null;
this.aiProfile = null;
this.init(); // Original functions from working implementation
function toggleAskAI() {
const panel = document.getElementById('askAiPanel');
const isVisible = panel.style.display !== 'none';
panel.style.display = isVisible ? 'none' : 'block';
if (!isVisible) {
checkAuthenticationStatus();
} }
}
init() { function checkAuthenticationStatus() {
this.setupEventListeners(); const userSections = document.querySelectorAll('.user-section');
this.checkAuthOnLoad(); const isAuthenticated = userSections.length > 0;
}
setupEventListeners() { if (isAuthenticated) {
// Listen for AI ready signal // User is authenticated - show Ask AI UI
window.addEventListener('aiChatReady', () => { document.getElementById('authCheck').style.display = 'none';
this.isReady = true; document.getElementById('chatForm').style.display = 'block';
console.log('AI Chat is ready'); document.getElementById('chatHistory').style.display = 'block';
});
// Listen for AI profile updates // Show initial greeting if chat history is empty
window.addEventListener('aiProfileLoaded', (event) => {
this.aiProfile = event.detail;
console.log('AI profile loaded:', this.aiProfile);
this.updateButton();
});
// Listen for AI responses
window.addEventListener('aiResponseReceived', (event) => {
this.handleAIResponse(event.detail);
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.hide();
}
if (e.key === 'Enter' && e.target.id === 'aiQuestion' && !e.shiftKey) {
e.preventDefault();
this.ask();
}
});
// Monitor authentication changes
this.observeAuth();
}
toggle() {
const panel = document.getElementById('askAiPanel');
const isVisible = panel.style.display !== 'none';
if (isVisible) {
this.hide();
} else {
this.show();
}
}
show() {
const panel = document.getElementById('askAiPanel');
panel.style.display = 'block';
this.checkAuth();
}
hide() {
const panel = document.getElementById('askAiPanel');
panel.style.display = 'none';
}
checkAuth() {
const userSections = document.querySelectorAll('.user-section');
const isAuthenticated = userSections.length > 0;
const authCheck = document.getElementById('authCheck');
const chatForm = document.getElementById('chatForm');
const chatHistory = document.getElementById('chatHistory'); const chatHistory = document.getElementById('chatHistory');
if (chatHistory.children.length === 0) {
if (isAuthenticated) { showInitialGreeting();
authCheck.style.display = 'none';
chatForm.style.display = 'block';
chatHistory.style.display = 'block';
if (chatHistory.children.length === 0) {
this.showGreeting();
}
setTimeout(() => {
document.getElementById('aiQuestion').focus();
}, 50);
} else {
authCheck.style.display = 'block';
chatForm.style.display = 'none';
chatHistory.style.display = 'none';
} }
}
checkAuthOnLoad() { // Focus on input
setTimeout(() => { setTimeout(() => {
this.checkAuth(); document.getElementById('aiQuestion').focus();
}, 500); }, 50);
} else {
// User not authenticated - show auth message
document.getElementById('authCheck').style.display = 'block';
document.getElementById('chatForm').style.display = 'none';
document.getElementById('chatHistory').style.display = 'none';
} }
}
observeAuth() { function askQuestion() {
const observer = new MutationObserver(() => { const question = document.getElementById('aiQuestion').value;
const userSections = document.querySelectorAll('.user-section'); if (!question.trim()) return;
if (userSections.length > 0) {
this.checkAuth();
observer.disconnect();
}
});
observer.observe(document.body, { const askButton = document.getElementById('askButton');
childList: true, askButton.disabled = true;
subtree: true askButton.textContent = 'Posting...';
});
try {
// Add user message to chat
addUserMessage(question);
// Clear input
document.getElementById('aiQuestion').value = '';
// Show loading
showLoadingMessage();
// Post question via OAuth app
window.dispatchEvent(new CustomEvent('postAIQuestion', {
detail: { question: question }
}));
} catch (error) {
console.error('Failed to ask question:', error);
showErrorMessage('Sorry, I encountered an error. Please try again.');
} finally {
askButton.disabled = false;
askButton.textContent = 'Ask';
} }
}
updateButton() { function addUserMessage(question) {
const button = document.getElementById('askAiButton'); const chatHistory = document.getElementById('chatHistory');
if (this.aiProfile && this.aiProfile.displayName) { const userSection = document.querySelector('.user-section');
const textNode = button.childNodes[2];
if (textNode) { let userAvatar = '👤';
textNode.textContent = this.aiProfile.displayName; 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('@', '');
} }
} }
showGreeting() { const questionDiv = document.createElement('div');
if (!this.aiProfile) return; questionDiv.className = 'chat-message user-message comment-style';
questionDiv.innerHTML = `
const chatHistory = document.getElementById('chatHistory'); <div class="message-header">
const greetingDiv = document.createElement('div'); <div class="avatar">${userAvatar}</div>
greetingDiv.className = 'chat-message ai-message comment-style initial-greeting'; <div class="user-info">
<div class="display-name">${userDisplay}</div>
const avatarElement = this.aiProfile.avatar <div class="handle">@${userHandle}</div>
? `<img src="${this.aiProfile.avatar}" alt="${this.aiProfile.displayName}" class="profile-avatar">` <div class="timestamp">${new Date().toLocaleString()}</div>
: '🤖';
greetingDiv.innerHTML = `
<div class="message-header">
<div class="avatar">${avatarElement}</div>
<div class="user-info">
<div class="display-name">${this.aiProfile.displayName}</div>
<div class="handle">@${this.aiProfile.handle}</div>
<div class="timestamp">${new Date().toLocaleString()}</div>
</div>
</div> </div>
<div class="message-content"> </div>
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 class="message-content">${question}</div>
`;
chatHistory.appendChild(questionDiv);
}
function showLoadingMessage() {
const chatHistory = document.getElementById('chatHistory');
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);
}
function showErrorMessage(message) {
const chatHistory = document.getElementById('chatHistory');
removeLoadingMessage();
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>
chatHistory.appendChild(greetingDiv); <div class="message-content">${message}</div>
`;
chatHistory.appendChild(errorDiv);
}
function removeLoadingMessage() {
const loadingMsg = document.querySelector('.ai-loading-simple');
if (loadingMsg) {
loadingMsg.remove();
} }
}
async ask() { function showInitialGreeting() {
const question = document.getElementById('aiQuestion').value; if (!aiProfileData) return;
const chatHistory = document.getElementById('chatHistory');
const askButton = document.getElementById('askButton');
if (!question.trim()) return; const chatHistory = document.getElementById('chatHistory');
const greetingDiv = document.createElement('div');
greetingDiv.className = 'chat-message ai-message comment-style initial-greeting';
// Wait for AI to be ready const avatarElement = aiProfileData.avatar
if (!this.isReady) { ? `<img src="${aiProfileData.avatar}" alt="${aiProfileData.displayName}" class="profile-avatar">`
await this.waitForReady(); : '🤖';
}
// Disable button greetingDiv.innerHTML = `
askButton.disabled = true; <div class="message-header">
askButton.textContent = 'Posting...'; <div class="avatar">${avatarElement}</div>
<div class="user-info">
try { <div class="display-name">${aiProfileData.displayName}</div>
// Add user message <div class="handle">@${aiProfileData.handle}</div>
this.addUserMessage(question); <div class="timestamp">${new Date().toLocaleString()}</div>
// Clear input
document.getElementById('aiQuestion').value = '';
// Show loading
this.showLoading();
// Post question
const event = new CustomEvent('postAIQuestion', {
detail: { question: question }
});
window.dispatchEvent(event);
} catch (error) {
this.showError('Sorry, I encountered an error. Please try again.');
} finally {
askButton.disabled = false;
askButton.textContent = 'Ask';
}
}
waitForReady() {
return new Promise(resolve => {
const checkReady = setInterval(() => {
if (this.isReady) {
clearInterval(checkReady);
resolve();
}
}, 100);
});
}
addUserMessage(question) {
const chatHistory = document.getElementById('chatHistory');
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('@', '');
}
}
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>
<div class="message-content">${question}</div> </div>
`; <div class="message-content">
chatHistory.appendChild(questionDiv); 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);
}
showLoading() { function updateAskAIButton() {
const chatHistory = document.getElementById('chatHistory'); const button = document.getElementById('askAiButton');
const loadingDiv = document.createElement('div'); if (!button) return;
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);
}
showError(message) { // Only update text, never modify the icon
const chatHistory = document.getElementById('chatHistory'); if (aiProfileData && aiProfileData.displayName) {
this.removeLoading(); const textNode = button.childNodes[2] || button.lastChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const errorDiv = document.createElement('div'); textNode.textContent = aiProfileData.displayName;
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">${message}</div>
`;
chatHistory.appendChild(errorDiv);
}
removeLoading() {
const loadingMsg = document.querySelector('.ai-loading-simple');
if (loadingMsg) {
loadingMsg.remove();
}
}
handleAIResponse(responseData) {
const chatHistory = document.getElementById('chatHistory');
this.removeLoading();
const aiProfile = responseData.aiProfile;
if (!aiProfile || !aiProfile.handle || !aiProfile.displayName) {
console.error('AI profile data is missing');
return;
}
const timestamp = new Date(responseData.timestamp || Date.now());
const avatarElement = aiProfile.avatar
? `<img src="${aiProfile.avatar}" alt="${aiProfile.displayName}" class="profile-avatar">`
: '🤖';
const answerDiv = document.createElement('div');
answerDiv.className = 'chat-message ai-message comment-style';
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">${responseData.answer}</div>
`;
chatHistory.appendChild(answerDiv);
// Limit chat history
this.limitChatHistory();
}
limitChatHistory() {
const chatHistory = document.getElementById('chatHistory');
if (chatHistory.children.length > 10) {
chatHistory.removeChild(chatHistory.children[0]);
if (chatHistory.children.length > 0) {
chatHistory.removeChild(chatHistory.children[0]);
}
} }
} }
} }
// Initialize Ask AI when DOM is loaded function handleAIResponse(responseData) {
document.addEventListener('DOMContentLoaded', () => { const chatHistory = document.getElementById('chatHistory');
try { removeLoadingMessage();
window.askAIInstance = new AskAI();
console.log('Ask AI initialized successfully'); const aiProfile = responseData.aiProfile;
} catch (error) { if (!aiProfile || !aiProfile.handle || !aiProfile.displayName) {
console.error('Failed to initialize Ask AI:', error); console.error('AI profile data is missing');
return;
} }
const timestamp = new Date(responseData.timestamp || Date.now());
const avatarElement = aiProfile.avatar
? `<img src="${aiProfile.avatar}" alt="${aiProfile.displayName}" class="profile-avatar">`
: '🤖';
const answerDiv = document.createElement('div');
answerDiv.className = 'chat-message ai-message comment-style';
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">${responseData.answer}</div>
`;
chatHistory.appendChild(answerDiv);
// Limit chat history
limitChatHistory();
}
function limitChatHistory() {
const chatHistory = document.getElementById('chatHistory');
if (chatHistory.children.length > 10) {
chatHistory.removeChild(chatHistory.children[0]);
if (chatHistory.children.length > 0) {
chatHistory.removeChild(chatHistory.children[0]);
}
}
}
// Event listeners setup
function setupAskAIEventListeners() {
// Listen for AI profile updates from OAuth app
window.addEventListener('aiProfileLoaded', function(event) {
aiProfileData = event.detail;
console.log('AI profile loaded:', aiProfileData);
updateAskAIButton();
});
// Listen for AI responses
window.addEventListener('aiResponseReceived', function(event) {
handleAIResponse(event.detail);
});
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const panel = document.getElementById('askAiPanel');
if (panel) {
panel.style.display = 'none';
}
}
// Enter key to send message
if (e.key === 'Enter' && e.target.id === 'aiQuestion' && !e.shiftKey) {
e.preventDefault();
askQuestion();
}
});
}
// Initialize Ask AI when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
setupAskAIEventListeners();
console.log('Ask AI initialized successfully');
}); });
// Global function for onclick // Global functions for onclick handlers
window.AskAI = { window.toggleAskAI = toggleAskAI;
toggle: function() { window.askQuestion = askQuestion;
console.log('AskAI.toggle called');
if (window.askAIInstance) {
window.askAIInstance.toggle();
} else {
console.error('Ask AI instance not available');
}
},
ask: function() {
console.log('AskAI.ask called');
if (window.askAIInstance) {
window.askAIInstance.ask();
} else {
console.error('Ask AI instance not available');
}
}
};

View File

@@ -15,7 +15,6 @@
<link rel="stylesheet" href="/pkg/icomoon/style.css"> <link rel="stylesheet" href="/pkg/icomoon/style.css">
<link rel="stylesheet" href="/pkg/font-awesome/css/all.min.css"> <link rel="stylesheet" href="/pkg/font-awesome/css/all.min.css">
{% include "oauth-assets.html" %}
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
@@ -50,7 +49,7 @@
</a> </a>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button class="ask-ai-btn" onclick="AskAI.toggle()" id="askAiButton"> <button class="ask-ai-btn" onclick="toggleAskAI()" id="askAiButton">
<span class="ai-icon icon-ai"></span> <span class="ai-icon icon-ai"></span>
ai ai
</button> </button>
@@ -92,5 +91,7 @@
<script src="/js/ask-ai.js"></script> <script src="/js/ask-ai.js"></script>
<script src="/js/theme.js"></script> <script src="/js/theme.js"></script>
{% include "oauth-assets.html" %}
</body> </body>
</html> </html>

View File

@@ -18,10 +18,14 @@ mod mcp;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "ailog")] #[command(name = "ailog")]
#[command(about = "A static blog generator with AI features")] #[command(about = "A static blog generator with AI features")]
#[command(version)] #[command(disable_version_flag = true)]
struct Cli { struct Cli {
/// Print version information
#[arg(short = 'V', long = "version")]
version: bool,
#[command(subcommand)] #[command(subcommand)]
command: Commands, command: Option<Commands>,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -136,7 +140,18 @@ enum OauthCommands {
async fn main() -> Result<()> { async fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { // Handle version flag
if cli.version {
println!("{}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
// Require subcommand if no version flag
let command = cli.command.ok_or_else(|| {
anyhow::anyhow!("No subcommand provided. Use --help for usage information.")
})?;
match command {
Commands::Init { path } => { Commands::Init { path } => {
commands::init::execute(path).await?; commands::init::execute(path).await?;
} }